Skip to content
/ afplay Public

Cross-platform audio playback library for Rust

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

emuell/afplay

Folders and files

NameName
Last commit message
Last commit date
May 15, 2024
Oct 10, 2022
Apr 26, 2023
May 15, 2024
Sep 15, 2022
May 15, 2024
Sep 6, 2022
Sep 6, 2022
Apr 13, 2023

Repository files navigation

afplay

afplay is an audio playback library for Rust, based on the psst-core audio playback implementation.

It aims to be a suitable audio player backend for game engines, but can also serve as a general-purpose low-latency music and sound playback engine for desktop music apps.

It was originally developed and is used in the AFEC-Explorer app and related projects which are using the Tauri app framework.

Features

  • Play, seek, stop, mix and monitor playback of preloaded (buffered) or streamed (on-the-fly decoded) audio files.
  • Play, stop, mix and monitor playback of custom synth tones thanks to dasp (optional feature: disabled by default).
  • Play audio on Windows, macOS and Linux via cpal or cubeb (cpal is enabled by default).
  • Decodes and thus plays back most common audio file formats, thanks to Symphonia.
  • Files are automatically resampled and channel mapped to the audio output's signal specs, thanks to libsamplerate.
  • Click free playback: when stopping sounds, a very short volume fade-out is applied to avoid clicks.
  • Sample precise playback scheduling, e.g. to play samples in a sequencer.

See Also

  • afwaveplot: to generate waveform plots from audio file paths or raw sample buffers.

Examples

See /examples directory for more examples.

Simple Audio Playback

Play and stop audio files on the system's default audio output device.

use afplay::{
    AudioFilePlayer, AudioOutput, AudioSink, DefaultAudioOutput,
    FilePlaybackOptions, Error
};

// Open the default audio device (cpal or cubeb, depending on the enabled output feature)
let audio_output = DefaultAudioOutput::open()?;
// Create a player and transfer ownership of the audio output to the player.
let mut player = AudioFilePlayer::new(audio_output.sink(), None);

// Play back a file with the default playback options.
player.play_file(
   "PATH_TO/some_file.wav",
    FilePlaybackOptions::default())?;
// Play back another file on top with custom playback options.
player.play_file(
    "PATH_TO/some_long_file.mp3",
    FilePlaybackOptions::default()
        .streamed() // decodes the file on-the-fly
        .volume_db(-6.0) // lower the volume a bit
        .speed(0.5) // play file at half the speed
        .repeat(2), // repeat, loop it 2 times
)?;

// Stop all playing files: this will quickly fade-out all playing files to avoid clicks.
player.stop_all_sources()?;

Advanced Audio Playback

Play, seek and stop audio files and synth sounds on the default audio output device. Monitor playback status of playing files and synth tones.

use afplay::{
    AudioFilePlayer, AudioOutput, AudioSink, DefaultAudioOutput,
    AudioFilePlaybackStatusEvent, FilePlaybackOptions, SynthPlaybackOptions,
    Error
};

#[cfg(feature = "dasp")]
use dasp::Signal;

// Open the default audio device (cpal or cubeb, depending on the enabled output feature)
let audio_output = DefaultAudioOutput::open()?;

// Create an optional channel to receive playback status events ("Position", "Stopped" events)
// Prefer using a bounded channel here to avoid memory allocations in the audio thread.
let (playback_status_sender, playback_status_receiver) = crossbeam_channel::bounded(32);
// Create a player and transfer ownership of the audio output to the player. The player will
// play, mix down and manage all files and synth sources for us from here.
let mut player = AudioFilePlayer::new(audio_output.sink(), Some(playback_status_sender));

// We'll start playing a file now: 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 buffer.
// Files played through the player are automatically resampled and channel-mapped to match the
// audio output's signal specs, so there's nothing more to do to get it played:
let small_file_id = 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 such as music, as it can start playing right away and won't need to allocate
// memory for the entire file.
// We're also repeating the file playback 2 times, lowering the volume and are pitching it
// down. As the player mixes down everything, we'll hear both files at the same time now:
let long_file_id = player.play_file(
    "PATH_TO/some_long_file.mp3",
    FilePlaybackOptions::default()
        .streamed()
        .volume_db(-6.0)
        .speed(0.5)
        .repeat(2),
)?;

