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