1use std::sync::Arc;
16use std::time::Duration;
17
18use crate::Pcm;
19
20pub const CHANNELS: usize = 64;
22
23const SEMITONE: f32 = 1.059_463_1;
25
26pub fn pitch_ratio(semitones: f32) -> f32 {
31 SEMITONE.powf(semitones)
32}
33
34#[derive(Clone)]
37pub struct Sound {
38 pcm: Arc<Pcm>,
39 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 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 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 pub fn pcm(&self) -> &Pcm {
68 &self.pcm
69 }
70
71 pub fn frames(&self) -> usize {
73 self.pcm.samples.len() / self.pcm.channels.max(1) as usize
74 }
75
76 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub struct Channel {
95 index: u8,
96 generation: u32,
97}
98
99impl Channel {
100 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#[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 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 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 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 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 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 pub fn len(&self) -> usize {
181 self.slots.iter().filter(|s| s.is_some()).count()
182 }
183
184 pub fn is_empty(&self) -> bool {
186 self.len() == 0
187 }
188}
189
190#[derive(Debug, Clone, Copy)]
192pub struct PlayParams {
193 pub offset: Duration,
195 pub volume: f32,
197 pub pitch: f32,
199 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 pub(crate) struct SharedSource {
230 pub(crate) sound: Sound,
231 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 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 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 pub fn stop(&mut self, c: Channel) {
349 if let Some(v) = self.voices.remove(c) {
350 v.sink.stop();
351 }
352 }
353
354 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 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 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 pub fn is_active(&self, c: Channel) -> bool {
386 self.voices.get(c).is_some_and(|v| !v.sink.empty())
387 }
388
389 pub fn active(&self) -> usize {
391 self.voices.len()
392 }
393
394 pub fn pause(&mut self, c: Channel) {
396 if let Some(v) = self.voices.get(c) {
397 v.sink.pause();
398 }
399 }
400
401 pub fn resume(&mut self, c: Channel) {
403 if let Some(v) = self.voices.get(c) {
404 v.sink.play();
405 }
406 }
407
408 pub fn is_paused(&self, c: Channel) -> bool {
410 self.voices.get(c).is_some_and(|v| v.sink.is_paused())
411 }
412
413 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 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 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 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 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 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 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 offset: f64,
497 started_at: f64,
498 paused: bool,
499 }
500
501 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 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 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 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 pub fn active(&self) -> usize {
687 self.voices.len()
688 }
689
690 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 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 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 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 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 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 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 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 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 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 #[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 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}