Skip to main content

Mixer & Effect Chain

All timeline audio goes through one function, vv_audio::mixer::mix_range. The preview calls it from the cpal callback and the export calls it on its own thread, so both produce the same mix.

Signal chain

audio-signal-chain.png
ConstantValue and why
PROJECT_SAMPLE_RATE48 kHz for every mix.
MIX_BLOCK_FRAMES512: the mix goes track by track through scratch buffers of this size.
GAIN_BLOCK_FRAMES800 (~60 Hz): keyframed clip gain is evaluated per block, not per sample, at the block start so the way a mix is split into calls never changes it.

Strips and inserts

Every audio track and the master have a ChannelStrip (vv-core): fader in dB, balance, and an ordered list of effects. Before mixing, each strip becomes a Channel: one Insert per effect, then the bus gain. Balance is not a pan law: the centre leaves both channels at unity, moving to one side only attenuates the other.

EffectImplementation
NormalizeA gain toward a target, from a level measured through the effects before it: sample peak, true peak, or integrated loudness (EBU R128 / BS.1770, loudness.rs). On a track, Relative applies one gain to all clips; Independent gives each clip its own gain (Insert::ClipGains). Levels are measured in the background (LevelAnalysis); until a new reading arrives the previous one keeps playing.
Multiband compressorLinkwitz-Riley crossovers into 3 bands, one stereo-linked compressor per band, soft knee (dynamics.rs).
Equalizer6 biquad bands in series (eq.rs).
MonoEvery channel gets the average of all channels.

Compressor and EQ keep filter state between blocks in a MixState. A call that does not continue from the previous position resets it. Meters (per-track and master peaks, compressor band activity, spectra for the EQ and compressor panels) are atomics that only the metered call writes.

Preview: from the UI thread to the callback

audio-snapshot-handoff.png
  • One stream for the whole session. Mixer::new opens a single cpal output stream at 48 kHz with the device's native channel count. Asking for a different count makes PipeWire insert a remix that adds latency.
  • Immutable snapshots. TimelineAudio (vv-app) rebuilds a MixSnapshot when the history generation changes or a buffer arrives, then publishes it through Mixer::set_state.
  • Real-time safe. The callback only uses try_lock, so it never blocks. If it cannot take a lock it plays silence for that buffer. Effect state is allocated on the UI thread, and states the callback dropped are freed there too (retained).
  • Clock. The mixer position, in timeline samples, is the playhead. A gap in the timeline is just silence while the clock keeps running. Without an output device the clock is wall time.
  • Partial buffers. mix_buffers.rs decodes and resamples each (file, stream) on a worker and publishes the buffer while it grows, so the start of a track plays before decoding finishes.
  • Compound clips. Their audio is the mixdown of the nested timeline (compound_mixdown), cached only when complete.
  • Scrub. Scrubbing plays an 80 ms snippet of the mix at the new position.