paavohtl Claude Opus 4.6 (1M context) commited on
Commit
aab6d6e
Β·
1 Parent(s): a553079

feat: port streaming sound chain to Rust, unify SoundId/SoundSlot

Browse files

Fully port play_worm_sound/play_worm_sound_2 β€” the streaming sound
playback chain no longer calls into WA.exe via naked asm bridges.

New Rust implementations (sound_ops.rs):
- distance_3d_attenuation: stereo panning with i64 fixed-point math
- compute_distance_params: reads listener pos + attenuation from DDGame
- record_active_sound: ring buffer insert into ActiveSoundTable
- dispatch_local_sound: 3D audio + play_sound_pooled + record
- load_and_play_streaming / load_and_play_streaming_positional

Removed: naked asm bridges (call_load_and_play_streaming,
call_load_and_play_streaming_positional, LOAD_AND_PLAY_STREAMING_ADDR).

Unify SoundId and SoundSlot into a single SoundId type β€” they were
the same packed u32 format (low 16 = slot index, bit 16 = loop,
bit 17 = raw volume). Flag accessors moved to SoundId.

Other changes:
- Add Fixed Div/Div<i32> operators
- Name DDGame+0x77B8 (level_width_sound), GameInfo+0xF38C
(sound_attenuation), DDGame listener_pos() accessor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/audio/active_sound.rs CHANGED
@@ -40,7 +40,7 @@ pub struct ActiveSoundEntry {
40
  /// 0x0C: Volume (fixed-point 16.16, 0x10000 = 1.0).
41
  pub volume: Fixed,
42
  /// 0x10: Running counter value at time of insertion (unique ID).
43
- pub sequence: u32,
44
  /// 0x14: DSSound channel handle (return from play_sound_pooled). 0 = free slot.
45
  pub channel_handle: u32,
46
  }
