トップ «前の日記(2012-03-22) 最新 次の日記(2012-03-26)» 編集

Cocoa練習帳

iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど

2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|

2012-03-23 [iOS]Looping Recorderの基礎(Audio Queue Services)

_ Audio Queue Services

Audio Queue Servicesは、OS XとiOSの両方で使用できるサービスだ。アプリケーション側で管理するバッファに対して録音と再生を行う方式で、バッファ管理の手間が発生するが、その分、アプリケーション側で自由にバッファを扱えるという利点がある。

追加するフレームワークは、『AudioToolbox.framework』。インポートするヘッダ・ファイルは『AudioToolbox/AudioToolbox.h』だ。

デモ・アプリケーションでは、録音も再生も同じバッファを使用している。バッファの長さは、音声データを4秒間録音できるサイズになっていて、再生時、再生位置がバッファの末尾に到達したら、頭に戻るようにしている。
そう、テープエコーのループするテープの仕組みを模している!

バッファ

以下がバッファの初期化のコードだ。

- (void)prepareBuffer
{
    UInt32  bytesPerPacket = 2;
    UInt32  sec = 4;
    self.startingPacketCount = 0;
    self.maxPacketCount = (44100 * sec);
    self.buffer = malloc(self.maxPacketCount * bytesPerPacket);
}

4秒分の長さのバッファで、self.startingPacketCountは録音/再生位置を指し、初期化時は先頭を表す0に設定される。self.maxPackerCountは、バッファの長さで、バッファの末尾を指す変数となる。

Audio Queue Servicesを使用したサンプルには、Audio File Servicesを使って音声データの読み書きを行っている場合があるが、本稿では、独自に用意したバッファで音声データを管理するので、バッファに対する独自の読み書きメソッドが必要となる。

以下が、読み出しメソッドのコードだ。

- (void)readPackets:(AudioQueueBufferRef)inBuffer
{
    UInt32  bytesPerPacket = 2;
    UInt32  numPackets = self.maxPacketCount - self.startingPacketCount;
    if (self.numPacketsToRead < numPackets) {
        numPackets = self.numPacketsToRead;
    }
    
    if (0 < numPackets) {
        memcpy(inBuffer->mAudioData,
               (self.buffer + (bytesPerPacket * self.startingPacketCount)),
               (bytesPerPacket * numPackets));
        inBuffer->mAudioDataByteSize = (bytesPerPacket * numPackets);
        inBuffer->mPacketDescriptionCount = numPackets;
        self.startingPacketCount += numPackets;
    }
    else {
        inBuffer->mAudioDataByteSize = 0;
        inBuffer->mPacketDescriptionCount = 0;
    }
}

self.numPacketsToRead単位で読み出す。読み出した分、self.startingPacketCountを進める。再生位置がバッファの末尾に到達したら、読み出したサイズを0に設定して、読み出し側に末尾に到達した事を伝える。

次が、書き込みメソッドのコードだ。

- (void)writePackets:(AudioQueueBufferRef)inBuffer
{
    UInt32  bytesPerPacket = 2;
    UInt32  numPackets = (inBuffer->mAudioDataByteSize / bytesPerPacket);    
    if ((self.maxPacketCount - self.startingPacketCount) < numPackets) {
        numPackets = (self.maxPacketCount - self.startingPacketCount);
    }
    
    if (0 < numPackets) {
        memcpy((self.buffer + (bytesPerPacket * self.startingPacketCount)),
               inBuffer->mAudioData,
               (bytesPerPacket * numPackets));
        self.startingPacketCount += numPackets;
    }
}

引数で渡されたinBufferのデータをバッファにコピーする。

録音機能について説明する。録音の前準備を行うコードだ。

- (void)prepareAudioQueueForRecord
{
    AudioStreamBasicDescription audioFormat;
    audioFormat.mSampleRate         = 44100.0;
    audioFormat.mFormatID           = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags        = kLinearPCMFormatFlagIsSignedInteger
                                    | kLinearPCMFormatFlagIsPacked;
    audioFormat.mFramesPerPacket    = 1;
    audioFormat.mChannelsPerFrame   = 1;
    audioFormat.mBitsPerChannel     = 16;
    audioFormat.mBytesPerPacket     = 2;
    audioFormat.mBytesPerFrame      = 2;
    audioFormat.mReserved           = 0;
    
    AudioQueueNewInput(&audioFormat, MyAudioQueueInputCallback,
        self, NULL, NULL, 0, &__audioQueueObject);
    
    self.startingPacketCount = 0;
    AudioQueueBufferRef buffers[3];
    
    self.numPacketsToWrite = 1024;
    UInt32  bufferByteSize = self.numPacketsToWrite * audioFormat.mBytesPerPacket;
    
    int bufferIndex;
    for (bufferIndex = 0; bufferIndex < 3; bufferIndex++) {
        AudioQueueAllocateBuffer(self.audioQueueObject,
            bufferByteSize, &buffers[bufferIndex]);
        AudioQueueEnqueueBuffer(self.audioQueueObject,
            buffers[bufferIndex], 0, NULL);
    }
}

