Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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), and rubato (sample rate conversion) internally. Exposes a Player + PlaybackHandle API 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, and FilePlaybackOptions::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 the PlaybackHandle.
  • 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 PlaybackEngine wrapper, 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 + Sync handles; raw access is possible through the mixer graph API if needed.

References

Was this page helpful?