2020年5月4日 星期一

[VB.NET] 使用windows API播放音檔 [Playing audio with windows API]


Command

Description
Open

Opens a MCI device.
Close

Closes a MCI device.
Play

Plays a MCI device.
Pause
 PAUSE or  RESUME
Pauses playing or recording.
Stop

Stops a MCI device.
Back

Steps backward through available tracks.
Step

Steps forward through available tracks.
Prev

Goes to the beginning of the current track using the Seek command. If executed within three seconds of the previous Prev command, goes to the beginning of the previous track or to the beginning of the first track if at the first track.
Next

Goes to the beginning of the next track (if at last track, goes to the beginning of the last track) using the Seek command.
Seek

Seeks track forward or backward.
Record

Records MCI device input.
Eject

Ejects Audio CD from CD drive (opens/closes door)
Save

Saves an open file.



How to play sound files in VB.NET via the Media Control Interface (MCI) from the windows API.

This tutorial is meant to provide a non-comprehensive introduction - tailored to the beginner - to playing audio with the MCI, and assumes some basic knowledge about functions, arguments, variables, etc...
If information on these subjects is required, I would recommend taking a look at the nice tutorial series by Vwse here:


Why did I write this tutorial?



Rewind to about a week ago. I had a vision for a little text based game I was going to try to create for fun, and had some ideas for music and sound effects in it. What I had in mind was a variety of sound effects being played/stopped at precise times and at different volumes, and different background musics which looped, and changed at different locations in the game. I quickly realized that this was not feasible via the audio functionality built into VB. I discovered the mci functions soon after, but found that the amount of online information about their use was quite pathetic. The information there was, excluding the online documentation, was not the easiest to find. I thought I'd try to compile the things I'd learned, into an easily accessible guide.

What is the MCI, and why would I want to use it?

The MCI is an interface in the windows API, which is used to control various media items.(sound files, video files, dvd drives, audio controllers, recording, etc…) It is useful because, from my current understanding (feel free to correct me on this), there is no built in way to play any sound file other than a wave (.wav extension) in Visual Basic .NET. A .wav file is an uncompressed, and therefore high quality and space consuming audio file. It is not a sound format which I often use for importing music, sound effects, or anything on my computer. It is possible to convert various audio file formats to .wav and play them that way, but that would be the easy way out wouldn’t it? Another problem is that I believe the built in code only supports playing one sound file at a time, which is no good for something like a game, where you might want to have background music, people talking, sound effects, etc. If you are only interested in playing solitary wave files, check out the following links:

Feel free to start a new console program and follow along.

To get started, we’re going to put this in our code:


PrivateDeclareFunctionmciSendStringLib"winmm.dll"Alias"mciSendStringA"(ByVallpszCommandAsString,ByVallpszReturnStringAsString,ByValcchReturnAsInteger,ByValhwndCallbackAsInteger)AsInteger


As I previously stated, this function is not a built in function in vb, it is in fact stored in a dynamic link library called “winmm.dll”, located at “C:\windows\system32\winmm.dll”. A .dll file is essentially a library of code/functions which provides simultaneous functionality to many different programs. The alias keyword refers to how the function is named in the .dll file. The declare keyword is used to utilize procedures in external files in vb. Anyway, this function is used to control the MCI. I’ll do a breakdown of the intimidating looking parameters this function has in ascending order from least to most important. First though, it's return value.

Return Value - As you know, this function returns an integer. If your command succeeded, it returns 0. Otherwise, it returns a number which corresponds to an error. A different function can interpret these errors for us, more on this later.



ByValhwndCallbackAsInteger

This parameter is only used if you used the “notify” flag at the end of a command string. For the most part, this can be left as 0 or null. I have never used it, and don’t know how useful it is. (I’ve read that it’s not especially useful, and I’ve never had a reason to look it up) Refer to the following page for more info on this subject.



ByVallpszReturnStringAsString

