rawdio 0.14.0

An Audio Engine, inspired by the Web Audio API
Documentation
use std::time::Duration;

use itertools::Itertools;
use rawdio::{prelude::*, Pan};

struct Fixture {
    sample_rate: usize,
    channel_count: usize,
    _context: Box<dyn Context>,
    audio_process: Box<dyn AudioProcess>,
    pan: Pan,
}

fn make_noise_buffer(
    frame_count: usize,
    channel_count: usize,
    sample_rate: usize,
) -> OwnedAudioBuffer {
    let reference = OwnedAudioBuffer::white_noise(frame_count, 1, sample_rate);

    let mut buffer = OwnedAudioBuffer::new(frame_count, channel_count, sample_rate);

    (0..channel_count).for_each(|channel| {
        buffer.copy_from(
            &reference,
            SampleLocation::origin(),
            SampleLocation::channel(channel),
            1,
            reference.frame_count(),
        );
    });

    buffer
}

impl Fixture {
    fn process(&mut self, duration: Duration) -> OwnedAudioBuffer {
        let frame_count = (duration.as_secs_f64() * self.sample_rate as f64).ceil() as usize;
        let input_buffer = make_noise_buffer(frame_count, self.channel_count, self.sample_rate);
        let mut output_buffer =
            OwnedAudioBuffer::new(frame_count, self.channel_count, self.sample_rate);
        self.audio_process
            .process(&input_buffer, &mut output_buffer);
        output_buffer
    }
}

impl Default for Fixture {
    fn default() -> Self {
        let sample_rate = 48_000;

        let (mut context, process) =
            create_engine_with_options(EngineOptions::default().with_sample_rate(sample_rate));

        let channel_count = 2;

        let pan = Pan::new(context.as_ref(), channel_count);

        connect_nodes!("input" => pan => "output");

        context.start();

        Self {
            _context: context,
            audio_process: process,
            pan,
            sample_rate,
            channel_count,
        }
    }
}

fn get_energy_of_channel(audio_buffer: &dyn AudioBuffer, channel_index: usize) -> f64 {
    let data = audio_buffer.get_channel_data(SampleLocation::new(channel_index, 0));
    data.iter().fold(0.0_f64, |total_energy, sample| {
        total_energy + (*sample).powf(2.0) as f64
    })
}

fn process_with_pan(pan: f64) -> Vec<f64> {
    let mut fixture = Fixture::default();

    fixture.pan.pan().set_value_at_time(pan, Timestamp::zero());

    let audio = fixture.process(Duration::from_secs_f64(1.0));

    (0..audio.channel_count())
        .map(|channel| get_energy_of_channel(&audio, channel))
        .collect()
}

#[test]
fn panned_fully_left() {
    let energy = process_with_pan(-1.0);

    assert!(energy[0] > 0.0);
    assert!(energy[1] == 0.0);
}

#[test]
fn panned_fully_right() {
    let energy = process_with_pan(1.0);

    assert!(energy[0] == 0.0);
    assert!(energy[1] > 0.0);
}

#[test]
fn panned_centrally() {
    let energy = process_with_pan(0.0);
    assert!(energy.iter().all_equal());
}

#[test]
fn panned_part_left() {
    let energy = process_with_pan(-0.5);

    assert!(energy[0] > 0.0);
    assert!(energy[1] > 0.0);
    assert!(energy[0] > energy[1]);
}

#[test]
fn panned_part_right() {
    let energy = process_with_pan(0.5);

    assert!(energy[0] > 0.0);
    assert!(energy[1] > 0.0);
    assert!(energy[0] < energy[1]);
}