ADR-004: Audio backend — phonic
ADR-004: Audio backend — phonic
Status
Accepted
Context
The playback engine (FR-CORE-1) must play audio from URLs and file paths. It must be source-agnostic and cross-platform (Linux, macOS, Windows). The Rust audio ecosystem offers several approaches:
- phonic: Higher-level audio library that wraps
cpal(cross-platform audio output),symphonia(audio decoding), andrubato(sample rate conversion) internally. Exposes aPlayer+PlaybackHandleAPI with built-in DSP effects, playback status monitoring, mixer graphs, and sample-accurate scheduling. Licensed AGPL-3.0. - cpal + symphonia (raw): Low-level control. Requires manually wiring decode → buffer → output pipeline, sample rate conversion, and playback event emission.
- rodio: Higher-level library built on
cpal+symphonia. Simpler API but less control over buffering, seeking, and gapless playback. - GStreamer (via gstreamer-rs): Full multimedia framework. Powerful but massive dependency tree, GObject introspection, platform-specific installation requirements.
The playback engine must support:
- Playing audio from URLs and file paths
- Emitting playback position events (FR-CORE-1: “engine emits a ‘playing’ event”)
- Seeking, pausing, stopping
- Volume control
- Future gapless playback (W8, v2+)
- Cross-platform output
Decision
Use phonic for all audio playback (pin at implementation time to the latest available version — v0.16+ as of March 2026). The playback engine becomes a thin wrapper around phonic’s Player API:
use phonic::{DefaultOutputDevice, Player, FilePlaybackOptions, PlaybackStatusEvent};
// Open output device and create player
let (status_tx, status_rx) = sync_channel(32);
let mut player = Player::new(DefaultOutputDevice::open()?, Some(status_tx));
// Play a file (streamed for large files, preloaded for small files)
let handle = player.play_file(path_or_url, FilePlaybackOptions::default().streamed())?;
// Monitor playback position
while let Ok(event) = status_rx.recv() {
match event {
PlaybackStatusEvent::Position { id, position, .. } => {
emit("playback_state", { id, position: position.as_secs_f32() });
}
PlaybackStatusEvent::Stopped { id, exhausted, .. } => {
if exhausted { advance_queue(); }
}
}
}
// Control via handle
handle.seek(Duration::from_secs(30), None)?;
handle.set_volume(0.5, None)?;
handle.stop(None)?;
Architecture:
Audio Source (URL / File Path)
|
phonic (Player)
|
+-- symphonia (decode) -- internal
+-- rubato (sample rate conversion) -- internal
+-- Internal audio buffer -- internal
+-- cpal (output stream) -- internal
|
Physical audio device
Key points:
- phonic handles decoding (via symphonia), sample rate conversion (via rubato), buffering, and output (via cpal) internally.
- The engine uses phonic’s
FilePlaybackOptions::streamed()for stream URLs and large files, andFilePlaybackOptions::default()(preloaded) for small local files. - Playback position events (
PlaybackStatusEvent::Position) and stop events (PlaybackStatusEvent::Stopped) are received on a channel and forwarded as Tauri events. - Tauri commands (
play,pause,seek,set_volume,stop) call methods on thePlaybackHandle. - Sample-accurate scheduling (
Some(sample_time)) is used for future gapless crossfade transitions (v2). - The phonic dependency implies AGPL-3.0 licensing for the project (phonic v0.8.0+ is AGPL-3.0; earlier versions were MIT/Apache-2.0).
Alternatives considered
- cpal + symphonia + rubato (raw): Requires manually building the ring buffer architecture, sample rate conversion pipeline, audio thread with channel-based control, and playback event emission. phonic already provides all of this as a tested, maintained library. The raw approach would require ~200-300 lines of custom audio plumbing that phonic gives us in 5 lines. Additionally, phonic’s internal rubato integration means sample rate conversion between plugin-provided audio (often 44.1kHz) and the output device’s preferred rate (often 48kHz) is handled automatically – a non-trivial piece of DSP to get right.
- rodio: phonic provides a similar level of abstraction but with significantly more control (sample-accurate scheduling, per-source volume/speed, playback handle monitoring). rodio’s decoder-per-stream model makes gapless crossfade harder to implement.
- GStreamer: Cross-platform, battle-tested, supports every format. But introduces a massive dependency (2,000+ files, system library requirement, GObject type system). For an app that plays local files and stream URLs, GStreamer is a cannon for a fly.
- Subprocess ffmpeg: Would make the core depend on an external executable not managed through the plugin system. Violates the platform philosophy that the core owns playback.
Consequences
Positive:
- Significantly less boilerplate than raw cpal + symphonia. The playback engine is ~50 lines instead of ~300.
- Sample rate conversion (rubato) is built-in – no separate integration work.
- Playback position and stop events are provided by the library via channels, directly satisfying FR-CORE-1’s “playing event” requirement.
- Sample-accurate scheduling supports future gapless playback (W8) – schedule crossfade start/end at exact sample positions.
- Built-in DSP effects (volume, speed, panning, EQ) are available if needed without additional dependencies.
- Actively maintained (20+ releases in 10 months as of March 2026).
- If phonic ever becomes unsuitable, dropping down to raw cpal + symphonia is straightforward since phonic wraps them – the format support and output device management are the same.
Negative:
- AGPL-3.0 license. phonic v0.8.0+ is AGPL-3.0 (v0.7.x and earlier were MIT/Apache-2.0). This means the Rymflux project must be AGPL-3.0 compatible when linking to phonic. This was accepted as a project-level decision.
- Young library (first published May 2025, 19 GitHub stars as of March 2026). Smaller community than raw cpal or symphonia. Risk of bugs or breaking changes. Mitigation: pin a specific version (latest at implementation time), test audio on all three platforms, and maintain awareness of the raw cpal+symphonia path as a fallback.
- Breaking changes expected before 1.0 (stated in phonic’s README). Mitigation: pin the version and evaluate upgrades deliberately. The audio engine is isolated behind our own thin
PlaybackEnginewrapper, so internal API changes are contained. - Less control over low-level audio buffer management. If custom visualization or analysis requires raw PCM access, phonic’s abstractions may need to be bypassed. Mitigation: phonic exposes
Send + Synchandles; raw access is possible through the mixer graph API if needed.
References
- Constraint C5 (cross-platform), R7 (platform fragmentation) – (constraints-assumptions.md, risk-register.md)
- FR-CORE-1 (functional.md)
- https://crates.io/crates/phonic
- https://github.com/emuell/phonic