init commit

This commit is contained in:
2026-01-25 17:13:15 -06:00
commit 1bbfc332d8
27 changed files with 2462 additions and 0 deletions

View 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
}

View 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))
}