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