Skip to main content

siglus_scene_vm/audio/
sfx_engine.rs

1use std::fs;
2use std::io::Cursor;
3use std::path::{Path, PathBuf};
4use crate::platform_time::{Duration, Instant};
5
6use anyhow::{anyhow, bail, Context, Result};
7
8use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
9use kira::tween::Tween;
10
11use crate::audio::bgm::{
12    decode_bgm_to_wav_bytes, decode_ovk_entry_by_no_to_wav_bytes, resolve_koe_source, KoeSource,
13};
14use crate::audio::{AudioHub, TrackKind};
15
16/// Best-effort WAV duration parsing for bring-up.
17///
18/// We use this only to implement WAIT-style commands without relying on
19/// backend handle state APIs (which differ across Kira versions).
20pub(crate) fn wav_duration_ms(wav: &[u8]) -> Option<u64> {
21    // Minimal RIFF/WAVE parser.
22    if wav.len() < 44 {
23        return None;
24    }
25    if &wav[0..4] != b"RIFF" || &wav[8..12] != b"WAVE" {
26        return None;
27    }
28
29    let mut pos = 12usize;
30    let mut byte_rate: Option<u32> = None;
31    let mut data_size: Option<u32> = None;
32
33    while pos + 8 <= wav.len() {
34        let id = &wav[pos..pos + 4];
35        let sz =
36            u32::from_le_bytes([wav[pos + 4], wav[pos + 5], wav[pos + 6], wav[pos + 7]]) as usize;
37        pos += 8;
38        if pos + sz > wav.len() {
39            break;
40        }
41        if id == b"fmt " {
42            if sz >= 16 {
43                // byte_rate is at offset 8 within fmt chunk.
44                let off = pos + 8;
45                if off + 4 <= wav.len() {
46                    byte_rate = Some(u32::from_le_bytes([
47                        wav[off],
48                        wav[off + 1],
49                        wav[off + 2],
50                        wav[off + 3],
51                    ]));
52                }
53            }
54        } else if id == b"data" {
55            data_size = Some(sz as u32);
56        }
57
58        // Chunks are word-aligned.
59        pos += sz;
60        if (sz & 1) != 0 {
61            pos += 1;
62        }
63
64        if byte_rate.is_some() && data_size.is_some() {
65            break;
66        }
67    }
68
69    let br = byte_rate?;
70    if br == 0 {
71        return None;
72    }
73    let ds = data_size? as u64;
74    Some((ds * 1000) / (br as u64))
75}
76
77#[derive(Debug, Default)]
78struct Slot {
79    handle: Option<StaticSoundHandle>,
80    until: Option<Instant>,
81    looping: bool,
82    last_name: Option<String>,
83}
84
85impl Slot {
86    fn is_playing(&mut self) -> bool {
87        if self.looping {
88            return self.handle.is_some();
89        }
90        if let Some(t) = self.until {
91            if Instant::now() >= t {
92                // Drop the handle reference; the backend should have ended.
93                self.until = None;
94                self.handle = None;
95                self.last_name = None;
96            }
97        }
98        self.until.is_some() && self.handle.is_some()
99    }
100}
101
102pub struct SfxEngine {
103    project_dir: PathBuf,
104    sub_dir: String,
105    volume_raw: u8,
106    track_kind: TrackKind,
107    slots: Vec<Slot>,
108}
109
110impl SfxEngine {
111    pub fn new(
112        project_dir: PathBuf,
113        sub_dir: impl Into<String>,
114        track_kind: TrackKind,
115        slot_cnt: usize,
116    ) -> Self {
117        Self {
118            project_dir,
119            sub_dir: sub_dir.into(),
120            volume_raw: 255,
121            track_kind,
122            slots: (0..slot_cnt).map(|_| Slot::default()).collect(),
123        }
124    }
125
126    pub fn slot_cnt(&self) -> usize {
127        self.slots.len()
128    }
129
130    pub fn volume_raw(&self) -> u8 {
131        self.volume_raw
132    }
133
134    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
135        self.volume_raw = volume_raw;
136        audio.set_track_volume_raw(self.track_kind, volume_raw);
137        Ok(())
138    }
139
140    pub fn set_volume_raw_fade(
141        &mut self,
142        audio: &mut AudioHub,
143        volume_raw: u8,
144        fade_ms: i64,
145    ) -> Result<()> {
146        self.volume_raw = volume_raw;
147        audio.set_track_volume_raw_fade(self.track_kind, volume_raw, fade_ms);
148        Ok(())
149    }
150
151    pub fn is_playing_any(&mut self) -> bool {
152        self.slots.iter_mut().any(|s| s.is_playing())
153    }
154
155    pub fn is_playing_slot(&mut self, slot: usize) -> bool {
156        self.slots
157            .get_mut(slot)
158            .map(|s| s.is_playing())
159            .unwrap_or(false)
160    }
161
162    pub fn last_name_slot(&self, slot: usize) -> Option<&str> {
163        self.slots.get(slot).and_then(|s| s.last_name.as_deref())
164    }
165
166    pub fn stop_all(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
167        for s in &mut self.slots {
168            if let Some(mut h) = s.handle.take() {
169                let tween = fade_time_ms
170                    .and_then(|v| {
171                        if v > 0 {
172                            Some(Duration::from_millis(v as u64))
173                        } else {
174                            None
175                        }
176                    })
177                    .map(|duration| Tween {
178                        duration,
179                        ..Tween::default()
180                    })
181                    .unwrap_or_default();
182                let _ = h.stop(tween);
183            }
184            s.until = None;
185            s.looping = false;
186            s.last_name = None;
187        }
188        Ok(())
189    }
190
191    pub fn stop_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
192        let Some(s) = self.slots.get_mut(slot) else {
193            return Ok(());
194        };
195        if let Some(mut h) = s.handle.take() {
196            let tween = fade_time_ms
197                .and_then(|v| {
198                    if v > 0 {
199                        Some(Duration::from_millis(v as u64))
200                    } else {
201                        None
202                    }
203                })
204                .map(|duration| Tween {
205                    duration,
206                    ..Tween::default()
207                })
208                .unwrap_or_default();
209            let _ = h.stop(tween);
210        }
211        s.until = None;
212        s.looping = false;
213        s.last_name = None;
214        Ok(())
215    }
216
217    pub fn play_file_name_in_slot(
218        &mut self,
219        audio: &mut AudioHub,
220        slot: usize,
221        file_name: &str,
222        loop_flag: bool,
223    ) -> Result<PathBuf> {
224        if slot >= self.slots.len() {
225            bail!("slot out of range: {slot}");
226        }
227        let path = self.resolve_path(file_name)?;
228        let wav = self.decode_to_wav(&path)?;
229        self.play_decoded_wav_in_slot(audio, slot, file_name, wav, loop_flag)?;
230        Ok(path)
231    }
232
233    pub fn play_koe_no_in_slot(
234        &mut self,
235        audio: &mut AudioHub,
236        slot: usize,
237        koe_no: i64,
238        loop_flag: bool,
239    ) -> Result<()> {
240        if slot >= self.slots.len() {
241            bail!("slot out of range: {slot}");
242        }
243
244        let resolved = resolve_koe_source(&self.project_dir, koe_no)?;
245        let wav = match &resolved {
246            KoeSource::File(path) => {
247                decode_bgm_to_wav_bytes(path, None)
248                    .with_context(|| format!("decode KOE file: {}", path.display()))?
249                    .wav_bytes
250            }
251            KoeSource::OvkEntryByNo { path, entry_no } => {
252                decode_ovk_entry_by_no_to_wav_bytes(path, *entry_no)
253                    .with_context(|| {
254                        format!("decode KOE OVK entry: {}#{entry_no}", path.display())
255                    })?
256                    .wav_bytes
257            }
258        };
259        if std::env::var_os("SG_AUDIO_TRACE").is_some() {
260            eprintln!(
261                "[SG_AUDIO_TRACE] koe resolved koe_no={} source={:?} wav_ms={:?}",
262                koe_no,
263                resolved,
264                wav_duration_ms(&wav)
265            );
266        }
267
268        self.play_decoded_wav_in_slot(audio, slot, &format!("koe:{koe_no}"), wav, loop_flag)
269    }
270
271    fn play_decoded_wav_in_slot(
272        &mut self,
273        audio: &mut AudioHub,
274        slot: usize,
275        display_name: &str,
276        wav: Vec<u8>,
277        loop_flag: bool,
278    ) -> Result<()> {
279        let dur_ms = wav_duration_ms(&wav);
280
281        // Stop previous sound on this slot.
282        let _ = self.stop_slot(slot, None);
283
284        let s = &mut self.slots[slot];
285        s.last_name = Some(display_name.to_string());
286        if audio.is_enabled() {
287            let data =
288                StaticSoundData::from_cursor(Cursor::new(wav)).context("kira: decode WAV bytes")?;
289            let handle = audio.play_static(self.track_kind, data)?;
290            s.handle = Some(handle);
291        } else {
292            s.handle = None;
293        }
294
295        s.looping = loop_flag;
296        if loop_flag {
297            s.until = None;
298        } else if let Some(ms) = dur_ms {
299            s.until = Some(Instant::now() + Duration::from_millis(ms));
300        } else {
301            // Unknown duration: keep a conservative 2s window to avoid indefinite waits.
302            s.until = Some(Instant::now() + Duration::from_millis(2000));
303        }
304
305        self.set_volume_raw(audio, self.volume_raw)?;
306        Ok(())
307    }
308
309    fn resolve_path(&self, file_name: &str) -> Result<PathBuf> {
310        let direct = Path::new(file_name);
311        if path_exists(direct) {
312            return Ok(direct.to_path_buf());
313        }
314
315        if let Ok((path, _ty)) = crate::resource::find_audio_path_with_append_dir(
316            &self.project_dir,
317            "",
318            &self.sub_dir,
319            file_name,
320        ) {
321            return Ok(path);
322        }
323
324        let dir = self.project_dir.join(&self.sub_dir);
325        let base = dir.join(file_name);
326
327        if base.extension().is_some() && path_exists(&base) {
328            return Ok(base);
329        }
330
331        let candidates = ["wav", "nwa", "ogg", "owp", "ovk"];
332        for ext in candidates {
333            let p = base.with_extension(ext);
334            if path_exists(&p) {
335                return Ok(p);
336            }
337        }
338
339        bail!(
340            "sound file not found: name={:?} (project_dir={:?}, sub_dir={:?})",
341            file_name,
342            self.project_dir,
343            self.sub_dir
344        );
345    }
346
347    fn decode_to_wav(&self, path: &Path) -> Result<Vec<u8>> {
348        let ext = path
349            .extension()
350            .and_then(|s| s.to_str())
351            .unwrap_or("")
352            .to_ascii_lowercase();
353
354        match ext.as_str() {
355            "wav" => crate::resource::read_file_bytes(path).with_context(|| format!("read wav: {}", path.display())),
356            "nwa" | "ogg" | "owp" | "ovk" => {
357                let decoded = decode_bgm_to_wav_bytes(path, None)
358                    .with_context(|| format!("decode audio: {}", path.display()))?;
359                Ok(decoded.wav_bytes)
360            }
361            _ => Err(anyhow!("unsupported sound extension: {}", path.display())),
362        }
363    }
364}
365
366
367fn path_exists(path: &Path) -> bool {
368    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
369    { crate::resource::wasm_path_is_file(path) }
370    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
371    { path.exists() }
372}
373
374pub struct PcmEngine {
375    inner: SfxEngine,
376}
377
378impl PcmEngine {
379    pub fn new(project_dir: PathBuf) -> Self {
380        // Original engine: TNM_PCM_PLAYER_CNT = 16.
381        Self {
382            inner: SfxEngine::new(project_dir, "wav", TrackKind::Pcm, 16),
383        }
384    }
385
386    pub fn play_file_name(&mut self, audio: &mut AudioHub, file_name: &str) -> Result<PathBuf> {
387        self.inner
388            .play_file_name_in_slot(audio, 0, file_name, false)
389    }
390
391    pub fn play_koe_no(&mut self, audio: &mut AudioHub, koe_no: i64) -> Result<()> {
392        self.inner.play_koe_no_in_slot(audio, 0, koe_no, false)
393    }
394
395    pub fn play_in_slot(
396        &mut self,
397        audio: &mut AudioHub,
398        slot: usize,
399        file_name: &str,
400        loop_flag: bool,
401    ) -> Result<PathBuf> {
402        self.inner
403            .play_file_name_in_slot(audio, slot, file_name, loop_flag)
404    }
405
406    pub fn play_koe_no_in_slot(
407        &mut self,
408        audio: &mut AudioHub,
409        slot: usize,
410        koe_no: i64,
411        loop_flag: bool,
412    ) -> Result<()> {
413        self.inner
414            .play_koe_no_in_slot(audio, slot, koe_no, loop_flag)
415    }
416
417    pub fn play_decoded_wav_in_slot(
418        &mut self,
419        audio: &mut AudioHub,
420        slot: usize,
421        display_name: &str,
422        wav: Vec<u8>,
423        loop_flag: bool,
424    ) -> Result<()> {
425        self.inner
426            .play_decoded_wav_in_slot(audio, slot, display_name, wav, loop_flag)
427    }
428
429    pub fn stop(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
430        self.inner.stop_slot(0, fade_time_ms)
431    }
432
433    pub fn stop_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
434        self.inner.stop_slot(slot, fade_time_ms)
435    }
436
437    pub fn stop_all(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
438        self.inner.stop_all(fade_time_ms)
439    }
440
441    pub fn is_playing_any(&mut self) -> bool {
442        self.inner.is_playing_any()
443    }
444
445    pub fn is_playing_slot(&mut self, slot: usize) -> bool {
446        self.inner.is_playing_slot(slot)
447    }
448
449    pub fn volume_raw(&self) -> u8 {
450        self.inner.volume_raw()
451    }
452
453    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
454        self.inner.set_volume_raw(audio, volume_raw)
455    }
456
457    pub fn set_volume_raw_fade(
458        &mut self,
459        audio: &mut AudioHub,
460        volume_raw: u8,
461        fade_ms: i64,
462    ) -> Result<()> {
463        self.inner.set_volume_raw_fade(audio, volume_raw, fade_ms)
464    }
465}
466
467pub struct KoeEngine {
468    inner: SfxEngine,
469}
470
471impl KoeEngine {
472    pub fn new(project_dir: PathBuf) -> Self {
473        // Original engine: C_elm_koe owns one active voice player and stops it
474        // before starting the next KOE.
475        Self {
476            inner: SfxEngine::new(project_dir, "wav", TrackKind::Koe, 1),
477        }
478    }
479
480    pub fn play_koe_no(&mut self, audio: &mut AudioHub, koe_no: i64) -> Result<()> {
481        let _ = self.stop(None);
482        self.inner.play_koe_no_in_slot(audio, 0, koe_no, false)
483    }
484
485    pub fn stop(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
486        self.inner.stop_slot(0, fade_time_ms)
487    }
488
489    pub fn is_playing_any(&mut self) -> bool {
490        self.inner.is_playing_any()
491    }
492
493    pub fn volume_raw(&self) -> u8 {
494        self.inner.volume_raw()
495    }
496
497    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
498        self.inner.set_volume_raw(audio, volume_raw)
499    }
500
501    pub fn set_volume_raw_fade(
502        &mut self,
503        audio: &mut AudioHub,
504        volume_raw: u8,
505        fade_ms: i64,
506    ) -> Result<()> {
507        self.inner.set_volume_raw_fade(audio, volume_raw, fade_ms)
508    }
509}
510
511pub struct SeEngine {
512    inner: SfxEngine,
513}
514
515impl SeEngine {
516    pub fn new(project_dir: PathBuf) -> Self {
517        // Original engine: TNM_SE_PLAYER_CNT = 16.
518        Self {
519            inner: SfxEngine::new(project_dir, "wav", TrackKind::Se, 16),
520        }
521    }
522
523    pub fn play_file_name(&mut self, audio: &mut AudioHub, file_name: &str) -> Result<PathBuf> {
524        self.inner
525            .play_file_name_in_slot(audio, 0, file_name, false)
526    }
527
528    pub fn play_koe_no(&mut self, audio: &mut AudioHub, koe_no: i64) -> Result<()> {
529        self.inner.play_koe_no_in_slot(audio, 0, koe_no, false)
530    }
531
532    pub fn play_in_slot(
533        &mut self,
534        audio: &mut AudioHub,
535        slot: usize,
536        file_name: &str,
537        loop_flag: bool,
538    ) -> Result<PathBuf> {
539        self.inner
540            .play_file_name_in_slot(audio, slot, file_name, loop_flag)
541    }
542
543    pub fn play_koe_no_in_slot(
544        &mut self,
545        audio: &mut AudioHub,
546        slot: usize,
547        koe_no: i64,
548        loop_flag: bool,
549    ) -> Result<()> {
550        self.inner
551            .play_koe_no_in_slot(audio, slot, koe_no, loop_flag)
552    }
553
554    pub fn play_decoded_wav_in_slot(
555        &mut self,
556        audio: &mut AudioHub,
557        slot: usize,
558        display_name: &str,
559        wav: Vec<u8>,
560        loop_flag: bool,
561    ) -> Result<()> {
562        self.inner
563            .play_decoded_wav_in_slot(audio, slot, display_name, wav, loop_flag)
564    }
565
566    pub fn stop(&mut self, fade_time_ms: Option<i64>) -> Result<()> {
567        self.inner.stop_all(fade_time_ms)
568    }
569
570    pub fn stop_slot(&mut self, slot: usize, fade_time_ms: Option<i64>) -> Result<()> {
571        self.inner.stop_slot(slot, fade_time_ms)
572    }
573
574    pub fn is_playing_any(&mut self) -> bool {
575        self.inner.is_playing_any()
576    }
577
578    pub fn is_playing_slot(&mut self, slot: usize) -> bool {
579        self.inner.is_playing_slot(slot)
580    }
581
582    pub fn volume_raw(&self) -> u8 {
583        self.inner.volume_raw()
584    }
585
586    pub fn set_volume_raw(&mut self, audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
587        self.inner.set_volume_raw(audio, volume_raw)
588    }
589
590    pub fn set_volume_raw_fade(
591        &mut self,
592        audio: &mut AudioHub,
593        volume_raw: u8,
594        fade_ms: i64,
595    ) -> Result<()> {
596        self.inner.set_volume_raw_fade(audio, volume_raw, fade_ms)
597    }
598
599    pub fn last_name(&self) -> Option<&str> {
600        self.inner.last_name_slot(0)
601    }
602}