1200円で作るSH-101型CV/GATEシーケンサー-モジュラーシンセ自作
Seeeduino xiaoを使って、モジュラーシンセサイザー のRoland SH-101タイプのCV/GATEシーケンサーを自作したので、その備忘録。
背景
自作モジュラーシンセの29作品目。
モジュラーシンセのシーケンサは種類が豊富だ。そのうちの一つが、Roland SH-101型のシーケンサ。事前に鍵盤等でフレーズをインプットして、それをクロックに合わせて再生するというもの。
Arturia keystep、mutable instruments yarns、Intellijel Designs Scalesなど、定番モジュールでSH-101型のシーケンサが採用されている。
「任意のメロディを再生する」モジュールが欲しくなったので、今回の企画に至った。
また、今回の作成するモジュールはソフトウェアを変えることで様々なモジュールに変更できるよう、汎用性を持たせている。ゲート入力1、CV入力2、CV出力4、操作はロータリーエンコーダ、OLEDを搭載することで汎用性を高めている。
制作物のスペック
ユーロラック規格 3U 6HPサイズ
電源:30mA ( at 5V )
5V単電源で動作可能。
インターフェース
step入力の場合 (REC)
ロータリーエンコーダ:左回転で前ステップに戻る。右回転で休符入力。
PUSH SW:REC終了
CLK IN:(未使用)
IN1:ステップ入力時(REC)ではCV INとして使用。
IN2:ステップ入力時(REC)ではGATE INとして使用。
OUT1:入力中のCH1 GATE出力
OUT2:入力中のCH1 CV出力(10bit)
OUT3:入力中のCH2 GATE出力
OUT4:入力中のCH2 CV出力(12bit)
step出力の場合(PLAY)
ロータリーエンコーダ:メニュー切り替え
PUSH SW:メニュー選択
CLK IN:入力時に次のstepを出力
IN1:(未使用)
IN2:(未使用)
OUT1:入力中のCH1 GATE出力
OUT2:入力中のCH1 CV出力(10bit)
OUT3:入力中のCH2 GATE出力
OUT4:入力中のCH2 CV出力(12bit)
シーケンサ機能
ステップ数:最大128step
出力チャンネル数:2ch
出力電圧範囲:0~5V
入力電圧範囲:0~5V
シーケンス出力
Clock入力があると、次ステップのCV/GATEを出力する。
RESETを選択すると、ステップが初めに戻る。
MUTEを選択すると、ステップは進むが、GATEは出力されない。
STOPを選択すると、Clock入力があってもステップは進まない。
Clock Divideでdivideレートを選択できる。指定した回数のClock入力があると、ステップが進む。
シーケンス入力
IN1にCV、IN2にGATEを接続し、外部入力装置(例えばarturia keystep)で1stepずつ入力していく。GATEがHighからLowに切り替わった瞬間の電圧をstepに記録する。
入力中にロータリーエンコーダを右回転すると、休符を入力。左回転すると入力をやり直り。
製作費
総額約1200円
---------------------------------
Seeeduino xiao 550円
OLED(SSD1306) 180円
op-amp 30円
パネル 100円
ロータリーエンコーダモジュール 100円
DACモジュール(MCP4725) 150円
他
プログラミング
外部入力を記憶して、Clockにあわせて出力するというシンプルなもの。
今回はArduino nanoではなく、Seeeduino xiaoを使用したが、Arduino向けのライブラリはそのまま流用できた。
Seeeduino xiao の電圧定格は3.3Vなので、5VのCV入力があると壊れてしまう。そのため抵抗で分圧しているのだが、抵抗の誤差により入力電圧に誤差が生じてしまう。
今回、その誤差を無くすためにキャリブレーション用変数を準備した。
float AD_CH1_calb = 1.085;//reduce resistance error
float AD_CH2_calb = 1.094;//reduce resistance error
ハードウェア
Seeeduino xiaoの電圧定格が3.3Vであるため、入力は5Vから3.3Vに変換、出力は3.3Vから5Vに変換する必要がある。
先述の通り、入力はキャリブレーションをソフトウェアに実装した。
出力は分解能をフルに生かすため、非反転増幅回路のキャリブレーションはソフトではなくハード(可変抵抗)で実装している。
GATE出力はトランジスタを使用することで、3.3Vから5Vに変換している。HIGH,LOWが反転してしまうため注意が必要。
トランジスタは電圧定格が大きい部品なので、ショットキーバリアダイオードの保護回路は削減している。
D1,D2の1uFのコンデンサは、今回は使用していない。将来の機能拡張でPWMによるCV出力をするためのリザーブだ。
宣伝:オープンソースプロジェクトの支援をお願いします
DIYモジュラーシンセのオープンソースプロジェクトを継続するために、patreonというサービスでパトロンを募集しています。
コーヒー一杯の支援をいただけると嬉しいです。
また、パトロン限定のコンテンツも配信しています。
ソースコード
粗末だが公開する。悪い点があれば教えてもらえると勉強になる。
//Display setting
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#define OLED_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
#include <Wire.h>
//Rotery encoder setting
#define ENCODER_OPTIMIZE_INTERRUPTS //counter measure of noise
#include <Encoder.h>
Encoder myEnc(6, 3);//rotery encoder library setting
float oldPosition = -999;//rotery encoder library setting
float newPosition = -999;//rotery encoder library setting
int i = 0;
bool SW = 0;
bool old_SW = 0;
bool CLK_in = 0;
bool old_CLK_in = 0;
int gate_timer1 = 0;
int gate_timer2 = 0;
float AD_CH1 = 0;
float AD_CH1_calb = 1.085;//reduce resistance error
float AD_CH2 = 0;
//float AD_CH2_calb = 1.094;//reduce resistance error
//menu setting
byte menu = 1;//1=ch1 rec/play , 2=ch1 divide , 3 = reset , 4~6 =ch2 , 7 = MUTE ch1 , 8 = STOP ch1 , 9~10 ch2
bool mode1 = 1;//0 =rec , 1 =play
bool mode2 = 1;//0 =rec , 1 =play
const byte div_ch1[7] = { 1, 2, 4, 8, 16, 32, 64};//divide rate
const byte div_ch2[7] = { 1, 2, 4, 8, 16, 32, 64};//divide rate
byte select_div_ch1 = 0;
byte select_div_ch2 = 0;
bool mute_ch1 = 0;//0=not mute , 1=mute
bool mute_ch2 = 0;//0=not mute , 1=mute
bool stop_ch1 = 0;//0=not stop , 1=stop
bool stop_ch2 = 0;//0=not stop , 1=stop
//CV setting
const int cv_qnt_out[61] = {
0, 68, 137, 205, 273, 341, 410, 478, 546, 614, 683, 751,
819, 887, 956, 1024, 1092, 1161, 1229, 1297, 1365, 1434, 1502, 1570,
1638, 1707, 1775, 1843, 1911, 1980, 2048, 2116, 2185, 2253, 2321, 2389,
2458, 2526, 2594, 2662, 2731, 2799, 2867, 2935, 3004, 3072, 3140, 3209,
3277, 3345, 3413, 3482, 3550, 3618, 3686, 3755, 3823, 3891, 3959, 4028, 4095
}; //output pre-quantize
const int cv_qnt_thr[62] = {
0, 9, 26, 43, 60, 77, 94, 111, 128, 145, 162, 179, 196, 213, 230, 247, 264, 281, 298, 315, 332, 349, 366, 383, 400, 417, 434, 451, 468, 485, 502, 519, 536, 553, 570, 587, 604, 621, 638, 655, 672, 689, 706, 723, 740, 757, 774, 791, 808, 825, 842, 859, 876, 893, 910, 927, 944, 961, 978, 995, 1012, 1024
};//input quantize
byte search_qnt = 0;
byte rec_step = 0;
byte stepcv_ch1[128] = {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, 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, 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, 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 stepcv_ch2[128] = {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, 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, 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, 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};
bool stepgate_ch1[128] = {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, 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, 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, 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};
bool stepgate_ch2[128] = {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, 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, 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, 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};
float step_ch1 = 1;
byte step_ch1_play = 1;
float step_ch2 = 1;
byte step_ch2_play = 1;
byte max_step_ch1 = 1;//count step input number
byte max_step_ch2 = 1;//count step input number
bool CV_in1 = 0; //record CV input or CH1 output trigger
bool CV_in2 = 0; //record TRIG input or CH2 output trigger
bool old_CV_in1 = 0; //for detect trig ON
bool old_CV_in2 = 0; //for detect trig ON
//display
byte disp_step1 = 0;
byte disp_step2 = 0;
bool disp_reflesh = 1;//0=not reflesh display , 1= reflesh display , countermeasure of display reflesh bussy
//-------------------------------Initial setting--------------------------
void setup() {
analogWriteResolution(10);
analogReadResolution(12);
pinMode(7, INPUT_PULLDOWN); //CLK in
pinMode(8, INPUT);//IN1
pinMode(9, INPUT);//IN2
pinMode(10, INPUT_PULLUP);//push sw
pinMode(1, OUTPUT); //CH1 gate out
pinMode(2, OUTPUT); //CH2 gate out
// OLED initialize
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
//I2C connect
Wire.begin();
}
void loop() {
old_SW = SW;
old_CV_in1 = CV_in1;
old_CV_in2 = CV_in2;
old_CLK_in = CLK_in;
//-------------------------------Rotery endoder--------------------------
newPosition = myEnc.read();
if (mode1 == 1 && mode2 == 1) { //menu select
if ( (newPosition - 3) / 4 > oldPosition / 4) { //4 is resolution of encoder
oldPosition = newPosition;
i = i - 1;
disp_reflesh = 1;
}
else if ( (newPosition + 3) / 4 < oldPosition / 4 ) { //4 is resolution of encoder
oldPosition = newPosition;
i = i + 1;
disp_reflesh = 1;
}
i = constrain(i, 1, 10);
menu = i;
}
else if ((mode1 == 0) || (mode2 == 0) ) { //REC operating
if ( ((newPosition - 3) / 4 > oldPosition / 4) && rec_step != 0) { //4 is resolution of encoder
oldPosition = newPosition;
rec_step = rec_step - 1;//while REC , turn left back step
disp_reflesh = 1;
if ((rec_step != 0) && (mode1 == 0)) {
max_step_ch1 = rec_step - 1;
}
else if ((rec_step != 0) && (mode2 == 0)) {
max_step_ch2 = rec_step - 1;
}
max_step_ch1 = constrain(max_step_ch1, 0, 127);
}
else if ( (newPosition + 3) / 4 < oldPosition / 4 ) { //4 is resolution of encoder
oldPosition = newPosition;
if (mode1 == 0) { //CH1 REC
stepgate_ch1[rec_step] = 0;//while REC , turn right set rest
stepcv_ch1[rec_step] = stepcv_ch1[rec_step - 1] ;
max_step_ch1 = rec_step;
}
else if (mode2 == 0) { //CH2 REC
stepgate_ch2[rec_step] = 0;//while REC , turn right set rest
stepcv_ch2[rec_step] = stepcv_ch2[rec_step - 1] ;
max_step_ch2 = rec_step;
}
rec_step++;//while REC , turn right rest step
disp_reflesh = 1;
}
}
//-----------------PUSH SW------------------------------------
SW = digitalRead(10);
if (SW == 1 && old_SW != 1) {
disp_reflesh = 1;
switch (menu) {
case 1:
mode1 = !mode1; //rec <-> play change
if (mode1 == 0) {// when play to rec
rec_step = 0;//reset rec_step
max_step_ch1 = rec_step;
}
if (mode1 == 1) {// when rec to play
step_ch1 = 0;//reset play step
}
break;
case 2:
select_div_ch1 ++;
step_ch1 = 0;
if (select_div_ch1 > 6) {
select_div_ch1 = 0;
}
break;
case 3:
step_ch1_play = 0;//reset count
step_ch1 = 0;//reset count
break;
case 4:
mode2 = !mode2;
if (mode2 == 0) {// when play to rec
rec_step = 0;//reset rec_step
max_step_ch2 = rec_step;
}
if (mode2 == 1) {// when rec to play
step_ch2 = 0;//reset play step
}
break;
case 5:
select_div_ch2 ++;
if (select_div_ch2 > 6) {
select_div_ch2 = 0;
}
break;
case 6:
step_ch2_play = 0;//reset count
step_ch2 = 0;//reset count
break;
case 7:
mute_ch1 = !mute_ch1;
break;
case 8:
stop_ch1 = !stop_ch1;
break;
case 9:
mute_ch2 = !mute_ch2;
break;
case 10:
stop_ch2 = !stop_ch2;
break;
}
}
//-------------------------------CH1 REC--------------------------
if (mode1 == 0) {
//when mode is REC and trig in
CV_in2 = analogRead(9) / 2048; // 0 or 1
if (old_CV_in2 == 1 && CV_in2 == 0) { //when trigger fall , record CV input
//analog read and quantize
AD_CH1 = analogRead(8) / 4 * AD_CH1_calb; //12bit to 10bit
for ( search_qnt = 0; search_qnt <= 61 ; search_qnt++ ) {// quantize
if ( AD_CH1 >= cv_qnt_thr[search_qnt] && AD_CH1 < cv_qnt_thr[search_qnt + 1]) {
stepcv_ch1[rec_step] = search_qnt;
}
}
stepgate_ch1[rec_step] = 1;
max_step_ch1 = rec_step;
//Check the input CV
intDAC(cv_qnt_out[stepcv_ch1[rec_step]]);//OUTPUT internal DAC
digitalWrite(1, LOW);// because LOW active , LOW is output
delay(5); //gate time 5msec
digitalWrite(1, HIGH);
//add step
rec_step ++;
rec_step = constrain(rec_step, 0, 127);
disp_reflesh = 1;
}
}
//-------------------------------CH2 REC--------------------------
if (mode2 == 0) {
//when mode is REC and trig in
CV_in2 = analogRead(9) / 2048; // 0 or 1
if (old_CV_in2 == 1 && CV_in2 == 0) { //when trigger fall , record CV input
//analog read and quantize
AD_CH2 = analogRead(8) / 4 * AD_CH1_calb; //12bit to 10bit
for ( search_qnt = 0; search_qnt <= 61 ; search_qnt++ ) {// quantize
if ( AD_CH2 >= cv_qnt_thr[search_qnt] && AD_CH2 < cv_qnt_thr[search_qnt + 1]) {
stepcv_ch2[rec_step] = search_qnt;
}
}
stepgate_ch2[rec_step] = 1;
max_step_ch2 = rec_step;
//Check the input CV
MCP(cv_qnt_out[stepcv_ch2[rec_step]]);//OUTPUT internal DAC
digitalWrite(2, LOW);// because LOW active , LOW is output
delay(5);
digitalWrite(2, HIGH);
//add step
rec_step ++;
rec_step = constrain(rec_step, 0, 127);
disp_reflesh = 1;
}
}
//-------------------------------OUTPUT SETTING--------------------------
CLK_in = digitalRead(7);
if (old_CLK_in == 0 && CLK_in == 1 ) {
disp_reflesh = 1;
if (mode1 == 1 && stop_ch1 != 1) { //CH1 output
intDAC(cv_qnt_out[stepcv_ch1[step_ch1_play]]);//OUTPUT internal DAC
if ((stepgate_ch1[step_ch1_play] == 1 ) && (step_ch1 == 0) && (mute_ch1 == 0) ) {
gate_timer1 = millis();
digitalWrite(1, LOW);// because LOW active , LOW is output
}
else if (stepgate_ch1[step_ch1_play] == 0) {
digitalWrite(1, HIGH);// because LOW active , HIGH is no output
}
step_ch1++;
// step_ch2++;
if (step_ch1 >= div_ch1[select_div_ch1]) {
step_ch1_play++;
step_ch1 = 0;
}
if (step_ch1_play > max_step_ch1) {
step_ch1_play = 0;
}
}
if (mode2 == 1 && stop_ch2 != 1) { //CH2 output
MCP(cv_qnt_out[stepcv_ch2[step_ch2_play]]);//OUTPUT MCP4725
if ((stepgate_ch2[step_ch2_play] == 1 ) && (step_ch2 == 0) && (mute_ch2 == 0) ) {
gate_timer2 = millis();
digitalWrite(2, LOW);// because LOW active , LOW is output
}
else if (stepgate_ch1[step_ch1_play] == 0) {
digitalWrite(2, HIGH);// because LOW active , HIGH is no output
}
// step_ch1++;
step_ch2++;
if (step_ch2 >= div_ch2[select_div_ch2]) {
step_ch2_play++;
step_ch2 = 0;
}
if (step_ch2_play > max_step_ch2) {
step_ch2_play = 0;
}
}
}
if ( gate_timer1 + 10 >= millis()) { //gate ON time is 10msec
digitalWrite(1, LOW);//because LOW active , HIGH is no output
}
else {
digitalWrite(1, HIGH);
}
if ( gate_timer2 + 10 >= millis()) { //gate ON time is 10msec
digitalWrite(2, LOW);//because LOW active , HIGH is no output
}
else {
digitalWrite(2, HIGH);
}
if (disp_reflesh == 1) {
OLED_display();//reflesh display
disp_reflesh = 0;
}
}
//-----------------------------DISPLAY----------------------------------------
void OLED_display() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
//display step
for ( disp_step1 = 0 ; disp_step1 <= max_step_ch1 ; disp_step1 ++) {
if ((step_ch1_play != disp_step1 + 1 ) || ( max_step_ch1 == disp_step1)) { //not active step -> fillRect
display.fillRect(47 + (disp_step1 % 16) * 5, 3 - stepgate_ch1[disp_step1] + disp_step1 / 16 * 4 , 4, 1 + stepgate_ch1[disp_step1] * 2, WHITE);
}
}
for ( disp_step2 = 0 ; disp_step2 <= max_step_ch2 ; disp_step2 ++) {
if (step_ch2_play != disp_step2 + 1) { //not active step -> fillRect
display.fillRect(47 + (disp_step2 % 16) * 5, 35 - stepgate_ch2[disp_step2] + disp_step2 / 16 * 4 , 4, 1 + stepgate_ch2[disp_step2] * 2, WHITE);
}
}
//display menu
if ( menu <= 3) {
display.drawTriangle(0, (menu - 1) * 9 , 0, menu * 9 - 3, 5, menu * 9 - 6, WHITE);
}
if ( menu >= 4 && menu <= 6) {
display.drawTriangle(0, menu * 9 , 0, (menu + 1) * 9 - 3, 5, (menu + 1) * 9 - 6, WHITE);
}
else if ( menu >= 7 && menu <= 8) {
display.drawTriangle(0, (menu - 7) * 9 , 0, (menu - 6) * 9 - 3, 5, (menu - 6) * 9 - 6, WHITE);
}
else if ( menu >= 9 && menu <= 10 ) {
display.drawTriangle(0, (menu - 7) * 9 + 18 , 0, (menu - 6) * 9 - 3 + 18, 5, (menu - 6) * 9 - 6 + 18, WHITE);
}
if (menu <= 6) {
display.setCursor(8, 0);
if (mode1 == 0) {
display.print("REC");
}
else {
display.print("PLAY");
}
display.setCursor(8, 9);
display.print("DIV:");
display.setCursor(32, 9);
display.print(div_ch1[select_div_ch1]);
display.setCursor(8, 18);
display.print("RESET");
display.setCursor(8, 36);
if (mode2 == 0) {
display.print("REC");
}
else {
display.print("PLAY");
}
display.setCursor(8, 45);
display.print("DIV:");
display.setCursor(32, 45);
display.print(div_ch2[select_div_ch2]);
display.setCursor(8, 54);
display.print("RESET");
}
if (menu >= 7) {
display.setCursor(8, 0);
display.print("MUTE");
if (mute_ch1 == 1) {
display.setCursor(32, 0);
display.print("on");
}
display.setCursor(8, 9);
display.print("STOP");
if (stop_ch1 == 1) {
display.setCursor(32, 9);
display.print("on");
}
display.setCursor(8, 36);
display.print("MUTE");
if (mute_ch2 == 1) {
display.setCursor(32, 36);
display.print("on");
}
display.setCursor(8, 45);
display.print("STOP");
if (stop_ch2 == 1) {
display.setCursor(32, 45);
display.print("on");
}
}
display.display();
}
//-----------------------------OUTPUT CV----------------------------------------
void intDAC(int intDAC_OUT) {
analogWrite(A0, intDAC_OUT / 4); // "/4" -> 12bit to 10bit
}
void MCP(int MCP_OUT) {
Wire.beginTransmission(0x60);
Wire.write((MCP_OUT >> 8) & 0x0F);
Wire.write(MCP_OUT);
Wire.endTransmission();
}
この記事が気に入ったらサポートをしてみませんか?