Skip to main content

sound_data_bridge/
sound_data_bridge.rs

1use 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}