1use std::{error::Error, thread, time::Duration};
2
3use kira::{
4 AudioManager, AudioManagerSettings, Frame, backend::cpal::CpalBackend, info::MockInfoBuilder,
5 sound::SoundData,
6};
7use melody_bay::{
8 AnalyserNode, AnalyserOptions, AudioBuffer, AudioBufferSourceOptions, AudioContext,
9 AudioContextOptions, BiquadFilterOptions, BiquadFilterType, ChannelMergerOptions,
10 ChannelSplitterOptions, ConstantSourceOptions, ConvolverOptions, DelayOptions,
11 DynamicsCompressorOptions, GainOptions, IirFilterOptions, OscillatorOptions, OscillatorType,
12 Oversample, PannerOptions, PeriodicWaveOptions, StereoPannerOptions, WaveShaperOptions,
13 Waveform,
14};
15
16const SAMPLE_RATE: u32 = 48_000;
17const DURATION: f64 = 6.0;
18
19fn main() -> Result<(), Box<dyn Error>> {
20 let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
21 let mut context = AudioContext::try_new_with_options(AudioContextOptions {
22 sample_rate: Some(SAMPLE_RATE),
23 ..Default::default()
24 })?;
25 let analyser = build_synth_lab(&mut context, DURATION)?;
26
27 println!(
28 "destination: {:?}",
29 context.node_info(context.destination())?
30 );
31 println!("analyser: {:?}", context.node_info(&analyser)?);
32 let stats = preflight(&context, 2.0)?;
33 println!(
34 "preflight peak={:.3} rms={:.3} max_step={:.4}",
35 stats.peak, stats.rms, stats.max_step
36 );
37
38 let handle = manager.play(context.sound_data().sample_rate(SAMPLE_RATE))?;
39 for tick in 0..12 {
40 thread::sleep(Duration::from_millis(500));
41 let mut bins = vec![0.0; analyser.frequency_bin_count().min(10)];
42 analyser.get_float_frequency_data(&mut bins);
43 let brightest = bins.iter().copied().fold(f32::NEG_INFINITY, f32::max);
44 println!(
45 "t={:.1}s peak={:.3} rms={:.3} brightest_bin_db={:.1}",
46 (tick + 1) as f32 * 0.5,
47 analyser.peak(),
48 analyser.rms(),
49 brightest
50 );
51 }
52 handle.stop();
53 Ok(())
54}
55
56fn build_synth_lab(
57 context: &mut AudioContext,
58 stop_time: f64,
59) -> Result<AnalyserNode, Box<dyn Error>> {
60 let custom_wave = context.try_create_periodic_wave_with_options(
61 [0.0, 0.0, 0.22, 0.12],
62 [0.0, 1.0, 0.35, 0.18],
63 PeriodicWaveOptions {
64 disable_normalization: false,
65 },
66 )?;
67 let carrier = context.try_create_oscillator_with_options(OscillatorOptions {
68 oscillator_type: OscillatorType::Custom(custom_wave),
69 frequency: 110.0,
70 detune: -8.0,
71 })?;
72 carrier
73 .frequency()
74 .linear_ramp_to_value_at_time(146.83, 3.0)?;
75 carrier.try_start(0.0)?;
76 carrier.try_stop(stop_time)?;
77
78 let overtone = context.try_create_oscillator_with_options(OscillatorOptions {
79 oscillator_type: OscillatorType::Basic(Waveform::Triangle),
80 frequency: 220.0,
81 detune: 4.0,
82 })?;
83 overtone.try_start(0.0)?;
84 overtone.try_stop(stop_time)?;
85
86 let buffer = AudioBuffer::try_from_mono(
87 SAMPLE_RATE,
88 SAMPLE_RATE as usize / 2,
89 (0..SAMPLE_RATE / 2).map(|i| {
90 let t = i as f32 / SAMPLE_RATE as f32;
91 let burst = (1.0 - t * 2.0).max(0.0);
92 (t * std::f32::consts::TAU * 880.0).sin() * burst * 0.18
93 }),
94 )?;
95 let buffer_source =
96 context.try_create_buffer_source_with_options(AudioBufferSourceOptions {
97 buffer: Some(buffer),
98 playback_rate: 0.85,
99 detune: 0.0,
100 looping: false,
101 loop_start: 0.0,
102 loop_end: 0.5,
103 })?;
104 buffer_source.try_start(2.0)?;
105 buffer_source.try_stop(3.2)?;
106
107 let lfo =
108 context.try_create_constant_source_with_options(ConstantSourceOptions { offset: 0.012 })?;
109 lfo.try_start(0.0)?;
110 lfo.try_stop(stop_time)?;
111
112 let carrier_gain = context.try_create_gain_with_options(GainOptions { gain: 0.0 })?;
113 carrier_gain.gain().set_value_at_time(0.0, 0.0)?;
114 carrier_gain
115 .gain()
116 .linear_ramp_to_value_at_time(0.11, 0.18)?;
117 carrier_gain
118 .gain()
119 .set_value_curve_at_time([0.11, 0.08, 0.13, 0.09, 0.12], 0.4, 2.8)?;
120 carrier_gain.gain().set_target_at_time(0.07, 3.4, 0.55)?;
121 carrier_gain
122 .gain()
123 .cancel_and_hold_at_time(stop_time - 0.35)?;
124 carrier_gain
125 .gain()
126 .linear_ramp_to_value_at_time(0.0, stop_time)?;
127
128 let burst_gain = context.try_create_gain_with_options(GainOptions { gain: 0.12 })?;
129 let delay = context.try_create_delay_with_options(DelayOptions {
130 max_delay_time: 0.75,
131 delay_time: 0.08,
132 })?;
133 let biquad = context.try_create_biquad_filter_with_options(BiquadFilterOptions {
134 filter_type: BiquadFilterType::Lowpass,
135 frequency: 1_200.0,
136 q: 0.9,
137 ..Default::default()
138 })?;
139 biquad
140 .frequency()
141 .linear_ramp_to_value_at_time(2_400.0, 2.5)?;
142 biquad.frequency().set_target_at_time(700.0, 3.5, 0.8)?;
143
144 let iir = context.try_create_iir_filter_with_options(IirFilterOptions {
145 feedforward: vec![0.55, 0.35],
146 feedback: vec![1.0, -0.18],
147 })?;
148 let shaper = context.try_create_wave_shaper_with_options(WaveShaperOptions {
149 curve: Some(vec![-1.0, -0.35, 0.0, 0.35, 1.0]),
150 oversample: Oversample::TwoX,
151 })?;
152 let stereo =
153 context.try_create_stereo_panner_with_options(StereoPannerOptions { pan: -0.15 })?;
154 stereo.pan().linear_ramp_to_value_at_time(0.25, stop_time)?;
155 let panner = context.try_create_panner_with_options(PannerOptions {
156 position_x: 0.8,
157 position_z: 1.5,
158 ..Default::default()
159 })?;
160 let compressor =
161 context.try_create_dynamics_compressor_with_options(DynamicsCompressorOptions {
162 threshold: -20.0,
163 knee: 18.0,
164 ratio: 2.5,
165 attack: 0.01,
166 release: 0.22,
167 })?;
168 let convolver = context.try_create_convolver_with_options(ConvolverOptions {
169 buffer: Some(AudioBuffer::try_from_mono(
170 SAMPLE_RATE,
171 8,
172 [0.55, 0.28, 0.15, 0.08, 0.04, 0.02, 0.01, 0.005],
173 )?),
174 disable_normalization: true,
175 })?;
176 let analyser = context.try_create_analyser_with_options(AnalyserOptions {
177 fft_size: 1024,
178 min_decibels: -100.0,
179 max_decibels: -12.0,
180 smoothing_time_constant: 0.35,
181 })?;
182 let splitter = context.try_create_channel_splitter_with_options(ChannelSplitterOptions {
183 number_of_outputs: 2,
184 })?;
185 let merger = context.try_create_channel_merger_with_options(ChannelMergerOptions {
186 number_of_inputs: 2,
187 })?;
188 let output = context.try_create_gain_with_options(GainOptions { gain: 0.82 })?;
189
190 context.connect(carrier, &carrier_gain)?;
191 context.connect(overtone, &carrier_gain)?;
192 context.connect(buffer_source, &burst_gain)?;
193 context.connect(&carrier_gain, &delay)?;
194 context.connect(&delay, &biquad)?;
195 context.connect_param(lfo, delay.delay_time())?;
196 context.connect(&biquad, &iir)?;
197 context.connect(&iir, &shaper)?;
198 context.connect(&shaper, &stereo)?;
199 context.connect(&stereo, &panner)?;
200 context.connect(&burst_gain, &splitter)?;
201 context.connect_with_indices(&splitter, 0, &merger, 0)?;
202 context.connect_with_indices(&splitter, 1, &merger, 1)?;
203 context.connect(&panner, &compressor)?;
204 context.connect(&merger, &compressor)?;
205 context.connect(&compressor, &convolver)?;
206 context.connect(&convolver, &output)?;
207 context.connect(&output, &analyser)?;
208 context.connect(&analyser, context.destination())?;
209
210 println!("output node info: {:?}", context.node_info(&output)?);
211 Ok(analyser)
212}
213
214#[derive(Clone, Copy, Debug)]
215struct AudioStats {
216 peak: f32,
217 rms: f32,
218 max_step: f32,
219}
220
221fn preflight(context: &AudioContext, seconds: f64) -> Result<AudioStats, Box<dyn Error>> {
222 let (mut sound, _) = context.sound_data().sample_rate(SAMPLE_RATE).into_sound()?;
223 let info = MockInfoBuilder::new().build();
224 let mut out = vec![Frame::ZERO; (seconds * SAMPLE_RATE as f64) as usize];
225 sound.process(&mut out, 1.0 / SAMPLE_RATE as f64, &info);
226 Ok(audio_stats(&out))
227}
228
229fn audio_stats(out: &[Frame]) -> AudioStats {
230 let peak = out
231 .iter()
232 .map(|frame| frame.left.abs().max(frame.right.abs()))
233 .fold(0.0, f32::max);
234 let rms = (out
235 .iter()
236 .map(|frame| {
237 let mono = (frame.left + frame.right) * 0.5;
238 mono * mono
239 })
240 .sum::<f32>()
241 / out.len().max(1) as f32)
242 .sqrt();
243 let max_step = out
244 .windows(2)
245 .map(|frames| {
246 (frames[1].left - frames[0].left)
247 .abs()
248 .max((frames[1].right - frames[0].right).abs())
249 })
250 .fold(0.0, f32::max);
251 AudioStats {
252 peak,
253 rms,
254 max_step,
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use melody_bay::OfflineAudioContext;
262
263 #[test]
264 fn synth_lab_live_graph_stays_listenable() {
265 let mut context = AudioContext::try_new_with_sample_rate(SAMPLE_RATE).unwrap();
266 build_synth_lab(&mut context, DURATION).unwrap();
267 let stats = preflight(&context, 4.0).unwrap();
268 assert!(stats.peak > 0.02, "rendered near silence: {stats:?}");
269 assert!(stats.peak < 0.55, "peak too hot: {stats:?}");
270 assert!(stats.rms < 0.18, "rms too dense: {stats:?}");
271 assert!(stats.max_step < 0.08, "click-like sample jump: {stats:?}");
272 }
273
274 #[test]
275 fn synth_lab_offline_context_renders() {
276 let mut context = OfflineAudioContext::try_new(2, SAMPLE_RATE as usize, SAMPLE_RATE)
277 .expect("offline context");
278 let osc = context.create_oscillator();
279 osc.set_type(Waveform::Sine);
280 osc.frequency().set_value(220.0).unwrap();
281 osc.try_start(0.0).unwrap();
282 osc.try_stop(0.25).unwrap();
283 let gain = context.create_gain();
284 gain.gain().set_value(0.1).unwrap();
285 context.connect(osc, &gain).unwrap();
286 context.connect(&gain, context.destination()).unwrap();
287 let rendered = context.start_rendering().unwrap();
288 assert!(
289 rendered
290 .channel_data(0)
291 .unwrap()
292 .iter()
293 .any(|sample| sample.abs() > 0.001)
294 );
295 }
296}