sound_data_bridge/
sound_data_bridge.rs1use std::{convert::Infallible, error::Error, thread, time::Duration};
2
3use kira::{
4 AudioManager, AudioManagerSettings, Frame,
5 backend::cpal::CpalBackend,
6 info::Info,
7 sound::{Sound, SoundData},
8};
9use melody_bay::{AnalyserNode, AudioContext, BiquadFilterType, StereoPannerOptions};
10
11const SAMPLE_RATE: u32 = 48_000;
12
13struct ProceduralPhrase {
14 notes: Vec<(f32, f64)>,
15}
16
17impl SoundData for ProceduralPhrase {
18 type Error = Infallible;
19 type Handle = ();
20
21 fn into_sound(self) -> Result<(Box<dyn Sound>, Self::Handle), Self::Error> {
22 Ok((
23 Box::new(ProceduralPhraseSound {
24 notes: self.notes,
25 note_index: 0,
26 note_elapsed: 0.0,
27 phase: 0.0,
28 }),
29 (),
30 ))
31 }
32}
33
34struct ProceduralPhraseSound {
35 notes: Vec<(f32, f64)>,
36 note_index: usize,
37 note_elapsed: f64,
38 phase: f32,
39}
40
41impl Sound for ProceduralPhraseSound {
42 fn process(&mut self, out: &mut [Frame], dt: f64, _info: &Info) {
43 let sample_dt = if dt.is_finite() && dt > 0.0 {
44 dt
45 } else {
46 1.0 / SAMPLE_RATE as f64
47 };
48 for frame in out {
49 let Some((frequency, duration)) = self.notes.get(self.note_index).copied() else {
50 *frame = Frame::ZERO;
51 continue;
52 };
53 let attack = (self.note_elapsed / 0.02).min(1.0) as f32;
54 let release = ((duration - self.note_elapsed) / 0.08).clamp(0.0, 1.0) as f32;
55 let envelope = attack.min(release);
56 let sample = self.phase.sin() * envelope * 0.16;
57 *frame = Frame::from_mono(sample);
58 self.phase = (self.phase + std::f32::consts::TAU * frequency * sample_dt as f32)
59 .rem_euclid(std::f32::consts::TAU);
60 self.note_elapsed += sample_dt;
61 if self.note_elapsed >= duration {
62 self.note_elapsed = 0.0;
63 self.note_index += 1;
64 }
65 }
66 }
67
68 fn finished(&self) -> bool {
69 self.note_index >= self.notes.len()
70 }
71}
72
73fn main() -> Result<(), Box<dyn Error>> {
74 let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
75 let mut context = AudioContext::try_new_with_sample_rate(SAMPLE_RATE)?;
76 let analyser = build_bridge(&mut context)?;
77 let handle = manager.play(context.sound_data().sample_rate(SAMPLE_RATE))?;
78 for tick in 0..6 {
79 thread::sleep(Duration::from_millis(500));
80 println!(
81 "t={:.1}s bridge peak={:.3} rms={:.3}",
82 (tick + 1) as f32 * 0.5,
83 analyser.peak(),
84 analyser.rms()
85 );
86 }
87 handle.stop();
88 Ok(())
89}
90
91fn build_bridge(context: &mut AudioContext) -> Result<AnalyserNode, Box<dyn Error>> {
92 let source = context.create_sound_data_source(ProceduralPhrase {
93 notes: vec![(261.63, 0.35), (329.63, 0.35), (392.0, 0.5), (523.25, 0.7)],
94 });
95 source.try_start(0.0)?;
96 source.try_stop(2.2)?;
97
98 let filter = context.create_biquad_filter();
99 filter.set_type(BiquadFilterType::Lowpass);
100 filter.frequency().set_value_at_time(700.0, 0.0)?;
101 filter
102 .frequency()
103 .linear_ramp_to_value_at_time(2_200.0, 1.6)?;
104 let pan = context.try_create_stereo_panner_with_options(StereoPannerOptions { pan: -0.3 })?;
105 pan.pan().linear_ramp_to_value_at_time(0.3, 2.0)?;
106 let analyser = context.create_analyser();
107 let output = context.create_gain();
108 output.gain().set_value(0.65)?;
109
110 context.connect(&source, &filter)?;
111 context.connect(&filter, &pan)?;
112 context.connect(&pan, &output)?;
113 context.connect(&output, &analyser)?;
114 context.connect(&analyser, context.destination())?;
115 Ok(analyser)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use kira::{info::MockInfoBuilder, sound::SoundData};
122
123 #[test]
124 fn bridge_sound_data_outputs_audio() {
125 let data = ProceduralPhrase {
126 notes: vec![(220.0, 0.2), (330.0, 0.2)],
127 };
128 let (mut sound, _) = data.into_sound().unwrap();
129 let info = MockInfoBuilder::new().build();
130 let mut out = vec![Frame::ZERO; 4_800];
131 sound.process(&mut out, 1.0 / SAMPLE_RATE as f64, &info);
132 assert!(out.iter().any(|frame| frame.left.abs() > 0.001));
133 }
134
135 #[test]
136 fn bridge_graph_exposes_analysis() {
137 let mut context = AudioContext::try_new_with_sample_rate(SAMPLE_RATE).unwrap();
138 let analyser = build_bridge(&mut context).unwrap();
139 assert!(analyser.frequency_bin_count() > 0);
140 }
141}