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

混音器中音频文件播放结束时触发事件的实现方案

问题描述

我正在开发一款背景音乐与环境音效应用,需要同时播放音乐和环境音,两者都通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 15:55:33