17 releases (10 breaking)
| 0.13.0 | Nov 26, 2025 |
|---|---|
| 0.11.2 | Oct 19, 2025 |
| 0.7.1 | Jul 16, 2025 |
#153 in Audio
535KB
11K
SLoC
phonic is a cross-platform audio playback and DSP library for Rust. It provides a flexible, low-latency audio engine and related tools for desktop and web-based music applications
Originally developed for the AFEC-Explorer app, phonic initially addressed the need for precise playback position monitoring not found in other Rust audio libraries. It is now also used as the default sample playback engine for the experimental algorithmic sequencer pattrns.
[!NOTE] phonic has not yet reached a stable version, so expect breaking changes.
Features
-
Cross-Platform Audio Playback:
- Play audio on Windows, macOS, and Linux via cpal.
- WebAssembly support for in-browser audio via emscripten.
- WAV file output for rendering computed audio to a file instead of playing it back.
-
Flexible Audio Source Handling:
-
Advanced Playback Control:
- Sample-precise scheduling for accurate sequencing.
- Real-time monitoring of playback position and status for GUI integration.
- Dynamic control over volume, panning, and playback speed via playback handles.
-
Custom Synthesis and DSPs:
- Build simple or complex DSP graphs by routing audio through optional sub-mixers.
- Play completely custom-built synthesizers or use the optional dasp integration for creating synth sources.
- Apply custom-built or use built-in DSP effects: gain, filter, eq, reverb, chorus, compressor, limiter, distortion.
- DSP effects are automatically bypassed to safe CPU cycles, when they receive no audible input.
Documentation
Rust docs for the last published versions are available at https://docs.rs/phonic
Examples
See /examples directory for more examples.
File Playback with Monitoring
Play, seek and stop audio files on the default audio output device. Monitor playback status of playing files.
use std::{time::Duration, sync::mpsc::sync_channel};
use phonic::{
DefaultOutputDevice, Player, PlaybackStatusEvent, Error,
FilePlaybackOptions, SynthPlaybackOptions
};
fn main() -> Result<(), Error> {
// Create a player with the default output device and a channel to receive playback events.
let (playback_status_sender, playback_status_receiver) = sync_channel(32);
let mut player = Player::new(DefaultOutputDevice::open()?, playback_status_sender);
// Start playing a file: The file below is going to be "preloaded" because it uses the
// default playback options. Preloaded means it's entirely decoded first, then played back
// from a decoded buffer. All files played through the player are automatically resampled
// and channel-mapped to match the audio output's signal specs.
// Preloaded files can also be cheaply cloned, so they can be allocated once and played back
// many times too. The returned handle allows changing playback properties of the files.
let small_file = player.play_file(
"PATH_TO/some_small_file.wav",
FilePlaybackOptions::default())?;
// The next file is going to be decoded and streamed on the fly, which is especially handy
// for long files, as it can start playing right away and won't need to allocate memory
// for the entire file.
let long_file = player.play_file(
"PATH_TO/some_long_file.mp3",
FilePlaybackOptions::default()
.streamed()
.volume_db(-6.0)
.speed(0.5)
.repeat(2),
)?;
// You can optionally track playback status events from the player.
std::thread::spawn(move || {
while let Ok(event) = playback_status_receiver.recv() {
match event {
PlaybackStatusEvent::Position { id, path, context: _, position } => {
// `context` is an optional, user defined payload, which can be passed
// along to the status with `player.play_file_with_context`
println!("Playback pos of source #{id} '{path}': {pos}",
pos = position.as_secs_f32()
);
}
PlaybackStatusEvent::Stopped { id, path, context: _, exhausted, } => {
if exhausted {
println!("Playback of #{id} '{path}' finished");
} else {
println!("Playback of #{id} '{path}' was stopped");
}
}
}
}
});
// The returned handles allow controlling playback properties of playing files.
// The second args is an optional sample time, where `None` means immediately.
long_file.seek(Duration::from_secs(5), None)?;
// Using Some sample time args, we can schedule changes (sample-accurate).
let now = player.output_sample_frame_position();
let samples_per_second = player.output_sample_rate() as u64;
// Use the handle's `is_playing` functions to check if a file is still playing.
if long_file.is_playing() {
long_file.set_volume(0.3, now + samples_per_second)?; // Fade down after 1 second
long_file.stop(now + 2 * samples_per_second)?; // Stop after 2 seconds
}
// If you only want one file to play at the same time, stop all playing sounds.
player.stop_all_sources()?;
// And then schedule a new source for playback.
let _boom = player.play_file("PATH_TO/boom.wav", FilePlaybackOptions::default())?;
Ok(())
}
File playback with DSP Effects in a Mixer Graph
Create DSP graphs by routing sources through different mixers and effects.
use phonic::{
DefaultOutputDevice, Player, Error, FilePlaybackOptions,
effects::{ChorusEffect, ReverbEffect}
};
fn main() -> Result<(), Error> {
// Create a player with the default output device.
let mut player = Player::new(DefaultOutputDevice::open()?, None);
// Add a reverb effect to the main mixer. All sounds played without a
// specific target mixer will now be routed through this effect.
let reverb = player.add_effect(ReverbEffect::with_parameters(0.6, 0.8), None)?;
// Create a new sub-mixer that is a child of the main mixer.
let chorus_mixer_id = player.add_mixer(None)?;
// Add a chorus effect to this new mixer. Sources routed to this mixer will
// now apply the chorus effect and reverb (the main mixer effects).
let chorus = player.add_effect(ChorusEffect::default(), chorus_mixer_id)?;
// Effect parameters can be automated via the returned handles.
// The `None` arguments are optional sample times to schedule events.
reverb.set_parameter(ReverbEffect::ROOM_SIZE_ID, 0.9f32, None)?;
chorus.set_parameter_normalized(ChorusEffect::RATE_ID, 0.5, None)?;
// Play a file through the main mixer (which has reverb only).
let _some_file = player.play_file(
"PATH_TO/some_file.wav",
FilePlaybackOptions::default(),
)?;
// Play another file through the chorus mixer (and main mixer with the reverb FX).
let _another_file = player.play_file(
"PATH_TO/another_file.wav",
FilePlaybackOptions::default().target_mixer(chorus_mixer_id),
)?;
Ok(())
}
Contributing
Patches are welcome! Please fork the latest git repository and create a feature or bugfix branch.
License
phonic is distributed under the terms of the GNU Affero General Public License V3.
Dependencies
~9–42MB
~651K SLoC