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:
-
mediais 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. -
submittedis 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. -
playedis an optional position reported by the output. It isNonewhen 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.