|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThe TgPlayOgg project is a .NET C# library that allows you to play Ogg Vorbis files from your managed code. Decoding a given Ogg Vorbis file into usable sound data is done by TgPlayOgg which makes calls to an unmanaged C++ project TGPlayOgg_vorbisfile. TgPlayOgg also requires managed DirectX for sound output. BackgroundAt TrayGames, we needed to add support for playing sound files to the multi-player online game development SDK (TGSDK) provided to third party developers. We started out using the MP3 audio format, but we were concerned about licensing issues (fees kick in after you reach a certain level of sales). After comparing alternatives, we chose to use the Ogg Vorbis format. Ogg Vorbis is a completely open, patent-free, professional audio encoding and streaming technology with all the benefits of Open Source. Using the CodeIf you download the source, there is an "OggPlayer.sln" solution file under the "OggPlayer Sample" folder that will build all of the projects mentioned in this article. A sample test application has been provided in the "Test App" folder under the TgPlayOgg project. This application demonstrates how to use the library. The steps are as follows:
Let's take a look at the highlights of the test application. First we see it has a method that does the initialization and handles the using TG.Sound;
private void InitTestOfOggPlayer()
{
try
{
oplay = new OggPlay(this, OggSampleSize.SixteenBits);
oplay.PlayOggFileResult += new PlayOggFileEventHandler(PlayOggFileResult);
textBox1.Text = "Initialization successful.\r\n";
}
catch(Exception e)
{
textBox1.Text = "Initialization failed: " + e.Message + "\r\n";
}
}
private void Button1Click(object sender, System.EventArgs e)
{
OggName = GetOggFileNameToOpen();
if (OggName != null)
{
oplay.PlayOggFile(OggName, ++PlayId);
textBox1.Text = "Playing " + OggName + " Id= " + PlayId.ToString() + "\r\n";
}
}
The Ogg Vorbis decoder may have encountered errors while decoding the Ogg Vorbis data. An Ogg Vorbis file may refuse to play if there is not enough data to stream through the initial buffers (the Ogg is too small) or if it simply can not read the file. There are two error counts that are for information purposes only, since if successful the created waveform data was played, but it may not have sounded as intended if either of these two counts are nonzero. The way we handle the private static void PlayOggFileResult(object sender, PlayOggFileEventArgs e)
{
if (e.Success)
{
MainForm.textBox1.Text += "PlayOggFile(" + e.PlayId + ") succeeded ("
+ "ErrorHoleCount: " + e.ErrorHoleCount + ", ErrorBadLinkCount: "
+ e.ErrorBadLinkCount + ").\r\n";
}
else
{
MainForm.textBox1.Text += "PlayOggFile(" + e.PlayId + ") failed: '"
+ e.ReasonForFailure + "'\r\n";
}
PlayId--;
}
Note that exiting a calling application does not kill the playback threads if one or more Ogg Vorbis files are still playing. These playback threads keep running although you can no longer hear them playing. The threads will finish playing whatever Ogg Vorbis files they were playing and then quit, unless the threads are specifically told to stop playback. So, when your application exits, it should probably kill any long playing Ogg Vorbis files that are still playing. This is why the test application handles the protected void Form1_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
// Determine if any Ogg files are still playing by checking the PlayId member
if (PlayId > 0)
{
// Display a MsgBox asking the user to save changes or abort
if (MessageBox.Show("Ogg files are still playing," +
" are you sure you want to exit?", "TrayGames Ogg Player",
MessageBoxButtons.YesNo) == DialogResult.No)
{
// Cancel the Closing event from closing the form
e.Cancel = true;
// Wait for files to finish playing...
}
else
{
// Kill all outstanding playbacks
while (PlayId > 0)
oplay.StopOggFile(PlayId--);
}
}
}
Other times you might want to kill playback threads in midstream would be if your application has a pause capability, or if you want to reset sounds when the user switches away from your game. The Ogg Vorbis WrapperOgg Vorbis' high-level API, Vorbisfile, has only two input choices: either a C file pointer or a set of custom callback functions that do the reading of the input Ogg Vorbis data. The better and more portable of these choices is probably custom callbacks, but I wasn't aware that .NET 1.1 gave any control over the calling convention of its methods, and its standard calling convention is I have since learned that you can use the [DllImport("SomeLibrary.DLL", EntryPoint="SomeFunction", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.Cdecl)]
public static extern bool SomeFunction(String param1, String param2);
For now though, the TGPlayOgg_vorbis project makes the calls into the Ogg Vorbis API for us. There are three wrapper functions: // External C functions in the TgPlayOgg_vorbisfile unmanaged DLL
[DllImport("TgPlayOgg_vorbisfile.dll", CharSet=CharSet.Unicode,
CallingConvention=CallingConvention.Cdecl)]
public unsafe static extern int init_for_ogg_decode(
string fileName, void **vf_out);
[DllImport("TgPlayOgg_vorbisfile.dll", CallingConvention=CallingConvention.Cdecl)]
public unsafe static extern int ogg_decode_one_vorbis_packet(
void *vf_ptr, void *buf_out, int buf_byte_size,
int bits_per_sample, int *channels_cnt, int *sampling_rate,
int *err_ov_hole_cnt, int *err_ov_ebadlink_cnt);
[DllImport("TgPlayOgg_vorbisfile.dll", CallingConvention=CallingConvention.Cdecl)]
public unsafe static extern int final_ogg_cleanup(void *vf_ptr);
What allows us to make these calls is the Platform Invoke (PInvoke) service. PInvoke will enable our managed code to call the unmanaged functions implemented in the DLL. It will locate and invoke the exported functions and marshal their parameters across the managed/unmanaged code boundary as needed. Note that PInvoke throws exceptions generated by the unmanaged function to the managed caller. Let's look at our unmanaged functions now. The int init_file_for_ogg_decode(wchar_t *filename, void **vf_out) { // . . . int ov_ret = ov_open(file_ptr, static_cast<OggVorbis_File*>(vf_ptr), NULL, 0); if (ov_ret < 0) { // There was an error so cleanup now fclose(file_ptr); free(vf_ptr); // Return the ifod_err_ code return err_code; } // Copy the memory pointer to the caller *vf_out = vf_ptr; return 0; // success } The Next it calls The Ogg Vorbis format allows for multiple logical bitstreams to be combined (with restrictions) into a single physical bitstream. Note that the Vorbisfile API could more or less hide the multiple logical bitstream nature of chaining from an application but, when reading audio back, the application must be aware that multiple bitstream sections do not necessarily use the same number of channels or sampling rate. The Ogg Vorbis documentation provides more information on Ogg logical bitstream framing. int ogg_decode_one_vorbis_packet(void *vf_ptr, void *buf_out, int buf_byte_size, int ogg_sample_size, int *channels_cnt, int *sampling_rate, int *err_ov_hole_cnt, int *err_ov_ebadlink_cnt) { // . . . for (bytes_put_in_buf = 0;;) { long ov_ret = ov_read(static_cast<OggVorbis_File*>(vf_ptr), static_cast<char*>(buf_out), buf_byte_size, 0, word_size, want_signed, &bitstream); if (ov_ret == 0) // at EOF { break; } else if (ov_ret < 0) { // An error occurred, bad ogg data of some kind if (ov_ret == OV_HOLE) ++(*err_ov_hole_cnt); else if (ov_ret == OV_EBADLINK) ++(*err_ov_ebadlink_cnt); } else { assert(ov_ret <= buf_byte_size); vorbis_info* vi_ptr = ov_info(static_cast<OggVorbis_File*>(vf_ptr), bitstream); if (vi_ptr != NULL) { // Number of channels in the bitstream *channels_cnt = vi_ptr->channels; // Sampling rate of the bitstream *sampling_rate = vi_ptr->rate; } bytes_put_in_buf = ov_ret; break; } } return bytes_put_in_buf; } After a bitstream has been opened using int ogg_final_cleanup(void *vf_ptr) { int ret = 0; if (vf_ptr != NULL) { ret = ov_clear(static_cast<OggVorbis_File*>(vf_ptr)); // non-zero is failure free(vf_ptr); } return ret; } The .NET Ogg Vorbis LibraryThe Microsoft .NET 1.1 Framework has no sound playing classes, so to play the waveform data constructed from the decoded Ogg Vorbis file data, there are basically two choices. The first is to write the waveform data out as a WAV file, and then use quartz.dll (on Win98 and later) to play that WAV file. The disadvantage of this choice is that WAV files can be very large (e.g. a 5.5 MB Ogg Vorbis file was tested and resulted in a 67 MB WAV file), and playback can't begin until after the entire WAV file has been written out (e.g. decoding that 5.5 MB Ogg Vorbis file and writing out a WAV file takes more than 20 seconds on a 1.6 GHz P4 PC). The other choice is to use methods in managed DirectX which means there's no need to write out any WAV file, and we can play the waveform data as it is generated, so playback can begin much quicker than the first approach. The TrayGames client already ensures that the managed DirectX APIs are installed on target computers so this was not an issue for us and it's the choice we went with. The public OggPlay(Control owner, OggSampleSize wantedOggSampleSize)
{
// Set DirectSoundDevice
DirectSoundDevice = new Device();
// NOTE: The DirectSound documentation recommends
// CooperativeLevel.Priority for games
DirectSoundDevice.SetCooperativeLevel(owner,
CooperativeLevel.Priority);
// Set OggSampleSize
OggFileSampleSize = wantedOggSampleSize;
}
The The TgPlayOgg library declares two events with delegates and an event argument class (which defines data for both events) for playing and stopping Ogg Vorbis files. The public sealed class PlayOggFileEventArgs : EventArgs
{
private bool success;
// If !Success then this is the explanation for the failure
private string reasonForFailure;
// The value of the playID parameter when PlayOggFile() was called
private int playId;
public int ErrorHoleCount,
// Count of encountered OV_HOLE errors during decoding
// indicates there was an interruption in the data.
ErrorBadLinkCount;
// Count of encountered OV_EBADLINK errors during decoding
// indicates that an invalid stream
// section was supplied to libvorbisfile,
// . . .
}
public void PlayOggFile(string fileName, int playId)
{
PlayOggFileEventArgs EventArgs = new PlayOggFileEventArgs(playId);
// Decode the ogg file in a separate thread
PlayOggFileThreadInfo pofInfo = new PlayOggFileThreadInfo(
EventArgs, fileName,
OggFileSampleSize == OggSampleSize.EightBits ? 8 : 16,
DirectSoundDevice, this);
Thread PlaybackThread = new Thread(new
ThreadStart(pofInfo.PlayOggFileThreadProc));
PlaybackThread.Start();
Thread.Sleep(0);
}
public void StopOggFile(int playId)
{
PlayOggFileEventArgs EventArgs = new PlayOggFileEventArgs(playId);
StopOggFileNow(this, EventArgs);
}
The The first thing that int ErrorCode = NativeMethods.init_file_for_ogg_decode(FileName, &vf);
if (ErrorCode != 0)
{
// . . .
oplay.PlayOggFileResult(this, EventArgs);
return;
}
Next // Get next chunk of PCM data, pin these so GC can't relocate them
fixed(byte *buf = &PcmBuffer[0])
{
fixed(int *HoleCount = &EventArgs.ErrorHoleCount)
{
fixed(int *BadLinkCount = &EventArgs.ErrorBadLinkCount)
{
// NOTE: The sample size of the returned PCM data -- either 8-bit
// or 16-bit samples -- is set by BitsPerSample
PcmBytes = NativeMethods.ogg_decode_one_vorbis_packet(
vf, buf, PcmBuffer.Length,
BitsPerSample,
&ChannelsCount, &SamplingRate,
HoleCount, BadLinkCount);
}
}
}
The first time we return from the int HoldThisManySamples =
(int)(SamplingRate * SecBufHoldThisManySeconds);
// Set the format
MyWaveFormat.AverageBytesPerSecond = AverageBytesPerSecond;
MyWaveFormat.BitsPerSample = (short)BitsPerSample;
MyWaveFormat.BlockAlign = (short)BlockAlign;
MyWaveFormat.Channels = (short)ChannelsCount;
MyWaveFormat.SamplesPerSecond = SamplingRate;
MyWaveFormat.FormatTag = WaveFormatTag.Pcm;
// Set BufferDescription
MyDescription = new BufferDescription();
MyDescription.Format = MyWaveFormat;
MyDescription.BufferBytes =
SecBufByteSize = HoldThisManySamples * BlockAlign;
MyDescription.CanGetCurrentPosition = true;
MyDescription.ControlPositionNotify = true;
// Create the buffer
SecBuf = new SecondaryBuffer(MyDescription, DirectSoundDevice);
// Set 3 notification points, at 0, 1/3, and 2/3 SecBuf size
MyNotify = new Notify(SecBuf);
BufferPositionNotify[] MyBufferPositions = new BufferPositionNotify[3];
MyBufferPositions[0].Offset = 0;
MyBufferPositions[0].EventNotifyHandle =
SecBufNotifyAtBegin.Handle;
MyBufferPositions[1].Offset =
(HoldThisManySamples / 3) * BlockAlign;
MyBufferPositions[1].EventNotifyHandle =
SecBufNotifyAtOneThird.Handle;
MyBufferPositions[2].Offset =
((HoldThisManySamples * 2) / 3) * BlockAlign;
MyBufferPositions[2].EventNotifyHandle =
SecBufNotifyAtTwoThirds.Handle;
MyNotify.SetNotificationPositions(MyBufferPositions);
After these objects are prepared, we load the decoded PCM data into a // Copy the new PCM data into PCM memory stream
PcmStream.SetLength(0);
PcmStream.Write(PcmBuffer, 0, PcmBytes);
PcmStream.Position = 0;
PcmStreamNextConsumPcmPosition = 0;
// Initial load of secondary buffer
if (SecBufInitialLoad)
{
int WriteCount = (int)Math.Min(
PcmStream.Length,
SecBufByteSize - SecBufNextWritePosition);
if (WriteCount > 0)
{
SecBuf.Write(
SecBufNextWritePosition,
PcmStream,
WriteCount,
LockFlag.None);
SecBufNextWritePosition += WriteCount;
PcmStreamNextConsumPcmPosition += WriteCount;
}
if (SecBufByteSize == SecBufNextWritePosition)
{
// Done filling the buffer
SecBufInitialLoad = false;
SecBufNextWritePosition = 0;
// So start the playback
// NOTE: Play does the playing in its own thread
SecBuf.Play(0, BufferPlayFlags.Looping);
Thread.Sleep(0);
//yield rest of timeslice
//so playback can start right away
}
else
{
continue; // Get more PCM data
}
}
Points of InterestThose are pretty much the highlights of the sample, TgPlayOgg, and TgPlayOgg_vorbisfile projects. These projects are interesting if you want to learn about decoding Ogg Vorbis audio files or as an example of how to call unmanaged code from the managed .NET environment. If you are interested in checking out the full TGSDK for producing your own multi-player online games, you can get it at the TrayGames web site. You may also want to check out the Ogg Vorbis web site to learn more about their encoding format and the many tools for manipulating it. Revision History
| ||||||||||||||||||||