Skip to main content

banished_audio/
scene.rs

1//! The sound scene: the layer above the [`Device`](crate::Device) that plays sound effects.
2//!
3//! - **256 effects.** Each one walks its effect's timeline and starts every sound whose start time
4//!   falls in the frame, with a random variant and a random pitch, at the volume
5//!   `variant × effect × kind`. When all its sounds have ended, the effect frees itself (and the 3D
6//!   effect that started it).
7//! - **4,096 effects in the world.** Each has a position or a list of emitters. Every update takes
8//!   the loudest emitter (the volume falls in a straight line from the inner radius to the radius,
9//!   plus the listener's reach), starts the effect when it can be heard, stops it when it cannot,
10//!   and passes the volume on.
11//! - **Kinds of sound:** 1 interface, 2 music, 3 ambient, 4 effects (kind 0 never plays). Each
12//!   kind has a volume (by default 100, 50, 100 and 100) and can be paused.
13//!
14//! Handles are encoded as in the original: an effect is `index·2 | counter<<9`, an effect in the
15//! world `index·2 | 1 | counter<<13`. A stale handle does nothing. See
16//! [Audio](https://tanepiper.github.io/unbanished/topics/audio).
17//!
18//! Original: `Audio::Scene` in the Runtime.
19// The effects are at Scene +0x10 (0x70 bytes each), the 3D effects at +0x20 (0x40 bytes), the
20// paused kinds at +0x30. See docs/AUDIO.md.
21
22use std::collections::HashMap;
23use std::rc::Rc;
24use std::time::Duration;
25
26use banished_formats::audio::SoundEffect;
27use glam::Vec3;
28
29use crate::device::{Channel, PlayParams, Sound};
30
31/// `0x100` and `0x1000` slots (`FindFreeEffect`, `FindFree3DEffect`).
32const EFFECTS: usize = 0x100;
33const EFFECTS_3D: usize = 0x1000;
34/// Sounds per effect (`+0x20`, eight channel pointers; `+0x60`, eight flag bytes).
35const SOUNDS: usize = 8;
36/// `DAT_1801647cc`: 2^-32.
37const UNIT: f32 = 2.328_306_4e-10;
38/// `DAT_180164870`: a 3D effect is not restarted within `max_length / 20` of itself.
39const RETRIGGER_DIVISOR: f32 = 20.0;
40
41/// The kinds of sound. Each kind has its own volume in the options. Kind 0 never plays.
42///
43/// Original: `Audio::Scene::SoundType`.
44pub mod sound_type {
45    /// Interface sounds.
46    pub const UI: u32 = 1;
47    /// Music.
48    pub const MUSIC: u32 = 2;
49    /// Ambient sounds: weather, rivers, forests.
50    pub const AMBIENT: u32 = 3;
51    /// Sound effects in the world.
52    pub const EFFECT: u32 = 4;
53}
54
55/// What the scene needs from the channel pool (implemented by [`crate::Device`]).
56pub trait Mixer {
57    /// Start a sound on a free channel, or return `None` when no channel is free.
58    fn play(&mut self, sound: &Sound, params: PlayParams) -> Option<Channel>;
59    /// Stop a channel.
60    fn stop(&mut self, channel: Channel);
61    /// Change a channel's volume.
62    fn set_volume(&mut self, channel: Channel, volume: f32);
63}
64
65impl Mixer for crate::Device {
66    fn play(&mut self, sound: &Sound, params: PlayParams) -> Option<Channel> {
67        crate::Device::play(self, sound, params).ok().flatten()
68    }
69    fn stop(&mut self, channel: Channel) {
70        crate::Device::stop(self, channel)
71    }
72    fn set_volume(&mut self, channel: Channel, volume: f32) {
73        crate::Device::set_volume(self, channel, volume)
74    }
75}
76
77/// The volume of each kind of sound, from 0 to 100, as in the Audio options.
78///
79/// Original: `Audio::Configuration`; its constructor `Configuration::Configuration`, Runtime
80/// `0x1800e0700`.
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct Volumes {
83    /// Interface sounds.
84    pub ui: i32,
85    /// Music.
86    pub music: i32,
87    /// Ambient sounds.
88    pub ambient: i32,
89    /// Sound effects.
90    pub effect: i32,
91}
92
93impl Default for Volumes {
94    fn default() -> Self {
95        Volumes {
96            ui: 100,
97            music: 50,
98            ambient: 100,
99            effect: 100,
100        }
101    }
102}
103
104impl Volumes {
105    /// `Scene::Update`'s per-type table: type 0 is silent.
106    fn table(&self) -> [f32; 5] {
107        [
108            0.0,
109            self.ui as f32 / 100.0,
110            self.music as f32 / 100.0,
111            self.ambient as f32 / 100.0,
112            self.effect as f32 / 100.0,
113        ]
114    }
115}
116
117/// A SoundEffect with its decoded buffers (per sound, per variant).
118pub struct Effect {
119    /// The effect's resource reference.
120    pub reference: String,
121    /// The effect: its timeline of sounds, variants, radius and length.
122    pub desc: SoundEffect,
123    /// The decoded sounds, for each sound of the timeline and each variant (`None` when one could
124    /// not be decoded).
125    pub buffers: Vec<Vec<Option<Sound>>>,
126}
127
128impl Effect {
129    /// Max length in microseconds (0 = endless).
130    fn max_length(&self) -> i64 {
131        self.desc.max_length
132    }
133}
134
135/// Loads each effect once.
136#[derive(Default)]
137pub struct EffectCache {
138    effects: HashMap<String, Option<Rc<Effect>>>,
139}
140
141impl EffectCache {
142    /// An effect by its reference, loaded and decoded the first time it is asked for.
143    pub fn get(
144        &mut self,
145        archive: &banished_formats::pkg::Archive,
146        reference: &str,
147    ) -> Option<Rc<Effect>> {
148        let key = reference.replace('/', "\\");
149        if !self.effects.contains_key(&key) {
150            self.preload(archive, &[key.as_str()]);
151        }
152        self.effects[&key].clone()
153    }
154
155    /// Load every effect of `references` that is not loaded yet, as [`EffectCache::get`] would
156    /// one by one.
157    pub fn preload(&mut self, archive: &banished_formats::pkg::Archive, references: &[&str]) {
158        let missing = self.missing(references);
159        if !missing.is_empty() {
160            self.add(Self::prepare(archive, &missing));
161        }
162    }
163
164    /// The effects of `references` that are not loaded yet.
165    pub fn missing<'a>(&self, references: &[&'a str]) -> Vec<&'a str> {
166        references
167            .iter()
168            .copied()
169            .filter(|r| !self.effects.contains_key(&r.replace('/', "\\")))
170            .collect()
171    }
172
173    /// Read and decode effects without the cache, so that it can run on another thread while
174    /// the caller loads something else. Each sound buffer decodes on its own (a pure function
175    /// of its data), so all the buffers of all the effects decode in parallel (native).
176    pub fn prepare(archive: &banished_formats::pkg::Archive, references: &[&str]) -> Prepared {
177        let mut keys: Vec<String> = Vec::new();
178        for r in references {
179            let key = r.replace('/', "\\");
180            if !keys.contains(&key) {
181                keys.push(key);
182            }
183        }
184        let descs: Vec<Result<SoundEffect, Option<String>>> = keys
185            .iter()
186            .map(|key| {
187                let path = key.replace('\\', "/");
188                let bytes = archive.read_ref(&path).map_err(|_| None)?;
189                banished_formats::audio::sound_effect(&bytes).map_err(|e| Some(e.to_string()))
190            })
191            .collect();
192        let buffers: Vec<Option<String>> = descs
193            .iter()
194            .flatten()
195            .flat_map(|d| d.sounds.iter().flat_map(|s| &s.variants))
196            .map(|v| Some(v.buffer.as_deref()?.replace('\\', "/")))
197            .collect();
198        let decoded = banished_formats::par::map(&buffers, |r| {
199            r.as_deref().map(|r| crate::load_sound(archive, r))
200        });
201        Prepared {
202            keys,
203            descs,
204            buffers,
205            decoded,
206        }
207    }
208
209    /// Put prepared effects in the cache (an effect already there is kept).
210    pub fn add(&mut self, prepared: Prepared) {
211        let Prepared {
212            keys,
213            descs,
214            buffers,
215            decoded,
216        } = prepared;
217        let mut decoded = buffers.iter().zip(decoded).map(|(r, d)| match d? {
218            Ok((pcm, looping)) => Some(Sound::new(pcm, looping)),
219            Err(e) => {
220                log::warn!("{}: {e}", r.as_deref().unwrap_or_default());
221                None
222            }
223        });
224        for (key, desc) in keys.into_iter().zip(descs) {
225            let desc = desc.map_err(|e| e.map(|e| log::warn!("{key}: {e}"))).ok();
226            let loaded = desc.map(|desc| {
227                let buffers = desc
228                    .sounds
229                    .iter()
230                    .map(|s| decoded.by_ref().take(s.variants.len()).collect())
231                    .collect();
232                Rc::new(Effect {
233                    reference: key.clone(),
234                    desc,
235                    buffers,
236                })
237            });
238            self.effects.entry(key).or_insert(loaded);
239        }
240    }
241}
242
243/// Effects read and decoded by [`EffectCache::prepare`], for [`EffectCache::add`].
244pub struct Prepared {
245    keys: Vec<String>,
246    /// Per key: the description, or why it did not load (`None`: not in the packages).
247    descs: Vec<Result<SoundEffect, Option<String>>>,
248    /// Every variant's buffer reference, in order, and its decoded samples.
249    buffers: Vec<Option<String>>,
250    decoded: Vec<Option<Result<(crate::Pcm, bool), String>>>,
251}
252
253/// One playing effect (`0x70` bytes).
254struct Effect2D {
255    effect: Rc<Effect>,
256    kind: u32,
257    /// `+0x10`: time into the effect (µs).
258    elapsed: i64,
259    /// `+0x18`: volume set by the 3D effect that drives it (1 otherwise).
260    volume: f32,
261    /// `+0x1c`: volume of the last variant started.
262    variant_volume: f32,
263    channels: [Option<Channel>; SOUNDS],
264    /// Bit 0 started, bit 1 ended.
265    flags: [u8; SOUNDS],
266    handle: u32,
267    /// `+0x6c`: cleared on start and unpause; the first update's window then begins at 0.
268    started: bool,
269}
270
271/// One positional effect (`0x40` bytes).
272struct Effect3D {
273    position: Vec3,
274    kind: u32,
275    effect: Rc<Effect>,
276    /// `+0x18`: µs.
277    elapsed: i64,
278    /// `+0x20`: the 2D effect playing it, or 0.
279    playing: u32,
280    handle: u32,
281    /// `+0x28`.
282    volume: f32,
283    /// `+0x30`: positions and volumes; empty means `position` at volume 1.
284    emitters: Vec<(Vec3, f32)>,
285}
286
287/// The sound scene: up to 256 playing effects and 4,096 effects in the world, their emitters,
288/// and the volumes of the kinds of sound. [`Scene::update`] mixes them every frame.
289///
290/// Original: `Audio::Scene`.
291pub struct Scene {
292    /// `+0x00`: the frame's simulation time step (µs), the minimum retrigger window.
293    step: i64,
294    /// `+0x08`: handle counter, starting at 1.
295    counter: u32,
296    effects: Vec<Option<Effect2D>>,
297    effects3d: Vec<Option<Effect3D>>,
298    /// `+0x30`: paused sound types.
299    paused: [bool; 5],
300    /// The volume of each kind of sound.
301    pub volumes: Volumes,
302}
303
304impl Default for Scene {
305    fn default() -> Self {
306        Scene {
307            step: 0,
308            counter: 1,
309            effects: (0..EFFECTS).map(|_| None).collect(),
310            effects3d: (0..EFFECTS_3D).map(|_| None).collect(),
311            paused: [false; 5],
312            volumes: Volumes::default(),
313        }
314    }
315}
316
317fn index2d(handle: u32) -> usize {
318    (handle >> 1 & 0xff) as usize
319}
320
321fn index3d(handle: u32) -> usize {
322    (handle >> 1 & 0xfff) as usize
323}
324
325/// `GetVolumeForPosition` (`0x1800e2720`).
326fn volume_for_position(at: Vec3, radius: f32, inner: f32, listener: Vec3) -> f32 {
327    let d = (at - listener).length();
328    if radius < d {
329        0.0
330    } else if inner <= d {
331        1.0 - (d - inner) / (radius - inner)
332    } else {
333        1.0
334    }
335}
336
337fn micros(seconds: f32) -> i64 {
338    (seconds as f64 * 1e6) as i64
339}
340
341impl Scene {
342    fn effect_mut(&mut self, handle: u32) -> Option<&mut Effect2D> {
343        self.effects
344            .get_mut(index2d(handle))?
345            .as_mut()
346            .filter(|e| (e.handle ^ handle) & 0xffff_fe00 == 0)
347    }
348
349    fn effect3d_mut(&mut self, handle: u32) -> Option<&mut Effect3D> {
350        self.effects3d
351            .get_mut(index3d(handle))?
352            .as_mut()
353            .filter(|e| (e.handle ^ handle) & 0xffff_e000 == 0)
354    }
355
356    /// `PlayEffect`: the first free slot, starting `start` µs into the effect. Returns 0 when all
357    /// 256 are busy.
358    ///
359    /// Original: Runtime `0x1800e17b0`.
360    pub fn play_effect(&mut self, effect: &Rc<Effect>, kind: u32, start: i64) -> u32 {
361        let Some(i) = self.effects.iter().position(Option::is_none) else {
362            return 0;
363        };
364        self.counter = self.counter.wrapping_add(1);
365        let handle = (i as u32) * 2 | self.counter << 9;
366        self.effects[i] = Some(Effect2D {
367            effect: effect.clone(),
368            kind,
369            elapsed: start,
370            volume: 1.0,
371            variant_volume: 1.0,
372            channels: [None; SOUNDS],
373            flags: [0; SOUNDS],
374            handle,
375            started: false,
376        });
377        handle
378    }
379
380    /// `PlayEffect3D` with `FindFree3DEffect`: refused when the same effect already plays within
381    /// its radius and started less than the retrigger window ago.
382    ///
383    /// Original: Runtime `0x1800e1700`.
384    pub fn play_effect_3d(&mut self, effect: &Rc<Effect>, kind: u32, start: i64, at: Vec3) -> u32 {
385        let mut window = self.step;
386        if effect.max_length() != 0 {
387            let w = micros(effect.max_length() as f32 / 1e6 / RETRIGGER_DIVISOR);
388            if (window as f32) < w as f32 {
389                window = w;
390            }
391        }
392        let radius = effect.desc.radius;
393        let mut free = None;
394        for (i, slot) in self.effects3d.iter().enumerate() {
395            match slot {
396                None => {
397                    if free.is_none() {
398                        free = Some(i);
399                    }
400                }
401                Some(e) => {
402                    if Rc::ptr_eq(&e.effect, effect)
403                        && (e.position - at).length_squared() < radius * radius
404                        && (e.elapsed - start).abs() < window
405                    {
406                        return 0;
407                    }
408                }
409            }
410        }
411        let Some(i) = free else { return 0 };
412        self.counter = self.counter.wrapping_add(1);
413        let handle = (i as u32) * 2 | 1 | self.counter << 13;
414        self.effects3d[i] = Some(Effect3D {
415            position: at,
416            kind,
417            effect: effect.clone(),
418            elapsed: start,
419            playing: 0,
420            handle,
421            volume: 1.0,
422            emitters: Vec::new(),
423        });
424        handle
425    }
426
427    /// `FindEffect3D`: the 3D effect playing this SoundEffect, or 0.
428    pub fn find_effect_3d(&self, effect: &Rc<Effect>) -> u32 {
429        self.effects3d
430            .iter()
431            .flatten()
432            .find(|e| Rc::ptr_eq(&e.effect, effect))
433            .map_or(0, |e| e.handle)
434    }
435
436    /// `AddEmitter`.
437    pub fn add_emitter(&mut self, handle: u32, at: Vec3, volume: f32) {
438        if let Some(e) = self.effect3d_mut(handle) {
439            e.emitters.push((at, volume));
440        }
441    }
442
443    /// `RemoveEmitter`: the first emitter at that position.
444    pub fn remove_emitter(&mut self, handle: u32, at: Vec3) {
445        if let Some(e) = self.effect3d_mut(handle) {
446            if let Some(i) = e.emitters.iter().position(|(p, _)| *p == at) {
447                e.emitters.remove(i);
448            }
449        }
450    }
451
452    /// `MoveEmitter`.
453    pub fn move_emitter(&mut self, handle: u32, from: Vec3, to: Vec3) {
454        if let Some(e) = self.effect3d_mut(handle) {
455            if let Some(m) = e.emitters.iter_mut().find(|(p, _)| *p == from) {
456                m.0 = to;
457            }
458        }
459    }
460
461    /// `SetEmitterVolume`.
462    pub fn set_emitter_volume(&mut self, handle: u32, at: Vec3, volume: f32) {
463        if let Some(e) = self.effect3d_mut(handle) {
464            if let Some(m) = e.emitters.iter_mut().find(|(p, _)| *p == at) {
465                m.1 = volume;
466            }
467        }
468    }
469
470    /// `GetEmitterCount`.
471    pub fn emitter_count(&self, handle: u32) -> usize {
472        self.effects3d
473            .get(index3d(handle))
474            .and_then(Option::as_ref)
475            .filter(|e| (e.handle ^ handle) & 0xffff_e000 == 0)
476            .map_or(0, |e| e.emitters.len())
477    }
478
479    /// `IsEffectPlaying`.
480    pub fn is_effect_playing(&self, handle: u32) -> bool {
481        self.effects
482            .get(index2d(handle))
483            .and_then(Option::as_ref)
484            .is_some_and(|e| (e.handle ^ handle) & 0xffff_fe00 == 0)
485    }
486
487    /// `StopEffect`: stop its channels and free it.
488    pub fn stop_effect(&mut self, mixer: &mut dyn Mixer, handle: u32) {
489        if let Some(e) = self.effect_mut(handle) {
490            for c in e.channels.iter().flatten() {
491                mixer.stop(*c);
492            }
493            self.effects[index2d(handle)] = None;
494        }
495    }
496
497    /// `StopEffect3D`.
498    pub fn stop_effect_3d(&mut self, mixer: &mut dyn Mixer, handle: u32) {
499        let Some(e) = self.effect3d_mut(handle) else {
500            return;
501        };
502        let playing = e.playing;
503        if playing != 0 {
504            self.stop_effect(mixer, playing);
505        }
506        self.effects3d[index3d(handle)] = None;
507    }
508
509    /// `StopAll`: every 2D effect (3D effects stay and restart when audible).
510    pub fn stop_all(&mut self, mixer: &mut dyn Mixer) {
511        for slot in &mut self.effects {
512            if let Some(e) = slot.take() {
513                for c in e.channels.iter().flatten() {
514                    mixer.stop(*c);
515                }
516            }
517        }
518    }
519
520    /// `PauseEffect`: stop the channels; started sounds are not restarted.
521    fn pause_effect(e: &mut Effect2D, mixer: &mut dyn Mixer) {
522        for (c, f) in e.channels.iter_mut().zip(&mut e.flags) {
523            if let Some(ch) = c.take() {
524                mixer.stop(ch);
525                *f &= !2;
526            }
527        }
528    }
529
530    /// `Pause(type, paused)`.
531    pub fn pause(&mut self, mixer: &mut dyn Mixer, kind: u32, paused: bool) {
532        let k = kind as usize;
533        if k >= self.paused.len() || self.paused[k] == paused {
534            return;
535        }
536        for e in self.effects.iter_mut().flatten().filter(|e| e.kind == kind) {
537            if paused {
538                Self::pause_effect(e, mixer);
539            } else {
540                e.started = false;
541            }
542        }
543        self.paused[k] = paused;
544    }
545
546    /// `IsPaused(type)`.
547    pub fn is_paused(&self, kind: u32) -> bool {
548        self.paused.get(kind as usize).copied().unwrap_or(false)
549    }
550
551    /// The device's end callback (`OnSoundEnded`): mark the sound ended.
552    pub fn on_ended(&mut self, channel: Channel) {
553        for e in self.effects.iter_mut().flatten() {
554            for (c, f) in e.channels.iter_mut().zip(&mut e.flags) {
555                if *c == Some(channel) {
556                    *c = None;
557                    *f |= 2;
558                }
559            }
560        }
561    }
562
563    /// `Update(dt, step, random, listener, reach)`. `dt` advances the effects; `step` is the
564    /// simulation step of the frame; `reach` extends every 3D effect's radius (the camera's
565    /// distance from its pivot).
566    ///
567    /// Original: Runtime `0x1800e1f10`.
568    pub fn update(
569        &mut self,
570        mixer: &mut dyn Mixer,
571        dt: i64,
572        step: i64,
573        rand: &mut dyn FnMut() -> u32,
574        listener: Vec3,
575        reach: f32,
576    ) {
577        self.step = step;
578        // 3D effects.
579        for i in 0..EFFECTS_3D {
580            let Some(e) = &self.effects3d[i] else {
581                continue;
582            };
583            if self.paused[e.kind as usize % 5] {
584                continue;
585            }
586            let volume = self.attenuated_volume(e, listener, reach);
587            let (handle, kind, effect, playing, elapsed) =
588                (e.handle, e.kind, e.effect.clone(), e.playing, e.elapsed);
589            let mut playing = playing;
590            if volume == 0.0 && playing != 0 {
591                self.stop_effect(mixer, playing);
592                playing = 0;
593            } else if volume > 0.0 && playing == 0 {
594                playing = self.play_effect(&effect, kind, elapsed);
595            }
596            if let Some(p) = self.effect_mut(playing) {
597                p.volume = volume;
598            }
599            let e = self.effects3d[i].as_mut().unwrap();
600            e.playing = playing;
601            if playing == 0 && effect.max_length() != 0 && effect.max_length() <= e.elapsed {
602                self.stop_effect_3d(mixer, handle);
603            } else {
604                e.elapsed += dt;
605            }
606        }
607        // 2D effects.
608        let volumes = self.volumes.table();
609        for i in 0..EFFECTS {
610            let Some(e) = self.effects[i].as_mut() else {
611                continue;
612            };
613            if self.paused[e.kind as usize % 5] {
614                continue;
615            }
616            let from = if e.started { e.elapsed } else { 0 };
617            e.started = true;
618            let to = e.elapsed + dt;
619            let mut done = false;
620            if e.kind != 0 {
621                let type_volume = volumes[e.kind as usize % 5];
622                let mut ended = 0;
623                for (s, sound) in e.effect.desc.sounds.iter().enumerate().take(SOUNDS) {
624                    let at = micros(sound.start);
625                    if at < from || to <= at {
626                        // Playing sounds follow the effect's volume.
627                        if e.flags[s] & 1 != 0 && e.flags[s] & 2 == 0 {
628                            if let Some(c) = e.channels[s] {
629                                mixer.set_volume(c, e.variant_volume * e.volume * type_volume);
630                            }
631                        }
632                    } else {
633                        // In the window: start it (`GetSoundBuffer`: a random variant and a
634                        // random pitch). The original does not check whether the sound already
635                        // started; after an unpause the window reopens at 0, so paused sounds
636                        // restart at the effect's elapsed time, which resumes them.
637                        let n = sound.variants.len() as u32;
638                        let v = if n == 0 { 0 } else { (rand() % n) as usize };
639                        let r = rand();
640                        let Some(variant) = sound.variants.get(v) else {
641                            e.flags[s] |= 3;
642                            ended += 1;
643                            continue;
644                        };
645                        let pitch = (variant.pitch_max - variant.pitch_min) * r as f32 * UNIT
646                            + variant.pitch_min;
647                        e.variant_volume = variant.volume;
648                        let channel = e.effect.buffers[s][v].as_ref().and_then(|buffer| {
649                            mixer.play(
650                                buffer,
651                                PlayParams {
652                                    offset: Duration::from_micros(e.elapsed.max(0) as u64),
653                                    volume: variant.volume * e.volume * type_volume,
654                                    pitch,
655                                    looping: None,
656                                },
657                            )
658                        });
659                        e.channels[s] = channel;
660                        e.flags[s] |= 1;
661                        if channel.is_none() {
662                            // No buffer or no free channel: the sound is over at once.
663                            e.flags[s] |= 2;
664                        }
665                    }
666                    if e.flags[s] & 2 != 0 {
667                        ended += 1;
668                    }
669                }
670                done = ended == e.effect.desc.sounds.len().min(SOUNDS);
671            }
672            e.elapsed = to;
673            if done {
674                let handle = e.handle;
675                self.effects[i] = None;
676                for slot in &mut self.effects3d {
677                    if slot.as_ref().is_some_and(|f| f.playing == handle) {
678                        *slot = None;
679                    }
680                }
681            }
682        }
683    }
684
685    /// `GetAttenuatedVolume` (`0x1800e2630`).
686    fn attenuated_volume(&self, e: &Effect3D, listener: Vec3, reach: f32) -> f32 {
687        let radius = reach + e.effect.desc.radius;
688        let inner = e.effect.desc.inner_radius;
689        let v = if e.emitters.is_empty() {
690            volume_for_position(e.position, radius, inner, listener)
691        } else {
692            e.emitters.iter().fold(0.0f32, |best, (p, w)| {
693                best.max(volume_for_position(*p, radius, inner, listener) * w)
694            })
695        };
696        v * e.volume
697    }
698
699    /// Effects currently held (tests, debugging).
700    pub fn active(&self) -> (usize, usize) {
701        (
702            self.effects.iter().flatten().count(),
703            self.effects3d.iter().flatten().count(),
704        )
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711    use banished_formats::audio::{EffectSound, SoundVariant};
712
713    #[derive(Default)]
714    struct Fake {
715        next: u32,
716        playing: Vec<(Channel, f32, f32)>,
717        stopped: Vec<Channel>,
718    }
719
720    impl Mixer for Fake {
721        fn play(&mut self, _: &Sound, p: PlayParams) -> Option<Channel> {
722            self.next += 1;
723            let c = Channel::test(self.next);
724            self.playing.push((c, p.volume, p.pitch));
725            Some(c)
726        }
727        fn stop(&mut self, c: Channel) {
728            self.stopped.push(c);
729        }
730        fn set_volume(&mut self, _: Channel, _: f32) {}
731    }
732
733    fn effect(starts: &[f32], radius: f32, max_length: i64) -> Rc<Effect> {
734        let pcm = crate::Pcm {
735            channels: 1,
736            sample_rate: 100,
737            samples: vec![0.0; 10],
738        };
739        let sound = Sound::new(pcm, false);
740        Rc::new(Effect {
741            reference: "test".into(),
742            desc: SoundEffect {
743                radius,
744                inner_radius: 1.0,
745                sounds: starts
746                    .iter()
747                    .map(|&s| EffectSound {
748                        start: s,
749                        variants: vec![SoundVariant {
750                            buffer: Some("x".into()),
751                            pitch_min: -1.0,
752                            pitch_max: 1.0,
753                            volume: 0.5,
754                        }],
755                    })
756                    .collect(),
757                max_length,
758            },
759            buffers: starts.iter().map(|_| vec![Some(sound.clone())]).collect(),
760        })
761    }
762
763    #[test]
764    fn timeline_starts_sounds_in_their_window_and_frees_when_ended() {
765        let mut scene = Scene::default();
766        let mut mixer = Fake::default();
767        let fx = effect(&[0.0, 0.5], 10.0, 1_000_000);
768        let h = scene.play_effect(&fx, sound_type::EFFECT, 0);
769        let mut seq = 0u32;
770        let mut rand = || {
771            seq += 1;
772            seq
773        };
774        scene.update(&mut mixer, 100_000, 0, &mut rand, Vec3::ZERO, 0.0);
775        assert_eq!(mixer.playing.len(), 1);
776        // Volume: variant 0.5 × effect 1 × effects 100%.
777        assert_eq!(mixer.playing[0].1, 0.5);
778        scene.update(&mut mixer, 500_000, 0, &mut rand, Vec3::ZERO, 0.0);
779        assert_eq!(mixer.playing.len(), 2);
780        let (a, b) = (mixer.playing[0].0, mixer.playing[1].0);
781        scene.on_ended(a);
782        scene.update(&mut mixer, 100_000, 0, &mut rand, Vec3::ZERO, 0.0);
783        assert!(scene.is_effect_playing(h));
784        scene.on_ended(b);
785        scene.update(&mut mixer, 100_000, 0, &mut rand, Vec3::ZERO, 0.0);
786        assert!(!scene.is_effect_playing(h));
787    }
788
789    #[test]
790    fn music_type_uses_its_volume_and_type_zero_is_silent() {
791        let mut scene = Scene::default();
792        let mut mixer = Fake::default();
793        let fx = effect(&[0.0], 10.0, 0);
794        scene.play_effect(&fx, sound_type::MUSIC, 0);
795        scene.play_effect(&fx, 0, 0);
796        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
797        assert_eq!(mixer.playing.len(), 1);
798        assert_eq!(mixer.playing[0].1, 0.25);
799    }
800
801    #[test]
802    fn three_d_effect_follows_the_loudest_emitter() {
803        let mut scene = Scene::default();
804        let mut mixer = Fake::default();
805        let fx = effect(&[0.0], 10.0, 0);
806        let h = scene.play_effect_3d(&fx, sound_type::AMBIENT, 0, Vec3::ZERO);
807        scene.add_emitter(h, Vec3::new(100.0, 0.0, 0.0), 1.0);
808        // Out of reach: nothing plays.
809        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
810        assert_eq!(scene.active(), (0, 1));
811        // A second emitter at 5.5 (half-way from inner 1 to radius 10).
812        scene.add_emitter(h, Vec3::new(5.5, 0.0, 0.0), 1.0);
813        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
814        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
815        assert_eq!(scene.active(), (1, 1));
816        assert!((mixer.playing[0].1 - 0.25).abs() < 1e-6);
817        assert_eq!(scene.emitter_count(h), 2);
818        assert_eq!(scene.find_effect_3d(&fx), h);
819    }
820
821    #[test]
822    fn pause_stops_channels_and_resume_continues() {
823        let mut scene = Scene::default();
824        let mut mixer = Fake::default();
825        let fx = effect(&[0.0], 10.0, 0);
826        scene.play_effect(&fx, sound_type::MUSIC, 0);
827        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
828        scene.pause(&mut mixer, sound_type::MUSIC, true);
829        assert_eq!(mixer.stopped.len(), 1);
830        scene.pause(&mut mixer, sound_type::MUSIC, false);
831        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
832        // Unpausing reopens the window at 0: the paused sound starts again (resumed at the
833        // effect's elapsed time).
834        assert_eq!(mixer.playing.len(), 2);
835        scene.update(&mut mixer, 1, 0, &mut || 0, Vec3::ZERO, 0.0);
836        assert_eq!(mixer.playing.len(), 2);
837    }
838}