Arduino Unoで「ずぼら "Generative" MIDIシーケンサ」作ってみた
できること
電源スイッチ1個「パチっ」と入れるだけで、すぐにシンセサイザーで音作りを始めたい!!
きっかけ
アナログシンセ(Prophet-5=ガキからの夢。だがRev.4。近頃復活した。これぞ一生の宝物じゃよ)のデスクトップ型を買った=鍵盤がついてないので、なにか音を鳴らすための道具が必要。
目標
曲を作り込むのではなく、シンセのツマミをテキトーにいじって「出てくる音を愛でる」のが目的なので
● 電源入れるだけで自動的に適当にいい感じの音列を鳴らし始める
● 適当にいい感じのシーケンスを自動生成してくれる
● 安くてカスタマイズ自由なシーケンサが欲しい
というとにかくずぼらな欲望で、「Arduino Unoを使ったらくちんGenerative(ジェネラティブ、生成的)MIDIシーケンサを作った」です。
なぜか作るところはずぼらにならず。
ラクするためにがんばるのはプログラマの性かしら。
見た目はこんなのです。かわいかろう。
スペック
● ローテータ1: テンポ変更
● ローテータ2: スケールの音数変更(
● 赤ボタン: 演奏停止ボタン
● 1シーケンス16音限定
● ランダムにシーケンスの一部の音がドロップ
というむちゃくちゃ単純な構成である。が、快適&楽しさこの上ない。
なにがうれしいの?
Arduinoはマイコンなので、電源を入れるといきなり走り始める(ようにプログラムした)
そして、電源をいきなりバチっと切ってもいい。
シンセもアナログのハードウェアシンセなので、電源をバチっと切っていい。(いいのか?)
つまり、とにかく、電源スイッチひとつで、いきなり音(楽)が鳴るようにしたかったんである。そして終わったらバチッとスイッチを切る。
ハードウェアたるもの、こうあってほしい!
システム構成
とにかくラクに、安く(?)、小さく、速く、なので、システムはこういう構成。
壁に挿しているスイッチ付きコンセントを入れると、全機材の電源が入って、いきなりシーケンサも走って音が鳴り始めるのである。
中心にProphet-5があるのに、周辺のものがやたら安っぽいのは、ゴージャスなのかチープなのか。(※細野さんが坂本さんに怒られるベッドルーム的なやつだな)
テンポとシーケンスの音数を調整するだけで、なかなか楽しめまっす。
Generativeシーケンスの仕組み
Generativeとかえらそーなこといってますが、なんの工夫もないです。
現状、つまみを回してシーケンスに採用する音数を1個〜10個の範囲で変えられるようにしてます。
ドリアンモードから下記の順番で、シーケンス生成に採用するようになってます。
この辺のアルゴリズムはテキトーなので、こういうの好きな人は自分好みに書き換えるともっといいものできるはず! これがハードウェアだけどソフトウェアなArduinoのいいとこじゃ。
Arduino Sketch
Arduino Code
#include <SoftwareSerial.h>
/* Main Loop */
volatile bool seqRunning = true;
void setup() {
Serial.begin(38400);
setupStopButton();
setupRotaryEncoders();
setupMIDI();
}
void loop() {
loopRotaryEncoders();
seqRunning = loopStopButton(seqRunning);
if (seqRunning) {
playSequenceNote();
} else {
delay(300);
}
}
/* MIDI Sequencer */
SoftwareSerial MIDI(5, 6); // RX, TX
#define MIDI_CH 1
#define MIDI_ON 0x90|(MIDI_CH - 1)
#define MIDI_OFF 0x80|(MIDI_CH - 1)
const byte VELOCITY = 127;
const byte MAX_NOTE_COUNT = 16;
const byte MAX_AVAILABLE_NOTE_NUM = 10;
const byte C4 = 60; // MIDI C4 NOTE
volatile byte sequenceArray[] = {
C4, C4, C4, C4,
C4, C4, C4, C4,
C4, C4, C4, C4,
C4, C4, C4, C4
};
volatile byte maxModeNoteNum = 1;
volatile int bpm = 60;
volatile byte currentStep = 0;
void setupMIDI() {
currentStep = 0;
randomSeed(analogRead(0)); //Noise as Random Seed
Serial.println("MIDI Setup");
MIDI.begin(31250);
}
int changeTempo(int change) {
if (change != 0) {
bpm += change * 10;
bpm = min(max(bpm, 40), 560);
Serial.print("BPM: ");
Serial.println(bpm);
}
return bpm;
}
const byte DORIAN_MODE_SCALE[] = {
0, 7, 3, 5, 2, 9, 10, 4, 11, 1
};
byte changeModeNoteNum(byte increasedNoteNum) {
byte old_maxModeNoteNum = maxModeNoteNum;
if (increasedNoteNum != 0) {
maxModeNoteNum += increasedNoteNum;
maxModeNoteNum = min(max(maxModeNoteNum, 1), 10);
if (old_maxModeNoteNum != maxModeNoteNum) {
reGenerateMIDISteps();
}
}
return maxModeNoteNum;
}
void reGenerateMIDISteps() {
for (byte i=0; i<MAX_NOTE_COUNT; i++) {
if (maxModeNoteNum > 1) {
byte note_idx = byte(random(maxModeNoteNum));
sequenceArray[i] = C4 + DORIAN_MODE_SCALE[note_idx];
} else {
sequenceArray[i] = C4;
}
}
}
void playSequenceNote() {
byte MIDINote = sequenceArray[currentStep];
int rnd = random(100);
if (rnd >= 3) {
// Drop 3% Notes for Change
SEND_MIDI(MIDI_ON, MIDINote);
delay(int(20000 / bpm));
SEND_MIDI(MIDI_OFF, MIDINote);
delay(int(40000 / bpm));
} else {
delay(int(60000 / bpm));
}
currentStep++;
if (currentStep >= MAX_NOTE_COUNT) {
currentStep = 0;
}
}
void SEND_MIDI(byte cate, byte note) {
if (note < 20) return;
byte data[3] = {cate, note, VELOCITY};
for (byte i = 0 ; i < 3 ; i++) {
MIDI.write(data[i]);
}
}
void MIDIPanic() {
MIDI.write(123);
Serial.println("MIDI Panic 123 sent.");
}
// Two Clickable Rotary Encoders
// using code on https://jumbleat.com/2017/10/22/encoder_3/
#include <MsTimer2.h>
#define TIM MsTimer2
#define ENCA_1 4
#define ENCA_2 12
#define ENCB_1 7
#define ENCB_2 8
#define ENC_NUM 2
#define ENC_TOLERANCE 25
#define TIMER_INTVAL 2
#define ENC_REPEAT ENC_TOLERANCE * (TIMER_INTVAL / 2)
byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2};
#define ECUR B00000011 // current enc position
#define EHOM B00001100 // previous enc position
#define ECHG B01000000 // flag that encoder has changed
#define ERES B10000000 // encoder resolution. '1' is for quarter resolution
volatile int enc_count[ENC_NUM];
volatile int enc_old[ENC_NUM];
volatile byte enc_status[ENC_NUM];
byte ELAYER(byte layer) {
for (byte i = 0 ; i < 8 ; i++) if ((layer >> i) & B00000001) return i;
}
byte EMASK(byte val, byte layer) {
byte tmp = (val & layer) >> ELAYER(layer);
return tmp;
}
bool ENC_CHECK() {
bool yes_no = false;
for (byte i = 0 ; i < ENC_NUM ; i++) yes_no |= ENC_CHECK(i);
return yes_no;
}
bool ENC_CHECK(byte pin) {
bool yes_no = enc_status[pin] & ECHG;
return yes_no;
}
void SET_ENC_RES(byte pin, bool res) {
if (pin < ENC_NUM) {
enc_status[pin] = (enc_status[pin] & ~ERES) | (res << ELAYER(ERES));
enc_status[pin] &= ~ECHG;
}
}
void ENC_RESET() {
for (byte i = 0 ; i < 5 ; i++) ENC_READ();
for (byte i = 0 ; i < (ENC_NUM) ; i++) {
enc_status[i] |= ECHG; // kick flag
enc_count[i] = 0;
ENC_COUNT(i);
enc_status[i] |= ECHG; // kick flag
}
}
int ENC_COUNT(byte pin) {
int enc_val = 0;
char vec = (enc_status[pin] & ERES) ? 4 : 1;
if (enc_status[pin] & ECHG) {
enc_val = (enc_count[pin] - enc_old[pin]) / vec;
enc_old[pin] = enc_count[pin]; //update as previous value
enc_status[pin] &= ~ECHG; // reset counting flags
}
return enc_val;
}
void ENC_READ() {
static byte enc_gauge[ENC_NUM];
// read pins value
for (byte ii = 0 ; ii < ENC_REPEAT ; ii++) {
short pin_val[ENC_NUM];
// get current pins status of Encoder1
pin_val[0] = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1
pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0); // ENC A-2
#if(ENC_NUM > 1)
// get current pins status of Encoder2
pin_val[1] = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1
pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0); // ENC B-2
#endif
// for each encoder pins
for (byte i = 0 ; i < ENC_NUM ; i++) {
// modify order of pins value
if (pin_val[i] < 2) pin_val[i] = 1 + (pin_val[i] * -1);
enc_status[i] = (enc_status[i] & ~ECUR) + pin_val[i];
short pos_old = EMASK(enc_status[i], EHOM);
if (pin_val[i] != pos_old) {
//gauging
enc_gauge[i] = min(ENC_TOLERANCE + 1, enc_gauge[i]++);
//counting
if (enc_gauge[i] >= ENC_TOLERANCE) {
// increase or decrease ?
bool dir = (pin_val[i] > pos_old) ? 1 : 0;
if (pin_val[i] == 0 && pos_old == 3) dir = 1;
else if (pin_val[i] == 3 && pos_old == 0) dir = 0;
// add count by the direction
if (dir) enc_count[i]++;
else enc_count[i]--;
boolean change_flag = false;
// forced count correction for Click-type
if (enc_status[i] & ERES) {
if (pin_val[i] == 3) {
char rem = enc_count[i] % 4;
if (rem != 0) {
enc_count[i] = (enc_count[i] / 4) * 4;
if (abs(rem) > 2) {
char vec = (enc_count[i] < 0) ? -1 : 1;
enc_count[i] += 4 * vec;
}
}
change_flag = true;
}
} else {
change_flag = true;
}
if (enc_count[i] == enc_old[i]) change_flag = false;
// update current pos to home pos
enc_status[i] = (enc_status[i] & ~EHOM) | (pin_val[i] << ELAYER(EHOM));
// set count change flag
enc_status[i] |= change_flag << ELAYER(ECHG);
enc_gauge[i] = 0;
}
} else {
enc_gauge[i] = max(0, enc_gauge[i]--);
}
}
}
}
void setupRotaryEncoders() {
for (byte i = 0 ; i < (ENC_NUM * 2) ; i++)
pinMode(enc_pins[i], INPUT_PULLUP);
SET_ENC_RES(0, 1); // SET_ENC_RES(encoder number, click = 1 / non_click = 0)
SET_ENC_RES(1, 1); // SET_ENC_RES(encoder number, click = 1 / non_click = 0)
TIM::set(TIMER_INTVAL, ENC_READ);
TIM::start();
//Serial.begin(38400);
delay(1000);
ENC_RESET();
}
bool loopRotaryEncoders() {
if (ENC_CHECK()) {
// Rotary 1 sets Mode Complexity
changeModeNoteNum(ENC_COUNT(0));
// Rotary 2 sets Tempo
changeTempo(ENC_COUNT(1));
return true;
}
return false;
}
#define BUTTON_PIN 3
void setupStopButton() {
pinMode(BUTTON_PIN, OUTPUT);
}
bool loopStopButton(bool running) {
if (digitalRead(BUTTON_PIN) == HIGH) {
if (running) {
MIDIPanic();
return false;
} else {
return true;
}
}
return running;
}
実演
スイッチひとつですぐ音が鳴り始めますね。
テンポはわかりやすいとして、最初のローテータを回すと出てくる音数が増えているのがわかるでしょうか・・・? 回転の向きが逆なのはご愛嬌。
まとめ
壁の電源スイッチ1つ入れると音が鳴り始め、つまみをいきなりいじって音づくりを楽しめ、あきたらスイッチ切って終わる、という快適環境ができます。カスタマイズしてぜひお楽しみあれ。 では❣️
この記事が気に入ったらサポートをしてみませんか?