MIDI音源でティンパニのロールを再現する

MIDIのサンプリング音源では、ティンパニのロールはロールのサンプリング音が入っているのが一般的です。同じ音量でずっとロールし続けるならそれでもいいですが、クレッシェンドやデクレッシェンドしようとすると、音質は変わらずに音量だけ変化することになるので、強弱の幅が大きいときには不自然な響きになってしまいます。音源によっては、ティンパニ・ロールのクレッシェンド、デクレッシェンドのサンプルも入ってたりするのですが、それだとクレッシェンドやデクレッシェンドのタイミングが調節できません。

ラウンドロビンに対応している音源なら、単発のティンパニ音を適度な間隔で細かく打ちこめば、それっぽく聞こえるのですが、普通に打ちこんだだけだと、途中で曲のテンポが変わるとロールの速度も変化してしまいます。ちょっとの変化なら気にしなくてもいいかもしれませんが、大きめのリタルダンドなんかがあると、最後はモタモタになってしまいます。

なんとかならないかなと思っていたところ、ReaperというDAWなら、jsfxというスクリプトで実現できそうだったので、試しに簡単なスクリプトを作ってみました。このページの最後にあるのがそのスクリプトです。

作成する際に使用した音源はVSCO2 CEというフリーの音源ですが、指定した時間間隔でひたすらノート・オン、オフの信号を出してるだけなので、おそらくどんな音源でも動くでしょう。

ロールの音の大きさは、Expression（CC11）で変えられるようにしたので、デクレッシェンドやクレッシェンドも自由自在です。このとき、Expressionの値を打音のベロシティに置き換えて音を鳴らすようにしているので、（デ）クレッシェンドすればベロシティに合わせてちゃんと音質も変化します。また、ロールの間隔は拍ベースではなく時間ベースなので、テンポを変化させてもロールの細かさには影響しません。

