Skip to content

异步音频播放

1 TaskCompletionSource

官方的 demo 中使用的是控制台程序,并通过 Console.ReadKey 来避免程序结束。

在 WPF 中像这样把所有步骤写在一个方法中时会出现一个问题,尽管程序没有结束,但是函数释放了。

由于 Play 不是一个阻塞方法,因此函数在结束后会释放其中所持有的资源,导致“点击了按钮,但是没有播放音频文件”的现象。

解决的方法是使用 TaskCompletionSource 并结合 await,这样使得 player 依赖上了 TaskCompletionSource 变量,TaskCompletionSource 又要在 player 的回调中设置值并返回,因此音乐能够完整地播放完毕,同时按钮的 IsEnabled 状态也正常了。

这种情况下 player 的生命周期被变相地延长了。

public sealed partial class PlayerViewModel : ObservableRecipient
{
    [RelayCommand]
    public async Task Play()
    {
        // Initialize the audio engine with the MiniAudio backend.
        using var audioEngine = new MiniAudioEngine();

        // Find the default playback device.
        var defaultPlaybackDevice = audioEngine
            .PlaybackDevices
            .FirstOrDefault(d => d.IsDefault);

        if (defaultPlaybackDevice.Id == IntPtr.Zero)
        {
            Messenger.Send("No default playback device found.", Channels.TOAST);
            return;
        }

        // The audio format for processing. We'll use 32-bit float, which is standard for processing.
        // The data provider will handle decoding the source file to this format.
        var audioFormat = new AudioFormat
        {
            Format = SampleFormat.F32,
            SampleRate = 48000,
            Channels = 2,
        };

        // Initialize the playback device. This manages the connection to the physical audio hardware.
        // The 'using' statement ensures it's properly disposed of.
        using var device = audioEngine
            .InitializePlaybackDevice(defaultPlaybackDevice, audioFormat);

        // Create a data provider for the audio file.
        // Replace "path/to/your/audiofile.wav" with the actual path to your audio file.
        using var dataProvider = new StreamDataProvider(
            audioEngine,
            audioFormat,
            File.OpenRead("Files/file_example_MP3_1MG.mp3")
        );

        // Create a SoundPlayer, linking the engine, format, and data provider.
        // The player is also IDisposable.
        using var player = new SoundPlayer(audioEngine, audioFormat, dataProvider);

        // Add the player to the device's master mixer to route its audio for playback.
        device.MasterMixer.AddComponent(player);

        // Start the device. This opens the audio stream to the hardware.
        device.Start();

        var tcs = new TaskCompletionSource<bool>();
        player.PlaybackEnded += (s, e) => tcs.SetResult(true);

        // Start playback.
        player.Play();

        await tcs.Task;
    }
}

2 一种验证

如果像这样使函数所使用的系统资源随 viewModel 生命周期,音频文件也能够播放至结束。

不过按钮的 IsEnabled 状态并不符合预期,播放命令在点击时就释放了,player.Play 能够播放完毕是因为它在后台线程上触发了播放任务,所需要的资源也未随着函数结束而释放,因此能够播放完毕。

public sealed partial class PlayerViewModel : ObservableRecipient
{
    private AudioEngine audioEngine = new MiniAudioEngine();
    private AudioFormat audioFormat;
    private AudioPlaybackDevice device;
    private StreamDataProvider dataProvider;
    private SoundPlayer player;

    public PlayerViewModel()
    {
        var defaultPlaybackDevice = audioEngine.PlaybackDevices.FirstOrDefault(d => d.IsDefault);

        if (defaultPlaybackDevice.Id == IntPtr.Zero)
        {
            Messenger.Send("No default playback device found.", Channels.TOAST);
            return;
        }

        audioFormat = new AudioFormat
        {
            Format = SampleFormat.F32,
            SampleRate = 48000,
            Channels = 2,
        };

        device = audioEngine.InitializePlaybackDevice(defaultPlaybackDevice, audioFormat);

        dataProvider = new StreamDataProvider(
            audioEngine,
            audioFormat,
            File.OpenRead("Files/file_example_MP3_1MG.mp3")
        );

        player = new SoundPlayer(audioEngine, audioFormat, dataProvider);
    }

    [RelayCommand]
    public void Play()
    {
        device.MasterMixer.AddComponent(player);

        device.Start();

        player.Play();
    }
}

Ref

  1. SoundFlow example