This parameter is only used if you are requesting return information. If your command is “open”, or “play”, you don’t need any return information, you would use an empty string, or just make the argument null (keyword “nothing” in VB.NET). However, you cannot use just any string, you must use a buffered string. From my current knowledge, a buffered string is essentially a string that has a set number of characters at all times. Say you requested the playing status of the sound file you were playing, and it returns "playing" in the string variable specified in the lpszReturnString argument. The rest of the characters in the buffered string would just be spaces. Anyway, if you are requesting return information, you use a string with a buffer of 128 (I'm not sure if any other size buffers work, I've never tried them) as the argument, which will receive the return information. This is easy to do:


PrivatereturnDataAsString=Space(128)

This simply fills the string "returnData" with 128 spaces, so it can receive the return information. This all probably sounds confusing at the moment, but I'll soon give a few examples.




ByValcchReturnAsInteger

Very simple – the size, in characters, of the lpszReturnString buffer. If that parameter is not being used, just set it to 0. If you are requesting return information, set it to 128.



ByVallpszCommandAsString


This is the command sent as a string to an MCI device - the main way we're going to be loading, playing, pausing, etc... The syntax for this string is always as so:

(Command verb) + (target device) + (Other command specific flags (generally only used if requesting information))

This section is going to cover the actual commands (play, stop, seek, etc...)

Using the Open command

Syntax: (in string form)

"Open "& (path to sound file in wrapped in quotes) & " Alias " & (future device name) 

– Opens a device (sound file) so it can be used/manipulated via the MCI. After opening a file, it is assigned the name after the alias keyword. This becomes the device name, and the sound file is now referenced to by this name. Any string not containing a space can be used for this, I’ll give some examples further below.

Note - If playing 2+ audio files simultaneously, a different alias should be used for each one. The same is true if you want to play different parts of the same audio file simultaneously, they must be separately opened, and be assigned different aliases.

Note - it is possible to leave out the alias keyword, and simply keep referring to the sound file by its path. However, I wouldn't recommend it. Seems like it would make it more confusing, and easier to make a mistake. 

Important - the path to the sound file must be wrapped in quotes. This is slightly easier said than done, as a quote would generally end the string. There are a few ways to do this, all pretty much the same:


PrivateConstPathAsString=Chr(34)&"C:\medeski.mp3"&Chr(34)
PrivateConstPath2AsString="""C:\medeski.mp3"""
PrivateConstPath3AsString=ControlChars.Quote&"C:\medeski.mp3"&ControlChars.Quote

In this example I assign the device name "thesong" to the opened audio file.



PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias thesong","",0,0)

Console.ReadLine()

mciSendString("close all","",0,0)
EndSub

Using the Play command

Syntax: (in string form)

"Play "& (device or "all") & (optional “ repeat”) or

"Play " & (device or "all") & " from " & (position in milliseconds) & " to " & (position in milliseconds) & (optional “ repeat”)

– Can be used to play an audio file after it has been loaded, or to play all loaded audio files. Can also be used to play from a certain start position to a certain end position in a file, and to loop playing of a file. Generally should not be used for resuming. Unfortunately, the repeat keyword will only repeat from the beginning of a song. That means it is not directly possible to, say, loop a 10 second portion in the middle of a song. More on this in the examples below.





PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias thesong","",0,0)
mciSendString("play thesong","",0,0)

Console.ReadLine()

mciSendString("close all","",0,0)
EndSub

An example of looping an audio file



PrivateConstPathAsString="""C:\song1.mp3"""

SubMain()

mciSendString("open "&Path&" alias theSong","",0,0)

'That's 2000 milliseconds,this will loop the first 2 seconds of a song

mciSendString("play theSong from 0 to 2000 repeat","",0,0)

Console.ReadLine()

mciSendString("close all","",0,0)

EndSub

This doesn't work



PrivateConstPathAsString="""C:\song1.mp3"""

SubMain()

mciSendString("open "&Path&" alias theSong","",0,0)

'At first, this looks like it might loop this 3 second stretch, but unfortunately
'it does not.It will in fact play the song from the 5th second to the 8th'second, and then start back from the beginning of the song, continuing to loop the first 8 seconds

mciSendString("play theSong from 5000 to 8000 repeat", "", 0, 0)

