spatial_effects_mixer/
spatial_effects_mixer.rs1use 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}