**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](https://github.com/emuell/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](https://github.com/renoise/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](https://github.com/RustAudio/cpal).
- WebAssembly support for in-browser audio via [emscripten](https://emscripten.org/).
- WAV file output for rendering computed audio to a file instead of playing it back.
- **Flexible Audio Source Handling**:
- Play, seek, stop, and mix **preloaded** (buffered) or **streamed** (on-the-fly decoded) audio files.
- Support for most common audio formats through [Symphonia](https://github.com/pdeljanov/Symphonia).
- Seamless loop playback using loop points from WAV and FLAC files.
- Automatic resampling and channel mapping via a fast custom resampler and [Rubato](https://github.com/HEnquist/rubato).
- **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](https://github.com/RustAudio/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](https://github.com/emuell/phonic/tree/master/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.
```rust no_run
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.
```rust no_run
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](https://www.gnu.org/licenses/agpl-3.0.html).