2010-10-07

OpenAL on Windows

In my previous post I discussed the graphics related word  I had to do to get my iPhone targeted game running on my PC. This was relatively easy to do and has improved my productivity significantly. Of course, my game contains sound as well, and it is important to be able to develop those aspects of it on my PC as well. This post discusses that.

From the beginning I decided to use OpenAL for sound in my game. The main reason is it provides relatively simple access to 3D positioned audio, which is something I wanted (actually, I really just am working 2D space). Getting this working on the iPhone initially in native Objective-C was pretty straight forward, but, as with OpenGL, OpenAL is not natively available on Windows. The OpenTK library that MonoTouch relies on has built in support for OpenAL, but it still needs an underlying library.

I searched around a bit and quickly found Creative Labs' OpenAL library for Windows. This installed easily, but I still needed to implement the layer to reads WAV files and converts them into a format that OpenAL understands.

In the iPhone version I do this to initialize OpenAL:
/// <summary>
/// As per the SDK:
/// <br/>
/// Your application must call this function before making any other Audio Session Services calls.
/// You may activate and deactivate your audio session as needed (see AudioSessionSetActive),
/// but should initialize it only once.
/// </summary>
public SoundManager()
{
    // setup our audio session
    AudioSession.Initialize();
    AudioSession.Category = AudioSessionCategory.AmbientSound;
    AudioSession.SetActive(true);
    AudioSession.Interrupted += HandleAudioSessionInterrupted;
    AudioSession.Resumed += HandleAudioSessionResumed;
    // TODO: Check if BGM is playing
    // UInt32 size = sizeof(iPodIsPlaying);
    // result = AudioSessionGetProperty(kAudioSessionProperty_OtherAudioIsPlaying, &size, &iPodIsPlaying);
    // if the iPod is playing, use the ambient category to mix with it
    // otherwise, use solo ambient to get the hardware for playing the app background track
    // UInt32 category = (iPodIsPlaying) ? kAudioSessionCategory_AmbientSound : kAudioSessionCategory_SoloAmbientSound;
    OpenALManager = new OpenALManager();
}

And then this to load CAF sound samples:
private static AudioFile GetAudioFile(string filename)
{
    if(!File.Exists(filename)) {
        throw new FileNotFoundException("Could not find sound file.", filename);
    }
    AudioFileType fileType;
    switch(Path.GetExtension(filename).ToUpper()) {
        case ".CAF":
            fileType = AudioFileType.CAF;
            break;
        default:
            throw new NotSupportedException("Audio files of type " + Path.GetExtension(filename) + " are not supported.");
    }
    using(CFUrl cfUrl = CFUrl.FromFile(filename)) {
        return AudioFile.Open(cfUrl, AudioFilePermission.Read, fileType);
    }
}
private static AudioData GetAudioData(AudioFile audioFile)
{
    // Set the client format to 16 bit signed integer (native-endian) data
    // Maintain the channel count and sample rate of the original source format
    AudioStreamBasicDescription outputFormat = new AudioStreamBasicDescription();
    outputFormat.SampleRate = audioFile.StreamBasicDescription.SampleRate;
    outputFormat.ChannelsPerFrame = audioFile.StreamBasicDescription.ChannelsPerFrame;
    outputFormat.Format = AudioFormatType.LinearPCM;
    outputFormat.BytesPerPacket = 2 * audioFile.StreamBasicDescription.ChannelsPerFrame;
    outputFormat.FramesPerPacket = 1;
    outputFormat.BytesPerFrame = 2 * audioFile.StreamBasicDescription.ChannelsPerFrame;
    outputFormat.BitsPerChannel = 16;
    outputFormat.FormatFlags = AudioFormatFlags.IsPacked | AudioFormatFlags.IsSignedInteger;
    // Set the desired client (output) data format
    bool hadError = audioFile.SetProperty(AudioFileProperty.DataFormat, Marshal.SizeOf(outputFormat), GCHandle.ToIntPtr(GCHandle.Alloc(outputFormat)));
    if(hadError) {
        throw new InvalidOperationException("Could not set output format.");
    }
    byte[] data = new byte[audioFile.Length];
    audioFile.Read(0, data, 0, (int)audioFile.Length, false);
    AudioData audioData = new AudioData(data, outputFormat.ChannelsPerFrame == 1 ? ALFormat.Mono16 : ALFormat.Stereo16, outputFormat.SampleRate);
    return audioData;
}

