iOSで効果音を鳴らす

現在公開中のシューティングゲーム制作にあたり、効果音で苦労したのでここに記録を残します。

# 評価の流れ

- AVAudioPlayer（BGMで採用、効果音では却下）
  - 動作が重い
  - シューティングゲームのような短時間で様々な効果音を鳴らすような用途には向いていない
- SystemSoundID（却下）
  - 動作は軽い
  - 再生するとBGM(AvAudioPlayer)がフェードアウトするケースがある
- OpenAL（採用）
  - 純正Musicアプリと交互に利用すると効果音が再生されなくなるケースがあった
    - 再現性は不明

# BGM再生

AVAudioPlayerの利用方法は色々な方が分かりやすい記事を残しているので本記事では割愛します。

# 効果音再生

以下の流れで素材の準備と実装を行いました。

## mp3をcafに変換

mp3のままでは利用できないので下記コマンドでファイル変換します。
<pre><code>
afconvert -f caff -d ima4 input.mp3 output.caf
</code></pre>

参考記事
[【OSX】afconvert【音声ファイル変換】](https://qiita.com/fromage-blanc/items/32e2ba83b79151e5ecb9)

## コード実装

OpenALデバイス初期化
<pre><code>
static ALCdevice *openALDevice;
static ALCcontext *openALContext;
</code></pre>
<pre><code>
- (void)initializeSound
{
   openALDevice = alcOpenDevice(NULL);
   openALContext = alcCreateContext(openALDevice, NULL);
   alcMakeContextCurrent(openALContext);
}
</code></pre>

サウンド登録
<pre><code>
- (ALuint)entryAL:(NSArray *)seItemArray
{
    ALuint sourceID;
    alGenSources(1, &sourceID);
    //
    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:[seItemArray objectAtIndex:0] ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];
    AudioFileID afid;
    OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
    if (0 != openAudioFileResult) {
        NSLog(@"An error occurred when attempting to open the audio file %@: %d", audioFilePath, (int)openAudioFileResult);
        return -1;
    }
    //
    UInt64 audioDataByteCount = 0;
    UInt32 propertySize = sizeof(audioDataByteCount);
    OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataByteCount);
    if (0 != getSizeResult) {
        NSLog(@"An error occurred when attempting to determine the size of audio file %@: %d", audioFilePath, (int)getSizeResult);
    }
    //
    UInt32 bytesRead = (UInt32)audioDataByteCount;
    void *audioData = malloc(bytesRead);
    OSStatus readBytesResult = AudioFileReadBytes(afid, false, 0, &bytesRead, audioData);
    if (0 != readBytesResult) {
        NSLog(@"An error occurred when attempting to read data from audio file %@: %d", audioFilePath, (int)readBytesResult);
    }
    AudioFileClose(afid);
    //
    ALuint outputBuffer;
    alGenBuffers(1, &outputBuffer);
    alBufferData(outputBuffer, AL_FORMAT_STEREO16, audioData, bytesRead, 22050);
    if (audioData) {
        free(audioData);
        audioData = NULL;
    }
    //
    alSourcef(sourceID, AL_GAIN, [[seItemArray objectAtIndex:2] floatValue]);
    alSourcef(sourceID, AL_PITCH, 1.0f);
    alSourcei(sourceID, AL_BUFFER, outputBuffer);
    return sourceID;
}
</code></pre>

再生
(短時間で同じ音を連続再生するとプチノイズが出やすいので再生後にインターバルを設けました)
<pre><code>
- (void)startSE:(int)index
{
    if (self.enabled == false || self.seEnabled == false) {
        return;
    }
    // 短時間で連続再生するとプチノイズが出やすいので再生後にインターバルを設ける
    if (![[self.seStatusArray objectAtIndex:index] boolValue]) {
        [self.seStatusArray replaceObjectAtIndex:index withObject:[NSNumber numberWithBool:true]];
        NSDictionary* userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:index] forKey:@"index"];
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                              target:self
                                                            selector:@selector(enableSE:)
                                                            userInfo:userInfo
                                                             repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        // OpenAL
        ALuint sourceID = [[self.seSoundArray objectAtIndex:index] intValue];
        alSourcePlay(sourceID);
    }
}
</code></pre>
<pre><code>
- (void)enableSE:(NSTimer *)timer
{
    int index = [[(NSDictionary*)timer.userInfo objectForKey:@"index"] intValue];
    [self.seStatusArray replaceObjectAtIndex:index withObject:[NSNumber numberWithBool:false]];
}
</code></pre>

雑な紹介になってしまいましたが何かの参考になれば幸いです。
コードはこちらで公開中です。
https://github.com/fugasat/openAL_sample/

あと、実際にサウンドを組み込んだゲームはこちらで公開中です。(iOS専用)
https://apps.apple.com/jp/app/trial-unit2/id1519638159