phonic 0.15.0

Audio playback library
Documentation

phonic

crates.io docs.rs license

phonic is a cross-platform audio playback and DSP library for Rust. It provides a flexible, low-latency audio engine and DSP tools for desktop and web-based music application development.

Originally developed for the AFEC-Explorer app, phonic was created to provide precise playback position monitoring - a feature lacking in other Rust audio libraries at the time. It is now used in the experimental algorithmic sequencer pattrns as example playback engine and related audio projects.

[!NOTE] phonic has not yet reached a stable version, so expect breaking changes.

Features

  • Cross-Platform Audio Output

    • Play audio on Windows, macOS, Linux thanks to CPAL.
    • Play on the web via WebAssembly and Emscripten.
    • WAV file output for rendering audio to files thanks to Hound.
  • Audio File Decoding and Playback

    • Support for most common audio formats via Symphonia.
    • Automatic resampling and channel mapping via a fast custom resampler and Rubato.
    • Play audio files preloaded (buffered) or streamed (on-the-fly decoded).
    • Custom WAV and FLAC file parser to enable looped file playback.
  • Event Scheduling, Sequencing

    • Thread-safe Send + Sync handles for controlling playback and parameters from any thread.
    • Sample-precise scheduling of playback events for accurate sequencing.
  • Sampler and Synthesizer Tooling

    • Built in polyphonic sampler with optional AHDSR envelopes, granular synthesis and glide/portamento.
    • Create custom synths from scratch, or with style and fun via the optional FunDSP integration.
    • Dynamically route a predefined modulation source set (LFOs, Envelopes...) to generators.
  • DSP Effects and Mixer Graphs

    • Built-in DSP effects: gain, filter, 5-band EQ, reverb, chorus, compressor, limiter, distortion.
    • Create custom effects or build DSP graphs by routing audio through nested sub-mixers.
    • DSP parameter abstraction with normalized or raw value updates, scaling, smoothing, and string formatting.
    • Sub-mixers are by default processed concurrently across multiple audio worker threads.
    • Effect chains auto-bypass when receiving silence to save CPU.
  • Playback Monitoring

    • Real-time average and peak CPU load metrics for the mixer and individual sub-mixers.
    • Monitoring of file and generator playback positions for GUI integration.

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 audio files on the default audio output device. Monitor playback status of 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()?, Some(playback_status_sender));

    // Start playing a file with default options: preloads the file into RAM, enables loop playback
    // when loop point chunks are present in the file and uses default resampling options.
    let small_file = player.play_file(
        "PATH_TO/some_small_file.wav",
        FilePlaybackOptions::default())?;

    // Playback options allow configuring loop, streaming, resampling and other properties.
    let long_file = player.play_file(
        "PATH_TO/some_long_file.mp3",
        FilePlaybackOptions::default()
            .streamed()
            .volume_db(-6.0)
            .speed(0.5)
            .repeat(2),
    )?;

    // 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 } => {
                    // NB: `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 arg 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` function 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 Generators, DSP Effects in a Mixer Graph

Create DSP graphs by routing sources through different mixers and effects.

use std::time::Duration;

use phonic::{
    DefaultOutputDevice, Player, Error, FilePlaybackOptions,
    effects::{ChorusEffect, ReverbEffect}, ParameterValueUpdate, FourCC,
    generators::Sampler, GeneratorPlaybackOptions,
};

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 = 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.value_update(0.9), None)?;
    // Or if no parameter description is available (e.g. in UIs), send a normalized value. 
    chorus.set_parameter((FourCC(*b"rate"), ParameterValueUpdate::Normalized(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()),
    )?;

    // Create a sampler generator to play a sample.
    // We configure it to play on the chorus mixer.
    let generator = player.play_generator(
        Sampler::from_file(
            "path/to/some_sample.wav",
            GeneratorPlaybackOptions::default().target_mixer(chorus_mixer.id()),
            player.output_channel_count(),
            player.output_sample_rate(),
        )?,
        None
    )?;
    
    // Trigger a note on the generator. The `generator` handle is `Send + Sync` as well,
    // so you could also pass it to some other thread (e.g. a MIDI thread) to trigger
    // the generator from there.
    generator.note_on(60, Some(1.0f32), None, None)?;

    // [... do something else ...]
 
    // Stop generator and sample files (keep `some_file` running until it plays to the end)
    another_file.stop(None)?;
    generator.stop(None)?;

    // Wait until all files and generators finished playing
    while some_file.is_playing() || another_file.is_playing() || generator.is_playing() {
        std::thread::sleep(Duration::from_millis(500));
    }

    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.