Example: Playing a sound (Low level) - OutOfTheVoid/ProjectRed GitHub Wiki
Playing a sound with Red is, relatively, a complex process, due to the way that the sound engine is set up. The reason for this complexity, however, is that the engine is designed to be much more flexible than a simple "Load a sound and call Play()" sort of deal, allowing for dynamic effects, synthesis, and analysis in an efficient and modular manner.
I'll go over the basics, from loading a WAV file to making it actually produce audio. Please note that I have not included the sample audio file I used, but you should be able to easily substitute it with any other (uncompressed) WAV file.
Opening an audio device
To start off, we'll initialize SDLX just like we did in Example 01, but this time, we'll use the kSDLFlag_Audio
flag instead of kSDLFlag_Video
, since we'll only be playing sound in this example.
uint32_t Status;
SDLX::Lib :: Init ( & Status, SDLX::Lib :: kSDLFlag_Audio );
if ( SDLX :: kStatus_Success )
return 1;
The basic process to getting sound playing involves a few steps, the first of which is to acquire an audio device, which is essentially a link with the sound card driver. This is done through the SDLX::AudioDevice
class. We begin by making sure there is at least one valid audio device by calling SDLX::AudioDevice::GetDeviceCount ()
. ( Failing gracefully if there is not. ) GetDeviceCount ()
takes one parameter, bool RequireRecording
, which allows us to tell SDLX that the device must have the ability to record, should we set it to true
, but for this example, we do not.
if ( SDLX::AudioDevice :: GetDeviceCount ( false ) == 0 )
{
SDLX::Lib :: DeInit ();
return 2;
}
In order to actually open an audio device, we have to have that device's name. This is achieved with the function SDLX::AudioDevice::GetDeviceName ()
, which takes two parameters: uint32_t DeviceIndex
, which selects which audio device we want from the N devices that GetDeviceCount ()
enumerates that we want, and bool RequireRecording
, which is just like in GetDeviceCount ()
. For the purposes of this example, we decide to just open the first available audio device, but you may want to apply your own criteria for device selection for a production application.
const char * AudioDevName = SDLX::AudioDevice :: GetDeviceName ( 0, false );
Now that we have the name of our device, we can open it! This is a somewhat complicated action, since opening a device specifies all of the parameters by which it will operate. The call to make is SDLX::AudioDevice::RequestAudioDevice ()
, which takes several parameters: const char * DeviceName
, the name of the device, which we just figured out, uint32_t Frequency
, the sampling frequency in samples per second, BufferFormat Format
, the way that the audio data should be encoded, uint32_t ChannelCount
, the number of channels, uint32_t SampleSize
, the number of samples to request per audio callback ( and hence playback latency ), bool FlexibleSource
, which tells SDLX whether or not we can handle a format different from the one we asked for, and bool RequireRecording
, which is just like before. Note that SDLX::AudioDevice
is a refcounted object.
I'll use some pretty reasonable values for this, but you may want to play around with them. A special note on Format
, the Red audio engine is designed to be able to convert from one format to another very easily, so you can select virtually anything that is defined as SDLX::AudioDevice::kBufferFormat_xxxxx
. Note however, that it is usually more efficient to select the system endian version of any type, which is denoted by the suffix '_SE' here.
SDLX::AudioDevice * MyAudioDevice = SDLX::AudioDevice :: RequestAudioDevice ( AudioDevName, SDLX::AudioDevice :: kStandardFrequencey_44K, SDLX::AudioDevice :: kBufferFormat_I16_SE, 2, 4096, true, false );
if ( MyAudioDevice == NULL )
{
SDLX::Lib :: DeInit ();
return 3;
}
MyAudioDevice -> Reference ();
Since SDLX is designed to be compatible with, but separate from Red, we need to adapt MyAudioDevice
using a class that works with the Red audio engine. This is done using the Red::Audio::AudioStreamOutput
class. This serves to abstract away the audio callback mechanics and provide a streaming audio mechanism. It's constructor takes two parameters, IAudioOutputDevice * OutputDevice
, of which SDLX::AudioDevice is derived, and uint32_t MaxChannelCount
, which for this example we'll set to two for stereo audio.
Red::Audio :: AudioStreamOutput MyAudioOutput ( MyAudioDevice, 2 );
Loading a WAV
Now that we have our audio stream set up, we need to load some audio to play in it. We'll be using the WAV format via the RAUX::WAVFile
class, which makes loading wav files simple. The constructor of WAVFile simply takes the file name as an argument, and we can then call RAUX::WAVFile::Load ()
with a status parameter to try and load the file.
RAUX :: WAVFile MyWAV ( "LittlePistol.wav" );
MyWAV.Load ( & Status );
if ( Status != RAUX::WAVFile :: kStatus_Success )
{
MyAudioDevice -> Dereference ();
MyAudioDevice = NULL;
SDLX::Lib :: DeInit ();
return 4;
}
Assuming the load completed successfully, we can now get the audio data into a buffer for playback. We simply call RAUX::WAVFile::LoadToBuffer ()
, which returns a pointer of type Red::Audio::AudioBuffer
, which is the primary audio data storage type in Red. If this operation fails, it will return NULL instead. Additionally, once Load ()
is called on a WAVFile instance, the file will remain open until you call Close ()
on it, so it is a good idea to call it once we have our buffer. As Red::Audio::AudioBuffer
is a RefCounted type, ( as defined in Red::Util::IRefCounted
), we will want to reference it once we know it's valid.
Red::Audio :: AudioBuffer * WAVBuffer = MyWAV.LoadToBuffer ();
MyWAV.Close ( & Status );
if ( WAVBuffer == NULL )
{
MyAudioDevice -> Dereference ();
MyAudioDevice = NULL;
SDLX::Lib :: DeInit ();
return 5;
}
WAVBuffer -> Reference ();
Alright, so we have a buffer, but a buffer alone is just a bunch of data. In order to make Red interpret it as playable audio, we'll need to create some Stream Sources ( as defined in Red::Audio::IStreamSource
). This is done by wrapping our buffer in an instance of Red::Audio::RawBufferStreamSource
( which is derived from IStreamSource
) for each channel, in this case, one for left and one for right. The constructor takes two arguments, AudioBuffer * SourceData
, which is the address of our audio buffer, and uint32_t Channel
, which is the channel that we'd like to use. I've selected channel 0 for left and 1 for right, which usually is correct, although this is somewhat dependent on the operating system and audio driver.
Note that RawBufferStreamSource
is a refcounted type, so to prevent accidental early destruction, we reference local instances beyond their scoped lifetime.
Red::Audio :: RawBufferStreamSource LeftChannelSource ( WAVBuffer, 0 );
Red::Audio :: RawBufferStreamSource RightChannelSource ( WAVBuffer, 1 );
LeftChannelSource.Reference ();
RightChannelSource.Reference ();
Now that we have our stream sources set up, we can tell our audio output that it should use them to source audio data. To do this, we call Red::Audio::AudioStreamOutput::SetStreamSource ()
, which takes two arguments: uint32_t Channel
, which specifies which channel we'd like to set the source for, and IStreamSource * Source
, a pointer to the stream source we're assigning to that channel, which in this case are the RawBufferStreamSource
's we created for the left and right channels of our WAV file audio.
MyAudioOutput.SetStreamSource ( 0, & LeftChannelSource );
MyAudioOutput.SetStreamSource ( 1, & RightChannelSource );
Now that everything is connected, we can begin playing audio! This is as simple as calling Red::Audio::AudioStreamOutput :: Start ()
. It is important to understand however, that the audio callback is a synchronous, so it happens in a separate thread. If we were to simply return from our main function here, we wouldn't hear very much audio if any, as the program would exit immediately. In order to solve this problem, I've simply blocked the thread for the duration of my audio clip, in this case about 390 seconds, using the Red::Util::Time :: BlockFor ()
function which accepts an instance of Red::Util::Time :: Duration
. This is basically just like calling sleep in a regular c program, but using Red's time semantics.
MyAudioOutput.Start ();
Red::Util::Time :: BlockFor ( Red::Util::Time :: Duration ( 390.0 ) );
If you were to run this now, you should hear your audio play, assuming that everything worked correctly. All that's left to do is to clean everything up and stop the audio stream.
MyAudioOutput.Stop ();
WAVBuffer -> Dereference ();
MyAudioDevice -> Dereference ();
MyAudioDevice = NULL;
SDLX::Lib :: DeInit ();
Note that it is important to call Stop ()
on the AudioOutputStream, as if the audio callback fires and any of our objects relevant to it have been deallocated, the audio thread will crash, causing a somewhat ungraceful exit and potentially leaking resources.
Here's a link to the complete application: Example 02 Code
Written by Liam Taylor / OutOfTheVoid