异步音频播放
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();
}
}