Files
bjarneo--cliamp/ui/visualizer_driver_test.go
wehub-resource-sync ead81af521
CI / go (push) Waiting to run
CI / build (darwin, macos-14) (push) Waiting to run
CI / build (windows, windows-2025) (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

347 lines
10 KiB
Go

package ui
import (
"math"
"reflect"
"testing"
"time"
)
func TestAnalyzeSupportsArbitraryBandCounts(t *testing.T) {
v := NewVisualizer(44100)
samples := make([]float64, classicPeakFFTSize)
for i := range samples {
samples[i] = math.Sin(2 * math.Pi * 440 * float64(i) / v.sr)
}
for _, spec := range []VisAnalysisSpec{
spectrumAnalysisSpec(DefaultSpectrumBands),
{BandCount: 17, FFTSize: defaultFFTSize},
{BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize},
} {
bands := v.Analyze(samples, spec)
if len(bands) != spec.BandCount {
t.Fatalf("Analyze(..., %+v) len = %d, want %d", spec, len(bands), spec.BandCount)
}
}
}
func TestBuildSpectrumEdgesPreservesLegacyDefaultLayout(t *testing.T) {
got := buildSpectrumEdges(DefaultSpectrumBands)
want := legacySpectrumEdges[:]
if !reflect.DeepEqual(got, want) {
t.Fatalf("buildSpectrumEdges(%d) = %v, want %v", DefaultSpectrumBands, got, want)
}
}
func TestAnalyzeDecayStateIsIndependentPerAnalysisSpec(t *testing.T) {
v := NewVisualizer(44100)
specA := spectrumAnalysisSpec(DefaultSpectrumBands)
specB := VisAnalysisSpec{BandCount: DefaultSpectrumBands, FFTSize: classicPeakFFTSize}
v.prevBySpec[specA] = uniformBandsN(specA.BandCount, 0.5)
v.prevBySpec[specB] = uniformBandsN(specB.BandCount, 0.8)
bandsA := v.Analyze(nil, specA)
bandsB := v.Analyze(nil, specB)
if got := bandsA[0]; math.Abs(got-0.4) > classicPeakTestEpsilon {
t.Fatalf("default-spec decay = %v, want 0.4", got)
}
if got := bandsB[0]; math.Abs(got-0.64) > classicPeakTestEpsilon {
t.Fatalf("classic-peak-spec decay = %v, want 0.64", got)
}
}
func TestAverageSpectrumRangeLinearDistinguishesSubBinLowBands(t *testing.T) {
magnitudes := make([]float64, classicPeakFFTSize/2)
for i := range magnitudes {
magnitudes[i] = float64(i)
}
spec := VisAnalysisSpec{BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize}
edges := buildSpectrumEdges(spec.BandCount)
binHz := 44100.0 / float64(spec.FFTSize)
low := make([]float64, 3)
for i := range low {
low[i] = averageSpectrumRangeLinear(magnitudes, edges[i]/binHz, edges[i+1]/binHz)
}
if !(low[0] < low[1] && low[1] < low[2]) {
t.Fatalf("low sub-bin bands collapsed unexpectedly: got %v", low)
}
}
func TestRenderOnlyDriverUsesDefaultTickInterval(t *testing.T) {
v := NewVisualizer(44100)
// Pulse is a per-frame-animated spectrum mode and stays on the default
// (TickFast) cadence — VisBars/Bricks/Columns/etc. opt into TickAnim.
activateMode(t, v, VisPulse)
if got := v.TickInterval(VisTickContext{Playing: true}); got != TickFast {
t.Fatalf("TickInterval(playing) = %v, want %v", got, TickFast)
}
if got := v.TickInterval(VisTickContext{OverlayActive: true}); got != TickSlow {
t.Fatalf("TickInterval(overlay) = %v, want %v", got, TickSlow)
}
if got := v.TickInterval(VisTickContext{}); got != TickSlow {
t.Fatalf("TickInterval(idle) = %v, want %v", got, TickSlow)
}
}
func TestSmoothBarsDriverUsesAnimTick(t *testing.T) {
v := NewVisualizer(44100)
activateMode(t, v, VisBars)
if got := v.TickInterval(VisTickContext{Playing: true}); got != TickAnim {
t.Fatalf("Bars TickInterval(playing) = %v, want %v", got, TickAnim)
}
}
func TestAdvanceSmoothingEasesTowardBands(t *testing.T) {
v := NewVisualizer(44100)
v.bands = []float64{1.0, 0.0}
v.smoothedBands = []float64{0.0, 1.0}
t0 := time.Unix(0, 0)
v.lastSmoothTick = t0
v.advanceSmoothing(t0.Add(TickAnim))
// Expectations derive from the same easing call the implementation uses,
// so changes to the rate constants don't silently invalidate the test.
dt := TickAnim.Seconds()
wantRise := classicPeakStep(0, 1, dt)
wantFall := classicPeakStep(1, 0, dt)
if got := v.smoothedBands[0]; math.Abs(got-wantRise) > 1e-9 {
t.Fatalf("rise step = %v, want %v", got, wantRise)
}
if got := v.smoothedBands[1]; math.Abs(got-wantFall) > 1e-9 {
t.Fatalf("fall step = %v, want %v", got, wantFall)
}
if (1 - wantFall) >= wantRise {
t.Fatalf("expected decay slower than attack: rise %v, fall %v", wantRise, wantFall)
}
}
func TestAdvanceSmoothingResizesOnBandCountChange(t *testing.T) {
v := NewVisualizer(44100)
v.smoothedBands = []float64{0.5, 0.5}
v.bands = []float64{0.1, 0.2, 0.3}
v.advanceSmoothing(time.Time{})
if len(v.smoothedBands) != len(v.bands) {
t.Fatalf("smoothedBands len = %d, want %d", len(v.smoothedBands), len(v.bands))
}
for i, got := range v.smoothedBands {
if got != v.bands[i] {
t.Fatalf("smoothedBands[%d] = %v, want %v after resync", i, got, v.bands[i])
}
}
}
func TestDefaultDriverTickGatesAnalyzeAtAnalyzeCadence(t *testing.T) {
v := NewVisualizer(44100)
activateMode(t, v, VisBars)
calls := 0
analyze := func(spec VisAnalysisSpec) []float64 {
calls++
return uniformBandsN(spec.BandCount, 0.4)
}
t0 := time.Unix(0, 0)
// First tick analyzes (no prior timestamp).
v.Tick(VisTickContext{Now: t0, Playing: true, Analyze: analyze})
// A tick well within TickAnalyze should NOT analyze again.
v.Tick(VisTickContext{Now: t0.Add(TickAnim), Playing: true, Analyze: analyze})
if calls != 1 {
t.Fatalf("Analyze() calls within analyze window = %d, want 1", calls)
}
// Past TickAnalyze, it should analyze again.
v.Tick(VisTickContext{Now: t0.Add(TickAnalyze + time.Millisecond), Playing: true, Analyze: analyze})
if calls != 2 {
t.Fatalf("Analyze() calls after analyze window = %d, want 2", calls)
}
}
func TestRenderOnlyDriverSkipsAnalyzeUnderOverlay(t *testing.T) {
v := NewVisualizer(44100)
activateMode(t, v, VisBars)
calls := 0
v.Tick(VisTickContext{
OverlayActive: true,
Analyze: func(VisAnalysisSpec) []float64 {
calls++
return uniformBands(0.6)
},
})
if calls != 0 {
t.Fatalf("Analyze() calls = %d, want 0 while overlay is active", calls)
}
}
func TestRenderOnlyDriverRequestsConfiguredBandCount(t *testing.T) {
v := NewVisualizer(44100)
activateMode(t, v, VisBars)
var requested VisAnalysisSpec
v.Tick(VisTickContext{
Analyze: func(spec VisAnalysisSpec) []float64 {
requested = spec
return uniformBandsN(spec.BandCount, 0.6)
},
})
want := spectrumAnalysisSpec(DefaultSpectrumBands)
if requested != want {
t.Fatalf("Analyze() requested %+v, want %+v", requested, want)
}
if len(v.bands) != DefaultSpectrumBands {
t.Fatalf("stored bands len = %d, want %d", len(v.bands), DefaultSpectrumBands)
}
}
func TestClassicPeakRequestsHighResBands(t *testing.T) {
v := NewVisualizer(44100)
activateMode(t, v, VisClassicPeak)
var requested VisAnalysisSpec
v.Tick(VisTickContext{
Now: time.Now(),
Playing: true,
Analyze: func(spec VisAnalysisSpec) []float64 {
requested = spec
return uniformBandsN(spec.BandCount, 0.6)
},
})
want := VisAnalysisSpec{BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize}
if requested != want {
t.Fatalf("Analyze() requested %+v, want %+v", requested, want)
}
if len(v.bands) != classicPeakSpectrumBands {
t.Fatalf("stored bands len = %d, want %d", len(v.bands), classicPeakSpectrumBands)
}
}
func TestRawSampleModesRefreshWaveBufAtZeroBandCount(t *testing.T) {
v := NewVisualizer(44100)
activateMode(t, v, VisWave)
samples := []float64{-0.5, -0.1, 0.25, 0.75}
requested := VisAnalysisSpec{BandCount: -1, FFTSize: -1}
v.Tick(VisTickContext{
Playing: true,
Analyze: func(spec VisAnalysisSpec) []float64 {
requested = spec
return v.Analyze(samples, spec)
},
})
want := spectrumAnalysisSpec(0)
if requested != want {
t.Fatalf("Analyze() requested %+v, want %+v for raw-sample modes", requested, want)
}
if !reflect.DeepEqual(v.waveBuf, samples) {
t.Fatalf("waveBuf = %v, want %v after zero-band tick refresh", v.waveBuf, samples)
}
}
func TestRawSampleModesClearSpectrumHistoryOnModeSwitch(t *testing.T) {
v := NewVisualizer(44100)
barsSpec := spectrumAnalysisSpec(DefaultSpectrumBands)
v.prevBySpec[barsSpec] = uniformBandsN(barsSpec.BandCount, 0.8)
activateMode(t, v, VisBars)
activateMode(t, v, VisWave)
if len(v.prevBySpec) != 0 {
t.Fatalf("prevBySpec len after switch to raw-sample mode = %d, want 0", len(v.prevBySpec))
}
activateMode(t, v, VisBars)
bands := v.Analyze(nil, barsSpec)
if got := bands[0]; math.Abs(got) > classicPeakTestEpsilon {
t.Fatalf("first bar after raw-sample switch = %v, want 0 with cleared spectrum history", got)
}
}
func TestTerrainPreservesStateAcrossModeSwitch(t *testing.T) {
withPanelWidth(t, 8)
v := NewVisualizer(44100)
activateMode(t, v, VisTerrain)
driver := terrainDriverFor(t, v)
bands := uniformBands(0.6)
v.bands = bands
v.Tick(VisTickContext{})
snapshot := append([]float64(nil), driver.buf...)
if len(snapshot) != PanelWidth*2 {
t.Fatalf("terrain buffer len = %d, want %d", len(snapshot), PanelWidth*2)
}
activateMode(t, v, VisBars)
activateMode(t, v, VisTerrain)
if len(driver.buf) != len(snapshot) {
t.Fatalf("terrain buffer len after switch = %d, want %d", len(driver.buf), len(snapshot))
}
for i, got := range driver.buf {
if got != snapshot[i] {
t.Fatalf("terrain buffer[%d] = %v after switch, want %v", i, got, snapshot[i])
}
}
}
func TestTerrainRenderDoesNotAdvanceWithoutTick(t *testing.T) {
withPanelWidth(t, 8)
v := NewVisualizer(44100)
activateMode(t, v, VisTerrain)
driver := terrainDriverFor(t, v)
v.bands = uniformBands(0.6)
v.Tick(VisTickContext{})
snapshot := append([]float64(nil), driver.buf...)
v.Render()
v.Render()
if !reflect.DeepEqual(driver.buf, snapshot) {
t.Fatalf("terrain buffer changed across redraws without tick: got %v want %v", driver.buf, snapshot)
}
}
func TestTerrainTickSkipsAnalyzeUnderOverlay(t *testing.T) {
withPanelWidth(t, 8)
v := NewVisualizer(44100)
activateMode(t, v, VisTerrain)
driver := terrainDriverFor(t, v)
driver.buf = append([]float64(nil), []float64{
0.1, 0.2, 0.3, 0.4,
0.5, 0.6, 0.7, 0.8,
0.2, 0.3, 0.4, 0.5,
0.6, 0.7, 0.8, 0.9,
}...)
snapshot := append([]float64(nil), driver.buf...)
calls := 0
v.Tick(VisTickContext{
OverlayActive: true,
Analyze: func(VisAnalysisSpec) []float64 {
calls++
return uniformBands(0.6)
},
})
if calls != 0 {
t.Fatalf("Analyze() calls = %d, want 0 while overlay is active", calls)
}
if !reflect.DeepEqual(driver.buf, snapshot) {
t.Fatalf("terrain buffer changed under overlay: got %v want %v", driver.buf, snapshot)
}
}