@@ -54,7 +54,7 @@ impl ActiveSoundTable {
54
  /// strips it before calling). The low 6 bits index into the entry table.
55
  ///
56
  /// Returns true if the sound was found and stopped.
57
- pub unsafe fn stop_sound(&mut self, handle: u32) -> bool {
58
  let slot = (handle & 0x3F) as usize;
59
  let entry = &mut self.entries[slot];
60
 
 
40
  /// 0x0C: Volume (fixed-point 16.16, 0x10000 = 1.0).
41
  pub volume: Fixed,
42
  /// 0x10: Running counter value at time of insertion (unique ID).
43
+ pub sequence: i32,
44
  /// 0x14: DSSound channel handle (return from play_sound_pooled). 0 = free slot.
45
  pub channel_handle: u32,
46
  }
 
54
  /// strips it before calling). The low 6 bits index into the entry table.
55
  ///
56
  /// Returns true if the sound was found and stopped.
57
+ pub unsafe fn stop_sound(&mut self, handle: i32) -> bool {
58
  let slot = (handle & 0x3F) as usize;
59
  let entry = &mut self.entries[slot];
60
 
crates/openwa-core/src/audio/dssound.rs CHANGED
@@ -2,41 +2,7 @@ use crate::fixed::Fixed;
2
 
3
  use windows::Win32::Media::Audio::DirectSound::IDirectSoundBuffer;
4
 
5
- /// Packed sound slot index + playback flags, passed as a single u32.
6
- ///
7
- /// Layout:
8
- /// - Bits 0-15: sound slot index (1-499)
9
- /// - Bit 16: loop flag (play in a loop)
10
- /// - Bit 17: raw volume flag (skip master volume scaling)
11
- #[derive(Clone, Copy)]
12
- #[repr(transparent)]
13
- pub struct SoundSlot(pub u32);
14
-
15
- impl SoundSlot {
16
- /// Sound slot index (0-499). 0 = invalid/empty.
17
- #[inline]
18
- pub fn index(self) -> usize {
19
- (self.0 & 0xFFFF) as usize
20
- }
21
-
22
- /// Whether the sound should loop continuously.
23
- #[inline]
24
- pub fn is_looping(self) -> bool {
25
- self.0 & 0x10000 != 0
26
- }
27
-
28
- /// Whether volume should be used directly (skip master volume scaling).
29
- #[inline]
30
- pub fn is_raw_volume(self) -> bool {
31
- self.0 & 0x20000 != 0
32
- }
33
-
34
- /// Return a copy with the loop flag cleared.
35
- #[inline]
36
- pub fn without_loop(self) -> Self {
37
- Self(self.0 & 0xFFFE_FFFF)
38
- }
39
- }
40
 
41
  /// Volume-to-dB attenuation table (64 entries of i16).
42
  /// Copied from WA.exe .rdata at 0x6A6A60.
@@ -69,10 +35,10 @@ type Ptr32 = u32;
69
  #[repr(C)]
70
  #[derive(Clone, Copy)]
71
  pub struct ChannelDescriptor {
72
- /// +0x00: The SoundSlot that was used to start this channel.
73
  /// Stored by core_play_sound; checked by set_master_volume/set_channel_volume
74
  /// for the raw-volume flag (bit 17).
75
- pub slot: SoundSlot,
76
  /// +0x04: Buffer pool index (0..63), or -1 if not pooled.
77
  /// Set by play_sound_pooled; checked by update_channels/stop_channel/allocate_channel.
78
  pub pool_idx: i32,
@@ -128,7 +94,7 @@ pub struct DSSoundVtable {
128
  /// play_sound β€” wrapper around core play. RET 0x14 = 5 stack params.
129
  pub play_sound: fn(
130
  this: *mut DSSound,
131
- slot: SoundSlot,
132
  priority: i32,
133
  volume: Fixed,
134
  pan: Fixed,
@@ -137,7 +103,7 @@ pub struct DSSoundVtable {
137
  /// play_sound_pooled β€” allocates from buffer pool, plays. RET 0x14.
138
  pub play_sound_pooled: fn(
139
  this: *mut DSSound,
140
- slot: SoundSlot,
141
  priority: i32,
142
  volume: Fixed,
143
  pan: Fixed,
@@ -508,7 +474,7 @@ pub unsafe extern "thiscall" fn destructor(this: *mut DSSound, flags: u8) -> *mu
508
  /// Returns channel descriptor index on success, -1 on failure.
509
  unsafe fn core_play_sound(
510
  snd: &mut DSSound,
511
- slot: SoundSlot,
512
  priority: i32,
513
  frequency: Fixed,
514
  volume: Fixed,
@@ -652,7 +618,7 @@ unsafe fn allocate_channel(snd: &mut DSSound, priority: i32) -> i32 {
652
  /// Params: slot (with flags), priority, frequency, volume, pan.
653
  pub unsafe extern "thiscall" fn play_sound(
654
  this: *mut DSSound,
655
- slot: SoundSlot,
656
  priority: i32,
657
  frequency: Fixed,
658
  volume: Fixed,
@@ -667,7 +633,7 @@ pub unsafe extern "thiscall" fn play_sound(
667
  /// Returns pool_id (1-based) on success, 0 on failure.
668
  pub unsafe extern "thiscall" fn play_sound_pooled(
669
  this: *mut DSSound,
670
- slot: SoundSlot,
671
  priority: i32,
672
  frequency: Fixed,
673
  volume: Fixed,
 
2
 
3
  use windows::Win32::Media::Audio::DirectSound::IDirectSoundBuffer;
4
 
5
+ use crate::audio::sound::SoundId;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  /// Volume-to-dB attenuation table (64 entries of i16).
8
  /// Copied from WA.exe .rdata at 0x6A6A60.
 
35
  #[repr(C)]
36
  #[derive(Clone, Copy)]
37
  pub struct ChannelDescriptor {
38
+ /// +0x00: The SoundId that was used to start this channel.
39
  /// Stored by core_play_sound; checked by set_master_volume/set_channel_volume
40
  /// for the raw-volume flag (bit 17).
41
+ pub slot: SoundId,
42
  /// +0x04: Buffer pool index (0..63), or -1 if not pooled.
43
  /// Set by play_sound_pooled; checked by update_channels/stop_channel/allocate_channel.
44
  pub pool_idx: i32,
 
94
  /// play_sound β€” wrapper around core play. RET 0x14 = 5 stack params.
95
  pub play_sound: fn(
96
  this: *mut DSSound,
97
+ slot: SoundId,
98
  priority: i32,
99
  volume: Fixed,
100
  pan: Fixed,
 
103
  /// play_sound_pooled β€” allocates from buffer pool, plays. RET 0x14.
104
  pub play_sound_pooled: fn(
105
  this: *mut DSSound,
106
+ slot: SoundId,
107
  priority: i32,
108
  volume: Fixed,
109
  pan: Fixed,
 
474
  /// Returns channel descriptor index on success, -1 on failure.
475
  unsafe fn core_play_sound(
476
  snd: &mut DSSound,
477
+ slot: SoundId,
478
  priority: i32,
479
  frequency: Fixed,
480
  volume: Fixed,
 
618
  /// Params: slot (with flags), priority, frequency, volume, pan.
619
  pub unsafe extern "thiscall" fn play_sound(
620
  this: *mut DSSound,
621
+ slot: SoundId,
622
  priority: i32,
623
  frequency: Fixed,
624
  volume: Fixed,
 
633
  /// Returns pool_id (1-based) on success, 0 on failure.
634
  pub unsafe extern "thiscall" fn play_sound_pooled(
635
  this: *mut DSSound,
636
+ slot: SoundId,
637
  priority: i32,
638
  frequency: Fixed,
639
  volume: Fixed,
crates/openwa-core/src/audio/mod.rs CHANGED
@@ -14,7 +14,7 @@ pub use dssound::{
14
  set_master_volume, set_pan, set_volume_params, stop_channel,
15
  sub_destructor as dssound_sub_destructor, update_channels,
16
  };
17
- pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable, SoundSlot};
18
  pub use music::Music;
19
  pub use sound::{KnownSoundId, SoundId};
20
  pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
 
14
  set_master_volume, set_pan, set_volume_params, stop_channel,
15
  sub_destructor as dssound_sub_destructor, update_channels,
16
  };
17
+ pub use dssound::{ChannelDescriptor, DSSound, DSSoundVtable};
18
  pub use music::Music;
19
  pub use sound::{KnownSoundId, SoundId};
20
  pub use speech::{SpeechLineId, SpeechLineTableEntry, SpeechSlotTable, SPEECH_LINE_TABLE_COUNT};
crates/openwa-core/src/audio/sound.rs CHANGED
@@ -1,10 +1,42 @@
1
- /// Opaque sound ID. WA uses a wider range than just the known SFX enum
2
- /// (e.g., speech/voice lines use IDs above 126). This newtype wraps the
3
- /// raw u32 value and can be constructed from [`KnownSoundId`] variants.
 
 
 
 
 
 
4
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5
  #[repr(transparent)]
6
  pub struct SoundId(pub u32);
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  impl From<KnownSoundId> for SoundId {
9
  #[inline]
10
  fn from(known: KnownSoundId) -> Self {
 
1
+ /// Packed sound ID + playback flags, passed as a single u32.
2
+ ///
3
+ /// Layout:
4
+ /// - Bits 0-15: sound slot index (1-499 for SFX, higher for speech)
5
+ /// - Bit 16: loop flag (play in a loop)
6
+ /// - Bit 17: raw volume flag (skip master volume scaling)
7
+ ///
8
+ /// Used throughout the sound system: queue insertion, DSSound playback,
9
+ /// and streaming dispatch all operate on this same packed format.
10
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11
  #[repr(transparent)]
12
  pub struct SoundId(pub u32);
13
 
14
+ impl SoundId {
15
+ /// Sound slot index (low 16 bits). 0 = invalid/empty.
16
+ #[inline]
17
+ pub fn index(self) -> usize {
18
+ (self.0 & 0xFFFF) as usize
19
+ }
20
+
21
+ /// Whether the sound should loop continuously (bit 16).
22
+ #[inline]
23
+ pub fn is_looping(self) -> bool {
24
+ self.0 & 0x10000 != 0
25
+ }
26
+
27
+ /// Whether volume should be used directly, skipping master volume scaling (bit 17).
28
+ #[inline]
29
+ pub fn is_raw_volume(self) -> bool {
30
+ self.0 & 0x20000 != 0
31
+ }
32
+
33
+ /// Return a copy with the loop flag cleared.
34
+ #[inline]
35
+ pub fn without_loop(self) -> Self {
36
+ Self(self.0 & 0xFFFE_FFFF)
37
+ }
38
+ }
39
+
40
  impl From<KnownSoundId> for SoundId {
41
  #[inline]
42
  fn from(known: KnownSoundId) -> Self {
crates/openwa-core/src/audio/sound_ops.rs CHANGED
@@ -13,9 +13,7 @@
13
  //! - DispatchGlobalSound (0x526270)
14
  //! - PlaySoundPooled_Direct (0x546B50)
15
 
16
- use core::sync::atomic::{AtomicU32, Ordering};
17
-
18
- use crate::audio::{play_sound, play_sound_pooled, SoundId, SoundSlot};
19
  use crate::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
20
  use crate::fixed::Fixed;
21
  use crate::task::worm::CTaskWorm;
@@ -141,9 +139,9 @@ pub unsafe fn play_worm_sound(worm: *mut CTaskWorm, sound_id: SoundId, volume: F
141
  }
142
  }
143
  }
144
- // Start new streaming sound via WA function (FUN_00546c20)
145
  // FUN_005150D0 hardcodes flags=3
146
- let new_handle = call_load_and_play_streaming(worm, sound_id.0, 3, volume.0 as u32);
147
  (*worm).sound_handle = new_handle;
148
  }
149
 
@@ -178,17 +176,17 @@ pub unsafe fn play_worm_sound_2(
178
  let worm_y = *((worm as *const u8).add(0x88) as *const i32);
179
  let new_handle = if worm_y < -0x270F_FFFF && sound_id.0 == 0x36 {
180
  // Special teleport case: play at weapon target position
181
- call_load_and_play_streaming_positional(
182
  worm,
183
- sound_id.0,
184
  flags,
185
- volume.0 as u32,
186
- (*worm).weapon_param_1 as u32,
187
- (*worm).weapon_param_2 as u32,
188
  )
189
  } else {
190
- // Normal case: play streaming sound
191
- call_load_and_play_streaming(worm, sound_id.0, flags, volume.0 as u32)
192
  };
193
 
194
  (*worm).sound_handle_2 = new_handle;
@@ -213,10 +211,10 @@ pub unsafe fn is_sound_suppressed(ddgame: *const DDGame) -> bool {
213
  /// Returns 0xFFFFFFFF if suppressed, 0 if no DSSound, otherwise the channel handle.
214
  pub unsafe fn dispatch_global_sound(
215
  ddgame_wrapper: *const DDGameWrapper,
216
- sound_id: u32,
217
- flags: u32,
218
- volume: u32,
219
- pitch: u32,
220
  ) -> u32 {
221
  let g = &*(*ddgame_wrapper).ddgame;
222
  let gi = &*g.game_info;
@@ -230,14 +228,7 @@ pub unsafe fn dispatch_global_sound(
230
  return 0;
231
  }
232
 
233
- play_sound(
234
- dssound,
235
- SoundSlot(sound_id),
236
- flags as i32,
237
- Fixed(volume as i32),
238
- Fixed(pitch as i32),
239
- Fixed(0),
240
- ) as u32
241
  }
242
 
243
  /// Direct pooled sound playback β€” port of PlaySoundPooled_Direct (0x546B50).
@@ -245,9 +236,9 @@ pub unsafe fn dispatch_global_sound(
245
  /// Bypasses queue, checks suppression + fast-forward, calls DSSound play_sound_pooled.
246
  pub unsafe fn play_sound_pooled_direct(
247
  task: *const CTask,
248
- param1: SoundSlot,
249
- param2: i32,
250
- param3: Fixed,
251
  ) -> i32 {
252
  let g = &*(*task).ddgame;
253
  let gi = &*g.game_info;
@@ -265,97 +256,309 @@ pub unsafe fn play_sound_pooled_direct(
265
  return 0;
266
  }
267
 
268
- play_sound_pooled(dssound, param1, param2, Fixed::ONE, param3, Fixed::ZERO)
269
  }
270
 
271
  // ============================================================
272
- // Streaming sound bridges (naked asm β€” WA usercall conventions)
273
  // ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- /// Rebased address for FUN_00546c20 (LoadAndPlayStreaming).
276
- /// Initialized by wormkit's sound::install().
277
- pub static LOAD_AND_PLAY_STREAMING_ADDR: AtomicU32 = AtomicU32::new(0);
278
 
279
- /// Bridge to FUN_00546c20 β€” load and play a streaming sound.
280
- /// usercall(EAX=worm, ESI=&worm.sound_emitter) + stdcall(sound_id, flags, volume), plain RET.
281
- /// Returns new sound handle (with bit 30 set for streaming, 0 on failure, -1 if suppressed).
282
- pub unsafe fn call_load_and_play_streaming(
 
 
 
 
283
  worm: *mut CTaskWorm,
284
- sound_id: u32,
285
  flags: u32,
286
- volume: u32,
287
- ) -> u32 {
288
- let addr = LOAD_AND_PLAY_STREAMING_ADDR.load(Ordering::Relaxed);
289
- call_load_and_play_streaming_bridge(worm, sound_id, flags, volume, addr)
290
- }
291
 
292
- #[unsafe(naked)]
293
- unsafe extern "C" fn call_load_and_play_streaming_bridge(
294
- _worm: *mut CTaskWorm,
295
- _sound_id: u32,
296
- _flags: u32,
297
- _volume: u32,
298
- _addr: u32,
299
- ) -> u32 {
300
- core::arch::naked_asm!(
301
- "push esi",
302
- "push ebx",
303
- // Stack: 2 saves(8) + ret(4) = 12 to first arg
304
- "mov eax, [esp+12]", // worm β†’ EAX
305
- "lea esi, [eax+0xE8]", // &worm.sound_emitter β†’ ESI
306
- "mov ebx, [esp+28]", // addr (12 + 4*4 = 28)
307
- "push [esp+24]", // volume (12+12=24)
308
- "push [esp+20]", // flags (12+8=20, shifted+4=24)
309
- "push [esp+24]", // sound_id (12+4=16, shifted+8=24)
310
- "call ebx",
311
- "pop ebx",
312
- "pop esi",
313
- "ret",
 
 
 
 
 
 
 
 
 
 
 
314
  );
 
 
 
 
315
  }
316
 
317
- /// Bridge to FUN_00546bb0 β€” load and play a positional streaming sound.
318
- /// usercall(EAX=worm, ESI=&worm.sound_emitter) + stdcall(sound_id, flags, volume, x, y).
319
- /// Returns handle | 0x40000000 on success, 0 on failure, -1 if suppressed.
320
- pub unsafe fn call_load_and_play_streaming_positional(
 
 
321
  worm: *mut CTaskWorm,
322
- sound_id: u32,
323
  flags: u32,
324
- volume: u32,
325
- x: u32,
326
- y: u32,
327
- ) -> u32 {
328
- let addr = crate::rebase::rb(0x546BB0u32);
329
- call_load_and_play_streaming_positional_bridge(worm, sound_id, flags, volume, x, y, addr)
330
- }
331
 
332
- #[unsafe(naked)]
333
- unsafe extern "C" fn call_load_and_play_streaming_positional_bridge(
334
- _worm: *mut CTaskWorm,
335
- _sound_id: u32,
336
- _flags: u32,
337
- _volume: u32,
338
- _x: u32,
339
- _y: u32,
340
- _addr: u32,
341
- ) -> u32 {
342
- core::arch::naked_asm!(
343
- "push esi",
344
- "push ebx",
345
- // Stack after saves: [ESP+12]=worm [+16]=sound_id [+20]=flags
346
- // [+24]=volume [+28]=x [+32]=y [+36]=addr
347
- "mov eax, [esp+12]", // worm β†’ EAX
348
- "lea esi, [eax+0xE8]", // &worm.sound_emitter β†’ ESI
349
- "mov ebx, [esp+36]", // addr
350
- // Push 5 stdcall params right-to-left: y, x, volume, flags, sound_id.
351
- "push [esp+32]", // y
352
- "push [esp+32]", // x (was +28, shifted +4)
353
- "push [esp+32]", // volume (was +24, shifted +8)
354
- "push [esp+32]", // flags (was +20, shifted +12)
355
- "push [esp+32]", // sound_id (was +16, shifted +16)
356
- "call ebx",
357
- "pop ebx",
358
- "pop esi",
359
- "ret",
360
  );
 
 
 
 
361
  }
 
13
  //! - DispatchGlobalSound (0x526270)
14
  //! - PlaySoundPooled_Direct (0x546B50)
15
 
16
+ use crate::audio::{play_sound, play_sound_pooled, SoundId};
 
 
17
  use crate::engine::{DDGame, DDGameWrapper, SoundQueueEntry};
18
  use crate::fixed::Fixed;
19
  use crate::task::worm::CTaskWorm;
 
139
  }
140
  }
141
  }
142
+ // Start new streaming sound β€” fully ported, no WA bridge needed
143
  // FUN_005150D0 hardcodes flags=3
144
+ let new_handle = load_and_play_streaming(worm, sound_id, 3, volume);
145
  (*worm).sound_handle = new_handle;
146
  }
147
 
 
176
  let worm_y = *((worm as *const u8).add(0x88) as *const i32);
177
  let new_handle = if worm_y < -0x270F_FFFF && sound_id.0 == 0x36 {
178
  // Special teleport case: play at weapon target position
179
+ load_and_play_streaming_positional(
180
  worm,
181
+ sound_id,
182
  flags,
183
+ volume,
184
+ Fixed((*worm).weapon_param_1 as i32),
185
+ Fixed((*worm).weapon_param_2 as i32),
186
  )
187
  } else {
188
+ // Normal case: play streaming sound β€” fully ported
189
+ load_and_play_streaming(worm, sound_id, flags, volume)
190
  };
191
 
192
  (*worm).sound_handle_2 = new_handle;
 
211
  /// Returns 0xFFFFFFFF if suppressed, 0 if no DSSound, otherwise the channel handle.
212
  pub unsafe fn dispatch_global_sound(
213
  ddgame_wrapper: *const DDGameWrapper,
214
+ slot: SoundId,
215
+ priority: i32,
216
+ frequency: Fixed,
217
+ volume: Fixed,
218
  ) -> u32 {
219
  let g = &*(*ddgame_wrapper).ddgame;
220
  let gi = &*g.game_info;
 
228
  return 0;
229
  }
230
 
231
+ play_sound(dssound, slot, priority, frequency, volume, Fixed::ZERO) as u32
 
 
 
 
 
 
 
232
  }
233
 
234
  /// Direct pooled sound playback β€” port of PlaySoundPooled_Direct (0x546B50).
 
236
  /// Bypasses queue, checks suppression + fast-forward, calls DSSound play_sound_pooled.
237
  pub unsafe fn play_sound_pooled_direct(
238
  task: *const CTask,
239
+ slot: SoundId,
240
+ priority: i32,
241
+ volume: Fixed,
242
  ) -> i32 {
243
  let g = &*(*task).ddgame;
244
  let gi = &*g.game_info;
 
256
  return 0;
257
  }
258
 
259
+ play_sound_pooled(dssound, slot, priority, Fixed::ONE, volume, Fixed::ZERO)
260
  }
261
 
262
  // ============================================================
263
+ // Local/streaming sound β€” full Rust port
264
  // ============================================================
265
+ //
266
+ // Replaces the former naked asm bridges to WA's LoadAndPlayStreaming (0x546C20)
267
+ // and LoadAndPlayStreamingPositional (0x546BB0) with pure Rust implementations
268
+ // of the entire call chain:
269
+ //
270
+ // load_and_play_streaming β†’ dispatch_local_sound
271
+ // β†’ compute_distance_params β†’ distance_3d_attenuation
272
+ // β†’ DSSound::play_sound_pooled (vtable, already Rust)
273
+ // β†’ ActiveSoundTable::record_sound
274
+
275
+ use crate::audio::active_sound::ActiveSoundTable;
276
+
277
+ /// 3D stereo distance attenuation β€” port of Distance3D_Attenuation (0x5430F0).
278
+ ///
279
+ /// Computes volume attenuation and stereo pan based on the distance between
280
+ /// a sound source and the listener, using an elliptical model.
281
+ ///
282
+ /// The function models two "ears" at Β±half_width from the listener's X position.
283
+ /// For each ear, it computes an inverse-distance attenuation from the sound source
284
+ /// using: `attenuation = scaledΒ² / (scaledΒ² + dyΒ² + dx_earΒ²)`, where `scaled` is
285
+ /// the characteristic distance (abs(half_width) * 0xDDB3 / 0x10000 β‰ˆ 0.866 * half_width).
286
+ ///
287
+ /// Returns `(volume, pan)` as Fixed values where:
288
+ /// - `volume` = max(left_atten, right_atten), clamped to Fixed::ONE
289
+ /// - `pan` = right_atten - left_atten (positive = sound is to the right)
290
+ ///
291
+ /// Convention: EAX = &listener_pos[x,y], stdcall(sound_x, sound_y, level_width_fixed,
292
+ /// attenuation_factor, out_volume, out_pan), RET 0x18.
293
+ fn distance_3d_attenuation(
294
+ listener_x: Fixed,
295
+ listener_y: Fixed,
296
+ sound_x: Fixed,
297
+ sound_y: Fixed,
298
+ level_width_fixed: Fixed,
299
+ attenuation_factor: i32,
300
+ ) -> (Fixed, Fixed) {
301
+ /// √3/2 β‰ˆ 0.866, stored as Fixed (0xDDB3 / 0x10000)
302
+ const SQRT3_HALF: Fixed = Fixed(0xDDB3);
303
+
304
+ // Step 1: half_width = level_width / attenuation_factor
305
+ // Uses 64-bit intermediate, clamped so abs() can't overflow i32
306
+ let wide = (level_width_fixed.0 as i64) * (Fixed::ONE.0 as i64);
307
+ let half_width_64 = wide / (attenuation_factor as i64);
308
+ let half_width = Fixed(if half_width_64 > 0x7FFFFFFF {
309
+ 0x7FFFFFFF
310
+ } else if half_width_64 < -0x7FFFFFFF {
311
+ // Clamp to -0x7FFFFFFF (not i32::MIN) so abs() in step 2 can't overflow
312
+ -0x7FFFFFFF
313
+ } else {
314
+ half_width_64 as i32
315
+ });
316
+
317
+ // Step 2: characteristic distance = abs(half_width) * √3/2
318
+ let scaled = half_width.abs() * SQRT3_HALF;
319
+
320
+ // Steps 3-5 use i64 intermediates to match the original's __allmul/__alldiv
321
+ // precision. The original keeps scaled_sq/dy_sq as full 64-bit values through
322
+ // the attenuation computation; truncating to i32 (Fixed) would lose precision.
323
+ let scaled_i = scaled.0 as i64;
324
+ let dy_i = (sound_y.0 - listener_y.0) as i64;
325
+ let dx = sound_x - listener_x;
326
+ let half = half_width / 2;
327
+
328
+ // squared terms: aΒ² / ONE (fixed-point squares, 64-bit)
329
+ let scaled_sq = (scaled_i * scaled_i) / Fixed::ONE.0 as i64;
330
+ let dy_sq = (dy_i * dy_i) / Fixed::ONE.0 as i64;
331
+
332
+ // Compute per-ear inverse-distance attenuation:
333
+ // atten = (scaled_sq * ONE) / (scaled_sq + dy_sq + dx_earΒ²)
334
+
335
+ // Step 4: Left ear β€” distance from sound to left ear = -(half + dx)
336
+ let left_dist = -(half + dx).0 as i64;
337
+ let left_dist_sq = (left_dist * left_dist) / Fixed::ONE.0 as i64;
338
+ let left_denom = scaled_sq + dy_sq + left_dist_sq;
339
+ let left_atten = if left_denom == 0 {
340
+ Fixed::ONE
341
+ } else {
342
+ Fixed(((scaled_sq * Fixed::ONE.0 as i64) / left_denom) as i32).min(Fixed::ONE)
343
+ };
344
+
345
+ // Step 5: Right ear β€” distance from sound to right ear = half - dx
346
+ let right_dist = (half - dx).0 as i64;
347
+ let right_dist_sq = (right_dist * right_dist) / Fixed::ONE.0 as i64;
348
+ let right_denom = right_dist_sq + dy_sq + scaled_sq;
349
+ let right_atten = if right_denom == 0 {
350
+ Fixed::ONE
351
+ } else {
352
+ Fixed(((scaled_sq * Fixed::ONE.0 as i64) / right_denom) as i32).min(Fixed::ONE)
353
+ };
354
+
355
+ // Step 6: volume = max(left, right), pan = right - left
356
+ (left_atten.max(right_atten), right_atten - left_atten)
357
+ }
358
+
359
+ /// Compute distance-based volume and pan for a local sound β€” port of
360
+ /// ComputeDistanceParams (0x546300).
361
+ ///
362
+ /// Reads the attenuation factor from GameInfo and the level width from DDGame.
363
+ /// If attenuation is disabled (factor == 0), returns full volume and center pan.
364
+ ///
365
+ /// Convention: fastcall(ECX=&out_pan, EDX=&out_volume, stack=[table, x, y]), RET 0xC.
366
+ unsafe fn compute_distance_params(ddgame: *const DDGame, x: Fixed, y: Fixed) -> (Fixed, Fixed) {
367
+ let gi = &*(*ddgame).game_info;
368
+ let attenuation = gi.sound_attenuation;
369
+
370
+ if attenuation == 0 {
371
+ // No 3D audio β€” full volume, center pan
372
+ return (Fixed::ONE, Fixed::ZERO);
373
+ }
374
+
375
+ let level_width_fixed = Fixed((*ddgame).level_width_sound << 16);
376
+ let (lx, ly) = (*ddgame).listener_pos();
377
+
378
+ distance_3d_attenuation(Fixed(lx), Fixed(ly), x, y, level_width_fixed, attenuation)
379
+ }
380
+
381
+ /// Record an active local sound in the tracking table β€” port of
382
+ /// RecordActiveSound (0x546260).
383
+ ///
384
+ /// Finds a free slot in the 64-entry ring buffer (probing by incrementing
385
+ /// counter until channel_handle == 0), then writes the entry.
386
+ ///
387
+ /// Convention: usercall(EAX=table, ESI=emitter) + stdcall(x, y, volume, channel_handle),
388
+ /// RET 0x10. Returns the counter value (used as handle with 0x40000000 bit).
389
+ unsafe fn record_active_sound(
390
+ table: *mut ActiveSoundTable,
391
+ emitter: *mut u8,
392
+ x: i32,
393
+ y: i32,
394
+ volume: i32,
395
+ channel_handle: i32,
396
+ ) -> i32 {
397
+ let t = &mut *table;
398
+
399
+ // Probe for a free slot (channel_handle == 0)
400
+ loop {
401
+ t.counter = t.counter.wrapping_add(1);
402
+ let slot = (t.counter & 0x3F) as usize;
403
+ if t.entries[slot].channel_handle == 0 {
404
+ break;
405
+ }
406
+ }
407
+
408
+ let slot = (t.counter & 0x3F) as usize;
409
+ let entry = &mut t.entries[slot];
410
+ entry.pos_x = Fixed(x);
411
+ entry.pos_y = Fixed(y);
412
+ entry.emitter = emitter;
413
+ entry.volume = Fixed(volume);
414
+ entry.sequence = t.counter as i32;
415
+ entry.channel_handle = channel_handle as u32;
416
+
417
+ // Increment emitter ref count (emitter+0x08)
418
+ if !emitter.is_null() {
419
+ let ref_count = emitter.add(8) as *mut i32;
420
+ *ref_count += 1;
421
+ }
422
+
423
+ t.counter as i32
424
+ }
425
+
426
+ /// Dispatch a local (positional) sound β€” port of DispatchLocalSound (0x546360).
427
+ ///
428
+ /// Clamps volume, computes 3D distance attenuation and pan, calls
429
+ /// DSSound::play_sound_pooled, then records the active sound.
430
+ ///
431
+ /// Convention: usercall(EAX=volume, EDI=active_sound_table) +
432
+ /// stdcall(sound_slot, flags, x, y, emitter), RET 0x14.
433
+ unsafe fn dispatch_local_sound(
434
+ table: *mut ActiveSoundTable,
435
+ volume: Fixed,
436
+ sound_slot: SoundId,
437
+ flags: i32,
438
+ pos: (Fixed, Fixed),
439
+ emitter: *mut u8,
440
+ ) -> i32 {
441
+ let volume = volume.min(Fixed::ONE);
442
+
443
+ let ddgame = (*table).ddgame;
444
+ let (volume_atten, pan) = compute_distance_params(ddgame, pos.0, pos.1);
445
+
446
+ let scaled_volume = volume * volume_atten;
447
+
448
+ // Call DSSound::play_sound_pooled (vtable slot 4)
449
+ let sound = (*ddgame).sound;
450
+ let sound_vt = &*(*sound).vtable;
451
+ let handle =
452
+ (sound_vt.play_sound_pooled)(sound, sound_slot, flags, Fixed::ONE, scaled_volume, pan);
453
+
454
+ if handle == 0 {
455
+ return 0;
456
+ }
457
 
458
+ record_active_sound(table, emitter, pos.0 .0, pos.1 .0, volume.0, handle)
459
+ }
 
460
 
461
+ /// Load and play a streaming sound β€” port of LoadAndPlayStreaming (0x546C20).
462
+ ///
463
+ /// Checks suppression conditions, gets emitter position via vtable call,
464
+ /// then dispatches as a local sound. Returns handle | 0x40000000 on success,
465
+ /// 0 on failure, -1 (0xFFFFFFFF) if suppressed.
466
+ ///
467
+ /// Replaces the former naked asm bridge `call_load_and_play_streaming`.
468
+ pub unsafe fn load_and_play_streaming(
469
  worm: *mut CTaskWorm,
470
+ sound_id: SoundId,
471
  flags: u32,
472
+ volume: Fixed,
473
+ ) -> i32 {
474
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
475
+ let gi = &*(*ddgame).game_info;
 
476
 
477
+ // Suppression checks (matching 0x546C20 exactly)
478
+ if gi.sound_mute != 0 || (*ddgame).frame_counter < gi.sound_start_frame {
479
+ return -1;
480
+ }
481
+ if (*ddgame).fast_forward_active != 0 {
482
+ return -1;
483
+ }
484
+
485
+ let sound = (*ddgame).sound;
486
+ if sound.is_null() {
487
+ return 0;
488
+ }
489
+
490
+ // Get emitter position via sound_emitter vtable[0] (GetPosition)
491
+ let task = worm as *mut CGameTask;
492
+ let emitter = &(*task).sound_emitter;
493
+ let mut pos_x: u32 = 0;
494
+ let mut pos_y: u32 = 0;
495
+ ((*emitter.vtable).get_position)(emitter, &mut pos_x, &mut pos_y);
496
+
497
+ // Dispatch as local sound
498
+ let table = (*ddgame).active_sounds;
499
+ if table.is_null() {
500
+ return 0;
501
+ }
502
+
503
+ let handle = dispatch_local_sound(
504
+ table,
505
+ volume,
506
+ sound_id,
507
+ flags as i32,
508
+ (Fixed(pos_x as i32), Fixed(pos_y as i32)),
509
+ emitter as *const _ as *mut u8,
510
  );
511
+ if handle == 0 {
512
+ return 0;
513
+ }
514
+ handle | 0x40000000
515
  }
516
 
517
+ /// Load and play a streaming sound at a specific position β€” port of
518
+ /// LoadAndPlayStreamingPositional (0x546BB0).
519
+ ///
520
+ /// Same as `load_and_play_streaming` but uses explicit (x, y) coordinates
521
+ /// instead of reading from the emitter. Used by teleport sound special case.
522
+ pub unsafe fn load_and_play_streaming_positional(
523
  worm: *mut CTaskWorm,
524
+ sound_id: SoundId,
525
  flags: u32,
526
+ volume: Fixed,
527
+ x: Fixed,
528
+ y: Fixed,
529
+ ) -> i32 {
530
+ let ddgame = CTask::ddgame_raw(worm as *const CTask);
531
+ let gi = &*(*ddgame).game_info;
 
532
 
533
+ // Suppression checks (matching 0x546BB0 exactly)
534
+ if gi.sound_mute != 0 || (*ddgame).frame_counter < gi.sound_start_frame {
535
+ return -1;
536
+ }
537
+ if (*ddgame).fast_forward_active != 0 {
538
+ return -1;
539
+ }
540
+
541
+ let sound = (*ddgame).sound;
542
+ if sound.is_null() {
543
+ return 0;
544
+ }
545
+
546
+ let table = (*ddgame).active_sounds;
547
+ if table.is_null() {
548
+ return 0;
549
+ }
550
+
551
+ // Note: positional variant passes 0 for emitter (no ref tracking)
552
+ let handle = dispatch_local_sound(
553
+ table,
554
+ volume,
555
+ sound_id,
556
+ flags as i32,
557
+ (x, y),
558
+ core::ptr::null_mut(),
 
 
559
  );
560
+ if handle == 0 {
561
+ return 0;
562
+ }
563
+ handle | 0x40000000
564
  }
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -285,8 +285,13 @@ pub struct DDGame {
285
  pub level_bound_min_y: i32,
286
  /// 0x77A8: Level bound max Y (Fixed-point).
287
  pub level_bound_max_y: i32,
288
- /// 0x77AC-0x77BF: Unknown
289
- pub _unknown_77ac: [u8; 0x77C0 - 0x77AC],
 
 
 
 
 
290
 
291
  /// 0x77C0: Level width in pixels (set by PCLandscape constructor).
292
  pub level_width: u32,
@@ -494,6 +499,14 @@ impl DDGame {
494
  self.hud_status_text = load_string(0x70F);
495
  }
496
  }
 
 
 
 
 
 
 
 
497
  }
498
 
499
  // DDGame constructor code (create_ddgame, init helpers, usercall bridges)
 
285
  pub level_bound_min_y: i32,
286
  /// 0x77A8: Level bound max Y (Fixed-point).
287
  pub level_bound_max_y: i32,
288
+ /// 0x77AC-0x77B7: Unknown
289
+ pub _unknown_77ac: [u8; 0x77B8 - 0x77AC],
290
+ /// 0x77B8: Level width for 3D sound distance computation (pixels, not fixed-point).
291
+ /// Read by ComputeDistanceParams, shifted left 16 before passing to Distance3D_Attenuation.
292
+ pub level_width_sound: i32,
293
+ /// 0x77BC-0x77BF: Unknown
294
+ pub _unknown_77bc: [u8; 0x77C0 - 0x77BC],
295
 
296
  /// 0x77C0: Level width in pixels (set by PCLandscape constructor).
297
  pub level_width: u32,
 
499
  self.hud_status_text = load_string(0x70F);
500
  }
501
  }
502
+
503
+ /// Listener position for 3D audio β€” stored at DDGame+0x8CEC (screen_coords[3]).
504
+ ///
505
+ /// Returns `(x, y)` as raw i32 values (fixed-point 16.16).
506
+ /// Used by ComputeDistanceParams / Distance3D_Attenuation.
507
+ pub fn listener_pos(&self) -> (i32, i32) {
508
+ (self.screen_coords[3].x, self.screen_coords[3].y)
509
+ }
510
  }
511
 
512
  // DDGame constructor code (create_ddgame, init helpers, usercall bridges)
crates/openwa-core/src/engine/game_info.rs CHANGED
@@ -144,8 +144,13 @@ pub struct GameInfo {
144
  /// 0xF374: Display flags passed to DDDisplay::Init.
145
  pub display_flags: u32,
146
 
147
- /// 0xF378-0xF39F: Unknown
148
- pub _unknown_f378: [u8; 0xF3A0 - 0xF378],
 
 
 
 
 
149
 
150
  // --- Cluster 2: game options (populated by LoadOptions) ---
151
  /// 0xF3A0: Unknown config byte (from global 0x7C0D38)
 
144
  /// 0xF374: Display flags passed to DDDisplay::Init.
145
  pub display_flags: u32,
146
 
147
+ /// 0xF378-0xF38B: Unknown
148
+ pub _unknown_f378: [u8; 0xF38C - 0xF378],
149
+ /// 0xF38C: Sound distance attenuation factor (i32). When nonzero, enables 3D
150
+ /// positional audio via Distance3D_Attenuation. Zero = all sounds at full volume.
151
+ pub sound_attenuation: i32,
152
+ /// 0xF390-0xF39F: Unknown
153
+ pub _unknown_f390: [u8; 0xF3A0 - 0xF390],
154
 
155
  // --- Cluster 2: game options (populated by LoadOptions) ---
156
  /// 0xF3A0: Unknown config byte (from global 0x7C0D38)
crates/openwa-core/src/fixed.rs CHANGED
@@ -1,4 +1,4 @@
1
- use core::ops::{Add, Mul, Neg, Sub};
2
  use std::ops::{AddAssign, SubAssign};
3
 
4
  /// 16.16 fixed-point number used throughout WA for coordinates and velocities.
@@ -104,6 +104,24 @@ impl Mul<i32> for Fixed {
104
  }
105
  }
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  impl Neg for Fixed {
108
  type Output = Self;
109
  #[inline]
 
1
+ use core::ops::{Add, Div, Mul, Neg, Sub};
2
  use std::ops::{AddAssign, SubAssign};
3
 
4
  /// 16.16 fixed-point number used throughout WA for coordinates and velocities.
 
104
  }
105
  }
106
 
107
+ impl Div for Fixed {
108
+ type Output = Self;
109
+ /// Fixed-point division: (self << 16) / rhs, using i64 intermediate.
110
+ #[inline]
111
+ fn div(self, rhs: Self) -> Self {
112
+ Self((((self.0 as i64) << Self::FRACTIONAL_BITS) / rhs.0 as i64) as i32)
113
+ }
114
+ }
115
+
116
+ /// Divide a Fixed value by a plain integer (no shift β€” just `raw / n`).
117
+ impl Div<i32> for Fixed {
118
+ type Output = Self;
119
+ #[inline]
120
+ fn div(self, rhs: i32) -> Self {
121
+ Self(self.0 / rhs)
122
+ }
123
+ }
124
+
125
  impl Neg for Fixed {
126
  type Output = Self;
127
  #[inline]
crates/openwa-core/src/task/worm.rs CHANGED
@@ -396,10 +396,10 @@ pub struct CTaskWorm {
396
  /// 0x3B0: Streaming sound handle. Nonzero when a worm sound effect
397
  /// (e.g., weapon charge-up) is actively playing. PlayWormSound stores the
398
  /// new handle here; StopWormSound clears it.
399
- pub sound_handle: u32,
400
  /// 0x3B4: Secondary sound handle, used by FUN_00515020 (teleport/weapon sounds).
401
  /// Same stop/play semantics as `sound_handle` but a separate channel.
402
- pub sound_handle_2: u32,
403
  /// 0x3B8–0x3DB: Unknown
404
  pub _unknown_3b8: [u8; 0x3DC - 0x3B8],
405
  /// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
 
396
  /// 0x3B0: Streaming sound handle. Nonzero when a worm sound effect
397
  /// (e.g., weapon charge-up) is actively playing. PlayWormSound stores the
398
  /// new handle here; StopWormSound clears it.
399
+ pub sound_handle: i32,
400
  /// 0x3B4: Secondary sound handle, used by FUN_00515020 (teleport/weapon sounds).
401
  /// Same stop/play semantics as `sound_handle` but a separate channel.
402
+ pub sound_handle_2: i32,
403
  /// 0x3B8–0x3DB: Unknown
404
  pub _unknown_3b8: [u8; 0x3DC - 0x3B8],
405
  /// 0x3DC: Facing direction. -1 = facing left, +1 = facing right.
crates/openwa-wormkit/src/replacements/sound.rs CHANGED
@@ -15,7 +15,7 @@ use std::sync::atomic::Ordering;
15
 
16
  use openwa_core::address::va;
17
  use openwa_core::audio::sound_ops;
18
- use openwa_core::audio::{KnownSoundId, SoundId, SoundSlot};
19
  use openwa_core::engine::{DDGame, DDGameWrapper};
20
  use openwa_core::fixed::Fixed;
21
  use openwa_core::task::worm::CTaskWorm;
@@ -31,23 +31,22 @@ static SOUND_LOG_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::Ato
31
 
32
  unsafe extern "thiscall" fn hook_play_sound_global(
33
  this: *const CGameTask,
34
- sound_id: u32,
35
  flags: u32,
36
  volume: Fixed,
37
  pitch: Fixed,
38
  ) -> u32 {
39
  if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
40
- let sound_name = KnownSoundId::try_from(sound_id)
41
  .map(|s| format!("{s:?}"))
42
  .unwrap_or_else(|v| format!("#{v}"));
43
  let _ = log_line(&format!(
44
- "[Sound] Global: task=0x{this:08X?} id={sound_id}({sound_name}) \
45
  p3={flags} p4={volume} p5={pitch}"
46
  ));
47
  }
48
 
49
- sound_ops::queue_sound((*this).base.ddgame, SoundId(sound_id), flags, volume, pitch).is_some()
50
- as u32
51
  }
52
 
53
  // ── PlaySoundLocal (0x4FDFE0): usercall(EAX=pitch, ECX=volume, EDI=task, stack) ──
@@ -59,20 +58,20 @@ unsafe extern "cdecl" fn play_sound_local_impl(
59
  pitch: Fixed,
60
  volume: Fixed,
61
  task: *mut CGameTask,
62
- sound_id: u32,
63
  flags: u32,
64
  ) -> u32 {
65
  if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
66
- let sound_name = KnownSoundId::try_from(sound_id)
67
  .map(|s| format!("{s:?}"))
68
  .unwrap_or_else(|v| format!("#{v}"));
69
  let _ = log_line(&format!(
70
  "[Sound] Local: pitch={pitch} volume={volume} task=0x{task:08X?} \
71
- id={sound_id}({sound_name}) flags={flags}"
72
  ));
73
  }
74
 
75
- sound_ops::play_sound_local(task, SoundId(sound_id), flags, volume, pitch) as u32
76
  }
77
 
78
  // ── WormPlaySound2 (0x515020): usercall(EDI=worm) + stdcall(sound_id, volume, flags) ──
@@ -93,11 +92,11 @@ unsafe extern "C" fn trampoline_worm_play_sound_2() {
93
 
94
  unsafe extern "cdecl" fn play_worm_sound_2_cdecl(
95
  worm: *mut CTaskWorm,
96
- sound_id: u32,
97
- volume: u32,
98
  flags: u32,
99
  ) {
100
- sound_ops::play_worm_sound_2(worm, SoundId(sound_id), Fixed(volume as i32), flags);
101
  }
102
 
103
  // ── IsSoundSuppressed (0x5261E0): thiscall(ECX=DDGame*) ──
@@ -111,12 +110,12 @@ unsafe extern "thiscall" fn hook_is_sound_suppressed(ddgame: *mut DDGame) -> u32
111
  unsafe extern "fastcall" fn hook_dispatch_global_sound(
112
  _ecx: u32,
113
  ddgame_wrapper: *const DDGameWrapper,
114
- sound_id: u32,
115
- flags: u32,
116
- volume: u32,
117
- pitch: u32,
118
  ) -> u32 {
119
- sound_ops::dispatch_global_sound(ddgame_wrapper, sound_id, flags, volume, pitch)
120
  }
121
 
122
  // ── PlaySoundPooled_Direct (0x546B50): fastcall(ECX=unused, EDX=task) + 3 stack ──
@@ -124,11 +123,11 @@ unsafe extern "fastcall" fn hook_dispatch_global_sound(
124
  unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
125
  _ecx: u32,
126
  task: *const CTask,
127
- param1: SoundSlot,
128
- param2: i32,
129
- param3: Fixed,
130
  ) -> i32 {
131
- sound_ops::play_sound_pooled_direct(task, param1, param2, param3)
132
  }
133
 
134
  // ── Hook installation ──
@@ -175,12 +174,6 @@ pub fn install() -> Result<(), String> {
175
  va::WORM_PLAY_SOUND_2,
176
  trampoline_worm_play_sound_2 as *const (),
177
  )?;
178
-
179
- // Initialize bridge address for streaming sound functions
180
- sound_ops::LOAD_AND_PLAY_STREAMING_ADDR.store(
181
- openwa_core::rebase::rb(va::LOAD_AND_PLAY_STREAMING),
182
- core::sync::atomic::Ordering::Relaxed,
183
- );
184
  }
185
 
186
  Ok(())
 
15
 
16
  use openwa_core::address::va;
17
  use openwa_core::audio::sound_ops;
18
+ use openwa_core::audio::{KnownSoundId, SoundId};
19
  use openwa_core::engine::{DDGame, DDGameWrapper};
20
  use openwa_core::fixed::Fixed;
21
  use openwa_core::task::worm::CTaskWorm;
 
31
 
32
  unsafe extern "thiscall" fn hook_play_sound_global(
33
  this: *const CGameTask,
34
+ sound_id: SoundId,
35
  flags: u32,
36
  volume: Fixed,
37
  pitch: Fixed,
38
  ) -> u32 {
39
  if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
40
+ let sound_name = KnownSoundId::try_from(sound_id.0)
41
  .map(|s| format!("{s:?}"))
42
  .unwrap_or_else(|v| format!("#{v}"));
43
  let _ = log_line(&format!(
44
+ "[Sound] Global: task=0x{this:08X?} id={sound_id:?}({sound_name}) \
45
  p3={flags} p4={volume} p5={pitch}"
46
  ));
47
  }
48
 
49
+ sound_ops::queue_sound((*this).base.ddgame, sound_id, flags, volume, pitch).is_some() as u32
 
50
  }
51
 
52
  // ── PlaySoundLocal (0x4FDFE0): usercall(EAX=pitch, ECX=volume, EDI=task, stack) ──
 
58
  pitch: Fixed,
59
  volume: Fixed,
60
  task: *mut CGameTask,
61
+ sound_id: SoundId,
62
  flags: u32,
63
  ) -> u32 {
64
  if SOUND_LOG_ENABLED.load(Ordering::Relaxed) {
65
+ let sound_name = KnownSoundId::try_from(sound_id.0)
66
  .map(|s| format!("{s:?}"))
67
  .unwrap_or_else(|v| format!("#{v}"));
68
  let _ = log_line(&format!(
69
  "[Sound] Local: pitch={pitch} volume={volume} task=0x{task:08X?} \
70
+ id={sound_id:?}({sound_name}) flags={flags}"
71
  ));
72
  }
73
 
74
+ sound_ops::play_sound_local(task, sound_id, flags, volume, pitch) as u32
75
  }
76
 
77
  // ── WormPlaySound2 (0x515020): usercall(EDI=worm) + stdcall(sound_id, volume, flags) ──
 
92
 
93
  unsafe extern "cdecl" fn play_worm_sound_2_cdecl(
94
  worm: *mut CTaskWorm,
95
+ sound_id: SoundId,
96
+ volume: Fixed,
97
  flags: u32,
98
  ) {
99
+ sound_ops::play_worm_sound_2(worm, sound_id, volume, flags);
100
  }
101
 
102
  // ── IsSoundSuppressed (0x5261E0): thiscall(ECX=DDGame*) ──
 
110
  unsafe extern "fastcall" fn hook_dispatch_global_sound(
111
  _ecx: u32,
112
  ddgame_wrapper: *const DDGameWrapper,
113
+ slot: SoundId,
114
+ priority: i32,
115
+ frequency: Fixed,
116
+ volume: Fixed,
117
  ) -> u32 {
118
+ sound_ops::dispatch_global_sound(ddgame_wrapper, slot, priority, frequency, volume)
119
  }
120
 
121
  // ── PlaySoundPooled_Direct (0x546B50): fastcall(ECX=unused, EDX=task) + 3 stack ──
 
123
  unsafe extern "fastcall" fn hook_play_sound_pooled_direct(
124
  _ecx: u32,
125
  task: *const CTask,
126
+ slot: SoundId,
127
+ priority: i32,
128
+ volume: Fixed,
129
  ) -> i32 {
130
+ sound_ops::play_sound_pooled_direct(task, slot, priority, volume)
131
  }
132
 
133
  // ── Hook installation ──
 
174
  va::WORM_PLAY_SOUND_2,
175
  trampoline_worm_play_sound_2 as *const (),
176
  )?;
 
 
 
 
 
 
177
  }
178
 
179
  Ok(())