ワープとテンポ同期
90 BPM(beats per minute、1 分あたりの拍数)で録ったドラムループを 120 BPM のプロジェクトに置くと、かみ合いません。グリッドのほうが速く進むのに、ループは自分のテンポで鳴り続けます。ワープ は、オーディオクリップをプロジェクトのテンポに合わせて曲げ、全体をグリッドに同期させる機能です。
ひとことで言うと
ワープ = オーディオクリップの速度や長さを(タイムストレッチまたはバリスピードで)変え、自分の元テンポではなくプロジェクトのテンポに追従させること。
テンポマップ: プロジェクトのグリッド
ワープを理解する前に、クリップが追従する 対象 が必要です。それがプロジェクトの テンポマップ です。
テンポマップは テンポセグメント のリストです。各セグメントは「タイムラインのこの地点以降、テンポは何 BPM」を表します。曲は単一の一定テンポでも、複数のセグメント(遅いイントロ、速い A メロ、最後のリタルダンドなど)でもかまいません。セグメントは ランプ することもでき、BPM を急に切り替えるのではなく滑らかに変化させられます。
テンポマップと並んで 拍子(4/4、3/4、6/8 …)が、1 小節に何拍入るかを定めます。テンポマップと拍子が合わさって 音楽的なグリッド を作ります。これは、クリップとトラック で扱ったすべてのクリップ位置が基準とする、小節と拍の定規です。
グリッドはクリップがワープする 対象 なので、テンポが音楽的な位置を秒へどう変換するかを見ておくと理解が進みます。下のテンポをドラッグすると、固定した秒の定規に対して小節が詰まったり広がったりします。クリップが追従するのは、まさにこの対応関係です。
ワープモード
すべてのオーディオクリップは ワープモード を持ちます。ワープモードは、そのクリップがグリッドに どのように 追従するか、そもそも追従するかどうかを決めます。
off / repitch / tempo-sync / time-stretch の違い
- off: 伸縮なし。クリップは元の速度と音程のまま、プロジェクトのテンポを無視して再生します。曲げたくないワンショットや効果音に使います。
- repitch: テープの速度を変えるような挙動。ワープマップに合わせてクリップを速く / 遅くし、音程も一緒に動きます。速ければ高く、遅ければ低くなります。シンプルで CPU が軽く、あの懐かしいバリスピードの質感が 欲しいときに音楽的に有効です。
- tempo-sync: 元の音程を保ったままテンポに追従。タイミングがグリッドに合うようクリップを時間伸縮しつつ、フェーズボコーダ(周波数領域で音声を伸縮するアルゴリズム)が音程を一定に保ちます。ボーカルやメロディックなループなど、ノートが音程を保たねばならないものに使います。
- time-stretch: こちらも元の音程を保ちますが、伸縮をオーディオスレッド上で生成します。repitch と 同じアンカーマップを読み、ソースの断片を一定のレートでオーバーラップ加算して出力を合成するため、マップを変えるとタイミングだけが動き、音程はそのまま残ります。ユーザーがテンポやアンカーをドラッグしていて、結果がすぐ聞こえてほしい場面に向きます。
| モード | タイミングがワープマップに追従? | 音程は同じまま? | 主な用途 |
|---|---|---|---|
| off | いいえ | はい | ワンショット、SFX |
| repitch | はい | いいえ(速度に連動) | テープ / バリスピードの質感、ドラム |
| tempo-sync | はい | はい | ボーカル、メロディックなループ |
| time-stretch | はい | はい | テンポ / アンカーのライブ編集 |
表の違いは、1つのクリップを同じレートで各モードに通すと聴き取れます。音程が動くのは repitch だけです。off は元の速さのまま鳴り、tempo-sync と time-stretch は同じ長さになって音程を保ちます。
time-stretch と tempo-sync の違い
どちらも音程を保つので、選択の基準は音そのものではありません。伸縮の処理がいつ走るか、そしてそれがホストに何を要求するかです。
tempo-sync は再生に先立って、コントロールスレッド上で伸縮後の音声をベイクします。フェーズボコーダがクリップ全体を見渡せる代わりに、アンカーを更新するたびにベイクをやり直すことになります。またワープマップをまったく持たない tempo-sync クリップは、黙ってフォールバックするのではなく compile() のエラーになります。time-stretch はオーディオスレッド上でブロックごとに処理するため、新しいアンカーは再ベイクなしで次のブロックから反映されます。
この即応性を支えるのが、事前に確保したボイスです。容量は既定で 8 本 で、各ボイスは 2 チャンネル まで扱います。コントロールスレッドから setWarpVoiceCapacity(voices) と warpVoiceCapacity() で容量を設定・確認できます。Python は set_warp_voice_capacity() と warp_voice_capacity()、C は sonare_engine_set_warp_voice_capacity() と sonare_engine_warp_voice_capacity() を使います。範囲は 0..64 で、64 を超える値は拒否されます。容量 0 は time-stretch を無効にし、対象のクリップは repitch を使います。この場合は warpStretchOverflowCount() に加算されません。準備済みのエンジンで容量を変えるとボイスプールを直ちに作り直し、使用中のクリップは WSOLA(波形類似オーバーラップ加算。各フレームを、直前のフレームの波形が最もよく続く位置に置く方式)の状態を引き継がずに再開します。割り当てとフォールバックの規則は リアルタイムエンジン にあります。
フォールバックは通知なしに起きます
フォールバックしてもエラーや警告は出ません。すべてのボイスが使用中で確保できなかった場合だけ、warpStretchOverflowCount() がフォールバックしたブロック数を数えます。容量 0 の意図的な無効化はカウンターに加算されません。ソースが 3 チャンネル以上の場合もストレッチャの状態に収まらないため repitch を使いますが、ボイス容量のオーバーフローには数えません。開発中はコントロールスレッドからこの値を監視してください。バインディングごとの名前は リアルタイムエンジン にあります。
time-stretch が行うこと、そのコスト
tempo-sync は周波数領域で伸縮します。フェーズボコーダがクリップを短く重なり合うスペクトル(STFT フレーム)に切り分け、各周波数ビンの位相をそろえて進めながら時間方向の間隔を並べ直し、足し合わせて戻します。time-stretch は時間領域にとどまります。ソースを 1024 サンプルの重なり合うフレームに切り、512 サンプルの固定ホップで並べて(重なった端を足し合わせるのがオーバーラップ加算です)出力が常に同じ量だけ進むようにし、各フレームを ソースのどこから 切り出すかをワープマップに従って決めます。速く再生するとは、切り出し位置が出力より速くソースを進むことで、フレームが飛ばされます。遅ければ繰り返されます。各フレームはソースをそのまま切り出したものなので、波形自身の周期、つまり音程には手が付きません。
任意の切り出しをただ並べると、重なった 2 つのフレームの位相が食い違う場所でクリックや打ち消しが起きます。そこが WSOLA の出番です。フレームを切り出す前に、ストレッチャはマップが指す位置の前後 ±512 サンプルを探索し、直前に出力したフレームを最もよく引き継ぐオフセットを選びます。これで連続するフレームは位相のそろった状態で重なり、加算結果がきれいに保たれます。探索は相関計算で、粗く探してから絞り込みます。これがフレームごとのコストのすべてで、オーディオスレッド上での有限回の積和だけです。変換もメモリ確保もありません。
この設計が、コストと音を決めます。
- 設定できるボイス予算。 ストレッチャの状態は事前に確保され、オーディオスレッドはメモリを確保しません。容量とオーバーフローの扱いは上のボイス予算にあります。
- トランジェントは残り、密なテクスチャは二重になりうる。 切り出しはソースそのものなので、ドラムのヒットはアタックの形を保ちます。例外は大きな伸縮です。大きく遅くするとアタックを含む切り出しが繰り返され、打音が二重に聞こえ(フラム)、大きく速くするとアタックが飛ばされることがあります。持続的でポリフォニックな素材(パッド、コーラスなど)を大きく伸縮すると、すべてのノートを同時にそろえられるオフセットが存在せず、繰り返しや飛ばしが起きて、フェーズボコーダのにじみとは違う、かすかな二重化やフラッターとして現れます。
- 約 10 ms の粒度。 位置は探索半径の範囲で守られます。各フレームはマップが指す位置から約 10 ms 以内に置かれるため、テンポ追従では気づきませんが、サンプル精度の編集を合わせる場面では効いてきます。
time-stretch を選ぶのは、マップが 再生中に変わる ときです。たとえば、ユーザーがテンポをドラッグしている、アンカーを動かしている、グルーヴを試聴している場面が当てはまります。次のブロックが新しいマップを読むだけで済みます。生のエンジンでは、ページ供給されるクリップに使える音程保持モードでもあります。tempo-sync はベイクするため、ページ供給には対応できません。マップが確定していて素材が持続的・和声的なら tempo-sync を選びます。ボコーダがクリップ全体を見渡せるぶん、伸縮はきれいになります。
ワープアンカー: オーディオをグリッドへ留める
テンポの変化はクリップに どれだけ速く 再生するかを伝えますが、実際の演奏は一様ではありません。ドラマーは前後にゆらし、フレーズはわずかに走ります。ワープアンカー は、オーディオ内の特定の点をタイムライン上の特定の点に留めさせてくれます。
各アンカーはソースオーディオ内の位置をプロジェクトのタイムライン上の位置に結び付けます。ループの頭が 1 小節目に、スネアの一打が 2 拍目にスナップし、アンカー間の伸縮は自動で計算されます。アンカーを十分に置けば、緩い演奏をグリッドへまっすぐにそろえたり、タイトな演奏を意図的にゆらしてグルーヴを出したりできます。
- 引き伸ばされる
- 縮められる
- ワープアンカー
クリップは、渡されたマップが示すぶんだけグリッドに寄ります。音楽上の位置からアンカーを計算する方法は秒・サンプル・PPQ の換算を参照してください。
アンカーを持つのはアプリ側
クリップが追従するのはワープマップで、テンポマップに直接追従するわけではありません。マップのアンカーはサンプル位置どうしの絶対的な対応で、アプリがテンポから計算して渡します。エンジンはアンカーを読むだけで、自分で作り出すことはありません。後からテンポマップを編集してもアンカーは書き換わらないので、ユーザーがテンポを変えられるホストアプリは、アンカーを計算し直して新しいワープマップを渡す必要があります。setWarpMap はアンカーが 2 個未満のマップを拒否します。ワープマップを持たない repitch や time-stretch のクリップはネイティブ速度で再生され、持たない tempo-sync クリップはコンパイルに失敗します。
tempo-sync は限界のある本物の時間伸縮
tempo-sync は音程を保ったまま長さを変えるため、内部でフェーズボコーダを使います。これは フェーズボコーダによるストレッチ で説明しているアルゴリズムと同じ系統です。小さな伸縮は透明ですが、非常に大きな伸縮はトランジェントをにじませたり位相がずれたような(フェイジーな)質感を加えたりします。ステレオやマルチチャンネルのクリップでは、すべてのチャンネルが共通の位相の進み方に従うため、左右のステレオイメージがずれることはありません。repitch にはこうしたアーティファクトはありませんが、音程が動きます。またソースを線形補間で読み、アンチエイリアスフィルターを掛けないので、大きく速めると明るい素材でエイリアシング(高い周波数が偽の低い音として折り返す現象)が出ることがあります。2 つのモードでは、引き換えにするものが違います。
tempo-sync は、隣り合う 2 つのアンカーの間をそれぞれ別の処理で伸縮し、区間どうしを短いブレンドでつなぐので、アンカーごとにボコーダーの位相が仕切り直されます。持続する音程感のある素材に間隔の詰まったアンカーを多数置くと、つなぎ目が聞こえることがあります。アンカーはなるべくアタックに置いてください(ソースのサンプルレートがプロジェクトと異なるオーディオは、コンパイル時に帯域制限付きのリサンプラーで一度だけ変換されるので、この変換ではエイリアシングは起きません)。
再生とバウンスの一貫性
地味ですが重要な保証があります。クリップは、ライブで試聴していてもファイナルをレンダリングしていても、同じように ワープします。テンポマップ、ワープモード、アンカーはプロジェクトの編集モデルの一部なので、リアルタイム再生とオフラインの バウンス でタイミングと音程が一致します。編集中に聞こえる音が、そのままレンダリング結果になります。
libsonare での実装
ワープは Project の編集モデルにあります。クリップのモードは setClipWarpMode(clipId, mode) で設定し、mode は 'off' | 'repitch' | 'tempo-sync' | 'time-stretch' のいずれかです。ワープアンカーは、クリップとは独立したワープマップとして管理されます。setWarpMap({ id, name?, anchors }) で各アンカーを { warpSample, sourceSample } として登録し、クリップは setClipWarpRef(clipId, warpRefId) で参照します(removeWarpMap でクリア)。グリッドは setTempoSegments(各 { startPpq, bpm, endBpm? }、endBpm がランプを駆動)と setTimeSignatures(各 { startPpq, numerator, denominator })で決まります。tempo-sync では伸縮を StreamingPhaseVocoder が行って音程を保ち、リアルタイム再生とオフライン bounce() の 両方 で同じ経路を使います。一方 repitch はリサンプリングし、音程を速度に連動させます。time-stretch は repitch と同じマップを読みつつ、オーディオスレッド上でソースの断片をオーバーラップ加算します。リサンプリングへフォールバックしたブロック数は warpStretchOverflowCount() が報告します。ワープモード・アンカー・テンポ・拍子のすべては toJson() / Project.fromJson(json) で JSON を往復します。