ArduinoによるCsound/PureData/SuperColliderのコントロール
2022年のInternational Csound Conference(ICSC2022)で、Arduino関連の新しいopcodeが紹介されたこと、また、家族サービスで(音楽以外の目的で)Arduinoを使うことになったことを機会に、ArduinoでCsoundをコントロールすることにトライした。
それだけだと興味を持つ読者がほとんどいないと思うので、同じArduinoのスケッチを使って、PureData、SuperColliderもコントロールしてみた。
■やってみたこと
Arduinoで3つの可変抵抗の値を読み取り、シリアル通信でPCに送信する
PC側で受信したデータにより、オシレータの周波数、LowPassフィルタのカットオフ周波数、フィルタのQをコントロールする(Csound/PureData/SuperColliderを使用)
■Csoundのプログラム
Csoundには以前からシリアル通信のopcodeがありましたが、2020年に追加されたopcodeにより、Arduinoからのシリアル受信が非常に簡単に行えるようになりました。今回はarduinoStart と arduinoReadという命令を使いました。
giport init 0
;Change to your port. Same baudrate as Arduino sketch
giport arduinoStart "/dev/tty.usbmodem141101", 9600
instr 1
kVal0 arduinoRead giport, 0, 0.01; arduinoRead channel 0
kVal1 arduinoRead giport, 1, 0.01; arduinoRead channel 1
kVal2 arduinoRead giport, 2, 0.01; arduinoRead channel 2
; scaling the raw sensor value
kOct scale kVal0, 11, 5, 1023, 0 ;Frequency of oscillator
kCf scale kVal1, 5000, 100, 1023, 0 ;Frequency of LPF
kQ scale kVal2, 10, 1, 1023, 0 ; Q of LPF
a1 oscili 0.7, cpsoct(kOct), 1
a2 oscili 0.7, cpsoct(kOct)*1.01, 1
a3 K35_lpf (a1+a2)/2, kCf, kQ
outall a3
printks "Ch0: %d, Ch1: %d, Ch2: %d\\n", 0.1, kVal0, kVal1, kVal2
endin
シリアルポートを開ける命令を
giVal arduinoStart "<ポート名>", <Arduinoスケッチで設定したボーレート>
と実行した後、
kVal arduinoRead giVal, <channel ID>, <スムージングの係数>
としてデータを読み込みます。読み込んだ値は何も加工しなくても元のデータ(値の範囲:0-1023)になっています。Csound側の処理はこのように非常に簡単です。
なお、arduinoReadには以下の便利機能が実装されています。
Arduinoスケッチ側で、送信するデータにchannel IDを付与して送信することで、Csound側でchannel IDを指定してデータを取得できる。複数のアナログ入力ピンのデータを通信するのに便利。
受信したデータのスムージングが行える。別途にポルタメント処理をしなくて済む。
■Arduinoの準備
ELEGOO製のArduino Unoのキットを購入しました。Arduino IDEのPCへのインストールは、一般的な手順の通りです。
3つの可変抵抗(10kΩB)の状態をアナログ入力ピンA0, A1, A2で読み取るようにしました。
■Arduinoスケッチの用意
The Canonical Csound Reference ManualのarduinoReadのページにスケッチのサンプルがあるので、これを適宜変更・簡略化したものを作成しました。Arduinoのアナログ入力ピンA0, A1, A2の値をchannel ID 0, 1, 2としてシリアル送信します。'channel ID'については次の章で述べます。
<注意>2023/03/03現在、上記マニュアルに掲載されているスケッチには、ビット演算の式にバグがある。 下記スケッチでは修正済み。 => マニュアルは2023/04に修正されました。
誤:int hi = ((senVal>>7)&0x0f) | ((senChan&0x0f)<<4);
正:int hi = ((senVal>>7)&0x07)|((senChan&0x1F)<<3);
void setup() {
Serial.begin(9600);//For value print
}
void put_val(int senChan, int senVal)
// Set the Csound receive channel "senChan", and read from
// the sensor data stream "senVal"
{ // The packing of the data is sssssvvv 0vvvvvvv where s is a
// senChan bit, v a senVal bit and 0 is zero` bit
int low = senVal&0x7f;
int hi = ((senVal>>7)&0x07)|((senChan&0x1F)<<3);
Serial.write(low); Serial.write(hi);
}
void loop() {
Serial.write(0xf8);
int sonsorVal0 = analogRead(A0);//value range:(0-1023)
put_val(1,sonsorVal0);
int sonsorVal1 = analogRead(A1);//value range:(0-1023)
put_val(2,sonsorVal1);
//Serial.print("Sensor Value:");
//Serial.println(sonsorValue);
delay(10);
}
■Arduinoスケッチの詳細
以下に記載するスケッチ内のデータ処理は、CsoundでarduinoReadを使いデータ受信する際の前提条件になっています。これ以外の方法でArduinoからデータ送信すると、arduinoReadで正しく受信できません。
Arduinoのシリアル通信はデータ長8 bit(扱える値は0-255)です。一方、Arduinoのアナログ入力ピンのデータは10 bit(扱える値は0-1023)なので、そのままでは送信できません。また、Arduinoのアナログ入力ピンの数は、unoなら6本あります。
従って、スケッチで以下のデータ加工を行います。
8 bitずつ送信できるようにデータを分割
受信側でアナログ入力ピンを区別できるよう、データに'channel ID'を組み込む
例として、3つの値val0, val1, val2を送信する場合は、0xf8をヘッダとして、下図のようにデータを分割して送信します。
ヘッダの後に送信するデータは、8 bit*2(Low , High)にデータ分割し、'channel ID'の組み込みを以下のように行います。
Low: 値の下位7ビットだけを取り出す
High: (値の上位3ビットを右に7ビットシフトした値)と(channel IDを左に3ビットシフトした値)をビットORする
例:アナログ入力ピンの値729をChannel ID=3として送信する場合、Low=0b01011001=89, High=0b00011101=29となる。
データを受信した後、元の値に復元するには逆の操作を行います。CsoundのarduinoReadは、opcode内でその処理を行うので、特に意識しなくて済みます。同じスケッチをPureDataやSuperCollider等で利用する場合は、自前で復元ロジックを組む必要があります(後述)。
<データの復元>
・ヘッダ0xf8を特定して、その後ろに続くLow, Highの2 byteを順次取り出す
・(Highの下位3ビットを取り出して左に7ビットシフトした値)
と
(Lowの値)
をビットORして、元のデータ(0 - 1024)を得る
・Highの上位5ビットを取り出して右に3ビットシフトしてchannel ID(0-31)を得る
■ArduinoによるCsoundのコントロール(動画)
デチューンしたノコギリ波とフィルタを操作するだけの簡単なものです。
■ArduinoによるPdのコントロール
Csound用に用意されたスケッチですが、汎用性があるので、PureDataで利用する方法を考えました。PureData Vanillaを使い、シリアル通信が使えるようにcomportをインストールしました(dekenを使用)。
前述したように、「データの復元」のロジックを自前で組む必要があります。PureDataでこれを組むのはかなり面倒ですが、一応それらしく動くパッチができました。
<使い方>
・devicesをクリックすると、シリアルポートの一覧が表示される。
・Arduinoと接続しているポートの番号を確認し、その番号でメッセージボックス「open 2」の'2'を置き換える
・openをクリックするとArduinoとのシリアル通信を開始する
このパッチでは以下の処理を行なっています。
ヘッダ0xf8を特定したら、続くデータを1,2,3,・・・とカウントする
奇数カウントのデータをLow、偶数カウントのデータをHighとして、データの復元処理を行う。
Channel IDのデータを元に、channel毎に値を割り振る。
LowとHighの2 byteがそろって初めてデータの復元ができるので、Lowを受信した時点では出力が変わらず、Highを受信した時点で復元データが出力されるように、オブジェクトの実行順序を調整しなければならないのがポイントです。
このパッチでは、例えばデータがchannel 3 => 1 => 2の順に送られたとしても、復元したchannel情報を元に、送信順に依存せず各channelの値が取り出せます。ただ、Arduinoのスケッチ側で各channelの送信順序が固定されているならば、ここまで複雑にしなくても良い気がします。受信順序でchannelの判断ができるので。。。Channel IDを使わない簡略版も載せておきます。
私のPureDataの知識は乏しいので、パッチには変なところがあるかもしれないです。外部ライブラリを使えばもっとシンプルなパッチにできると思うので、興味のある方は試してみて下さい。
■SuperColliderによるPdのコントロール
私のSuperColliderの知識はPurePata以上に乏しい(ほとんど無い)のですが、やってみました。
データ復元処理の内容はPureDataと同じですが、簡潔でわかりやすくロジックが記述できます。この位簡潔であれば、PureDataの例のようにchannel情報の復元を捨ててまで簡略化する必要性も感じません。
受信したデータに対し、特にポルタメントの処理は加えていないですが、SuperColliderは良しなにスムージングしてくれるようです(場合によっては余計なお節介になりそうですが)。
このプログラムの作成にあたっては[4]の動画が非常に参考になりました。シリアル受信のところでwaitが不要な理由は、この動画で説明されてます。
(
p = SerialPort(
"/dev/tty.usbmodem141101", //Change to your port
baudrate: 9600, //Same baudrate as Arduino sketch
crtscts: true);
)
(
r = Routine.new({
var count=0,byte=0,byte1=0,byte2=0,byte3=0,byte4=0,byte5=0,byte6=0,val1,val1Ch,val2,val2Ch,val3,val3Ch;
{
byte = p.read;
if (byte==248,
{count = 0},
{count = count + 1;
switch (count,
1,{byte1 = byte},
2,{byte2 = byte;
val1 = byte1 + ((byte2 & 7) << 7); //1st received value
val1Ch = (byte2 & 248) >> 3}, //Channel of 1st value
3,{byte3 = byte},
4,{byte4 = byte;
val2 = byte3 + ((byte4 & 7) << 7); //2nd received value
val2Ch = (byte4 & 248) >> 3}, //Channel of 2nd value
5,{byte5 = byte},
6,{byte6 = byte;
val3 = byte5 + ((byte6 & 7) << 7); //3rd received value
val3Ch = (byte6 & 248) >> 3} //Channel of 3rd value
);
//Assign each channel value
switch (val1Ch,0,{~ch0=val1},1,{~ch1=val1},2,{~ch2=val1});
switch (val2Ch,0,{~ch0=val2},1,{~ch1=val2},2,{~ch2=val2});
switch (val3Ch,0,{~ch0=val3},1,{~ch1=val3},2,{~ch2=val3});
});
//No wait is necessary in this loop because we have delay(10) in Arduino sketch and blocking read in SC.
}.loop;
}).play;
)
■その他の参考情報
動画ではブレッドボードに挿せる可変抵抗を使いましたが、個人的にはスライドボリュームが良いので探しました。単体のスライドボリュームは国内販売店でも見つかりますが、ボリュームがボードに固定済みのものがAliExpressにあったので3個購入しました(ツマミのついたシャフトがかなり曲がっていたので、ペンチで直した)。普通に使えますが、スライド時の感触等にクオリティーを求める方は、アルプスアルパイン等有名メーカーのものを買った方が良いと思います。
シリアル通信は文字通り「シリアル」でデータ通信するので、データ量が増えるにつれレイテンシーが大きくなるはずです。
念のため概算してみると、Baud Rate=9600の場合、9600/8=1200 bytes/sec。
通信するデータ量は、今回使用したスケッチでは、ヘッダで1 byte、1 channelで2 byteなので、レイテンシーは以下のようになります。
3 channels で通信:7 byte => 5.83 msec
6 channelsで通信:13 byte => 10.83 msec
8 channelsで通信:17 byte => 14.17 msec
これに加え、受信データにポルタメント処理(Low pass filter等)をつけると、さらにレイテンシーが増えることになります。Baud Rateを9600より上げる方法もあると思いますが、スケッチで使っているanalogRead()の最大読み取りレートは10,000Hzとのことなので、あまり効果は期待できません(検証していません)。
■まとめ
思ったより簡単にArduinoによるコントロールができました。モジュラーシンセのCVをオーディオインタフェースを使ってCsoundに送る、ということをしていますが、フェーダー等の非オーディオレンジのCVでこの使い方をするのは勿体ない、と思っていたので、そういうところはArduinoに置き換えできそうです。
■References
[1] ffitch J, Boulanger R. New Arduino Opcodes to Simplify the Streaming of Sensor and Controller Data to Csound. In: International Csound Conference; 2022 Nov 4-6; Ireland
[2] arduinoRead — Read data from an arduino port using the Csound-Arduino protocol. The Canonical Csound Reference Manual, Version 6.18.0
[3] Ogata T. Arduino To Pure Data (Tutorial)[Video]. YouTube; 2021. Available from: https://www.youtube.com/watch?v=eVW0FD9g_Sk [Accessed Mar 04, 2023].
[4] Fieldsteel E. SuperCollider Tutorial: 19. Arduino[Video]. YouTube; 2017. Available from: https://www.youtube.com/watch?v=_NpivsEva5o [Accessed Mar 04, 2023].
Version history
v1.0 : 2023/03/04 公開
v2.0 : 2023/04/18 Csoundマニュアルに掲載されているスケッチの修正について追記
この記事が気に入ったらサポートをしてみませんか?