ボイス・アサイナ (4)

グランド・ピアノには 3 本、アップライト・ピアノには 2 本か 3 本の「べダル」がありますが、その一番右側のべダルが「ダンパー・ペダル」で、そのダンパー・ペダルを踏み込むと、すべてのダンパー (damper) が一斉に持ち上がり、鍵盤を離してもすぐに音が消えずに長く残るようになります。
このピアノのダンパー・ペダルと同様の機能を実現するものとして、MIDI コントロール・チェンジ (control change) 64 番 (以下「cc#64」と略します) の「Hold1」コントロールが、用意されています。
MIDI コントロール・チェンジには、cc#0 〜 cc#31 までのグループと、cc#64 〜 cc#95 までのグループの、計 2 つのグループが存在しています。
cc#0 〜 cc#31 までのグループは「連続可変コントローラ」を表すもので、そのデータバイトは、設定値の MSB側 7 ビットとして扱われます。
コントロール・チェンジの番号に「32」を足した、cc#32 〜 cc#63 の値は、独立したコントロールではなく、その対応する 0 から 31 までのコントロールに対する LSB側 7 ビットとして扱われます。
結局、0 〜 31 までの 32 種類のコントロールに対し、64 種のコントロール・チェンジ・メッセージを使って、各コントロールあたり 14 ビット分解能の値を設定できるようになっています。
ただし、LSB 側も使っている場合はまれで、多くの場合は cc#0 〜 cc#31 までの MSB 側 7 ビットしか使っていません。
一方、cc#64 〜 cc#95 までのグループは、基本的には ON / OFF の「スイッチ」を想定して設定され、そのデータバイト 7 ビット分の設定値しか持っていません。
「スイッチ」として扱う場合には、メッセージの送出側は「OFF」はデータバイトを「0」とし、「ON」はデータバイトを「127」として送ります。
そして、メッセージの受け側はデータバイトの値が「63」以下ならば「OFF」と解釈し、「64」以上なら「ON」と解釈するのが普通です。
「Hold1」コントロールは、cc#64 ですから、基本的にはダンパーペダルが「踏まれた」/「踏まれてない」の 2 択に対応する ON / OFF の 2 値を取りますが、最近のピアノ音源などでは、「ハーフベダル」に対応する製品もあります。
ピアノのダンパー・ベダルを一杯に踏むと、ダンパーは完全に弦から離れ、弦が開放されますが、ペダルを浅く踏んだ状態では、ダンパーは一部が弦に接触し、弱いけれど振動を減衰させるので、響きの長さをペダルの踏み具合で調節できることになります。
そのペダルの「踏み具合」を cc#64 のデータで表現し、音源側がそれに対応してコントロールするのが「ハーフペダル」対応です。
しかし、後に示すアルゴリズムでは「Hold1」は ON / OFF の 2 値として扱っていて、ハーフペダルには対応していません。
また、実際のピアノでは、鍵盤から指を離してリリースに入った後で完全に減衰する前の状態、あるいはダンパーを下ろしてリリースに入った後で完全に減衰する前の状態で、(再び) ペダルを踏んでダンパーを上げた場合には、減衰が中止され、その時点での音量で長く音が残ることになります。
これを「リダンパー」といいますが、後に示すアルゴリズムでは、いったんリリースに入ると減衰する一方となるので、リダンパーには対応していません。
GM2 (GENERAL MIDI Level 2 Recommended Practice (RP024)) では、ピアノ系の音色でリダンパーに対応することが「必須」とされています。
MIDI コントロール・チェンジ「Hold1」の効果を、実際の楽器と対比させて考えた場合、「鍵盤を押す」のが「ノートオン」に対応し、「鍵盤を離す」のが「ノートオフ」に対応しますが、Hold1 が「オン」の場合には「ノートオフ」で音が消えずに Hold1 がオフになる時点まで音が残ります。
ホールド中は、実質的には「ノートオフ」が保留され、 Hold1 オフになった時点で他の保留中のボイスと一緒に本当のノートオフ状態となります。
前回のアルゴリズムでは、ノートオフになると、発音中のボイスを保持しているキューから、そのボイスを取り除く処理をしていましたが、Hold1 がオンの場合には、そのボイスはまだ発音を続ける必要があるので、キューから取り除くわけには行きません。
しかし、ノートオンによって発音が維持されているボイスと、すでにノートオフが到着しているけれども Hold1 によってノートオフが保留されているボイスとは、はっきり区別する必要があります。
ボイスデータ中のフラグによって区別しても良いのですが、ここでは、ノートオンで「発音中」のボイスを「アクティブ・キュー」に保持し、「保留中」のボイスには、新たな「ホールド・キュー」を設け、その中に保持することで区別します。
いま考えているような「リソース無制限版」のアルゴリズムではあまり違いはありませんが、リソースが有限な実際のアルゴリズムではメリットが出てきます。
それは、Hold1 がオフになった時、「保留中」の全部のボイスに対しノートオフ処理を施す必要がありますが、ひとつのキューの中に「アクティブ」と「ホールド」のボイスを混在させていると、いちいちキューを先頭からスキャンしながら判別し、ホールド中のボイスに対してノートオフ処理を行ったのち、そのボイスをキューから外す処理を行う必要があります。
一方、最初から「アクティブ」と「ホールド」のキューを分けておくと、Hold1 オフになったら、「ホールド」キュー内に保持されているボイス全部についてノートオフ処理を行い、ホールド・キューの中身全体を外してしまって (フリー・キューの最後尾に追加して)、ホールド・キューを空にするだけで済みます。
さらに、データの同時発音数がハードウェアの同時発音数を越えて、現在発音中のボイスどれかを選んで新しい音の発音に割り当てなければならない場合に、その優先度付けが容易になります。
現在ノートオン中で発音している音と、すでにノートオフが到着してベダルによって維持されている音とでは、ペダルで維持されている音の方が重要性が低いと考えられますから、ホールド中のボイスがひとつのキューにまとまって保持されていれば、「引っこ抜く」ボイスの候補を、まずホールド・キュー、ついでアクティブ・キューの順番で見ていくことで簡単に優先度が設定できます。
この「ホールド・キュー」を新設する方針 (2048 個の配列要素すべてに各 1 個ずつで、計 2048 個) で、前回のアルゴリズムを修正すると、
ノートオン・メッセージに対して、

  • ボイスを1個生成する
  • ボイスにノート番号に対応する音程データをセットし、アタック処理を開始する
  • ノートオン・メッセージの MIDI チャンネル番号とノート番号から 2 次元配列内の対応する「アクティブ・キュー」要素の位置を求める
  • ボイスをその「アクティブ・キュー」の最後尾に追加する

という処理を行えばよいことになります。 これは、操作対象が「アクティブ・キュー」となっただけで、内容は前回と同じです。
また、ノートオフ・メッセージに対しては、

  • ノートオフ・メッセージの MIDI チャンネル番号とノート番号から 2 次元配列内の対応する FIFO キュー要素の位置を求める
  • Hold1 オフの場合
    • アクティブ・キューの先頭からボイスを取り出す
    • ボイスにノートオフ処理 (リリース開始) を施す
  • Hold1 オンの場合
    • アクティブ・キューの先頭からボイスを取り出す
    • そのボイスをホールド・キューの最後尾に追加する

処理を行います。
そして、cc#64 メッセージを受け取って Hold1 オフになった場合は、

  • 対応する MIDI チャンネルに属する 128 個の配列要素の全てのホールド・キュー内の全ボイスに対してノートオフ処理 (リリース開始) を施す
  • 対応する MIDI チャンネルに属する 128 個の配列要素の全てのホールド・キューを空にする

処理を行います。 (11/16 追記: 誤りを訂正しました)
Hold1 オンになった場合には、特に処理は必要ありません。
次回からは、リソースに制限のある実際の場合を扱います。