Skip to main content

siglus_scene_vm/audio/
sfx_engine.rs

1use crate::platform_time::{Duration, Instant};
2use std::collections::HashMap;
3use std::fs;
4use std::io::Cursor;
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result, anyhow, bail};
8
9use kira::Volume;
10use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
11use kira::tween::Tween;
12
13#[cfg(not(target_arch = "wasm32"))]
14type StreamingSoundData = kira::sound::streaming::StreamingSoundData<kira::sound::FromFileError>;
15#[cfg(not(target_arch = "wasm32"))]
16type StreamingSoundHandle =
17    kira::sound::streaming::StreamingSoundHandle<kira::sound::FromFileError>;
18
19/// Plain OGG sounds at least this large (roughly ten seconds or more:
20/// ambience and long loops) are decoded while they play. Decoded in full
21/// they cost ~375 KiB per second as Kira frames, 15 MiB for one of
22/// GameData's loops.
23#[cfg(not(target_arch = "wasm32"))]
24const STREAM_OGG_MIN_BYTES: u64 = 256 * 1024;
25
26/// A playing slot sound: short sounds are decoded up front, long OGG ones
27/// stream.
28#[derive(Debug)]
29enum SlotHandle {
30    Static(StaticSoundHandle),
31    #[cfg(not(target_arch = "wasm32"))]
32    Streaming(StreamingSoundHandle),
33}
34
35impl SlotHandle {
36    fn set_volume(&mut self, volume: Volume, tween: Tween) {
37        match self {
38            Self::Static(handle) => handle.set_volume(volume, tween),
39            #[cfg(not(target_arch = "wasm32"))]
40            Self::Streaming(handle) => handle.set_volume(volume, tween),
41        }
42    }
43
44    fn pause(&mut self, tween: Tween) {
45        match self {
46            Self::Static(handle) => handle.pause(tween),
47            #[cfg(not(target_arch = "wasm32"))]
48            Self::Streaming(handle) => handle.pause(tween),
49        }
50    }
51
52    fn resume(&mut self, tween: Tween) {
53        match self {
54            Self::Static(handle) => handle.resume(tween),
55            #[cfg(not(target_arch = "wasm32"))]
56            Self::Streaming(handle) => handle.resume(tween),
57        }
58    }
59
60    fn stop(&mut self, tween: Tween) {
61        match self {
62            Self::Static(handle) => handle.stop(tween),
63            #[cfg(not(target_arch = "wasm32"))]
64            Self::Streaming(handle) => handle.stop(tween),
65        }
66    }
67}
68
69/// Sound data for `SfxEngine::start_in_slot`.
70enum SlotSound {
71    Static(StaticSoundData),
72    #[cfg(not(target_arch = "wasm32"))]
73    Streaming(StreamingSoundData),
74}
75
76use crate::audio::bgm::{
77    KoeSource, decode_bgm_to_wav_bytes, decode_ovk_entry_by_no_to_wav_bytes, resolve_koe_source,
78};
79use crate::audio::{AudioHub, TrackKind};
80
81fn decode_koe_no_for_project(project_dir: &Path, koe_no: i64) -> Result<Vec<u8>> {
82    let resolved = resolve_koe_source(project_dir, koe_no)?;
83    let wav = match &resolved {
84        KoeSource::File(path) => {
85            decode_bgm_to_wav_bytes(path, None)
86                .with_context(|| format!("decode KOE file: {}", path.display()))?
87                .wav_bytes
88        }
89        KoeSource::OvkEntryByNo { path, entry_no } => {
90            decode_ovk_entry_by_no_to_wav_bytes(path, *entry_no)
91                .with_context(|| format!("decode KOE OVK entry: {}#{entry_no}", path.display()))?
92                .wav_bytes
93        }
94    };
95    if env_is_set!("SG_AUDIO_TRACE") {
96        eprintln!(
97            "[SG_AUDIO_TRACE] koe resolved koe_no={} source={:?} wav_ms={:?}",
98            koe_no,
99            resolved,
100            wav_duration_ms(&wav)
101        );
102    }
103    Ok(wav)
104}
105
106/// Best-effort WAV duration parsing for bring-up.
107///
108/// We use this only to implement WAIT-style commands without relying on
109/// backend handle state APIs (which differ across Kira versions).
110pub(crate) fn wav_duration_ms(wav: &[u8]) -> Option<u64> {
111    // Minimal RIFF/WAVE parser.
112    if wav.len() < 44 {
113        return None;
114    }
115    if &wav[0..4] != b"RIFF" || &wav[8..12] != b"WAVE" {
116        return None;
117    }
118
119    let mut pos = 12usize;
120    let mut byte_rate: Option<u32> = None;
121    let mut data_size: Option<u32> = None;
122
123    while pos + 8 <= wav.len() {
124        let id = &wav[pos..pos + 4];
125        let sz =
126            u32::from_le_bytes([wav[pos + 4], wav[pos + 5], wav[pos + 6], wav[pos + 7]]) as usize;
127        pos += 8;
128        if pos + sz > wav.len() {
129            break;
130        }
131        if id == b"fmt " {
132            if sz >= 16 {
133                // byte_rate is at offset 8 within fmt chunk.
134                let off = pos + 8;
135                if off + 4 <= wav.len() {
136                    byte_rate = Some(u32::from_le_bytes([
137                        wav[off],
138                        wav[off + 1],
139                        wav[off + 2],
140                        wav[off + 3],
141                    ]));
142                }
143            }
144        } else if id == b"data" {
145            data_size = Some(sz as u32);
146        }
147
148        // Chunks are word-aligned.
149        pos += sz;
150        if (sz & 1) != 0 {
151            pos += 1;
152        }
153
154        if byte_rate.is_some() && data_size.is_some() {
155            break;
156        }
157    }
158
159    let br = byte_rate?;
160    if br == 0 {
161        return None;
162    }
163    let ds = data_size? as u64;
164    Some((ds * 1000) / (br as u64))
165}
166
167#[derive(Debug)]
168struct Slot {
169    handle: Option<SlotHandle>,
170    /// Logical end time for non-looping playback. This remains available when
171    /// audio output is disabled, so WAIT/CHECK keep script-time semantics.
172    until: Option<Instant>,
173    duration_ms: Option<u64>,
174    looping: bool,
175    last_name: Option<String>,
176    /// Time at which a pause transition becomes fully effective. Presence of
177    /// this field is enough for CHECK to report false, matching is_pausing().
178    paused_at: Option<Instant>,
179    /// READY prepares a source in the paused state and starts it on RESUME.
180    ready_only: bool,
181    /// A STOP fade remains observable by WAIT_FADE until this deadline.
182    /// CHECK/WAIT already treat the slot as stopped while the fade runs.
183    fade_until: Option<Instant>,
184    /// Delayed PCMCH.RESUME target time.
185    resume_at: Option<Instant>,
186    resume_fade_ms: i64,
187    /// Script-visible channel volume (`PCMCH.SET_VOLUME`).
188    volume_raw: u8,
189    /// Original `m_system_volume`: category/chara/BGM-fade routing, updated
190    /// independently from the script-visible channel volume.
191    system_volume_raw: u8,
192}
193
194impl Default for Slot {
195    fn default() -> Self {
196        Self {
197            handle: None,
198            until: None,
199            duration_ms: None,
200            looping: false,
201            last_name: None,
202            paused_at: None,
203            ready_only: false,
204            fade_until: None,
205            resume_at: None,
206            resume_fade_ms: 0,
207            volume_raw: 255,
208            system_volume_raw: 255,
209        }
210    }
211}
212
213impl Slot {
214    fn amplitude(&self) -> f64 {
215        let combined = u16::from(self.volume_raw) * u16::from(self.system_volume_raw) / 255;
216        f64::from(combined) / 255.0
217    }
218
219    fn tween_for_ms(ms: i64) -> Tween {
220        if ms > 0 {
221            Tween {
222                duration: Duration::from_millis(ms as u64),
223                ..Tween::default()
224            }
225        } else {
226            Tween::default()
227        }
228    }
229
230    fn clear(&mut self) {
231        self.handle = None;
232        self.until = None;
233        self.duration_ms = None;
234        self.looping = false;
235        self.last_name = None;
236        self.paused_at = None;
237        self.ready_only = false;
238        self.fade_until = None;
239        self.resume_at = None;
240        self.resume_fade_ms = 0;
241    }
242
243    fn has_source(&self) -> bool {
244        self.last_name.is_some()
245    }
246
247    fn resume_now(&mut self, fade_ms: i64) {
248        if !self.has_source() || self.fade_until.is_some() {
249            self.resume_at = None;
250            self.resume_fade_ms = 0;
251            return;
252        }
253
254        let now = Instant::now();
255        let was_ready = self.ready_only;
256        if let Some(mut handle) = self.handle.take() {
257            if fade_ms > 0 {
258                let amplitude = self.amplitude();
259                handle.set_volume(Volume::Amplitude(0.0), Tween::default());
260                handle.resume(Tween::default());
261                handle.set_volume(Volume::Amplitude(amplitude), Self::tween_for_ms(fade_ms));
262            } else {
263                let amplitude = self.amplitude();
264                handle.resume(Tween::default());
265                handle.set_volume(Volume::Amplitude(amplitude), Tween::default());
266            }
267            self.handle = Some(handle);
268        }
269
270        if was_ready {
271            if self.looping {
272                self.until = None;
273            } else {
274                let duration_ms = self.duration_ms.unwrap_or(2000);
275                self.until = Some(now + Duration::from_millis(duration_ms));
276            }
277        } else if let Some(paused_at) = self.paused_at {
278            // A fade-pause continues until paused_at. Resuming before then must
279            // not add time that the sound actually kept playing.
280            let effective = if paused_at > now { now } else { paused_at };
281            let paused_for = now.saturating_duration_since(effective);
282            if let Some(until) = self.until {
283                self.until = Some(until + paused_for);
284            }
285        }
286
287        self.paused_at = None;
288        self.ready_only = false;
289        self.fade_until = None;
290        self.resume_at = None;
291        self.resume_fade_ms = 0;
292    }
293
294    fn tick(&mut self) {
295        let now = Instant::now();
296        if self.fade_until.is_some_and(|at| now >= at) {
297            self.clear();
298            return;
299        }
300        if self.resume_at.is_some_and(|at| now >= at) {
301            self.resume_now(self.resume_fade_ms);
302        }
303
304        let fully_paused = self.paused_at.is_some_and(|at| now >= at);
305        if !self.looping
306            && !self.ready_only
307            && !fully_paused
308            && self.until.is_some_and(|at| now >= at)
309        {
310            self.clear();
311        }
312    }
313
314    fn is_playing(&mut self) -> bool {
315        self.tick();
316        if !self.has_source()
317            || self.fade_until.is_some()
318            || self.ready_only
319            || self.paused_at.is_some()
320            || self.resume_at.is_some()
321        {
322            return false;
323        }
324        self.looping || self.until.is_some()
325    }
326
327    fn is_fading_out(&mut self) -> bool {
328        self.tick();
329        self.fade_until.is_some()
330    }
331
332    fn play_pos_ms(&mut self) -> u64 {
333        self.tick();
334        let Some(duration) = self.duration_ms else {
335            return 0;
336        };
337        if self.ready_only || !self.has_source() {
338            return 0;
339        }
340        let now = self.paused_at.unwrap_or_else(Instant::now);
341        if let Some(until) = self.until {
342            let remaining = until.saturating_duration_since(now).as_millis() as u64;
343            return duration.saturating_sub(remaining.min(duration));
344        }
345        0
346    }
347
348    fn needs_tick(&self) -> bool {
349        self.resume_at.is_some() || self.fade_until.is_some()
350    }
351}
352
353pub struct SfxEngine {
354    project_dir: PathBuf,
355    sub_dir: String,
356    volume_raw: u8,
357    track_kind: TrackKind,
358    slots: Vec<Slot>,
359    /// `streamed_ogg_duration_ms` results by file.
360    #[cfg(not(target_arch = "wasm32"))]
361    streamed_durations: HashMap<PathBuf, Option<u64>>,
362}
363
364impl SfxEngine {
365    pub fn new(
366        project_dir: PathBuf,
367        sub_dir: impl Into<String>,
368        track_kind: TrackKind,
369        slot_cnt: usize,
370    ) -> Self {
371        Self {
372            project_dir,
373            sub_dir: sub_dir.into(),
374            volume_raw: 255,
375            track_kind,
376            slots: (0..slot_cnt).map(|_| Slot::default()).collect(),
377            #[cfg(not(target_arch = "wasm32"))]
378            streamed_durations: HashMap::new(),
379        }
380    }
381
382    pub fn slot_cnt(&self) -> usize {
383        self.slots.len()
384    }
385
386    pub fn volume_raw(&self) -> u8 {
387        self.volume_raw
388    }
389
390    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
391        self.volume_raw = volume_raw;
392        audio.set_track_volume_raw(self.track_kind, volume_raw);
393        Ok(())
394    }
395
396    pub fn set_volume_raw_fade(
397        &mut self,
398        audio: &mut AudioHub,
399        volume_raw: u8,
400        fade_ms: i64,
401    ) -> Result<()> {
402        self.volume_raw = volume_raw;
403        audio.set_track_volume_raw_fade(self.track_kind, volume_raw, fade_ms);
404        Ok(())
405    }
406
407    pub fn is_playing_any(&mut self) -> bool {
408        self.slots.iter_mut().any(|s| s.is_playing())
409    }
410
411    pub fn is_playing_slot(&mut self, slot: usize) -> bool {
412        self.slots
413            .get_mut(slot)
414            .map(|s| s.is_playing())
415            .unwrap_or(false)
416    }
417
418    pub fn slot_play_pos_ms(&mut self, slot: usize) -> u64 {
419        self.slots
420            .get_mut(slot)
421            .map(|s| s.play_pos_ms())
422            .unwrap_or(0)
423    }
424
425    pub fn is_fading_slot(&mut self, slot: usize) -> bool {
426        self.slots
427            .get_mut(slot)
428            .map(|s| s.is_fading_out())
429            .unwrap_or(false)
430    }
431
432    pub fn last_name_slot(&self, slot: usize) -> Option<&str> {
433        self.slots.get(slot).and_then(|s| s.last_name.as_deref())
434    }
435
436    pub fn stop_all(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
437        for slot in 0..self.slots.len() {
438            self.stop_slot(slot, fade_time_ms)?;
439        }
440        Ok(())
441    }
442
443    pub fn stop_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
444        let Some(s) = self.slots.get_mut(slot) else {
445            return Ok(());
446        };
447        s.tick();
448        let fade_ms = fade_time_ms.unwrap_or(0).max(0);
449        if fade_ms == 0 || !s.has_source() {
450            if let Some(mut handle) = s.handle.take() {
451                handle.stop(Tween::default());
452            }
453            s.clear();
454            return Ok(());
455        }
456
457        if let Some(handle) = &mut s.handle {
458            handle.stop(Slot::tween_for_ms(fade_ms));
459        }
460        s.until = None;
461        s.paused_at = None;
462        s.ready_only = false;
463        s.resume_at = None;
464        s.resume_fade_ms = 0;
465        s.fade_until = Some(Instant::now() + Duration::from_millis(fade_ms as u64));
466        Ok(())
467    }
468
469    pub fn play_file_name_in_slot(
470        &mut self,
471        audio: &mut AudioHub,
472        slot: usize,
473        file_name: &str,
474        loop_flag: bool,
475    ) -> Result<PathBuf> {
476        self.play_file_name_in_slot_with_options(audio, slot, file_name, loop_flag, 0, false)
477    }
478
479    pub fn play_file_name_in_slot_with_options(
480        &mut self,
481        audio: &mut AudioHub,
482        slot: usize,
483        file_name: &str,
484        loop_flag: bool,
485        fade_in_ms: i64,
486        ready_only: bool,
487    ) -> Result<PathBuf> {
488        if slot >= self.slots.len() {
489            bail!("slot out of range: {slot}");
490        }
491        let path = self.resolve_path(file_name)?;
492        #[cfg(not(target_arch = "wasm32"))]
493        if let Some(bytes) = self.streamable_ogg(&path)? {
494            let duration_ms = self.streamed_ogg_duration_ms(&path, &bytes)?;
495            let data = StreamingSoundData::from_cursor(Cursor::new(bytes))
496                .with_context(|| format!("kira: open OGG stream: {}", path.display()))?;
497            self.start_in_slot(
498                audio,
499                slot,
500                file_name,
501                Some(SlotSound::Streaming(data)),
502                duration_ms,
503                loop_flag,
504                fade_in_ms,
505                ready_only,
506            )?;
507            return Ok(path);
508        }
509        let wav = self.decode_to_wav(&path)?;
510        self.play_decoded_wav_in_slot_with_options(
511            audio, slot, file_name, wav, loop_flag, fade_in_ms, ready_only,
512        )?;
513        Ok(path)
514    }
515
516    pub fn play_koe_no_in_slot(
517        &mut self,
518        audio: &mut AudioHub,
519        slot: usize,
520        koe_no: i64,
521        loop_flag: bool,
522    ) -> Result<()> {
523        self.play_koe_no_in_slot_with_options(audio, slot, koe_no, loop_flag, 0, false)
524    }
525
526    pub fn play_koe_no_in_slot_with_options(
527        &mut self,
528        audio: &mut AudioHub,
529        slot: usize,
530        koe_no: i64,
531        loop_flag: bool,
532        fade_in_ms: i64,
533        ready_only: bool,
534    ) -> Result<()> {
535        if slot >= self.slots.len() {
536            bail!("slot out of range: {slot}");
537        }
538
539        let wav = self.decode_koe_no(koe_no)?;
540        self.play_decoded_wav_in_slot_with_options(
541            audio,
542            slot,
543            &format!("koe:{koe_no}"),
544            wav,
545            loop_flag,
546            fade_in_ms,
547            ready_only,
548        )
549    }
550
551    fn decode_koe_no(&self, koe_no: i64) -> Result<Vec<u8>> {
552        decode_koe_no_for_project(&self.project_dir, koe_no)
553    }
554
555    fn play_decoded_wav_in_slot(
556        &mut self,
557        audio: &mut AudioHub,
558        slot: usize,
559        display_name: &str,
560        wav: Vec<u8>,
561        loop_flag: bool,
562    ) -> Result<()> {
563        self.play_decoded_wav_in_slot_with_options(
564            audio,
565            slot,
566            display_name,
567            wav,
568            loop_flag,
569            0,
570            false,
571        )
572    }
573
574    fn play_decoded_wav_in_slot_with_options(
575        &mut self,
576        audio: &mut AudioHub,
577        slot: usize,
578        display_name: &str,
579        wav: Vec<u8>,
580        loop_flag: bool,
581        fade_in_ms: i64,
582        ready_only: bool,
583    ) -> Result<()> {
584        if slot >= self.slots.len() {
585            bail!("slot out of range: {slot}");
586        }
587        let duration_ms = wav_duration_ms(&wav).or(Some(2000));
588        let sound = if audio.is_enabled() {
589            Some(SlotSound::Static(
590                StaticSoundData::from_cursor(Cursor::new(wav)).context("kira: decode WAV bytes")?,
591            ))
592        } else {
593            None
594        };
595        self.start_in_slot(
596            audio,
597            slot,
598            display_name,
599            sound,
600            duration_ms,
601            loop_flag,
602            fade_in_ms,
603            ready_only,
604        )
605    }
606
607    /// A streamed OGG's length as the whole-file decode measured it (the
608    /// WAV `decode_to_wav` made): script waits end by it, and the stream's
609    /// own length can be tens of milliseconds longer. Counted once per file.
610    #[cfg(not(target_arch = "wasm32"))]
611    fn streamed_ogg_duration_ms(&mut self, path: &Path, bytes: &[u8]) -> Result<Option<u64>> {
612        if let Some(&duration) = self.streamed_durations.get(path) {
613            return Ok(duration);
614        }
615        let (channels, sample_rate, samples) =
616            siglus_assets::vorbis::ogg_vorbis_decoded_len(Cursor::new(bytes))
617                .with_context(|| format!("measure ogg: {}", path.display()))?;
618        // As `pcm16_to_wav_bytes` sizes the WAV and `wav_duration_ms` reads
619        // it back.
620        let data_size = u64::from((samples * 2) as u32);
621        let byte_rate = sample_rate.saturating_mul(u32::from(channels.saturating_mul(2)));
622        let duration = (byte_rate != 0)
623            .then(|| data_size * 1000 / u64::from(byte_rate))
624            .or(Some(2000));
625        self.streamed_durations.insert(path.to_path_buf(), duration);
626        Ok(duration)
627    }
628
629    /// The file's bytes when it is played as a stream (see
630    /// `STREAM_OGG_MIN_BYTES`). Encrypted OWP/OVK and small files are
631    /// decoded whole.
632    #[cfg(not(target_arch = "wasm32"))]
633    fn streamable_ogg(&self, path: &Path) -> Result<Option<Vec<u8>>> {
634        let is_ogg = path
635            .extension()
636            .and_then(|ext| ext.to_str())
637            .is_some_and(|ext| ext.eq_ignore_ascii_case("ogg"));
638        if !is_ogg {
639            return Ok(None);
640        }
641        let bytes = crate::resource::read_file_bytes(path)
642            .with_context(|| format!("read ogg: {}", path.display()))?;
643        Ok((bytes.len() as u64 >= STREAM_OGG_MIN_BYTES).then_some(bytes))
644    }
645
646    #[allow(clippy::too_many_arguments)]
647    fn start_in_slot(
648        &mut self,
649        audio: &mut AudioHub,
650        slot: usize,
651        display_name: &str,
652        sound: Option<SlotSound>,
653        duration_ms: Option<u64>,
654        loop_flag: bool,
655        fade_in_ms: i64,
656        ready_only: bool,
657    ) -> Result<()> {
658        // C_tnm_player::reinit/release_sound discards the previous slot source.
659        self.stop_slot(slot, None)?;
660
661        let mut handle = None;
662        if let Some(sound) = sound.filter(|_| audio.is_enabled()) {
663            let mut new_handle = match sound {
664                SlotSound::Static(mut data) => {
665                    if loop_flag {
666                        data = data.loop_region(0.0..);
667                    }
668                    SlotHandle::Static(audio.play_static(self.track_kind, data)?)
669                }
670                #[cfg(not(target_arch = "wasm32"))]
671                SlotSound::Streaming(mut data) => {
672                    if loop_flag {
673                        data = data.loop_region(0.0..);
674                    }
675                    SlotHandle::Streaming(audio.play_streaming(self.track_kind, data)?)
676                }
677            };
678            let amplitude = self.slots[slot].amplitude();
679            if ready_only {
680                new_handle.set_volume(Volume::Amplitude(amplitude), Tween::default());
681                new_handle.pause(Tween::default());
682            } else if fade_in_ms > 0 {
683                new_handle.set_volume(Volume::Amplitude(0.0), Tween::default());
684                new_handle.set_volume(Volume::Amplitude(amplitude), Slot::tween_for_ms(fade_in_ms));
685            } else {
686                // Natural one-shot playback stays at the requested amplitude until
687                // EOF. The previous implementation scheduled a zero-volume tween
688                // here, which made long voice lines decay while they were playing.
689                new_handle.set_volume(Volume::Amplitude(amplitude), Tween::default());
690            }
691            handle = Some(new_handle);
692        }
693
694        let now = Instant::now();
695        let s = &mut self.slots[slot];
696        s.handle = handle;
697        s.duration_ms = duration_ms;
698        s.looping = loop_flag;
699        s.last_name = Some(display_name.to_string());
700        s.ready_only = ready_only;
701        s.fade_until = None;
702        s.paused_at = ready_only.then_some(now);
703        s.resume_at = None;
704        s.resume_fade_ms = 0;
705        s.until = if ready_only || loop_flag {
706            None
707        } else {
708            duration_ms.map(|ms| now + Duration::from_millis(ms))
709        };
710
711        Ok(())
712    }
713
714    pub fn slot_volume_raw(&self, slot: usize) -> u8 {
715        self.slots.get(slot).map(|s| s.volume_raw).unwrap_or(255)
716    }
717
718    pub fn slot_system_volume_raw(&self, slot: usize) -> u8 {
719        self.slots
720            .get(slot)
721            .map(|s| s.system_volume_raw)
722            .unwrap_or(255)
723    }
724
725    pub fn slot_duration_ms(&self, slot: usize) -> u64 {
726        self.slots
727            .get(slot)
728            .and_then(|s| s.duration_ms)
729            .unwrap_or(0)
730    }
731
732    pub fn slot_loop_flag(&self, slot: usize) -> bool {
733        self.slots.get(slot).map(|s| s.looping).unwrap_or(false)
734    }
735
736    pub fn slot_ready_flag(&self, slot: usize) -> bool {
737        self.slots.get(slot).map(|s| s.ready_only).unwrap_or(false)
738    }
739
740    pub fn slot_resume_delay_ms(&self, slot: usize) -> i64 {
741        self.slots
742            .get(slot)
743            .and_then(|s| s.resume_at)
744            .map(|deadline| {
745                deadline
746                    .saturating_duration_since(Instant::now())
747                    .as_millis()
748                    .min(i64::MAX as u128) as i64
749            })
750            .unwrap_or(0)
751    }
752
753    pub fn set_slot_volume_raw_fade(
754        &mut self,
755        slot: usize,
756        volume_raw: u8,
757        fade_ms: i64,
758    ) -> Result<()> {
759        let Some(s) = self.slots.get_mut(slot) else {
760            return Ok(());
761        };
762        s.volume_raw = volume_raw;
763        let amplitude = s.amplitude();
764        if let Some(handle) = &mut s.handle {
765            handle.set_volume(
766                Volume::Amplitude(amplitude),
767                Slot::tween_for_ms(fade_ms.max(0)),
768            );
769        }
770        Ok(())
771    }
772
773    pub fn set_slot_system_volume_raw(&mut self, slot: usize, volume_raw: u8) -> Result<()> {
774        let Some(s) = self.slots.get_mut(slot) else {
775            return Ok(());
776        };
777        if s.system_volume_raw == volume_raw {
778            return Ok(());
779        }
780        s.system_volume_raw = volume_raw;
781        let amplitude = s.amplitude();
782        if let Some(handle) = &mut s.handle {
783            handle.set_volume(Volume::Amplitude(amplitude), Tween::default());
784        }
785        Ok(())
786    }
787
788    pub fn pause_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
789        let Some(s) = self.slots.get_mut(slot) else {
790            return Ok(());
791        };
792        s.tick();
793        if !s.has_source() || s.ready_only || s.paused_at.is_some() || s.resume_at.is_some() {
794            return Ok(());
795        }
796        let fade_ms = fade_time_ms.unwrap_or(0).max(0);
797        if let Some(handle) = &mut s.handle {
798            handle.pause(Slot::tween_for_ms(fade_ms));
799        }
800        let now = Instant::now();
801        s.paused_at = Some(now + Duration::from_millis(fade_ms as u64));
802        s.resume_at = None;
803        s.resume_fade_ms = 0;
804        Ok(())
805    }
806
807    pub fn resume_slot(&mut self, slot: usize, fade_ms: i64, delay_ms: i64) -> Result<()> {
808        let Some(s) = self.slots.get_mut(slot) else {
809            return Ok(());
810        };
811        s.tick();
812        if !s.has_source() || s.fade_until.is_some() || (!s.ready_only && s.paused_at.is_none()) {
813            return Ok(());
814        }
815        let delay_ms = delay_ms.max(0);
816        if delay_ms == 0 {
817            s.resume_now(fade_ms.max(0));
818        } else {
819            s.resume_at = Some(Instant::now() + Duration::from_millis(delay_ms as u64));
820            s.resume_fade_ms = fade_ms.max(0);
821        }
822        Ok(())
823    }
824
825    pub fn tick(&mut self) {
826        for slot in &mut self.slots {
827            slot.tick();
828        }
829    }
830
831    pub fn needs_tick(&self) -> bool {
832        self.slots.iter().any(Slot::needs_tick)
833    }
834
835    fn resolve_path(&self, file_name: &str) -> Result<PathBuf> {
836        let direct = Path::new(file_name);
837        if path_exists(direct) {
838            return Ok(direct.to_path_buf());
839        }
840
841        if let Ok((path, _ty)) = crate::resource::find_audio_path_with_append_dir(
842            &self.project_dir,
843            "",
844            &self.sub_dir,
845            file_name,
846        ) {
847            return Ok(path);
848        }
849
850        let dir = self.project_dir.join(&self.sub_dir);
851        let base = dir.join(file_name);
852
853        if base.extension().is_some() && path_exists(&base) {
854            return Ok(base);
855        }
856
857        let candidates = ["wav", "nwa", "ogg", "owp", "ovk"];
858        for ext in candidates {
859            let p = base.with_extension(ext);
860            if path_exists(&p) {
861                return Ok(p);
862            }
863        }
864
865        bail!(
866            "sound file not found: name={:?} (project_dir={:?}, sub_dir={:?})",
867            file_name,
868            self.project_dir,
869            self.sub_dir
870        );
871    }
872
873    fn decode_to_wav(&self, path: &Path) -> Result<Vec<u8>> {
874        let ext = path
875            .extension()
876            .and_then(|s| s.to_str())
877            .unwrap_or("")
878            .to_ascii_lowercase();
879
880        match ext.as_str() {
881            "wav" => crate::resource::read_file_bytes(path)
882                .with_context(|| format!("read wav: {}", path.display())),
883            "nwa" | "ogg" | "owp" | "ovk" => {
884                let decoded = decode_bgm_to_wav_bytes(path, None)
885                    .with_context(|| format!("decode audio: {}", path.display()))?;
886                Ok(decoded.wav_bytes)
887            }
888            _ => Err(anyhow!("unsupported sound extension: {}", path.display())),
889        }
890    }
891}
892
893fn path_exists(path: &Path) -> bool {
894    crate::resource::game_file_exists(path)
895}
896
897pub struct PcmEngine {
898    inner: SfxEngine,
899}
900
901impl PcmEngine {
902    pub fn new(project_dir: PathBuf) -> Self {
903        // The original has one independent global PCM player plus a 16-entry
904        // PCMCH list. Keep them separate internally; channel N maps to N + 1.
905        Self {
906            inner: SfxEngine::new(project_dir, "wav", TrackKind::Pcm, 17),
907        }
908    }
909
910    fn channel_slot(channel: usize) -> usize {
911        channel.saturating_add(1)
912    }
913
914    pub fn play_file_name(&mut self, audio: &mut AudioHub, file_name: &str) -> Result<PathBuf> {
915        self.inner
916            .play_file_name_in_slot(audio, 0, file_name, false)
917    }
918
919    pub fn play_koe_no(&mut self, audio: &mut AudioHub, koe_no: i64) -> Result<()> {
920        self.inner.play_koe_no_in_slot(audio, 0, koe_no, false)
921    }
922
923    pub fn play_in_slot(
924        &mut self,
925        audio: &mut AudioHub,
926        slot: usize,
927        file_name: &str,
928        loop_flag: bool,
929    ) -> Result<PathBuf> {
930        self.inner
931            .play_file_name_in_slot(audio, Self::channel_slot(slot), file_name, loop_flag)
932    }
933
934    pub fn play_in_slot_with_options(
935        &mut self,
936        audio: &mut AudioHub,
937        slot: usize,
938        file_name: &str,
939        loop_flag: bool,
940        fade_in_ms: i64,
941        ready_only: bool,
942    ) -> Result<PathBuf> {
943        self.inner.play_file_name_in_slot_with_options(
944            audio,
945            Self::channel_slot(slot),
946            file_name,
947            loop_flag,
948            fade_in_ms,
949            ready_only,
950        )
951    }
952
953    pub fn play_koe_no_in_slot(
954        &mut self,
955        audio: &mut AudioHub,
956        slot: usize,
957        koe_no: i64,
958        loop_flag: bool,
959    ) -> Result<()> {
960        self.inner
961            .play_koe_no_in_slot(audio, Self::channel_slot(slot), koe_no, loop_flag)
962    }
963
964    pub fn play_koe_no_in_slot_with_options(
965        &mut self,
966        audio: &mut AudioHub,
967        slot: usize,
968        koe_no: i64,
969        loop_flag: bool,
970        fade_in_ms: i64,
971        ready_only: bool,
972    ) -> Result<()> {
973        self.inner.play_koe_no_in_slot_with_options(
974            audio,
975            Self::channel_slot(slot),
976            koe_no,
977            loop_flag,
978            fade_in_ms,
979            ready_only,
980        )
981    }
982
983    pub fn play_decoded_wav_in_slot(
984        &mut self,
985        audio: &mut AudioHub,
986        slot: usize,
987        display_name: &str,
988        wav: Vec<u8>,
989        loop_flag: bool,
990    ) -> Result<()> {
991        self.inner.play_decoded_wav_in_slot(
992            audio,
993            Self::channel_slot(slot),
994            display_name,
995            wav,
996            loop_flag,
997        )
998    }
999
1000    pub fn play_decoded_wav_in_slot_with_options(
1001        &mut self,
1002        audio: &mut AudioHub,
1003        slot: usize,
1004        display_name: &str,
1005        wav: Vec<u8>,
1006        loop_flag: bool,
1007        fade_in_ms: i64,
1008        ready_only: bool,
1009    ) -> Result<()> {
1010        self.inner.play_decoded_wav_in_slot_with_options(
1011            audio,
1012            Self::channel_slot(slot),
1013            display_name,
1014            wav,
1015            loop_flag,
1016            fade_in_ms,
1017            ready_only,
1018        )
1019    }
1020
1021    pub fn stop(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
1022        self.inner.stop_slot(0, fade_time_ms)
1023    }
1024
1025    pub fn stop_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
1026        self.inner.stop_slot(Self::channel_slot(slot), fade_time_ms)
1027    }
1028
1029    pub fn stop_all(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
1030        self.inner.stop_all(fade_time_ms)
1031    }
1032
1033    pub fn pause_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
1034        self.inner
1035            .pause_slot(Self::channel_slot(slot), fade_time_ms)
1036    }
1037
1038    pub fn resume_slot(&mut self, slot: usize, fade_ms: i64, delay_ms: i64) -> Result<()> {
1039        self.inner
1040            .resume_slot(Self::channel_slot(slot), fade_ms, delay_ms)
1041    }
1042
1043    pub fn tick(&mut self) {
1044        self.inner.tick();
1045    }
1046
1047    pub fn needs_tick(&self) -> bool {
1048        self.inner.needs_tick()
1049    }
1050
1051    pub fn is_playing_any(&mut self) -> bool {
1052        self.inner.is_playing_any()
1053    }
1054
1055    pub fn is_playing_slot(&mut self, slot: usize) -> bool {
1056        self.inner.is_playing_slot(Self::channel_slot(slot))
1057    }
1058
1059    pub fn is_fading_slot(&mut self, slot: usize) -> bool {
1060        self.inner.is_fading_slot(Self::channel_slot(slot))
1061    }
1062
1063    pub fn slot_volume_raw(&self, slot: usize) -> u8 {
1064        self.inner.slot_volume_raw(Self::channel_slot(slot))
1065    }
1066
1067    pub fn slot_loop_flag(&self, slot: usize) -> bool {
1068        self.inner.slot_loop_flag(Self::channel_slot(slot))
1069    }
1070
1071    pub fn slot_ready_flag(&self, slot: usize) -> bool {
1072        self.inner.slot_ready_flag(Self::channel_slot(slot))
1073    }
1074
1075    pub fn slot_resume_delay_ms(&self, slot: usize) -> i64 {
1076        self.inner.slot_resume_delay_ms(Self::channel_slot(slot))
1077    }
1078
1079    pub fn set_slot_volume_raw_fade(
1080        &mut self,
1081        slot: usize,
1082        volume_raw: u8,
1083        fade_ms: i64,
1084    ) -> Result<()> {
1085        self.inner
1086            .set_slot_volume_raw_fade(Self::channel_slot(slot), volume_raw, fade_ms)
1087    }
1088
1089    pub fn global_duration_ms(&self) -> u64 {
1090        self.inner.slot_duration_ms(0)
1091    }
1092
1093    pub fn slot_duration_ms(&self, slot: usize) -> u64 {
1094        self.inner.slot_duration_ms(Self::channel_slot(slot))
1095    }
1096
1097    pub fn set_global_system_volume_raw(&mut self, volume_raw: u8) -> Result<()> {
1098        self.inner.set_slot_system_volume_raw(0, volume_raw)
1099    }
1100
1101    pub fn set_slot_system_volume_raw(&mut self, slot: usize, volume_raw: u8) -> Result<()> {
1102        self.inner
1103            .set_slot_system_volume_raw(Self::channel_slot(slot), volume_raw)
1104    }
1105
1106    pub fn volume_raw(&self) -> u8 {
1107        self.inner.volume_raw()
1108    }
1109
1110    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
1111        self.inner.set_volume_raw(audio, volume_raw)
1112    }
1113
1114    pub fn set_volume_raw_fade(
1115        &mut self,
1116        audio: &mut AudioHub,
1117        volume_raw: u8,
1118        fade_ms: i64,
1119    ) -> Result<()> {
1120        self.inner.set_volume_raw_fade(audio, volume_raw, fade_ms)
1121    }
1122}
1123
1124fn load_koe_mouth_volume_table(
1125    project_dir: &Path,
1126    current_append_dir: &str,
1127    koe_no: i64,
1128) -> Result<Vec<f32>> {
1129    let name = format!("z{koe_no:09}.vol.csv");
1130    let Some(path) =
1131        crate::resource::resolve_dat_file_path(project_dir, current_append_dir, &name)?
1132    else {
1133        // Original play_koe accepts a missing .vol.csv and simply leaves the
1134        // mouth-volume table empty.
1135        return Ok(Vec::new());
1136    };
1137
1138    let bytes = crate::resource::read_file_bytes(&path)
1139        .with_context(|| format!("read KOE mouth-volume table: {}", path.display()))?;
1140    if bytes.len() % 2 != 0 {
1141        bail!(
1142            "KOE mouth-volume table has odd UTF-16 byte length: {}",
1143            path.display()
1144        );
1145    }
1146
1147    let (payload, big_endian) = if bytes.starts_with(&[0xff, 0xfe]) {
1148        (&bytes[2..], false)
1149    } else if bytes.starts_with(&[0xfe, 0xff]) {
1150        (&bytes[2..], true)
1151    } else {
1152        (&bytes[..], false)
1153    };
1154    let units: Vec<u16> = payload
1155        .as_chunks::<2>()
1156        .0
1157        .iter()
1158        .map(|pair| {
1159            if big_endian {
1160                u16::from_be_bytes([pair[0], pair[1]])
1161            } else {
1162                u16::from_le_bytes([pair[0], pair[1]])
1163            }
1164        })
1165        .collect();
1166    let text = String::from_utf16(&units).with_context(|| {
1167        format!(
1168            "decode KOE mouth-volume table as UTF-16: {}",
1169            path.display()
1170        )
1171    })?;
1172    let first_line = text.lines().next().unwrap_or("");
1173    if first_line.is_empty() {
1174        bail!("KOE mouth-volume table is empty: {}", path.display());
1175    }
1176
1177    first_line
1178        .split(',')
1179        .map(|part| {
1180            part.trim().parse::<f32>().with_context(|| {
1181                format!("parse KOE mouth-volume value {part:?}: {}", path.display())
1182            })
1183        })
1184        .collect()
1185}
1186
1187type PendingKoeDecode = (
1188    (i64, u16),
1189    String,
1190    std::sync::mpsc::Receiver<Result<Vec<u8>, String>>,
1191);
1192
1193pub struct KoeEngine {
1194    inner: SfxEngine,
1195    current_koe_no: i64,
1196    /// Original C_tnm_player owns the mouth table together with the currently
1197    /// loaded KOE. It is rebuilt on every play_koe() reinit, not cached by
1198    /// voice number across append-directory changes.
1199    mouth_volume_table: Vec<f32>,
1200    /// Asynchronous decode in flight: the voice is decoded on a worker thread
1201    /// so a 300ms Vorbis decode never freezes the frame loop.
1202    pending_decode: Option<PendingKoeDecode>,
1203    /// Decoded/JITAN-converted voice cache keyed by (koe_no, jitan_rate).
1204    /// The original re-runs JITAN when the configured rate changes.
1205    decode_cache: HashMap<(i64, u16), std::sync::Arc<Vec<u8>>>,
1206}
1207
1208impl KoeEngine {
1209    /// HUD-only accounting for decoded voice data retained by the KOE cache.
1210    pub fn debug_cache_memory(&self) -> (usize, usize) {
1211        let bytes = self.decode_cache.values().map(|wav| wav.len()).sum();
1212        (bytes, self.decode_cache.len())
1213    }
1214
1215    pub fn new(project_dir: PathBuf) -> Self {
1216        // Original engine: C_elm_koe owns one active voice player and stops it
1217        // before starting the next KOE.
1218        Self {
1219            inner: SfxEngine::new(project_dir, "wav", TrackKind::Koe, 1),
1220            current_koe_no: -1,
1221            mouth_volume_table: Vec::new(),
1222            pending_decode: None,
1223            decode_cache: HashMap::new(),
1224        }
1225    }
1226
1227    pub fn play_koe_no(
1228        &mut self,
1229        audio: &mut AudioHub,
1230        koe_no: i64,
1231        current_append_dir: &str,
1232    ) -> Result<()> {
1233        self.play_koe_no_with_rate(audio, koe_no, current_append_dir, 100)
1234    }
1235
1236    pub fn play_koe_no_with_rate(
1237        &mut self,
1238        audio: &mut AudioHub,
1239        koe_no: i64,
1240        current_append_dir: &str,
1241        jitan_rate: u16,
1242    ) -> Result<()> {
1243        // C_tnm_player::play_koe starts with reinit(): clear the old player
1244        // metadata and mouth table before resolving/loading the new voice.
1245        let _ = self.stop(None);
1246        self.current_koe_no = -1;
1247        self.mouth_volume_table.clear();
1248
1249        if koe_no < 0 {
1250            return Ok(());
1251        }
1252
1253        // C_jitan_cnv::convert clamps its low-level input to 100..400.  Normal
1254        // configuration commands clamp JITAN_SPEED to 100..300, but preserving
1255        // the converter clamp also matches loaded/legacy config values.
1256        let jitan_rate = jitan_rate.clamp(100, 400);
1257        let cache_key = (koe_no, jitan_rate);
1258        if let Some(wav) = self.decode_cache.get(&cache_key).cloned() {
1259            self.finish_koe_start(audio, koe_no, current_append_dir, (*wav).clone())?;
1260            return Ok(());
1261        }
1262
1263        // Decode and perform the original on-memory JITAN conversion on the
1264        // worker.  This mirrors elm_sound_player.cpp: decode KOE first, then
1265        // construct a mono time-compressed memory sound before playback.
1266        let project_dir = self.inner.project_dir.clone();
1267        let append_dir = current_append_dir.to_string();
1268        let (tx, rx) = std::sync::mpsc::channel();
1269        std::thread::spawn(move || {
1270            let result = decode_koe_no_for_project(&project_dir, koe_no)
1271                .and_then(|wav| crate::audio::jitan::convert_koe_wav(wav, jitan_rate))
1272                .map_err(|err| format!("{err:#}"));
1273            let _ = tx.send(result);
1274        });
1275        self.pending_decode = Some((cache_key, append_dir, rx));
1276        Ok(())
1277    }
1278
1279    fn finish_koe_start(
1280        &mut self,
1281        audio: &mut AudioHub,
1282        koe_no: i64,
1283        current_append_dir: &str,
1284        wav: Vec<u8>,
1285    ) -> Result<()> {
1286        self.mouth_volume_table =
1287            load_koe_mouth_volume_table(&self.inner.project_dir, current_append_dir, koe_no)?;
1288        self.inner.play_decoded_wav_in_slot_with_options(
1289            audio,
1290            0,
1291            &format!("koe:{koe_no}"),
1292            wav,
1293            false,
1294            0,
1295            false,
1296        )?;
1297        self.current_koe_no = koe_no;
1298        Ok(())
1299    }
1300
1301    pub fn tick(&mut self, audio: &mut AudioHub) {
1302        let Some((cache_key, append_dir, rx)) = self.pending_decode.take() else {
1303            return;
1304        };
1305        let koe_no = cache_key.0;
1306        // Virtual-clock replays wait for the decode, so the frame a voice
1307        // starts on does not depend on the worker thread's speed.
1308        #[cfg(feature = "virtual-clock")]
1309        let received = rx
1310            .recv()
1311            .map_err(|_| std::sync::mpsc::TryRecvError::Disconnected);
1312        #[cfg(not(feature = "virtual-clock"))]
1313        let received = rx.try_recv();
1314        match received {
1315            Ok(Ok(wav)) => {
1316                if self.decode_cache.len() < 24 {
1317                    self.decode_cache
1318                        .insert(cache_key, std::sync::Arc::new(wav.clone()));
1319                }
1320                if let Err(err) = self.finish_koe_start(audio, koe_no, &append_dir, wav) {
1321                    log::warn!("koe async start failed koe_no={koe_no}: {err:#}");
1322                }
1323            }
1324            Ok(Err(err)) => {
1325                log::warn!("koe async decode failed koe_no={koe_no}: {err}");
1326            }
1327            Err(std::sync::mpsc::TryRecvError::Empty) => {
1328                self.pending_decode = Some((cache_key, append_dir, rx));
1329            }
1330            Err(std::sync::mpsc::TryRecvError::Disconnected) => {
1331                log::warn!("koe async decode worker died koe_no={koe_no}");
1332            }
1333        }
1334    }
1335
1336    pub fn stop(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
1337        self.pending_decode = None;
1338        self.inner.stop_slot(0, fade_time_ms)
1339    }
1340
1341    pub fn is_playing_any(&mut self) -> bool {
1342        self.pending_decode.is_some() || self.inner.is_playing_any()
1343    }
1344
1345    pub fn current_koe_no(&self) -> i64 {
1346        self.current_koe_no
1347    }
1348
1349    pub fn current_play_pos_ms(&mut self) -> u64 {
1350        self.inner.slot_play_pos_ms(0)
1351    }
1352
1353    pub fn current_mouth_volume(&mut self) -> f32 {
1354        if !self.inner.is_playing_slot(0) {
1355            return 0.0;
1356        }
1357        let frame = (self.inner.slot_play_pos_ms(0).saturating_mul(60) / 1000) as usize;
1358        self.mouth_volume_table.get(frame).copied().unwrap_or(0.0)
1359    }
1360
1361    pub fn volume_raw(&self) -> u8 {
1362        self.inner.volume_raw()
1363    }
1364
1365    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
1366        self.inner.set_volume_raw(audio, volume_raw)
1367    }
1368
1369    pub fn set_volume_raw_fade(
1370        &mut self,
1371        audio: &mut AudioHub,
1372        volume_raw: u8,
1373        fade_ms: i64,
1374    ) -> Result<()> {
1375        self.inner.set_volume_raw_fade(audio, volume_raw, fade_ms)
1376    }
1377}
1378
1379pub struct SeEngine {
1380    inner: SfxEngine,
1381}
1382
1383impl SeEngine {
1384    pub fn new(project_dir: PathBuf) -> Self {
1385        // Original engine: TNM_SE_PLAYER_CNT = 16.
1386        Self {
1387            inner: SfxEngine::new(project_dir, "wav", TrackKind::Se, 16),
1388        }
1389    }
1390
1391    pub fn play_file_name(&mut self, audio: &mut AudioHub, file_name: &str) -> Result<PathBuf> {
1392        self.inner
1393            .play_file_name_in_slot(audio, 0, file_name, false)
1394    }
1395
1396    pub fn play_koe_no(&mut self, audio: &mut AudioHub, koe_no: i64) -> Result<()> {
1397        self.inner.play_koe_no_in_slot(audio, 0, koe_no, false)
1398    }
1399
1400    pub fn play_in_slot(
1401        &mut self,
1402        audio: &mut AudioHub,
1403        slot: usize,
1404        file_name: &str,
1405        loop_flag: bool,
1406    ) -> Result<PathBuf> {
1407        self.inner
1408            .play_file_name_in_slot(audio, slot, file_name, loop_flag)
1409    }
1410
1411    pub fn play_koe_no_in_slot(
1412        &mut self,
1413        audio: &mut AudioHub,
1414        slot: usize,
1415        koe_no: i64,
1416        loop_flag: bool,
1417    ) -> Result<()> {
1418        self.inner
1419            .play_koe_no_in_slot(audio, slot, koe_no, loop_flag)
1420    }
1421
1422    pub fn play_decoded_wav_in_slot(
1423        &mut self,
1424        audio: &mut AudioHub,
1425        slot: usize,
1426        display_name: &str,
1427        wav: Vec<u8>,
1428        loop_flag: bool,
1429    ) -> Result<()> {
1430        self.inner
1431            .play_decoded_wav_in_slot(audio, slot, display_name, wav, loop_flag)
1432    }
1433
1434    pub fn stop(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
1435        self.inner.stop_all(fade_time_ms)
1436    }
1437
1438    pub fn stop_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
1439        self.inner.stop_slot(slot, fade_time_ms)
1440    }
1441
1442    pub fn is_playing_any(&mut self) -> bool {
1443        self.inner.is_playing_any()
1444    }
1445
1446    pub fn is_playing_slot(&mut self, slot: usize) -> bool {
1447        self.inner.is_playing_slot(slot)
1448    }
1449
1450    pub fn volume_raw(&self) -> u8 {
1451        self.inner.volume_raw()
1452    }
1453
1454    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
1455        self.inner.set_volume_raw(audio, volume_raw)
1456    }
1457
1458    pub fn set_volume_raw_fade(
1459        &mut self,
1460        audio: &mut AudioHub,
1461        volume_raw: u8,
1462        fade_ms: i64,
1463    ) -> Result<()> {
1464        self.inner.set_volume_raw_fade(audio, volume_raw, fade_ms)
1465    }
1466
1467    pub fn last_name(&self) -> Option<&str> {
1468        self.inner.last_name_slot(0)
1469    }
1470}