1use std::io::Cursor;
2use std::path::{Path, PathBuf};
3use crate::platform_time::{Duration, Instant};
4
5use anyhow::{anyhow, Context, Result};
6use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
7use kira::sound::{EndPosition, Region};
8use kira::tween::Tween;
9use kira::Volume;
10use siglus_assets::gameexe::{decode_gameexe_dat_bytes, GameexeConfig, GameexeDecodeOptions};
11
12use super::bgm::{decode_bgm_to_playback_bytes, BgmPlaybackFormat};
13use super::{AudioHub, TrackKind};
14
15const TNM_BGM_START_POS_INI: i64 = -1;
16const TNM_BGM_PLAYER_CNT: usize = 2;
17
18#[derive(Debug, Clone)]
19struct WavPcmRegion {
20 data_offset: usize,
21 data_len: usize,
22 data_len_field_offset: usize,
23 riff_len_field_offset: usize,
24 byte_rate: u32,
25 block_align: usize,
26 sample_rate: u32,
27 channels: u16,
28 bits_per_sample: u16,
29 audio_format: u16,
30}
31
32impl WavPcmRegion {
33 fn sample_count(&self) -> u64 {
34 (self.data_len / self.block_align.max(1)) as u64
35 }
36}
37
38fn wav_pcm_region(wav: &[u8]) -> Option<WavPcmRegion> {
39 if wav.len() < 44 {
40 return None;
41 }
42 if &wav[0..4] != b"RIFF" || &wav[8..12] != b"WAVE" {
43 return None;
44 }
45
46 let mut pos = 12usize;
47 let mut byte_rate: Option<u32> = None;
48 let mut block_align: Option<usize> = None;
49 let mut sample_rate: Option<u32> = None;
50 let mut channels: Option<u16> = None;
51 let mut bits_per_sample: Option<u16> = None;
52 let mut audio_format: Option<u16> = None;
53 let mut data_offset: Option<usize> = None;
54 let mut data_len: Option<usize> = None;
55 let mut data_len_field_offset: Option<usize> = None;
56
57 while pos + 8 <= wav.len() {
58 let id = &wav[pos..pos + 4];
59 let sz =
60 u32::from_le_bytes([wav[pos + 4], wav[pos + 5], wav[pos + 6], wav[pos + 7]]) as usize;
61 let sz_field_off = pos + 4;
62 pos += 8;
63 if pos + sz > wav.len() {
64 break;
65 }
66 if id == b"fmt " {
67 if sz >= 16 {
68 audio_format = Some(u16::from_le_bytes([wav[pos], wav[pos + 1]]));
69 channels = Some(u16::from_le_bytes([wav[pos + 2], wav[pos + 3]]));
70 sample_rate = Some(u32::from_le_bytes([
71 wav[pos + 4],
72 wav[pos + 5],
73 wav[pos + 6],
74 wav[pos + 7],
75 ]));
76 byte_rate = Some(u32::from_le_bytes([
77 wav[pos + 8],
78 wav[pos + 9],
79 wav[pos + 10],
80 wav[pos + 11],
81 ]));
82 block_align = Some(u16::from_le_bytes([wav[pos + 12], wav[pos + 13]]) as usize);
83 bits_per_sample = Some(u16::from_le_bytes([wav[pos + 14], wav[pos + 15]]));
84 }
85 } else if id == b"data" {
86 data_offset = Some(pos);
87 data_len = Some(sz);
88 data_len_field_offset = Some(sz_field_off);
89 }
90 pos += sz;
91 if (sz & 1) != 0 {
92 pos += 1;
93 }
94 }
95
96 Some(WavPcmRegion {
97 data_offset: data_offset?,
98 data_len: data_len?,
99 data_len_field_offset: data_len_field_offset?,
100 riff_len_field_offset: 4,
101 byte_rate: byte_rate?,
102 block_align: block_align?.max(1),
103 sample_rate: sample_rate?.max(1),
104 channels: channels?.max(1),
105 bits_per_sample: bits_per_sample?,
106 audio_format: audio_format?,
107 })
108}
109
110fn wav_slice_samples(wav: &[u8], start_sample: u64, end_sample: Option<u64>) -> Option<Vec<u8>> {
111 let region = wav_pcm_region(wav)?;
112 let total_samples = region.sample_count();
113 let start_sample = start_sample.min(total_samples);
114 let end_sample = end_sample
115 .unwrap_or(total_samples)
116 .min(total_samples)
117 .max(start_sample);
118
119 let start_byte = (start_sample as usize) * region.block_align;
120 let end_byte = (end_sample as usize) * region.block_align;
121 let src_begin = region.data_offset + start_byte;
122 let src_end = region.data_offset + end_byte;
123 if src_begin > wav.len() || src_end > wav.len() || src_begin > src_end {
124 return None;
125 }
126
127 let mut out = wav.to_vec();
128 out.splice(
129 region.data_offset..region.data_offset + region.data_len,
130 wav[src_begin..src_end].iter().copied(),
131 );
132 let new_data_len = (src_end - src_begin) as u32;
133 out[region.data_len_field_offset..region.data_len_field_offset + 4]
134 .copy_from_slice(&new_data_len.to_le_bytes());
135 let riff_len = (out.len().saturating_sub(8)) as u32;
136 out[region.riff_len_field_offset..region.riff_len_field_offset + 4]
137 .copy_from_slice(&riff_len.to_le_bytes());
138 Some(out)
139}
140
141fn parse_i64_like(s: &str) -> Option<i64> {
142 let s = s.trim();
143 if s.is_empty() {
144 return None;
145 }
146 if let Ok(v) = s.parse::<i64>() {
147 return Some(v);
148 }
149 if let Some(rest) = s.strip_prefix("0x") {
150 return i64::from_str_radix(rest, 16).ok();
151 }
152 if let Some(rest) = s.strip_prefix("-0x") {
153 return i64::from_str_radix(rest, 16).ok().map(|v| -v);
154 }
155 None
156}
157
158fn normalize_regist_name(name: &str) -> String {
159 name.trim().to_ascii_lowercase()
160}
161
162fn clamp_sample_range(
163 total_samples: u64,
164 start_sample: i64,
165 end_sample: i64,
166 restart_sample: i64,
167) -> (u64, u64, u64) {
168 let total_samples_i64 = total_samples as i64;
169 let end_sample = if end_sample < 0 {
170 total_samples_i64
171 } else {
172 end_sample.clamp(0, total_samples_i64)
173 };
174 let start_sample = start_sample.clamp(0, end_sample);
175 let restart_sample = restart_sample.clamp(0, end_sample);
176 (
177 start_sample as u64,
178 end_sample as u64,
179 restart_sample as u64,
180 )
181}
182
183fn tween_ms(ms: i64) -> Tween {
184 if ms > 0 {
185 Tween {
186 duration: Duration::from_millis(ms as u64),
187 ..Tween::default()
188 }
189 } else {
190 Tween::default()
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195enum PendingBgmActionKind {
196 Stop,
197 Pause,
198}
199
200#[derive(Debug, Clone, Copy)]
201struct PendingBgmAction {
202 kind: PendingBgmActionKind,
203 at: Instant,
204}
205
206pub const TNM_PLAYER_STATE_FREE: i32 = 0;
207pub const TNM_PLAYER_STATE_PLAY: i32 = 1;
208pub const TNM_PLAYER_STATE_FADE_OUT: i32 = 2;
209pub const TNM_PLAYER_STATE_PAUSE: i32 = 3;
210
211#[derive(Debug, Clone)]
212struct BgmScriptEntry {
213 file_name: String,
214 start_sample: i64,
215 end_sample: i64,
216 repeat_sample: i64,
217}
218
219#[derive(Debug, Default)]
220struct BgmPlayerSlot {
221 handle: Option<StaticSoundHandle>,
222 source_bytes: Option<Vec<u8>>,
223 source_format: Option<BgmPlaybackFormat>,
224 sample_rate_hz: u32,
225 total_samples: u64,
226 start_sample: u64,
227 end_sample: u64,
228 restart_sample: u64,
229 current_segment_start_sample: u64,
230 current_segment_samples: u64,
231 start_time: Option<Instant>,
232 paused_at: Option<Instant>,
233 paused_total: Duration,
234 pending: Option<PendingBgmAction>,
235 fade_outing: bool,
236 loop_flag: bool,
237 name: Option<String>,
238 file_name: Option<String>,
239 ready_only: bool,
240}
241
242impl BgmPlayerSlot {
243 fn reset_all(&mut self) {
244 if let Some(mut h) = self.handle.take() {
245 let _ = h.stop(Tween::default());
246 }
247 self.source_bytes = None;
248 self.source_format = None;
249 self.sample_rate_hz = 0;
250 self.total_samples = 0;
251 self.start_sample = 0;
252 self.end_sample = 0;
253 self.restart_sample = 0;
254 self.current_segment_start_sample = 0;
255 self.current_segment_samples = 0;
256 self.start_time = None;
257 self.paused_at = None;
258 self.paused_total = Duration::from_millis(0);
259 self.pending = None;
260 self.fade_outing = false;
261 self.loop_flag = false;
262 self.name = None;
263 self.file_name = None;
264 self.ready_only = false;
265 }
266
267 fn clear_runtime_only(&mut self) {
268 self.handle = None;
269 self.current_segment_start_sample = self.start_sample;
270 self.current_segment_samples = self.end_sample.saturating_sub(self.start_sample);
271 self.start_time = None;
272 self.paused_at = None;
273 self.paused_total = Duration::from_millis(0);
274 self.pending = None;
275 self.fade_outing = false;
276 self.ready_only = false;
277 }
278
279 fn elapsed_ms(&self) -> u64 {
280 let Some(start) = self.start_time else {
281 return 0;
282 };
283 let now = self.paused_at.unwrap_or_else(Instant::now);
284 now.saturating_duration_since(start)
285 .saturating_sub(self.paused_total)
286 .as_millis() as u64
287 }
288
289 fn elapsed_samples(&self) -> u64 {
290 if self.sample_rate_hz == 0 {
291 return 0;
292 }
293 self.elapsed_ms().saturating_mul(self.sample_rate_hz as u64) / 1000
294 }
295
296 fn playback_window_samples(&self) -> u64 {
297 self.end_sample.saturating_sub(self.start_sample)
298 }
299
300 fn loop_span_samples(&self) -> u64 {
301 self.end_sample.saturating_sub(self.restart_sample)
302 }
303
304 fn has_loop_region(&self) -> bool {
305 self.loop_flag && self.restart_sample < self.end_sample
306 }
307
308 fn play_pos_samples(&self) -> u64 {
309 let elapsed = self.elapsed_samples();
310 if self.has_loop_region() {
311 let intro_samples = self.restart_sample.saturating_sub(self.start_sample);
312 let loop_span = self.loop_span_samples();
313 if loop_span == 0 {
314 return self.end_sample;
315 }
316 if self.start_sample < self.restart_sample && elapsed < intro_samples {
317 return self.start_sample + elapsed.min(intro_samples);
318 }
319 let loop_elapsed = if self.start_sample < self.restart_sample {
320 elapsed.saturating_sub(intro_samples)
321 } else {
322 elapsed
323 };
324 return self.restart_sample + (loop_elapsed % loop_span);
325 }
326 self.start_sample + elapsed.min(self.playback_window_samples())
327 }
328
329 fn check_state(&self) -> i32 {
330 if self.handle.is_none() {
331 return TNM_PLAYER_STATE_FREE;
332 }
333 if self.paused_at.is_some() {
334 return TNM_PLAYER_STATE_PAUSE;
335 }
336 if self.fade_outing {
337 return TNM_PLAYER_STATE_FADE_OUT;
338 }
339 TNM_PLAYER_STATE_PLAY
340 }
341
342 fn is_playing(&self) -> bool {
343 self.handle.is_some() && !self.fade_outing && self.paused_at.is_none()
344 }
345}
346
347pub struct BgmEngine {
348 project_dir: PathBuf,
349 current_append_dir: String,
350 game_volume_raw: u8,
351 system_volume_raw: u8,
352 current_name: Option<String>,
353 current_player_id: Option<usize>,
354 players: Vec<BgmPlayerSlot>,
355 retired: Vec<(StaticSoundHandle, Instant)>,
356 delay_deadline: Option<Instant>,
357 delayed_fade_in_ms: i64,
358 loop_flag: bool,
359 pause_flag: bool,
360}
361
362impl std::fmt::Debug for BgmEngine {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 f.debug_struct("BgmEngine")
365 .field("game_volume_raw", &self.game_volume_raw)
366 .field("system_volume_raw", &self.system_volume_raw)
367 .field("current_name", &self.current_name)
368 .field("current_player_id", &self.current_player_id)
369 .field("loop_flag", &self.loop_flag)
370 .field("pause_flag", &self.pause_flag)
371 .finish()
372 }
373}
374
375impl BgmEngine {
376 pub fn new(project_dir: PathBuf) -> Self {
377 Self {
378 project_dir,
379 current_append_dir: String::new(),
380 game_volume_raw: 255,
381 system_volume_raw: 255,
382 current_name: None,
383 current_player_id: None,
384 players: (0..TNM_BGM_PLAYER_CNT)
385 .map(|_| BgmPlayerSlot::default())
386 .collect(),
387 retired: Vec::new(),
388 delay_deadline: None,
389 delayed_fade_in_ms: 0,
390 loop_flag: false,
391 pause_flag: false,
392 }
393 }
394
395 pub fn current_name(&self) -> Option<&str> {
396 self.current_name.as_deref()
397 }
398
399 pub fn set_current_append_dir(&mut self, append_dir: impl Into<String>) {
400 self.current_append_dir = append_dir.into();
401 }
402
403 pub fn volume_raw(&self) -> u8 {
404 self.game_volume_raw
405 }
406
407 fn total_gain_amplitude(&self) -> f64 {
408 (self.game_volume_raw as f64 / 255.0) * (self.system_volume_raw as f64 / 255.0)
409 }
410
411 fn apply_all_active_volumes(&mut self, fade_ms: i64) {
412 let amp = self.total_gain_amplitude();
413 let tween = tween_ms(fade_ms);
414 for slot in &mut self.players {
415 if let Some(h) = &mut slot.handle {
416 let _ = h.set_volume(Volume::Amplitude(amp), tween);
417 }
418 }
419 }
420
421 pub fn set_volume_raw(&mut self, _audio: &mut AudioHub, volume_raw: u8) -> Result<()> {
422 self.game_volume_raw = volume_raw;
423 self.apply_all_active_volumes(0);
424 Ok(())
425 }
426
427 pub fn set_volume_raw_fade(
428 &mut self,
429 _audio: &mut AudioHub,
430 volume_raw: u8,
431 fade_ms: i64,
432 ) -> Result<()> {
433 self.game_volume_raw = volume_raw;
434 self.apply_all_active_volumes(fade_ms);
435 Ok(())
436 }
437
438 pub fn set_system_volume_raw(&mut self, volume_raw: u8) {
439 self.system_volume_raw = volume_raw;
440 self.apply_all_active_volumes(0);
441 }
442
443 pub fn set_looping(&mut self, looping: bool) -> Result<()> {
444 self.loop_flag = looping;
445 Ok(())
446 }
447
448 pub fn check_state(&self) -> i32 {
449 self.current_slot()
450 .map(|slot| slot.check_state())
451 .unwrap_or(TNM_PLAYER_STATE_FREE)
452 }
453
454 pub fn is_playing(&self) -> bool {
455 self.current_slot()
456 .map(|slot| slot.is_playing())
457 .unwrap_or(false)
458 }
459
460 pub fn is_fade_out_doing(&self) -> bool {
461 self.check_state() == TNM_PLAYER_STATE_FADE_OUT
462 }
463
464 pub fn can_wait(&self) -> bool {
465 self.is_playing() && !self.loop_flag
466 }
467
468 fn current_slot(&self) -> Option<&BgmPlayerSlot> {
469 self.current_player_id.and_then(|id| self.players.get(id))
470 }
471
472 fn current_slot_mut(&mut self) -> Option<&mut BgmPlayerSlot> {
473 self.current_player_id
474 .and_then(|id| self.players.get_mut(id))
475 }
476
477 fn lookup_gameexe_bgm_entry(&self, regist_name: &str) -> Option<BgmScriptEntry> {
478 let cfg = load_gameexe_config(&self.project_dir)?;
479 let target = normalize_regist_name(regist_name);
480 let cnt = cfg.indexed_count("BGM");
481 for i in 0..cnt {
482 let Some(key_name) = cfg.get_indexed_item_unquoted("BGM", i, 0) else {
483 continue;
484 };
485 if normalize_regist_name(key_name) != target {
486 continue;
487 }
488 let file_name = cfg
489 .get_indexed_item_unquoted("BGM", i, 1)?
490 .trim()
491 .to_string();
492 if file_name.is_empty() {
493 continue;
494 }
495 let start_sample = cfg
496 .get_indexed_item_unquoted("BGM", i, 2)
497 .and_then(parse_i64_like)
498 .unwrap_or(0);
499 let end_sample = cfg
500 .get_indexed_item_unquoted("BGM", i, 3)
501 .and_then(parse_i64_like)
502 .unwrap_or(-1);
503 let repeat_sample = cfg
504 .get_indexed_item_unquoted("BGM", i, 4)
505 .and_then(parse_i64_like)
506 .unwrap_or(0);
507 return Some(BgmScriptEntry {
508 file_name,
509 start_sample,
510 end_sample,
511 repeat_sample,
512 });
513 }
514 None
515 }
516
517 fn resolve_bgm_script(&self, regist_name: &str) -> Result<(BgmScriptEntry, PathBuf)> {
518 if let Some(entry) = self.lookup_gameexe_bgm_entry(regist_name) {
519 let (path, _ty) = crate::resource::find_audio_path_with_append_dir(
520 &self.project_dir,
521 &self.current_append_dir,
522 "bgm",
523 &entry.file_name,
524 )
525 .map_err(|err| {
526 anyhow!(
527 "BGM file not found for regist name {}: {}; {}",
528 regist_name,
529 entry.file_name,
530 err
531 )
532 })?;
533 return Ok((entry, path));
534 }
535
536 let direct_name = regist_name.trim();
537 let (path, _ty) = crate::resource::find_audio_path_with_append_dir(
538 &self.project_dir,
539 &self.current_append_dir,
540 "bgm",
541 direct_name,
542 )
543 .map_err(|err| anyhow!("BGM regist name not found in script table: {regist_name}; {err}"))?;
544 Ok((
545 BgmScriptEntry {
546 file_name: direct_name.to_string(),
547 start_sample: 0,
548 end_sample: -1,
549 repeat_sample: 0,
550 },
551 path,
552 ))
553 }
554
555 fn prepare_slot(
556 &mut self,
557 slot_id: usize,
558 regist_name: &str,
559 loop_flag: bool,
560 start_pos_sample: i64,
561 ready_only: bool,
562 ) -> Result<()> {
563 let (script_entry, path) = self.resolve_bgm_script(regist_name)?;
564
565
566 let decoded = decode_bgm_to_playback_bytes(&path, None)
567 .with_context(|| format!("prepare BGM playback: {}", path.display()))?;
568
569 let total_samples = decoded.total_samples;
570
571 let script_start = script_entry.start_sample;
572 let script_end = script_entry.end_sample;
573 let script_repeat = script_entry.repeat_sample;
574 let effective_start = if start_pos_sample == TNM_BGM_START_POS_INI {
575 script_start
576 } else {
577 start_pos_sample
578 };
579 let (start_sample, end_sample, restart_sample) =
580 clamp_sample_range(total_samples, effective_start, script_end, script_repeat);
581
582 if std::env::var_os("SG_AUDIO_TRACE").is_some() {
583 eprintln!(
584 "[SG_AUDIO_TRACE] bgm.prepare name={} file={} source_format={:?} container={:?} channels={} sample_rate={} total_frames={} start={} end={} repeat={}",
585 regist_name,
586 path.display(),
587 decoded.format,
588 decoded.container,
589 decoded.channels,
590 decoded.sample_rate,
591 total_samples,
592 start_sample,
593 end_sample,
594 restart_sample
595 );
596 }
597
598 let slot = &mut self.players[slot_id];
599 slot.reset_all();
600 slot.source_bytes = Some(decoded.bytes);
601 slot.source_format = Some(decoded.format);
602 slot.sample_rate_hz = decoded.sample_rate;
603 slot.total_samples = total_samples;
604 slot.start_sample = start_sample;
605 slot.end_sample = end_sample;
606 slot.restart_sample = restart_sample;
607 slot.current_segment_start_sample = start_sample;
608 slot.current_segment_samples = end_sample.saturating_sub(start_sample);
609 slot.loop_flag = loop_flag;
610 slot.name = Some(regist_name.to_string());
611 slot.file_name = Some(path.to_string_lossy().to_string());
612 slot.ready_only = ready_only;
613 Ok(())
614 }
615
616 fn start_slot_internal(
617 &mut self,
618 audio: &mut AudioHub,
619 slot_id: usize,
620 fade_in_ms: i64,
621 start_paused: bool,
622 ) -> Result<()> {
623 let amp = self.total_gain_amplitude();
624 let slot = &mut self.players[slot_id];
625 let Some(source) = slot.source_bytes.as_ref() else {
626 return Err(anyhow!("BGM slot not prepared"));
627 };
628
629 let mut effective_start = slot.start_sample;
630 if effective_start >= slot.end_sample {
631 if slot.has_loop_region() {
632 effective_start = slot.restart_sample;
633 } else {
634 return Err(anyhow!("invalid BGM range: start >= end"));
635 }
636 }
637 if effective_start >= slot.end_sample {
638 return Err(anyhow!("invalid BGM range after restart clamp"));
639 }
640
641 if !audio.is_enabled() {
642 slot.handle = None;
643 slot.current_segment_start_sample = effective_start;
644 slot.current_segment_samples = slot.end_sample.saturating_sub(effective_start);
645 slot.start_time = Some(Instant::now());
646 slot.paused_at = if start_paused { slot.start_time } else { None };
647 slot.paused_total = Duration::from_millis(0);
648 slot.pending = None;
649 slot.fade_outing = false;
650 slot.start_sample = effective_start;
651 slot.ready_only = start_paused;
652 return Ok(());
653 }
654
655 let start_sec = if slot.sample_rate_hz == 0 {
656 0.0
657 } else {
658 effective_start as f64 / slot.sample_rate_hz as f64
659 };
660 let mut data = StaticSoundData::from_cursor(Cursor::new(source.clone()))
661 .context("kira: decode BGM playback bytes")?;
662 data = data.start_position(start_sec);
663 if slot.has_loop_region() {
664 let loop_region = Region {
665 start: (slot.restart_sample as f64 / slot.sample_rate_hz.max(1) as f64).into(),
666 end: EndPosition::Custom(
667 (slot.end_sample as f64 / slot.sample_rate_hz.max(1) as f64).into(),
668 ),
669 };
670 data = data.loop_region(loop_region);
671 }
672 let mut handle = audio.play_static(TrackKind::Bgm, data)?;
673
674 if start_paused {
675 let _ = handle.set_volume(Volume::Amplitude(amp), Tween::default());
676 let _ = handle.pause(Tween::default());
677 } else if fade_in_ms > 0 {
678 let _ = handle.set_volume(Volume::Amplitude(0.0), Tween::default());
679 let _ = handle.set_volume(Volume::Amplitude(amp), tween_ms(fade_in_ms));
680 } else {
681 let _ = handle.set_volume(Volume::Amplitude(amp), Tween::default());
682 }
683
684 slot.handle = Some(handle);
685 slot.current_segment_start_sample = effective_start;
686 slot.current_segment_samples = slot.end_sample.saturating_sub(effective_start);
687 slot.start_time = Some(Instant::now());
688 slot.paused_at = if start_paused { slot.start_time } else { None };
689 slot.paused_total = Duration::from_millis(0);
690 slot.pending = None;
691 slot.fade_outing = false;
692 slot.start_sample = effective_start;
693 slot.ready_only = start_paused;
694 Ok(())
695 }
696
697 fn start_slot(&mut self, audio: &mut AudioHub, slot_id: usize, fade_in_ms: i64) -> Result<()> {
698 self.start_slot_internal(audio, slot_id, fade_in_ms, false)
699 }
700
701 fn ready_slot(&mut self, audio: &mut AudioHub, slot_id: usize) -> Result<()> {
702 self.start_slot_internal(audio, slot_id, 0, true)
703 }
704
705 fn handoff_current_to_retired(&mut self, fade_out_ms: i64) {
706 let Some(cur_id) = self.current_player_id else {
707 return;
708 };
709 let slot = &mut self.players[cur_id];
710 if let Some(mut h) = slot.handle.take() {
711 if fade_out_ms > 0 {
712 let _ = h.stop(tween_ms(fade_out_ms));
713 self.retired.push((
714 h,
715 Instant::now() + Duration::from_millis(fade_out_ms as u64),
716 ));
717 } else {
718 let _ = h.stop(Tween::default());
719 }
720 }
721 slot.clear_runtime_only();
722 }
723
724 pub fn play_name_script(
725 &mut self,
726 audio: &mut AudioHub,
727 name: &str,
728 loop_flag: bool,
729 fade_in_ms: i64,
730 fade_out_ms: i64,
731 start_pos_sample: i64,
732 ready_only: bool,
733 delay_time_ms: i64,
734 ) -> Result<()> {
735 let regist_name = normalize_regist_name(name);
736 if self.current_name.as_deref() == Some(regist_name.as_str()) && self.loop_flag && loop_flag
737 {
738 return Ok(());
739 }
740
741 self.handoff_current_to_retired(fade_out_ms);
742 let next_id = match self.current_player_id {
743 Some(id) => (id + 1) % TNM_BGM_PLAYER_CNT,
744 None => 0,
745 };
746 let total_ready_only = ready_only || delay_time_ms > 0;
747 self.prepare_slot(
748 next_id,
749 ®ist_name,
750 loop_flag,
751 start_pos_sample,
752 total_ready_only,
753 )?;
754 self.current_player_id = Some(next_id);
755 self.current_name = Some(regist_name);
756 self.loop_flag = loop_flag;
757 self.pause_flag = ready_only;
758 self.delayed_fade_in_ms = fade_in_ms;
759 self.delay_deadline = if delay_time_ms > 0 {
760 Some(Instant::now() + Duration::from_millis(delay_time_ms.max(0) as u64))
761 } else {
762 None
763 };
764
765 if total_ready_only {
766 self.ready_slot(audio, next_id)?;
767 } else {
768 self.start_slot(audio, next_id, fade_in_ms)?;
769 }
770 Ok(())
771 }
772
773 pub fn ready_name(
774 &mut self,
775 audio: &mut AudioHub,
776 name: &str,
777 start_pos_sample: i64,
778 ) -> Result<()> {
779 self.play_name_script(audio, name, self.loop_flag, 0, 0, start_pos_sample, true, 0)
780 }
781
782 pub fn play_name_with_options(
783 &mut self,
784 audio: &mut AudioHub,
785 name: &str,
786 start_pos_sample: i64,
787 fade_in_ms: i64,
788 ) -> Result<()> {
789 self.play_name_script(
790 audio,
791 name,
792 self.loop_flag,
793 fade_in_ms,
794 0,
795 start_pos_sample,
796 false,
797 0,
798 )
799 }
800
801 pub fn play_name(&mut self, audio: &mut AudioHub, name: &str) -> Result<()> {
802 self.play_name_script(audio, name, true, 0, 0, TNM_BGM_START_POS_INI, false, 0)
803 }
804
805 pub fn play_pos_samples(&self) -> u64 {
806 self.current_slot()
807 .map(|s| s.play_pos_samples())
808 .unwrap_or(0)
809 }
810
811 pub fn pause_fade(&mut self, _audio: &mut AudioHub, fade_ms: i64) -> Result<()> {
812 self.delay_deadline = None;
813 self.pause_flag = true;
814 let amp = self.total_gain_amplitude();
815 let Some(slot) = self.current_slot_mut() else {
816 return Ok(());
817 };
818 if slot.handle.is_none() || slot.paused_at.is_some() {
819 return Ok(());
820 }
821 if fade_ms > 0 {
822 if let Some(h) = &mut slot.handle {
823 let _ = h.set_volume(Volume::Amplitude(amp), Tween::default());
824 let _ = h.set_volume(Volume::Amplitude(0.0), tween_ms(fade_ms));
825 }
826 slot.fade_outing = true;
827 slot.pending = Some(PendingBgmAction {
828 kind: PendingBgmActionKind::Pause,
829 at: Instant::now() + Duration::from_millis(fade_ms as u64),
830 });
831 } else {
832 if let Some(h) = &mut slot.handle {
833 let _ = h.pause(Tween::default());
834 }
835 slot.paused_at = Some(Instant::now());
836 }
837 Ok(())
838 }
839
840 pub fn pause(&mut self) -> Result<()> {
841 self.delay_deadline = None;
842 self.pause_flag = true;
843 let Some(slot) = self.current_slot_mut() else {
844 return Ok(());
845 };
846 if slot.handle.is_none() || slot.paused_at.is_some() {
847 return Ok(());
848 }
849 if let Some(h) = &mut slot.handle {
850 let _ = h.pause(Tween::default());
851 }
852 slot.paused_at = Some(Instant::now());
853 Ok(())
854 }
855
856 pub fn resume_script(
857 &mut self,
858 audio: &mut AudioHub,
859 fade_in_ms: i64,
860 delay_time_ms: i64,
861 ) -> Result<()> {
862 if delay_time_ms > 0 {
863 self.delay_deadline =
864 Some(Instant::now() + Duration::from_millis(delay_time_ms as u64));
865 self.delayed_fade_in_ms = fade_in_ms;
866 self.pause_flag = false;
867 return Ok(());
868 }
869
870 let amp = self.total_gain_amplitude();
871 let Some(cur_id) = self.current_player_id else {
872 return Ok(());
873 };
874 if self.players[cur_id].handle.is_none() {
875 self.start_slot(audio, cur_id, fade_in_ms)?;
876 self.pause_flag = false;
877 self.delay_deadline = None;
878 return Ok(());
879 }
880
881 let slot = &mut self.players[cur_id];
882 if let Some(p) = slot.paused_at.take() {
883 slot.paused_total += Instant::now().saturating_duration_since(p);
884 }
885 if let Some(h) = &mut slot.handle {
886 let _ = h.resume(Tween::default());
887 if fade_in_ms > 0 {
888 let _ = h.set_volume(Volume::Amplitude(0.0), Tween::default());
889 let _ = h.set_volume(Volume::Amplitude(amp), tween_ms(fade_in_ms));
890 } else {
891 let _ = h.set_volume(Volume::Amplitude(amp), Tween::default());
892 }
893 }
894 slot.fade_outing = false;
895 slot.pending = None;
896 slot.ready_only = false;
897 self.pause_flag = false;
898 self.delay_deadline = None;
899 Ok(())
900 }
901
902 pub fn resume_fade(&mut self, audio: &mut AudioHub, fade_ms: i64) -> Result<()> {
903 self.resume_script(audio, fade_ms, 0)
904 }
905
906 pub fn resume(&mut self) -> Result<()> {
907 let amp = self.total_gain_amplitude();
908 let Some(slot) = self.current_slot_mut() else {
909 return Ok(());
910 };
911 if let Some(p) = slot.paused_at.take() {
912 slot.paused_total += Instant::now().saturating_duration_since(p);
913 if let Some(h) = &mut slot.handle {
914 let _ = h.resume(Tween::default());
915 let _ = h.set_volume(Volume::Amplitude(amp), Tween::default());
916 }
917 }
918 slot.ready_only = false;
919 self.pause_flag = false;
920 Ok(())
921 }
922
923 pub fn stop(&mut self) -> Result<()> {
924 self.stop_current_internal(0)
925 }
926
927 fn stop_current_internal(&mut self, fade_out_ms: i64) -> Result<()> {
928 self.delay_deadline = None;
929 if let Some(slot) = self.current_slot_mut() {
930 if slot.handle.is_none() {
931 slot.clear_runtime_only();
932 } else if fade_out_ms > 0 {
933 if let Some(h) = &mut slot.handle {
934 let _ = h.stop(tween_ms(fade_out_ms));
935 }
936 slot.fade_outing = true;
937 slot.pending = Some(PendingBgmAction {
938 kind: PendingBgmActionKind::Stop,
939 at: Instant::now() + Duration::from_millis(fade_out_ms as u64),
940 });
941 } else {
942 if let Some(mut h) = slot.handle.take() {
943 let _ = h.stop(Tween::default());
944 }
945 slot.clear_runtime_only();
946 }
947 }
948 self.current_name = None;
949 self.loop_flag = false;
950 self.pause_flag = false;
951 Ok(())
952 }
953
954 pub fn stop_fade(&mut self, fade_out_ms: i64) -> Result<()> {
955 self.stop_current_internal(fade_out_ms)
956 }
957
958 pub fn tick(&mut self, audio: &mut AudioHub) -> Result<()> {
959 let now = Instant::now();
960 self.retired.retain_mut(|(h, deadline)| {
961 if now >= *deadline {
962 let _ = h.stop(Tween::default());
963 false
964 } else {
965 true
966 }
967 });
968
969 if let Some(deadline) = self.delay_deadline {
970 if now >= deadline {
971 self.delay_deadline = None;
972 self.resume_script(audio, self.delayed_fade_in_ms, 0)?;
973 }
974 }
975
976 let Some(cur_id) = self.current_player_id else {
977 return Ok(());
978 };
979
980 let pending = self.players[cur_id].pending;
981 if let Some(pending) = pending {
982 if now >= pending.at {
983 let slot = &mut self.players[cur_id];
984 slot.pending = None;
985 match pending.kind {
986 PendingBgmActionKind::Stop => {
987 if let Some(mut h) = slot.handle.take() {
988 let _ = h.stop(Tween::default());
989 }
990 slot.clear_runtime_only();
991 }
992 PendingBgmActionKind::Pause => {
993 if let Some(h) = &mut slot.handle {
994 let _ = h.pause(Tween::default());
995 }
996 slot.paused_at = Some(now);
997 slot.fade_outing = false;
998 }
999 }
1000 }
1001 }
1002
1003 let (paused, has_handle, segment_samples, elapsed_samples, has_loop_region) = {
1004 let slot = &self.players[cur_id];
1005 (
1006 slot.paused_at.is_some(),
1007 slot.handle.is_some(),
1008 slot.playback_window_samples(),
1009 slot.elapsed_samples(),
1010 slot.has_loop_region(),
1011 )
1012 };
1013 if paused || !has_handle {
1014 return Ok(());
1015 }
1016 if segment_samples == 0 {
1017 return Ok(());
1018 }
1019 if elapsed_samples < segment_samples {
1020 return Ok(());
1021 }
1022 if has_loop_region {
1023 return Ok(());
1024 } else {
1025 let slot = &mut self.players[cur_id];
1026 if let Some(mut h) = slot.handle.take() {
1027 let _ = h.stop(Tween::default());
1028 }
1029 slot.clear_runtime_only();
1030 }
1031 Ok(())
1032 }
1033}
1034
1035
1036fn path_is_file(path: &Path) -> bool {
1037 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1038 { crate::resource::wasm_path_is_file(path) }
1039 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1040 { path.is_file() }
1041}
1042
1043fn load_gameexe_decode_options(project_dir: &Path) -> Result<GameexeDecodeOptions> {
1044 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1045 {
1046 let mut opt = GameexeDecodeOptions::default();
1047 opt.game_angou_code = Some(siglus_assets::keys::GAMEEXE_KEY.to_vec());
1048 for name in ["key.toml", "Key.toml"] {
1049 let p = project_dir.join(name);
1050 if crate::resource::wasm_path_is_file(&p) {
1051 let text = crate::resource::read_file_to_string(&p)?;
1052 let cfg = siglus_assets::key_toml::parse_key_toml(&text)?;
1053 opt.exe_key16 = cfg.exe_key16;
1054 opt.base_angou_code = cfg.base_angou_code;
1055 if cfg.game_angou_code.is_some() { opt.game_angou_code = cfg.game_angou_code; }
1056 if let Some(order) = cfg.chain_order { opt.chain_order = order; }
1057 break;
1058 }
1059 }
1060 Ok(opt)
1061 }
1062 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1063 { GameexeDecodeOptions::from_project_dir(project_dir) }
1064}
1065
1066fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
1067 const CANDIDATES: &[&str] = &[
1068 "Gameexe.dat",
1069 "Gameexe.ini",
1070 "gameexe.dat",
1071 "gameexe.ini",
1072 "GameexeEN.dat",
1073 "GameexeEN.ini",
1074 "GameexeZH.dat",
1075 "GameexeZH.ini",
1076 "GameexeZHTW.dat",
1077 "GameexeZHTW.ini",
1078 "GameexeDE.dat",
1079 "GameexeDE.ini",
1080 "GameexeES.dat",
1081 "GameexeES.ini",
1082 "GameexeFR.dat",
1083 "GameexeFR.ini",
1084 "GameexeID.dat",
1085 "GameexeID.ini",
1086 ];
1087 for name in CANDIDATES {
1088 let p = project_dir.join(name);
1089 if path_is_file(&p) {
1090 return Some(p);
1091 }
1092 }
1093 None
1094}
1095
1096fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
1097 let path = find_gameexe_path(project_dir)?;
1098 let raw = crate::resource::read_file_bytes(&path).ok()?;
1099 if path
1100 .extension()
1101 .and_then(|s| s.to_str())
1102 .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
1103 {
1104 let text = String::from_utf8(raw).ok()?;
1105 return Some(GameexeConfig::from_text(&text));
1106 }
1107 let opt = load_gameexe_decode_options(project_dir).ok()?;
1108 let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
1109 Some(GameexeConfig::from_text(&text))
1110}