KSPでフレットレスベースを作ろうとして四苦八苦した話
note で FuruBass-Fretless の記事を読んで、「自分でも Kontakt インストゥルメント作れるんじゃないか」と思い立ってしまった。
結論から言うと、KSP (Kontakt Script Processor) は見た目よりずっと癖が強い。ポルタメントひとつ実装するだけで半日潰した。
何を作りたかったか
フレットレスベースの一番の特徴は「音と音の間がなめらかにつながる」こと。フレットがないので、弦を押さえた位置から次の音へ指をスライドさせるだけでポルタメントになる。
これを Kontakt 上で再現したくて、以下の3つを実装しようとした。
- ポルタメント: 直前のノートから現在のノートへピッチをグライドさせる
- スライドイン: ノートに到達する前に、数セミトーン離れた位置から滑らかに近づく
- ビブラート: フレットレス特有の揺れ
KSP とは
Kontakt に内蔵されたスクリプト言語。Native Instruments の独自仕様で、公式ドキュメントは存在するものの情報が古かったり英語のみだったりする。構文は Pascal っぽいが、独自のコールバック構造を持っている。
on note { ← ノートオン時に呼ばれる }
...
end on
on release { ← ノートオフ時に呼ばれる }
...
end on
変数の型は $(整数)、%(整数配列)、@(文字列)、~(実数)で区別する。型を間違えるとエラーにはならずそのまま無視されるので、バグが静かに死ぬ。
ハマりポイント① change_tune の単位が謎
ポルタメントの実装は change_tune 関数で行う。
change_tune($EVENT_ID, $tune_value, $relative)
$tune_value に何を入れればいいのか、ドキュメントを読んでも最初はよくわからなかった。
「1 セミトーン = いくつ?」を確認するために、まず手動で値を変えながら音を出してみた。
{ 試行錯誤の記録 }
change_tune($EVENT_ID, 100, 0) { ← ほぼ動かない }
change_tune($EVENT_ID, 10000, 0) { ← 少し上がる? }
change_tune($EVENT_ID, 1000, 0) { ← ちょうど1セミトーン上がった }
1000 = 1 セミトーン。つまり単位は「ミリセミトーン」に近い何か。ドキュメントには「semitone × 1000」と書いてあるが、最初は別の箇所を参照していてずっと間違った値を入れていた。
ハマりポイント② wait() を on note の中で使うと音が詰まる
ポルタメントのアニメーションは「ピッチを少しずつ変えながら wait() で待つ」ループで実装した。
on note
$offset := $porta_offset
while ($offset # 0)
$offset := $offset - $step_size
change_tune($EVENT_ID, $offset, 0)
wait(5000) { 5ms 待機 }
end while
end on
これ、連打すると前のノートのループが残ったまま次のノートが鳴り始める。テストしていたら和音みたいなピッチのズレが発生して「壊れた?」と焦った。
対処として:
- アニメーション中に新しいノートが来たら古いループを打ち切る(フラグ管理)
- そもそも
$ANIM_STEPSを少なくしてwait()時間を短く抑える
どちらも完璧ではないが、今回は 25ステップ に固定してループ時間を短くする方針にした。連打してもそこまで崩れなくなった。
ハマりポイント③ スライドインの方向判定
スライドインは「次のノートへ近づくとき、少し遠い位置から滑り込む」演奏技法だ。
前のノートより高い音へ → 下から滑り上げる(slide_offset = -range)
前のノートより低い音へ → 上から滑り下げる(slide_offset = +range)
コードにするとこうなる。
if ($note > $prev_note)
$slide_offset := -($knob_slide_range * $SEMITONE)
else
$slide_offset := $knob_slide_range * $SEMITONE
end if
一見シンプルだが、スライドインとポルタメントを両方有効にすると干渉する。
スライドインが終わった時点でピッチは 0(基準音)にいる。そこにポルタメントが「直前のノートとの差分だけずらせ」と change_tune を叩く。このとき $relative を 1(相対値)にしておかないとスライドインの分がリセットされてしまう。
{ スライドインの後はピッチが 0 になっているので相対的に加算 }
if ($sw_slide = 1)
change_tune($EVENT_ID, $porta_offset, 1) { relative = 1 }
else
change_tune($EVENT_ID, $porta_offset, 0) { absolute }
end if
この 0 と 1 の違いを見落として30分くらい「なんかピッチが飛ぶな」と悩んでいた。
ハマりポイント④ レガートモードの判定
ポルタメントを「レガートのときだけ」有効にしたかった。つまり「前のノートを押しながら次のノートを弾いたときだけグライドする」動作。
declare %is_held[128] { 各ノートの押鍵状態 }
on note
if ($menu_porta_mode = 1 and %is_held[$prev_note] = 1)
{ レガート条件を満たすのでポルタメント実行 }
end if
%is_held[$NOTE] := 1
end on
on release
%is_held[$EVENT_NOTE] := 0
end on
落とし穴は on note の時点でまだ %is_held を更新していないこと。新しいノートの押鍵を記録する前に $prev_note の状態をチェックしないといけない。順番を間違えると「常にレガートになる」か「絶対にレガートにならない」かの2択になる。
最終的なスクリプト構成
試行錯誤の末、以下の構成に落ち着いた。
on init → UI ノブ・スイッチの定義、変数の初期化
on note → スライドイン → ポルタメント → ビブラートの順に処理
on release → 押鍵フラグの解除
on controller → CC11 (Expression) と CC64 (Sustain) の処理
UI は以下の通り。
| セクション | コントロール | 内容 |
|---|---|---|
| PORTAMENTO | ON スイッチ | ポルタメント有効/無効 |
| Mode | Always / Legato Only | |
| Time | グライド時間 (ms) | |
| SLIDE IN | ON スイッチ | スライドイン有効/無効 |
| Range | スライドする音程幅 (semitone) | |
| Time | スライド時間 (ms) | |
| VIBRATO | Depth | ビブラートの深さ |
| Rate | ビブラートの速さ |
実際に弾いてみて
ポルタメント Time を 150ms、スライドイン Range を 4 半音に設定すると、かなりフレットレスっぽい動きになった。レガートモードで弾くと、フレーズの流れに応じて自然にグライドがかかる。
まだ課題はある。
wait()ループを使っているので BPM と連動したポルタメント時間(16分音符ぶん、とか)はまだ未実装- ビブラートが速弾き中に残り続けるケースがある
- 実際のサンプルとの組み合わせ(特に同一音への連打)の挙動確認が必要
KSP は「動いているように見えるけど実は壊れている」状態になりやすい。変数のスコープと wait() のタイミングを意識しながら慎重に書くのが大事だと学んだ。
スクリプト全文は GitHub の kontakt_test リポジトリ に置いている(予定)。
Live with a Smile!
