Skip to main content

worklet_instrument/
worklet_instrument.rs

1use std::{collections::HashMap, error::Error, thread, time::Duration};
2
3use kira::{AudioManager, AudioManagerSettings, backend::cpal::CpalBackend};
4use melody_bay::{
5    AudioContext, AudioContextOptions, AudioWorkletNode, AudioWorkletNodeOptions,
6    AudioWorkletParameterDescriptor, AudioWorkletProcessContext, AudioWorkletProcessor,
7    AutomationRate, Waveform,
8};
9
10const SAMPLE_RATE: u32 = 48_000;
11
12#[derive(Clone)]
13struct FoldbackTremolo {
14    phase: f32,
15}
16
17impl AudioWorkletProcessor for FoldbackTremolo {
18    fn process(
19        &mut self,
20        inputs: &[Vec<Vec<f32>>],
21        outputs: &mut [Vec<Vec<f32>>],
22        context: AudioWorkletProcessContext,
23    ) -> bool {
24        let Some(output_port) = outputs.first_mut() else {
25            return true;
26        };
27        let rate = context
28            .processor_options
29            .get("rate_hz")
30            .and_then(|value| value.parse::<f32>().ok())
31            .unwrap_or(5.0);
32        let drive = context
33            .processor_options
34            .get("drive")
35            .and_then(|value| value.parse::<f32>().ok())
36            .unwrap_or(1.2);
37        let stop_after = context
38            .processor_options
39            .get("stop_after")
40            .and_then(|value| value.parse::<f64>().ok());
41
42        for (channel_index, output_channel) in output_port.iter_mut().enumerate() {
43            for (frame, output_sample) in output_channel.iter_mut().enumerate() {
44                let input = inputs
45                    .first()
46                    .and_then(|port| port.get(channel_index).or_else(|| port.first()))
47                    .and_then(|channel| channel.get(frame))
48                    .copied()
49                    .unwrap_or(0.0);
50                let time = context.current_time + frame as f64 * context.sample_dt;
51                let depth = context
52                    .parameter_values
53                    .get("depth")
54                    .and_then(|values| values.get(frame))
55                    .copied()
56                    .or_else(|| context.parameters.get("depth").copied())
57                    .unwrap_or(0.5);
58                let mix = context
59                    .parameter_values
60                    .get("mix")
61                    .and_then(|values| values.get(frame))
62                    .copied()
63                    .or_else(|| context.parameters.get("mix").copied())
64                    .unwrap_or(0.6);
65                let lfo = (time as f32 * std::f32::consts::TAU * rate + self.phase).sin();
66                let tremolo = 1.0 - depth * 0.5 + depth * 0.5 * lfo;
67                let folded = (input * drive).sin() * tremolo;
68                *output_sample = input * (1.0 - mix) + folded * mix;
69            }
70        }
71        self.phase = (self.phase + 0.01).rem_euclid(std::f32::consts::TAU);
72        stop_after.is_none_or(|stop_after| context.current_time < stop_after)
73    }
74}
75
76fn main() -> Result<(), Box<dyn Error>> {
77    let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
78    let mut context = AudioContext::try_new_with_options(AudioContextOptions {
79        sample_rate: Some(SAMPLE_RATE),
80        ..Default::default()
81    })?;
82    build_worklet_patch(&mut context, 4.0)?;
83    let handle = manager.play(context.sound_data().sample_rate(SAMPLE_RATE))?;
84    println!("Playing AudioWorklet foldback tremolo with automated parameters...");
85    thread::sleep(Duration::from_millis(4_200));
86    handle.stop();
87    Ok(())
88}
89
90fn build_worklet_patch(
91    context: &mut AudioContext,
92    stop_time: f64,
93) -> Result<AudioWorkletNode, Box<dyn Error>> {
94    let osc = context.create_oscillator();
95    osc.set_type(Waveform::Sawtooth);
96    osc.frequency().set_value(165.0)?;
97    osc.frequency()
98        .linear_ramp_to_value_at_time(220.0, stop_time)?;
99    osc.try_start(0.0)?;
100    osc.try_stop(stop_time)?;
101
102    let mut parameter_data = HashMap::new();
103    parameter_data.insert("depth".to_owned(), 0.65);
104    parameter_data.insert("mix".to_owned(), 0.55);
105    let mut processor_options = HashMap::new();
106    processor_options.insert("rate_hz".to_owned(), "5.5".to_owned());
107    processor_options.insert("drive".to_owned(), "1.35".to_owned());
108    processor_options.insert("stop_after".to_owned(), stop_time.to_string());
109    let worklet = context.try_create_audio_worklet_node(
110        FoldbackTremolo { phase: 0.0 },
111        AudioWorkletNodeOptions {
112            number_of_inputs: 1,
113            number_of_outputs: 1,
114            output_channel_count: Some(vec![2]),
115            parameter_descriptors: vec![
116                AudioWorkletParameterDescriptor {
117                    name: "depth".to_owned(),
118                    default_value: 0.5,
119                    min_value: 0.0,
120                    max_value: 1.0,
121                    automation_rate: AutomationRate::ARate,
122                },
123                AudioWorkletParameterDescriptor {
124                    name: "mix".to_owned(),
125                    default_value: 0.6,
126                    min_value: 0.0,
127                    max_value: 1.0,
128                    automation_rate: AutomationRate::KRate,
129                },
130            ],
131            parameter_data,
132            processor_options,
133        },
134    )?;
135    worklet
136        .param("depth")
137        .expect("depth parameter")
138        .linear_ramp_to_value_at_time(0.15, stop_time)?;
139    worklet
140        .param("mix")
141        .expect("mix parameter")
142        .set_target_at_time(0.85, 1.25, 0.5)?;
143    let output = context.create_gain();
144    output.gain().set_value(0.18)?;
145    context.connect(osc, &worklet)?;
146    context.connect(&worklet, &output)?;
147    context.connect(&output, context.destination())?;
148    Ok(worklet)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn worklet_processor_transforms_a_render_quantum() {
157        let mut processor = FoldbackTremolo { phase: 0.0 };
158        let inputs = vec![vec![vec![0.2; 128], vec![0.1; 128]]];
159        let mut outputs = vec![vec![vec![0.0; 128], vec![0.0; 128]]];
160        let mut parameters = HashMap::new();
161        parameters.insert("depth".to_owned(), 0.5);
162        parameters.insert("mix".to_owned(), 0.75);
163        let mut parameter_values = HashMap::new();
164        parameter_values.insert("depth".to_owned(), vec![0.5; 128]);
165        let mut processor_options = HashMap::new();
166        processor_options.insert("rate_hz".to_owned(), "5.5".to_owned());
167        processor_options.insert("drive".to_owned(), "1.35".to_owned());
168        processor_options.insert("stop_after".to_owned(), "1.0".to_owned());
169
170        assert!(processor.process(
171            &inputs,
172            &mut outputs,
173            AudioWorkletProcessContext {
174                current_time: 0.0,
175                sample_dt: 1.0 / SAMPLE_RATE as f64,
176                parameters,
177                parameter_values,
178                processor_options,
179            },
180        ));
181        let peak = outputs[0]
182            .iter()
183            .flatten()
184            .map(|sample| sample.abs())
185            .fold(0.0, f32::max);
186        assert!(peak > 0.01, "processor rendered near silence");
187        assert!(
188            outputs[0][0]
189                .iter()
190                .any(|sample| (*sample - 0.2).abs() > 0.001)
191        );
192    }
193
194    #[test]
195    fn worklet_exposes_expected_parameters() {
196        let mut context = AudioContext::try_new_with_sample_rate(SAMPLE_RATE).unwrap();
197        let worklet = build_worklet_patch(&mut context, 0.5).unwrap();
198        assert!(worklet.param("depth").is_some());
199        assert!(worklet.param("mix").is_some());
200    }
201}