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

C#使用NAudio实现音频先全量播放再从指定位置循环的问题

问题原因
  1. 第一版代码的问题:你初始化LoopStream后直接将Position设置为了循环起始位置,所以首次播放直接从X点开始,且默认LoopStream是循环整个流区间,自然不会只循环X到末尾的片段。
  2. 优化后版本的问题:你是等第一次全量播放完全触发PlaybackStopped事件后,才重新初始化循环流、重新初始化音频设备,这个重新初始化的过程就是卡顿延迟的来源。

解决方案

不需要拆分两次播放流程,直接自定义一个支持「首次播放全量音频、后续循环指定区间」的流,一次性初始化给音频播放器即可,完全避免二次初始化的开销,实现无缝切换。

自定义流实现代码

public class FirstFullThenLoopStream : WaveStream
{
    private readonly WaveStream _sourceStream;
    private readonly long _loopStartPos;
    private bool _hasPlayedFullOnce = false;

    public FirstFullThenLoopStream(WaveStream sourceStream, long loopStartPosition)
    {
        _sourceStream = sourceStream;
        _loopStartPos = loopStartPosition;
    }

    public override WaveFormat WaveFormat => _sourceStream.WaveFormat;
    public override long Length => _sourceStream.Length;

    public override long Position
    {
        get => _sourceStream.Position;
        set => _sourceStream.Position = value;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        int read = _sourceStream.Read(buffer, offset, count);
        // 读取到流末尾
        if (read == 0)
        {
            if (!_hasPlayedFullOnce)
            {
                // 首次播放完成,标记状态,跳转到循环起始点
                _hasPlayedFullOnce = true;
                _sourceStream.Position = _loopStartPos;
                return _sourceStream.Read(buffer, offset, count);
            }
            else
            {
                // 循环阶段,跳回循环起始点继续读取
                _sourceStream.Position = _loopStartPos;
                return _sourceStream.Read(buffer, offset, count);
            }
        }
        return read;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _sourceStream.Dispose();
        }
        base.Dispose(disposing);
    }

    // 提供重置方法,方便停止播放后重置状态
    public void ResetPlayState()
    {
        _hasPlayedFullOnce = false;
        Position = 0;
    }
}

调整后的播放方法代码

internal void PlayAudioLoopPoint(WaveOut AudioPlayer, uint LoopPos, decimal Volume, byte[] PCMData, int Frequency, int Pitch, int Bits, int Channels, decimal AudioPan)
{
    if (WaveOut.DeviceCount == 0)
    {
        MessageBox.Show(GenericFunctions.resourcesManager.GetString("NoAudioDevices"), "EuroSound", MessageBoxButtons.OK, MessageBoxIcon.Error);
        StopAudio(AudioPlayer);
        return;
    }

    if (AudioPlayer.PlaybackState != PlaybackState.Stopped) return;

    var audioSample = new MemoryStream(PCMData);
    var soundWavFormat = new WaveFormat(CalculateValidRate(Frequency, Pitch), Bits, Channels);
    var rawSource = new RawSourceWaveStream(audioSample, soundWavFormat);
    // 用自定义流包装原始流,传入循环起始位置
    var customLoopStream = new FirstFullThenLoopStream(rawSource, LoopPos);
    
    // 挂载音量、声道处理逻辑
    var volumeProvider = new VolumeSampleProvider(customLoopStream.ToSampleProvider())
    {
        Volume = (float)Volume
    };
    var panProvider = new PanningSampleProvider(volumeProvider)
    {
        Pan = (float)(AudioPan / 100)
    };

    AudioPlayer.DeviceNumber = GlobalPreferences.DefaultAudioDevice;
    AudioPlayer.Volume = 1;
    // 一次性初始化,无需后续二次Init
    AudioPlayer.Init(panProvider);
    AudioPlayer.Play();
}

注意事项
  • 自定义流的Read方法会在NAudio的音频缓冲区回调中自动调用,切换循环时是无缝的,没有重新初始化设备的开销,完全消除延迟
  • 不需要再监听PlaybackStopped事件做二次初始化,避免重复绑定事件导致的内存泄漏和逻辑异常
  • 调用StopAudio停止播放时,记得调用自定义流的ResetPlayState方法重置播放状态,方便下次播放逻辑正常执行

内容的提问来源于stack exchange,提问作者jms2505

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 23:09:01