Documentation
¶
Overview ¶
Package audio provides the PCM ingest and FFT analysis that feed the TUI's visualizers. PipeReader consumes raw little-endian s16le stereo samples from librespot's stdout; the FFT layer produces FrequencyData with log-spaced bands plus bass/mid/high convenience averages.
FrequencyData also exposes LeftLevel and RightLevel, time-domain per-channel peak amplitudes shared-AGC normalized to 0–1, for visualizers (e.g. the VU meter) that need true stereo loudness rather than the post-mix spectral peak. ProgressMs is derived from the running sample count so visualizers can display playback progress without a separate Spotify poll. PipeReader is safe to call Start/Stop from any goroutine; reads are internally synchronized.
Index ¶
Constants ¶
const ChunkBytes = WindowSize * 2 * 2
ChunkBytes is the number of raw bytes per FFT frame (stereo, 16-bit). 2048 samples × 2 channels × 2 bytes = 8192.
const NumBands = 64
NumBands is the number of frequency bands in FFT output.
const WindowSize = 2048
WindowSize is the number of mono samples per FFT frame. 2048 at 44100 Hz = ~46 ms, a good latency/resolution tradeoff.
Variables ¶
var DefaultFormat = PCMFormat{SampleRate: 44100, Channels: 2, BitDepth: 16}
DefaultFormat is librespot's default output: 44100 Hz, stereo, 16-bit signed LE.
Functions ¶
This section is empty.
Types ¶
type Analyzer ¶
type Analyzer struct {
// contains filtered or unexported fields
}
Analyzer performs FFT analysis on PCM audio chunks and produces FrequencyData.
func NewAnalyzer ¶
NewAnalyzer creates an Analyzer with a precomputed Hann window of the given size.
func (*Analyzer) Analyze ¶
func (a *Analyzer) Analyze(samples []int16) FrequencyData
Analyze takes interleaved stereo int16 PCM samples and returns FrequencyData. The samples slice must contain at least WindowSize*2 values (stereo pairs).
type FrequencyData ¶
type FrequencyData struct {
Bands [NumBands]float32 // log-spaced frequency bands, normalized 0.0–1.0
Peak float32 // overall spectral peak this frame, 0.0–1.0
Bass float32 // average of bands 0–7
Mid float32 // average of bands 8–31
High float32 // average of bands 32–63
LeftLevel float32 // time-domain per-channel peak, AGC-normalized 0.0–1.0
RightLevel float32 // time-domain per-channel peak, AGC-normalized 0.0–1.0
ProgressMs int32 // playback progress derived from PCM sample count
}
FrequencyData holds FFT output mapped to visualization-friendly bands.
func (*FrequencyData) ComputeConvenienceFields ¶
func (fd *FrequencyData) ComputeConvenienceFields()
ComputeConvenienceFields fills Bass, Mid, and High from Bands.
type PipeReader ¶ added in v0.2.0
type PipeReader struct {
// NewPlayer creates an audio player. Defaults to oto. Override in tests.
NewPlayer playerFactory
// contains filtered or unexported fields
}
PipeReader reads raw PCM from librespot's stdout pipe, plays it via oto, runs FFT analysis, and stores the latest FrequencyData atomically.
func NewPipeReader ¶ added in v0.2.0
func NewPipeReader() *PipeReader
NewPipeReader creates a PipeReader ready to accept pipes via Start().
func (*PipeReader) Latest ¶ added in v0.2.0
func (pr *PipeReader) Latest() *FrequencyData
Latest returns the most recent FrequencyData, or nil if no fresh data. Returns nil if the last frame is older than 150ms (e.g., paused or between restarts). Thread-safe; called from the Bubble Tea goroutine.
Bands are compensated by the inverse of the current device volume so visualizers stay bright at low playback volume (librespot's softvol scales the PCM before piping). Compensation is capped at maxVolumeGain.
func (*PipeReader) SetVolumePercent ¶ added in v0.3.0
func (pr *PipeReader) SetVolumePercent(v int)
SetVolumePercent records the current Spotify device volume (0–100) so Latest() can compensate the FFT bands for it. Safe to call from any goroutine; applies to the next Latest() read. Out-of-range values are clamped and logged so upstream data quality issues surface in the log.
func (*PipeReader) Start ¶ added in v0.2.0
func (pr *PipeReader) Start(pipe io.ReadCloser)
Start begins reading PCM from pipe, playing audio and running FFT analysis. Safe to call multiple times — each call cancels the previous read loop. This handles librespot restarts which create a new stdout pipe each time.
func (*PipeReader) Stop ¶ added in v0.2.0
func (pr *PipeReader) Stop()
Stop cancels any active read loop. Safe to call multiple times.