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!(
126            self.has_start,
127            "InvalidStateError - cannot stop before start"
128        );
129
130        self.registration.post_message(Schedule::Stop(when));
131    }
132}
133
134impl ConstantSourceNode {
135    /// Constructs a new `ConstantSourceNode` from explicit options.
136    ///
137    /// [`BaseAudioContext::create_constant_source`] is an alternative that
138    /// applies the spec defaults (`offset = 1.0`).
139    ///
140    /// # Arguments
141    ///
142    /// * `context` - audio context in which the audio node will live
143    /// * `options` - initial value of the offset parameter
144    pub fn new<C: BaseAudioContext>(context: &C, options: ConstantSourceOptions) -> Self {
145        context.base().register(move |registration| {
146            let ConstantSourceOptions { offset } = options;
147
148            let param_options = AudioParamDescriptor {
149                name: String::new(),
150                min_value: f32::MIN,
151                max_value: f32::MAX,
152                default_value: 1.,
153                automation_rate: AutomationRate::A,
154            };
155            let (param, proc) = context.create_audio_param(param_options, &registration);
156            param.set_value(offset);
157
158            let render = ConstantSourceRenderer {
159                offset: proc,
160                start_time: f64::MAX,
161                stop_time: f64::MAX,
162                ended_triggered: false,
163            };
164
165            let node = ConstantSourceNode {
166                registration,
167                channel_config: ChannelConfig::default(),
168                offset: param,
169                has_start: false,
170            };
171
172            (node, Box::new(render))
173        })
174    }
175
176    /// Returns the offset `AudioParam`. Default is `1.0`.
177    ///
178    /// Useful as a constructible `AudioParam`: connect this once to several
179    /// sink params and automate it to drive them all in lockstep.
180    #[must_use]
181    pub fn offset(&self) -> &AudioParam {
182        &self.offset
183    }
184}
185
186struct ConstantSourceRenderer {
187    offset: AudioParamId,
188    start_time: f64,
189    stop_time: f64,
190    ended_triggered: bool,
191}
192
193impl AudioProcessor for ConstantSourceRenderer {
194    fn process(
195        &mut self,
196        _inputs: &[AudioRenderQuantum],
197        outputs: &mut [AudioRenderQuantum],
198        params: AudioParamValues<'_>,
199        scope: &AudioWorkletGlobalScope,
200    ) -> bool {
201        // single output node
202        let output = &mut outputs[0];
203
204        let dt = 1. / scope.sample_rate as f64;
205        let next_block_time = scope.current_time + dt * RENDER_QUANTUM_SIZE as f64;
206
207        if self.start_time >= next_block_time {
208            output.make_silent();
209
210            if self.stop_time <= next_block_time {
211                if !self.ended_triggered {
212                    scope.send_ended_event();
213                    self.ended_triggered = true;
214                }
215
216                return false;
217            }
218
219            // #462 AudioScheduledSourceNodes that have not been scheduled to start can safely
220            // return tail_time false in order to be collected if their control handle drops.
221            return self.start_time != f64::MAX;
222        }
223
224        output.force_mono();
225
226        let offset = params.get(&self.offset);
227        let output_channel = output.channel_data_mut(0);
228
229        // fast path
230        if offset.len() == 1
231            && self.start_time <= scope.current_time
232            && self.stop_time >= next_block_time
233        {
234            output_channel.fill(offset[0]);
235        } else {
236            // sample accurate path
237            let mut current_time = scope.current_time;
238
239            output_channel
240                .iter_mut()
241                .zip(offset.iter().cycle())
242                .for_each(|(o, &value)| {
243                    if current_time < self.start_time || current_time >= self.stop_time {
244                        *o = 0.;
245                    } else {
246                        // as we pick values directly from the offset param which is already
247                        // computed at sub-sample accuracy, we don't need to do more than
248                        // copying the values to their right place.
249                        *o = value;
250                    }
251
252                    current_time += dt;
253                });
254        }
255
256        // tail_time false when output has ended this quantum
257        let still_running = self.stop_time > next_block_time;
258
259        if !still_running {
260            // @note: we need this check because this is called a until the program
261            // ends, such as if the node was never removed from the graph
262            if !self.ended_triggered {
263                scope.send_ended_event();
264                self.ended_triggered = true;
265            }
266        }
267
268        still_running
269    }
270
271    fn onmessage(&mut self, msg: &mut dyn Any) {
272        if let Some(schedule) = msg.downcast_ref::<Schedule>() {
273            match *schedule {
274                Schedule::Start(v) => self.start_time = v,
275                Schedule::Stop(v) => self.stop_time = v,
276            }
277            return;
278        }
279
280        log::warn!("ConstantSourceRenderer: Dropping incoming message {msg:?}");
281    }
282
283    fn before_drop(&mut self, scope: &AudioWorkletGlobalScope) {
284        if !self.ended_triggered
285            && (scope.current_time >= self.start_time || scope.current_time >= self.stop_time)
286        {
287            scope.send_ended_event();
288            self.ended_triggered = true;
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use crate::context::{BaseAudioContext, OfflineAudioContext};
296    use crate::node::{AudioNode, AudioScheduledSourceNode};
297
298    use float_eq::assert_float_eq;
299
300    use super::*;
301
302    #[test]
303    fn test_audioparam_value_applies_immediately() {
304        let context = OfflineAudioContext::new(1, 128, 48000.);
305        let options = ConstantSourceOptions { offset: 12. };
306        let src = ConstantSourceNode::new(&context, options);
307        assert_float_eq!(src.offset.value(), 12., abs_all <= 0.);
308    }
309
310    #[test]
311    fn test_start_stop() {
312        let sample_rate = 48000.;
313        let start_in_samples = (128 + 1) as f64; // start rendering in 2d block
314        let stop_in_samples = (256 + 1) as f64; // stop rendering of 3rd block
315        let mut context = OfflineAudioContext::new(1, 128 * 4, sample_rate);
316
317        let mut src = context.create_constant_source();
318        src.connect(&context.destination());
319
320        src.start_at(start_in_samples / sample_rate as f64);
321        src.stop_at(stop_in_samples / sample_rate as f64);
322
323        let buffer = context.start_rendering_sync();
324        let channel = buffer.get_channel_data(0);
325
326        // 1rst block should be silence
327        assert_float_eq!(channel[0..128], vec![0.; 128][..], abs_all <= 0.);
328
329        // 2d block - start at second frame
330        let mut res = vec![1.; 128];
331        res[0] = 0.;
332        assert_float_eq!(channel[128..256], res[..], abs_all <= 0.);
333
334        // 3rd block - stop at second frame
335        let mut res = vec![0.; 128];
336        res[0] = 1.;
337        assert_float_eq!(channel[256..384], res[..], abs_all <= 0.);
338
339        // 4th block is silence
340        assert_float_eq!(channel[384..512], vec![0.; 128][..], abs_all <= 0.);
341    }
342
343    #[test]
344    fn test_start_in_the_past() {
345        let sample_rate = 48000.;
346        let mut context = OfflineAudioContext::new(1, 2 * 128, sample_rate);
347
348        context.suspend_sync((128. / sample_rate).into(), |context| {
349            let mut src = context.create_constant_source();
350            src.connect(&context.destination());
351            src.start_at(0.);
352        });
353
354        let buffer = context.start_rendering_sync();
355        let channel = buffer.get_channel_data(0);
356
357        // 1rst block should be silence
358        assert_float_eq!(channel[0..128], vec![0.; 128][..], abs_all <= 0.);
359        assert_float_eq!(channel[128..], vec![1.; 128][..], abs_all <= 0.);
360    }
361
362    #[test]
363    fn test_start_in_the_future_while_dropped() {
364        let sample_rate = 48000.;
365        let mut context = OfflineAudioContext::new(1, 4 * 128, sample_rate);
366
367        let mut src = context.create_constant_source();
368        src.connect(&context.destination());
369        src.start_at(258. / sample_rate as f64); // in 3rd block
370        drop(src); // explicit drop
371
372        let buffer = context.start_rendering_sync();
373        let channel = buffer.get_channel_data(0);
374
375        assert_float_eq!(channel[0..258], vec![0.; 258][..], abs_all <= 0.);
376        assert_float_eq!(channel[258..], vec![1.; 254][..], abs_all <= 0.);
377    }
378}