You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)); //异常触发位置
    }
}

问题根源分析

  1. 多音频输出实例冲突:每个BaseSoundChannel都初始化独立的WindowsSoundOut实例,4个通道抢占音频设备,既产生噪音,又导致多实例同时写缓冲,极易溢出。
  2. 缓冲写入逻辑错误:BufferSoundSamples中长度计算错误(length + sizeof(float)),且float转byte的直接强制转换与BufferedWaveProvider的16位整数格式不匹配,导致数据混乱。
  3. SPU时序与采样不同步:未根据GameBoy的CPU时钟(4194304Hz)和音频采样率(44100Hz)计算采样生成频率,导致音频数据生成速度远快于播放速度,填满缓冲。
  4. 无缓冲溢出防护:写入缓冲前未检查剩余空间,直接写入导致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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 21:09:51