変数audioFormatに録音する音声データの形式を設定する。サンプルは、44.1kH/16ビット/モノラのリニアPCMの例となっている。

Audio Queue Servicesでは、コールバック関数を使って、バッファ・キューの管理を行うが、 AudioQueueNewInput()でコールバック関数を指定する。バッファ・キューはアプリケーション側で生成するが、サンプルはキューのバッファの個数は3個となっている。

簡単に説明すると、録音されたデータはキューで指しているバッファに書き込まれ、バッファが一杯になるとコールバック関数を呼ぶので、そこで、アプリケーションで管理している4秒分のバッファにコピーする、ということになる。

次が、そのコールバック関数のコードだ。

static void MyAudioQueueInputCallback(
    void                                *inUserData,
    AudioQueueRef                       inAQ,
    AudioQueueBufferRef                 inBuffer,
    const AudioTimeStamp                *inStartTime,
    UInt32                              inNumberPacketDescriptions,
    const AudioStreamPacketDescription  *inPacketDescs)
{
    AudioQueueServicesViewController    *viewController
        = (AudioQueueServicesViewController *)inUserData;
    [viewController writePackets:inBuffer];
    AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    
    if (viewController.maxPacketCount <= viewController.startingPacketCount) {
        [viewController stop:nil];
    }
}

先ほど説明した、 witePackets:メソッドで録音したデータをバッファにコピーしている。サンプルでは、録音位置がバッファの末尾に到達したら、録音を停止している。

この後、AudioQueueStart() を呼ぶと、録音は開始される。

OSStatus    err = AudioQueueStart(self.audioQueueObject, NULL);

次に再生の前準備を行うコードを説明する。

- (void)prepareAudioQueueForPlay
{
    AudioStreamBasicDescription audioFormat;
    audioFormat.mSampleRate         = 44100.0;
    audioFormat.mFormatID           = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags        = kLinearPCMFormatFlagIsSignedInteger
                                    | kLinearPCMFormatFlagIsPacked;
    audioFormat.mFramesPerPacket    = 1;
    audioFormat.mChannelsPerFrame   = 1;
    audioFormat.mBitsPerChannel     = 16;
    audioFormat.mBytesPerPacket     = 2;
    audioFormat.mBytesPerFrame      = 2;
    audioFormat.mReserved           = 0;
    
    AudioQueueNewOutput(&audioFormat, MyAudioQueueOutputCallback,
        self, NULL, NULL, 0, &__audioQueueObject);
    
    self.startingPacketCount = 0;
    AudioQueueBufferRef buffers[3];
    
    self.numPacketsToRead = 1024;
    UInt32  bufferByteSize = self.numPacketsToRead * audioFormat.mBytesPerPacket;
    
    int bufferIndex;
    for (bufferIndex = 0; bufferIndex < 3; bufferIndex++) {
        AudioQueueAllocateBuffer(self.audioQueueObject,
            bufferByteSize, &buffers[bufferIndex]);
        MyAudioQueueOutputCallback(self, self.audioQueueObject,
            buffers[bufferIndex]);
    }
}

録音したデータを再生するので、音声データの形式は録音と同じとなる。

AudioQueueNewOutput()関数で、再生するデータを設定するコールバック関数を登録する。

キューで管理するバッファの個数も録音と同じ3個だ。

次が、再生用のコールバック関数のコードだ。

static void MyAudioQueueOutputCallback(
    void                 *inUserData,
    AudioQueueRef        inAQ,
    AudioQueueBufferRef  inBuffer)
{
    AudioQueueServicesViewController    *viewController
        = (AudioQueueServicesViewController *)inUserData;
    [viewController readPackets:inBuffer];
    AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    
    if (viewController.maxPacketCount <= viewController.startingPacketCount) {
        viewController.startingPacketCount = 0;
    }
}

コードの流れは、録音用コールバック関数と似ているが、異なるのは再生位置がバッファの末尾に到達したら、再生位置を0、つまり先頭に戻している。この為、ループ再生されます。

この後、AudioQueueStart()を呼ぶと、再生は開始される。

OSStatus    err = AudioQueueStart(self.audioQueueObject, NULL);

デモ・アプリケーションでは、録音するデータは1個で、再生も1個。また、再生と録音は別々に行っているが、内部バファを複数持たせ、再生用コールバックで合成すれば、複数の音声データをずれなく再生できると思われる。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub

_ 関連情報

Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
iPhone Core Audioプログラミング(永野 哲久 著)
とれも参考にさせていただきました。

トップ «前の日記(2012-03-22) 最新 次の日記(2012-03-26)» 編集