Console.ReadLine()

mciSendString("close all" , "", 0, 0)

End Sub

Using the Close command

Syntax: (in string form)

"Close "& (device or "all")

Closes one or all opened devices (files). To use them again, they must be reopened. Good practice (in my opinion) is to always close after opening something, although this might not be necessary.


PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias thesong","",0,0)
mciSendString("play thesong","",0,0)

Console.ReadLine()

mciSendString("close thesong","",0,0)
EndSub

Using the Pause command

Syntax: (in string form)

"Pause "& (device or "all") – Can be used to pause an audio file after it has been loaded and is playing, or to pause all playing audio files. I tend to use stop instead of pause, because of an issue I talk about below.


mciSendString("pause thesong","",0,0)

mciSendString("pause all","",0,0)

Using the Stop command

Syntax: (in string form)

"Stop "& (device or "all") - Can be used to stop an audio file after it has been loaded and is playing, or to stop all playing audio files. After an audio file has been loaded but not played, it is in the “stopped” mode. Otherwise, I do not know how this differs from pause save a bug which I will mention later.


mciSendString("stop thesong","",0,0)

mciSendString("stop all","",0,0)

Using the Resume command

Syntax: (in string form)

"Resume "& (device or "all") – Used to resume audio when it is paused, or when it is stopped (cannot be used to start playing the initial time). Good to note is that something like "resume all repeat" does not work. Repeat can only be used with the play command.


In this example, a song is opened, played for 10 seconds, paused for 2 seconds, and then resumed.



PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias theSong","",0,0)

mciSendString("play all","",0,0)

'halts the code for 10000 milliseconds (10 seconds)
System.Threading.Thead.Sleep(10000)

mciSendString("pause all", "", 0, 0)

System.Threading.Thead.Sleep(2000)

mciSendString("resume all", "", 0, 0)

Console.ReadLine()

mciSendString("close all" , "", 0, 0)

End Sub

Note – the play command can be used to resume playback of a file in the “stopped” position, and sometimes works to resume paused files. However, there is a bug when using the play command to resume paused files, which can result in the song restarting, and other strange issues. For this reason I tend to use stop more than pause, as then if needed I can resume a song which isn't currently looping, and have it loop.

For instance:


PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias medeski","",0,0)
mciSendString("play medeski","",0,0)

System.Threading.Thead.Sleep(10000)

mciSendString("stop medeski","",0,0)

System.Threading.Thead.Sleep(5000)

'If I wanted to resume using the "resume" command, I would have no way
'to make the song loop, but if I use play...

mciSendString("play medeski repeat","",0,0)

'This will resume from the stopped position, and loop

Console.ReadLine()

mciSendString("close all" , "", 0, 0)
End Sub

Using the Seek command

Syntax: (in string form)

"Seek "& (device name) & " to " & (position in milliseconds) 

– Goes to a specified location in a sound file, and stops. 0 is the beginning of the song, the end will vary. This should only be used when the file is paused or stopped, otherwise there is a strange issue that can cause the file to not play. Another thing to remember when using the seek command, is that after seeking, the play command must be used, not resume.

"seek alias to start" and "seek alias to end" - work predictably

Example: plays the first 4 seconds of a song, then starts playing from the
40th second


PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias medeski","",0,0)
mciSendString("play medeski","",0,0)

System.Theading.Thead.Sleep(4000)

mciSendString("pause all","",0,0)
mciSendString("seek medeski to 40000","",0,0)

mciSendString("play medeski","",0,0)

Console.ReadLine()

mciSendString("close all","",0,0)
EndSub

Using the Status command

Syntax: (in string form)

"Status "& (device name) & (“ mode”, or “ position”, or “ length”, or " volume")