For the PC I had to do something similar, except for WAV files, not CAF:

using System;
using System.IO;
using OpenTK.Audio;
using OpenTK.Audio.OpenAL;
using Qythyx.OpenTKTools.Sound;
namespace Qythyx.Launcher_PC
{
    internal sealed class WaveReader
    {
        private static ALFormat GetALFormat(WaveData data)
        {
            switch(data.Channels)
            {
                case 1:
                if(data.BitsPerSample == 8)
                {
                return ALFormat.Mono8;
                }
                else if(data.BitsPerSample == 16)
                {
                return ALFormat.Mono16;
                }
                break;
                case 2:
                if(data.BitsPerSample == 8)
                {
                return ALFormat.Stereo8;
                }
                else if(data.BitsPerSample == 16)
                {
                return ALFormat.Stereo16;
                }
                break;
            }
            throw new AudioException("Unsupported audio format. Channels = " + data.Channels + ", Bits per Sample = " + data.BitsPerSample);
        }
        /// <summary>Reads and decodes the sound file.</summary>
        /// <param name="filename">The WAVE filename.</param>
        /// <returns>An <see cref="AudioData"/> object that contains the decoded data.</returns>
        public static AudioData ReadWave(string filename)
        {
            WaveData data = ReadWaveData(filename);
            return new AudioData(data.Data, GetALFormat(data), data.SampleRate);
        }
        private struct WaveData
        {
            public int RiffChunckSize;
            public int FormatChunkSize;
            public short AudioFormat;
            public short Channels;
            public int SampleRate;
            public int ByteRate;
            public short BlockAlign;
            public short BitsPerSample;
            public int DataChunkSize;
            public byte[] Data;
        }
        // Read the WAVE/RIFF headers.
        private static WaveData ReadWaveData(string filename)
        {
            using(Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read))
            {
                using(BinaryReader reader = new BinaryReader(stream))
                {
                WaveData data = new WaveData();
                // RIFF header
                if(new string(reader.ReadChars(4))!= "RIFF")
                {
                throw new FormatException("File is not recognized as valid WAVE format. Can't find RIFF signature.");
                }
                data.RiffChunckSize = reader.ReadInt32();
                if(new string(reader.ReadChars(4)) != "WAVE")
                {
                throw new FormatException("File is not recognized as valid WAVE format. Can't find expected WAVE format.");
                }
                // WAVE header
                if(new string(reader.ReadChars(4)) != "fmt ")
                {
                throw new FormatException("File is not recognized as valid WAVE format. Can't find 'fmt' marker.");
                }
                data.FormatChunkSize = reader.ReadInt32();
                data.AudioFormat = reader.ReadInt16();
                data.Channels = reader.ReadInt16();
                data.SampleRate = reader.ReadInt32();
                data.ByteRate = reader.ReadInt32();
                data.BlockAlign = reader.ReadInt16();
                data.BitsPerSample = reader.ReadInt16();
                while(reader.PeekChar() == 0)
                {
                reader.Read();
                }
                if(new string(reader.ReadChars(4)) != "data")
                {
                throw new FormatException("File is not recognized as valid WAVE format. Can't find data marker.");
                }
                data.DataChunkSize = reader.ReadInt32();
                data.Data = reader.ReadBytes((int)stream.Length);
                return data;
                }
            }
        }
    }
}

Well, after this OpenAL was happy on my PC and I've now got sound and graphics and I can develop happily in Visual Studio. Woohoo!

Next time: Problems deploying to iPhone.

3 comments:

  1. Cool story! Thanks for sharing and I'm waiting for the next one as I also learning by doing MonoDevelop + MonoTouch too! :)

    ReplyDelete
  2. Glad you're enjoying it. If you have any suggestions for other info to include let me know.

    ReplyDelete
  3. What platforms are you targetting and dev stack are you using? I was porting a game from Flash to iOS, and the Packager for iPhone was unacceptably slow. I ended up writing a C++ library that feels like SDL reworked for a good Box2D integration. I also chose OpenAL, like you have.

    C++ is generally pretty shitty, but Objective C is horrible to mix with C++. That's what I learned from a few weeks messing with Cocos2D. Anyhow, thanks for the direction with this post, although I was hoping to find out how to parse the .caf header so that I could use one file format for all sounds. Also, did you know about IMA compression on iPhone? It can significantly reduce your caf file sizes.

    ReplyDelete