C#使用NAudio实现音频先全量播放再从指定位置循环的问题
问题原因
- 第一版代码的问题:你初始化
LoopStream后直接将Position设置为了循环起始位置,所以首次播放直接从X点开始,且默认LoopStream是循环整个流区间,自然不会只循环X到末尾的片段。 - 优化后版本的问题:你是等第一次全量播放完全触发
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
相关产品推荐
相关产品推荐

