Remedia

Rust filters and positions

Filtergraph replacement, gain, seeking, and source-to-output timing.

Native FFmpeg filtergraphs and master volume

PlaybackSession::set_filter_graph accepts a trusted, raw libavfilter audio graph expression with one effective audio input and output. The graph receives and returns Remedia's packed stereo f32, 48 kHz PCM. FFmpeg validates filter names, options, connections, and negotiated output format. Whitespace-only input is rejected with FilterGraphError::Empty. Pass None to clear filtering.

session.set_filter_graph(Some("bass=gain=8,treble=gain=-2"))?;
session.set_volume(0.75)?; // 75% linear master gain, independent of graph filters
println!("graph={:?}, gain={}", session.filter_graph(), session.volume());
session.set_filter_graph(Some("tremolo=f=5:d=0.6"))?; // replace while active
session.set_filter_graph(None)?;
# Ok::<(), Box<dyn std::error::Error>>(())

Replacement boundaries

Remedia builds and validates a replacement graph before requesting a change. The worker installs it at a decoded PCM boundary and discards delayed output still held inside the previous graph. PCM already accepted by the output stays accepted. A live change does not call the seek-flush operation, reset the Opus encoder, invalidate queued RTP packets, reopen the source, or restart the decoder. The Opus adapter keeps its partial frame, so one encoded packet can contain accepted samples from either side of the graph boundary. Sample positions remain contiguous.

Invalid replacements leave the current graph active. Changes can be made before playback, while playing, or while paused. Pause and seek serialize with graph replacement. Stateful filter delay does not carry across replacement. Seeking is different: it rebuilds filter state at the new source position and flushes pre-seek output. Natural EOF drains delayed output from the active graph before output completion. If EOF wins a race with a queued graph update after no PCM boundary remains, the update is acknowledged as the session's configuration for its next source. It does not alter audio already being finished. A seek that loses the same EOF race returns an invalid-state error instead of waiting indefinitely.

Source and output timelines

Filtergraphs may change duration or sample count. seek(Duration) remains relative to the original media timeline. position().media is the source position associated with the latest filtered PCM accepted by the output; Timestamp means it came from a valid decoded source timestamp, while Estimated means timestamps were missing or the graph can buffer/change the source-to-output mapping. It is a processing position, not a claim about what a listener has heard.

position().submitted is the end of the contiguous filtered-output sample timeline and is the timeline used by Opus. played is the output's own consumed/audible report when available. Discord currently reports no audible position. The Opus/RTP transport timeline remains continuous across graph changes. A seek deliberately invalidates pre-seek queued media but keeps the connection's RTP sequence and timestamp state.

Gain validation

The optional master gain is a finite linear value in 0.0..=2.0: zero mutes, one is unity, and 1.5 is 150% amplitude. Values outside that range, NaN, and infinity return PlaybackError::InvalidVolume. Gain is applied after filtering at a frame boundary. Output samples are clamped to [-1.0, 1.0]. Gains above unity can therefore clip and do not provide limiting or loudness normalization. Volume and graph configuration survive track replacement on the same session and are independent across sessions.

Trusted graph expressions

Treat graph expressions as trusted application configuration. Some FFmpeg filters can read files, URLs, or other external resources. Endpoint validation enforces the single-input/single-output PCM contract. It is not a sandbox and does not make arbitrary graphs safe to expose to untrusted remote clients. When no graph is configured, Remedia bypasses libavfilter entirely.

Position fields

position() exposes three positions:

  • media is the original source timeline associated with the latest output boundary. It can be estimated for timestamp-less media and for any graph that buffers or changes timing. The core does not infer speed ratios by parsing filter text.

  • submitted is the end of the latest accepted frame. It follows filtered PCM sample counts, not source time. This keeps Opus input contiguous even for tempo and sample-rate filters.

  • played is an optional position reported by the output. It is None when an output can only report submission or buffering, and should not be interpreted as audible playback progress.

NullOutput consumes a frame immediately, so its played position follows its consumed sample count. Its statistics distinguish scalar interleaved f32 values from sample frames per channel. Duration is based on total consumed sample frames, including audio consumed again after a seek.

Encoder delay and padding

FFmpeg may discard encoder-delay or padding samples described by a codec's skip-sample metadata. Remedia sets the decoder packet time base from the selected stream so FFmpeg can adjust decoded timestamps for those samples. The messages “Could not update timestamps for skipped samples” and “Could not update timestamps for discarded samples” are FFmpeg warnings about that timestamp adjustment when the decoder lacks a usable packet time base. They do not mean the decoder failed to apply the skip/discard metadata. Container duration may therefore differ slightly from the number of audible decoded samples.

On this page