混音器中音频文件播放结束时触发事件的实现方案
问题描述
我正在开发一款背景音乐与环境音效应用,需要同时播放音乐和环境音,两者都通过FadeInOutSampleProvider加入混音器输入。现在要实现两个声道的连续播放——一个文件播放完毕后自动播放下一个。之前单文件播放时用WaveOutEvent.PlaybackStopped事件就能判断结束,但用混音器时这个事件不会触发(推测是因为数据流并未真正终止)。除了计算音频片段播放时长来判断播放完成外,有没有更优雅的实现方式?
现有代码
FadingAudioPlaybackContainer类
public sealed class FadingAudioPlaybackContainer: IDisposable { private readonly AudioPlaybackEngine _engine; private readonly FadeInOutSampleProvider _fader; private readonly AudioFileReader _fileReader; public event EventHandler? PlaybackStopped; public FadingAudioPlaybackContainer(string filePath, AudioPlaybackEngine engine) { _engine = engine; _fileReader = new AudioFileReader(filePath); _fader = new FadeInOutSampleProvider(_fileReader, true); // 这个事件不会触发,因为混音流并未终止 _engine.PlaybackStopped += (sender, args) => PlaybackStopped?.Invoke(sender, EventArgs.Empty); } public async Task FadeInAsync(int durationInMs) { _fader.BeginFadeIn(durationInMs); _engine.AddMixerInput(_fader); await Task.Delay(durationInMs); } /*...其他方法...*/ }
AudioPlaybackEngine类
// 感谢Mark! public class AudioPlaybackEngine : IDisposable { public event EventHandler? PlaybackStopped; private readonly IWavePlayer _outputDevice; private readonly MixingSampleProvider _mixer; public AudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2) { _outputDevice = new WaveOutEvent(); _mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount)) { ReadFully = true }; _outputDevice.Init(_mixer); // 同样不会触发,因为混音流从未终止 _outputDevice.PlaybackStopped += (sender, args) => PlaybackStopped?.Invoke(this, EventArgs.Empty); _outputDevice.Play(); } public void AddMixerInput(ISampleProvider input) { _mixer.AddMixerInput(input); } public void RemoveMixerInput(ISampleProvider input) { _mixer.RemoveMixerInput(input); } public void Dispose() { _outputDevice.Dispose(); } }
解决方案:监听单个音频流的结束事件
混音器的PlaybackStopped事件仅在整个混音流终止时触发,单个音频文件播放完毕不会终止混音流。要实现单音频文件的结束监听,需从单个音频流的读取状态入手,具体步骤如下:
1. 自定义SampleProvider包装类,跟踪播放结束
创建继承自ISampleProvider的类,在Read方法中检测原始音频流是否读取完毕,一旦读取到0个样本就触发结束事件:
public class EndTrackingSampleProvider : ISampleProvider { private readonly ISampleProvider _source; public event EventHandler? PlaybackEnded; public WaveFormat WaveFormat => _source.WaveFormat; public EndTrackingSampleProvider(ISampleProvider source) { _source = source; } public int Read(float[] buffer, int offset, int count) { int samplesRead = _source.Read(buffer, offset, count); if (samplesRead == 0) { PlaybackEnded?.Invoke(this, EventArgs.Empty); } return samplesRead; } }
2. 修改FadingAudioPlaybackContainer,监听单个音频结束
调整音频流的包装顺序,先加入结束跟踪器,再包装淡入淡出逻辑,从而监听单个音频的结束事件:
public sealed class FadingAudioPlaybackContainer: IDisposable { private readonly AudioPlaybackEngine _engine; private readonly FadeInOutSampleProvider _fader; private readonly AudioFileReader _fileReader; private readonly EndTrackingSampleProvider _endTracker; public event EventHandler? PlaybackStopped; public FadingAudioPlaybackContainer(string filePath, AudioPlaybackEngine engine) { _engine = engine; _fileReader = new AudioFileReader(filePath); // 先包装结束跟踪器,再添加淡入淡出处理 _endTracker = new EndTrackingSampleProvider(_fileReader); _fader = new FadeInOutSampleProvider(_endTracker, true); // 绑定单个音频的结束事件 _endTracker.PlaybackEnded += OnPlaybackEnded; } private void OnPlaybackEnded(object? sender, EventArgs e) { // 音频结束后从混音器移除输入,并触发容器的结束事件 _engine.RemoveMixerInput(_fader); PlaybackStopped?.Invoke(this, EventArgs.Empty); } public async Task FadeInAsync(int durationInMs) { _fader.BeginFadeIn(durationInMs); _engine.AddMixerInput(_fader); await Task.Delay(durationInMs); } public void Dispose() { _endTracker.PlaybackEnded -= OnPlaybackEnded; _fileReader.Dispose(); } /*...其他方法...*/ }
3. 实现连续播放逻辑
在业务代码中监听FadingAudioPlaybackContainer.PlaybackStopped事件,触发时自动创建并播放下一个音频:
private async Task PlayNextTrack(string nextFilePath) { var container = new FadingAudioPlaybackContainer(nextFilePath, _audioEngine); container.PlaybackStopped += async (s, e) => { container.Dispose(); // 此处替换为获取下一个音频文件的逻辑 await PlayNextTrack(GetNextFilePath()); }; await container.FadeInAsync(1000); }
方案优势
- 无需手动计算音频时长,避免采样率、声道数等参数导致的计算误差
- 直接从音频流读取状态判断结束,响应准确及时
- 逻辑与混音器解耦,每个音频容器独立管理结束事件,代码模块化程度更高
内容的提问来源于stack exchange,提问作者Nicolas
相关产品推荐
相关产品推荐

