-
Notifications
You must be signed in to change notification settings - Fork 3
kithara waveform
Documentation reviewed from source revision 4a470dd2c. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
Contracts and invariants of the three-band display waveform. The README owns the overview and the feature list.
This crate owns the waveform value (Bucket, Waveform), its byte codec, its
tunables (AnalysisParams), and the position-addressed analyzer that produces
it (WaveformAnalyzer, behind the dsp feature). The analyzer is
synchronous: it owns no async, I/O, cancellation, scheduling, or colour. A
pass, its coverage record, persistence and cache identity belong to
kithara-analysis; colour belongs to the painter.
Frame-range coverage types come from kithara-signal, the versioned framing
from kithara-blob, pooled buffers from the caller's kithara-bufpool
region, and the FFT, band sums and downmix from kithara-dsp.
The model, the codec, the parameters, the resume record and AnalyzerError
compile without dsp, so a crate that reads a served or stored waveform
(kithara-play) links no analyzer. AnalysisParams names its FFT length as a
kithara_dsp::spectrum::FftLen, so kithara-dsp is a dependency either way;
dsp turns on its spectrum feature, so a build without the analyzer
compiles no FFT.
-
Bucket { low, mid, high }is one display column: three band heights, each in[0, 1]on one scale shared by the whole waveform. All-zero is silence, neverNaN. Consumers paint the bands as concentric mirrored bars, low behind, so all three stay visible. -
Waveformholds its buckets in anArc<[Bucket]>: a clone shares them. Bucket order is normalized track position[0, 1], never wall-clock seconds; playback rate and mixing remap that axis and never re-run analysis. -
Waveform::try_from(Vec<Bucket>)is the checked way in for a waveform a caller already holds. It refuses more thanMAX_BUCKETS(65 536) buckets withWaveformError::TooLarge, and a band that is not finite or lies outside[0, 1]withWaveformError::Band { index, value }. It admits exactly what the codec admits, so a structure and a blob cannot disagree about what a valid waveform is. Only the analyzer builds aWaveformunchecked, because it normalizes every band before it fills a bucket.
Waveform::write_to appends a kithara-blob frame to caller-owned storage:
the u32 WAVEFORM_BYTES_VERSION, then three little-endian f32 per bucket
in the order low, mid, high. Waveform::try_from(&[u8]) returns
BlobError::Version for another version and BlobError::Corrupt for a body
that is not a whole number of buckets or that try_from(Vec<Bucket>)
refuses; a reader treats both as a cache miss. WAVEFORM_BYTES_VERSION moves
when the encoding, the analysis parameters, or the bucket resolution
consumers ask for changes.
WaveformAnalyzer::new(sample_rate, params, pools) plans a
kithara_dsp::spectrum::Fft of params.fft_size() and takes its Spectrum
from the caller's region. It returns AnalyzerError::Spectrum when no backend
runs that length and AnalyzerError::Pool when the spectrum does not fit the
region. A window is N = fft_size frames long, and windows start every
hop = N / 4 frames: window k spans [k·hop, k·hop + N) of the source.
Push. push(pools, pcm, channels, at) takes interleaved PCM starting at
source frame at:
- It downmixes every whole frame to mono by the channel mean through
kithara_dsp::downmix. Zero channels, or no whole frame, is a no-op. - It copies the mono block into every window it overlaps and reduces a window once all of that window's frames are present. Blocks may arrive in any order, twice, or overlapping; a window split across blocks reduces to what the whole window would, and a reduced window is never reduced again.
- At most 256 windows wait for missing frames. Past that the window opened first is dropped with a debug log, and its span stays unanalysed: frames arriving later never reduce it from the part that survived.
- The downmix buffer and the waiting windows come from the caller's
PoolRegion<S>,S: HasPool<f32>;PoolErrorwhen one cannot grow under the region budget.
Reduce. One window becomes three band energies:
-
Fft::forwardapplies the symmetric Hann window, zero-pads a shorter frame, and writes bins0..=N/2. - A crossover maps to bin
⌊hz / (rate / N)⌋, clamped toN/2 + 1, and the mid/high bin is never below the low/mid bin. Low is bins1..low_mid, midlow_mid..mid_high, highmid_high..=N/2. The DC bin is left out, so a constant offset never colors the low band, and a crossover at 0 Hz or above Nyquist leaves its band empty. - A band's energy is
kithara_dsp::sum_squaresover the real and imaginary parts of its bins, divided by its bin count, so bands compare as energy density rather than by width. - A window whose RMS over the bins past DC,
√(Σ|X|² / N), is belowenergy_floorcontributes zero to every band.
Snapshot. snapshot(buckets, extent) folds the reduced windows into a
Waveform and leaves the pass able to take further ranges:
- The window count comes from the extent when it is known,
(extent − N) / hop + 1, so bucket boundaries stay put as coverage grows; while it is unknown, from the highest reduced window. A known extent shorter than one window reduces window 0 from the frames it has, zero-padded. - A window not reduced yet — a gap, or an evicted window — reads as silence until its frames arrive.
- The waveform carries
min(buckets, windows)buckets: never more columns than analysed windows. BucketbofBfolds windows[b·W/B, (b+1)·W/B)ofW, keeping each band's loudest window; this is the only mapping from windows to track position. - Each band then takes
√and itsband_gain, and every band of every bucket divides by one shared maximum, so the loudness tilt between bands survives, the loudest band of the track is1, and silence stays all zero.
Defaults. AnalysisParams is a bon builder whose Default is the
builder's defaults: fft_size 4096, crossovers at 250 Hz and 2500 Hz,
energy_floor 1e-4, band_gain [1.0, 2.5, 12.0]. Music tilts energy
toward the low end, so without lifting mid and high the upper bands render
as slivers; band_gain is that balance, not a colour, and low stays the
dominant hull.
Why fft_size is an FftLen: the analysis FFT runs on vDSP on Apple and
on realfft elsewhere, and vDSP builds only f·2ⁿ. A plain usize would
accept a length that builds on Linux and fails on Apple; FftLen::new
refuses it where the parameters are made.
write_resume appends a checkpoint of the algorithm, not a result: a served
waveform never carries one. It holds every reduced window's band energies in
index order, every waiting window's samples, covered frames and opening
order, and the opening counter.
-
WaveformResume::decodereads that section from akithara-blobReaderand returnsBlobError::Corruptfor a truncated section, a non-finite energy, an index out of order, a waiting window beside a reduced window of the same index, a waiting window with no samples or no covered frames, or an opening order not below the counter. -
restore(pools, resume)also refuses more than 256 waiting windows, a waiting window whose length is not this analyzer's window, and covered frames outside a window's span;BlobError::Poolwhen the samples do not fit the region. - A restored pass snapshots what the stopped one would have.
The record type compiles without dsp, so a build without the analyzer
still reads the section; kithara-analysis then refuses a record that
carries one.
The analysis lane of just test runs the crate's tests with dsp. Unit
tests generate their own signal. tests/reference.rs holds the heights of a
full-spectrum mix and a stereo tone within 1e-4 of references recorded
before the FFT moved to kithara-dsp, on every backend.
-
kithara-analysisruns one analyzer per pass behind itsanalysis-waveformfeature, which enablesdsp. It builds the analyzer fromAnalysisParams::default(), sets the bucket ceiling, feeds decoded source ranges, and stores the resume section in its progress record. -
kithara-playreads a prepared waveform document throughWaveform::try_from(&[u8])and needs only the model. - The
kitharafacade re-exports the crate askithara::waveformunder itswaveformfeature; itsanalysis-waveformfeature enablesdsp.