800円で作るジェネレーティブシーケンサ/モジュラーシンセ
Arduinoプログラミングに挑戦しつつ、モジュラーシンセサイザーのランダムジェネレーティブモジュールを自作したので、その備忘録。
背景
コードの書けないシステムエンジニア脱却のために始めたプログラミングの5作品目。これまで作ってきたモジュールの3/4個が音源モジュールだが、それを鳴らすCV/Gateソースが無いという事で、作成に至る。
手持ちのブランクパネルが6HPのため、大量のツマミやスイッチを配置することは不可能。シーケンスの生成は機械任せにするのがベターと考え、generative sequencerの作成に至る。(たいそうな名前だが、機能の根本はランダム信号発生器である)
制作物のスペック
ユーロラック規格 3U 6HPサイズ
電源:56mA ( at 5V ) / 51mA ( at12V )
出力:CV(0-5VでWIDTH制御可能) / Gate
レイテンシ:2ms
ステップ数:4,6,8,12,16stepより選択
総額:800円
ツマミの位置で、ランダム具合を調整できる。
12時の方向なら、全音のCV/Gateが変更される。
5時、17時の方向なら、シーケンスはロックされる。
10時、14時の方向なら、2音のみ切り替わる。といった具合。
また、フレーズの繰り返しも1~8で設定可能。
フレーズも1種類を繰り返す、2種類を交互に繰り返す、で設定可能
Music thing modular の Truing machineに大きなインスピレーションを受けている。
また、反復的な出力の機能は、Mutable instruments Marblesのデジャブ機能を意識している。
偉大な作品のエッセンスを取り入れ、自分なりにアルゴリズムを調整して作り上げた。
「自らリズム・音階を生成するアルゴリズムをプログラミング」と聞くと、現代音楽家っぽく聞こえるね。前衛音楽とか、実験音楽とか、そういうの名乗ってる人が使いそうなフレーズ。
プログラム
今回初めて使用した関数は以下の通り。
random:
ランダムと見せかけて、実は疑似ランダムな関数。マイコンをリセットすると、ランダムなはずなのに毎回同じ値が選出される。でも、その必然性もかえって面白い。
map:
Aの範囲から、Bの範囲に値を丸める。といったときに使いやすい。
12bitを10bitにまとめるとか。CVのWIDTHの設定で使用した。
switch case:
変数が多く分岐が多いときに if に代わって登場させた。プログラムの構造がシンプルになるメリットがある。でも、ifにできてswitchにできないことは多い。使い方次第。
ハード
外付けの10bit DACを使用した。
Arduino内蔵のPWMでもCVは生成可能だが、"リプルと応答性のトレードオフが、音楽的に成立しない"という欠点があり、見送ることとなった。
具体的には、
リプルを数mVに収めようとすると、応答性が数十msとなり応答性が成立しない。
応答性を数msに収めようとすると、リプルが数十mVとなり音階が不安定になる。
といった具合である。
PWMによる疑似定電圧は、モジュラーシンセのCVソースの使用に向かない。DAC使うべし。
ソースコード
粗末ではあるが公開する。
#include <SPI.h>
//#include <EEPROM.h>
byte stgAgate[2][16] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
byte stgBgate[2][16] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
int stgAcv[2][16] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
int stgBcv[2][16] = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
byte i = 0;
byte j = 0;
byte k = 0;
byte gate_set = 1;
byte gate_count = 0;
byte old_gate_count = 0;
byte chance_set = 3;//メインツマミの読み取り値。chanceに数字を渡す
byte chance = 1;//chance_setとlengthから決定。抽選で音が変わる数
byte lottery_stepA = 2;//stgA抽選
byte lottery_stepB = 2;//stgB抽選
byte lottery_done = 2;//抽選を頭一回だけに限定する
const int LDAC = 9; // ラッチ動作出力ピン
int sub_knob = 0 ;//サブツマミの読み取りAD値
int old_sub_knob = 0;//SW切り替え直後、切り替え前のAD値を反映させないため、古いADを記憶
byte refrain_set = 1;//リフレインの設定回数。AD値から導出。
byte refrain_count = 0;//リフレインした回数をカウント。指定値に達すると0に戻る。
byte length_set = 4;//レングスの数値
int width_max = 1023;
int width_min = 0;
int main_knob = 0 ;//メインツマミの読み取りAD値
byte repeat_set = 0;//リピートの設定回数。AD値から導出。
byte repeat_count = 0;//リピートした回数をカウント。指定値に達すると0に戻る
byte mode_swA = 0;
byte mode_swB = 0;
byte mode_set = 0; //0=length,1=width,2=refrain
byte old_mode_set = 4;//SW切り替え直後、切り替え前のAD値を反映させないため、SW状態を記憶
void setup() {
pinMode(LDAC, OUTPUT) ;//DAC通信用
pinMode(SS, OUTPUT) ;//DAC通信用
pinMode(7, INPUT); //gate_input
pinMode(5, OUTPUT); //gate_output
pinMode(6, OUTPUT);//LED_output
pinMode(2, INPUT_PULLUP);//mode_swA
pinMode(4, INPUT_PULLUP);//mode_swB
//
// length_set = EEPROM.read(0);//前回終了時のデータ読み出し
// refrain_set = EEPROM.read(1);//前回終了時のデータ読み出し
// width_max = EEPROM.read(2);//前回終了時のデータ読み出し
// width_min = EEPROM.read(3);//前回終了時のデータ読み出し
Serial.begin(57600);
SPI.begin();
SPI.setBitOrder(MSBFIRST) ; // ビットオーダー
SPI.setClockDivider(SPI_CLOCK_DIV4) ;// クロック(CLK)をシステムクロックの1/4で使用(16MHz/4)
SPI.setDataMode(SPI_MODE0) ; // クロック極性0(LOW) クロック位相0
delay(50);
for (i = 0; i < 2 ; i = i + 1) {
for (j = 0; j < 16 ; j = j + 1) {
stgAgate [i][j] = random(2);
stgBgate [i][j] = random(2);
stgAcv [i][j] = random(1024);
stgBcv [i][j] = random(1024);
}
}
}
void loop() {
//-------------外部入力状態の読み取り-----------------
gate_set = digitalRead(7);//gate_input状態を読み取り
main_knob = analogRead(2);
sub_knob = analogRead(4);
mode_swA = digitalRead(2);
mode_swB = digitalRead(4);
old_mode_set = mode_set;
if ( mode_swA == 1 && mode_swB == 0 ) {
mode_set = 0;//length
}
else if ( mode_swA == 1 && mode_swB == 1 ) {
mode_set = 1;//width
}
else if ( mode_swA == 0 && mode_swB == 1 ) {
mode_set = 2; //refrain
}
if (old_mode_set != mode_set ) {
old_sub_knob = sub_knob;
// EEPROM.update(0, length_set); //変更データをEEPROMに書き込み
// EEPROM.update(1, refrain_set); //変更データをEEPROMに書き込み
// EEPROM.update(2, width_max); //変更データをEEPROMに書き込み
// EEPROM.update(3, width_min); //変更データをEEPROMに書き込み
}
//-------------refrainの設定----------------------
if ( mode_set == 2 && abs(old_sub_knob - sub_knob ) > 30) {
old_sub_knob = 1200;//abs(old_sub_knob - sub_knob ) > 30が絶対に成立するため
if ( sub_knob < 25 ) {
refrain_set = 1;//refrainしない
}
else if ( sub_knob < 313 && sub_knob >= 26 ) {
refrain_set = 2;//1回繰り返し
}
else if ( sub_knob < 624 && sub_knob >= 314 ) {
refrain_set = 3;//2回繰り返し
}
else if ( sub_knob < 873 && sub_knob >= 625 ) {
refrain_set = 4;//3回繰り返し
}
else if ( sub_knob >= 874 ) {
refrain_set = 8;//7回繰り返し
}
}
//----------------length設定---------------------
if ( mode_set == 0 && abs(old_sub_knob - sub_knob ) > 30) {
old_sub_knob = 1200;//abs(old_sub_knob - sub_knob ) > 30が絶対に成立するため
if ( sub_knob < 25 ) {
length_set = 4;
}
else if ( sub_knob < 313 && sub_knob >= 26 ) {
length_set = 6;
}
else if ( sub_knob < 624 && sub_knob >= 314 ) {
length_set = 8;
}
else if ( sub_knob < 873 && sub_knob >= 625 ) {
length_set = 12;
}
else if ( sub_knob >= 874 ) {
length_set = 16;
}
}
//-------------widthの設定----------------------
if ( mode_set == 1 && abs(old_sub_knob - sub_knob ) > 30) {
old_sub_knob = 1200;//abs(old_sub_knob - sub_knob ) > 30が絶対に成立するため
width_max = 612 + sub_knob * 4 / 10;
width_min = 412 - sub_knob * 4 / 10;
}
//-------------repeatの設定----------------------
if ( main_knob < 5 ) {
repeat_set = 0;//repeatしない
chance = 0;
}
else if ( main_knob < 111 && main_knob >= 6 ) {
repeat_set = 0;//repeatしない
chance = 1;
}
else if ( main_knob < 214 && main_knob >= 112 ) {
repeat_set = 0;//repeatしない
chance = 2;
}
else if ( main_knob < 376 && main_knob >= 215 ) {
repeat_set = 0;//repeatしない
if ( length_set == 4 || length_set == 6 ) {
chance = 3;
}
else {
chance = 4;
}
}
else if ( main_knob < 555 && main_knob >= 377 ) {
repeat_set = 0;//repeatしない
chance = length_set;
}
else if ( main_knob < 700 && main_knob >= 556 ) {
repeat_set = 1;//repeatしない
if ( length_set == 4 || length_set == 6 ) {
chance = 3;
}
else {
chance = 4;
}
}
else if ( main_knob < 861 && main_knob >= 701 ) {
repeat_set = 1;//repeatしない
chance = 2;
}
else if ( main_knob < 970 && main_knob >= 862 ) {
repeat_set = 1;//repeatしない
chance = 1;
}
else if ( main_knob < 1024 && main_knob >= 971 ) {
repeat_set = 1;//repeatしない
chance = 0;
}
//----------------抽選処理開始---------------------
if (refrain_count == 0 && gate_count == 1 && lottery_done == 0 && repeat_count == 0 ) {
lottery();//抽選処理開始
lottery_done = 1;
}
//-----------------stgシーケンス出力-----------------
switch (repeat_count) {
case 0:
switch (gate_set) {
case 0: //gate_inputがLOWのとき
digitalWrite(5, LOW); //gate_outputをLOWにする
old_gate_count = gate_count;
break;
case 1: //gate_inputがHIGHのとき
if (old_gate_count == gate_count) {
gate_count ++;
if ( gate_count > length_set ) {
gate_count = 1;
refrain_count ++;
if (refrain_count >= refrain_set ) {//stgAからstgBへの橋渡し
repeat_count++;
refrain_count = 0;
lottery_done = 0;
}
}
WriteRegister(map(stgAcv[0][gate_count - 1], 0, 1023, width_min, width_max));
digitalWrite(5, stgAgate[0][gate_count - 1]); //CV出力を出してからゲート出力
analogWrite(6, stgAcv[0][gate_count] / 4);
break;
}
break;
}
case 1:
if (repeat_set == 0 ) {
repeat_count = 0;
}
else {
switch (gate_set) {
case 0: //gate_inputがLOWのとき
digitalWrite(5, LOW); //gate_outputをLOWにする
old_gate_count = gate_count;
break;
case 1: //gate_inputがHIGHのとき
if (old_gate_count == gate_count) {
gate_count ++;
if ( gate_count > length_set ) {
gate_count = 1;
refrain_count ++;
if (refrain_count >= refrain_set ) {//stgBからstgAへの橋渡し
repeat_count++;
refrain_count = 0;
if ( repeat_count >= 2 ) {
repeat_count = 0;
lottery_done = 0;
}
}
}
WriteRegister(map(stgBcv[0][gate_count - 1], 0, 1023, width_min, width_max));
digitalWrite(5, stgBgate[0][gate_count - 1]); //CV出力を出してからゲート出力
analogWrite(6, stgBcv[0][gate_count] / 4);
break;
}
break;
}
}
//開発用
// Serial.print( sub_knob);
// Serial.print(",");
// Serial.print( length_set);
// Serial.println("");
}
}
void lottery() {
if ( chance != 0 ) {
for (k = 0; k <= chance ; k = k + 1) {
lottery_stepA = random(length_set);
stgAgate[0][lottery_stepA] = 1 - stgAgate[0][lottery_stepA];
stgAcv[0][lottery_stepA] = random(1024);
lottery_stepB = random(length_set);
stgBgate[0][lottery_stepB] = 1 - stgBgate[0][lottery_stepB];
stgBcv[0][lottery_stepB] = random(1024);
}
}
}
void WriteRegister(int dat) {
digitalWrite(LDAC, HIGH) ;
digitalWrite(SS, LOW) ;
SPI.transfer((dat >> 6) | 0x30) ; // Highバイト(0x30=OUTA/BUFなし/1x/シャットダウンなし)
SPI.transfer((dat << 2) & 0xff) ; // Lowバイトの出力
digitalWrite(SS, HIGH) ;
digitalWrite(LDAC, LOW) ; // ラッチ信号を出す
}
この記事が気に入ったらサポートをしてみませんか?