玩转声音
一次性播放您喜欢的音乐和声音文件。
引言
本文介绍了如何在应用程序中使用声音文件,并同时播放多个声音文件。
背景
在 Windows Forms 应用程序中播放声音文件至少有 3 种方法
- 使用 System.Media.SoundPlayer
- 使用 Windows Media Player 控件
- 使用 winmm.dll mciSendString
如果您只需要播放一个 WAV 格式的单个声音文件,那么 System.Media.SoundPlayer 是最简单的。但您无法播放 MP3 文件,也无法同时播放多个文件。
如果您想拥有一个漂亮的 UI 来控制媒体文件的播放,Windows Media Player 控件将是理想的选择。
如果您只是想一种简单的方法来播放声音文件,并且需要比 System.Media.SoundPlayer 更大的灵活性,那么您可能需要考虑使用 winmm.dll 的 mciSendString 函数。
winmm.dll mciSendString 的 MSDN 文档在此
winmm.dll 的 mciSendString 函数可用于播放各种类型的媒体文件,其功能非常广泛。在本文中,我们将只处理 WAV 和 MP3 格式的声音文件。
使用代码
下面的代码是 MciPlayer 类的实现,它封装了常用所需的功能。
Imports System.Collections.Generic
Imports System.Text
Imports System.Runtime.InteropServices
Namespace MCIDEMO
Class MciPlayer
<dllimport("winmm.dll")> _
Private Shared Function mciSendString(strCommand As [String], _
strReturn As StringBuilder, _
iReturnLength As Integer, _
hwndCallback As IntPtr) As Integer
End Function
<dllimport("winmm.dll")> _
Public Shared Function mciGetErrorString(errCode As Integer, _
errMsg As StringBuilder, _
buflen As Integer) As Integer
End Function
<dllimport("winmm.dll")> _
Public Shared Function mciGetDeviceID(lpszDevice As String) As Integer
End Function
Public Sub New()
End Sub
Public Sub New(filename As String, [alias] As String)
_medialocation = filename
_alias = [alias]
LoadMediaFile(_medialocation, _alias)
End Sub
Private _deviceid As Integer = 0
Public ReadOnly Property Deviceid() As Integer
Get
Return _deviceid
End Get
End Property
Private _isloaded As Boolean = False
Public Property Isloaded() As Boolean
Get
Return _isloaded
End Get
Set
_isloaded = value
End Set
End Property
Private _medialocation As String = ""
Public Property MediaLocation() As String
Get
Return _medialocation
End Get
Set
_medialocation = value
End Set
End Property
Private _alias As String = ""
Public Property [Alias]() As String
Get
Return _alias
End Get
Set
_alias = value
End Set
End Property
Public Function LoadMediaFile(filename As String, [alias] As String) As Boolean
_medialocation = filename
_alias = [alias]
StopPlaying()
CloseMediaFile()
Dim Pcommand As String = "open """ & filename & """ alias " & [alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
_isloaded = If((ret = 0), True, False)
If _isloaded Then
_deviceid = mciGetDeviceID(_alias)
End If
Return _isloaded
End Function
Public Sub PlayFromStart()
If _isloaded Then
Dim Pcommand As String = "play " & [Alias] & " from 0"
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
End If
End Sub
Public Sub PlayFromStart(callback As IntPtr)
If _isloaded Then
Dim Pcommand As String = "play " & [Alias] & " from 0 notify"
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, callback)
End If
End Sub
Public Sub PlayLoop()
If _isloaded Then
Dim Pcommand As String = "play " & [Alias] & " repeat"
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
End If
End Sub
Public Sub CloseMediaFile()
Dim Pcommand As String = "close " & [Alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
_isloaded = False
End Sub
Public Sub StopPlaying()
Dim Pcommand As String = "stop " & [Alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
End Sub
End Class
End Namespace
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace MCIDEMO
{
class MciPlayer
{
[DllImport("winmm.dll")]
private static extern int mciSendString(String strCommand, StringBuilder strReturn, int iReturnLength, IntPtr hwndCallback);
[DllImport("winmm.dll")]
public static extern int mciGetErrorString(int errCode, StringBuilder errMsg, int buflen);
[DllImport("winmm.dll")]
public static extern int mciGetDeviceID(string lpszDevice);
public MciPlayer()
{
}
public MciPlayer(string filename, string alias)
{
_medialocation = filename;
_alias = alias;
LoadMediaFile(_medialocation, _alias);
}
int _deviceid = 0;
public int Deviceid
{
get { return _deviceid; }
}
private bool _isloaded = false;
public bool Isloaded
{
get { return _isloaded; }
set { _isloaded = value; }
}
private string _medialocation = "";
public string MediaLocation
{
get { return _medialocation; }
set { _medialocation = value; }
}
private string _alias = "";
public string Alias
{
get { return _alias; }
set { _alias = value; }
}
public bool LoadMediaFile(string filename, string alias)
{
_medialocation = filename;
_alias = alias;
StopPlaying();
CloseMediaFile();
string Pcommand = "open \"" + filename + "\" alias " + alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
_isloaded = (ret == 0) ? true : false;
if (_isloaded)
_deviceid = mciGetDeviceID(_alias);
return _isloaded;
}
public void PlayFromStart()
{
if (_isloaded)
{
string Pcommand = "play " + Alias + " from 0";
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
}
}
public void PlayFromStart(IntPtr callback)
{
if (_isloaded)
{
string Pcommand = "play " + Alias + " from 0 notify";
int ret = mciSendString(Pcommand, null, 0, callback);
}
}
public void PlayLoop()
{
if (_isloaded)
{
string Pcommand = "play " + Alias + " repeat";
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
}
}
public void CloseMediaFile()
{
string Pcommand = "close " + Alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
_isloaded = false;
}
public void StopPlaying()
{
string Pcommand = "stop " + Alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
}
}
}
要使用 MciPlayer 类播放声音文件,我们需要调用其构造函数,提供声音文件的完整路径名和一个别名,然后调用其中一个播放函数。在下面的代码中,MciPlayer 通过 MciPlayer(string filename, string alias) 构造函数实例化,并通过 PlayFromStart 播放函数播放文件。
Dim filename As String = "Accordion-SoundBible.com-74362576.mp3"
Dim m As New MciPlayer(Application.StartupPath + "\" & filename, "1")
m.PlayFromStart()
string filename = "Accordion-SoundBible.com-74362576.mp3";
MciPlayer m = new MciPlayer(Application.StartupPath + @"\" + filename, "1");
m.PlayFromStart();
请注意,每个文件的别名必须是唯一的。我们使用别名来告知 MCI 系统我们要播放的文件。
要播放多个文件,只需为每个文件实例化一个 MciPlayer,每个文件使用一个唯一的别名。然后您可以同时播放任何 MciPlayer。
Dim filename As String = "Accordion-SoundBible.com-74362576.mp3"
Dim m As New MciPlayer(Application.StartupPath + "\" & filename, "1")
filename = "Music_Box-Big_Daddy-1389738694.mp3"
Dim m1 As New MciPlayer(Application.StartupPath + "\" & filename, "2")
m.PlayLoop()
m1.PlayLoop()
string filename = "Accordion-SoundBible.com-74362576.mp3";
MciPlayer m = new MciPlayer(Application.StartupPath + @"\" + filename, "1");
filename="Music_Box-Big_Daddy-1389738694.mp3";
MciPlayer m1 = new MciPlayer(Application.StartupPath + @"\" + filename, "2");
m.PlayLoop();
m1.PlayLoop();
如果您正在播放一个文件,并且需要知道文件何时播放完毕,您可以使用 PlayFromStart(IntPtr callback),其中 callback 是一个 Windows 窗体的句柄,该窗体将接收回调。
MCI 系统通过发送一个 Windows 消息 MM_MCINOTIFY(值为 953)来触发回调。
要接收此 Windows 消息,回调处理程序会重写窗体的默认 WinProc() 函数。
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = MM_MCINOTIFY Then
' The file is done playing, do whatever
System.Diagnostics.Debug.WriteLine(m.ToString())
For Each itm As Form1.ListItem In DirectCast(Me.parent, Form1).listBox1.Items
If itm.DeviceId = CInt(m.LParam) Then
'To handle wav file play looping
If (itm.Filename.Substring(itm.Filename.Length - 4).ToUpper() = ".WAV") _
AndAlso (CInt(m.WParam) = MCI_NOTIFY_SUCCESSFUL) _
AndAlso (itm.Playlooping) Then
Dim p As New MciPlayer()
p.[Alias] = itm.[Alias]
p.Isloaded = True
p.PlayFromStart(Me.Handle)
Exit For
Else
listBox1.Items.Add(DateTime.Now.ToString() & " " & _
DirectCast(itm.Filename, String))
Exit For
End If
End If
Next
End If
MyBase.WndProc(m)
End Sub
protected override void WndProc(ref Message m)
{
if (m.Msg == MM_MCINOTIFY)
{
// The file is done playing, do whatever
System.Diagnostics.Debug.WriteLine(m.ToString());
foreach (Form1.ListItem itm in ((Form1)this.parent).listBox1.Items)
{
if (itm.DeviceId == (int)m.LParam)
{
//To handle wav file play looping
if (
(itm.Filename.Substring(itm.Filename.Length - 4).ToUpper() == ".WAV")
&& ((int)m.WParam == MCI_NOTIFY_SUCCESSFUL)
&& (itm.Playlooping)
)
{
MciPlayer p = new MciPlayer();
p.Alias = itm.Alias;
p.Isloaded = true;
p.PlayFromStart(this.Handle);
break;
}
else
{
listBox1.Items.Add(DateTime.Now.ToString() + " " + (string)itm.Filename);
break;
}
}
}
}
base.WndProc(ref m);
}
在我们的演示中,Form2 用于在我们单击“播放通知”按钮时接收回调
Private f2 As Form2
f2 = New Form2()
..
Private Sub button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles button1.Click
If listBox1.SelectedIndex < 0 Then
Return
End If
Dim itm As ListItem = DirectCast(listBox1.SelectedItem, ListItem)
Dim filename As String = itm.ToString()
Dim m As MciPlayer = Nothing
If itm.[Alias] <> "" Then
m = New MciPlayer()
m.[Alias] = itm.[Alias]
m.Isloaded = True
Else
Dim [alias] As String = ""
m = CreateMCIPlayer(filename, [alias])
itm.[Alias] = [alias]
itm.DeviceId = m.Deviceid
End If
itm.Playlooping = False
m.PlayFromStart(f2.Handle)
End Sub
Form2 f2;
f2 = new Form2();
....
private void button1_Click(object sender, EventArgs e)
{
if (listBox1.SelectedIndex < 0) return;
ListItem itm=(ListItem)listBox1.SelectedItem;
string filename = itm.ToString();
MciPlayer m=null;
if (itm.Alias != "")
{
m = new MciPlayer();
m.Alias = itm.Alias;
m.Isloaded = true;
}
else
{
string alias = "";
m = CreateMCIPlayer(filename, ref alias);
itm.Alias = alias;
itm.DeviceId = m.Deviceid;
}
itm.Playlooping = false;
m.PlayFromStart(f2.Handle);
}
我们的演示仅显示声音停止播放的时间和声音文件的文件名。由于 miciSendString() 对 WAV 文件不支持“播放..重复”,因此我们还利用回调来实现 WAV 文件在重写的 Winproc() 函数中的“播放循环”。
文件加载注意事项
Public Function LoadMediaFile(filename As String, [alias] As String) As Boolean
_medialocation = filename
_alias = [alias]
StopPlaying()
CloseMediaFile()
Dim Pcommand As String = "open """ & filename & """ alias " & [alias]
Dim ret As Integer = mciSendString(Pcommand, Nothing, 0, IntPtr.Zero)
_isloaded = If((ret = 0), True, False)
If _isloaded Then
_deviceid = mciGetDeviceID(_alias)
End If
Return _isloaded
End Function
public bool LoadMediaFile(string filename, string alias)
{
_medialocation = filename;
_alias = alias;
StopPlaying();
CloseMediaFile();
string Pcommand = "open \"" + filename + "\" alias " + alias;
int ret = mciSendString(Pcommand, null, 0, IntPtr.Zero);
_isloaded = (ret == 0) ? true : false;
if (_isloaded)
_deviceid = mciGetDeviceID(_alias);
return _isloaded;
}
请注意,在我们通过 open 命令将文件加载到 MCI 系统之前,我们必须确保别名尚未被使用。为确保这一点,我们首先停止播放文件,然后使用其别名关闭媒体文件。
演示
当演示应用程序启动时,它会在当前目录中查找所有 .mp3 和 .wav 文件,并将它们列在 Form1 的列表框中。同时,Form2 将弹出在旁边作为回调监视器。
从列表框中选择任何文件,然后单击任何播放按钮。
“循环播放”会连续播放文件,在到达末尾时循环到开头。
“播放”会播放一次文件。
“播放通知”会播放一次文件,然后触发一个由 Form2 处理的回调。当文件播放完毕后,Form 2 的列表框中会出现一行,显示播放完成的时间和文件的别名。
要停止任何正在播放的文件,请从列表中选择文件,然后单击“停止播放”按钮。
如果您不记得哪个文件正在播放,可以通过单击“全部停止”按钮来停止所有文件。
为了好玩,您可以将您喜欢的 MP3 文件放入当前路径(MCIDEMO.exe 所在的位置),以便在列表框中列出,然后通过单击“循环播放”按钮来播放其中两个或多个文件。
玩得开心!
Unicode 文件名
mciSendString() 的一个问题是对 Unicode 文件名的支持。Windows 资源管理器和 C# 对 Unicode 文件名有完整的支持。但 mciSendString() 似乎无法与 Unicode 文件名一起使用。为了解决这个问题,我在当前目录创建了一个名为“unicodenamesupport”的子目录。如果列表框中的文件名是非 ANSI 文件名,我们就会将文件复制到此子目录,并将其名称用作递增的索引器。这个复制的文件以及以索引器作为别名将被用来实例化 MciPlayer。
Private Function IsAnsiName(s As String) As Boolean
Dim u As New UnicodeEncoding()
Dim b As Byte() = u.GetBytes(s)
For i As Integer = 1 To b.Length - 1 Step 2
If b(i) <> 0 Then
Return False
End If
Next
Return True
End Function
Private Function CreateMCIPlayer(filename As String, ByRef [alias] As String) As MciPlayer
Dim isansiname As Boolean = Me.IsAnsiName(filename)
Dim m As MciPlayer = Nothing
nextnum += 1
If isansiname Then
m = New MciPlayer(Application.StartupPath & "\" & filename, nextnum & "")
[alias] = nextnum & ""
Else
Dim ext As String = filename.Substring(filename.Length - 4)
Dim relocatedfile As String = Application.StartupPath & _
"\unicodenamesupport\" & nextnum & ext
If System.IO.File.Exists(relocatedfile) Then
System.IO.File.Delete(relocatedfile)
End If
System.IO.File.Copy(Application.StartupPath & "\" & filename, relocatedfile)
[alias] = nextnum & ""
m = New MciPlayer(relocatedfile, nextnum & "")
End If
Return m
End Function
private bool IsAnsiName(string s)
{
UnicodeEncoding u = new UnicodeEncoding();
byte[] b=u.GetBytes(s);
for (int i = 1; i < b.Length; i += 2)
{
if (b[i] != 0) return false;
}
return true;
}
private MciPlayer CreateMCIPlayer(string filename, ref string alias)
{
bool isansiname = this.IsAnsiName(filename);
MciPlayer m = null;
nextnum++;
if (isansiname)
{
m = new MciPlayer(Application.StartupPath + @"\" + filename, nextnum + "");
alias = nextnum + "";
}
else
{
string ext = filename.Substring(filename.Length - 4);
string relocatedfile = Application.StartupPath + "\\unicodenamesupport\\" + nextnum + ext;
if (System.IO.File.Exists(relocatedfile))
System.IO.File.Delete(relocatedfile);
System.IO.File.Copy(Application.StartupPath + @"\" + filename, relocatedfile);
alias = nextnum + "";
m = new MciPlayer(relocatedfile, nextnum + "");
}
return m;
}
关注点
在某些应用程序中,尤其是在游戏中,声音可以使体验更具吸引力,如果使用得当的话。您可能希望在下一个应用程序中考虑添加对声音的支持。
历史
声音的乐趣 V1
2014年6月3日:添加函数 getAliasFromFileName(string s) 来替换文件名中的所有空格,因为别名不能包含空格。这将干扰发送到 cmciSendString 函数的最终命令字符串。
2014年6月4日:添加了对非 ANSI Unicode 文件名的支持的解决方法。例如,包含中文字符的文件名。
2014年6月6日:添加了设备 ID 来识别设备(在本例中为声音文件名),以便我们能够跟踪从回调返回到重写 Winproc 的 lparam。这样,我们可以播放任意数量的“播放通知”文件,并且仍然知道哪个文件已停止播放。
2014年6月8日:通过回调 Winproc 添加了对 WAV 文件重复播放的支持的解决方法。