Skip to main content

script/dom/audio/
audioparam.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::sync::mpsc;
7
8use dom_struct::dom_struct;
9use js::context::JSContext;
10use script_bindings::cformat;
11use script_bindings::reflector::{Reflector, reflect_dom_object};
12use servo_media::audio::audio_node::{AudioNodeMessage, AudioNodeType};
13use servo_media::audio::graph::NodeId;
14use servo_media::audio::param::{ParamRate, ParamType, RampKind, UserAutomationEvent};
15
16use crate::conversions::Convert;
17use crate::dom::audio::baseaudiocontext::BaseAudioContext;
18use crate::dom::bindings::codegen::Bindings::AudioParamBinding::{
19    AudioParamMethods, AutomationRate,
20};
21use crate::dom::bindings::error::{Error, Fallible};
22use crate::dom::bindings::num::Finite;
23use crate::dom::bindings::root::{Dom, DomRoot};
24use crate::dom::window::Window;
25
26#[dom_struct]
27pub(crate) struct AudioParam {
28    reflector_: Reflector,
29    context: Dom<BaseAudioContext>,
30    #[no_trace]
31    node: Option<NodeId>,
32    #[no_trace]
33    node_type: AudioNodeType,
34    #[no_trace]
35    param: ParamType,
36    automation_rate: Cell<AutomationRate>,
37    default_value: f32,
38    min_value: f32,
39    max_value: f32,
40}
41
42impl AudioParam {
43    #[expect(clippy::too_many_arguments)]
44    pub(crate) fn new_inherited(
45        context: &BaseAudioContext,
46        node: Option<NodeId>,
47        node_type: AudioNodeType,
48        param: ParamType,
49        automation_rate: AutomationRate,
50        default_value: f32,
51        min_value: f32,
52        max_value: f32,
53    ) -> AudioParam {
54        AudioParam {
55            reflector_: Reflector::new(),
56            context: Dom::from_ref(context),
57            node,
58            node_type,
59            param,
60            automation_rate: Cell::new(automation_rate),
61            default_value,
62            min_value,
63            max_value,
64        }
65    }
66
67    #[expect(clippy::too_many_arguments)]
68    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
69    pub(crate) fn new(
70        cx: &mut JSContext,
71        window: &Window,
72        context: &BaseAudioContext,
73        node: Option<NodeId>,
74        node_type: AudioNodeType,
75        param: ParamType,
76        automation_rate: AutomationRate,
77        default_value: f32,
78        min_value: f32,
79        max_value: f32,
80    ) -> DomRoot<AudioParam> {
81        let audio_param = AudioParam::new_inherited(
82            context,
83            node,
84            node_type,
85            param,
86            automation_rate,
87            default_value,
88            min_value,
89            max_value,
90        );
91        // Update the value range
92        audio_param.message_node(AudioNodeMessage::SetParamRange(
93            audio_param.param,
94            (min_value, max_value),
95        ));
96        reflect_dom_object(cx, Box::new(audio_param), window)
97    }
98
99    fn message_node(&self, message: AudioNodeMessage) {
100        if let Some(node_id) = self.node {
101            self.context
102                .audio_context_impl()
103                .lock()
104                .unwrap()
105                .message_node(node_id, message);
106        }
107    }
108
109    pub(crate) fn context(&self) -> &BaseAudioContext {
110        &self.context
111    }
112
113    pub(crate) fn node_id(&self) -> Option<NodeId> {
114        self.node
115    }
116
117    pub(crate) fn param_type(&self) -> ParamType {
118        self.param
119    }
120}
121
122impl AudioParamMethods<crate::DomTypeHolder> for AudioParam {
123    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-automationrate>
124    fn AutomationRate(&self) -> AutomationRate {
125        self.automation_rate.get()
126    }
127
128    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-automationrate>
129    fn SetAutomationRate(&self, automation_rate: AutomationRate) -> Fallible<()> {
130        // > AudioBufferSourceNode
131        // > The AudioParams playbackRate and detune MUST be "k-rate". An InvalidStateError must be
132        // > thrown if the rate is changed to "a-rate".
133        if automation_rate == AutomationRate::A_rate &&
134            self.node_type == AudioNodeType::AudioBufferSourceNode &&
135            (self.param == ParamType::Detune || self.param == ParamType::PlaybackRate)
136        {
137            return Err(Error::InvalidState(None));
138        }
139
140        self.automation_rate.set(automation_rate);
141        self.message_node(AudioNodeMessage::SetParamRate(
142            self.param,
143            automation_rate.convert(),
144        ));
145
146        Ok(())
147    }
148
149    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-value>
150    fn Value(&self) -> Finite<f32> {
151        if self.node.is_none() {
152            return Finite::wrap(self.default_value);
153        }
154        let (tx, rx) = mpsc::channel();
155        self.message_node(AudioNodeMessage::GetParamValue(self.param, tx));
156        Finite::wrap(rx.recv().unwrap())
157    }
158
159    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-value>
160    fn SetValue(&self, value: Finite<f32>) {
161        self.message_node(AudioNodeMessage::SetParam(
162            self.param,
163            UserAutomationEvent::SetValue(*value),
164        ));
165    }
166
167    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-defaultvalue>
168    fn DefaultValue(&self) -> Finite<f32> {
169        Finite::wrap(self.default_value)
170    }
171
172    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-minvalue>
173    fn MinValue(&self) -> Finite<f32> {
174        Finite::wrap(self.min_value)
175    }
176
177    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-maxvalue>
178    fn MaxValue(&self) -> Finite<f32> {
179        Finite::wrap(self.max_value)
180    }
181
182    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-setvalueattime>
183    fn SetValueAtTime(
184        &self,
185        value: Finite<f32>,
186        start_time: Finite<f64>,
187    ) -> Fallible<DomRoot<AudioParam>> {
188        if *start_time < 0. {
189            return Err(Error::Range(cformat!(
190                "start time {} should not be negative",
191                *start_time
192            )));
193        }
194        self.message_node(AudioNodeMessage::SetParam(
195            self.param,
196            UserAutomationEvent::SetValueAtTime(*value, *start_time),
197        ));
198        Ok(DomRoot::from_ref(self))
199    }
200
201    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-linearramptovalueattime>
202    fn LinearRampToValueAtTime(
203        &self,
204        value: Finite<f32>,
205        end_time: Finite<f64>,
206    ) -> Fallible<DomRoot<AudioParam>> {
207        if *end_time < 0. {
208            return Err(Error::Range(cformat!(
209                "end time {} should not be negative",
210                *end_time
211            )));
212        }
213        self.message_node(AudioNodeMessage::SetParam(
214            self.param,
215            UserAutomationEvent::RampToValueAtTime(RampKind::Linear, *value, *end_time),
216        ));
217        Ok(DomRoot::from_ref(self))
218    }
219
220    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-exponentialramptovalueattime>
221    fn ExponentialRampToValueAtTime(
222        &self,
223        value: Finite<f32>,
224        end_time: Finite<f64>,
225    ) -> Fallible<DomRoot<AudioParam>> {
226        if *end_time < 0. {
227            return Err(Error::Range(cformat!(
228                "end time {} should not be negative",
229                *end_time
230            )));
231        }
232        if *value == 0. {
233            return Err(Error::Range(cformat!(
234                "target value {} should not be 0",
235                *value
236            )));
237        }
238        self.message_node(AudioNodeMessage::SetParam(
239            self.param,
240            UserAutomationEvent::RampToValueAtTime(RampKind::Exponential, *value, *end_time),
241        ));
242        Ok(DomRoot::from_ref(self))
243    }
244
245    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-settargetattime>
246    fn SetTargetAtTime(
247        &self,
248        target: Finite<f32>,
249        start_time: Finite<f64>,
250        time_constant: Finite<f32>,
251    ) -> Fallible<DomRoot<AudioParam>> {
252        if *start_time < 0. {
253            return Err(Error::Range(cformat!(
254                "start time {} should not be negative",
255                *start_time
256            )));
257        }
258        if *time_constant < 0. {
259            return Err(Error::Range(cformat!(
260                "time constant {} should not be negative",
261                *time_constant
262            )));
263        }
264        self.message_node(AudioNodeMessage::SetParam(
265            self.param,
266            UserAutomationEvent::SetTargetAtTime(*target, *start_time, (*time_constant).into()),
267        ));
268        Ok(DomRoot::from_ref(self))
269    }
270
271    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-setvaluecurveattime>
272    fn SetValueCurveAtTime(
273        &self,
274        values: Vec<Finite<f32>>,
275        start_time: Finite<f64>,
276        end_time: Finite<f64>,
277    ) -> Fallible<DomRoot<AudioParam>> {
278        if *start_time < 0. {
279            return Err(Error::Range(cformat!(
280                "start time {} should not be negative",
281                *start_time
282            )));
283        }
284        if values.len() < 2. as usize {
285            return Err(Error::InvalidState(None));
286        }
287
288        if *end_time < 0. {
289            return Err(Error::Range(cformat!(
290                "end time {} should not be negative",
291                *end_time
292            )));
293        }
294        self.message_node(AudioNodeMessage::SetParam(
295            self.param,
296            UserAutomationEvent::SetValueCurveAtTime(
297                values.into_iter().map(|v| *v).collect(),
298                *start_time,
299                *end_time,
300            ),
301        ));
302        Ok(DomRoot::from_ref(self))
303    }
304
305    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelscheduledvalues>
306    fn CancelScheduledValues(&self, cancel_time: Finite<f64>) -> Fallible<DomRoot<AudioParam>> {
307        if *cancel_time < 0. {
308            return Err(Error::Range(cformat!(
309                "cancel time {} should not be negative",
310                *cancel_time
311            )));
312        }
313        self.message_node(AudioNodeMessage::SetParam(
314            self.param,
315            UserAutomationEvent::CancelScheduledValues(*cancel_time),
316        ));
317        Ok(DomRoot::from_ref(self))
318    }
319
320    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelandholdattime>
321    fn CancelAndHoldAtTime(&self, cancel_time: Finite<f64>) -> Fallible<DomRoot<AudioParam>> {
322        if *cancel_time < 0. {
323            return Err(Error::Range(cformat!(
324                "cancel time {} should not be negative",
325                *cancel_time
326            )));
327        }
328        self.message_node(AudioNodeMessage::SetParam(
329            self.param,
330            UserAutomationEvent::CancelAndHoldAtTime(*cancel_time),
331        ));
332        Ok(DomRoot::from_ref(self))
333    }
334}
335
336// https://webaudio.github.io/web-audio-api/#enumdef-automationrate
337impl Convert<ParamRate> for AutomationRate {
338    fn convert(self) -> ParamRate {
339        match self {
340            AutomationRate::A_rate => ParamRate::ARate,
341            AutomationRate::K_rate => ParamRate::KRate,
342        }
343    }
344}