Skip to main content

spatial_effects_mixer/
spatial_effects_mixer.rs

1use std::{error::Error, thread, time::Duration};
2
3use kira::{AudioManager, AudioManagerSettings, backend::cpal::CpalBackend};
4use melody_bay::{
5    AudioContext, AudioContextOptions, ChannelMergerOptions, ChannelSplitterOptions, DelayOptions,
6    GainOptions, PannerOptions, StereoPannerOptions, Waveform,
7};
8
9const SAMPLE_RATE: u32 = 48_000;
10
11fn main() -> Result<(), Box<dyn Error>> {
12    let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
13    let mut context = AudioContext::try_new_with_options(AudioContextOptions {
14        sample_rate: Some(SAMPLE_RATE),
15        ..Default::default()
16    })?;
17    build_spatial_scene(&mut context, 5.0)?;
18    let handle = manager.play(context.sound_data().sample_rate(SAMPLE_RATE))?;
19    println!("Playing moving panner, stereo sends, indexed routing, and disconnect helpers...");
20    thread::sleep(Duration::from_millis(5_200));
21    handle.stop();
22    Ok(())
23}
24
25fn build_spatial_scene(context: &mut AudioContext, stop_time: f64) -> Result<(), Box<dyn Error>> {
26    let listener = context.listener();
27    listener.position_x().set_value_at_time(-0.6, 0.0)?;
28    listener
29        .position_x()
30        .linear_ramp_to_value_at_time(0.6, stop_time)?;
31    listener.forward_z().set_value(-1.0)?;
32
33    let lead = context.create_oscillator();
34    lead.set_type(Waveform::Triangle);
35    lead.frequency().set_value(330.0)?;
36    lead.try_start(0.0)?;
37    lead.try_stop(stop_time)?;
38
39    let source_amp = context.try_create_gain_with_options(GainOptions { gain: 0.12 })?;
40    source_amp.gain().set_value_at_time(0.0, 0.0)?;
41    source_amp.gain().linear_ramp_to_value_at_time(0.12, 0.12)?;
42    source_amp
43        .gain()
44        .linear_ramp_to_value_at_time(0.0, stop_time)?;
45
46    let panner = context.try_create_panner_with_options(PannerOptions {
47        position_x: -1.2,
48        position_z: 1.5,
49        ..Default::default()
50    })?;
51    panner
52        .position_x()
53        .linear_ramp_to_value_at_time(1.2, stop_time)?;
54    panner.orientation_z().set_value(-1.0)?;
55
56    let stereo =
57        context.try_create_stereo_panner_with_options(StereoPannerOptions { pan: -0.35 })?;
58    stereo.pan().linear_ramp_to_value_at_time(0.35, stop_time)?;
59
60    let splitter = context.try_create_channel_splitter_with_options(ChannelSplitterOptions {
61        number_of_outputs: 2,
62    })?;
63    let merger = context.try_create_channel_merger_with_options(ChannelMergerOptions {
64        number_of_inputs: 2,
65    })?;
66    let delay = context.try_create_delay_with_options(DelayOptions {
67        max_delay_time: 0.6,
68        delay_time: 0.16,
69    })?;
70    let send_gain = context.try_create_gain_with_options(GainOptions { gain: 0.18 })?;
71    let return_gain = context.try_create_gain_with_options(GainOptions { gain: 0.22 })?;
72    let output = context.try_create_gain_with_options(GainOptions { gain: 0.8 })?;
73
74    let control = context.create_constant_source();
75    control.offset().set_value(0.04)?;
76    control.try_start(0.0)?;
77    control.try_stop(stop_time)?;
78
79    context.connect(lead, &source_amp)?;
80    context.connect(&source_amp, &panner)?;
81    context.connect(&panner, &stereo)?;
82    context.connect(&stereo, &splitter)?;
83    context.connect_with_indices(&splitter, 0, &merger, 0)?;
84    context.connect_with_indices(&splitter, 1, &merger, 1)?;
85    context.connect_with_indices(&splitter, 0, &send_gain, 0)?;
86    context.connect(&send_gain, &delay)?;
87    context.connect(&delay, &return_gain)?;
88    context.connect(&return_gain, &merger)?;
89    context.connect(&merger, &output)?;
90    context.connect(&output, context.destination())?;
91    context.connect_param_from_output(&splitter, 0, stereo.pan())?;
92    context.connect_param(&control, delay.delay_time())?;
93
94    context.disconnect_param_from_output(&splitter, 0, stereo.pan())?;
95    context.disconnect_with_indices(&splitter, 0, &send_gain, 0)?;
96    context.connect_with_indices(&splitter, 1, &send_gain, 0)?;
97    context.disconnect_outputs(&return_gain)?;
98    context.connect(&return_gain, &merger)?;
99    context.disconnect_param_outputs(&control)?;
100    context.connect_param(&control, delay.delay_time())?;
101
102    println!("listener position: {:?}", listener.position_value());
103    println!("routing mixer output: {:?}", context.node_info(&output)?);
104    Ok(())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use melody_bay::OfflineAudioContext;
111
112    #[test]
113    fn spatial_scene_renders_offline() {
114        let mut context = OfflineAudioContext::try_new(2, SAMPLE_RATE as usize, SAMPLE_RATE)
115            .expect("offline context");
116        let osc = context.create_oscillator();
117        osc.set_type(Waveform::Sine);
118        osc.frequency().set_value(220.0).unwrap();
119        osc.try_start(0.0).unwrap();
120        osc.try_stop(0.5).unwrap();
121        let panner = context
122            .try_create_panner_with_options(PannerOptions {
123                position_x: 0.5,
124                position_z: 1.0,
125                ..Default::default()
126            })
127            .unwrap();
128        context.connect(osc, &panner).unwrap();
129        context.connect(&panner, context.destination()).unwrap();
130        let rendered = context.start_rendering().unwrap();
131        let left = rendered.channel_data(0).unwrap();
132        assert!(left.iter().any(|sample| sample.abs() > 0.001));
133    }
134}