Skip to main content

banished_audio/
device.rs

1//! A pool of concurrent voices, after the original `Audio::Device` (Runtime DLL): 64 channels
2//! (`0x28` bytes each, flag bit 0 = in use, bit 1 = ended). `Device::Play` takes the first free
3//! channel and returns null when all are busy; `PlayPlatform` starts one XAudio2 source voice at
4//! `offset % duration`, with the given volume and a frequency ratio of `1.05946314^pitch` (pitch in
5//! semitones), looping when the sound buffer's loop flag is set. `Device::Update` frees ended
6//! channels and calls their end callbacks; `Device::Stop` stops, frees and calls back.
7//!
8//! The port plays every channel at once through rodio's mixer natively and through one Web Audio
9//! graph (a gain node per channel into a master gain) in the browser. The end callback becomes the
10//! list of channels `update` returns.
11//!
12//! Original: Runtime `0x1800e0ad0`, Runtime `0x1800e0ff0`, Runtime `0x1800e0d70`, Runtime
13//! `0x1800e0a50`.
14
15use std::sync::Arc;
16use std::time::Duration;
17
18use crate::Pcm;
19
20/// `Audio::Device` channel count (loops in `FindFreeChannel`, `Play`, `Update`).
21pub const CHANNELS: usize = 64;
22
23/// `DAT_180164810`: one semitone (the twelfth root of two).
24const SEMITONE: f32 = 1.059_463_1;
25
26/// The playback speed for a pitch in semitones: one semitone is the twelfth root of two, as
27/// XAudio2's frequency ratio.
28///
29/// Original: `FUN_180129df0(1.05946314, pitch)`.
30pub fn pitch_ratio(semitones: f32) -> f32 {
31    SEMITONE.powf(semitones)
32}
33
34/// A decoded sound buffer (`Audio::SoundBuffer`) that any number of channels can play at once;
35/// the samples are shared, not copied, per channel.
36#[derive(Clone)]
37pub struct Sound {
38    pcm: Arc<Pcm>,
39    // SoundBuffer +0x20
40    /// Whether the sound loops.
41    pub looping: bool,
42    #[cfg(target_arch = "wasm32")]
43    web: std::rc::Rc<std::cell::RefCell<Option<web_sys::AudioBuffer>>>,
44}
45
46impl Sound {
47    /// A sound from decoded samples and its loop flag.
48    pub fn new(pcm: Pcm, looping: bool) -> Sound {
49        Sound {
50            pcm: Arc::new(pcm),
51            looping,
52            #[cfg(target_arch = "wasm32")]
53            web: Default::default(),
54        }
55    }
56
57    /// Load and decode a sound by resource reference (see [`crate::load_sound`]).
58    pub fn load(
59        archive: &banished_formats::pkg::Archive,
60        reference: &str,
61    ) -> Result<Sound, String> {
62        let (pcm, looping) = crate::load_sound(archive, reference)?;
63        Ok(Sound::new(pcm, looping))
64    }
65
66    /// The decoded samples.
67    pub fn pcm(&self) -> &Pcm {
68        &self.pcm
69    }
70
71    /// The length in frames (samples per channel).
72    pub fn frames(&self) -> usize {
73        self.pcm.samples.len() / self.pcm.channels.max(1) as usize
74    }
75
76    /// The length.
77    pub fn duration(&self) -> Duration {
78        Duration::from_secs_f64(self.frames() as f64 / self.pcm.sample_rate.max(1) as f64)
79    }
80
81    /// `PlayPlatform`'s start point: the requested time wrapped into the sound.
82    pub fn wrap(&self, offset: Duration) -> Duration {
83        let d = self.duration().as_secs_f64();
84        if d <= 0.0 {
85            return Duration::ZERO;
86        }
87        Duration::from_secs_f64(offset.as_secs_f64() % d)
88    }
89}
90
91/// A playing channel. Handles carry a generation, so one kept after its channel was freed and
92/// reused refers to nothing (calls on it do nothing).
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub struct Channel {
95    index: u8,
96    generation: u32,
97}
98
99impl Channel {
100    /// The channel's number, 0 to 63.
101    pub fn index(self) -> usize {
102        self.index as usize
103    }
104
105    #[cfg(test)]
106    pub(crate) fn test(n: u32) -> Channel {
107        Channel {
108            index: (n % 64) as u8,
109            generation: n,
110        }
111    }
112}
113
114/// Channel bookkeeping without any output: first-free allocation and generations.
115#[derive(Debug)]
116pub struct Slots<T> {
117    slots: Vec<Option<(u32, T)>>,
118    generation: u32,
119}
120
121impl<T> Default for Slots<T> {
122    fn default() -> Self {
123        Slots {
124            slots: (0..CHANNELS).map(|_| None).collect(),
125            generation: 0,
126        }
127    }
128}
129
130impl<T> Slots<T> {
131    /// `FindFreeChannel`: the lowest free index, or `None` when all 64 are in use.
132    pub fn insert(&mut self, value: T) -> Option<Channel> {
133        let index = self.slots.iter().position(Option::is_none)?;
134        self.generation = self.generation.wrapping_add(1);
135        self.slots[index] = Some((self.generation, value));
136        Some(Channel {
137            index: index as u8,
138            generation: self.generation,
139        })
140    }
141
142    /// The value of a channel, or `None` when the handle is stale.
143    pub fn get(&self, c: Channel) -> Option<&T> {
144        match self.slots.get(c.index())? {
145            Some((g, v)) if *g == c.generation => Some(v),
146            _ => None,
147        }
148    }
149
150    /// The value of a channel, to change it, or `None` when the handle is stale.
151    pub fn get_mut(&mut self, c: Channel) -> Option<&mut T> {
152        match self.slots.get_mut(c.index())? {
153            Some((g, v)) if *g == c.generation => Some(v),
154            _ => None,
155        }
156    }
157
158    /// Free a channel and return its value.
159    pub fn remove(&mut self, c: Channel) -> Option<T> {
160        self.get(c)?;
161        self.slots[c.index()].take().map(|(_, v)| v)
162    }
163
164    /// The channels in use, with their values.
165    pub fn iter(&self) -> impl Iterator<Item = (Channel, &T)> {
166        self.slots.iter().enumerate().filter_map(|(i, s)| {
167            s.as_ref().map(|(g, v)| {
168                (
169                    Channel {
170                        index: i as u8,
171                        generation: *g,
172                    },
173                    v,
174                )
175            })
176        })
177    }
178
179    /// The number of channels in use.
180    pub fn len(&self) -> usize {
181        self.slots.iter().filter(|s| s.is_some()).count()
182    }
183
184    /// Whether no channel is in use.
185    pub fn is_empty(&self) -> bool {
186        self.len() == 0
187    }
188}
189
190/// How a channel starts (`Device::Play`'s time and floats).
191#[derive(Debug, Clone, Copy)]
192pub struct PlayParams {
193    /// Start time, wrapped into the sound.
194    pub offset: Duration,
195    /// The volume, from 0 to 1.
196    pub volume: f32,
197    /// Semitones.
198    pub pitch: f32,
199    /// Defaults to the sound's own loop flag.
200    pub looping: Option<bool>,
201}
202
203impl Default for PlayParams {
204    fn default() -> Self {
205        PlayParams {
206            offset: Duration::ZERO,
207            volume: 1.0,
208            pitch: 0.0,
209            looping: None,
210        }
211    }
212}
213
214#[cfg(not(target_arch = "wasm32"))]
215pub use native::Device;
216#[cfg(target_arch = "wasm32")]
217pub use web::Device;
218
219#[cfg(not(target_arch = "wasm32"))]
220mod native {
221    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
222    use std::sync::Arc;
223    use std::time::Duration;
224
225    use super::{pitch_ratio, Channel, PlayParams, Slots, Sound};
226
227    /// A rodio source over a shared buffer. Position and looping are shared with the channel,
228    /// so they can be read and changed while it plays.
229    pub(crate) struct SharedSource {
230        pub(crate) sound: Sound,
231        /// Next sample index (interleaved).
232        pub(crate) cursor: Arc<AtomicUsize>,
233        pub(crate) looping: Arc<AtomicBool>,
234    }
235
236    impl Iterator for SharedSource {
237        type Item = f32;
238
239        fn next(&mut self) -> Option<f32> {
240            let samples = &self.sound.pcm.samples;
241            let mut i = self.cursor.load(Ordering::Relaxed);
242            if i >= samples.len() {
243                if !self.looping.load(Ordering::Relaxed) || samples.is_empty() {
244                    return None;
245                }
246                i = 0;
247            }
248            self.cursor.store(i + 1, Ordering::Relaxed);
249            Some(samples[i])
250        }
251    }
252
253    impl rodio::Source for SharedSource {
254        fn current_span_len(&self) -> Option<usize> {
255            None
256        }
257
258        fn channels(&self) -> rodio::ChannelCount {
259            self.sound.pcm.channels.max(1)
260        }
261
262        fn sample_rate(&self) -> rodio::SampleRate {
263            self.sound.pcm.sample_rate.max(1)
264        }
265
266        fn total_duration(&self) -> Option<Duration> {
267            (!self.looping.load(Ordering::Relaxed)).then(|| self.sound.duration())
268        }
269
270        fn try_seek(&mut self, pos: Duration) -> Result<(), rodio::source::SeekError> {
271            let channels = self.sound.pcm.channels.max(1) as usize;
272            let frame = (pos.as_secs_f64() * self.sound.pcm.sample_rate as f64) as usize;
273            let i = (frame.min(self.sound.frames())) * channels;
274            self.cursor.store(i, Ordering::Relaxed);
275            Ok(())
276        }
277    }
278
279    struct Voice {
280        sink: rodio::Sink,
281        sound: Sound,
282        cursor: Arc<AtomicUsize>,
283        looping: Arc<AtomicBool>,
284        volume: f32,
285    }
286
287    /// The multi-channel output (rodio).
288    pub struct Device {
289        stream: Option<rodio::OutputStream>,
290        voices: Slots<Voice>,
291        master: f32,
292    }
293
294    impl Default for Device {
295        fn default() -> Self {
296            Device {
297                stream: None,
298                voices: Slots::default(),
299                master: 1.0,
300            }
301        }
302    }
303
304    impl Device {
305        fn mixer(&mut self) -> Result<&rodio::mixer::Mixer, String> {
306            if self.stream.is_none() {
307                let mut stream =
308                    rodio::OutputStreamBuilder::open_default_stream().map_err(|e| e.to_string())?;
309                stream.log_on_drop(false);
310                self.stream = Some(stream);
311            }
312            Ok(self.stream.as_ref().unwrap().mixer())
313        }
314
315        /// `Device::Play`: start `sound` on the first free channel. `Ok(None)` when all 64 are
316        /// busy (the original returns null); `Err` when there is no audio output.
317        pub fn play(
318            &mut self,
319            sound: &Sound,
320            params: PlayParams,
321        ) -> Result<Option<Channel>, String> {
322            if self.voices.len() >= super::CHANNELS {
323                return Ok(None);
324            }
325            let sink = rodio::Sink::connect_new(self.mixer()?);
326            let looping = Arc::new(AtomicBool::new(params.looping.unwrap_or(sound.looping)));
327            let channels = sound.pcm.channels.max(1) as usize;
328            let start = sound.wrap(params.offset);
329            let frame = (start.as_secs_f64() * sound.pcm.sample_rate as f64) as usize;
330            let cursor = Arc::new(AtomicUsize::new(frame.min(sound.frames()) * channels));
331            sink.set_volume((params.volume * self.master).max(0.0));
332            sink.set_speed(pitch_ratio(params.pitch));
333            sink.append(SharedSource {
334                sound: sound.clone(),
335                cursor: cursor.clone(),
336                looping: looping.clone(),
337            });
338            Ok(self.voices.insert(Voice {
339                sink,
340                sound: sound.clone(),
341                cursor,
342                looping,
343                volume: params.volume,
344            }))
345        }
346
347        /// `Device::Stop`.
348        pub fn stop(&mut self, c: Channel) {
349            if let Some(v) = self.voices.remove(c) {
350                v.sink.stop();
351            }
352        }
353
354        /// `Scene::StopAll`.
355        pub fn stop_all(&mut self) {
356            let all: Vec<_> = self.voices.iter().map(|(c, _)| c).collect();
357            for c in all {
358                self.stop(c);
359            }
360        }
361
362        /// `Device::Update`: free the channels that finished, returning them (the end
363        /// callbacks).
364        pub fn update(&mut self) -> Vec<Channel> {
365            let ended: Vec<_> = self
366                .voices
367                .iter()
368                .filter(|(_, v)| v.sink.empty())
369                .map(|(c, _)| c)
370                .collect();
371            for &c in &ended {
372                self.voices.remove(c);
373            }
374            ended
375        }
376
377        /// Whether the channel is still allocated and has not finished.
378        pub fn is_playing(&self, c: Channel) -> bool {
379            self.voices
380                .get(c)
381                .is_some_and(|v| !v.sink.empty() && !v.sink.is_paused())
382        }
383
384        /// Whether the channel is allocated and has not finished, paused or not.
385        pub fn is_active(&self, c: Channel) -> bool {
386            self.voices.get(c).is_some_and(|v| !v.sink.empty())
387        }
388
389        /// The number of channels in use.
390        pub fn active(&self) -> usize {
391            self.voices.len()
392        }
393
394        /// Pause a channel. The other channels go on.
395        pub fn pause(&mut self, c: Channel) {
396            if let Some(v) = self.voices.get(c) {
397                v.sink.pause();
398            }
399        }
400
401        /// Resume a paused channel.
402        pub fn resume(&mut self, c: Channel) {
403            if let Some(v) = self.voices.get(c) {
404                v.sink.play();
405            }
406        }
407
408        /// Whether the channel is paused.
409        pub fn is_paused(&self, c: Channel) -> bool {
410            self.voices.get(c).is_some_and(|v| v.sink.is_paused())
411        }
412
413        /// Position in the sound (wrapped for looping channels).
414        pub fn position(&self, c: Channel) -> Duration {
415            let Some(v) = self.voices.get(c) else {
416                return Duration::ZERO;
417            };
418            let channels = v.sound.pcm.channels.max(1) as usize;
419            let frame = v.cursor.load(Ordering::Relaxed) / channels;
420            Duration::from_secs_f64(frame as f64 / v.sound.pcm.sample_rate.max(1) as f64)
421                .min(v.sound.duration())
422        }
423
424        /// Move a channel to a position in its sound.
425        pub fn seek(&mut self, c: Channel, position: Duration) -> Result<(), String> {
426            if let Some(v) = self.voices.get(c) {
427                v.sink
428                    .try_seek(position.min(v.sound.duration()))
429                    .map_err(|e| e.to_string())?;
430            }
431            Ok(())
432        }
433
434        /// `Device::SetChannelVolume`.
435        pub fn set_volume(&mut self, c: Channel, volume: f32) {
436            let master = self.master;
437            if let Some(v) = self.voices.get_mut(c) {
438                v.volume = volume.max(0.0);
439                v.sink.set_volume(v.volume * master);
440            }
441        }
442
443        /// Change a channel's pitch, in semitones.
444        pub fn set_pitch(&mut self, c: Channel, semitones: f32) {
445            if let Some(v) = self.voices.get(c) {
446                v.sink.set_speed(pitch_ratio(semitones));
447            }
448        }
449
450        /// Change whether a channel loops.
451        pub fn set_looping(&mut self, c: Channel, looping: bool) {
452            if let Some(v) = self.voices.get(c) {
453                v.looping.store(looping, Ordering::Relaxed);
454            }
455        }
456
457        /// Scales every channel.
458        pub fn set_master_volume(&mut self, volume: f32) {
459            self.master = volume.max(0.0);
460            for (_, v) in self.voices.iter() {
461                v.sink.set_volume(v.volume * self.master);
462            }
463        }
464
465        /// Browsers start audio suspended until a user gesture; nothing to do natively.
466        pub fn unlock(&mut self) {}
467    }
468}
469
470#[cfg(target_arch = "wasm32")]
471mod web {
472    use std::time::Duration;
473
474    use super::{pitch_ratio, Channel, PlayParams, Slots, Sound};
475
476    fn err(e: wasm_bindgen::JsValue) -> String {
477        format!("{e:?}")
478    }
479
480    fn spawn(promise: Result<js_sys::Promise, wasm_bindgen::JsValue>) {
481        if let Ok(p) = promise {
482            wasm_bindgen_futures::spawn_local(async move {
483                let _ = wasm_bindgen_futures::JsFuture::from(p).await;
484            });
485        }
486    }
487
488    struct Voice {
489        sound: Sound,
490        source: Option<web_sys::AudioBufferSourceNode>,
491        gain: web_sys::GainNode,
492        looping: bool,
493        rate: f64,
494        volume: f32,
495        /// Sound time at `started_at` (or while paused).
496        offset: f64,
497        started_at: f64,
498        paused: bool,
499    }
500
501    /// The multi-channel output (Web Audio): one buffer source and gain node per channel,
502    /// into a master gain.
503    pub struct Device {
504        context: Option<web_sys::AudioContext>,
505        master_gain: Option<web_sys::GainNode>,
506        voices: Slots<Voice>,
507        master: f32,
508    }
509
510    impl Default for Device {
511        fn default() -> Self {
512            Device {
513                context: None,
514                master_gain: None,
515                voices: Slots::default(),
516                master: 1.0,
517            }
518        }
519    }
520
521    impl Device {
522        fn context(&mut self) -> Result<(web_sys::AudioContext, web_sys::GainNode), String> {
523            if self.context.is_none() {
524                let ctx = web_sys::AudioContext::new().map_err(err)?;
525                let gain = ctx.create_gain().map_err(err)?;
526                gain.gain().set_value(self.master);
527                gain.connect_with_audio_node(&ctx.destination())
528                    .map_err(err)?;
529                self.context = Some(ctx);
530                self.master_gain = Some(gain);
531            }
532            Ok((
533                self.context.clone().unwrap(),
534                self.master_gain.clone().unwrap(),
535            ))
536        }
537
538        fn buffer(
539            ctx: &web_sys::AudioContext,
540            sound: &Sound,
541        ) -> Result<web_sys::AudioBuffer, String> {
542            if let Some(b) = sound.web.borrow().as_ref() {
543                return Ok(b.clone());
544            }
545            let pcm = sound.pcm();
546            let channels = pcm.channels as usize;
547            if channels == 0
548                || pcm.sample_rate == 0
549                || pcm.samples.is_empty()
550                || pcm.samples.len() % channels != 0
551            {
552                return Err(
553                    "PCM must contain complete frames with a channel count and sample rate".into(),
554                );
555            }
556            let frames = u32::try_from(pcm.samples.len() / channels)
557                .map_err(|_| "PCM is too long for Web Audio")?;
558            let buffer = ctx
559                .create_buffer(channels as u32, frames, pcm.sample_rate as f32)
560                .map_err(err)?;
561            for ch in 0..channels {
562                let mut plane: Vec<f32> = pcm
563                    .samples
564                    .iter()
565                    .skip(ch)
566                    .step_by(channels)
567                    .copied()
568                    .collect();
569                buffer.copy_to_channel(&mut plane, ch as i32).map_err(err)?;
570            }
571            *sound.web.borrow_mut() = Some(buffer.clone());
572            Ok(buffer)
573        }
574
575        fn now(&self) -> f64 {
576            self.context.as_ref().map_or(0.0, |c| c.current_time())
577        }
578
579        fn start(ctx: &web_sys::AudioContext, v: &mut Voice) -> Result<(), String> {
580            let buffer = Self::buffer(ctx, &v.sound)?;
581            let source = ctx.create_buffer_source().map_err(err)?;
582            source.set_buffer(Some(&buffer));
583            source.set_loop(v.looping);
584            source.playback_rate().set_value(v.rate as f32);
585            source.connect_with_audio_node(&v.gain).map_err(err)?;
586            v.started_at = ctx.current_time();
587            source
588                .start_with_when_and_grain_offset(0.0, v.offset)
589                .map_err(err)?;
590            v.source = Some(source);
591            Ok(())
592        }
593
594        fn halt(v: &mut Voice) {
595            if let Some(s) = v.source.take() {
596                #[allow(deprecated)]
597                let _ = s.stop();
598                let _ = s.disconnect();
599            }
600        }
601
602        /// Unwrapped sound time of a voice.
603        fn time(now: f64, v: &Voice) -> f64 {
604            if v.paused || v.source.is_none() {
605                v.offset
606            } else {
607                v.offset + (now - v.started_at) * v.rate
608            }
609        }
610
611        fn ended(now: f64, v: &Voice) -> bool {
612            !v.looping && !v.paused && Self::time(now, v) >= v.sound.duration().as_secs_f64()
613        }
614
615        /// `Device::Play`: see the native backend.
616        pub fn play(
617            &mut self,
618            sound: &Sound,
619            params: PlayParams,
620        ) -> Result<Option<Channel>, String> {
621            if self.voices.len() >= super::CHANNELS {
622                return Ok(None);
623            }
624            let (ctx, master) = self.context()?;
625            let gain = ctx.create_gain().map_err(err)?;
626            gain.gain().set_value(params.volume.max(0.0));
627            gain.connect_with_audio_node(&master).map_err(err)?;
628            let mut v = Voice {
629                sound: sound.clone(),
630                source: None,
631                gain,
632                looping: params.looping.unwrap_or(sound.looping),
633                rate: pitch_ratio(params.pitch) as f64,
634                volume: params.volume,
635                offset: sound.wrap(params.offset).as_secs_f64(),
636                started_at: 0.0,
637                paused: false,
638            };
639            Self::start(&ctx, &mut v)?;
640            spawn(ctx.resume());
641            Ok(self.voices.insert(v))
642        }
643
644        pub fn stop(&mut self, c: Channel) {
645            if let Some(mut v) = self.voices.remove(c) {
646                Self::halt(&mut v);
647                let _ = v.gain.disconnect();
648            }
649        }
650
651        pub fn stop_all(&mut self) {
652            let all: Vec<_> = self.voices.iter().map(|(c, _)| c).collect();
653            for c in all {
654                self.stop(c);
655            }
656        }
657
658        pub fn update(&mut self) -> Vec<Channel> {
659            let now = self.now();
660            let ended: Vec<_> = self
661                .voices
662                .iter()
663                .filter(|(_, v)| Self::ended(now, v))
664                .map(|(c, _)| c)
665                .collect();
666            for &c in &ended {
667                self.stop(c);
668            }
669            ended
670        }
671
672        pub fn is_playing(&self, c: Channel) -> bool {
673            let now = self.now();
674            self.voices
675                .get(c)
676                .is_some_and(|v| !v.paused && !Self::ended(now, v))
677        }
678
679        /// Whether the channel is allocated and has not finished, paused or not.
680        pub fn is_active(&self, c: Channel) -> bool {
681            let now = self.now();
682            self.voices.get(c).is_some_and(|v| !Self::ended(now, v))
683        }
684
685        /// The number of channels in use.
686        pub fn active(&self) -> usize {
687            self.voices.len()
688        }
689
690        /// Pause a channel. The other channels go on.
691        pub fn pause(&mut self, c: Channel) {
692            let now = self.now();
693            if let Some(v) = self.voices.get_mut(c) {
694                if !v.paused {
695                    v.offset = Self::time(now, v);
696                    if v.looping {
697                        v.offset = v
698                            .offset
699                            .rem_euclid(v.sound.duration().as_secs_f64().max(1e-9));
700                    }
701                    Self::halt(v);
702                    v.paused = true;
703                }
704            }
705        }
706
707        /// Resume a paused channel.
708        pub fn resume(&mut self, c: Channel) {
709            let Some(ctx) = self.context.clone() else {
710                return;
711            };
712            if let Some(v) = self.voices.get_mut(c) {
713                if v.paused {
714                    v.paused = false;
715                    if let Err(e) = Self::start(&ctx, v) {
716                        log::error!("resume: {e}");
717                    }
718                }
719            }
720        }
721
722        /// Whether the channel is paused.
723        pub fn is_paused(&self, c: Channel) -> bool {
724            self.voices.get(c).is_some_and(|v| v.paused)
725        }
726
727        pub fn position(&self, c: Channel) -> Duration {
728            let Some(v) = self.voices.get(c) else {
729                return Duration::ZERO;
730            };
731            let d = v.sound.duration().as_secs_f64();
732            let t = Self::time(self.now(), v);
733            let t = if v.looping && d > 0.0 {
734                t.rem_euclid(d)
735            } else {
736                t.clamp(0.0, d)
737            };
738            Duration::from_secs_f64(t.max(0.0))
739        }
740
741        /// Move a channel to a position in its sound.
742        pub fn seek(&mut self, c: Channel, position: Duration) -> Result<(), String> {
743            let Some(ctx) = self.context.clone() else {
744                return Ok(());
745            };
746            if let Some(v) = self.voices.get_mut(c) {
747                v.offset = position
748                    .as_secs_f64()
749                    .clamp(0.0, v.sound.duration().as_secs_f64());
750                if !v.paused {
751                    Self::halt(v);
752                    Self::start(&ctx, v)?;
753                }
754            }
755            Ok(())
756        }
757
758        pub fn set_volume(&mut self, c: Channel, volume: f32) {
759            if let Some(v) = self.voices.get_mut(c) {
760                v.volume = volume.max(0.0);
761                v.gain.gain().set_value(v.volume);
762            }
763        }
764
765        /// Change a channel's pitch, in semitones.
766        pub fn set_pitch(&mut self, c: Channel, semitones: f32) {
767            let now = self.now();
768            if let Some(v) = self.voices.get_mut(c) {
769                // Re-base the clock so the position stays continuous.
770                v.offset = Self::time(now, v);
771                v.started_at = now;
772                v.rate = pitch_ratio(semitones) as f64;
773                if let Some(s) = &v.source {
774                    s.playback_rate().set_value(v.rate as f32);
775                }
776            }
777        }
778
779        /// Change whether a channel loops.
780        pub fn set_looping(&mut self, c: Channel, looping: bool) {
781            if let Some(v) = self.voices.get_mut(c) {
782                v.looping = looping;
783                if let Some(s) = &v.source {
784                    s.set_loop(looping);
785                }
786            }
787        }
788
789        pub fn set_master_volume(&mut self, volume: f32) {
790            self.master = volume.max(0.0);
791            if let Some(g) = &self.master_gain {
792                g.gain().set_value(self.master);
793            }
794        }
795
796        /// Resume a context the browser created suspended (autoplay policy). Call from a user
797        /// gesture handler such as a click or key press.
798        pub fn unlock(&mut self) {
799            if let Some(context) = &self.context {
800                if context.state() == web_sys::AudioContextState::Suspended {
801                    log::info!("resuming Web Audio after a user gesture");
802                    spawn(context.resume());
803                }
804            }
805        }
806    }
807
808    impl Drop for Device {
809        fn drop(&mut self) {
810            self.stop_all();
811            if let Some(context) = self.context.take() {
812                let _ = context.close();
813            }
814        }
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    fn tone(frames: usize, channels: u16) -> Sound {
823        Sound::new(
824            Pcm {
825                channels,
826                sample_rate: 100,
827                samples: (0..frames * channels as usize).map(|i| i as f32).collect(),
828            },
829            false,
830        )
831    }
832
833    #[test]
834    fn first_free_channel_and_stale_handles() {
835        let mut s = Slots::default();
836        let a = s.insert('a').unwrap();
837        let b = s.insert('b').unwrap();
838        assert_eq!((a.index(), b.index()), (0, 1));
839        assert_eq!(s.remove(a), Some('a'));
840        // The freed channel is reused first; the old handle no longer reaches it.
841        let c = s.insert('c').unwrap();
842        assert_eq!(c.index(), 0);
843        assert_eq!(s.get(a), None);
844        assert_eq!(s.get(c), Some(&'c'));
845        for _ in 2..CHANNELS {
846            assert!(s.insert('x').is_some());
847        }
848        assert_eq!(s.len(), CHANNELS);
849        assert!(
850            s.insert('y').is_none(),
851            "64 channels, as Device::FindFreeChannel"
852        );
853    }
854
855    #[test]
856    fn wrap_and_pitch() {
857        let t = tone(250, 2);
858        assert_eq!(t.duration(), Duration::from_millis(2500));
859        assert_eq!(
860            t.wrap(Duration::from_millis(3000)),
861            Duration::from_millis(500)
862        );
863        assert!((pitch_ratio(12.0) - 2.0).abs() < 1e-4);
864        assert_eq!(pitch_ratio(0.0), 1.0);
865    }
866
867    #[cfg(not(target_arch = "wasm32"))]
868    #[test]
869    fn shared_source_loops_seeks_and_shares_samples() {
870        use rodio::Source;
871        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
872        let t = tone(3, 2);
873        let cursor = Arc::new(AtomicUsize::new(0));
874        let looping = Arc::new(AtomicBool::new(true));
875        let mut src = native::SharedSource {
876            sound: t.clone(),
877            cursor: cursor.clone(),
878            looping: looping.clone(),
879        };
880        let first: Vec<f32> = (&mut src).take(8).collect();
881        assert_eq!(first, vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 0.0, 1.0]);
882        src.try_seek(Duration::from_millis(20)).unwrap();
883        assert_eq!(cursor.load(Ordering::Relaxed), 4);
884        // Many channels can hold one buffer.
885        assert_eq!(Arc::strong_count(&t.pcm), 2);
886        looping.store(false, Ordering::Relaxed);
887        assert_eq!(src.collect::<Vec<_>>(), vec![4.0, 5.0]);
888    }
889
890    /// Needs an audio output: `cargo test -p banished-audio -- --ignored`.
891    #[cfg(not(target_arch = "wasm32"))]
892    #[test]
893    #[ignore]
894    fn channels_play_together_and_free_on_end() {
895        let mut device = Device::default();
896        device.set_master_volume(0.0);
897        let long = tone(100, 1);
898        let short = tone(5, 1);
899        let music = device
900            .play(
901                &long,
902                PlayParams {
903                    looping: Some(true),
904                    ..Default::default()
905                },
906            )
907            .unwrap()
908            .unwrap();
909        let effect = device.play(&short, PlayParams::default()).unwrap().unwrap();
910        assert_ne!(music.index(), effect.index());
911        assert!(device.is_playing(music) && device.is_active(effect));
912        std::thread::sleep(Duration::from_millis(300));
913        // The short effect ended and is freed; the looping music keeps its channel.
914        assert_eq!(device.update(), vec![effect]);
915        assert!(device.is_playing(music));
916        assert_eq!(device.active(), 1);
917        device.stop(music);
918        assert_eq!(device.active(), 0);
919    }
920}