7.4 KiB
MusicEditor 1.5x Audio Speed Bug - Handoff Document
Date: 2025-12-05 Status: Unresolved Priority: High
Problem Statement
The MusicEditor plays audio at approximately 1.5x speed. The exact ratio (48000/32040 = 1.498) indicates that samples generated at 32040 Hz are being played at 48000 Hz without proper resampling.
Additionally, there's a "first play" issue where clicking Play produces no audio the first time, but stopping and playing again works (at 1.5x speed).
Audio Pipeline Overview
MusicPlayer::Update() [called at ~60 Hz]
│
▼
Emulator::RunAudioFrame()
│
├─► Snes::RunAudioFrame()
│ │
│ ├─► cpu_.RunOpcode() loop until vblank
│ │ └─► RunCycle() → CatchUpApu() → apu_.RunCycles()
│ │ └─► DSP generates ~533 samples at 32040 Hz
│ │
│ └─► [At vblank] dsp.NewFrame() sets lastFrameBoundary
│
└─► snes_.SetSamples() → dsp.GetSamples()
│
└─► Reads ~533 samples from DSP ring buffer
│
▼
audio_backend->QueueSamplesNative(samples, 533, 2, 32040)
│
├─► SDL_AudioStreamPut(samples) at 32040 Hz
│
└─► SDL_AudioStreamGet(resampled) → SDL_QueueAudio()
└─► Output at 48000 Hz (resampled by SDL)
What Has Been Verified Working
1. APU Timing (VERIFIED CORRECT)
- APU runs at ~1,024,000 Hz (tests pass)
- DSP generates samples at ~32040 Hz (tests pass)
- ~533 samples generated per NTSC frame
2. SDL_AudioStream Resampling (VERIFIED CORRECT)
Diagnostic logs confirm correct resampling ratio:
QueueSamplesNative: In=2132 bytes (32040Hz) → Out=3192 bytes (48000Hz)
Resampling ratio: 1.497 (expected: 1.498)
3. Audio Backend Configuration (VERIFIED CORRECT)
- SDL audio device opens at 48000 Hz
- SDL_AudioStream created: 32040 Hz stereo → 48000 Hz stereo
audio_stream_enabled_ = trueconfirmed in logs
4. Shared Audio Backend (IMPLEMENTED)
- MusicPlayer's
audio_emulator_now uses external backend from main emulator Emulator::RunAudioFrame()usesaudio_backend()accessor (not direct member)- Single SDL device shared between main emulator and MusicPlayer
What Has Been Tried and Ruled Out
1. Duplicate NewFrame() Calls - REMOVED
Preview methods had explicit dsp.NewFrame() calls that conflicted with the internal call in RunAudioFrame(). These were removed but didn't fix the issue.
2. Audio Backend Member vs Accessor - FIXED
Emulator::RunAudioFrame() was using audio_backend_ directly instead of audio_backend() accessor. When external backend was set, audio_backend_ was null, so no audio was queued. Fixed to use accessor.
3. Two SDL Audio Devices - FIXED
Main emulator and MusicPlayer were creating separate SDL audio devices. Implemented SetExternalAudioBackend() to share a single device. Verified in logs that same device ID is used.
4. Initialization Order - VERIFIED CORRECT
SetSharedAudioBackend()called inMusicEditor::Initialize()EnsureAudioReady()sets external backend beforeEnsureInitialized()- Resampling configured before playback starts
5. First Play Silence - PARTIALLY UNDERSTOOD
Logs show the device is already "playing" with stale audio from main emulator when MusicPlayer starts. The exclusivity callback sets running=false on main emulator, but this may not immediately stop audio generation.
Current Code State
Key Files Modified
src/app/emu/emulator.h- AddedSetExternalAudioBackend(),audio_backend()accessorsrc/app/emu/emulator.cc-RunAudioFrame()andResetFrameTiming()use accessorsrc/app/editor/music/music_player.h- AddedSetSharedAudioBackend()src/app/editor/music/music_player.cc- Uses shared backend, removed duplicate NewFrame() callssrc/app/editor/music/music_editor.cc- Shares main emulator's backend with MusicPlayersrc/app/emu/audio/audio_backend.cc- Added diagnostic logging
Diagnostic Logging Added
QueueSamplesNative()logs input/output byte counts and resampling ratioGetStatus()logs device ID and queue stateClear()logs device ID and queue before/afterPlay()logs device status transitionsRunAudioFrame()logs which backend is being used (external vs owned)
Remaining Hypotheses
1. SDL_AudioStream Not Actually Being Used
Theory: Despite logs showing resampling, audio might be taking a different path. Investigation: Add logging at every audio queue call site to trace actual execution path.
2. Frame Timing Issue
Theory: MusicPlayer::Update() might not be called at the expected rate, or RunAudioFrame() might be called multiple times per frame.
Investigation: Add frame timing logs to verify Update() is called at ~60 Hz and RunAudioFrame() once per call.
3. DSP Sample Extraction Bug
Theory: dsp.GetSamples() might return wrong number of samples or from wrong position.
Investigation: Log actual sample counts returned by GetSamples() vs expected (~533).
4. Main Emulator Still Generating Audio
Theory: Even with running=false, main emulator's Update() might still be called and generating audio.
Investigation: Add logging to main emulator's audio generation path to verify it stops when MusicPlayer is active.
5. Audio Stream Bypass Path
Theory: There might be a code path that calls QueueSamples() (direct, non-resampled) instead of QueueSamplesNative().
Investigation: Search for all QueueSamples calls and verify none are being hit during music playback.
6. Resampling Disabled Mid-Playback
Theory: audio_stream_config_dirty_ or another flag might disable resampling during playback.
Investigation: Add logging to SetAudioStreamResampling() to catch any disable calls.
Suggested Next Steps
- Add comprehensive tracing to follow a single frame of audio from DSP generation through to SDL queue
- Verify frame timing - confirm Update() runs at expected rate
- Check for bypass paths - ensure all audio goes through QueueSamplesNative()
- Monitor resampling state - ensure it stays enabled throughout playback
- Test with simpler case - generate known test tone and verify output rate
Test Commands
# Build with debug
cmake --preset mac-dbg && cmake --build build --target yaze -j8
# Run with logging visible
./build/bin/Debug/yaze.app/Contents/MacOS/yaze 2>&1 | grep -E "(AudioBackend|MusicPlayer|Emulator)"
# Run audio timing tests
ctest --test-dir build -R "apu_timing|dsp_sample" -V
Key Constants
| Value | Meaning |
|---|---|
| 32040 Hz | Native SNES DSP sample rate |
| 48000 Hz | SDL audio device sample rate |
| 1.498 | Correct resampling ratio (48000/32040) |
| 533 | Samples per NTSC frame at 32040 Hz |
| ~60.0988 Hz | NTSC frame rate |
| 1,024,000 Hz | APU clock rate |
Files to Investigate
| File | Relevance |
|---|---|
src/app/editor/music/music_player.cc |
Main playback logic, Update() loop |
src/app/emu/emulator.cc |
RunAudioFrame(), audio queuing |
src/app/emu/audio/audio_backend.cc |
SDL audio, resampling |
src/app/emu/audio/dsp.cc |
Sample generation, GetSamples() |
src/app/emu/snes.cc |
RunAudioFrame(), SetSamples() |
Contact
Previous investigation done by Claude Code agents. See git history for detailed changes.