euv_engine/audio/impl.rs
1use super::*;
2
3/// Implements audio context management for `GameAudioContext`.
4impl GameAudioContext {
5 /// Creates a new audio context with default master volume.
6 ///
7 /// # Returns
8 ///
9 /// - `Option<GameAudioContext>` - The new audio context, or `None` when
10 /// the browser does not expose a WebAudio `AudioContext`.
11 pub fn create() -> Option<GameAudioContext> {
12 let Ok(context) = AudioContext::new() else {
13 return None;
14 };
15 let Ok(master_gain) = context.create_gain() else {
16 return None;
17 };
18 let _: Result<AudioNode, JsValue> =
19 master_gain.connect_with_audio_node(&context.destination());
20 master_gain.gain().set_value(AUDIO_DEFAULT_VOLUME as f32);
21 Some(GameAudioContext::new(context, master_gain))
22 }
23
24 /// Sets the master volume for all audio output.
25 ///
26 /// # Arguments
27 ///
28 /// - `f64` - The volume level in the range 0.0 to 1.0.
29 pub fn apply_master_volume(&self, volume: f64) {
30 let clamped: f64 = Numeric::clamp(volume, 0.0, 1.0);
31 self.get_master_gain().gain().set_value(clamped as f32);
32 }
33
34 /// Resumes the audio context if it was suspended.
35 pub fn resume(&self) {
36 let _: Result<Promise, JsValue> = self.get_context().resume();
37 }
38
39 /// Suspends the audio context, temporarily halting all audio processing.
40 pub fn suspend(&self) {
41 let _: Result<Promise, JsValue> = self.get_context().suspend();
42 }
43
44 /// Closes the audio context and releases all resources.
45 pub fn close(&self) {
46 let _: Result<Promise, JsValue> = self.get_context().close();
47 }
48
49 /// Returns the current sample rate of the audio context in Hz.
50 ///
51 /// # Returns
52 ///
53 /// - `f64` - The sample rate.
54 pub fn sample_rate(&self) -> f64 {
55 self.get_context().sample_rate() as f64
56 }
57
58 /// Returns the current playback time in seconds.
59 ///
60 /// # Returns
61 ///
62 /// - `f64` - The current time.
63 pub fn current_time(&self) -> f64 {
64 self.get_context().current_time()
65 }
66}
67
68/// Implements playback control for `AudioClip`.
69impl AudioClip {
70 /// Creates a new audio clip from a decoded buffer.
71 ///
72 /// A clip carries no name: playback is driven by `play` / `stop`, which
73 /// address a clip by identity rather than by label, so a name field would
74 /// only ever be stored and never read.
75 ///
76 /// # Arguments
77 ///
78 /// - `AudioBuffer` - The decoded audio data.
79 ///
80 /// # Returns
81 ///
82 /// - `AudioClip` - The new clip.
83 pub fn create(buffer: AudioBuffer) -> AudioClip {
84 AudioClip::new(
85 buffer,
86 AudioPlayState::Stopped,
87 AUDIO_DEFAULT_LOOP,
88 AUDIO_DEFAULT_VOLUME,
89 AUDIO_DEFAULT_PLAYBACK_RATE,
90 )
91 }
92
93 /// Plays this clip through the given audio context.
94 ///
95 /// Creates a new `AudioBufferSourceNode`, connects it through a gain node
96 /// to the master gain, and starts playback. If already playing, does nothing.
97 ///
98 /// # Arguments
99 ///
100 /// - `&GameAudioContext` - The audio context to play through.
101 pub fn play(&mut self, audio_context: &GameAudioContext) {
102 if self.get_state() == AudioPlayState::Playing {
103 return;
104 }
105 let Ok(source) = audio_context.get_context().create_buffer_source() else {
106 return;
107 };
108 source.set_buffer(Some(self.get_buffer()));
109 source.set_loop(self.get_looping());
110 source
111 .playback_rate()
112 .set_value(self.get_playback_rate() as f32);
113 let Ok(gain) = audio_context.get_context().create_gain() else {
114 return;
115 };
116 gain.gain().set_value(self.get_volume() as f32);
117 let _: Result<AudioNode, JsValue> = source.connect_with_audio_node(&gain);
118 let _: Result<AudioNode, JsValue> =
119 gain.connect_with_audio_node(audio_context.get_master_gain());
120 let _: Result<(), JsValue> = source.start();
121 self.set_source_node(Some(source));
122 self.set_state(AudioPlayState::Playing);
123 }
124
125 /// Stops playback of this clip and releases the source node.
126 pub fn stop(&mut self) {
127 if let Some(source) = self.get_mut_source_node().take() {
128 let scheduled: &AudioScheduledSourceNode = source.as_ref();
129 let _: Result<(), JsValue> = scheduled.stop_with_when(0.0);
130 let _: Result<(), JsValue> = source.disconnect();
131 }
132 self.set_state(AudioPlayState::Stopped);
133 }
134
135 /// Sets whether the clip should loop.
136 ///
137 /// # Arguments
138 ///
139 /// - `bool` - True to enable looping.
140 pub fn update_looping(&mut self, looping: bool) {
141 self.set_looping(looping);
142 if let Some(source) = self.get_mut_source_node() {
143 source.set_loop(looping);
144 }
145 }
146
147 /// Sets the volume of this clip.
148 ///
149 /// # Arguments
150 ///
151 /// - `f64` - The volume in the range 0.0 to 1.0.
152 pub fn update_volume(&mut self, volume: f64) {
153 self.set_volume(Numeric::clamp(volume, 0.0, 1.0));
154 }
155
156 /// Sets the playback rate multiplier.
157 ///
158 /// # Arguments
159 ///
160 /// - `f64` - The playback rate (1.0 = normal speed).
161 pub fn update_playback_rate(&mut self, rate: f64) {
162 self.set_playback_rate(rate);
163 if let Some(source) = self.get_mut_source_node() {
164 source.playback_rate().set_value(rate as f32);
165 }
166 }
167
168 /// Returns the duration of the audio buffer in seconds.
169 ///
170 /// # Returns
171 ///
172 /// - `f64` - The duration.
173 pub fn duration(&self) -> f64 {
174 self.get_buffer().duration()
175 }
176
177 /// Returns the number of channels in the audio buffer.
178 ///
179 /// # Returns
180 ///
181 /// - `u32` - The channel count.
182 pub fn channel_count(&self) -> u32 {
183 self.get_buffer().number_of_channels()
184 }
185}