C# GameBoy模拟器NAudio音频爆音崩溃问题排查求助
GameBoy模拟器噪音与NAudio缓冲溢出问题
我基于ConsoleNexusEngine开发GameBoy模拟器,启动任意ROM时会出现强烈干扰噪音,随后模拟器崩溃,抛出NAudio的InvalidOperationException: "Buffer full"异常。无法确定是时序问题、NAudio问题还是SPU模拟错误,核心代码如下:
主循环Update方法
public sealed class GameBoyEmulator : NexusConsoleGame { private readonly Processor _cpu; private readonly SoundProcessor _spu; private readonly MemoryManagement _mmu; private readonly PixelProcessor _ppu; private readonly Timer _timer; private readonly Joypad _joypad; private double accumulatedTime; private int cpuCycles; private int cyclesThisUpdate; public GameBoyEmulator(string rom) { _spu = new SoundProcessor(); _mmu = MemoryManagement.LoadGamePak(rom, _spu); _cpu = new Processor(_mmu); _ppu = new PixelProcessor(Graphic, _mmu); _timer = new Timer(_mmu); _joypad = new Joypad(_mmu); } protected override void Load() { Settings.ColorPalette = new GameBoyColorPalette(); Settings.Font = new NexusFont("Consolas", new NexusSize(8)); Settings.Title = "NexusGB"; Settings.StopGameKey = NexusKey.Escape; } protected override void Update() { accumulatedTime += DeltaTime * 1_000_000_000; while (accumulatedTime >= 16740000) { accumulatedTime -= 16740000; Input.UpdateGamepads(); Input.Update(); _joypad.HandleInputs(Input.Gamepad1, Input.Keys); while (cyclesThisUpdate < GameBoySystem.CyclesPerUpdate) { cpuCycles = _cpu.Execute(); cyclesThisUpdate += cpuCycles; _timer.Update(cpuCycles); _ppu.Update(cpuCycles); _spu.Update(cpuCycles); _joypad.Update(); HandleInterrupts(); } cyclesThisUpdate -= GameBoySystem.CyclesPerUpdate; } } protected override void OnCrash(Exception exception) => Utility.ShowAlert("Error", $"An error occured:\n{exception}", NexusAlertIcon.Error); protected override void CleanUp() { } private void HandleInterrupts() { var interruptEnable = _mmu.InterruptEnable; var interruptFlag = _mmu.InterruptFlag; for (int i = 0; i < 5; i++) { if ((((interruptEnable & interruptFlag) >> i) & 0x01) == 1) { _cpu.ExecuteInterrupt(i); } } _cpu.UpdateIme(); } }
SoundProcessor类
public sealed class SoundProcessor { private readonly ImmutableArray<BaseSoundChannel> _channels; private readonly WaveSoundChannel _wave; private byte number50; private byte number51; private byte number52; private byte Sound1Volume => (byte)(number50 & 0x7); public SoundProcessor() { _channels = [ new SquareSweepChannel(this), new SquareChannel(this), _wave = new WaveSoundChannel(this), new NoiseChannel(this) ]; _channels[0].WriteNumber(0, 0x80); _channels[0].WriteNumber(1, 0xBF); _channels[0].WriteNumber(2, 0xF3); _channels[0].WriteNumber(4, 0xBF); _channels[1].WriteNumber(1, 0x3F); _channels[1].WriteNumber(2, 0x00); _channels[1].WriteNumber(4, 0xBF); _channels[2].WriteNumber(0, 0x7F); _channels[2].WriteNumber(1, 0xFF); _channels[2].WriteNumber(2, 0x9F); _channels[2].WriteNumber(3, 0xBF); _channels[3].WriteNumber(1, 0xFF); _channels[3].WriteNumber(2, 0x00); _channels[3].WriteNumber(3, 0x00); _channels[3].WriteNumber(4, 0xBF); number50 = 0x77; number51 = 0xF3; number52 = 0xF1; } public void Update(in int cycles) { if ((number52 & (1 << 7)) == 0) return; foreach (var channel in _channels) { channel.Update(cycles); } } public byte ReadByte(in ushort address) { switch (address) { case 0xFF24: return number50; case 0xFF25: return number51; case 0xFF26: return number52; case >= 0xFF27 and < 0xFF30: return 0x00; case >= 0xFF30 and < 0xFF40: return _wave.ReadRam((ushort)(address - 0xFF30)); } var relativeAddress = address - 0xFF10; return _channels[relativeAddress / 5].ReadNumber(relativeAddress % 5); } public void WriteByte(in ushort address, in byte value) { switch (address) { case 0xFF24: number50 = value; return; case 0xFF25: number51 = value; return; case 0xFF26: number52 = value; return; case >= 0xFF27 and < 0xFF30: return; case >= 0xFF30 and < 0xFF40: _wave.WriteRam((ushort)(address - 0xFF30), value); return; } var relativeAddress = address - 0xFF10; _channels[relativeAddress / 5].WriteNumber(relativeAddress % 5, value); } public void WriteToSoundBuffer(in int channel, in Span<float> totalBuffer, in int index, float sample) { sample *= Sound1Volume / 7f; if ((number51 & (1 << (channel - 1))) != 0) totalBuffer[index + 1] = sample; if ((number51 & (1 << (channel + 3))) != 0) totalBuffer[index] = sample; } }
BaseSoundChannel抽象类
public abstract class BaseSoundChannel { private readonly byte[] _numbers; protected readonly int _channelNumber; protected readonly SoundProcessor _spu; protected readonly WindowsSoundOut _out; private float volume; public ref float ChannelVolume => ref volume; protected BaseSoundChannel(SoundProcessor spu, in int channelNumber) { _numbers = new byte[5]; _channelNumber = channelNumber; _spu = spu; _out = new WindowsSoundOut(); ChannelVolume = 0.05f; } public abstract void Update(in int cycles); public virtual byte ReadNumber(in int index) => _numbers[index]; public virtual void WriteNumber(in int index, in byte value) => _numbers[index] = value; }
WindowsSoundOut类
public sealed class WindowsSoundOut { private readonly DirectSoundOut soundOut; private readonly BufferedWaveProvider _provider; private readonly byte[] _addSampleData; public int SampleRate => _provider.WaveFormat.SampleRate; public WindowsSoundOut() { _provider = new BufferedWaveProvider(new WaveFormat(44100, 16, 2)); soundOut = new DirectSoundOut(100); soundOut.Init(_provider); soundOut.Play(); _addSampleData = new byte[_provider.BufferLength]; } public void BufferSoundSamples(in Span<float> sampleData, in int length) { var index = 0; foreach (var current in MemoryMarshal.Cast<float, byte>(sampleData)) { _addSampleData[index++] = current; } _provider.AddSamples(_addSampleData, 0, length + sizeof(float)); //异常触发位置 } }
问题根源分析
- 多音频输出实例冲突:每个
BaseSoundChannel都初始化独立的WindowsSoundOut实例,4个通道抢占音频设备,既产生噪音,又导致多实例同时写缓冲,极易溢出。 - 缓冲写入逻辑错误:
BufferSoundSamples中长度计算错误(length + sizeof(float)),且float转byte的直接强制转换与BufferedWaveProvider的16位整数格式不匹配,导致数据混乱。 - SPU时序与采样不同步:未根据GameBoy的CPU时钟(4194304Hz)和音频采样率(44100Hz)计算采样生成频率,导致音频数据生成速度远快于播放速度,填满缓冲。
- 无缓冲溢出防护:写入缓冲前未检查剩余空间,直接写入导致
Buffer full异常。
修复方案
1. 统一音频输出实例
所有通道共用一个WindowsSoundOut实例,避免多实例冲突:
// 修改BaseSoundChannel,移除内置_out public abstract class BaseSoundChannel { private readonly byte[] _numbers; protected readonly int _channelNumber; protected readonly SoundProcessor _spu; private float volume; public ref float ChannelVolume => ref volume; protected BaseSoundChannel(SoundProcessor spu, in int channelNumber) { _numbers = new byte[5]; _channelNumber = channelNumber; _spu = spu; ChannelVolume = 0.05f; } public abstract void Update(in int cycles); public abstract float GetCurrentSample(); // 新增采样生成方法 // ... 其他代码不变 } // 在SoundProcessor中创建统一输出实例 public sealed class SoundProcessor { private readonly ImmutableArray<BaseSoundChannel> _channels; private readonly WaveSoundChannel _wave; private readonly WindowsSoundOut _soundOut; public SoundProcessor() { _soundOut = new WindowsSoundOut(); _channels = [ new SquareSweepChannel(this), new SquareChannel(this), _wave = new WaveSoundChannel(this), new NoiseChannel(this) ]; // ... 初始化代码不变 } public WindowsSoundOut SoundOut => _soundOut; }
2. 修复缓冲写入与格式转换
适配16位整数格式,添加缓冲溢出检查:
public sealed class WindowsSoundOut { private readonly DirectSoundOut soundOut; private readonly BufferedWaveProvider _provider; private readonly short[] _sampleBuffer; public int SampleRate => _provider.WaveFormat.SampleRate; public WindowsSoundOut() { var waveFormat = new WaveFormat(44100, 16, 2); _provider = new BufferedWaveProvider(waveFormat); _provider.BufferDuration = TimeSpan.FromMilliseconds(100); soundOut = new DirectSoundOut(100); soundOut.Init(_provider); soundOut.Play(); _sampleBuffer = new short[_provider.BufferLength / sizeof(short)]; } public void BufferSoundSamples(in Span<float> sampleData, in int length) { // 将float转换为16位整数(范围-32768~32767) for (int i = 0; i < length; i++) { float sample = Math.Clamp(sampleData[i], -1f, 1f); _sampleBuffer[i] = (short)(sample * short.MaxValue); } int byteCount = length * sizeof(short); byte[] byteBuffer = new byte[byteCount]; Buffer.BlockCopy(_sampleBuffer, 0, byteBuffer, 0, byteCount); // 检查缓冲剩余空间,避免溢出 if (_provider.BufferedDuration < _provider.BufferDuration) { _provider.AddSamples(byteBuffer, 0, byteCount); } } }
3. 同步SPU时序与音频采样
根据CPU时钟和采样率计算采样生成时机:
public sealed class SoundProcessor { private double _audioCycleAccumulator; private const double CpuClock = 4194304.0; private const double AudioSampleRate = 44100.0; private const double CyclesPerSample = CpuClock / AudioSampleRate; public void Update(in int cycles) { if ((number52 & (1 << 7)) == 0) return; _audioCycleAccumulator += cycles; // 累积足够周期时生成采样 while (_audioCycleAccumulator >= CyclesPerSample) { _audioCycleAccumulator -= CyclesPerSample; GenerateMixedSample(); } foreach (var channel in _channels) { channel.Update(cycles); } } private void GenerateMixedSample() { float left = 0, right = 0; for (int i = 0; i < _channels.Length; i++) { float sample = _channels[i].GetCurrentSample() * _channels[i].ChannelVolume; // 根据声道控制位分配左右声道 if ((number51 & (1 << (i + 3))) != 0) left += sample; if ((number51 & (1 << i)) != 0) right += sample; } // 限制音量避免削波 left = Math.Clamp(left, -1f, 1f); right = Math.Clamp(right, -1f, 1f); Span<float> sampleData = stackalloc float[] { left, right }; _soundOut.BufferSoundSamples(sampleData, 2); } }
4. 实现通道采样生成
以SquareChannel为例,实现采样生成逻辑:
public class SquareChannel : BaseSoundChannel { private int _timer; private bool _outputState; public SquareChannel(SoundProcessor spu) : base(spu, 1) { } public override float GetCurrentSample() { return _outputState ? ChannelVolume : -ChannelVolume; } public override void Update(in int cycles) { _timer -= cycles; if (_timer <= 0) { _outputState = !_outputState; // 根据通道频率寄存器计算定时器值 ushort frequency = (ushort)((ReadNumber(3) << 8) | ReadNumber(2)); _timer = (int)((2048 - frequency) * 4); } } }
内容的提问来源于stack exchange,提问作者Jule
相关产品推荐
相关产品推荐

