feat: port Music streaming audio system to pure Rust
Browse filesReplace WA's Music/StreamingAudio system with a pure Rust implementation
using hound for WAV parsing and DirectSound for streaming playback.
Music system (0x354-byte object, vtable 0x66B3E0):
- Unified Music + StreamingAudio into single struct with full field map
- 6-slot vtable fully replaced via vtable_replace! macro
- Constructor hooked (0x58BC10), destructor uses wa_free
- Playlist management, track ID-to-filename mapping (1-13)
- Volume control with dB lookup table, using Fixed type
Streaming engine:
- WAV streaming via hound + BufReader<File> (replaces mmio API)
- DirectSound double-buffered ring buffer with timeSetEvent refill
- Buffer size matches original formula (timer_delay * avg_bytes/1000)
- advance_track guards against premature track changes (checks
IDirectSoundBuffer::GetStatus before advancing)
Sound hooks:
- Hook PlayWormSound, StopWormSound, LoadAndPlayStreaming
- Remove old streaming.rs stub, update GameSession to use *mut Music
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/address.rs +10 -1
- crates/openwa-core/src/audio/mod.rs +1 -3
- crates/openwa-core/src/audio/music.rs +992 -7
- crates/openwa-core/src/audio/streaming.rs +0 -16
- crates/openwa-core/src/engine/game_session.rs +4 -2
- crates/openwa-wormkit/src/replacements/hardware_init.rs +4 -4
- crates/openwa-wormkit/src/replacements/mod.rs +2 -0
- crates/openwa-wormkit/src/replacements/music.rs +132 -0
- crates/openwa-wormkit/src/replacements/sound.rs +89 -0
|
@@ -56,8 +56,15 @@ pub mod va {
|
|
| 56 |
};
|
| 57 |
pub use crate::task::worm::{CTASK_WORM_CONSTRUCTOR, CTASK_WORM_VTABLE};
|
| 58 |
|
| 59 |
-
// Re-exported from
|
| 60 |
pub use crate::audio::dssound::DS_SOUND_VTABLE;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
pub use crate::display::base::DISPLAY_BASE_VTABLE;
|
| 62 |
pub use crate::display::dd_display::DD_DISPLAY_VTABLE;
|
| 63 |
pub use crate::display::palette::PALETTE_VTABLE;
|
|
@@ -538,6 +545,8 @@ pub mod va {
|
|
| 538 |
fn/Fastcall PLAY_SOUND_POOLED_DIRECT = 0x0054_6B50;
|
| 539 |
/// Distance3D_Attenuation
|
| 540 |
fn/Usercall DISTANCE_3D_ATTENUATION = 0x0054_30F0;
|
|
|
|
|
|
|
| 541 |
}
|
| 542 |
|
| 543 |
// =========================================================================
|
|
|
|
| 56 |
};
|
| 57 |
pub use crate::task::worm::{CTASK_WORM_CONSTRUCTOR, CTASK_WORM_VTABLE};
|
| 58 |
|
| 59 |
+
// Re-exported from audio modules
|
| 60 |
pub use crate::audio::dssound::DS_SOUND_VTABLE;
|
| 61 |
+
pub use crate::audio::music::{
|
| 62 |
+
MUSIC_CONSTRUCTOR, MUSIC_DESTRUCTOR, MUSIC_PLAY_TRACK, MUSIC_VOLUME_DB_TABLE,
|
| 63 |
+
MUSIC_VTABLE, STREAMING_AUDIO_FILL_AND_START, STREAMING_AUDIO_INIT,
|
| 64 |
+
STREAMING_AUDIO_INIT_PLAYBACK, STREAMING_AUDIO_OPEN, STREAMING_AUDIO_OPEN_WAV,
|
| 65 |
+
STREAMING_AUDIO_READ_CHUNK, STREAMING_AUDIO_RESET, STREAMING_AUDIO_STOP,
|
| 66 |
+
STREAMING_AUDIO_TIMER_CALLBACK,
|
| 67 |
+
};
|
| 68 |
pub use crate::display::base::DISPLAY_BASE_VTABLE;
|
| 69 |
pub use crate::display::dd_display::DD_DISPLAY_VTABLE;
|
| 70 |
pub use crate::display::palette::PALETTE_VTABLE;
|
|
|
|
| 545 |
fn/Fastcall PLAY_SOUND_POOLED_DIRECT = 0x0054_6B50;
|
| 546 |
/// Distance3D_Attenuation
|
| 547 |
fn/Usercall DISTANCE_3D_ATTENUATION = 0x0054_30F0;
|
| 548 |
+
/// ActiveSoundTable::stop_sound β stops an active streaming sound by handle.
|
| 549 |
+
fn ACTIVE_SOUND_TABLE_STOP_SOUND = 0x0054_6490;
|
| 550 |
}
|
| 551 |
|
| 552 |
// =========================================================================
|
|
@@ -6,7 +6,6 @@ pub mod sound_id;
|
|
| 6 |
pub mod sound_ops;
|
| 7 |
pub mod speech;
|
| 8 |
pub mod speech_ops;
|
| 9 |
-
pub mod streaming;
|
| 10 |
pub mod wav_player;
|
| 11 |
|
| 12 |
pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
|
|
@@ -19,7 +18,6 @@ pub use dssound::{
|
|
| 19 |
};
|
| 20 |
pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
|
| 21 |
pub use known_sound_id::KnownSoundId;
|
| 22 |
-
pub use music::Music;
|
| 23 |
pub use sound_id::SoundId;
|
| 24 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
| 25 |
-
pub use streaming::StreamingAudio;
|
|
|
|
| 6 |
pub mod sound_ops;
|
| 7 |
pub mod speech;
|
| 8 |
pub mod speech_ops;
|
|
|
|
| 9 |
pub mod wav_player;
|
| 10 |
|
| 11 |
pub use active_sound::{ActiveSoundEntry, ActiveSoundTable};
|
|
|
|
| 18 |
};
|
| 19 |
pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
|
| 20 |
pub use known_sound_id::KnownSoundId;
|
| 21 |
+
pub use music::{Music, MusicVtable, StreamingAudio};
|
| 22 |
pub use sound_id::SoundId;
|
| 23 |
pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
|
|
|
|
@@ -1,12 +1,997 @@
|
|
| 1 |
-
/// Music β music playback object.
|
| 2 |
///
|
| 3 |
-
///
|
| 4 |
-
///
|
| 5 |
-
/// Passed as "music" parameter to DDGame__Constructor, stored at DDGame+0x014.
|
| 6 |
///
|
| 7 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
#[repr(C)]
|
| 9 |
pub struct Music {
|
| 10 |
-
/// 0x000: Vtable pointer (0x66B3E0)
|
| 11 |
-
pub vtable: *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
}
|
|
|
|
| 1 |
+
/// Music β music playback object (0x354 bytes).
|
| 2 |
///
|
| 3 |
+
/// Combines a playlist controller with an embedded [`StreamingAudio`] engine.
|
| 4 |
+
/// Created by `GameEngine::InitHardware`, stored at `DDGame+0x014`.
|
|
|
|
| 5 |
///
|
| 6 |
+
/// Constructor: 0x58BC10, usercall(ESI=this) + stack(IDirectSound*, path), RET 0x8.
|
| 7 |
+
/// Vtable: 0x66B3E0 (6 slots).
|
| 8 |
+
///
|
| 9 |
+
/// The music system maps track IDs 1-13 to WAV files under the base path
|
| 10 |
+
/// (typically `Data\streams`).
|
| 11 |
+
use std::ffi::c_char;
|
| 12 |
+
|
| 13 |
+
use crate::fixed::Fixed;
|
| 14 |
+
|
| 15 |
+
/// Raw COM pointer stored as u32 (matches the 32-bit target's pointer size).
|
| 16 |
+
type Ptr32 = u32;
|
| 17 |
+
|
| 18 |
+
define_addresses! {
|
| 19 |
+
class "Music" {
|
| 20 |
+
ctor MUSIC_CONSTRUCTOR = 0x0058_BC10;
|
| 21 |
+
}
|
| 22 |
+
/// Music destructor (called from scalar deleting dtor).
|
| 23 |
+
fn MUSIC_DESTRUCTOR = 0x0058_BC80;
|
| 24 |
+
/// Music::PlayTrack internal β maps track ID to filename, calls StreamingAudio::Open.
|
| 25 |
+
fn/Usercall MUSIC_PLAY_TRACK = 0x0058_BD20;
|
| 26 |
+
/// Music volume dB lookup table (65 entries, i32).
|
| 27 |
+
data MUSIC_VOLUME_DB_TABLE = 0x006A_F960;
|
| 28 |
+
/// StreamingAudio::Init (zero-init base object).
|
| 29 |
+
fn/Usercall STREAMING_AUDIO_INIT = 0x0047_07B0;
|
| 30 |
+
/// StreamingAudio::Open β opens WAV, configures buffer, starts playback.
|
| 31 |
+
fn/Usercall STREAMING_AUDIO_OPEN = 0x0057_4B30;
|
| 32 |
+
/// StreamingAudio::Stop β kills timer, releases DS buffer, closes mmio.
|
| 33 |
+
fn/Usercall STREAMING_AUDIO_STOP = 0x0057_4CA0;
|
| 34 |
+
/// StreamingAudio::InitPlayback β core setup (timer caps, DS buffer, timer).
|
| 35 |
+
fn/Stdcall STREAMING_AUDIO_INIT_PLAYBACK = 0x0057_4D10;
|
| 36 |
+
/// StreamingAudio::OpenWAV β parse RIFF/WAVE via mmio.
|
| 37 |
+
fn/Usercall STREAMING_AUDIO_OPEN_WAV = 0x0057_4F80;
|
| 38 |
+
/// StreamingAudio::FillAndStart β initial buffer fill + DS Play + timeSetEvent.
|
| 39 |
+
fn/Usercall STREAMING_AUDIO_FILL_AND_START = 0x0057_51B0;
|
| 40 |
+
/// StreamingAudio::Reset β kill timer, stop buffer, seek back.
|
| 41 |
+
fn/Usercall STREAMING_AUDIO_RESET = 0x0057_52D0;
|
| 42 |
+
/// StreamingAudio::TimerCallback β periodic buffer refill (stdcall, RET 0x14).
|
| 43 |
+
fn/Stdcall STREAMING_AUDIO_TIMER_CALLBACK = 0x0057_53A0;
|
| 44 |
+
/// StreamingAudio::ReadChunk β read clamped WAV data from mmio.
|
| 45 |
+
fn STREAMING_AUDIO_READ_CHUNK = 0x0057_5370;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// Music object β playlist controller + embedded streaming engine.
|
| 49 |
+
///
|
| 50 |
+
/// Total size: 0x354 bytes (852 bytes).
|
| 51 |
#[repr(C)]
|
| 52 |
pub struct Music {
|
| 53 |
+
/// 0x000: Vtable pointer (0x66B3E0, 6 slots).
|
| 54 |
+
pub vtable: *const MusicVtable,
|
| 55 |
+
/// 0x004: Playlist track IDs (max 32 entries).
|
| 56 |
+
pub playlist: [u32; 32],
|
| 57 |
+
/// 0x084: Base path for music files (e.g. "DATA\\Music"), 256 bytes.
|
| 58 |
+
pub base_path: [c_char; 256],
|
| 59 |
+
/// 0x184: Number of tracks in current playlist.
|
| 60 |
+
pub playlist_count: u32,
|
| 61 |
+
/// 0x188: Index of currently playing track in playlist.
|
| 62 |
+
pub current_track_index: u32,
|
| 63 |
+
/// 0x18C: IDirectSound instance (raw COM pointer).
|
| 64 |
+
pub direct_sound: Ptr32,
|
| 65 |
+
/// 0x190: Embedded streaming audio engine (0x1C4 bytes).
|
| 66 |
+
pub streaming: StreamingAudio,
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const _: () = assert!(core::mem::size_of::<Music>() == 0x354);
|
| 70 |
+
|
| 71 |
+
/// Streaming audio engine β WAV file streaming via DirectSound double-buffering.
|
| 72 |
+
///
|
| 73 |
+
/// Embedded at Music+0x190. The original WA code uses mmio API for WAV I/O;
|
| 74 |
+
/// our Rust port uses `hound` + `std::fs::File` for reading, and
|
| 75 |
+
/// `timeSetEvent` for periodic buffer refill.
|
| 76 |
+
///
|
| 77 |
+
/// Size: 0x1C4 bytes (452 bytes).
|
| 78 |
+
#[repr(C)]
|
| 79 |
+
pub struct StreamingAudio {
|
| 80 |
+
/// 0x00: DirectSound ring buffer size in bytes.
|
| 81 |
+
pub ds_buffer_size: u32,
|
| 82 |
+
/// 0x04: Unknown / unused.
|
| 83 |
+
pub _unknown_04: u32,
|
| 84 |
+
/// 0x08: Buffer segment count (quality-dependent: 24/5/4/3 for 8k/16k/22k/44kHz).
|
| 85 |
+
pub buffer_segments: u32,
|
| 86 |
+
/// 0x0C: log2(disk sector size) β shift count for sector alignment.
|
| 87 |
+
pub sector_shift: u8,
|
| 88 |
+
/// 0x0D: Padding.
|
| 89 |
+
pub _pad_0d: [u8; 3],
|
| 90 |
+
/// 0x10: Timer interval in milliseconds (quality-dependent: 2000/600/400/100ms).
|
| 91 |
+
pub timer_delay_ms: u32,
|
| 92 |
+
/// 0x14: Timer resolution in milliseconds (timer_delay_ms / 4).
|
| 93 |
+
pub timer_resolution_ms: u32,
|
| 94 |
+
/// 0x18: Number of aligned sectors in ring buffer.
|
| 95 |
+
pub num_sectors: u32,
|
| 96 |
+
/// 0x1C: Ring buffer write cursor position (bytes).
|
| 97 |
+
pub write_cursor: u32,
|
| 98 |
+
/// 0x20: Total bytes played (accumulated from play cursor delta).
|
| 99 |
+
pub bytes_played: u32,
|
| 100 |
+
/// 0x24: Total bytes read from WAV data chunk.
|
| 101 |
+
pub file_bytes_read: u32,
|
| 102 |
+
/// 0x28: Previous play cursor position for delta calculation.
|
| 103 |
+
pub last_play_cursor: u32,
|
| 104 |
+
/// 0x2C: Flags bitfield.
|
| 105 |
+
/// - bit 0: looping
|
| 106 |
+
/// - bit 1: threaded open
|
| 107 |
+
/// - bit 2: auto-start
|
| 108 |
+
/// - bit 14 (0x4000): DS buffer created/active
|
| 109 |
+
/// - bit 15 (0x8000): stopping
|
| 110 |
+
pub flags: u32,
|
| 111 |
+
/// 0x30: Completion callback (LPTHREAD_START_ROUTINE), called when non-looping playback ends.
|
| 112 |
+
pub completion_callback: u32,
|
| 113 |
+
/// 0x34: User data for completion callback.
|
| 114 |
+
pub completion_callback_data: u32,
|
| 115 |
+
/// 0x38: Timer capabilities (TIMECAPS: wPeriodMin, wPeriodMax).
|
| 116 |
+
pub time_caps: [u32; 2],
|
| 117 |
+
/// 0x40: Resolved timer period (clamped from TIMECAPS).
|
| 118 |
+
pub timer_period: u32,
|
| 119 |
+
/// 0x44: Timer ID from timeSetEvent.
|
| 120 |
+
pub timer_id: u32,
|
| 121 |
+
/// 0x48: Absolute path to WAV file (MAX_PATH = 260 bytes).
|
| 122 |
+
pub file_path: [c_char; 260],
|
| 123 |
+
/// 0x14C: WAV format (WAVEFORMATEX, 18 bytes for PCM).
|
| 124 |
+
pub wave_format: WaveFormatPcm,
|
| 125 |
+
/// 0x15E: Padding after WAVEFORMATEX.
|
| 126 |
+
pub _pad_15e: [u8; 2],
|
| 127 |
+
/// 0x160: mmio file handle β repurposed as Box<std::fs::File> in Rust.
|
| 128 |
+
/// Original: HMMIO. We store a raw pointer to a heap-allocated File.
|
| 129 |
+
/// 0 / null when no file is open.
|
| 130 |
+
pub file_handle: u32,
|
| 131 |
+
/// 0x164: Data chunk info (reused MMCKINFO in original; we store data_size + data_offset).
|
| 132 |
+
pub data_chunk: MmckInfo,
|
| 133 |
+
/// 0x178: Snapshot of bytes_played (copied each timer tick).
|
| 134 |
+
pub bytes_played_snapshot: u32,
|
| 135 |
+
/// 0x17C: 1 when DS buffer is live, 0 otherwise.
|
| 136 |
+
pub is_initialized: u32,
|
| 137 |
+
/// 0x180: Last HRESULT from DirectSound operations.
|
| 138 |
+
pub last_error: u32,
|
| 139 |
+
/// 0x184: Unknown / unused.
|
| 140 |
+
pub _unknown_184: u32,
|
| 141 |
+
/// 0x188: End-of-stream threshold in bytes (fade-out distance).
|
| 142 |
+
pub fade_out_bytes: u32,
|
| 143 |
+
/// 0x18C: Cumulative timer callback duration (timeGetTime).
|
| 144 |
+
pub timer_time_total: u32,
|
| 145 |
+
/// 0x190: Average timer callback time (timer_time_total / timer_call_count).
|
| 146 |
+
pub avg_timer_time: u32,
|
| 147 |
+
/// 0x194: Number of timer callback invocations.
|
| 148 |
+
pub timer_call_count: u32,
|
| 149 |
+
/// 0x198: DS buffer status from GetStatus.
|
| 150 |
+
pub ds_buffer_status: u32,
|
| 151 |
+
/// 0x19C: The DirectSound secondary buffer for playback (raw COM pointer).
|
| 152 |
+
pub ds_buffer: Ptr32,
|
| 153 |
+
/// 0x1A0: IDirectSound device reference (raw COM pointer).
|
| 154 |
+
pub direct_sound: Ptr32,
|
| 155 |
+
/// 0x1A4: Unknown trailing data.
|
| 156 |
+
pub _unknown_1a4: [u8; 0x20],
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
const _: () = assert!(core::mem::size_of::<StreamingAudio>() == 0x1C4);
|
| 160 |
+
|
| 161 |
+
/// WAVEFORMATEX PCM subset (18 bytes) β matches the Windows WAVEFORMATEX layout.
|
| 162 |
+
#[repr(C, packed)]
|
| 163 |
+
#[derive(Clone, Copy, Debug)]
|
| 164 |
+
pub struct WaveFormatPcm {
|
| 165 |
+
pub format_tag: u16,
|
| 166 |
+
pub channels: u16,
|
| 167 |
+
pub samples_per_sec: u32,
|
| 168 |
+
pub avg_bytes_per_sec: u32,
|
| 169 |
+
pub block_align: u16,
|
| 170 |
+
pub bits_per_sample: u16,
|
| 171 |
+
pub cb_size: u16,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
const _: () = assert!(core::mem::size_of::<WaveFormatPcm>() == 18);
|
| 175 |
+
|
| 176 |
+
/// MMCKINFO (20 bytes) β multimedia chunk info.
|
| 177 |
+
/// We reuse the struct layout for compatibility but only care about ck_size (data length)
|
| 178 |
+
/// and data_offset (file offset of PCM data start).
|
| 179 |
+
#[repr(C)]
|
| 180 |
+
#[derive(Clone, Copy, Debug)]
|
| 181 |
+
pub struct MmckInfo {
|
| 182 |
+
pub ck_id: u32,
|
| 183 |
+
pub ck_size: u32,
|
| 184 |
+
pub fcc_type: u32,
|
| 185 |
+
pub data_offset: u32,
|
| 186 |
+
pub dw_flags: u32,
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
const _: () = assert!(core::mem::size_of::<MmckInfo>() == 20);
|
| 190 |
+
|
| 191 |
+
/// Music vtable β 6 slots at 0x66B3E0.
|
| 192 |
+
///
|
| 193 |
+
/// Slot order verified by reading vtable data from binary:
|
| 194 |
+
/// +0x00: 0x58BC60 +0x04: 0x58BEE0 +0x08: 0x58BF40
|
| 195 |
+
/// +0x0C: 0x58BE70 +0x10: 0x58BE90 +0x14: 0x58BCE0
|
| 196 |
+
#[openwa_core::vtable(size = 6, va = 0x0066_B3E0, class = "Music")]
|
| 197 |
+
pub struct MusicVtable {
|
| 198 |
+
/// scalar_deleting_dtor(flags) β 0x58BC60
|
| 199 |
+
pub scalar_deleting_dtor: fn(this: *mut Music, flags: u32),
|
| 200 |
+
/// advance_track β 0x58BEE0
|
| 201 |
+
pub advance_track: fn(this: *mut Music),
|
| 202 |
+
/// set_volume(volume) β 0x58BF40. Volume is Fixed 16.16.
|
| 203 |
+
pub set_volume: fn(this: *mut Music, volume: Fixed),
|
| 204 |
+
/// start_music(track_id) β 0x58BE70
|
| 205 |
+
pub start_music: fn(this: *mut Music, track_id: u32),
|
| 206 |
+
/// set_playlist(tracks, count) β 0x58BE90
|
| 207 |
+
pub set_playlist: fn(this: *mut Music, tracks: *const u32, count: u32),
|
| 208 |
+
/// stop_and_cleanup β 0x58BCE0
|
| 209 |
+
pub stop_and_cleanup: fn(this: *mut Music),
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/// Track ID to WAV filename mapping (from PlayTrack switch at 0x58BD20).
|
| 213 |
+
/// Track IDs 1-13 map to these filenames.
|
| 214 |
+
pub const MUSIC_TRACKS: [&str; 13] = [
|
| 215 |
+
"ingame-01-generic.wav", // 1
|
| 216 |
+
"ingame-02-cavern.wav", // 2
|
| 217 |
+
"ingame-03-jungle.wav", // 3
|
| 218 |
+
"ingame-04-battlezone.wav", // 4
|
| 219 |
+
"ingame-05-forest.wav", // 5
|
| 220 |
+
"ingame-06-weird-alien-plan.wav", // 6
|
| 221 |
+
"ingame-07-outerspace.wav", // 7
|
| 222 |
+
"ingame-08-desert.wav", // 8
|
| 223 |
+
"ingame-09-hell.wav", // 9
|
| 224 |
+
"ingame-10-mech-workshop.wav", // 10
|
| 225 |
+
"ingame-11-rain&surf.wav", // 11
|
| 226 |
+
"suddendeath2-loop.wav", // 12
|
| 227 |
+
"suddendeath1-loop.wav", // 13
|
| 228 |
+
];
|
| 229 |
+
|
| 230 |
+
/// Streaming audio flags.
|
| 231 |
+
pub mod streaming_flags {
|
| 232 |
+
/// Sound should loop when reaching end of data.
|
| 233 |
+
pub const LOOP: u32 = 0x0001;
|
| 234 |
+
/// Open the file on a worker thread.
|
| 235 |
+
pub const THREADED: u32 = 0x0002;
|
| 236 |
+
/// Auto-start playback after opening.
|
| 237 |
+
pub const AUTO_START: u32 = 0x0004;
|
| 238 |
+
/// DirectSound buffer has been created and is active.
|
| 239 |
+
pub const DS_BUFFER_ACTIVE: u32 = 0x4000;
|
| 240 |
+
/// Playback is stopping / fading out.
|
| 241 |
+
pub const STOPPING: u32 = 0x8000;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// ============================================================
|
| 245 |
+
// Music volume dB lookup table
|
| 246 |
+
// ============================================================
|
| 247 |
+
|
| 248 |
+
/// Volume-to-dB lookup table for music (65 entries).
|
| 249 |
+
/// Index: volume * 64 / 0x10000, maps Fixed 16.16 volume to centibels.
|
| 250 |
+
/// Copied from WA.exe .rdata at 0x6AF960.
|
| 251 |
+
/// Index 0 = -10000 cB (silence), index 64 = 0 cB (full volume).
|
| 252 |
+
const MUSIC_VOLUME_DB: [i32; 65] = [
|
| 253 |
+
-10000, -6644, -5644, -5059, -4644, -4322, -4059, -3837, -3644, -3473, -3322,
|
| 254 |
+
-3185, -3059, -2943, -2837, -2737, -2644, -2557, -2474, -2396, -2322, -2251,
|
| 255 |
+
-2184, -2119, -2059, -2000, -1943, -1889, -1837, -1787, -1737, -1690, -1644,
|
| 256 |
+
-1600, -1557, -1515, -1474, -1434, -1396, -1358, -1322, -1286, -1251, -1217,
|
| 257 |
+
-1184, -1151, -1119, -1088, -1059, -1028, -1000, -971, -943, -915, -889,
|
| 258 |
+
-862, -837, -811, -787, -762, -737, -714, -690, -667, 0,
|
| 259 |
+
];
|
| 260 |
+
|
| 261 |
+
// ============================================================
|
| 262 |
+
// StreamingAudio implementation
|
| 263 |
+
// ============================================================
|
| 264 |
+
|
| 265 |
+
use std::io::{Read, Seek, SeekFrom};
|
| 266 |
+
use std::sync::atomic::{AtomicU32, Ordering};
|
| 267 |
+
|
| 268 |
+
use windows::Win32::Media::Audio::DirectSound::{
|
| 269 |
+
IDirectSound, IDirectSoundBuffer, DSBUFFERDESC,
|
| 270 |
+
};
|
| 271 |
+
use windows::Win32::Media::Audio::{WAVEFORMATEX, WAVE_FORMAT_PCM};
|
| 272 |
+
|
| 273 |
+
/// Global pointer to the active StreamingAudio for the timer callback.
|
| 274 |
+
/// Only one streaming audio instance is active at a time (music playback).
|
| 275 |
+
static STREAMING_AUDIO_PTR: AtomicU32 = AtomicU32::new(0);
|
| 276 |
+
|
| 277 |
+
/// Reinterpret a `*const Ptr32` field (pointing to a stored raw COM pointer)
|
| 278 |
+
/// as `&IDirectSoundBuffer`. The pointer must point to stable memory (e.g. a
|
| 279 |
+
/// struct field), NOT a stack local β the returned reference borrows it.
|
| 280 |
+
#[inline]
|
| 281 |
+
unsafe fn ds_buf_from_field(field: *const Ptr32) -> &'static IDirectSoundBuffer {
|
| 282 |
+
&*(field as *const IDirectSoundBuffer)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/// Reinterpret a `*const Ptr32` field (pointing to a stored raw COM pointer)
|
| 286 |
+
/// as `&IDirectSound`.
|
| 287 |
+
#[inline]
|
| 288 |
+
unsafe fn ds_from_field(field: *const Ptr32) -> &'static IDirectSound {
|
| 289 |
+
&*(field as *const IDirectSound)
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
impl StreamingAudio {
|
| 293 |
+
/// Zero-initialize the streaming audio engine. Port of FUN_004707B0.
|
| 294 |
+
pub fn init(&mut self) {
|
| 295 |
+
// Zero everything except keep the struct's memory layout intact
|
| 296 |
+
unsafe {
|
| 297 |
+
core::ptr::write_bytes(self as *mut Self as *mut u8, 0, core::mem::size_of::<Self>());
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/// Open a WAV file and start streaming playback. Port of FUN_00574B30.
|
| 302 |
+
///
|
| 303 |
+
/// `flags` is a combination of `streaming_flags` constants.
|
| 304 |
+
/// `path` is the absolute path to the WAV file.
|
| 305 |
+
/// `ds` is the IDirectSound instance for buffer creation.
|
| 306 |
+
pub unsafe fn open(
|
| 307 |
+
&mut self,
|
| 308 |
+
path: &str,
|
| 309 |
+
flags: u32,
|
| 310 |
+
ds: Ptr32,
|
| 311 |
+
) -> bool {
|
| 312 |
+
// If already initialized, stop first
|
| 313 |
+
if self.is_initialized != 0 {
|
| 314 |
+
self.stop();
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
self.direct_sound = ds;
|
| 318 |
+
self.flags = flags;
|
| 319 |
+
self.completion_callback = 0;
|
| 320 |
+
self.completion_callback_data = 0;
|
| 321 |
+
|
| 322 |
+
// Determine quality parameters from flags (original switch on flags & 0x78).
|
| 323 |
+
match flags & 0x78 {
|
| 324 |
+
0x08 => {
|
| 325 |
+
// 8kHz quality
|
| 326 |
+
self.buffer_segments = 24;
|
| 327 |
+
self.timer_delay_ms = 2000;
|
| 328 |
+
}
|
| 329 |
+
0x10 => {
|
| 330 |
+
// 16kHz
|
| 331 |
+
self.buffer_segments = 5;
|
| 332 |
+
self.timer_delay_ms = 600;
|
| 333 |
+
}
|
| 334 |
+
0x20 => {
|
| 335 |
+
// 22kHz (default for music with flags 0x24/0x25)
|
| 336 |
+
self.buffer_segments = 4;
|
| 337 |
+
self.timer_delay_ms = 400;
|
| 338 |
+
}
|
| 339 |
+
0x40 => {
|
| 340 |
+
// 44kHz
|
| 341 |
+
self.buffer_segments = 3;
|
| 342 |
+
self.timer_delay_ms = 100;
|
| 343 |
+
}
|
| 344 |
+
_ => {
|
| 345 |
+
// Invalid quality β original returns E_INVALIDARG
|
| 346 |
+
return false;
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// Copy path to file_path buffer
|
| 351 |
+
let path_bytes = path.as_bytes();
|
| 352 |
+
let copy_len = path_bytes.len().min(259);
|
| 353 |
+
unsafe {
|
| 354 |
+
core::ptr::copy_nonoverlapping(
|
| 355 |
+
path_bytes.as_ptr(),
|
| 356 |
+
self.file_path.as_mut_ptr() as *mut u8,
|
| 357 |
+
copy_len,
|
| 358 |
+
);
|
| 359 |
+
self.file_path[copy_len] = 0; // null terminate
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// Initialize playback (open WAV, create buffer, start timer)
|
| 363 |
+
self.init_playback()
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
/// Core playback setup: open WAV file, create DirectSound buffer, start timer.
|
| 367 |
+
/// Port of FUN_00574D10 + FUN_00574F80 + FUN_005751B0.
|
| 368 |
+
unsafe fn init_playback(&mut self) -> bool {
|
| 369 |
+
// Open the WAV file using hound
|
| 370 |
+
let path_cstr = std::ffi::CStr::from_ptr(self.file_path.as_ptr());
|
| 371 |
+
let path_str = match path_cstr.to_str() {
|
| 372 |
+
Ok(s) => s,
|
| 373 |
+
Err(_) => {
|
| 374 |
+
let _ = crate::log::log_line("[Music] init_playback: invalid UTF-8 path");
|
| 375 |
+
return false;
|
| 376 |
+
}
|
| 377 |
+
};
|
| 378 |
+
|
| 379 |
+
let reader = match hound::WavReader::open(path_str) {
|
| 380 |
+
Ok(r) => r,
|
| 381 |
+
Err(e) => {
|
| 382 |
+
let _ = crate::log::log_line(&format!(
|
| 383 |
+
"[Music] init_playback: failed to open \"{}\": {}", path_str, e
|
| 384 |
+
));
|
| 385 |
+
return false;
|
| 386 |
+
}
|
| 387 |
+
};
|
| 388 |
+
|
| 389 |
+
let spec = reader.spec();
|
| 390 |
+
|
| 391 |
+
// Only PCM format supported
|
| 392 |
+
if spec.sample_format != hound::SampleFormat::Int {
|
| 393 |
+
return false;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// Store format info
|
| 397 |
+
let block_align = spec.channels as u16 * (spec.bits_per_sample as u16 / 8);
|
| 398 |
+
let avg_bytes_per_sec = spec.sample_rate * block_align as u32;
|
| 399 |
+
self.wave_format = WaveFormatPcm {
|
| 400 |
+
format_tag: 1, // WAVE_FORMAT_PCM
|
| 401 |
+
channels: spec.channels as u16,
|
| 402 |
+
samples_per_sec: spec.sample_rate,
|
| 403 |
+
avg_bytes_per_sec,
|
| 404 |
+
block_align,
|
| 405 |
+
bits_per_sample: spec.bits_per_sample as u16,
|
| 406 |
+
cb_size: 0,
|
| 407 |
+
};
|
| 408 |
+
|
| 409 |
+
// Store data chunk info
|
| 410 |
+
let data_len = reader.len() * (spec.bits_per_sample as u32 / 8);
|
| 411 |
+
self.data_chunk.ck_size = data_len;
|
| 412 |
+
|
| 413 |
+
// Get data start position from the inner reader, then extract the File.
|
| 414 |
+
// After WavReader::new(), the inner reader is positioned at data start.
|
| 415 |
+
let mut inner = reader.into_inner();
|
| 416 |
+
let data_start = inner.stream_position().unwrap_or(0);
|
| 417 |
+
self.data_chunk.data_offset = data_start as u32;
|
| 418 |
+
|
| 419 |
+
// Store the BufReader<File> as a raw pointer for streaming reads
|
| 420 |
+
let file = Box::new(inner);
|
| 421 |
+
self.file_handle = Box::into_raw(file) as u32;
|
| 422 |
+
|
| 423 |
+
// Compute buffer size matching original float arithmetic (constant at 0x679760 = 1000.0):
|
| 424 |
+
// per_tick = round(timer_delay_ms * avg_bytes_per_sec / 1000.0)
|
| 425 |
+
// ds_buffer_size = per_tick * buffer_segments
|
| 426 |
+
let per_tick = (self.timer_delay_ms as f64 * avg_bytes_per_sec as f64 / 1000.0)
|
| 427 |
+
.round() as u32;
|
| 428 |
+
let buffer_bytes = per_tick * self.buffer_segments;
|
| 429 |
+
self.ds_buffer_size = buffer_bytes;
|
| 430 |
+
|
| 431 |
+
// Timer resolution
|
| 432 |
+
self.timer_resolution_ms = self.timer_delay_ms / 4;
|
| 433 |
+
|
| 434 |
+
// Fade-out threshold
|
| 435 |
+
let fade_ms = self.timer_delay_ms + self.timer_delay_ms / 4;
|
| 436 |
+
self.fade_out_bytes = fade_ms * (avg_bytes_per_sec / 1000);
|
| 437 |
+
|
| 438 |
+
// Create DirectSound secondary buffer (streaming, no STATIC flag)
|
| 439 |
+
let wfx = WAVEFORMATEX {
|
| 440 |
+
wFormatTag: WAVE_FORMAT_PCM as u16,
|
| 441 |
+
nChannels: spec.channels as u16,
|
| 442 |
+
nSamplesPerSec: spec.sample_rate,
|
| 443 |
+
nAvgBytesPerSec: avg_bytes_per_sec,
|
| 444 |
+
nBlockAlign: block_align,
|
| 445 |
+
wBitsPerSample: spec.bits_per_sample as u16,
|
| 446 |
+
cbSize: 0,
|
| 447 |
+
};
|
| 448 |
+
|
| 449 |
+
// Flags matching original: 0xE8 = DSBCAPS_LOCSOFTWARE(0x08) |
|
| 450 |
+
// DSBCAPS_CTRLPAN(0x20) | DSBCAPS_CTRLFREQUENCY(0x40) | DSBCAPS_CTRLVOLUME(0x80)
|
| 451 |
+
let desc = DSBUFFERDESC {
|
| 452 |
+
dwSize: core::mem::size_of::<DSBUFFERDESC>() as u32,
|
| 453 |
+
dwFlags: 0xE8,
|
| 454 |
+
dwBufferBytes: buffer_bytes,
|
| 455 |
+
dwReserved: 0,
|
| 456 |
+
lpwfxFormat: &wfx as *const _ as *mut _,
|
| 457 |
+
..Default::default()
|
| 458 |
+
};
|
| 459 |
+
|
| 460 |
+
let ds = ds_from_field(core::ptr::addr_of!(self.direct_sound));
|
| 461 |
+
let mut buf_opt: Option<IDirectSoundBuffer> = None;
|
| 462 |
+
if ds.CreateSoundBuffer(&desc, &mut buf_opt, None).is_err() {
|
| 463 |
+
self.close_file();
|
| 464 |
+
return false;
|
| 465 |
+
}
|
| 466 |
+
let ds_buf = match buf_opt {
|
| 467 |
+
Some(b) => b,
|
| 468 |
+
None => {
|
| 469 |
+
self.close_file();
|
| 470 |
+
return false;
|
| 471 |
+
}
|
| 472 |
+
};
|
| 473 |
+
|
| 474 |
+
// Store buffer as raw COM pointer (u32)
|
| 475 |
+
self.ds_buffer = core::mem::transmute_copy::<IDirectSoundBuffer, u32>(&ds_buf);
|
| 476 |
+
core::mem::forget(ds_buf); // Don't Release β we own the pointer now
|
| 477 |
+
self.is_initialized = 1;
|
| 478 |
+
self.flags |= streaming_flags::DS_BUFFER_ACTIVE;
|
| 479 |
+
|
| 480 |
+
// Reset playback state
|
| 481 |
+
self.write_cursor = 0;
|
| 482 |
+
self.bytes_played = 0;
|
| 483 |
+
self.file_bytes_read = 0;
|
| 484 |
+
self.last_play_cursor = 0;
|
| 485 |
+
self.bytes_played_snapshot = 0;
|
| 486 |
+
self.timer_time_total = 0;
|
| 487 |
+
self.avg_timer_time = 0;
|
| 488 |
+
self.timer_call_count = 0;
|
| 489 |
+
|
| 490 |
+
// Fill initial buffer and start playback
|
| 491 |
+
self.fill_and_start()
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
/// Fill the buffer with initial data and start playback + timer.
|
| 495 |
+
/// Port of FUN_005751B0.
|
| 496 |
+
unsafe fn fill_and_start(&mut self) -> bool {
|
| 497 |
+
if self.is_initialized == 0 || self.ds_buffer == 0 {
|
| 498 |
+
return false;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
let buf = ds_buf_from_field(core::ptr::addr_of!(self.ds_buffer));
|
| 502 |
+
|
| 503 |
+
// Lock the entire buffer for initial fill
|
| 504 |
+
let mut ptr1: *mut core::ffi::c_void = core::ptr::null_mut();
|
| 505 |
+
let mut len1: u32 = 0;
|
| 506 |
+
let mut ptr2: *mut core::ffi::c_void = core::ptr::null_mut();
|
| 507 |
+
let mut len2: u32 = 0;
|
| 508 |
+
|
| 509 |
+
if buf
|
| 510 |
+
.Lock(
|
| 511 |
+
0,
|
| 512 |
+
self.ds_buffer_size,
|
| 513 |
+
&mut ptr1,
|
| 514 |
+
&mut len1,
|
| 515 |
+
Some(&mut ptr2),
|
| 516 |
+
Some(&mut len2),
|
| 517 |
+
0,
|
| 518 |
+
)
|
| 519 |
+
.is_err()
|
| 520 |
+
{
|
| 521 |
+
return false;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
// Read from WAV file into buffer
|
| 525 |
+
let bytes_read = self.read_file_data(
|
| 526 |
+
core::slice::from_raw_parts_mut(ptr1 as *mut u8, len1 as usize),
|
| 527 |
+
);
|
| 528 |
+
|
| 529 |
+
// Zero-fill remainder if we didn't fill the whole buffer
|
| 530 |
+
if (bytes_read as u32) < len1 {
|
| 531 |
+
core::ptr::write_bytes(
|
| 532 |
+
(ptr1 as *mut u8).add(bytes_read),
|
| 533 |
+
0,
|
| 534 |
+
(len1 as usize) - bytes_read,
|
| 535 |
+
);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
let _ = buf.Unlock(ptr1 as *const _, len1, Some(ptr2 as *const _), len2);
|
| 539 |
+
|
| 540 |
+
self.write_cursor = bytes_read as u32;
|
| 541 |
+
if self.write_cursor >= self.ds_buffer_size {
|
| 542 |
+
self.write_cursor = 0;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
// Start playback (looping the DS buffer β we manage the data ourselves)
|
| 546 |
+
// DSBPLAY_LOOPING = 1
|
| 547 |
+
let _ = buf.Play(0, 0, 1);
|
| 548 |
+
|
| 549 |
+
// Set up the global pointer for the timer callback
|
| 550 |
+
STREAMING_AUDIO_PTR.store(self as *mut Self as u32, Ordering::Release);
|
| 551 |
+
|
| 552 |
+
// Start periodic timer using timeSetEvent
|
| 553 |
+
let timer_id = time_set_event(
|
| 554 |
+
self.timer_delay_ms,
|
| 555 |
+
self.timer_resolution_ms,
|
| 556 |
+
streaming_timer_callback,
|
| 557 |
+
0,
|
| 558 |
+
1, // TIME_PERIODIC
|
| 559 |
+
);
|
| 560 |
+
self.timer_id = timer_id;
|
| 561 |
+
|
| 562 |
+
true
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
/// Stop streaming playback and release resources. Port of FUN_00574CA0 + FUN_005752D0.
|
| 566 |
+
pub unsafe fn stop(&mut self) {
|
| 567 |
+
// Kill timer first
|
| 568 |
+
if self.timer_id != 0 {
|
| 569 |
+
time_kill_event(self.timer_id);
|
| 570 |
+
self.timer_id = 0;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
// Clear the global pointer
|
| 574 |
+
STREAMING_AUDIO_PTR.store(0, Ordering::Release);
|
| 575 |
+
|
| 576 |
+
// Stop and release DS buffer
|
| 577 |
+
if self.is_initialized != 0 {
|
| 578 |
+
if self.ds_buffer != 0 {
|
| 579 |
+
let buf = ds_buf_from_field(core::ptr::addr_of!(self.ds_buffer));
|
| 580 |
+
let _ = buf.Stop();
|
| 581 |
+
// Release COM reference via transmute + drop
|
| 582 |
+
let com: IDirectSoundBuffer = core::mem::transmute_copy(&self.ds_buffer);
|
| 583 |
+
drop(com);
|
| 584 |
+
self.ds_buffer = 0;
|
| 585 |
+
}
|
| 586 |
+
self.is_initialized = 0;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
self.flags &= !streaming_flags::DS_BUFFER_ACTIVE;
|
| 590 |
+
self.close_file();
|
| 591 |
+
|
| 592 |
+
// Reset state
|
| 593 |
+
self.write_cursor = 0;
|
| 594 |
+
self.bytes_played = 0;
|
| 595 |
+
self.file_bytes_read = 0;
|
| 596 |
+
self.last_play_cursor = 0;
|
| 597 |
+
self.bytes_played_snapshot = 0;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
/// Close the WAV file handle.
|
| 602 |
+
unsafe fn close_file(&mut self) {
|
| 603 |
+
if self.file_handle != 0 {
|
| 604 |
+
let _ = Box::from_raw(self.file_handle as *mut std::io::BufReader<std::fs::File>);
|
| 605 |
+
self.file_handle = 0;
|
| 606 |
+
}
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
/// Read PCM data from the WAV file. Reads until `buf` is full or EOF.
|
| 610 |
+
/// Returns number of bytes actually read.
|
| 611 |
+
unsafe fn read_file_data(&mut self, buf: &mut [u8]) -> usize {
|
| 612 |
+
if self.file_handle == 0 {
|
| 613 |
+
return 0;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
let file = &mut *(self.file_handle as *mut std::io::BufReader<std::fs::File>);
|
| 617 |
+
let data_remaining = self.data_chunk.ck_size.saturating_sub(self.file_bytes_read);
|
| 618 |
+
let to_read = buf.len().min(data_remaining as usize);
|
| 619 |
+
|
| 620 |
+
if to_read == 0 {
|
| 621 |
+
return 0;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
// Loop to fill the entire requested amount (read() may return partial).
|
| 625 |
+
let mut total = 0;
|
| 626 |
+
while total < to_read {
|
| 627 |
+
match file.read(&mut buf[total..to_read]) {
|
| 628 |
+
Ok(0) => break, // EOF
|
| 629 |
+
Ok(n) => total += n,
|
| 630 |
+
Err(_) => break,
|
| 631 |
+
}
|
| 632 |
+
}
|
| 633 |
+
self.file_bytes_read += total as u32;
|
| 634 |
+
total
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/// Seek back to the start of the PCM data (for looping).
|
| 638 |
+
unsafe fn seek_to_data_start(&mut self) {
|
| 639 |
+
if self.file_handle == 0 {
|
| 640 |
+
return;
|
| 641 |
+
}
|
| 642 |
+
let file = &mut *(self.file_handle as *mut std::io::BufReader<std::fs::File>);
|
| 643 |
+
let _ = file.seek(SeekFrom::Start(self.data_chunk.data_offset as u64));
|
| 644 |
+
self.file_bytes_read = 0;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
/// Timer callback handler β refills the DirectSound buffer.
|
| 648 |
+
/// Port of FUN_005753A0 (the heart of streaming).
|
| 649 |
+
unsafe fn timer_tick(&mut self) {
|
| 650 |
+
if self.is_initialized == 0 || self.ds_buffer == 0 {
|
| 651 |
+
return;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
let buf = ds_buf_from_field(core::ptr::addr_of!(self.ds_buffer));
|
| 655 |
+
|
| 656 |
+
// Get current play position
|
| 657 |
+
let mut play_cursor: u32 = 0;
|
| 658 |
+
let mut _write_cursor: u32 = 0;
|
| 659 |
+
if buf
|
| 660 |
+
.GetCurrentPosition(Some(&mut play_cursor), Some(&mut _write_cursor))
|
| 661 |
+
.is_err()
|
| 662 |
+
{
|
| 663 |
+
return;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
// Calculate how many bytes have been played since last tick
|
| 667 |
+
let played_delta = if play_cursor >= self.last_play_cursor {
|
| 668 |
+
play_cursor - self.last_play_cursor
|
| 669 |
+
} else {
|
| 670 |
+
// Wraparound
|
| 671 |
+
(self.ds_buffer_size - self.last_play_cursor) + play_cursor
|
| 672 |
+
};
|
| 673 |
+
self.last_play_cursor = play_cursor;
|
| 674 |
+
self.bytes_played += played_delta;
|
| 675 |
+
self.bytes_played_snapshot = self.bytes_played;
|
| 676 |
+
self.timer_call_count += 1;
|
| 677 |
+
|
| 678 |
+
// Check if we've reached end of stream (non-looping)
|
| 679 |
+
let is_looping = self.flags & streaming_flags::LOOP != 0;
|
| 680 |
+
if !is_looping
|
| 681 |
+
&& self.bytes_played >= self.data_chunk.ck_size.saturating_sub(self.fade_out_bytes)
|
| 682 |
+
{
|
| 683 |
+
self.flags |= streaming_flags::STOPPING;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
if self.flags & streaming_flags::STOPPING != 0 {
|
| 687 |
+
// Check if playback has finished
|
| 688 |
+
if self.bytes_played >= self.data_chunk.ck_size {
|
| 689 |
+
// Signal completion via callback if set
|
| 690 |
+
if self.completion_callback != 0 {
|
| 691 |
+
// Original spawns a thread; we just call it directly since
|
| 692 |
+
// the callback is typically Music::advance_track
|
| 693 |
+
let cb: unsafe extern "system" fn(u32) -> u32 =
|
| 694 |
+
core::mem::transmute(self.completion_callback as usize);
|
| 695 |
+
cb(self.completion_callback_data);
|
| 696 |
+
}
|
| 697 |
+
return;
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// Calculate how much buffer space is available to fill
|
| 702 |
+
let available = if play_cursor >= self.write_cursor {
|
| 703 |
+
play_cursor - self.write_cursor
|
| 704 |
+
} else {
|
| 705 |
+
(self.ds_buffer_size - self.write_cursor) + play_cursor
|
| 706 |
+
};
|
| 707 |
+
|
| 708 |
+
if available == 0 {
|
| 709 |
+
return;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// Lock the buffer region from write_cursor for `available` bytes
|
| 713 |
+
let mut ptr1: *mut core::ffi::c_void = core::ptr::null_mut();
|
| 714 |
+
let mut len1: u32 = 0;
|
| 715 |
+
let mut ptr2: *mut core::ffi::c_void = core::ptr::null_mut();
|
| 716 |
+
let mut len2: u32 = 0;
|
| 717 |
+
|
| 718 |
+
if buf
|
| 719 |
+
.Lock(
|
| 720 |
+
self.write_cursor,
|
| 721 |
+
available,
|
| 722 |
+
&mut ptr1,
|
| 723 |
+
&mut len1,
|
| 724 |
+
Some(&mut ptr2),
|
| 725 |
+
Some(&mut len2),
|
| 726 |
+
0,
|
| 727 |
+
)
|
| 728 |
+
.is_err()
|
| 729 |
+
{
|
| 730 |
+
return;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
// Fill first region
|
| 734 |
+
let mut total_written: u32 = 0;
|
| 735 |
+
if len1 > 0 {
|
| 736 |
+
let slice = core::slice::from_raw_parts_mut(ptr1 as *mut u8, len1 as usize);
|
| 737 |
+
let read = self.read_file_data(slice);
|
| 738 |
+
if (read as u32) < len1 {
|
| 739 |
+
if is_looping {
|
| 740 |
+
// Loop: seek back and read more
|
| 741 |
+
self.seek_to_data_start();
|
| 742 |
+
self.bytes_played = 0;
|
| 743 |
+
self.last_play_cursor = play_cursor;
|
| 744 |
+
let remaining = &mut slice[read..];
|
| 745 |
+
let extra = self.read_file_data(remaining);
|
| 746 |
+
total_written += read as u32 + extra as u32;
|
| 747 |
+
// Zero any remaining
|
| 748 |
+
if read + extra < len1 as usize {
|
| 749 |
+
core::ptr::write_bytes(
|
| 750 |
+
(ptr1 as *mut u8).add(read + extra),
|
| 751 |
+
0,
|
| 752 |
+
len1 as usize - read - extra,
|
| 753 |
+
);
|
| 754 |
+
}
|
| 755 |
+
} else {
|
| 756 |
+
// End of file: zero-fill remainder
|
| 757 |
+
core::ptr::write_bytes(
|
| 758 |
+
(ptr1 as *mut u8).add(read),
|
| 759 |
+
0,
|
| 760 |
+
len1 as usize - read,
|
| 761 |
+
);
|
| 762 |
+
total_written += len1;
|
| 763 |
+
}
|
| 764 |
+
} else {
|
| 765 |
+
total_written += len1;
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
// Fill second region (wraparound)
|
| 770 |
+
if len2 > 0 {
|
| 771 |
+
let slice = core::slice::from_raw_parts_mut(ptr2 as *mut u8, len2 as usize);
|
| 772 |
+
let read = self.read_file_data(slice);
|
| 773 |
+
if (read as u32) < len2 {
|
| 774 |
+
if is_looping {
|
| 775 |
+
self.seek_to_data_start();
|
| 776 |
+
self.bytes_played = 0;
|
| 777 |
+
self.last_play_cursor = play_cursor;
|
| 778 |
+
let remaining = &mut slice[read..];
|
| 779 |
+
let extra = self.read_file_data(remaining);
|
| 780 |
+
total_written += read as u32 + extra as u32;
|
| 781 |
+
if read + extra < len2 as usize {
|
| 782 |
+
core::ptr::write_bytes(
|
| 783 |
+
(ptr2 as *mut u8).add(read + extra),
|
| 784 |
+
0,
|
| 785 |
+
len2 as usize - read - extra,
|
| 786 |
+
);
|
| 787 |
+
}
|
| 788 |
+
} else {
|
| 789 |
+
core::ptr::write_bytes(
|
| 790 |
+
(ptr2 as *mut u8).add(read),
|
| 791 |
+
0,
|
| 792 |
+
len2 as usize - read,
|
| 793 |
+
);
|
| 794 |
+
total_written += len2;
|
| 795 |
+
}
|
| 796 |
+
} else {
|
| 797 |
+
total_written += len2;
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
let _ = buf.Unlock(ptr1 as *const _, len1, Some(ptr2 as *const _), len2);
|
| 802 |
+
|
| 803 |
+
// Advance write cursor
|
| 804 |
+
self.write_cursor = (self.write_cursor + total_written) % self.ds_buffer_size;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
/// Check if the DS buffer is currently playing.
|
| 808 |
+
pub unsafe fn is_playing(&self) -> bool {
|
| 809 |
+
if self.is_initialized == 0 || self.ds_buffer == 0 {
|
| 810 |
+
return false;
|
| 811 |
+
}
|
| 812 |
+
let buf = ds_buf_from_field(core::ptr::addr_of!(self.ds_buffer));
|
| 813 |
+
match buf.GetStatus() {
|
| 814 |
+
Ok(status) => status & 1 != 0, // DSBSTATUS_PLAYING
|
| 815 |
+
Err(_) => false,
|
| 816 |
+
}
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
/// Set the playback volume. Volume is in centibels (0 = full, -10000 = silent).
|
| 820 |
+
pub unsafe fn set_ds_volume(&self, volume_cb: i32) {
|
| 821 |
+
if self.is_initialized == 0 || self.ds_buffer == 0 {
|
| 822 |
+
return;
|
| 823 |
+
}
|
| 824 |
+
let buf = ds_buf_from_field(core::ptr::addr_of!(self.ds_buffer));
|
| 825 |
+
let _ = buf.SetVolume(volume_cb);
|
| 826 |
+
}
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
/// Timer callback invoked by timeSetEvent. Dispatches to the active StreamingAudio.
|
| 830 |
+
unsafe extern "system" fn streaming_timer_callback(
|
| 831 |
+
_timer_id: u32,
|
| 832 |
+
_msg: u32,
|
| 833 |
+
_user: u32,
|
| 834 |
+
_dw1: u32,
|
| 835 |
+
_dw2: u32,
|
| 836 |
+
) {
|
| 837 |
+
let ptr = STREAMING_AUDIO_PTR.load(Ordering::Acquire);
|
| 838 |
+
if ptr != 0 {
|
| 839 |
+
let stream = &mut *(ptr as *mut StreamingAudio);
|
| 840 |
+
stream.timer_tick();
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
// ============================================================
|
| 845 |
+
// Music implementation
|
| 846 |
+
// ============================================================
|
| 847 |
+
|
| 848 |
+
impl Music {
|
| 849 |
+
/// Initialize a Music object. Port of constructor at 0x58BC10.
|
| 850 |
+
///
|
| 851 |
+
/// `ds` is the IDirectSound instance (from DSSound).
|
| 852 |
+
/// `base_path` is the music directory path (e.g. "DATA\\Music").
|
| 853 |
+
pub unsafe fn init(
|
| 854 |
+
this: *mut Music,
|
| 855 |
+
ds: Ptr32,
|
| 856 |
+
base_path: &str,
|
| 857 |
+
) {
|
| 858 |
+
// Zero the streaming sub-object
|
| 859 |
+
(*this).streaming.init();
|
| 860 |
+
|
| 861 |
+
// Set vtable β will be replaced by hook installation
|
| 862 |
+
(*this).vtable = core::ptr::null();
|
| 863 |
+
|
| 864 |
+
// Copy base path
|
| 865 |
+
let bytes = base_path.as_bytes();
|
| 866 |
+
let copy_len = bytes.len().min(255);
|
| 867 |
+
core::ptr::copy_nonoverlapping(
|
| 868 |
+
bytes.as_ptr(),
|
| 869 |
+
(*this).base_path.as_mut_ptr() as *mut u8,
|
| 870 |
+
copy_len,
|
| 871 |
+
);
|
| 872 |
+
(*this).base_path[copy_len] = 0;
|
| 873 |
+
|
| 874 |
+
(*this).playlist_count = 0;
|
| 875 |
+
(*this).current_track_index = 0;
|
| 876 |
+
(*this).direct_sound = ds;
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
/// Play a specific track by ID (1-13). Port of FUN_0058BD20.
|
| 880 |
+
///
|
| 881 |
+
/// Maps the track ID to a WAV filename, builds the full path, and starts streaming.
|
| 882 |
+
pub unsafe fn play_track(this: *mut Music, track_id: u32, do_loop: bool) {
|
| 883 |
+
if track_id < 1 || track_id > 13 {
|
| 884 |
+
return;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
let filename = MUSIC_TRACKS[(track_id - 1) as usize];
|
| 888 |
+
|
| 889 |
+
// Build full path: base_path/filename
|
| 890 |
+
let base = std::ffi::CStr::from_ptr((*this).base_path.as_ptr())
|
| 891 |
+
.to_str()
|
| 892 |
+
.unwrap_or("");
|
| 893 |
+
let full_path = format!("{}/{}", base, filename);
|
| 894 |
+
|
| 895 |
+
// Flags: 0x24 = AUTO_START | quality=2 (22kHz), plus LOOP if requested
|
| 896 |
+
let mut flags: u32 = 0x24;
|
| 897 |
+
if do_loop {
|
| 898 |
+
flags |= streaming_flags::LOOP;
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
let _ = (*this).streaming.open(&full_path, flags, (*this).direct_sound);
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
/// Start a specific music track (looping). Port of vtable[1] at 0x58BE70.
|
| 905 |
+
///
|
| 906 |
+
/// Clears the playlist (count=0) and plays the given track with loop=true.
|
| 907 |
+
pub unsafe fn start_music(this: *mut Music, track_id: u32) {
|
| 908 |
+
(*this).playlist_count = 0;
|
| 909 |
+
Music::play_track(this, track_id, true);
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
/// Set a playlist and start the first track. Port of vtable[4] at 0x58BE90.
|
| 913 |
+
///
|
| 914 |
+
/// Original passes loop=false (0) to PlayTrack β the track plays once,
|
| 915 |
+
/// then advance_track is called via the completion callback.
|
| 916 |
+
pub unsafe fn set_playlist(this: *mut Music, tracks: *const u32, count: u32) {
|
| 917 |
+
let count = count.min(32) as usize;
|
| 918 |
+
core::ptr::copy_nonoverlapping(tracks, (*this).playlist.as_mut_ptr(), count);
|
| 919 |
+
(*this).playlist_count = count as u32;
|
| 920 |
+
(*this).current_track_index = 0;
|
| 921 |
+
|
| 922 |
+
if count > 0 {
|
| 923 |
+
let first_track = (*this).playlist[0];
|
| 924 |
+
Music::play_track(this, first_track, false);
|
| 925 |
+
}
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
/// Advance to the next track in the playlist (wrapping). Port of vtable[1] at 0x58BEE0.
|
| 929 |
+
///
|
| 930 |
+
/// The original checks if the DS buffer is still playing β if so, it returns
|
| 931 |
+
/// without advancing. The game calls this speculatively (e.g., each turn),
|
| 932 |
+
/// relying on the "still playing" guard to avoid premature track changes.
|
| 933 |
+
pub unsafe fn advance_track(this: *mut Music) {
|
| 934 |
+
if (*this).playlist_count == 0 {
|
| 935 |
+
return;
|
| 936 |
+
}
|
| 937 |
+
// Don't advance if the current track is still playing
|
| 938 |
+
if (*this).streaming.is_playing() {
|
| 939 |
+
return;
|
| 940 |
+
}
|
| 941 |
+
(*this).current_track_index =
|
| 942 |
+
((*this).current_track_index + 1) % (*this).playlist_count;
|
| 943 |
+
let track_id = (*this).playlist[(*this).current_track_index as usize];
|
| 944 |
+
Music::play_track(this, track_id, false);
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
/// Set music volume. Port of vtable[2] at 0x58BF40.
|
| 948 |
+
pub unsafe fn set_volume(this: *mut Music, volume: Fixed) {
|
| 949 |
+
let clamped = volume.clamp(Fixed::ZERO, Fixed::ONE);
|
| 950 |
+
// Map to dB lookup table: index = volume * 64 / ONE
|
| 951 |
+
let idx = ((clamped.0 as u64 * 64) / Fixed::ONE.0 as u64) as usize;
|
| 952 |
+
let db = MUSIC_VOLUME_DB[idx.min(64)];
|
| 953 |
+
(*this).streaming.set_ds_volume(db);
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
/// Stop playback and clean up. Port of vtable[5] at 0x58BCE0.
|
| 957 |
+
pub unsafe fn stop_and_cleanup(this: *mut Music) {
|
| 958 |
+
(*this).streaming.stop();
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
/// Destructor. Port of 0x58BC80.
|
| 962 |
+
pub unsafe fn destructor(this: *mut Music) {
|
| 963 |
+
// Original does Sleep(1) for thread safety β we rely on atomic ptr clearing in stop()
|
| 964 |
+
(*this).streaming.stop();
|
| 965 |
+
}
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
// ============================================================
|
| 969 |
+
// Windows timer API bindings (timeSetEvent / timeKillEvent)
|
| 970 |
+
// ============================================================
|
| 971 |
+
|
| 972 |
+
#[link(name = "winmm")]
|
| 973 |
+
extern "system" {
|
| 974 |
+
fn timeSetEvent(
|
| 975 |
+
delay: u32,
|
| 976 |
+
resolution: u32,
|
| 977 |
+
callback: unsafe extern "system" fn(u32, u32, u32, u32, u32),
|
| 978 |
+
user: u32,
|
| 979 |
+
event_type: u32,
|
| 980 |
+
) -> u32;
|
| 981 |
+
|
| 982 |
+
fn timeKillEvent(timer_id: u32) -> u32;
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
fn time_set_event(
|
| 986 |
+
delay: u32,
|
| 987 |
+
resolution: u32,
|
| 988 |
+
callback: unsafe extern "system" fn(u32, u32, u32, u32, u32),
|
| 989 |
+
user: u32,
|
| 990 |
+
event_type: u32,
|
| 991 |
+
) -> u32 {
|
| 992 |
+
unsafe { timeSetEvent(delay, resolution, callback, user, event_type) }
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
fn time_kill_event(timer_id: u32) -> u32 {
|
| 996 |
+
unsafe { timeKillEvent(timer_id) }
|
| 997 |
}
|
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
/// StreamingAudio β streaming audio playback subsystem (music).
|
| 2 |
-
///
|
| 3 |
-
/// Constructor: FUN_0058BC10, usercall(ESI=this) + 2 stack params, RET 0x8.
|
| 4 |
-
/// Size: 0x354 bytes.
|
| 5 |
-
///
|
| 6 |
-
/// Created by GameEngine__InitHardware when speech is enabled
|
| 7 |
-
/// (GameInfo+0xDAA4 != 0) and DSSound initialized successfully.
|
| 8 |
-
/// Stored at GameSession+0xB4.
|
| 9 |
-
///
|
| 10 |
-
/// OPAQUE: Internal layout not yet mapped.
|
| 11 |
-
#[repr(C)]
|
| 12 |
-
pub struct StreamingAudio {
|
| 13 |
-
pub _data: [u8; 0x354],
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
const _: () = assert!(core::mem::size_of::<StreamingAudio>() == 0x354);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
use crate::audio::dssound::DSSound;
|
|
|
|
| 2 |
use crate::display::palette::Palette;
|
| 3 |
use crate::engine::ddgame_wrapper::DDGameWrapper;
|
| 4 |
use crate::input::keyboard::DDKeyboard;
|
|
@@ -81,8 +82,9 @@ pub struct GameSession {
|
|
| 81 |
pub display: *mut u8,
|
| 82 |
/// 0x0B0: `Palette*` β 0x28 bytes, vtable `Palette_vtable_Maybe`
|
| 83 |
pub palette: *mut Palette,
|
| 84 |
-
/// 0x0B4:
|
| 85 |
-
|
|
|
|
| 86 |
/// 0x0B8: input controller β 0x1800 bytes; null if `param_4 == 0` at init
|
| 87 |
pub input_ctrl: *mut u8,
|
| 88 |
/// 0x0BC: timing object β 0x30 bytes (`FUN_0053e950`)
|
|
|
|
| 1 |
use crate::audio::dssound::DSSound;
|
| 2 |
+
use crate::audio::music::Music;
|
| 3 |
use crate::display::palette::Palette;
|
| 4 |
use crate::engine::ddgame_wrapper::DDGameWrapper;
|
| 5 |
use crate::input::keyboard::DDKeyboard;
|
|
|
|
| 82 |
pub display: *mut u8,
|
| 83 |
/// 0x0B0: `Palette*` β 0x28 bytes, vtable `Palette_vtable_Maybe`
|
| 84 |
pub palette: *mut Palette,
|
| 85 |
+
/// 0x0B4: Music object β 0x354 bytes (constructor 0x58BC10, vtable 0x66B3E0).
|
| 86 |
+
/// Combines playlist controller + embedded streaming audio engine.
|
| 87 |
+
pub streaming_audio: *mut Music,
|
| 88 |
/// 0x0B8: input controller β 0x1800 bytes; null if `param_4 == 0` at init
|
| 89 |
pub input_ctrl: *mut u8,
|
| 90 |
/// 0x0BC: timing object β 0x30 bytes (`FUN_0053e950`)
|
|
@@ -45,7 +45,7 @@ use super::game_session;
|
|
| 45 |
use crate::hook;
|
| 46 |
use crate::log_line;
|
| 47 |
use openwa_core::address::va;
|
| 48 |
-
use openwa_core::audio::{DSSound,
|
| 49 |
use openwa_core::display::{DDDisplay, DisplayBase, DisplayGfx, Palette};
|
| 50 |
use openwa_core::engine::{DDGameWrapper, DDNetGameWrapper, GameInfo, GameSession, GameTimer};
|
| 51 |
use openwa_core::input::{DDKeyboard, InputCtrl, InputCtrlVtable};
|
|
@@ -377,14 +377,14 @@ unsafe extern "cdecl" fn impl_init_hardware(
|
|
| 377 |
// ββ Streaming audio βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 378 |
(*session).streaming_audio = core::ptr::null_mut();
|
| 379 |
if !(*session).sound.is_null() && gi.speech_enabled != 0 {
|
| 380 |
-
let stream = WABox::<
|
| 381 |
let ids = (*(*session).sound).direct_sound as *mut u8;
|
| 382 |
call_streaming_audio_ctor(
|
| 383 |
stream as *mut u8,
|
| 384 |
ids,
|
| 385 |
gi.streaming_audio_config.as_mut_ptr(),
|
| 386 |
);
|
| 387 |
-
(*session).streaming_audio = stream
|
| 388 |
}
|
| 389 |
} else {
|
| 390 |
// ββ Headless / stats mode βββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -408,7 +408,7 @@ unsafe extern "cdecl" fn impl_init_hardware(
|
|
| 408 |
(*session).sound,
|
| 409 |
(*session).keyboard as *mut u8,
|
| 410 |
(*session).palette,
|
| 411 |
-
(*session).streaming_audio,
|
| 412 |
(*session).input_ctrl,
|
| 413 |
);
|
| 414 |
(*session).ddgame_wrapper = wrapper;
|
|
|
|
| 45 |
use crate::hook;
|
| 46 |
use crate::log_line;
|
| 47 |
use openwa_core::address::va;
|
| 48 |
+
use openwa_core::audio::{DSSound, Music};
|
| 49 |
use openwa_core::display::{DDDisplay, DisplayBase, DisplayGfx, Palette};
|
| 50 |
use openwa_core::engine::{DDGameWrapper, DDNetGameWrapper, GameInfo, GameSession, GameTimer};
|
| 51 |
use openwa_core::input::{DDKeyboard, InputCtrl, InputCtrlVtable};
|
|
|
|
| 377 |
// ββ Streaming audio βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 378 |
(*session).streaming_audio = core::ptr::null_mut();
|
| 379 |
if !(*session).sound.is_null() && gi.speech_enabled != 0 {
|
| 380 |
+
let stream = WABox::<Music>::alloc(0x354, 0x334).leak();
|
| 381 |
let ids = (*(*session).sound).direct_sound as *mut u8;
|
| 382 |
call_streaming_audio_ctor(
|
| 383 |
stream as *mut u8,
|
| 384 |
ids,
|
| 385 |
gi.streaming_audio_config.as_mut_ptr(),
|
| 386 |
);
|
| 387 |
+
(*session).streaming_audio = stream;
|
| 388 |
}
|
| 389 |
} else {
|
| 390 |
// ββ Headless / stats mode βββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 408 |
(*session).sound,
|
| 409 |
(*session).keyboard as *mut u8,
|
| 410 |
(*session).palette,
|
| 411 |
+
(*session).streaming_audio as *mut u8,
|
| 412 |
(*session).input_ctrl,
|
| 413 |
);
|
| 414 |
(*session).ddgame_wrapper = wrapper;
|
|
@@ -9,6 +9,7 @@ mod game_session;
|
|
| 9 |
mod game_state_hooks;
|
| 10 |
mod hardware_init;
|
| 11 |
mod headless;
|
|
|
|
| 12 |
mod render;
|
| 13 |
mod replay;
|
| 14 |
mod replay_test;
|
|
@@ -56,6 +57,7 @@ pub fn install_all() -> Result<(), String> {
|
|
| 56 |
sprite::install()?;
|
| 57 |
sound::install()?;
|
| 58 |
speech::install()?;
|
|
|
|
| 59 |
ddgame_init::install()?;
|
| 60 |
game_state_hooks::install()?;
|
| 61 |
replay::install()?;
|
|
|
|
| 9 |
mod game_state_hooks;
|
| 10 |
mod hardware_init;
|
| 11 |
mod headless;
|
| 12 |
+
mod music;
|
| 13 |
mod render;
|
| 14 |
mod replay;
|
| 15 |
mod replay_test;
|
|
|
|
| 57 |
sprite::install()?;
|
| 58 |
sound::install()?;
|
| 59 |
speech::install()?;
|
| 60 |
+
music::install()?;
|
| 61 |
ddgame_init::install()?;
|
| 62 |
game_state_hooks::install()?;
|
| 63 |
replay::install()?;
|
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Music playback hooks.
|
| 2 |
+
//!
|
| 3 |
+
//! Replaces WA's Music vtable (0x66B3E0, 6 slots) with Rust implementations
|
| 4 |
+
//! from `openwa_core::audio::music`.
|
| 5 |
+
//!
|
| 6 |
+
//! The Music system handles background music during gameplay:
|
| 7 |
+
//! - Track selection from playlist (track IDs 1-13 β WAV filenames)
|
| 8 |
+
//! - Streaming WAV playback via DirectSound double-buffering
|
| 9 |
+
//! - Volume control with dB lookup table
|
| 10 |
+
//! - Playlist cycling (advance to next track when current finishes)
|
| 11 |
+
|
| 12 |
+
use openwa_core::address::va;
|
| 13 |
+
use openwa_core::audio::music::{Music, MusicVtable};
|
| 14 |
+
use openwa_core::fixed::Fixed;
|
| 15 |
+
use openwa_core::rebase::rb;
|
| 16 |
+
use openwa_core::wa_alloc::wa_free;
|
| 17 |
+
|
| 18 |
+
use crate::hook;
|
| 19 |
+
use crate::log_line;
|
| 20 |
+
|
| 21 |
+
// ββ Music vtable replacement methods ββ
|
| 22 |
+
|
| 23 |
+
/// Slot 0: scalar_deleting_dtor β calls destructor, optionally frees.
|
| 24 |
+
unsafe extern "thiscall" fn hook_scalar_deleting_dtor(this: *mut Music, flags: u32) {
|
| 25 |
+
let _ = log_line(&format!(
|
| 26 |
+
"[Music] scalar_deleting_dtor: this=0x{:08X} flags={}",
|
| 27 |
+
this as u32, flags
|
| 28 |
+
));
|
| 29 |
+
Music::destructor(this);
|
| 30 |
+
if flags & 1 != 0 {
|
| 31 |
+
// Free via WA's operator delete (0x5C0AE8) β matches WABox allocation.
|
| 32 |
+
wa_free(this as *mut u8);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/// Slot 1: start_music β start a specific track (looping).
|
| 37 |
+
unsafe extern "thiscall" fn hook_start_music(this: *mut Music, track_id: u32) {
|
| 38 |
+
let _ = log_line(&format!("[Music] start_music: track_id={}", track_id));
|
| 39 |
+
Music::start_music(this, track_id);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/// Slot 2: set_playlist β copy track IDs and start first track.
|
| 43 |
+
unsafe extern "thiscall" fn hook_set_playlist(this: *mut Music, tracks: *const u32, count: u32) {
|
| 44 |
+
let _ = log_line(&format!("[Music] set_playlist: count={}", count));
|
| 45 |
+
Music::set_playlist(this, tracks, count);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// Slot 3: advance_track β next track in playlist (wrapping).
|
| 49 |
+
unsafe extern "thiscall" fn hook_advance_track(this: *mut Music) {
|
| 50 |
+
let _ = log_line("[Music] advance_track");
|
| 51 |
+
Music::advance_track(this);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/// Slot 4: set_volume β set music volume (Fixed 16.16).
|
| 55 |
+
unsafe extern "thiscall" fn hook_set_volume(this: *mut Music, volume: Fixed) {
|
| 56 |
+
Music::set_volume(this, volume);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/// Slot 5: stop_and_cleanup β stop playback.
|
| 60 |
+
unsafe extern "thiscall" fn hook_stop_and_cleanup(this: *mut Music) {
|
| 61 |
+
let _ = log_line("[Music] stop_and_cleanup");
|
| 62 |
+
Music::stop_and_cleanup(this);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ββ Music constructor hook ββ
|
| 66 |
+
|
| 67 |
+
/// Hook for Music::Constructor (0x58BC10).
|
| 68 |
+
/// usercall(ESI=this) + stack(IDirectSound*, path_ptr), RET 0x8.
|
| 69 |
+
#[unsafe(naked)]
|
| 70 |
+
unsafe extern "C" fn trampoline_music_constructor() {
|
| 71 |
+
core::arch::naked_asm!(
|
| 72 |
+
"push [esp+8]", // path_ptr
|
| 73 |
+
"push [esp+8]", // ids (was +4, shifted +4)
|
| 74 |
+
"push esi", // this
|
| 75 |
+
"call {f}",
|
| 76 |
+
"add esp, 12",
|
| 77 |
+
"ret 0x8",
|
| 78 |
+
f = sym music_constructor_cdecl,
|
| 79 |
+
);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
unsafe extern "cdecl" fn music_constructor_cdecl(this: *mut Music, ids: u32, path_ptr: *const u8) {
|
| 83 |
+
let path = std::ffi::CStr::from_ptr(path_ptr as *const i8)
|
| 84 |
+
.to_str()
|
| 85 |
+
.unwrap_or("DATA\\Music");
|
| 86 |
+
let _ = log_line(&format!(
|
| 87 |
+
"[Music] Constructor: this=0x{:08X} ids=0x{:08X} path=\"{}\"",
|
| 88 |
+
this as u32, ids, path
|
| 89 |
+
));
|
| 90 |
+
Music::init(this, ids, path);
|
| 91 |
+
// Set vtable to the patched version (points to our hooked functions)
|
| 92 |
+
(*this).vtable = rb(va::MUSIC_VTABLE) as *const MusicVtable;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// ββ Hook installation ββ
|
| 96 |
+
|
| 97 |
+
pub fn install() -> Result<(), String> {
|
| 98 |
+
unsafe {
|
| 99 |
+
// Patch the Music vtable using vtable_replace!
|
| 100 |
+
use openwa_core::audio::music::MusicVtable;
|
| 101 |
+
use openwa_core::vtable_replace;
|
| 102 |
+
|
| 103 |
+
vtable_replace!(MusicVtable, va::MUSIC_VTABLE, {
|
| 104 |
+
scalar_deleting_dtor => hook_scalar_deleting_dtor,
|
| 105 |
+
advance_track => hook_advance_track,
|
| 106 |
+
set_volume => hook_set_volume,
|
| 107 |
+
start_music => hook_start_music,
|
| 108 |
+
set_playlist => hook_set_playlist,
|
| 109 |
+
stop_and_cleanup => hook_stop_and_cleanup,
|
| 110 |
+
})?;
|
| 111 |
+
|
| 112 |
+
let _ = log_line("[Music] vtable: patched 6/6 slots with Rust");
|
| 113 |
+
|
| 114 |
+
// Hook the Music constructor
|
| 115 |
+
let _ = hook::install(
|
| 116 |
+
"Music_Constructor",
|
| 117 |
+
va::MUSIC_CONSTRUCTOR,
|
| 118 |
+
trampoline_music_constructor as *const (),
|
| 119 |
+
)?;
|
| 120 |
+
|
| 121 |
+
// Trap the internal destructor (only called from our scalar_deleting_dtor)
|
| 122 |
+
hook::install_trap!("Music_Destructor", va::MUSIC_DESTRUCTOR);
|
| 123 |
+
// Trap PlayTrack (only called from our vtable methods)
|
| 124 |
+
hook::install_trap!("Music_PlayTrack", va::MUSIC_PLAY_TRACK);
|
| 125 |
+
|
| 126 |
+
// NOTE: StreamingAudio functions have WA callers outside the Music system
|
| 127 |
+
// (GameEngine::Shutdown calls Stop, Init is used generically).
|
| 128 |
+
// These can only be trapped once the shutdown path is also ported.
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
Ok(())
|
| 132 |
+
}
|
|
@@ -130,6 +130,71 @@ unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
|
|
| 130 |
sound_ops::play_sound_pooled_direct(task, slot, priority, volume)
|
| 131 |
}
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
// ββ Hook installation ββ
|
| 134 |
|
| 135 |
pub fn install() -> Result<(), String> {
|
|
@@ -180,6 +245,30 @@ pub fn install() -> Result<(), String> {
|
|
| 180 |
"LoadAndPlayStreamingPositional",
|
| 181 |
va::LOAD_AND_PLAY_STREAMING_POSITIONAL
|
| 182 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
Ok(())
|
|
|
|
| 130 |
sound_ops::play_sound_pooled_direct(task, slot, priority, volume)
|
| 131 |
}
|
| 132 |
|
| 133 |
+
// ββ PlayWormSound (0x5150D0): usercall(EDI=worm) + stack(sound_id, volume), RET 0x8 ββ
|
| 134 |
+
|
| 135 |
+
#[unsafe(naked)]
|
| 136 |
+
unsafe extern "C" fn trampoline_play_worm_sound() {
|
| 137 |
+
core::arch::naked_asm!(
|
| 138 |
+
"push [esp+8]", // volume
|
| 139 |
+
"push [esp+8]", // sound_id (was +4, shifted +4)
|
| 140 |
+
"push edi", // worm
|
| 141 |
+
"call {f}",
|
| 142 |
+
"add esp, 12",
|
| 143 |
+
"ret 0x8",
|
| 144 |
+
f = sym play_worm_sound_cdecl,
|
| 145 |
+
);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
unsafe extern "cdecl" fn play_worm_sound_cdecl(
|
| 149 |
+
worm: *mut CTaskWorm,
|
| 150 |
+
sound_id: SoundId,
|
| 151 |
+
volume: Fixed,
|
| 152 |
+
) {
|
| 153 |
+
sound_ops::play_worm_sound(worm, sound_id, volume);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// ββ StopWormSound (0x515180): usercall(ESI=worm), plain RET ββ
|
| 157 |
+
|
| 158 |
+
#[unsafe(naked)]
|
| 159 |
+
unsafe extern "C" fn trampoline_stop_worm_sound() {
|
| 160 |
+
core::arch::naked_asm!(
|
| 161 |
+
"push esi",
|
| 162 |
+
"call {f}",
|
| 163 |
+
"add esp, 4",
|
| 164 |
+
"ret",
|
| 165 |
+
f = sym stop_worm_sound_cdecl,
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
unsafe extern "cdecl" fn stop_worm_sound_cdecl(worm: *mut CTaskWorm) {
|
| 170 |
+
sound_ops::stop_worm_sound(worm);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// ββ LoadAndPlayStreaming (0x546C20): usercall(EAX=task, ESI=emitter) + stack(sound_id, flags, volume), RET 0xC ββ
|
| 174 |
+
|
| 175 |
+
#[unsafe(naked)]
|
| 176 |
+
unsafe extern "C" fn trampoline_load_and_play_streaming() {
|
| 177 |
+
core::arch::naked_asm!(
|
| 178 |
+
"push [esp+12]", // volume
|
| 179 |
+
"push [esp+12]", // flags (was +8, shifted +4)
|
| 180 |
+
"push [esp+12]", // sound_id (was +4, shifted +8)
|
| 181 |
+
"push eax", // task (EAX)
|
| 182 |
+
"call {f}",
|
| 183 |
+
"add esp, 16",
|
| 184 |
+
"ret 0xC",
|
| 185 |
+
f = sym load_and_play_streaming_cdecl,
|
| 186 |
+
);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
unsafe extern "cdecl" fn load_and_play_streaming_cdecl(
|
| 190 |
+
task: *mut CGameTask,
|
| 191 |
+
sound_id: SoundId,
|
| 192 |
+
flags: u32,
|
| 193 |
+
volume: Fixed,
|
| 194 |
+
) -> i32 {
|
| 195 |
+
sound_ops::load_and_play_streaming(task, sound_id, flags, volume)
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
// ββ Hook installation ββ
|
| 199 |
|
| 200 |
pub fn install() -> Result<(), String> {
|
|
|
|
| 245 |
"LoadAndPlayStreamingPositional",
|
| 246 |
va::LOAD_AND_PLAY_STREAMING_POSITIONAL
|
| 247 |
);
|
| 248 |
+
|
| 249 |
+
// Hook PlayWormSound + StopWormSound β these have WA callers beyond weapon_release.rs
|
| 250 |
+
let _ = hook::install(
|
| 251 |
+
"PlayWormSound",
|
| 252 |
+
va::PLAY_WORM_SOUND,
|
| 253 |
+
trampoline_play_worm_sound as *const (),
|
| 254 |
+
)?;
|
| 255 |
+
let _ = hook::install(
|
| 256 |
+
"StopWormSound",
|
| 257 |
+
va::STOP_WORM_SOUND,
|
| 258 |
+
trampoline_stop_worm_sound as *const (),
|
| 259 |
+
)?;
|
| 260 |
+
|
| 261 |
+
// NOTE: Sound sub-functions (Distance3D_Attenuation, ActiveSoundTable::stop_sound,
|
| 262 |
+
// RecordActiveSound, DispatchLocalSound, ComputeDistanceParams) all have WA callers
|
| 263 |
+
// through unported paths (PlayLocalNoEmitter, PlayLocalWithEmitter) that are
|
| 264 |
+
// exercised in headful mode. Cannot trap until those entry points are also hooked.
|
| 265 |
+
|
| 266 |
+
// Hook LoadAndPlayStreaming β has many WA callers (CTaskMissile, etc.)
|
| 267 |
+
let _ = hook::install(
|
| 268 |
+
"LoadAndPlayStreaming",
|
| 269 |
+
va::LOAD_AND_PLAY_STREAMING,
|
| 270 |
+
trampoline_load_and_play_streaming as *const (),
|
| 271 |
+
)?;
|
| 272 |
}
|
| 273 |
|
| 274 |
Ok(())
|