– This command requests some information about the playing status of the sound file. “Mode”, will request the mode of the given device name. It will leave the info in the string given to it in the lpszReturnString parameter, and it will contain “stopped”, “paused”, “playing”, or nothing at all if it did not recognize the device name. (There are a couple other returns, but I've never encountered them. I think they might only be used for non audio) Remember, the string you supply to the lpszReturnString argument must be buffered with 128 spaces. "position" will give the current position in the audio file in milliseconds. "Length" will give the length of the audio file in milliseconds. "Volume" will give the current volume level. More on this later.


PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

DimreturnDataAsString=Space(128)
Dim returnData2 AsString=Space(128)
Dim returnData3 AsString=Space(128)
Dim returnData4 AsString=Space(128)

mciSendString("open "&Path&" alias medeski","",0,0)

mciSendString("play medeski","",0,0)

mciSendString("status medeski mode",returnData,128,0)

mciSendString("status medeski position", returnData2,128,0)

mciSendString("status medeski length", returnData3,128,0)

mciSendString("status medeski volume", returnData4,128,0)


'The .trim gets rid of all of the excess leading and trailing spaces
'This should display "playing", the position in milliseconds in the song,and the
' length of the song (in milliseconds), and the volume level of the song, in the 'console,if a real path was used

Console.WriteLine(returnData.Trim)
Console.WriteLine(returnData2.Trim)
Console.WriteLine(returnData3.Trim)
Console.WriteLine(returnData4.Trim)

Console.ReadLine()

mciSendString("close all","",0,0)
EndSub


Using the Setaudio command

Syntax: (in string form)

"Setaudio "& (device) & (Many different possibilities)

The "setaudio" command is extremely versatile. It can be used to change the volume immediately or over a period, to manipulate the volume of the left or right channel, to do recording, set the bass/treble, etc...

All we will cover in this tutorial is how to change the overall volume, as this is the only thing I've really used. Refer to a full list of possibilities here:


Anyway, if you have played any sound with the MCI commands, you've probably noticed one thing. The volume is extremely loud! This is because the default volume is a level of 1000 - the maximum. The minimum volume is 0 - completely off. The syntax for changing the volume is:

"Setaudio "& device & " volume to " & (a number 0-1000)


PrivateConstPathAsString="""C:\medeski.mp3"""

SubMain()

mciSendString("open "&Path&" alias medeski","",0,0)

'Lowers the volume to half the default

mciSendString("setaudiomedeski volume to 500", "", 0, 0)

mciSendString("play medeski", "", 0, 0)


Console.ReadLine()

mciSendString("close all" , "", 0, 0)
End Sub


This has barely scratched the surface of the various commands that can be used. For a full list, check here:


Troubleshooting

Remember when I was talking about the return value of the mciSendString function? It returns a 0 if successful, otherwise it returns a number corresponding to a specific error. We can use a related api function to interpret this error for us, and give us an error message.


PrivateDeclareFunctionmciGetErrorStringLib"winmm.dll"Alias"mciGetErrorStringA"(ByValfdwErrorAsInteger,ByVallpszErrorTextAsString,ByValcchErrorTextAsInteger)AsBoolean

A quick breakdown:

Return Value - returns a boolean representing whether or not the function successfully interpreted the error



ByValfdwErrorAsInteger

This parameter takes the integer return value of the mciSendString function.


ByVallpszErrorTextAsString

Very similar to the lpszReturnString parameter of mciSendString. This takes a similarly buffered string, which will receive the error message


ByValcchErrorTextAsInteger

Just like cchReturn - the number of characters the buffed string contains. Make this 128

For the example, I'll try to open a file at a path that doesn't exist



SubMain()

'Will receive the integer return of mciSendString
Dim mciReturn As Integer

'Will receive the error message
DimerrorStringAsString=Space(128)

'bogus path
mciReturn = mciSendString("open fdassh alias medeski" , "", 0, 0)

mciGetErrorString(mciReturn, errorString, 128)

'Will write an error message in the console
Console.WriteLine(errorString.Trim)

Console.ReadLine()

mciSendString("close all","",0,0)
EndSub

This is very useful if you're trying to figure out how to do something, and aren't sure why it isn't working. 

Anyway, this is the end of the tutorial. I hope I've been able to successfully pass on some knowledge about using the MCI to play audio. If there are any questions, put them in this thread.

Note - there may be typing errors

Some additional resources:

沒有留言:

張貼留言