こんにちは、ギャップラー小林です。
遅くなりましたが、新年明けましておめでとうございます。今年もマイペース更新になるかと思いますが、ギャップロをご愛顧いただきますようお願い申し上げます。
さて、前回のブログで紹介した社内ハッカソンについて、メンバーから「紹介があっさり過ぎる!頑張ったんだからちょっと解説させてくれ」とプッシュされましたので、勝手ながら本ブログにて各チームの成果物の解説をさせていただこうと思います!
今回は Leap Motion でテルミンを作ったチーム「立花早紀子」です。
立花早紀子では、Leap Motion で操作するテルミンを作成し、そこで演奏したデータを iPhone でエフェクトをかけながら再生するというシステムを開発しました。簡単な図にしたものが以下です。
要素技術としては以下のものを利用しました。
- Mac アプリ (通称: DJ. SAKIKO)
- LeapMotion SDK
- CoreImage: QR コード生成
- SpriteKit: ビジュアライザ
- AudioUnit+AudioToolBox: テルミン
- iOS アプリ (通称: OL 早紀子)
- AudioUnit+AudioToolBox: 演奏再現&イコライザ
- サーバ
- ConoHa: VPS
- Nginx+Unicorn: Web サーバ
- Ruby+Sinatra: REST-API
このうち、システムの要となったビジュアライザとオーディオの処理について紹介したいと思います。
ビジュアライザを作る – SpriteKit でビジュアライゼーション
ビジュアライゼーションの実装には SpriteKit を利用しました。まず結論から言ってしまうと、SpriteKit はビジュアライゼーションにはあまり向いていません!その理由は、SpriteKit には以下の機能が十分備わっていないためです。
- 線や図形を、毎フレーム形を変えながら描画する
- フレームバッファの内容を取得したり、マルチパスレンダリングを行う
はじめは SpriteKit が提供するパーティクル機能でいろいろな演出が作れると思ったのですが、上記のようなことが容易にできない…パーティクルだけでは演出が弱いので、独自実装を試みました。
独自に実装したのは「アキュムレーション・テクスチャ」の生成です。ビジュアライザーなどによく使われる、残像効果 を実現するためのものです。以下はこの残像効果をかけない場合とかけた場合のスクリーンショットですが、印象がだいぶ変わりますよね。
はじめに、アキュムレーション・テクスチャを表示するスプライトを用意します。このスプライトはシーン上の一番奥に表示させたいので、先頭に追加します。
CGSize size = self.skView.bounds.size; // スプライトの大きさ = ビューの大きさ self.accumlationSprite = [[SKSpriteNode alloc] initWithColor:[NSColor colorWithCalibratedWhite:0.0 alpha:0.0] size:size]; self.accumlationSprite.position = CGPointMake(size.width / 2, size.height / 2); self.accumlationSprite.color = [NSColor colorWithCalibratedRed:0.8 green:0.7 blue:0.95 alpha:1.0]; // 残像に掛けわせる色で、残像の残り具合や色味を調整 self.accumlationSprite.xScale = 1.03; // > 1.0 で残像が外に広がっていく self.accumlationSprite.yScale = 1.03; // 同上 self.accumlationSprite.zRotation = (2.0 / 360.0) * (2.0 * M_PI); // 残像にかける回転 self.accumlationSprite.colorBlendFactor = 1.0; [self.scene addChild:self.accumlationSprite];
さて問題はアキュムレーション・テクスチャの生成をどう行うかなのですが、先述のとおり SpriteKit には(調べた限りでは)フレームバッファのイメージをキャプチャする方法がありません。ただ SKView クラスには、オフスクリーン・レンダリングを行った結果をテクスチャとして返す textureFromNode:
というメソッドががありましたので、これを利用することにしました。以下のように同じシーンを使ってレンダリングを行うことで、アキュムレーション・テククチャに残像が「累積」するようになります。
self.accumlationSprite.texture = [self.skView textureFromNode:self.scene];
テクスチャの生成は毎フレーム行う必要があります。どのタイミングで行うかはいろいろな方法が考えられますが、今回は Leap Motion からのモーション検出コールバックのタイミングで行っています。この方法だとレンダリングのフレームレートと一致していないのですが、クオリティ面での問題は無かったので良しとしています。
さらに Leap Motion で取れた動きを zRotation や xScale, yScale に反映させることで、よりインタラクティブなものに仕上げています。
この方法の問題点は、同じシーンを 2 度レンダリングしていることです。一度のレンダリングでできるようになれば、大幅に性能を改善できるでしょう。
冒頭 SpriteKit はビジュアライゼーションに向いてない!と言いましたが、その気になればそれっぽいものは作れる とは思います。物理演算や CoreImage などを活用すれば、ひと味違ったモノが生み出せるかも…引き続きチャレンジしていきます!
テルミンを作る – Core Audioでの波形生成
Audio Unitは、iOS の Core Audio が持っているプラグインの規格です。何かしらのオーディオ処理を行う「ユニット」を複数繋げていくことで、オーディオ処理を拡張し、複雑なオーディオ処理を行うことができます。
ユニットが行うオーディオ処理には、以下のようなものがあります。
- 音源の入出力
- フォーマット変換
- ミキシング
- エフェクト
- etc…
立花早紀子では、手の動きに合わせて音量、音高、音色を変化させて音を鳴らしています。 任意の波形を生成する、いわゆるシンセサイザの実装方法について説明したいと思います。
例えば、サンプリングレート 44,100 Hzならば、1秒間に 44,100フレーム分の音量を設定することで、波形を書き込むことになります。 どういうタイミングで書き込むのかというと、1秒間に数十回、OS側から「ちょっとだけ波形書き込んで」という命令が来るので、これに応答することで波形をちょっとずつ作って行く形になります。この辺りの詳細は後程。
波形を書き込む下準備
「波形を書き込む機講」もまた、AUGraphの一部として接続される AUNode となります。 波形生成機講用のAUNodeの作成と初期化は、大まかに以下の流れになります。
- AudioComponentDescription構造体に コンポーネントの種類などの設定をセットする
- 上記で作成した設定情報を指定して、AudioUnitを生成
- AudioStreamBasicDescription構造体に、波形生成に関する設定をセットし、AudioUnitに適用
- 実際に波形書き込み処理を行うコールバック関数を、AudioUnitに登録する
順に説明していきます。
AudioUnitの情報をセット
まずは AudioComponentDescription 構造体に、AudioUnitの設定情報をセットしていきます。
OSStatus status; AudioComponentDescription audioCompDesc; #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) // iOS用 audioCompDesc.componentType = kAudioUnitType_Output; audioCompDesc.componentSubType = kAudioUnitSubType_DefaultOutput; #elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) // OSX用 audioCompDesc.componentType = kAudioUnitType_Mixer; audioCompDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer; #endif audioCompDesc.componentManufacturer = kAudioUnitManufacturer_Apple; audioCompDesc.componentFlags = 0; audioCompDesc.componentFlagsMask = 0;
プリプロセッサで分岐していることから分かる通り、実は、iOSとMac OSXで、設定すべき値が異なります。
iOS | OSX | |
---|---|---|
componentType | kAudioUnitType_Output | kAudioUnitType_Mixer |
componentSubType | kAudioUnitSubType_DefaultOutput | kAudioUnitSubType_MultiChannelMixer |
続いて AUNode、AudioUnit を生成します。
AudioUnit audioUnit; AUNode audioNode; AUGraph audioGraph; // AUGraphに登録しつつ AUNodeを生成 AUGraphAddNode(audioGraph, &audioCompDesc, &audioNode); // AUGraphと生成したAUNodeを指定して AudioUnitを取得 AUGraphNodeInfo(audioGraph, audioNode, NULL, &audioUnit);
若干手順が複雑なのですが、AUGraph に追加することで AUNodeが生成され、またそれらからAudioUnitが取得できます。
フィルタなどを適用するために Audio Graphに追加する必要があるのですが、一方で諸々の設定は AudioUnitインスタンスに対して行うため、このような流れになります。
Audio Unitの細かい設定
次に波形生成機講としてのデータフォーマットなどの設定を行います。
AudioStreamBasicDescription streamDesc; streamDesc.mSampleRate = 44100; streamDesc.mFormatID = kAudioFormatLinearPCM; streamDesc.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved; // 非インターブリード設定(ステレオの左右バッファが独立) streamDesc.mChannelsPerFrame = 2; streamDesc.mFramesPerPacket = 1; streamDesc.mBitsPerChannel = sizeof(Float32) * 8; // 非インターブリードなので(インターブリードの場合はチャンネル分で2倍する必要あり) streamDesc.mBytesPerFrame = sizeof(Float32); streamDesc.mBytesPerPacket = streamDesc.mBytesPerFrame * streamDesc.mFramesPerPacket; streamDesc.mReserved = 0; AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &streamDesc, sizeof(streamDesc));
一口に「波形を書き込む」と言っても、そもそもどういうフォーマットなのかを決めておかないと、想定した通りの再生になりません。実はiOS、Mac OSXでも、OSバージョンによって このフォーマットのデフォルト設定が異なったりするのですが、明示的に指定すれば問題ないです。
フォーマットは mFormatFlags にフラグのビット和を指定することで設定します。上記の例では、
- 1フレームは float型で表現する
- 非インターブリードである
という設定にしています。インターブリードに設定すると、ステレオ2チャンネルの場合、左チャンネルのフレームと右チャンネルのフレームが交互に並ぶことになります。
非インターブリードに設定すると、左チャンネル用のバッファと右チャンネル用のバッファが別々になります。今回は書き込み処理のし易さという観点から、非インターブリードに設定しています。
その他、チャンネル数、1チャンネル辺りのビット数、1フレーム辺りのバイト数などを設定しますが、チャンネル数とインターブリード/非インターブリードを決めてしまえば、あとはほぼ自動的に決まります。
コールバック設定
最後に、実際に波形を書き込む関数をコールバック関数として登録します。コールバック関数のインターフェースは決まっており、以下のようなものになっています。
OSStatus RenderCallback( void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData );
というわけで、このインターフェースに従った関数を作った上で、以下のようにして登録します。
AURenderCallbackStruct renderCallback; renderCallback.inputProc = RenderCallback; renderCallback.inputProcRefCon = NULL; AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &renderCallback, sizeof(renderCallback));
ここで、inputProcRefCon に設定する値は、何でも良いです。設定したものがそのまま、コールバック関数の引数 *inRefCon に設定されます。
コールバック関数は C言語の関数であり、Objective-Cのメソッドではないので、self レシーバなどを参照できません。しかし、inputProcRefCon に self を渡しておくことで、コールバック関数内からも self の参照が可能になります。
self の渡し方と受け取り方は、以下のようになります。
renderCallback.inputProcRefCon = (__bridge void *)self;
OSStatus RenderCallback( void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData){ @autoreleasepool { id self = (__bridge id)inRefCon; // 以降、self レシーバに対してメッセージ送信可能 } }
__bridge キャストを忘れずに!
波形を書き込む
ここまででようやく準備が整いました。以降は、先程登録したコールバック関数が頻繁に呼ばれますので、引数で渡されるバッファに波形を書き込んで行くことになります。
OSStatus RenderCallback( void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { // ステレオ2ちゃんねるの それぞれのバッファポインタ float *bufferL = (float*)(ioData->mBuffers[0].mData); float *bufferR = (float*)(ioData->mBuffers[1].mData); // 書き込む for (int i = 0; i < inNumberFrames; i++) { bufferL[i] = XXX; bufferR[i] = XXX; } }
ステレオ2チャンネルで 非インターブリード設定の場合は、左右それぞれのチャンネル用のバッファは、上記のように取得できます。引数で渡る inNumberFrames が「今回書いて欲しいフレーム数」になりますので、要求された分だけ書き込みましょう。
また、「どれだけの長さを書き込めばいいのか?」が渡る反面、「どの部分から(何秒目から?)書けばいいのか?」という情報は渡りません。ですので、inNumberFrames の値を累加できるような変数を用意しておきましょう。
フォーマットをfloat型にしている場合は、1.0 〜 -1.0 の範囲で音量を書き込むことになります。試しに、sinf 関数の結果を書き込んでみると、正弦波が鳴ると思います。
余談ですが、1.0 〜 -1.0の範囲と書きましたが、それを超えた値を書き込んでも上限に丸められたりはせず、普通に大きな音が鳴ります。PCの音量設定を小さくしていても、それを超えた大きさの音が鳴るので注意しましょう。開発中、計算式を間違えて、うっかりイヤフォンから爆音を鳴らしてしまったのはいい思い出です…。
また小ネタですが、生成する波形のパターンが固定的なら、予め「1周期分」の波形データをテーブリングしておきましょう。決して重い処理でなくても、秒間数万回呼ばれる処理ですので、その方が負荷を軽減できます。テーブルのインデックスを飛ばして参照することで、周波数は変えることができます。
イコライザを作る – Audio Unit で音をアレンジする
今回の早紀子アプリでは、OS X / iOS 共通の音源生成用ユニットを用いていますが、iOS側ではさらにエフェクト用のユニットを複数繋げて、音源に様々なエフェクトをかけられるようにしています。
AUGraph
複数のユニットをつなげるためには「Audio Unit Processing Graph Services」(AUGraph)と呼ばれる機能を利用します。
AUGraphは、音源に対してエフェクトをかけて出力する、といった一連のオーディオシグナルの流れの接続を管理します。
AUNode
接続する各種ユニットは、AUNodeという形式で取り扱います。さまざまな処理を担う複数のAUNodeを用意し、AUGraphを使って繋いでいく、ということになります。
繋いで音を出す
AUNodeは、以下のようにして生成し、AUGraphを利用して接続します。接続が完了したら、AUGraphを開始することで、音が鳴る仕組みとなっています。
今回利用するのは、以下のようなユニットです。
- オーディオファイルを音源の入力として利用するAUNode
- サウンド出力用のAUNode
// AUGraphを定義. AUGraph graph; // AUNodeを定義. AUNode fileNode; AUNode outputNode; ・・・ // AUGraphを作る. NewAUGraph(&graph); // AudioFilePlayerを利用した、入力用のAUNodeを作る. AudioComponentDescription acd; acd.componentManufacturer = kAudioUnitManufacturer_Apple; acd.componentFlags = 0; acd.componentFlagsMask = 0; acd.componentType = kAudioUnitType_Generator; acd.componentSubType = kAudioUnitSubType_AudioFilePlayer; AUGraphAddNode(graph, &acd, &fileNode); // Remote IO を利用した、出力用のAUNodeを作る. acd.componentType = kAudioUnitType_Output; acd.componentSubType = kAudioUnitSubType_DefaultOutput; AUGraphNewNode(graph, &acd, &outputNode); // 入力用AUNodeと出力用AUNodeを繋ぐ. AUGraphConnectNodeInput(graph, fileNode, 0, outputNode, 1); // AUGraphの開始 AUGraphOpen(graph); AUGraphInitialize(graph); AUGraphStart (graph);
エフェクトをかけてみる
上記の例では、入力用のユニットと出力用のユニットを直接接続しましたが、この間にエフェクト用のユニットを接続することで、音源にエフェクトをかけることができます。
今回は上記の例に対して、リバーブのエフェクトを追加してみます。
// AUGraphを定義. AUGraph graph; // AUNodeを定義. AUNode fileNode; AUNode outputNode; AUNode reverbNode; // <-追加 ・・・ // AUGraphを作る. NewAUGraph(&graph); // AudioFilePlayerを利用した、入力用のAUNodeを作る. AudioComponentDescription acd; acd.componentManufacturer = kAudioUnitManufacturer_Apple; acd.componentFlags = 0; acd.componentFlagsMask = 0; acd.componentType = kAudioUnitType_Generator; acd.componentSubType = kAudioUnitSubType_AudioFilePlayer; AUGraphAddNode(graph, &acd, &fileNode); // Remote IO を利用した、出力用のAUNodeを作る. acd.componentType = kAudioUnitType_Output; acd.componentSubType = kAudioUnitSubType_DefaultOutput; AUGraphAddNode(graph, &acd, &outputNode); // ↓↓↓追加 // リバーブ用のAUNodeを作る. acd.componentType = kAudioUnitType_Effect; acd.componentSubType = kAudioUnitSubType_Reverb2; AUGraphAddNode(graph, &acd, &reverbNode); // ↓↓↓削除 //// 入力用AUNodeと出力用AUNodeを繋ぐ. //AUGraphConnectNodeInput(graph, fileNode, 0, outputNode, 1); // ↓↓↓追加 // 入力用AUNodeとリバーブ用AUNodeを繋いで、リバーブ用AUNodeと出力用AUNodeを繋ぐ. AUGraphConnectNodeInput(graph, fileNode, 0, reverbNode, 1); AUGraphConnectNodeInput(graph, reverbNode, 0, outputNode, 1); // AUGraphの開始 AUGraphOpen(graph); AUGraphInitialize(graph); AUGraphStart (graph);
AUNodeが一つ増えて、接続処理も二つに増えています。これで、音源にリバーブをかけた状態で再生することができるようになります。
同じように、入力と出力の間にエフェクターのAUNodeを複数繋げていくことで、様々なエフェクトをかけていくことができます。
エフェクトのパラメータをいじる
リバーブのかかり具合やミキサーのボリューム調整などのように、各ユニットにはさまざまなパラメータ調整を行うことができます。
各パラメータを変更するには、AudioUnitを利用する必要があります。AudioUnitは、AUGraphとAUNodeを利用して、以下のように生成します。
AUGraph graph; AUNode node; AudioUnit unit; // 中略... // AudioUnitを生成する. AUGraphNodeInfo(graph, node, NULL, &unit);
生成されたAudioUnitに対して、各種パラメータを設定します。設定できるパラメータは、AUNodeの内容によって異なります。ミキサーのAUNodeなら音量、ディストーションエフェクトのAUNodeであればゲイン、などなど様々です。
今回は、リバーブのAUNodeのDryWetMixを調整してみます。このパラメータは、エフェクトがかかった音とオリジナルの音の比率を設定するパラメータになります。
// AUGraphを定義. AUGraph graph; // AUNodeを定義. AUNode fileNode; AUNode outputNode; AUNode reverbNode; AudioUnit reverbUnit; // <-追加 ・・・ // AUGraphを作る. NewAUGraph(&graph); // AudioFilePlayerを利用した、入力用のAUNodeを作る. AudioComponentDescription acd; acd.componentManufacturer = kAudioUnitManufacturer_Apple; acd.componentFlags = 0; acd.componentFlagsMask = 0; acd.componentType = kAudioUnitType_Generator; acd.componentSubType = kAudioUnitSubType_AudioFilePlayer; AUGraphAddNode(graph, &acd, &fileNode); // Remote IO を利用した、出力用のAUNodeを作る. acd.componentType = kAudioUnitType_Output; acd.componentSubType = kAudioUnitSubType_DefaultOutput; AUGraphAddNode(graph, &acd, &outputNode); // リバーブ用のAUNodeを作る. acd.componentType = kAudioUnitType_Effect; acd.componentSubType = kAudioUnitSubType_Reverb2; AUGraphAddNode(graph, &acd, &reverbNode); // ↓↓↓追加 // リバーブ用のAudioUnitを作る. AUGraphNodeInfo(graph, reverbNode, NULL, &reverbUnit); // ↓↓↓追加 // リバーブのDryWetMixを調整する. AudioUnitSetParameter(reverbUnit, // <- 設定対象のAudioUnit. kReverb2Param_DryWetMix, // <- 設定したい項目. kAudioUnitScope_Global, 0, 50, // <- 設定するパラメータ. 0); // 入力用AUNodeとリバーブ用AUNodeを繋いで、リバーブ用AUNodeと出力用AUNodeを繋ぐ. AUGraphConnectNodeInput(graph, fileNode, 0, reverbNode, 1); AUGraphConnectNodeInput(graph, reverbNode, 0, outputNode, 1); // AUGraphの開始 AUGraphOpen(graph); AUGraphInitialize(graph); AUGraphStart (graph);
このように、AUGraphとリバーブ用のAUNodeから、リバーブ用のAudioUnitを生成し、そのAudioUnitに対して任意の項目を指定し、任意のパラメータを設定することで、エフェクトのかかり具合を調整することができます。この値は動的に変更することが可能ですので、UISliderなどで値を調整し、スライドさせることでパラメータを切り替えることなどもできます。
AUNodeやAudioUnitの設定可能項目はたくさんありますので、色々と試して遊んでみましょう!