init commit
This commit is contained in:
77
internal/ui/waveform/render.go
Normal file
77
internal/ui/waveform/render.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package waveform
|
||||
|
||||
// Block characters for waveform rendering (bottom to top)
|
||||
var blocks = []rune{' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
// RenderWaveform converts normalized samples (0-1) to block characters
|
||||
func RenderWaveform(samples []float64, width int) string {
|
||||
if len(samples) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := make([]rune, width)
|
||||
|
||||
for i := 0; i < width; i++ {
|
||||
// Map position to sample index
|
||||
sampleIdx := i * len(samples) / width
|
||||
if sampleIdx >= len(samples) {
|
||||
sampleIdx = len(samples) - 1
|
||||
}
|
||||
|
||||
// Get sample value and map to block character
|
||||
value := samples[sampleIdx]
|
||||
if value < 0 {
|
||||
value = 0
|
||||
}
|
||||
if value > 1 {
|
||||
value = 1
|
||||
}
|
||||
|
||||
blockIdx := int(value * float64(len(blocks)-1))
|
||||
result[i] = blocks[blockIdx]
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// RenderWaveformWithNeedle renders the waveform with a position indicator
|
||||
func RenderWaveformWithNeedle(samples []float64, width int, position float64) (string, int) {
|
||||
waveform := RenderWaveform(samples, width)
|
||||
|
||||
// Calculate needle position
|
||||
needlePos := int(position * float64(width))
|
||||
if needlePos < 0 {
|
||||
needlePos = 0
|
||||
}
|
||||
if needlePos >= width {
|
||||
needlePos = width - 1
|
||||
}
|
||||
|
||||
return waveform, needlePos
|
||||
}
|
||||
|
||||
// RenderWithColors returns the waveform with the needle position marked
|
||||
// Returns: left part, needle char, right part
|
||||
func RenderWithColors(samples []float64, width int, position float64) (string, string, string) {
|
||||
waveform := []rune(RenderWaveform(samples, width))
|
||||
if len(waveform) == 0 {
|
||||
return "", "|", ""
|
||||
}
|
||||
|
||||
needlePos := int(position * float64(len(waveform)))
|
||||
if needlePos < 0 {
|
||||
needlePos = 0
|
||||
}
|
||||
if needlePos >= len(waveform) {
|
||||
needlePos = len(waveform) - 1
|
||||
}
|
||||
|
||||
left := string(waveform[:needlePos])
|
||||
needle := "|"
|
||||
right := ""
|
||||
if needlePos+1 < len(waveform) {
|
||||
right = string(waveform[needlePos+1:])
|
||||
}
|
||||
|
||||
return left, needle, right
|
||||
}
|
||||
133
internal/ui/waveform/waveform.go
Normal file
133
internal/ui/waveform/waveform.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package waveform
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"playback/internal/audio"
|
||||
"playback/internal/ui"
|
||||
)
|
||||
|
||||
// Model represents the waveform visualization component
|
||||
type Model struct {
|
||||
Width int
|
||||
Height int
|
||||
Focused bool
|
||||
Samples []float64
|
||||
Position float64 // 0.0 to 1.0
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// New creates a new waveform model
|
||||
func New() Model {
|
||||
return Model{
|
||||
Height: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// SetSamples sets the waveform samples
|
||||
func (m *Model) SetSamples(data *audio.WaveformData) {
|
||||
if data != nil {
|
||||
m.Samples = data.Normalized()
|
||||
}
|
||||
}
|
||||
|
||||
// SetPosition sets the playback position (0.0 to 1.0)
|
||||
func (m *Model) SetPosition(pos float64) {
|
||||
m.Position = pos
|
||||
}
|
||||
|
||||
// SetDuration sets the total duration
|
||||
func (m *Model) SetDuration(d time.Duration) {
|
||||
m.Duration = d
|
||||
}
|
||||
|
||||
// SetSize sets the component dimensions
|
||||
func (m *Model) SetSize(width, height int) {
|
||||
m.Width = width
|
||||
m.Height = height
|
||||
}
|
||||
|
||||
// SetFocused sets the focus state
|
||||
func (m *Model) SetFocused(focused bool) {
|
||||
m.Focused = focused
|
||||
}
|
||||
|
||||
// View renders the waveform
|
||||
func (m Model) View() string {
|
||||
contentWidth := m.Width - 4 // Account for border and padding
|
||||
|
||||
if contentWidth < 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Render waveform with needle position
|
||||
left, needle, right := RenderWithColors(m.Samples, contentWidth, m.Position)
|
||||
|
||||
// Apply styles
|
||||
waveformLine := lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
ui.BaseStyle.Render(left),
|
||||
ui.NeedleStyle.Render(needle),
|
||||
ui.BaseStyle.Render(right),
|
||||
)
|
||||
|
||||
// Time markers
|
||||
startTime := "00:00"
|
||||
endTime := formatDuration(m.Duration)
|
||||
currentTime := formatDuration(time.Duration(m.Position * float64(m.Duration)))
|
||||
|
||||
timeMarkerWidth := contentWidth - len(startTime) - len(endTime)
|
||||
if timeMarkerWidth < 0 {
|
||||
timeMarkerWidth = 0
|
||||
}
|
||||
|
||||
// Calculate current time position
|
||||
currentTimePos := int(m.Position * float64(contentWidth))
|
||||
currentTimeWidth := len(currentTime)
|
||||
|
||||
// Build time marker line
|
||||
timeLine := ui.TimestampStyle.Render(startTime)
|
||||
|
||||
// Position current time
|
||||
spaceBefore := currentTimePos - len(startTime) - currentTimeWidth/2
|
||||
if spaceBefore < 0 {
|
||||
spaceBefore = 0
|
||||
}
|
||||
spaceAfter := contentWidth - len(startTime) - spaceBefore - currentTimeWidth - len(endTime)
|
||||
if spaceAfter < 0 {
|
||||
spaceAfter = 0
|
||||
}
|
||||
|
||||
timeLine = lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
ui.TimestampStyle.Render(startTime),
|
||||
lipgloss.NewStyle().Width(spaceBefore).Render(""),
|
||||
ui.NeedleStyle.Render(currentTime),
|
||||
lipgloss.NewStyle().Width(spaceAfter).Render(""),
|
||||
ui.TimestampStyle.Render(endTime),
|
||||
)
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
waveformLine,
|
||||
timeLine,
|
||||
)
|
||||
|
||||
// Apply border style based on focus
|
||||
style := ui.WaveformStyle
|
||||
if m.Focused {
|
||||
style = ui.WaveformFocusedStyle
|
||||
}
|
||||
|
||||
return style.Width(m.Width - 2).Render(content)
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
d = d.Round(time.Second)
|
||||
m := int(d / time.Minute)
|
||||
s := int((d % time.Minute) / time.Second)
|
||||
return string(rune('0'+m/10)) + string(rune('0'+m%10)) + ":" +
|
||||
string(rune('0'+s/10)) + string(rune('0'+s%10))
|
||||
}
|
||||
Reference in New Issue
Block a user