iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
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個。また、再生と録音は別々に行っているが、内部バファを複数持たせ、再生用コールバックで合成すれば、複数の音声データをずれなく再生できると思われる。