ALURE + AVFoundation で aac や mp3 でも OpenAL で再生出来るようにする


[OpenAL](http://openal.org) のヘルパーライブラリである [ALURE](http://kcat.strangesoft.net/alure.html) に実装されているカスタムデコーダの機能を使ってデフォルトでは対応していない mp3 や aac を AVFoundation を利用して OpenAL 上でも再生出来るようにするコードを書いてみる。

## カスタムデコーダの実装

エラー処理がないので、実際に使う際はエラー処理を書いておくこと。

```objc++
#import <AVFoundation/AVFoundation.h>
#import <OpenAL/al.h>

#include "OpenAL/alure.h"

/* ARC 前提 */
struct AlureDecoder {
    AlureDecoder(const char *path)
    : asset(nil),
      reader(nil),
      output(nil)
    {
        NSString *filePath = [[NSString alloc] initWithUTF8String:path];
        NSURL *fileUrl = [[NSURL alloc] initFileURLWithPath:filePath];
        asset = [AVURLAsset assetWithURL:fileUrl];
        NSError *error = nil;
        reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
        if (error == nil) {
            /* オーディオトラックのみを取り出す */
            NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeAudio];
            /* 44.1KHz 2ch 16bits PCM で取り出す設定を行う */
            NSDictionary *outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithUnsignedLong:kAudioFormatLinearPCM], AVFormatIDKey, [NSNumber numberWithFloat:44100.0f], AVSampleRateKey, [NSNumber numberWithInt:2], AVNumberOfChannelsKey, [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleavedKey, [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey, [NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey, [NSNumber numberWithInt:16], AVLinearPCMBitDepthKey, nil];
            output = [[AVAssetReaderAudioMixOutput alloc] initWithAudioTracks:tracks audioSettings:outputSettings];
            if ([reader canAddOutput:output]) {
                [reader addOutput:output];
                [reader startReading];
            }
            else {
                /* エラー処理（普通ここに到達しないはず） */
            }
        }
        else {
            /* エラー処理 */
        }
    }
    ~AlureDecoder() {
    }
    static void install() {
        alureInstallDecodeCallbacks(0, createFromFile, createFromMemory, getFormat, decode, rewind, destroy);
    }
    static void *createFromFile(const ALchar *path) {
        AlureDecoder *decoder = new AlureDecoder(path);
        return decoder;
    }
    static void *createFromMemory(const ALubyte * /* bytes */, ALuint /* buffer */) {
        /* 未対応 */
        return 0;
    }
    static ALboolean getFormat(void *opaque, ALenum *format, ALuint *samples, ALuint *bps) {
        /* 出力設定に従って設定 */
        *format = AL_FORMAT_STEREO16;
        *samples = 44100;
        /* channels * (bits / 8) */
        *bps = 4;
        return AL_TRUE;
    }
    static ALuint decode(void *opaque, ALubyte *bytes, ALuint size) {
        ALuint output = 0;
        AlureDecoder *decoder = static_cast<AlureDecoder *>(opaque);
        if (decoder->reader.status == AVAssetReaderStatusReading) {
            CMSampleBufferRef sampleBuffer = [decoder->output copyNextSampleBuffer];
            if (sampleBuffer) {
                AudioBufferList audioBufferList;
                CMBlockBufferRef blockBuffer;
                /* 生の PCM データを取り出し、bytes にコピーする */
                CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);
                UInt64 sampleSize = CMSampleBufferGetTotalSampleSize(sampleBuffer);
                memcpy(bytes, audioBufferList.mBuffers[0].mData, sampleSize);
                CFRelease(blockBuffer);
                CFRelease(sampleBuffer);
                output = ALuint(sampleSize);
            }
        }
        /* （エラー含む）読み込み完了した場合 0 を返す */
        return output;
    }
    static ALboolean rewind(void * /* opaque */) {
        /* 未対応 */
        return AL_FALSE;
    }
    static void destroy(void *opaque) {
        AlureDecoder *decoder = static_cast<AlureDecoder *>(opaque);
        delete decoder;
    }
    AVAsset *asset;
    AVAssetReader *reader;
    AVAssetReaderAudioMixOutput *output;
};
```

## 動作確認のための実装

以下は動作確認のためのコード。コンパイルする際は先ほどのコードと AVFoundation.framework と OpenAL.framework と ALURE をリンクする必要がある。

```objc++
static bool g_playing = true;

static void callback(void *opaque, ALuint source)
{
    g_playing = false;
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        alureInitDevice(NULL, NULL);
        AlureDecoder::install();
        ALuint buffer, source;
        alGenBuffers(1, &buffer);
        alGenSources(1, &source);
        alureBufferDataFromFile("/path/to/audio.mp3", buffer);
        alSourcei(source, AL_BUFFER, buffer);
        if (alurePlaySource(source, callback, NULL)) {
            while (g_playing) {
                alureSleep(0.125f);
                alureUpdate();
            }
        }
        alDeleteBuffers(1, &buffer);
        alDeleteSources(1, &source);
        alureShutdownDevice();
    }
    return 0;
}
```

ALURE は外部向けの API は C ではあるが内部実装は C++ のため静的リンクする場合は C++ としてコンパイルする必要がある。

### おまけ

バッファの長さを求める際はサイズから計算する

```c
int size, channels, bits, frequency, bytesPerSecond, durationSeconds;
alGetBufferi(buffer, AL_SIZE, &size);
alGetBufferi(buffer, AL_CHANNELS, &channels);
alGetBufferi(buffer, AL_BITS, &bits);
alGetBufferi(buffer, AL_FREQUENCY, &frequency);
bytesPerSecond = (frequency * channels * bits) / 8;
durationSeconds = size / bytesPerSecond;
```

from http://stackoverflow.com/questions/7978912/how-to-get-length-duration-of-a-source-with-single-buffer-in-openal