// !! NB: optional `dasp-synth` feature needs to be enabled for the following to work !!
// Let's play a simple synth tone as well. You can play any dasp::Signal here. The passed
// signal will be wrapped in a dasp::signal::UntilExhausted, so this can be used to create
// one-shots. The example below plays a sine wave for two secs at 440hz.
#[cfg(feature = "dasp")]
let sample_rate = player.output_sample_rate();
#[cfg(feature = "dasp")]
let dasp_signal = dasp::signal::from_iter(
    dasp::signal::rate(sample_rate as f64)
        .const_hz(440.0)
        .sine()
        .take(sample_rate as usize * 2),
);
#[cfg(feature = "dasp")]
let synth_id = player.play_dasp_synth(
    dasp_signal,
    "my_synth_sound",
    SynthPlaybackOptions::default())?;

// You can optionally track playback status events from the player:
std::thread::spawn(move || {
    while let Ok(event) = playback_status_receiver.recv() {
        match event {
            AudioFilePlaybackStatusEvent::Position { 
                id, 
                path, 
                context: _, 
                position 
            } => {
                println!(
                    "Playback pos of source #{} '{}': {}",
                    id,
                    path,
                    position.as_secs_f32()
                );
            }
            AudioFilePlaybackStatusEvent::Stopped {
                id,
                path,
                context: _,
                exhausted,
            } => {
                if exhausted {
                    println!("Playback of #{} '{}' finished", id, path);
                } else {
                    println!("Playback of #{} '{}' was stopped", id, path);
                }
            }
        }
    }
});

// Playing files can be seeked or stopped:
player.seek_source(long_file_id, std::time::Duration::from_secs(5))?;
player.stop_source(small_file_id)?;

// Synths can not be seeked, but they can be stopped.
#[cfg(feature = "dasp")]
player.stop_source(synth_id)?;

// If you only want one file to play at the same time, simply stop all playing
// sounds before starting a new one:
player.stop_all_sources()?;
player.play_file("PATH_TO/boom.wav", FilePlaybackOptions::default())?;

Playback Sequencing

Play a sample file sequence in time with e.g. musical beats.

use afplay::{
   source::file::preloaded::PreloadedFileSource, utils::speed_from_note, AudioFilePlayer,
   AudioOutput, DefaultAudioOutput, Error, FilePlaybackOptions,
};

// create a player
let mut player = AudioFilePlayer::new(DefaultAudioOutput::open()?.sink(), None);

// calculate at which rate the sample file should be emitted
let beats_per_min = 120.0;
let samples_per_sec = player.output_sample_rate();
let samples_per_beat = samples_per_sec as f64 * 60.0 / beats_per_min as f64;

// preload a sample file
let preloaded_sample_source = PreloadedFileSource::new(
    "path/to_some_file.wav",
    None, // we don't need a channel for playback events
    FilePlaybackOptions::default(),
    samples_per_sec,
)?;

// schedule playback of the sample file every beat for 8 beats
let playback_start = player.output_sample_frame_position();
for beat_counter in 0..8 {
    // when is the next beat playback due?
    let next_beats_sample_time =
        (playback_start as f64 + beat_counter as f64 * samples_per_beat()) as u64;
    // play a clone of the preloaded sample at the next beat's sample time.
    // cloning is very cheap as the sample buffer is shared...
    player.play_file_source(
        preloaded_sample_source.clone(
            FilePlaybackOptions::default()
              .speed(speed_from_note(60)), // middle-c
            samples_per_sec)?,
        Some(next_beats_sample_time),
    )?;
}

License

afplay is distributed under the terms of both the MIT license and the Apache License (Version 2.0).

About

Cross-platform audio playback library for Rust

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages