package ui import ( "math" "strings" "time" ) const ( classicPeakSpectrumBands = 64 classicPeakFFTSize = 4096 // Default animation timestep when elapsed time is missing or non-positive (~60 Hz). tickClassicPeak = time.Second / 60 // Minimum frame rate used when deriving redraw interval from terminal size. classicPeakMinFPS = 24.0 // Maximum frame rate used when deriving redraw interval from terminal size. classicPeakMaxFPS = 60.0 // Divides the FFT window duration to set spectrum analysis hop size (overlap factor). classicPeakFFTOverlap = 2.0 // Minimum spacing between spectrum analyses, regardless of sample rate. classicPeakSampleFloor = 20 * time.Millisecond // Minimum upward launch velocity for a newly detached peak cap. classicPeakLaunchBase = 0.8 // Extra launch velocity added in proportion to the bar's rise amount. classicPeakLaunchGain = 1.4 // Maximum upward launch velocity for the peak cap. classicPeakLaunchMax = 1.7 // Downward acceleration applied to the peak cap after launch. classicPeakGravity = 9.5 // Time the peak cap pauses at the apex before falling. classicPeakApexHold = 0.08 // Rendered width of each spectrum bar in terminal cells. classicPeakBarWidth = 1 // Number of spaces inserted between adjacent bars. classicPeakBarGap = 1 // Smoothing rate used when bar bodies move upward. classicPeakBarRiseRate = 34.0 // Smoothing rate used when bar bodies move downward. classicPeakBarFallRate = 10.0 // Highest normalized height a peak cap may reach. classicPeakMaxHeight = 1.0 // Small tolerance for treating peak and bar positions as visually equal. classicPeakVisibleEpsilon = 0.01 ) var classicPeakGlyphs = [4]rune{ '⎺', '⎻', '⎼', '⎽', } type classicPeakDriver struct { barPos []float64 peakPos []float64 peakVel []float64 peakHold []float64 lastTick time.Time bandsAt time.Time } func newClassicPeakDriver() visModeDriver { return &classicPeakDriver{} } func (*classicPeakDriver) AnalysisSpec(*Visualizer) VisAnalysisSpec { return VisAnalysisSpec{ BandCount: classicPeakSpectrumBands, FFTSize: classicPeakFFTSize, } } func (d *classicPeakDriver) Render(v *Visualizer) string { height := v.Rows cols, peaks := d.renderState(v) rowPad := max(0, PanelWidth-classicPeakRenderWidth(len(cols))) lines := make([]string, height) for row := range height { var content strings.Builder if rowPad > 0 { content.WriteString(strings.Repeat(" ", rowPad)) } rowBottom := float64(height-1-row) / float64(height) rowTop := float64(height-row) / float64(height) for col, level := range cols { capVisible := classicPeakDetached(level, peaks[col], height) capRow, capGlyph := classicPeakGlyph(peaks[col], height) cell := fracBlock(level, rowBottom, rowTop) if capVisible && row == capRow { cell = string(capGlyph) } content.WriteString(strings.Repeat(cell, classicPeakBarWidth)) if col < len(cols)-1 { content.WriteString(strings.Repeat(" ", classicPeakBarGap)) } } lines[row] = specWrap(rowBottom, content.String()) } return strings.Join(lines, "\n") } func (d *classicPeakDriver) Tick(v *Visualizer, ctx VisTickContext) { if ctx.OverlayActive { d.bandsAt = time.Time{} d.lastTick = time.Time{} return } if ctx.Playing { if d.bandsAt.IsZero() || ctx.Now.Sub(d.bandsAt) >= d.analysisInterval(v) { if ctx.Analyze != nil { v.bands = ctx.Analyze(d.AnalysisSpec(v)) } d.bandsAt = ctx.Now } } else { d.bandsAt = time.Time{} v.bands = v.Analyze(nil, d.AnalysisSpec(v)) } d.sync(v) if d.animating(v) { d.advance(v, ctx.Now) } } func (d *classicPeakDriver) TickInterval(v *Visualizer, ctx VisTickContext) time.Duration { if ctx.OverlayActive { return TickSlow } if ctx.Playing || d.animating(v) { return d.frameInterval(v) } return TickSlow } func (d *classicPeakDriver) OnEnter(*Visualizer) { *d = classicPeakDriver{} } func (d *classicPeakDriver) OnLeave(*Visualizer) {} func (d *classicPeakDriver) animating(v *Visualizer) bool { levels := d.levels(v) if len(levels) != len(d.barPos) || len(levels) != len(d.peakPos) { return false } for i, vel := range d.peakVel { if math.Abs(d.barPos[i]-levels[i]) > classicPeakVisibleEpsilon || vel != 0 || d.peakPos[i] > d.barPos[i]+classicPeakVisibleEpsilon { return true } } return false } func (d *classicPeakDriver) levels(v *Visualizer) []float64 { activeCols := classicPeakColsForWidth(PanelWidth) return resampleBandsLinear(v.bands, activeCols) } func (d *classicPeakDriver) frameInterval(v *Visualizer) time.Duration { rows := DefaultVisRows if v != nil && v.Rows > rows { rows = v.Rows } fps := classicPeakLaunchMax * float64(rows*len(classicPeakGlyphs)) fps = min(classicPeakMaxFPS, max(classicPeakMinFPS, fps)) return time.Duration(float64(time.Second) / fps) } func (d *classicPeakDriver) analysisInterval(v *Visualizer) time.Duration { interval := d.frameInterval(v) if v == nil || v.sr <= 0 { return interval } spec := d.AnalysisSpec(v) window := time.Duration(float64(time.Second) * float64(spec.FFTSize) / v.sr) if window <= 0 { return interval } sampleInterval := max(classicPeakSampleFloor, time.Duration(float64(window)/classicPeakFFTOverlap)) return max(interval, sampleInterval) } func classicPeakGlyph(level float64, height int) (row int, glyph rune) { dotRows := max(1, height*4) dotY := int(math.Round((1 - min(1.0, level)) * float64(dotRows-1))) row = dotY / 4 glyph = classicPeakGlyphs[dotY%4] return row, glyph } func classicPeakDetached(level, peak float64, height int) bool { minGap := max(classicPeakVisibleEpsilon, 0.5/float64(max(1, height*4))) return peak > level+minGap } func classicPeakColsForWidth(width int) int { return max(1, (width+classicPeakBarGap)/(classicPeakBarWidth+classicPeakBarGap)) } func classicPeakRenderWidth(cols int) int { if cols <= 0 { return 0 } return (classicPeakBarWidth+classicPeakBarGap)*cols - classicPeakBarGap } func classicPeakStep(current, target, dt float64) float64 { rate := classicPeakBarFallRate if target > current { rate = classicPeakBarRiseRate } return current + (target-current)*(1-math.Exp(-rate*dt)) } func (d *classicPeakDriver) landed(i int) bool { return d.peakVel[i] == 0 && d.peakPos[i] <= d.barPos[i]+classicPeakVisibleEpsilon } func (d *classicPeakDriver) reset(levels []float64, now time.Time) { d.barPos = make([]float64, len(levels)) copy(d.barPos, levels) d.peakPos = make([]float64, len(levels)) copy(d.peakPos, levels) if cap(d.peakVel) >= len(levels) { d.peakVel = d.peakVel[:len(levels)] clear(d.peakVel) } else { d.peakVel = make([]float64, len(levels)) } if cap(d.peakHold) >= len(levels) { d.peakHold = d.peakHold[:len(levels)] clear(d.peakHold) } else { d.peakHold = make([]float64, len(levels)) } d.lastTick = now } func (d *classicPeakDriver) sync(v *Visualizer) { levels := d.levels(v) if len(levels) != len(d.barPos) || len(levels) != len(d.peakPos) { d.reset(levels, time.Time{}) return } for i, level := range levels { if d.landed(i) && level > d.peakPos[i] { delta := level - d.peakPos[i] d.peakPos[i] = level d.peakVel[i] = min(classicPeakLaunchMax, classicPeakLaunchBase+classicPeakLaunchGain*delta) d.peakHold[i] = 0 } } } func (d *classicPeakDriver) advance(v *Visualizer, now time.Time) { levels := d.levels(v) if len(levels) != len(d.barPos) || len(levels) != len(d.peakPos) { d.reset(levels, now) return } dtSeconds := tickClassicPeak.Seconds() if !now.IsZero() && !d.lastTick.IsZero() { dtSeconds = now.Sub(d.lastTick).Seconds() } // Clamp dt so long gaps (pause, sleep, stalled frame) step like one frame // instead of integrating physics over a huge interval. if dtSeconds <= 0 || dtSeconds > 10*tickClassicPeak.Seconds() { dtSeconds = tickClassicPeak.Seconds() } d.lastTick = now for i, level := range levels { d.barPos[i] = classicPeakStep(d.barPos[i], level, dtSeconds) if d.peakHold[i] > 0 { d.peakHold[i] = max(0, d.peakHold[i]-dtSeconds) if d.peakHold[i] > 0 { continue } } prevVel := d.peakVel[i] d.peakPos[i] += d.peakVel[i] * dtSeconds d.peakVel[i] -= classicPeakGravity * dtSeconds if d.peakPos[i] > classicPeakMaxHeight { d.peakPos[i] = classicPeakMaxHeight } if prevVel > 0 && d.peakVel[i] <= 0 && d.peakPos[i] > d.barPos[i]+classicPeakVisibleEpsilon { d.peakVel[i] = 0 d.peakHold[i] = classicPeakApexHold continue } if d.peakPos[i] <= d.barPos[i] { d.peakPos[i] = d.barPos[i] d.peakVel[i] = 0 d.peakHold[i] = 0 } } } func (d *classicPeakDriver) renderState(v *Visualizer) ([]float64, []float64) { levels := d.levels(v) if len(levels) != len(d.barPos) || len(levels) != len(d.peakPos) { return levels, levels } return d.barPos, d.peakPos }