#1. 電子楽器が作りたい
youtubeに上がっていたこの動画。LOOKMUMNOCOMPUTERという御茶目なアーティスト名の彼の動画はいつも刺激的で、気の狂ったテンションで紹介するDIYの電子楽器はひときわ目立っている。
######あぁ、俺も電子楽器が作りたい
突如そう感じた私tueksは部屋に転がっていたArduinoを引っ張り出し2週間ほど慣れないプログラミングと格闘していました。
そしてついに昨晩、私のArduinoからパーカッションの音を聴くことができたので共有します。
私の本業はアナログ回路設計なのでメッシーなコードをインターネット上に散りばめることになりますが誰かのお役に立てれば幸いです。
#2.製作物
「シンセを作る」と言うとノイズとかブーンとかうにゅぉーんとか変な音出して喜んでいる変態的なイメージがあったので、「パキッと締まりのある音を作ろう」をコンセプトにドラムシーケンサを作ります。
######今回はキック、スネア、ハイハット、カウベルの音作り編です。
百聞は一見に如かず、というわけで以下の動画をご覧ください。
キックの音 pic.twitter.com/Jn3Hd82Rv5
— tueks (@tueks3) January 27, 2020
スネアっぽい音 pic.twitter.com/Rtn6jxnRKH
— tueks (@tueks3) January 27, 2020
ハイハットの音 pic.twitter.com/toGBHPVJbr
— tueks (@tueks3) January 27, 2020
cowbellっぽくなった?
— tueks (@tueks3) January 29, 2020
カウベルって牛さんに付けるベルだったんだな pic.twitter.com/JYsOXwavR0
Arduinoがパーカッションの音色を奏でているではありませんか!
どうやってこの音を鳴らしたのか、ヒントはアナログシンセサイザーにありました。作り方を見ていきましょう。
#3. Mozziライブラリ
Arduinoを使用した電子楽器を始めるにあたりとても役に立ったのがこのMozziというライブラリ。
おそらくMozziの開発者がいなければ私は今もはんだごてを握り、緻密な(そして大変面倒な)アナログシンセサイザーを作っていたことでしょう。これは決してハードウェアを否定しているわけではなく、プログラミングを始めたばかりの私でも2週間ほどで納得のいく音が出せるくらいにMozziは素晴らしいライブラリだということです。
######MozziはArduinoをシンセサイザー用にカスタマイズできる非常に強力な音声合成ライブラリです。
Mozziの導入方法や使い方は調べてみてください。簡単に日本語の情報がヒットしますよ。
中でもこのサイトは非常に参考になりました。
私は本当にプログラミング初心者ですから、このサイトのAuthorであるyoshihito-nakanishi氏の書籍を軸にMozziの全体の枠組みを理解し、足りない部分をインターネットで補完していきました。これは私の電子楽器バイブルといえるでしょう。
#4. 接続
郷に入っては郷に従えというわけでQiitaでよく見る配線図エディタFritzingを使用して実体配線図を書いてみました。もちろん、どこに何がつながっているかをもっとシンプルに捉えたい人向けに回路図も添付します。
######第5章で示す3つのコードは、すべてこの回路で動かしています。
回路図に変な点線が出てますが、あまり気にしないで下さい。全ての線は接続されています。
#5.コード
######キック
#include <MozziGuts.h> // Mozziの基本ヘッダファイル
#include <Oscil.h> // オシレータのテンプレート
#include <tables/sin2048_int8.h> // サイン波のテーブル
#include <ADSR.h> // Attack,Decay,Sus,Releaseのための指数関数
#include <EventDelay.h>// トリガー(発音タイミング)のためのヘッダ
// コントロールレートをあらかじめ定義
#define CONTROL_RATE 256
// SIN2048_DATA2048のデータをaSinに格納
Oscil <SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);
EventDelay kTriggerDelay;//スイッチを押したら音が鳴るという割込み処理のクラス。
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープ(アタック・ディケイ・サスティン・リリース)をかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数
int TeKick;
int var1;
int var2;
void setup() {
aSin.setFreq(0); //aSinの周波数設定。なり続けるのを防ぐためにまずは0に
startMozzi(CONTROL_RATE); // Mozziの初期化と処理開始
}
void updateControl() {
var1 = mozziAnalogRead(0) / 4;
var2 = mozziAnalogRead(1) / 4;
//kickのためのADSR設定。
Atk = 5;
Dec = 80;
Sus = 0;
Rel = 0;
//setTimesでADSRそれぞれの時間を定義
//setADLevelsでアタックとディケイの音量を定義
//setLevelsでADSR音量を定義
envelope.setTimes(Atk, Dec, Sus, Rel);
envelope.setADLevels(255, var2); //Decayの強さをvar2の可変抵抗(A1ピン)による分圧値から読み取る
TeKick = digitalRead(3); //3ピンの入力で反応
if (TeKick == 1) {
aSin.setFreq(var1); //基本波である正弦波の周波数をvar1の可変抵抗(A0ピン)から読み取る
if (kTriggerDelay.ready()) {
envelope.noteOn(); //ADSRを有効にする。
kTriggerDelay.start(250); //繰り返しの間隔
}
} else {
aSin.setFreq(0); //スイッチオフの時は音が鳴らないように周波数を0
}
}
int updateAudio() {
envelope.update();//enveloeの初期化
return (envelope.next() * aSin.next()) >> 8;//サイン波とADSRを乗算。>>8はおまじない
}
void loop() {
audioHook();
}
キックは、正弦波に掛けるエンベロープを調整することで作れます。
アタックタイムを短く、ディケイタイムを少し長めに。
アタックレベルを強く、ディケイレベルを少し弱めに。
正弦波にこのエンベロープを掛けるだけでキックだったりベースのような音が出ちゃいます。
これはまさにアナログシンセで音を作るときの手法です。
######ハイハット
#include <MozziGuts.h> // Mozziの基本ヘッダファイル
#include <Oscil.h> // オシレータのテンプレート
#include <tables/whitenoise8192_int8.h> //ホワイトノイズのテーブル
#include <ADSR.h> // Attack,Decay,Sus,Releaseのための指数関数
#include <EventDelay.h>// トリガー(発音タイミング)のためのヘッダ
// コントロールレートをあらかじめ定義
#define CONTROL_RATE 256
//ホワイトノイズのデータをaWhiteに格納
Oscil <WHITENOISE8192_NUM_CELLS, AUDIO_RATE> aWhite(WHITENOISE8192_DATA);
EventDelay kTriggerDelay;
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数
int TrHihat;
int var1;
int var2;
void setup() {
aWhite.setFreq(0); //whitenoiseの周波数設定。なり続けるのを防ぐためにまずは0に
startMozzi(CONTROL_RATE); // Mozziの初期化と処理開始
}
void updateControl() {
var1 = mozziAnalogRead(0) / 4;
var2 = mozziAnalogRead(1) / 4;
//2つの可変抵抗のアナログ値を格納
//ハイハットのためのADSR設定。
//デフォは setTimes(A,D,S,R)=(1,50,10,0), setADLevels(255, 15)
//Dec,DLevelにそれぞれvar1,var2入れるとハイハットも(スネアも?)作れる。
Atk = 1;
Dec = var1;//ハイハットのopen, close
Sus = 10;
Rel = 0;
//setTimesでADSRそれぞれの時間を定義
//setADLevelsでアタックとディケイの音量を定義
//setLevelsでADSR音量を定義
//setADLevelsではADのみの音量を定義
envelope.setTimes(Atk, Dec, Sus, Rel);
envelope.setLevels(255, var2, 0, 0);
//envelope.setADLevels(255, var2);
TrHihat = digitalRead(3); //3ピンの入力で反応
if (TrHihat == 1) {
aWhite.setFreq((float)AUDIO_RATE / WHITENOISE8192_SAMPLERATE);
if (kTriggerDelay.ready()) {
envelope.noteOn(); //envelop開始
kTriggerDelay.start(220);
}
} else {
aWhite.setFreq(0);
}
}
int updateAudio() {
envelope.update(); //envelopの初期化
return (envelope.next() * aWhite.next()) >> 8; //ホワイトノイズとエンベロープを乗算
}
void loop() {
audioHook();
}
ハイハットはホワイトノイズにエンベロープをかけてやります。
キックの時、正弦波に掛けたエンベロープと同じように、
アタックは短く強く、ディケイは弱く少し長く。
ディケイの強さ・長さによってハイハットのクローズ/オープンが表現できます。
######スネア
#include <MozziGuts.h> // Mozziの基本ヘッダファイル
#include <Oscil.h> // オシレータのテンプレート
#include <tables/sin2048_int8.h> // サイン波のテーブル
#include <tables/whitenoise8192_int8.h> //ホワイトノイズのテーブル
//#include <tables/triangle2048_int8.h>//三角波
//#include <tables/square_analogue512_int8.h> // 矩形波
#include <ADSR.h> // Attack,Decay,Sus,Releaseのための指数関数
#include <EventDelay.h>// トリガー(発音タイミング)のためのヘッダ
// コントロールレートをあらかじめ定義
#define CONTROL_RATE 256
// SIN2048_DATA2048のデータをaSinに格納
Oscil <SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);
//ホワイトノイズのデータをaWhiteに格納
Oscil <WHITENOISE8192_NUM_CELLS, AUDIO_RATE> aWhite(WHITENOISE8192_DATA);
//矩形波をaSq
//Oscil <SQUARE_ANALOGUE512_NUM_CELLS, AUDIO_RATE> aSq(SQUARE_ANALOGUE512_DATA);
//三角波をaTr
//Oscil <TRIANGLE2048_NUM_CELLS, AUDIO_RATE> aTr(TRIANGLE2048_DATA);
EventDelay kTriggerDelay;
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープをかけるためのクラス
ADSR <AUDIO_RATE, AUDIO_RATE> envelope2;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数
unsigned int Dur2, Atk2, Dec2, Sus2, Rel2;//ADSRの長さを入れておく変数
int TrSnare2;
int var1;
int var2;
void setup() {
aWhite.setFreq(0); //whitenoiseの周波数設定。なり続けるのを防ぐためにまずは0に
aSin.setFreq(0);
startMozzi(CONTROL_RATE); // Mozziの初期化と処理開始
}
void updateControl() {
var1 = mozziAnalogRead(0) / 4;
var2 = mozziAnalogRead(1) / 4;
//snareのためのADSR設定。
//デフォは setTimes(A,D,S,R)=(5,50,0,), setADLevels(255,80)
//ホワイトノイズ用のADSR設定
Atk = 5;
Dec = var1;
Sus = 0;
Rel = 0;
//setTimesでADSRそれぞれの時間を定義
//setADLevelsでアタックとディケイの音量を定義
//setLevelsでADSR音量を定義
envelope.setTimes(Atk, Dec, Sus, Rel);
envelope.setADLevels(254, var2);
//基本波用のADSR設定
Atk2 = Atk+30;
Dec2 = round(Dec*0.4); //ホワイトノイズよりもディケイを短くして整数に丸める(音に締まりを出す。)
Sus2 = Sus;
Rel2 = Rel;
//setTimesでADSRそれぞれの時間を定義
//setADLevelsでアタックとディケイの音量を定義
//setLevelsでADSR音量を定義
envelope2.setTimes(Atk2, Dec2, Sus2, Rel2);
envelope2.setADLevels(255, round(var2*0.3)); //ホワイトノイズよりもディケイを弱くして整数に丸める
TrSnare2 = digitalRead(3); //3ピンの入力で反応
if (TrSnare2 == 1) {
aSin.setFreq(50); //締まりのあるアタック音のための基本波の周波数設定。低くする。
aWhite.setFreq((float)AUDIO_RATE / WHITENOISE8192_SAMPLERATE);
if (kTriggerDelay.ready()) {
envelope.noteOn();
envelope2.noteOn();
//envelopeを2つともオンにする
kTriggerDelay.start(250);
}
} else {
aSin.setFreq(0);
aWhite.setFreq(0);
}
}
int updateAudio() {
envelope.update();
envelope2.update();
return ((envelope.next() * aWhite.next() + envelope2.next() * aSin.next()) / 2)>>8;
//ホワイトノイズにエンベロープを掛けたもの、正弦波にエンベロープを掛けたもの、の2つを加算合成
}
void loop() {
audioHook();
}
スネアが一番難しかった!ハイハットとキックの組み合わせです。
構成は、
・ホワイトノイズ+ADSRで作ったスネアの残響
・正弦波+ADSRで作った芯のある音
の加算合成です。
ポイントは正弦波に掛けるエンベロープのディケイレベル、ディケイタイムをホワイトノイズよりも短くしてあげることで締まりのある音を作るという点です。
キックやスネアの基本波を正弦波以外に、矩形波や三角波、鋸波にすると違った音色が得られます。試してみて下さい。
エンベロープはディケイレベルとディケイタイムを変えるだけで音色が大幅に変化します。弄り甲斐が有りそうです。
######以上 私がMozziで合成したパーカッションでした!
これを組み合わせればドラムシーケンサができるかな?
最後までお読みいただきありがとうございました。
追記
######ハイハット2
スネアと音が似てたので上のハイハットにハイパスフィルタをかけて締まりある音にしました。
#include <MozziGuts.h> // Mozziの基本ヘッダファイル
#include <Oscil.h> // オシレータのテンプレート
#include <tables/whitenoise8192_int8.h> //ホワイトノイズのテーブル
#include <ADSR.h> // Attack,Decay,Sus,Releaseのための指数関数
#include <EventDelay.h>// トリガー(発音タイミング)のためのヘッダ
#include <StateVariable.h>// フィルタのヘッダ
// コントロールレートをあらかじめ定義
#define CONTROL_RATE 256
//ホワイトノイズのデータをaWhiteに格納
Oscil <WHITENOISE8192_NUM_CELLS, AUDIO_RATE> aWhite(WHITENOISE8192_DATA);
EventDelay kTriggerDelay;
StateVariable <HIGHPASS> svf; // can be LOWPASS, BANDPASS, HIGHPASS or NOTCH
//フィルタのためのクラス。上に書いたフィルタから選べる。
//フィルタの中央周波数、フィルタのQ(レゾナンスというらしい、鋭さ。)を定義して使う。
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数
int TrHihat;
int var1;
int var2;
void setup() {
aWhite.setFreq(0); //whitenoiseの周波数設定。なり続けるのを防ぐためにまずは0に
startMozzi(CONTROL_RATE); // Mozziの初期化と処理開始
}
void updateControl() {
var1 = mozziAnalogRead(0) / 4;
var2 = mozziAnalogRead(1) / 4;
//ハイハットのためのADSR設定。
//デフォは setTimes(A,D,S,R)=(5,50,10,15), setADLevels(255, 15)
//Dec,DLevelにそれぞれvar1,var2入れるとハイハットも(スネアも?)作れる。
Atk = 1;
Dec = var1;//ハイハットのopen, close
Sus = 10;
Rel = 0;
//setTimesでADSRそれぞれの時間を定義
//setADLevelsでアタックとディケイの音量を定義
//setLevelsでADSR音量を定義
envelope.setTimes(Atk, Dec, Sus, Rel);
envelope.setLevels(255, var2, 0, 0);
//envelope.setADLevels(255, var2);
//ハイハットのためのHPF設定
int cutoff_freq = 2000;
svf.setCentreFreq(cutoff_freq); //20~4096Hz
svf.setResonance(255); //1~255,大きいほど締まりある。
TrHihat = digitalRead(3); //3ピンの入力で反応
if (TrHihat == 1) {
aWhite.setFreq((float)AUDIO_RATE / WHITENOISE8192_SAMPLERATE);
if (kTriggerDelay.ready()) {
envelope.noteOn();
kTriggerDelay.start(220);
}
} else {
aWhite.setFreq(0);
}
}
int updateAudio() {
envelope.update();
return svf.next((envelope.next() * aWhite.next()) >> 8);
//フィルタ関数の中に>>8したエンヴェロープ入れてあげると動く
}
void loop() {
audioHook();
}
######カウベル
#include <MozziGuts.h> // Mozziの基本ヘッダファイル
#include <Oscil.h> // オシレータのテンプレート
#include <tables/square_analogue512_int8.h> // 矩形波
#include <ADSR.h> // Attack,Decay,Sus,Releaseのための指数関数
#include <EventDelay.h>// トリガー(発音タイミング)のためのヘッダ
#include <StateVariable.h>// フィルタのヘッダ
// コントロールレートをあらかじめ定義
#define CONTROL_RATE 256
//矩形波をaSq
Oscil <SQUARE_ANALOGUE512_NUM_CELLS, AUDIO_RATE> aSq(SQUARE_ANALOGUE512_DATA);
//矩形波をaSq1
Oscil <SQUARE_ANALOGUE512_NUM_CELLS, AUDIO_RATE> aSq1(SQUARE_ANALOGUE512_DATA);
EventDelay kTriggerDelay;
StateVariable <BANDPASS> svf1; // can be LOWPASS, BANDPASS, HIGHPASS or NOTCH
ADSR <AUDIO_RATE, AUDIO_RATE> envelope;//エンベロープをかけるためのクラス
unsigned int Dur, Atk, Dec, Sus, Rel;//ADSRの長さを入れておく変数
int TrCowbell;
int var1;
int var2;
void setup() {
aSq.setFreq(0);
aSq1.setFreq(0);
startMozzi(CONTROL_RATE); // Mozziの初期化と処理開始
}
void updateControl() {
var1 = map(mozziAnalogRead(0), 0, 1023, 1000, 2000 );//BPFのcenter_freq
var2 = map(mozziAnalogRead(1), 0, 1023, 200, 1500);//cowbellの周波数
//ホワイトノイズ用のADSR設定
Atk = 2;
Dec = 170;
Sus = 0;
Rel = 0;
//setTimesでADSRそれぞれの時間を定義
//setADLevelsでアタックとディケイの音量を定義
//setLevelsでADSR音量を定義
envelope.setTimes(Atk, Dec, Sus, Rel);
envelope.setADLevels(255, 10);
int cutoff_freq1 = var1;
//int cutoff_freq1 = 1200;
svf1.setCentreFreq(cutoff_freq1); //20~4096Hz
svf1.setResonance(255); //1~255,大きいほど締まりある。
TrCowbell = digitalRead(3); //3ピンの入力で反応
if (TrCowbell == 1) {
aSq.setFreq(var2);
aSq1.setFreq(var2 + 400); //+400Hzくらいがちょうどいいみたい。
if (kTriggerDelay.ready()) {
envelope.noteOn();
kTriggerDelay.start(250);
}
} else {
aSq.setFreq(0);
aSq1.setFreq(0);
}
}
int updateAudio() {
envelope.update();
return envelope.next() * svf1.next((aSq1.next() + aSq.next()) /2) >> 8;
}
void loop() {
audioHook();
}
みんな大好きカウベル(CowBell)はその名の通り牛さんにつけるベルのことで、かの有名なTR808にも付いているアクセントに最適な音色です。
作り方は周波数の異なる2つの矩形波を加算合成して、それをBPF(バンドパスフィルタ)に掛けてエンベロープを調整すればいい感じです。
これで4種類の基本の音色が作れました。
いよいよ組上げです。
シーケンサはいろいろな形がありますが、だれでもわかるインターフェース、触りたくなっちゃう見た目
をコンセプトに現在製作中です。(2020.1.30)
######参考
導入
https://yoshihito-nakanishi.com/workshops/mozzi/
和音
https://sites.google.com/site/tuadws20140806/m5
エンヴェロープ
http://sheep-me.me/2019/10/24/geidai_16/
mozziのリファレンス(フィルタとかここから探した)
https://sensorium.github.io/Mozzi/doc/html/index.html
カウベル音作り
http://www.rittor-music.co.jp/seminar/sr/synthe/201102.html