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(Some(
138                "This AudioParam must be \"k-rate\"".into(),
139            )));
140        }
141
142        self.automation_rate.set(automation_rate);
143        self.message_node(AudioNodeMessage::SetParamRate(
144            self.param,
145            automation_rate.convert(),
146        ));
147
148        Ok(())
149    }
150
151    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-value>
152    fn Value(&self) -> Finite<f32> {
153        if self.node.is_none() {
154            return Finite::wrap(self.default_value);
155        }
156        let (tx, rx) = mpsc::channel();
157        self.message_node(AudioNodeMessage::GetParamValue(self.param, tx));
158        Finite::wrap(rx.recv().unwrap())
159    }
160
161    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-value>
162    fn SetValue(&self, value: Finite<f32>) {
163        self.message_node(AudioNodeMessage::SetParam(
164            self.param,
165            UserAutomationEvent::SetValue(*value),
166        ));
167    }
168
169    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-defaultvalue>
170    fn DefaultValue(&self) -> Finite<f32> {
171        Finite::wrap(self.default_value)
172    }
173
174    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-minvalue>
175    fn MinValue(&self) -> Finite<f32> {
176        Finite::wrap(self.min_value)
177    }
178
179    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-maxvalue>
180    fn MaxValue(&self) -> Finite<f32> {
181        Finite::wrap(self.max_value)
182    }
183
184    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-setvalueattime>
185    fn SetValueAtTime(
186        &self,
187        value: Finite<f32>,
188        start_time: Finite<f64>,
189    ) -> Fallible<DomRoot<AudioParam>> {
190        if *start_time < 0. {
191            return Err(Error::Range(cformat!(
192                "start time {} should not be negative",
193                *start_time
194            )));
195        }
196        self.message_node(AudioNodeMessage::SetParam(
197            self.param,
198            UserAutomationEvent::SetValueAtTime(*value, *start_time),
199        ));
200        Ok(DomRoot::from_ref(self))
201    }
202
203    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-linearramptovalueattime>
204    fn LinearRampToValueAtTime(
205        &self,
206        value: Finite<f32>,
207        end_time: Finite<f64>,
208    ) -> Fallible<DomRoot<AudioParam>> {
209        if *end_time < 0. {
210            return Err(Error::Range(cformat!(
211                "end time {} should not be negative",
212                *end_time
213            )));
214        }
215        self.message_node(AudioNodeMessage::SetParam(
216            self.param,
217            UserAutomationEvent::RampToValueAtTime(RampKind::Linear, *value, *end_time),
218        ));
219        Ok(DomRoot::from_ref(self))
220    }
221
222    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-exponentialramptovalueattime>
223    fn ExponentialRampToValueAtTime(
224        &self,
225        value: Finite<f32>,
226        end_time: Finite<f64>,
227    ) -> Fallible<DomRoot<AudioParam>> {
228        if *end_time < 0. {
229            return Err(Error::Range(cformat!(
230                "end time {} should not be negative",
231                *end_time
232            )));
233        }
234        if *value == 0. {
235            return Err(Error::Range(cformat!(
236                "target value {} should not be 0",
237                *value
238            )));
239        }
240        self.message_node(AudioNodeMessage::SetParam(
241            self.param,
242            UserAutomationEvent::RampToValueAtTime(RampKind::Exponential, *value, *end_time),
243        ));
244        Ok(DomRoot::from_ref(self))
245    }
246
247    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-settargetattime>
248    fn SetTargetAtTime(
249        &self,
250        target: Finite<f32>,
251        start_time: Finite<f64>,
252        time_constant: Finite<f32>,
253    ) -> Fallible<DomRoot<AudioParam>> {
254        if *start_time < 0. {
255            return Err(Error::Range(cformat!(
256                "start time {} should not be negative",
257                *start_time
258            )));
259        }
260        if *time_constant < 0. {
261            return Err(Error::Range(cformat!(
262                "time constant {} should not be negative",
263                *time_constant
264            )));
265        }
266        self.message_node(AudioNodeMessage::SetParam(
267            self.param,
268            UserAutomationEvent::SetTargetAtTime(*target, *start_time, (*time_constant).into()),
269        ));
270        Ok(DomRoot::from_ref(self))
271    }
272
273    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-setvaluecurveattime>
274    fn SetValueCurveAtTime(
275        &self,
276        values: Vec<Finite<f32>>,
277        start_time: Finite<f64>,
278        end_time: Finite<f64>,
279    ) -> Fallible<DomRoot<AudioParam>> {
280        if *start_time < 0. {
281            return Err(Error::Range(cformat!(
282                "start time {} should not be negative",
283                *start_time
284            )));
285        }
286        if values.len() < 2. as usize {
287            return Err(Error::InvalidState(Some(
288                "Values must contain at least 2 elements".into(),
289            )));
290        }
291
292        if *end_time < 0. {
293            return Err(Error::Range(cformat!(
294                "end time {} should not be negative",
295                *end_time
296            )));
297        }
298        self.message_node(AudioNodeMessage::SetParam(
299            self.param,
300            UserAutomationEvent::SetValueCurveAtTime(
301                values.into_iter().map(|v| *v).collect(),
302                *start_time,
303                *end_time,
304            ),
305        ));
306        Ok(DomRoot::from_ref(self))
307    }
308
309    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelscheduledvalues>
310    fn CancelScheduledValues(&self, cancel_time: Finite<f64>) -> Fallible<DomRoot<AudioParam>> {
311        if *cancel_time < 0. {
312            return Err(Error::Range(cformat!(
313                "cancel time {} should not be negative",
314                *cancel_time
315            )));
316        }
317        self.message_node(AudioNodeMessage::SetParam(
318            self.param,
319            UserAutomationEvent::CancelScheduledValues(*cancel_time),
320        ));
321        Ok(DomRoot::from_ref(self))
322    }
323
324    /// <https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelandholdattime>
325    fn CancelAndHoldAtTime(&self, cancel_time: Finite<f64>) -> Fallible<DomRoot<AudioParam>> {
326        if *cancel_time < 0. {
327            return Err(Error::Range(cformat!(
328                "cancel time {} should not be negative",
329                *cancel_time
330            )));
331        }
332        self.message_node(AudioNodeMessage::SetParam(
333            self.param,
334            UserAutomationEvent::CancelAndHoldAtTime(*cancel_time),
335        ));
336        Ok(DomRoot::from_ref(self))
337    }
338}
339
340// https://webaudio.github.io/web-audio-api/#enumdef-automationrate
341impl Convert<ParamRate> for AutomationRate {
342    fn convert(self) -> ParamRate {
343        match self {
344            AutomationRate::A_rate => ParamRate::ARate,
345            AutomationRate::K_rate => ParamRate::KRate,
346        }
347    }
348}