Skip to main content

web_audio_api/node/
constant_source.rs

1use std::any::Any;
2
3use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
4use crate::param::{AudioParam, AudioParamDescriptor, AutomationRate};
5use crate::render::{
6    AudioParamValues, AudioProcessor, AudioRenderQuantum, AudioWorkletGlobalScope,
7};
8use crate::{assert_valid_time_value, RENDER_QUANTUM_SIZE};
9
10use super::{AudioNode, AudioScheduledSourceNode, ChannelConfig};
11
12/// Options for constructing an [`ConstantSourceNode`]
13// dictionary ConstantSourceOptions {
14//   float offset = 1;
15// };
16// https://webaudio.github.io/web-audio-api/#ConstantSourceOptions
17//
18// @note - Does not extend AudioNodeOptions because AudioNodeOptions are
19// useless for source nodes, because they instruct how to upmix the inputs.
20// This is a common source of confusion, see e.g. mdn/content#18472
21#[derive(Clone, Debug)]
22pub struct ConstantSourceOptions {
23    /// Initial parameter value of the constant signal
24    pub offset: f32,
25}
26
27impl Default for ConstantSourceOptions {
28    fn default() -> Self {
29        Self { offset: 1. }
30    }
31}
32
33/// Instructions to start or stop processing
34#[derive(Debug, Copy, Clone)]
35enum Schedule {
36    Start(f64),
37    Stop(f64),
38}
39
40/// Audio source whose output is nominally a constant value. A `ConstantSourceNode`
41/// can be used as a constructible `AudioParam` by automating the value of its offset.
42///
43/// - MDN documentation: <https://developer.mozilla.org/en-US/docs/Web/API/ConstantSourceNode>
44/// - specification: <https://webaudio.github.io/web-audio-api/#ConstantSourceNode>
45/// - see also: [`BaseAudioContext::create_constant_source`]
46///
47/// # Usage
48///
49/// ```no_run
50/// use web_audio_api::context::{BaseAudioContext, AudioContext};
51/// use web_audio_api::node::AudioNode;
52///
53/// let audio_context = AudioContext::default();
54///
55/// let gain1 = audio_context.create_gain();
56/// gain1.gain().set_value(0.);
57///
58/// let gain2 = audio_context.create_gain();
59/// gain2.gain().set_value(0.);
60///
61/// let automation = audio_context.create_constant_source();
62/// automation.offset().set_value(0.);
63/// automation.connect(gain1.gain());
64/// automation.connect(gain2.gain());
65///
66/// // control both `GainNode`s with 1 automation
67/// automation.offset().set_target_at_time(1., audio_context.current_time(), 0.1);
68/// ```
69///
70/// # Example
71///
72/// - `cargo run --release --example constant_source`
73///
74#[derive(Debug)]
75pub struct ConstantSourceNode {
76    registration: AudioContextRegistration,
77    channel_config: ChannelConfig,
78    offset: AudioParam,
79    has_start: bool,
80}
81
82impl AudioNode for ConstantSourceNode {
83    fn registration(&self) -> &AudioContextRegistration {
84        &self.registration
85    }
86
87    fn channel_config(&self) -> &ChannelConfig {
88        &self.channel_config
89    }
90
91    fn number_of_inputs(&self) -> usize {
92        0
93    }
94
95    fn number_of_outputs(&self) -> usize {
96        1
97    }
98}
99
100impl AudioScheduledSourceNode for ConstantSourceNode {
101    fn start(&mut self) {
102        let when = self.registration.context().current_time();
103        self.start_at(when);
104    }
105
106    fn start_at(&mut self, when: f64) {
107        assert_valid_time_value(when);
108        assert!(
109            !self.has_start,
110            "InvalidStateError - Cannot call `start` twice"
111        );
112
113        self.has_start = true;
114        self.registration.post_message(Schedule::Start(when));
115    }
116
117    fn stop(&mut self) {
118        let when = self.registration.context().current_time();
119        self.stop_at(when);
120    }
121
122    fn stop_at(&mut self, when: f64) {
123        assert_valid_time_value(when);
124        assert!(self.has_start, "InvalidStateError cannot stop before start");
125
126        self.registration.post_message(Schedule::Stop(when));
127    }
128}
129
130impl ConstantSourceNode {
131    pub fn new<C: BaseAudioContext>(context: &C, options: ConstantSourceOptions) -> Self {
132        context.base().register(move |registration| {
133            let ConstantSourceOptions { offset } = options;
134
135            let param_options = AudioParamDescriptor {
136                name: String::new(),
137                min_value: f32::MIN,
138                max_value: f32::MAX,
139                default_value: 1.,
140                automation_rate: AutomationRate::A,
141            };
142            let (param, proc) = context.create_audio_param(param_options, &registration);
143            param.set_value(offset);
144
145            let render = ConstantSourceRenderer {
146                offset: proc,
147                start_time: f64::MAX,
148                stop_time: f64::MAX,
149                ended_triggered: false,
150            };
151
152            let node = ConstantSourceNode {
153                registration,
154                channel_config: ChannelConfig::default(),
155                offset: param,
156                has_start: false,
157            };
158
159            (node, Box::new(render))
160        })
161    }
162
163    pub fn offset(&self) -> &AudioParam {
164        &self.offset
165    }
166}
167
168struct ConstantSourceRenderer {
169    offset: AudioParamId,
170    start_time: f64,
171    stop_time: f64,
172    ended_triggered: bool,
173}
174
175impl AudioProcessor for ConstantSourceRenderer {
176    fn process(
177        &mut self,
178        _inputs: &[AudioRenderQuantum],
179        outputs: &mut [AudioRenderQuantum],
180        params: AudioParamValues<'_>,
181        scope: &AudioWorkletGlobalScope,
182    ) -> bool {
183        // single output node
184        let output = &mut outputs[0];
185
186        let dt = 1. / scope.sample_rate as f64;
187        let next_block_time = scope.current_time + dt * RENDER_QUANTUM_SIZE as f64;
188
189        if self.start_time >= next_block_time {
190            output.make_silent();
191
192            if self.stop_time <= next_block_time {
193                if !self.ended_triggered {
194                    scope.send_ended_event();
195                    self.ended_triggered = true;
196                }
197
198                return false;
199            }
200
201            // #462 AudioScheduledSourceNodes that have not been scheduled to start can safely
202            // return tail_time false in order to be collected if their control handle drops.
203            return self.start_time != f64::MAX;
204        }
205
206        output.force_mono();
207
208        let offset = params.get(&self.offset);
209        let output_channel = output.channel_data_mut(0);
210
211        // fast path
212        if offset.len() == 1
213            && self.start_time <= scope.current_time
214            && self.stop_time >= next_block_time
215        {
216            output_channel.fill(offset[0]);
217        } else {
218            // sample accurate path
219            let mut current_time = scope.current_time;
220
221            output_channel
222                .iter_mut()
223                .zip(offset.iter().cycle())
224                .for_each(|(o, &value)| {
225                    if current_time < self.start_time || current_time >= self.stop_time {
226                        *o = 0.;
227                    } else {
228                        // as we pick values directly from the offset param which is already
229                        // computed at sub-sample accuracy, we don't need to do more than
230                        // copying the values to their right place.
231                        *o = value;
232                    }
233
234                    current_time += dt;
235                });
236        }
237
238        // tail_time false when output has ended this quantum
239        let still_running = self.stop_time > next_block_time;
240
241        if !still_running {
242            // @note: we need this check because this is called a until the program
243            // ends, such as if the node was never removed from the graph
244            if !self.ended_triggered {
245                scope.send_ended_event();
246                self.ended_triggered = true;
247            }
248        }
249
250        still_running
251    }
252
253    fn onmessage(&mut self, msg: &mut dyn Any) {
254        if let Some(schedule) = msg.downcast_ref::<Schedule>() {
255            match *schedule {
256                Schedule::Start(v) => self.start_time = v,
257                Schedule::Stop(v) => self.stop_time = v,
258            }
259            return;
260        }
261
262        log::warn!("ConstantSourceRenderer: Dropping incoming message {msg:?}");
263    }
264
265    fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
266        if !self.ended_triggered
267            && (scope.current_time >= self.start_time || scope.current_time >= self.stop_time)
268        {
269            scope.send_ended_event();
270            self.ended_triggered = true;
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use crate::context::{BaseAudioContext, OfflineAudioContext};
278    use crate::node::{AudioNode, AudioScheduledSourceNode};
279
280    use float_eq::assert_float_eq;
281
282    use super::*;
283
284    #[test]
285    fn test_audioparam_value_applies_immediately() {
286        let context = OfflineAudioContext::new(1, 128, 48000.);
287        let options = ConstantSourceOptions { offset: 12. };
288        let src = ConstantSourceNode::new(&context, options);
289        assert_float_eq!(src.offset.value(), 12., abs_all <= 0.);
290    }
291
292    #[test]
293    fn test_start_stop() {
294        let sample_rate = 48000.;
295        let start_in_samples = (128 + 1) as f64; // start rendering in 2d block
296        let stop_in_samples = (256 + 1) as f64; // stop rendering of 3rd block
297        let mut context = OfflineAudioContext::new(1, 128 * 4, sample_rate);
298
299        let mut src = context.create_constant_source();
300        src.connect(&context.destination());
301
302        src.start_at(start_in_samples / sample_rate as f64);
303        src.stop_at(stop_in_samples / sample_rate as f64);
304
305        let buffer = context.start_rendering_sync();
306        let channel = buffer.get_channel_data(0);
307
308        // 1rst block should be silence
309        assert_float_eq!(channel[0..128], vec![0.; 128][..], abs_all <= 0.);
310
311        // 2d block - start at second frame
312        let mut res = vec![1.; 128];
313        res[0] = 0.;
314        assert_float_eq!(channel[128..256], res[..], abs_all <= 0.);
315
316        // 3rd block - stop at second frame
317        let mut res = vec![0.; 128];
318        res[0] = 1.;
319        assert_float_eq!(channel[256..384], res[..], abs_all <= 0.);
320
321        // 4th block is silence
322        assert_float_eq!(channel[384..512], vec![0.; 128][..], abs_all <= 0.);
323    }
324
325    #[test]
326    fn test_start_in_the_past() {
327        let sample_rate = 48000.;
328        let mut context = OfflineAudioContext::new(1, 2 * 128, sample_rate);
329
330        context.suspend_sync((128. / sample_rate).into(), |context| {
331            let mut src = context.create_constant_source();
332            src.connect(&context.destination());
333            src.start_at(0.);
334        });
335
336        let buffer = context.start_rendering_sync();
337        let channel = buffer.get_channel_data(0);
338
339        // 1rst block should be silence
340        assert_float_eq!(channel[0..128], vec![0.; 128][..], abs_all <= 0.);
341        assert_float_eq!(channel[128..], vec![1.; 128][..], abs_all <= 0.);
342    }
343
344    #[test]
345    fn test_start_in_the_future_while_dropped() {
346        let sample_rate = 48000.;
347        let mut context = OfflineAudioContext::new(1, 4 * 128, sample_rate);
348
349        let mut src = context.create_constant_source();
350        src.connect(&context.destination());
351        src.start_at(258. / sample_rate as f64); // in 3rd block
352        drop(src); // explicit drop
353
354        let buffer = context.start_rendering_sync();
355        let channel = buffer.get_channel_data(0);
356
357        assert_float_eq!(channel[0..258], vec![0.; 258][..], abs_all <= 0.);
358        assert_float_eq!(channel[258..], vec![1.; 254][..], abs_all <= 0.);
359    }
360}