[作成したティンパニロールのサンプル](https://soundcloud.com/sbtseiji/timpani-roll)

とりあえず作ってみただけの割には、なかなかよくできたので、Reaperを使っている人がどれくらいいるか、またどの程度需要があるかわかりませんが、スクリプトを公開しておきます。

## 解説
（2024.02.29追加）

このスクリプトですが、ノートのオン・オフに関する処理は`@block`の`while`ループで実施しています。

```
while (midirecv(ts, msg1, msg2, msg3)) (
```

`while`文の中の`midirecv(ts, msg1, msg2, msg3)`がMIDI信号の受け取りで、`ts`はオフセット、`msg1`はMIDIのステータス、`msg2`はノート・ナンバーやコントロール・ナンバー、`msg3`はベロシティやCCの値です。

```
m = msg1 & 240; 
```
ループの最初の`msg1 & 240`でステータス・メッセージの上位バイトを取り出します。これが`9n`なら`ノートオン`、`8n`ならノートオフ、`11n (BnH)`ならCCです。

以降の処理では、`msg2`で押されたのがキースイッチに割り当てたキーなのかを判断して、ロールの開始・停止処理、ベロシティの変更を行っています。また、CCの場合はExpression（CC11）かどうかをみています。
MIDIの信号をシンセ側に送る処理は、`midisend()`で行っています。この関数の引数は`midirecv()`と同じです。

なお、`while (midirecv(ts, msg1, msg2, msg3)) (...`の部分は最初、MIDIキーボードのキーを押し続けている間はずっと処理されるのかと勘違いしていましたが、このループは何らかのMIDI信号を受信した瞬間しか処理されないので、キーを押したとき、離したとき、CCを送ったときのみしか処理されません。なので、この`while`文の中の`midisend(ts,9*16,note,msg3);`の部分は、ロール開始時の1発目の音だけを鳴らしていることになります。

それ以外のロールの持続処理は、`while`文を抜けた後の最後の部分で行っています。

```
(roll_mode && roll_on) ?(
  counter += inc;
  (counter > 1000) ? (
    counter = 0;
    midisend(ts,8*16,note);
    midisend(ts,9*16,note,vel - velocity_rand + floor(rand(velocity_rand)) );
  );
);
```

ここで、このスクリプトが実行されるたびカウンターに数値を加えていき、それが閾値を超えたらノートをオフ・オンしんてカウンターをリセットする、の繰り返しです。

なお、ロールの開始と停止は、本当はノートごとにフラグを設定したほうがいいのでしょうが、ここでは`roll_on`という変数1つしか使っていないので、複数の音を同時に鳴らしたとき、いずれか音でノートオフの信号が発生すると、そこでロールはストップします。これはキースイッチについても同様なので、ロール音を発生させる場合には、その音のノートオンより前にキースイッチのキーを離すか、あるいは、ロールが終了するまでキースイッチのキーを押しっぱなしにしておく必要があります。

```
desc:TimpRoller

slider1:127<0,127,1>Velocity Max
slider2:64<0,127,1>Velocity Min
slider3:2<0,10,1>Velocity Rand

slider4:1.45<1,2>Speed in Velocity Max
slider5:1.8<1,2>Speed in Velocity Min
slider6:5<0,10,1>Speed Rand

slider7:13<-0,127,1{0: C-1,1: C#-1,2: D-1,3: D#-1,4: E-1,5: F-1,6: F#-1,7: G-1,8: G#-1,9: A-1,10: Bb-1,11: B-1,12: C0,13: C#0,14: D0,15: D#0-1,16: E0,17: F0,18: F#0,19: G0,20: G#0,21: A0,22: Bb0,23: B0,24: C1,25: C#1,26: D1,27: D#1,28: E1,29: F1,30: F#1,31: G1,32: G#1,33: A1,34: Bb1,35: B1,36: C2,37: C#2,38: D2,39: D#2,40: E2,41: F2,42: F#2,43: G2,44: G#2,45: A2,46: Bb2,47: B2,48: C3,49: C#3,50: D3,51: D#3,52: E3,53: F3,54: F#3,55: G3,56: G#3,57: A3,58: Bb3,59: B3,60: C4,61: C#4,62: D4,63: D#4,64: E4,65: F4,66: F#4,67: G4,68: G#4,69: A4,70: Bb4,71: B4,72: C5,73: C#5,74: D5,75: D#5,76: E5,77: F5,78: F#5,79: G5,80: G#5,81: A5,82: Bb5,83: B5,84: C6,85: C#6,86: D6,87: D#6,88: E6,89: F6,90: F#6,91: G6,92: G#6,93: A6,94: Bb6,95: B6,96: C7,97: C#7,98: D7,99: D#7,100: E7,101: F7,102: F#7,103: G7,104: G#7,105: A7,106: Bb7,107: B7,108: C8,109: C#8,110: D8,111: D#8,112: E8,113: F8,114: F#8,115: G8,116: G#8,117: A8,118: Bb8,119: B8,120: C9,121: C#9,122: D9,123: D#9,124: E9,125: F9,126: F#9,127: G9}>Roll On Switch
slider8:15<-0,127,1{0: C-1,1: C#-1,2: D-1,3: D#-1,4: E-1,5: F-1,6: F#-1,7: G-1,8: G#-1,9: A-1,10: Bb-1,11: B-1,12: C0,13: C#0,14: D0,15: D#0-1,16: E0,17: F0,18: F#0,19: G0,20: G#0,21: A0,22: Bb0,23: B0,24: C1,25: C#1,26: D1,27: D#1,28: E1,29: F1,30: F#1,31: G1,32: G#1,33: A1,34: Bb1,35: B1,36: C2,37: C#2,38: D2,39: D#2,40: E2,41: F2,42: F#2,43: G2,44: G#2,45: A2,46: Bb2,47: B2,48: C3,49: C#3,50: D3,51: D#3,52: E3,53: F3,54: F#3,55: G3,56: G#3,57: A3,58: Bb3,59: B3,60: C4,61: C#4,62: D4,63: D#4,64: E4,65: F4,66: F#4,67: G4,68: G#4,69: A4,70: Bb4,71: B4,72: C5,73: C#5,74: D5,75: D#5,76: E5,77: F5,78: F#5,79: G5,80: G#5,81: A5,82: Bb5,83: B5,84: C6,85: C#6,86: D6,87: D#6,88: E6,89: F6,90: F#6,91: G6,92: G#6,93: A6,94: Bb6,95: B6,96: C7,97: C#7,98: D7,99: D#7,100: E7,101: F7,102: F#7,103: G7,104: G#7,105: A7,106: Bb7,107: B7,108: C8,109: C#8,110: D8,111: D#8,112: E8,113: F8,114: F#8,115: G8,116: G#8,117: A8,118: Bb8,119: B8,120: C9,121: C#9,122: D9,123: D#9,124: E9,125: F9,126: F#9,127: G9}>Roll Off Switch

in_pin:none
out_pin:none

@init


status=0;
status2=128;
roll_mode = 0;
roll_on = 0;
vel = 64;

velocity_lo= 64;
velocity_hi= 127;
speed_lo= 145;
speed_hi= 180;

speed_rand = 5;
velocity_rand = 2;

memset(status,-1,128);
memset(status2,0,128);

@slider 
velocity_hi=floor(slider1);
velocity_lo=floor(slider2);
velocity_rand=floor(slider3);
speed_hi=slider5*100;
speed_lo=slider4*100;
speed_rand=slider6;
roll_on_key = slider7;
roll_off_key = slider8;

@block
      
while (midirecv(ts, msg1, msg2, msg3)) (
  m = msg1 & 240; 

  (m == 9*16 && msg2 == roll_on_key ) ? (roll_mode = 1):
  (m == 9*16 && msg2 == roll_off_key ) ? (roll_mode = 0):
  (m == 9*16)?
  (
    note = msg2;
    status[note]=0;
    status2[note]=vel;
    midisend(ts,9*16,note,msg3);
    roll_on = 1;
 ) : (m == 8*16 || m == 9*16) ? (
    (m == 8*16)? roll_on = 0;
    note = msg2;
    status[note]=-1;
    status2[note]>0.0 ? (
    midisend(ts,8*16,note); // send note off
      status2[note]=0;
    );
  ) : (m == 11*16)?(
    vel=msg3;
  ):(
    midisend(0,9*16,61,vel); 
  );
);

velocity_diff = velocity_hi - velocity_lo;
speed_diff  = speed_hi - speed_lo;
inc_rt = speed_diff/velocity_diff;

vel < 64 ?(inc = 180):(inc = speed_hi - velocity_diff*inc_rt + rand(speed_rand));

(roll_mode && roll_on) ?(
  counter += inc;
  (counter > 1000) ? (
    counter = 0;
    midisend(ts,8*16,note);
    midisend(ts,9*16,note,vel - velocity_rand + floor(rand(velocity_rand)) );
  );
);
```

# 追記（2024.10.3）
トラック数が多い場合，処理が重すぎてうまく動かないことがわかりました。そのため，今は次のスクリプトで，指定したノートをエディタ上で分割して配置する方式に変えました。

このスクリプトでは，エディタ上で選択されたノートを，指定幅+変動幅の間隔のノーと二分割します。このとき，作成されるノートのピッチ，ベロシティはもとのノートと同じです。ベロシティは一定なので，そのままだとマシンガンのようになってしまいますが，スクリプト実行後，ベロシティカーブを編集し，最後にHumanizeでベロシティのみばらつかせてやればよいでしょう。

また，指定幅（初期値80ms），変動幅（初期値10ms）はプロンプトから指定でき，各ノートの間隔は，`指定幅 + 変動幅 * 正規乱数`で変化を付けるようにしてあります。なお，正規乱数は-1.5〜1.5の範囲を超えないようにしています。ノートの間隔はmsベースなので，リタルダンドの際に間隔が間延びしたりすることはありません。ただし，あとからテンポの編集をした場合には，その変更には対応しませんので，再度このスクリプトを用いてロールを作成し直すことになります。



``` lua
-- ユーザー入力を取得（ミリ秒単位）
local retval, user_inputs = reaper.GetUserInputs("ノート設定", 2, "間隔 (ミリ秒),間隔の標準偏差 (ミリ秒)", "80,10")
if not retval then return end

local interval_ms, interval_jitter_std_ms = user_inputs:match("([^,]+),([^,]+)")
interval_ms = tonumber(interval_ms) or 80
interval_jitter_std_ms = tonumber(interval_jitter_std_ms) or 10

-- ミリ秒を秒に変換
local interval = interval_ms / 1000
local interval_jitter_std = interval_jitter_std_ms / 1000

-- ガウス分布の乱数を生成する関数
local function gaussian_random(mean, stddev)
  local u1, u2
  repeat
    u1, u2 = math.random(), math.random()
  until u1 > 1e-7
  local z0 = math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2)
  return mean + stddev * z0
end

-- アクティブなMIDIエディタとテイクを取得
local editor = reaper.MIDIEditor_GetActive()
local take = reaper.MIDIEditor_GetTake(editor)

if not take then return end

-- 編集開始
reaper.Undo_BeginBlock()
reaper.MIDI_DisableSort(take)

-- 選択されたノートを取得
local _, notecnt, _, _ = reaper.MIDI_CountEvts(take)
local notes_to_process = {}

for i = 0, notecnt - 1 do
  local _, selected, _, startppq, endppq, _, pitch, velocity = reaper.MIDI_GetNote(take, i)
  if selected then
    table.insert(notes_to_process, {startppq=startppq, endppq=endppq, pitch=pitch, velocity=velocity, index=i})
  end
end

-- ノートを処理（逆順で処理して削除の影響を避ける）
for i = #notes_to_process, 1, -1 do
  local note = notes_to_process[i]
  local start_time = reaper.MIDI_GetProjTimeFromPPQPos(take, note.startppq)
  local end_time = reaper.MIDI_GetProjTimeFromPPQPos(take, note.endppq)

  local current_time = start_time
  while current_time < end_time do
    local jittered_interval = gaussian_random(interval, interval_jitter_std)
    local jittered_time = current_time
    
    if jittered_time + jittered_interval <= end_time then
      local startppq = reaper.MIDI_GetPPQPosFromProjTime(take, jittered_time)
      local endppq = reaper.MIDI_GetPPQPosFromProjTime(take, jittered_time + jittered_interval)
      reaper.MIDI_InsertNote(take, false, false, startppq, endppq, 0, note.pitch, note.velocity, false)
    end
    
    current_time = current_time + jittered_interval
  end

  -- 元のノートを削除
  reaper.MIDI_DeleteNote(take, note.index)
end

-- 編集終了
reaper.MIDI_Sort(take)

-- MIDIエディタを更新
reaper.MIDIEditor_OnCommand(editor, 40435)
reaper.Undo_EndBlock("Replace notes with jittered notes", -1)
```