Resonata は、中に録音が一つも入っていないピアノです。聞こえる音はすべて、フェルトのハンマー、1本から3本の鋼鉄の弦、木の響板、そして部屋から、その場で計算して作られます。プラグインとしても、単体のプレーヤーとしても動作し、無料のオープンソースです。
これは、それをまともな音にするまでの最初の数日間の話です。ほとんどが、自分が逆にしていたことと、それを教えてくれたテストや測定の話になります。数字は残しておきました。あとで何かを壊していないか確かめたくなるはずだからです。
鍵盤を押すと何が起こるか
それぞれの鍵には小さな弦のグループがあります。低音域では1本、中音域では2本、高音域では3本です。ハンマーが短いパルスをそこへ送り込みます。弦はすべて駒で出会い、そこで互いに影響し合い、駒が響板を駆動します。少し離れたところでは、別の弦が共鳴の出番を待っています。その後はただの部屋と、オンにしたエフェクトだけです。
どれも新しいことではありません。Julius Smith、Balázs Bank、Vesa Välimäki たちが、私が登場するずっと前に解き明かしたことで、参考文献は末尾にあります。私に付け加えられるのは、間違え方の一覧くらいです。
弦は遅延線である
打たれた弦とは、弦の上を行ったり来たりする波です。コンピューターの中では、1往復ぶんの長さの遅延線になり、ループの中のフィルターが一周ごとに少しずつエネルギーを失わせます。音の高さは、全体の遅延で決まります。
\[ f_0 = \frac{f_s}{D} \]
ここで \(f_s\) はサンプルレート、 \(D\) はフィルターが加える遅延も含めたループ全体の遅延(サンプル数)です。つまずいたのは最後の部分でした。私はフィルターの分として、1.5サンプルをいい加減に引いていました。実際の分散カスケードの遅延は約7サンプルでした。C4 でピアノは55セント低く鳴っていて、これは半音の半分以上です。それに気づいたのは、フィッティングツールが教えてくれたからでした。解決策は、各フィルターの正確な位相遅延を計算し、小数部分は二分法で見つけさせることでした。(単純な反復は2つの答えの間を行き来するだけで、その上に小数項を足すと二重に数えてしまい、さらに5セントずれます。どちらも試しました。)チューニングのテストは現在、C2、C4、C7 を5セント以内に収めています。
実際の弦は倍音的ではない
硬い弦は、部分音を上がるにつれてわずかに高く鳴ります。
\[ f_n = n f_0 \sqrt{1 + B n^2} \]
\(B\) は非調和性係数で、音の高さとともに 上がり ます。私のものは逆に動いていて、低音で大きく、高音ではほとんどゼロでした。その結果、C4 の部分音は純粋な倍音列より4〜9セント低くなりましたが、硬い弦ではそんなことは起こりません。一方 C7 はほぼ正弦波でした。曲線を正しい向きに直し、さらに分散フィルターをループの実際の位相に合わせて較正すると、C4 の部分音は理論どおり2〜12セント高くなりました。
外部のレビュアーが、ひと目でこれを見つけてくれました。わかってしまえば明らかなことです。実際の高音は、部分音が聞こえるほど引き伸ばされていますが、ほぼ純粋な正弦波の C7 にはそれが全くありません。
決して合わないピアノを調律する
コンサートピアノは平均律には調律されません。低音は低めに、高音は高めに調律され、非調和な部分音との間を取ります。これはストレッチと呼ばれ、調律師は Railsback が記述した曲線に従います。
私は1つの係数を両方向に使ってしまい、C1 が -41 セント、C2 が -28 セントになりました。モデルは自分自身の曲線にはぴったり一致しており、これは特に腹立たしい種類のバグです。弦は正しく、曲線が間違っていたのです。本物の Railsback 曲線の両端のずれは同じくらいの大きさで、およそ25〜30セントですが、A4 より上の半音は下よりずっと少ないので、高音側は半音あたりの傾きが急になります。そのため係数は、A4 より下で0.20、上で0.35になりました。C1 は -23.9 セント、C7 は +21.3 セントです。
全オクターブを対象にした同じチェックで、ピアノの最高音域が長く鳴りすぎていることもわかりました。C6 は2.53秒、C7 は1.77秒続き、私は0.5秒から1秒に近い値を望んでいました。減衰の下限を1.2秒から0.9秒に下げると、それぞれ2.08秒と1.41秒になりました。近づきましたが、まだ信用していません。本物の楽器と比較することが、私のリストに載っています。
ハンマーが当たる場所
励振の最初のバージョンは、その場しのぎの寄せ集めでした。どれもほかの何かを壊し、大きな音で失敗する回帰テストだけが、それに気づく唯一の方法でした。
最初は、短いハンマーのバーストをループ全体に引き伸ばしました。26サンプルのバーストと1381サンプルの低音ループでは、約50倍も遅く再生することになり、スペクトルは500 Hz 以下で崩れて、強い音ほど暗くなってしまいました。これはきっぱりやめました。次に、バーストを空のループに落とし込みましたが、これだと波が一周して戻ってくるまで出力がデジタルのゼロのままになります。C1 では、すべての音の頭に約31ミリ秒(44.1 kHz のとき)の完全な無音が入り、それは聞こえます。さらに、大きな音の問題がありました。フォルティッシモのホワイトなバーストは一定のエネルギーを22 kHz にわたって広げますが、ループは1 kHz 未満しか保持しないので、大きな音のほうが中くらいの音より小さくなってしまったのです。
生き残ったバージョンは、駒が弦を読み取る位置より弦の長さの1/16だけ手前の点に、ハンマーの力のパルスを注入します。そのため C1 でも最初の音は2ミリ秒未満で届きます。あらかじめ詰めておくノイズはありません。
ハンマーが当たる場所は、もう一つの意味でも重要です。弦の \(1/8\) の位置を打つハンマーは、8番目ごとの部分音をほぼ無音にします。それらの部分音はそこに節を持つからです。これを実現するコムフィルターは、最初の試みでは偽物でした。信号を、1サンプル遅らせた自分自身のコピーと平均していましたが、それは穏やかなローパスであり、ノッチではありません。現在は本物のフィードフォワード・コムフィルターで、部分音8、16、24にくぼみがあり、その間にうつろなピークがあります。
ハンマーは年齢を偽っていた
ハンマーのフェルトは数ミリ秒のあいだ弦に触れており、その長さがどの部分音が励振されるかを決めます。接触が短ければ、高音は明るくなります。私のものは、どの音高でも約1.7ミリ秒でした。これはどこでもほぼ同じカットオフのローパスフィルターということで、3番目から上の高音域の部分音は、フラットなノイズで駆動されていました。さらに硬さの法則が逆で、硬いハンマーほど 長く 鳴っていました。
現在は、接触時間がオクターブごとに \(2^{-1/2}\) 倍ずつ短くなります。中程度のベロシティ(0.6)で、硬さが標準のとき、C1 で約4.8ミリ秒、C7 で0.59ミリ秒です。法則全体は Rust で5行です。
let v = velocity.clamp(0.0, 1.0); let h = self.hardness.clamp(0.25, 4.0).sqrt(); let base_ms = 3.0 - 2.2 * v; // 強く叩くほど接触は短い let register = 2f32.powf(-(f32::from(midi) - 60.0) / 24.0); let ms = (base_ms / h * register).clamp(0.25, 6.0);
\(\sqrt{h}\) で割ることが、修正のもう半分です。古い法則は硬さの扱いが逆だったので、硬いハンマーほど長く鳴っていたのです。
パルスの形には3回かかりました。ハーバーサインはスペクトルにヌルがあり、部分音をまるごと食べてしまいました。ガウス関数は鈍すぎました。残したのは \(u e^{1-u}\) に末尾の短いフェードを付けたもので、可聴帯域の中にヌルがありません。
高音域では、ループフィルターも入れ替える必要がありました。現在は1極フィルターで、音ごとに解かれ、部分音8が部分音1の約3倍の速さで消えるようにしながら、基音は表にある減衰時間にきちんと到達します。テストでは、C4、C6、C7 で基音が0.2 % 以内、比が5 % 以内に収まることを確認しています。
意見が合わない2本の弦
2本か3本の弦で音を鳴らすと、減衰は1つになりません。弦が一緒に動くあいだは素早く落ち、その後、弦が離れて駒で打ち消し合い、駒がほとんど動かなくなると、長く静かな尾が続きます。ピアニストはこの尾をアフターサウンドと呼びます。
最初の試みは、勉強になる形で間違っていました。弦に、自分の平均の一部を自分から引かせたのですが、それは同相の動きからエネルギーを抜いてしまいました。通常の減衰を保つべき部分です。ゆっくりうなる弦のペアは、何秒ものあいだこの同相の状態の近くにとどまるので、どの音も単純に早く死んでしまいました。気づいたのは、直前のビルドで同じ音をレンダリングしたからです。C4 は6.57秒から2.64秒に落ちていました。
うまくいくバージョンは、引くのではなく補正を加えます。
\[ y_i \leftarrow y_i + \eta\,(y_i - \bar y), \qquad 1 + \eta = \rho^{\,1/P - 1}, \qquad P = 1 + 3c \]
ここで \(\bar y\) は弦の平均、 \(\rho\) は通常のループゲイン、 \(c\) は結合量です。同相の動きは通常の減衰を保ち、逆相の部分は最大で \(P\) 倍長く鳴ります。わずかにデチューンしたユニゾンでは、C4 の第1部分音のアフターサウンドは、結合をオフにしたときの7.3秒に対して12.2秒になりました。完全に調律された弦では何も変わらず、それもまた正しい挙動です。(このモデルは1977年の Weinreich によるものです。)
コードにすると数行です。まず、音が始まるときに一度だけ計算するゲインで、おかしな設定でも暴走しないよう上限を付けます。
// 駒の結合: eta = rho^(1/P - 1) - 1, P = 1 + 3c. let p = 1.0 + 3.0 * coupling.clamp(0.0, 1.0); let eta = (rho.max(1e-3).powf(1.0 / p - 1.0) - 1.0).clamp(0.0, 0.05);
次に、サンプルごとに、各弦の出力を、自分のループに戻る前にすべての弦の平均から押し離します。
let mean = sum / n; // 駒の動き for (s, &v) in strings.iter_mut().zip(&outputs) { s.commit_looped(v, v + eta * (v - mean)); }
弦が一致しているときは \(y_i - \bar y\) がゼロなので、何も起こりません。食い違っているときは、その食い違いが、一致している部分よりわずかに少ない損失を受けます。
なぜシンセのように聞こえたのか
あるとき、シンセのように聞こえると言いました。その理由は、小さくて具体的なものでした。
コーラスを組む弦がすべてまったく同じ高さに調律されていましたが、これはピアノがやりうる中でいちばんデジタルなことです。そこで、鍵ごとに穏やかで決定論的なゆらぎを加えました。調律で約2.4セント、ハンマーの強さで6 % ほどで、その鍵を弾くたびに同じです。ユニゾンの弦は一定のセント数でデチューンされていたため、うなりは音が高くなるほど速くなりました。C7 では3.6 Hz で、コーラスペダルを通したスーパーソーのように聞こえます。現在のデチューンは、約0.5 Hz の一定のうなり速度です。最後の修正はドライさでした。ルームとシンパシーの初期値を少し上げました(ルーム 0.15 から 0.22、シンパシー 0.5 から 0.6)。響きの広がりがないピアノは、シンセに聞こえるからです。
別の問題を追いかけているときに、ループゲインがループの周期ではなく遅延線の長さから計算されていたことも見つけました。そのせいで波が何周するかを数え間違え、C7 の減衰が27 % 長くなっていました。これは、バグとしては聞こえない種類のバグです。「うーん、高音域がどこか変だ」と聞こえるのです。
1つのエンジン、7つの楽器
同じ仕組みが、チェンバロ、クラヴィコード、フォルテピアノ、ハープのほとんどを占めることがわかり、励振の仕方と、いくつかの追加ストップだけが違います。
- チェンバロ: ハンマーではなく羽軸で弦をはじき、バフ(リュート)ストップが付きます。
- クラヴィコード: 弦に触れたままの真鍮のタンジェントがあり、音が始まったあとにさらに強く押すと音が高くなります。これはベーブングと呼ばれます。最大の圧力では、音が約25セント上がります。
- フォルテピアノ: 軽い革のハンマー、モデレーター、そしてガタガタ鳴るバスーンストップ。
- コンサートハープ: 止めるまで鳴り続く、指ではじく弦。ピンチハーモニクスとグリッサンドペダルを備えます。
2種類のエレクトリックピアノには、新しい物理が必要でした。タインは固定-自由の梁で、そのモードは 1 : 6.267 : 17.547 の比になり、さらにトーンバーがあり、磁気ピックアップで読み取ります。リードは叩かれた鋼で、静電型ピックアップで読み取ります。どちらも起動時にスパイクが出て、出力が16にまで達しました。静止状態ではなくゼロから始まる磁束を微分していたからです。磁束を静止時の値から始めるようにしたら直りました。
10個のペダルはすべての楽器で動作し、そのうちの任意の4つを画面上のペダルに置けます。私が考案したペダル(スーパーソステヌート、グリッサンド)は、従うべき標準がないので、マニュアルで定義してあります。
リアルタイム処理と、痛い目を見ないとわからないこと
オーディオスレッドは何も待ってはいけません。ロックなし、メモリ確保なし、ファイルなし。私はこれを3か所で間違えるか、それに近いことをして、どれも偶然に見つけました。
- 単体プレーヤーは、エンジンをロックの裏側で共有し、コールバックのたびにベクターを作っていました。現在はエンジンを単独で所有し、MIDI をリングバッファ経由で受け取ります。
- プラグインのスムージングは何もしていませんでした。生の値を読んでから、スムーザーを、捨ててしまうバッファに通していたのです。つまみを動かすたびに段差ができ、どのパラメーターでもジッパーノイズが聞こえたはずです。パラメーターは現在、64サンプルのブロックごとにスムージングされます。
- メーターの減衰が最大で64倍も遅くなっていました。サンプルごとの係数が、ブロックごとに一度だけ適用されていたからです。
エディターにも独自のバグがありました。メーターとランプは、カーソルがウィンドウの上にあるときだけ動きました。グラフィックスのツールキットは、イベントに起こされたときだけフレームを描画し、オーディオスレッドからの「再描画してください」という丁寧な依頼は、描画の直前にしか見られず、その描画を誰も引き起こさなかったのです。毎秒15フレームのタイマーで直りました。
音には、それ自体の予算があります。最悪のケースの積み重なり(32ボイスすべて、サステインペダルを踏み、すべてのエフェクトがオン)でも、2コアのクラウドマシンでリアルタイムの約11倍の速さで動きます。その大半は SIMD のおかげです。弦のグループを1つの命令で、響板のモードを8つずつ2つのバンクで処理します。そのテストの最適化されていない版はずっと遅いので、速度のチェックはリリースビルドでのみ実行されます。
テストが言い返してきた
私のテストのうちいくつかは間違っていたか、間違った理由で失敗しており、そういうものから最も多くを学びました。
- 高音域の部分音の鋭さを調べるテストは、何週間も合格していました。のちに、それがノイズで合格していたことがわかりました。古いサスティーンでは部分音3から6がすべて約 -7 dB で、これはフラットなノイズの特徴であり、フラットなスペクトルには期待されるどの周波数の近くにも何かがあるのです。ノイズがなくなると、テストは本物の構造を要求しなければならなくなりました。
- ゴールデンファイルのテストは、レンダリング結果を保存済みのものと比較します。これは、モノラルのピークが隠していたクリッピングを検出しました。左チャンネルは +1.05 に達していたのに、右は +0.59 で、平均すれば無難な0.82になります。
- ペダルについて何かを見るには、音の早い段階ではなく遅い段階で測るしかありませんでした。リリースのトランジェントはどの場合も大きく、効果をかき消してしまうからです。
- ツールのテストは、ピアノのテストと同じくらい重要でした。符号が間違った放物線ピークピッカーが、B の測定値をすべて9セントずらしており、私は壊れた物差しに合わせてピアノを調律するところでした。
- プラグインをバリデーターにかけると、非常に低いサンプルレートでクランプの上限と下限が逆になっている箇所が2つ、周波数がサンプルレートの半分を超えると不安定になるフェイザーが1つ見つかりました。現在は1 kHz から 768 kHz まで何でも耐えられます。
私を騙したもの
いくつかの測定は、必要以上に長く私を騙しました。
C6 では、分析が基音を19 dB 低く示しました。存在しない穴です。分析器には完全に調律された弦を想定するよう指示していましたが、ピアノ本来のストレッチがあるため、部屋のコムモードの1046.2 Hz が私の予測から0.3 Hz の位置に来て、その枠を奪っていたのです。弦は問題ありませんでした。
2.7〜3 kHz のこぶは共鳴に見えました。実際には、縦方向と横方向のほぼ等しい2つの部分音が3〜4 % 離れているだけで、ピークピッカーがそれらを混同し続けていました。響板に罪はありませんでした。
ある作り直しのあと、高音域が0.1 dB まで同一に測定されました。それは微妙なバグではなく、ホスト内のプラグインが再スキャンされておらず、古いバイナリを聴いていたのです。
それから、自分自身の演奏がありました。自分の演奏の録音を基準の録音と比べると、数字は大きくかけ離れていました。スペクトル重心は5251 Hz 対 730 Hz です。私はシンセのせいだと決めつけました。1音だけをレンダリングして同じように測ると、基準と同じ位置に収まりました。違いは私自身でした。約1.4倍も速く弾き、E4 より下は一音もなく、最初から最後まで同じ音量で、ドライで狭い録音だったのです。コードは1行も変えていません。
箱の中のその他のもの
ここには、きちんと説明する余裕がないほどたくさん入っているので、手短にまとめます。
- エフェクト: トレモロ、ワウ、コーラス、フランジャー、フェイザー、ファズ、ディレイ、アンプシミュレーション、コンプレッサー、2つのイコライザー。それぞれ、オフのときはビット単位で完全に透過です。
- ルーム: 軽量な内蔵リバーブと、4つの合成空間または自分のインパルス応答を使えるコンボリューションモード。レイテンシーは加わりません。
- 空間: 1次アンビソニックスの出力と、7.1.2 ベッドへのデコード。
- チューニング: 音律、Scala ファイル、ストレッチ。
- ツール: 録音のための分析器、録音から音作りを学習するフィッター、ブラインドテスト用ページのジェネレーター。
- フィッター: エンジンには硬いスイッチ(フェルト、ダンパー)があるため、微分できませんでした。そこで、すべてのパラメーターを一度に揺らして2回レンダリングすることで勾配を推定します。この手法は SPSA と呼ばれます。テスト用のターゲットでは、40ステップでスペクトル距離を20〜50 % 縮めます。
実際の出来栄え
| 項目 | 結果 |
|---|---|
| 最悪ケースの積み重なり、全エフェクトオン | リアルタイムの11.2倍 (2コア x86) |
| clap-validator | 36件合格、0件不合格、8件スキップ |
| サンプルレート | 32〜96 kHz で同じ音に聞こえる |
| クラッシュせずに耐える範囲 | 1 kHz〜768 kHz |
| 最大ベロシティの C1 単音 | フルスケールの0.77でピーク |
最後の行は、最後に追加した3 dB のトリムです。最大ベロシティの低い音は、エフェクトを通す前の段階で、以前はフルスケールをわずかに超えていました。
/// 固定の -3 dB マスタートリム、10^(-3/20)。Volume コントロールと一緒に適用される。 pub const OUTPUT_TRIM: f32 = 0.707_945_8; // ... let vol = self.params.volume * OUTPUT_TRIM;
参考文献
5つだけ読むなら:
- Julius O. Smith, Physical Audio Signal Processing.
- Bank、Välimäki たちによるピアノのモデリング:コミューテッド合成、響板、分散。
- Chaigne と Askenfelt による、ハンマーと弦の数値モデル。
- Giordano と Jiang による、ハンマーと弦の相互作用。
- Weinreich (1977) による、結合したピアノ弦。