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
16pub(crate) fn wav_duration_ms(wav: &[u8]) -> Option<u64> {
21 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 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 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 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 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 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 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 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 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}