65.9K
CodeProject 正在变化。 阅读更多。
Home

玩转声音

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (25投票s)

2014年6月4日

CPOL

5分钟阅读

viewsIcon

43986

downloadIcon

2366

一次性播放您喜欢的音乐和声音文件。

引言

本文介绍了如何在应用程序中使用声音文件,并同时播放多个声音文件。

背景

在 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 文档在此

MSDN mcSendString 文档

mcSendString 命令字符串文档

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 文件重复播放的支持的解决方法。

 

© . All rights reserved.