🔍 Module Scanned
modules/engine-audio/src/backends/sdl_audio.zig (automated audit scan)
📝 Summary
The Mixer.mix() function assumes all SoundData buffers contain mono S16 audio data, but SoundData stores a format: AudioFormat field and channels: u8 field that are never checked. Stereo sounds are mixed as mono (reading only left channel, writing to both outputs), and float32/unsigned8 sounds are incorrectly interpreted as S16.
📍 Location
File: modules/engine-audio/src/backends/sdl_audio.zig
Function/Scope: Mixer.mix() lines 249-296
🔴 Severity: High
High: Incorrect audio rendering (wrong stereo balance, volume, or garbled audio)
💥 Impact
If a sound with format .float32 or .unsigned8 is loaded:
The mixer treats the raw bytes as S16, interpreting float bits as sample values.
Result is extremely loud, distorted, or silent audio.
For stereo sounds with channels: 2:
The mixer reads only the left channel samples.
Each mono sample is written to BOTH left and right output channels.
Stereo imaging is destroyed.
🔎 Evidence
SoundData stores format and channel info but mixer ignores them:
pub const SoundData = struct {
buffer: []u8,
frequency: u32,
channels: u8, // Stored but never checked
format: AudioFormat, // Stored but never checked
length_samples: u32,
};
Mixer.mix() blindly assumes S16 mono:
// TODO: Handle other formats
const lo = u8_buf[valid_pos_idx * 2];
const hi = u8_buf[valid_pos_idx * 2 + 1];
const sample: i16 = std.mem.readInt(i16, &[2]u8{ lo, hi }, .little);
// Writes same mono sample to BOTH stereo channels
mix_buf[i * 2] += ...sample... * voice.effective_volume_l;
mix_buf[i * 2 + 1] += ...sample... * voice.effective_volume_r;
🛠️ Proposed Fix
- Add format validation in Mixer.play() to reject non-mono-S16 sounds
- OR implement proper format conversion in mix() for float32/unsigned8
- For stereo sounds, read interleaved L/R pairs separately
✅ Acceptance Criteria
SoundManager.createTestSound() continues to work (mono S16)
Stereo sounds play with correct left/right channel separation
Float32 audio is properly converted to S16 for mixing
zig build test passes
📚 References
Related issue: #683 (Mixer pointer lifetime)
Related issue: #718 (Cursor reset bounds)
🔍 Module Scanned
modules/engine-audio/src/backends/sdl_audio.zig (automated audit scan)
📝 Summary
The Mixer.mix() function assumes all SoundData buffers contain mono S16 audio data, but SoundData stores a format: AudioFormat field and channels: u8 field that are never checked. Stereo sounds are mixed as mono (reading only left channel, writing to both outputs), and float32/unsigned8 sounds are incorrectly interpreted as S16.
📍 Location
File: modules/engine-audio/src/backends/sdl_audio.zig
Function/Scope: Mixer.mix() lines 249-296
🔴 Severity: High
High: Incorrect audio rendering (wrong stereo balance, volume, or garbled audio)
💥 Impact
If a sound with format .float32 or .unsigned8 is loaded:
The mixer treats the raw bytes as S16, interpreting float bits as sample values.
Result is extremely loud, distorted, or silent audio.
For stereo sounds with channels: 2:
The mixer reads only the left channel samples.
Each mono sample is written to BOTH left and right output channels.
Stereo imaging is destroyed.
🔎 Evidence
SoundData stores format and channel info but mixer ignores them:
pub const SoundData = struct {
buffer: []u8,
frequency: u32,
channels: u8, // Stored but never checked
format: AudioFormat, // Stored but never checked
length_samples: u32,
};
Mixer.mix() blindly assumes S16 mono:
// TODO: Handle other formats
const lo = u8_buf[valid_pos_idx * 2];
const hi = u8_buf[valid_pos_idx * 2 + 1];
const sample: i16 = std.mem.readInt(i16, &[2]u8{ lo, hi }, .little);
// Writes same mono sample to BOTH stereo channels
mix_buf[i * 2] += ...sample... * voice.effective_volume_l;
mix_buf[i * 2 + 1] += ...sample... * voice.effective_volume_r;
🛠️ Proposed Fix
✅ Acceptance Criteria
SoundManager.createTestSound() continues to work (mono S16)
Stereo sounds play with correct left/right channel separation
Float32 audio is properly converted to S16 for mixing
zig build test passes
📚 References
Related issue: #683 (Mixer pointer lifetime)
Related issue: #718 (Cursor reset bounds)