SonicPiをMIDIコントローラで制御する話
こんにちは。
久し振りにSonicPiでライブをしたのですが、今回はMIDIコントローラを初導入しました。
#GHOSTCLUB
— 0b4k3 (@3k4b0) 2021年11月27日
Player: @lactoice251
CRT: @fotfla pic.twitter.com/34pvel75Ki
記事 (https://t.co/7knURkQx1u) で書いた #SonicPi をMIDIでコントロールする手元の映像。 pic.twitter.com/WWcxWrjFxB
— らくとあいす (@lactoice251) 2021年11月28日
SonicPiでのMIDIコンからの入力の受け方について少しまとめておきたいと思います。 本記事は、程度SonicPiに慣れ親しんでいる方を想定しています。
環境
- SonicPi for Win x64 v3.3.1
- DJ TechTools MIDI FIGHTER TWISTER
SonicPiでのMIDIの受け方
Sonic Piでは接続されているMIDIデバイスからの入力を、Time Stateと呼ばれる共有メモリに保持しています。
ここは、良くわかってないで書きますが、(SonicPiのTutorialによれば) Time Stateはグローバル変数と似ているがスレッドセーフであるとのことです。 例えば、複数スレッド (ループ) 間で共通の値 (例えばBPM) にアクセスしたいとき、グローバル変数ではそれがあるスレッドで書き換えられると他のスレッドに影響を及ぼしますが、Time Stateの変更はスレッド間で独立に行われるため、determinism guaranteesであるということみたいです。
Time Stateは、シンボル (コロン+文字列) または文字列によって名前付けられる。MIDI入力は次のような文字列でTime Stateに格納されます:
"/midi:[MIDIデバイス名]:[MIDIチャネル]/[MIDIイベント]"
MIDIデバイスの名称は、環境設定>入出力>MIDIポートから確認出来ます*1。
格納されたMIDI Time Stateは、get
または sync
によって取得することが出来ます:
note, velocity = get (or sync) "/midi:[MIDIデバイス名]:[MIDIチャネル]/[MIDIイベント]"
get
による取得は、MIDI Time Stateにその時保持されている値を取り出すもので、sync
による取得はMIDI Time Stateが新しい入力によって更新されたときにその値を取得するものです。
また、MIDI Time Stateの名前はアスタリスク ( * ) によるワイルドカードを用いて指定することが出来ます。
これは例えば、MIDIイベントとMIDIチャネルを "*"に置き換えることによってあるMIDIデバイスから入力される信号を全て受け取るといったように利用することが出来ます。
受け取ったMIDI入力を使用出来る形にする
基本方針は次の通りです:
Note On/Off
Note On/Offは今回スイッチ/トグル的な情報の取得 (例えば全ミュートや、乱数の更新) と、手でSFXを鳴らすために使いました。
下記のコードのように、Note On/Offでそれぞれスレッドを用意してそれぞれはON/OFFの入力をsync
で待ちます。そして、やってきたMIDI信号のNoteに応じて処理の振り分けを行います。n==1
の場合の処理は、ON/OFFで連動していてONが押されてからOFFまでの間mute=true
とする処理。また、n==11
の時はONが押されたら即座にサンプルを再生します。
live_loop:midion do
use_real_time
n,v = sync "/midi:midi_fighter_twister_2:1/note_on"
if n==1
mute = true
end
[中略]
if n==11
sample fx2s,rand_i(fx2N)
end
end
live_loop:midioff do
use_real_time
n,v = sync "/midi:midi_fighter_twister_2:1/note_off"
if n==1
mute = false
end
end
Control Change
Control changeは、連続的な値の変化 (サンプルを鳴らす頻度、乱雑さ、リバーブの深さ、LPFなど) のために使いました。
ただし、SonicPiでは一度鳴った後の音に対して音を連続的に変化させることが出来ないことには注意が必要で、あくまでも各音を鳴らす時の設定を変化させるというところにとどまります。
Control changeも、MIDI ON/OFFと同じように受け取れますが、複数のノブを同時に回して同一チャネルにCC信号を送ったときMIDI ON/OFFと同じような書き方だと片方が取れなくなるのではないかと思うので [未検証]、今回は各16個のノブを回したときに別のチャネルにControl change信号が送られるようにし、それぞれを独立に受信します。
ただし、この場合に下記のようにsync
を使ってしまうと、ch1のControl change信号が来ないとそれ以降の信号が受け取れないようになってしまう。
live_loop:midicc do
use_real_time
_,v1 = sync "/midi:midi_fighter_twister_2:1/control_change"
_,v2 = sync "/midi:midi_fighter_twister_2:2/control_change"
_,v3 = sync "/midi:midi_fighter_twister_2:3/control_change"
end
そこで今回はやや無駄があるようにも思われるがget
によって信号を受信することにしました。ただし、getの場合は受信を待ってはくれないので別途sleepが必要となる:
live_loop:midicc do
use_real_time
_,v1 = get "/midi:midi_fighter_twister_2:1/control_change"
_,v2 = get "/midi:midi_fighter_twister_2:2/control_change"
[中略]
if v1 != nil
rbd = v1/127.0
end
if v2 != nil
rhh = v2/127.0
end
[中略]
sleep 0.3
end
受け取ったMIDI入力を元にリズムや音を作る
今回はリズムの生成アルゴリズムの変数とエフェクトのパラメータの部分に主にMIDI信号を利用した。 明確な機能というよりは、お気持ち的に割り当てている変数が多いですが...下記が実際に行った機能割り当てです。
ざっくりと各項目について書いていきます。
Control changeのパラメータ
各サンプルの頻度
今回のリズム生成の基本的な方針は、6つのサンプルグループと2つのシンセのいずれかを時間単位あたりひとつだけ鳴らすというもので、この8つのグループから各拍にどれを選択するかという頻度を8つのノブでコントロールします。例えば、1,2番目のノブだけを振り切るとバスドラとハイハットだけが鳴るといったような感じです。
1ループの音数
時間単位あたりひとつだけ音を鳴らすという方針の上では、各拍の音は独立、つまりループという概念は生まれませんが音楽的まとまりの導入(とエフェクトの負荷軽減)のため、いくつかの拍を1まとまりとして「1ループ」という概念を導入しています。ここでは、1ループに含まれる音の数を変更します。
ループのまとまり感
前項のようにループを導入してもそのままでは、各音は完全に独立です。 今回は音楽的なまとまりをかなりヒューリスティックに導入していて、この「まとまり感」変数をあげていくと、ループ先頭でバスドラが鳴りやすく、ループ中間でハットが鳴りやすくなり、逆にそれ以外の拍では鳴りづらくなっていきます。ループの音数が4のときは普通の四つ打ちに近づいていく感じですね。ループの音数を5や7にして変拍子を作り出すことも出来ます。
音程の広がり
今回音程のある楽器として、ベース、単音のシンセ、コードの三つが導入されていますがこれらのうちシンセとコードについて、ある割合でオクターブジャンプするようになっています。その割合をここでコントロールします。
サンプルの種類幅
各リズムサンプルグループ (バスドラ、ハイハット、クラップ、タム、パーカッション、グリッチ系) は、それぞれ複数のサンプルを持っています。この中からどの程度のバリエーションを取り出すかという部分がこの変数です。サンプルの順番として、おとなしめのものが前半に、激しめのものが後半にくるようにしておきます。
HPF/LPF cutoff
サンプルに対するHigh pass, Low pass filterのカットオフです。 ここで、他のエフェクトはループ単位でかけていますが、このエフェクトだけは各拍毎に更新しています。
リバーブMIX
リバーブのdry/wet割合です。今回はもともと深めのリバーブをかけておいて、dry/wetのmixだけでリバーブをコントロールすることにしています。
PAN
グローバルなPAN振りです。もともと各サンプルはある程度の乱数幅を持ってそれぞれPANが振られていますが、このノブを回すとその乱数幅が収束していって、振り切った時にはL/Rに偏るようになっています。
Note On/Offのパラメータ
MUTE
全ミュートです。ただし、SonicPiは既に鳴らした音をミュート出来ないので、実際には次の拍以降に鳴るはずの音を鳴らさないということをしています。このスイッチは押している間ずっとミュートで、NoteOffを受信したときにミュートを解除するという動作にしています。
ループ内seed固定
1ループごとに固定のseedを設定しなおすかどうかのスイッチです。1ループごとにset_random_seed [seed値]
でseedを設定しなおすと、各ループではまったく同じ音が繰り返されることになります。これによって、例えば、1ループの音数が16の時は1小節のリズムパターンのように振舞います。逆にseedを設定しないときは、ループ間ではまったく別の音が鳴ることになります。
seed変更
これは、前項のループ内seed固定が有効な時に効果があります。ループ内seed固定をしているそのseedをこのボタンを押すたびに変更します。これによって、ループパターンを変更して好みのものを「ガチャ」出来るようになります。
Bass ON
これは主にエンコーダが足りないためにBassだけ独立させているだけですが、Bassはバスドラと連動させた動きが多いので、鳴らし方・頻度はバスドラの頻度パラメータに紐づけて、鳴らすかどうかをこのスイッチで決めています。
エフェクトON/OFF
今回は、bitcrusherと、ixi_technoの二つのエフェクトを用意しています。エフェクトのパラメータをいじるのではなく、単にON/OFFをするスイッチです。
SFX
その他のNote ON/OFFと違って楽器的な振舞いをします。ボタンを押したタイミングで、性質の違う3つのSFXグループからランダムなサンプルを選んでそれぞれ再生されます。
時間単位
最小時間単位の長さを変更します。例えば、最小時間単位が1のときに比べて1/4の時は4倍のテンポになるといった感じです。
まとめ
Sonic PiをMIDIコンで制御するために、MIDI入力を受け取る方法とそれを使った実際の制御方法例を示しました。 別の使い方としては、シンセサイザーのように各シンセのパラメータを変更したりするようにも使っている人が多いようですね *2。 最後に、参考までに全体コードを載せておきます。コメントも何もないのでかなり見づらいかもしれませんが... 改変等はご自由にどうぞ!
use_bpm 138
load_synthdefs "C:/Synth/my-synth"
set_volume! 5
bds = "C:/SPS/gc1127/bd/"
bdN = 2
hhs = "C:/SPS/gc1127/hh/"
hhN = 2
claps = "C:/SPS/gc1127/clap/"
clapN = 3
toms = "C:/SPS/gc1127/tom/"
tomN = 6
percs = "C:/SPS/gc1127/perc/"
percN = 54
glitchs = "C:/SPS/gc1127/glitch/"
glitchN = 30
fx1s = "C:/SPS/gc1127/fx1/"
fx1N = 3
fx2s = "C:/SPS/gc1127/fx2/"
fx2N = 3
revs = "C:/SPS/gc1127/rev/"
revN = 4
Ns = [3,4,5,6,7,11,16]
Ms = [0.25,0.5,3.0/4.0,1.0]
key = -4
bsl = [:C3,:C3,:C2,:C2,:C2,:C2,:C2,:C2,:C2,:C1,:C1,:C1,:C1,:Db1,:Db2,:G1,:G2,:Db3].ring
synl = [:C2,:C3,:C3,:F3,:G3,:Bb3,:C4,:Db4,:Eb4,:F4,:G4,:Bb4,:C5].ring
cdl = [:C4,:C4,:Eb4,:F4,:C5]
mute = false
fixseed = false
withbs = false
seed = 0
rbd = 0
rhh = 0
rclap = 0
rtom = 0
rperc = 0
rglitch = 0
rsyn = 0
rcd = 0
octr = 0.0
width = 0.6
gpan = 0.0
acc = 0
var = 0.0
rev = 0.0
hpfco = 0.0
lpfco = 0.0
N = 4
M = 1
ixi = 0
bit = 0
set_mixer_control! hpf:0,hpf_slide:0.5
set_mixer_control! lpf:127,lpf_slide:0
live_loop:metro do
sleep 4
end
live_loop:loop1,sync: :metro do
F = 4.0 - var*4.0
fbd = 1.0
fhh = 1.0
it = 0
if fixseed
use_random_seed seed
end
with_fx :reverb,room:rev,mix:0.2+rev*0.8 do
with_fx :ixi_techno,res:0.9,mix:ixi do
with_fx :bitcrusher,bits:5,mix:bit do
N.times do
with_fx :rhpf,cutoff:hpfco,res:0.9 do
with_fx :rlpf,cutoff:lpfco,res:0.9 do
if not mute
oct = 12*(5.0*octr*rand-1.0).to_i
frbd = rbd
frhh = rhh
if it==0
frbd = rbd*(1.0 + 50.0*acc)
else
frbd = rbd/(1.0 + acc*20.0)
end
if it==(N/2.0).to_i
frhh = rhh*(1.0 + 50.0*acc)
else
frhh = rhh/(1.0 + acc*20.0)
end
rsum = frbd+frhh+rclap+rtom+rperc+rglitch+rsyn+rcd
if rand<rsum
r = rsum*rand
if r<frbd
sample bds,(bdN*rand**F).to_i,amp:0.75,pan:gpan
with_synth:sine do
play 30,amp:0.75,pan:gpan
end
if withbs
with_synth :tb303 do
play bsl.choose+0+key,amp:0.4,release:0.3+0.1*rand,cutoff:60+70*rand if rand<0.8*(1.0 - acc)
end
end
elsif r<frbd+frhh
sample hhs,(hhN*rand**F).to_i,amp:0.75,pan:(gpan + 0.2 + (rand**0.5 - 0.2)*width).clamp(1)
elsif r<frbd+frhh+rclap
sample claps,(clapN*rand**F).to_i,amp:0.75,pan:(gpan + -0.2 + (0.2 - rand**0.5)*width).clamp(1),rate:1.0 + 2.0*(rand-0.5)*var*var
elsif r<frbd+frhh+rclap+rtom
sample toms,(tomN*rand**F).to_i,amp:0.9+0.2*rand,pan:(gpan + -0.4 + (0.4 - rand**0.5)*width).clamp(1)
elsif r<frbd+frhh+rclap+rtom+rperc
sample percs,(percN*rand**F).to_i,amp:0.3+0.8*rand*rand,pan:(gpan + width*(rand**0.5-0.5)*2.0).clamp(1),rate:1.0 + 3.0*(rand-0.5)*var*var
elsif r<frbd+frhh+rclap+rtom+rperc+rglitch
sample glitchs,(glitchN*rand).to_i,amp:0.3+0.8*rand*rand,pan:(gpan + width*(rand**0.5-0.5)*1.5).clamp(1),rate:1.0 + 4.0*(rand-0.5)*var*var
elsif r<frbd+frhh+rclap+rtom+rperc+rglitch+rsyn
with_synth :myssaw do
play synl.choose+0+key+oct,release:0.3+0.13*rand,amp:0.15+0.3*rand
end
elsif r<frbd+frhh+rclap+rtom+rperc+rglitch+rsyn+rcd
with_synth :dtri do
play chord(cdl.choose+key+oct,'m7'),release:0.2,amp:1.5
end
end
end
if withbs
with_synth :tb303 do
play bsl.choose+0+key,amp:0.3+rand*0.22,release:0.18+0.1*rand,cutoff:40+70*rand if rand<acc*rbd
end
end
with_synth :myssaw do
play synl.choose+0+key+oct,release:0.2+0.04*rand,amp:0.15+0.15*rand if rand<rsyn
end
end
it += 1
sleep 0.25/M
end
end
end
end
end
end
end
live_loop:midion do
use_real_time
n,v = sync "/midi:midi_fighter_twister_2:1/note_on"
if n==1
mute = true
end
if n==2
fixseed = !fixseed
end
if n==3
seed = seed + 7
end
if n==4
withbs = !withbs
end
if n==5
bit = 1.0 - bit
end
if n==6
ixi = 1.0 - ixi
end
if n==9
sample revs,rand_i(revN),amp:1.5
end
if n==10
sample fx1s,rand_i(fx1N)
end
if n==11
sample fx2s,rand_i(fx2N)
end
if n>=13
M = Ms[n-13]
end
end
live_loop:midioff do
use_real_time
n,v = sync "/midi:midi_fighter_twister_2:1/note_off"
if n==1
mute = false
end
end
live_loop:getmidicc do
use_real_time
_,v1 = get "/midi:midi_fighter_twister_2:1/control_change"
_,v2 = get "/midi:midi_fighter_twister_2:2/control_change"
_,v3 = get "/midi:midi_fighter_twister_2:3/control_change"
_,v4 = get "/midi:midi_fighter_twister_2:4/control_change"
_,v5 = get "/midi:midi_fighter_twister_2:5/control_change"
_,v6 = get "/midi:midi_fighter_twister_2:6/control_change"
_,v7 = get "/midi:midi_fighter_twister_2:7/control_change"
_,v8 = get "/midi:midi_fighter_twister_2:8/control_change"
_,v9 = get "/midi:midi_fighter_twister_2:9/control_change"
_,v10 = get "/midi:midi_fighter_twister_2:10/control_change"
_,v11 = get "/midi:midi_fighter_twister_2:11/control_change"
_,v12 = get "/midi:midi_fighter_twister_2:12/control_change"
_,v13 = get "/midi:midi_fighter_twister_2:13/control_change"
_,v14 = get "/midi:midi_fighter_twister_2:14/control_change"
_,v15 = get "/midi:midi_fighter_twister_2:15/control_change"
_,v16 = get "/midi:midi_fighter_twister_2:16/control_change"
if v1 != nil
rbd = v1/127.0
end
if v2 != nil
rhh = v2/127.0
end
if v3 != nil
rclap = v3/127.0
end
if v4 != nil
rtom = v4/127.0
end
if v5 != nil
rperc = v5/127.0
end
if v6 != nil
rglitch = v6/127.0
end
if v7 != nil
rsyn = v7/127.0
end
if v8 != nil
rcd = v8/127.0
end
if v9 != nil
N = Ns[(7.0*(v9-1.0)/127).to_i]
end
if v10 != nil
acc = v10/127.0
end
if v11 != nil
octr = v11/127.0
end
if v12 != nil
var = v12/127.0
end
if v13 != nil
hpfco = v13
end
if v14 != nil
rev = v14/127.0
end
if v15 != nil
gpan = 2.0 * (v15/127.0 - 0.5)
end
if v16 != nil
lpfco = v16
end
sleep 0.3
end