347 lines
10 KiB
Go
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)
|
|
}
|
|
}
|