はじめに
私はRakuChordというArduinoを基にした電子楽器を作っています。
RakuChordはArduinoたった1つでキーの押下の読み取り、ディスプレイの制御、音の生成を制御しています。
この記事では、RakuChordでどのように和音を生成しているか?ということを解説していきます。
実はこのRakuChord。初期バージョンは10年前くらいで、和音のロジックもそのころから少しずつ改良を加えてきています。
しかし、どこにもドキュメントを残さずにここまで来たため、自分でもなぜこのソースコードで和音が鳴っているのかよくわからなくなることがありまして・・・自分の備忘録的な意味も込めてこの記事を書く事にしました。
Arduinoで「単音」
Arduinoで「単音」を出すのはとても簡単です。最も簡単なのは無限ループを作り、そこでdigitalWriteであるピンの出力を上げたり下げたりする方法です。そのピンにスピーカをつなげることで、単音を再生することができます。
適切に待ち時間を入れることで、これだけで単音を鳴らすことができます。
例えば440Hz(ラの音)を鳴らす時は 1/440秒の半分の待ち時間を入れてdigitalWriteでHIGH/LOWを繰り返せばよいのです。
void setup() {
pinMode(9, OUTPUT);
}
bool flip = false;
void loop() {
flip = !flip;
digitalWrite(9, flip);
delayMicroseconds(2272/2); // 1/440 = 0.0022727... = 2272uS
}
この方法では、音を鳴らしている間はほかの処理を行うことができません。
しかしArduinoの関数であるtoneを使うとこの問題も解決します。toneは周波数を指定すると即座に処理が終わり、次の命令を実行することができますが、音は鳴り続ける。という動作をします。
void setup() {
pinMode(9, OUTPUT);
tone(9,440);
}
void loop() {
// ここで処理中も音は鳴り続けている
}
https://www.arduino.cc/reference/jp/language/functions/advanced-io/tone/
例えばゲーム機などを作る際、簡単なBGMであればtoneで十分です。
Arduinoで「和音」
Arduinoで「和音」というのは「単音」の時ほど簡単ではありません。
和音を鳴らすためにはいくつかの方法が考えられます。
1. 複数のピンを使う
和音の数だけピンを使い、オペアンプなどで合成したのちにスピーカーとつなげることで和音を鳴らすことができます。この方法は「単音」を複数並べるという点で単純でわかりやすいですが、ピンをたくさん使うことや、音の合成のためにオペアンプなどの部品が必要なことがデメリットです。
2. ソフトウェアで合成する
今回紹介するのはこちらの方法です。この方法では1つのピンの出力で和音を演奏する方法です。ソフトウェアの処理が複雑になりますが、単純な回路で和音を再生することができます。(音量が小さくて良いなら、それこそArduinoに直接スピーカをつなげるだけでもOKです)
2.1 既存のライブラリ
ソフトウェアで和音を合成するためには、これから紹介する「1から処理を書く」方法とは別に、既存のライブラリを利用することもできます。
2.1.1 Mozzi
MozziはArduinoで利用できるシンセサイザーライブラリとして非常に有名なものです。下記ページで大量のサンプルを聞くことができます。
おそらく音関係でやりたいことの大半は、このライブラリを使うことで実現できます。
このライブラリは音を扱う様々な処理の詰め合わせセットのようなものです。そのため単に「和音を鳴らしたい」という場合でもそこそこのプログラミングが必要となります。単純に和音を鳴らしたい場合はこの後紹介する別のライブラリのほうが簡単に実現できると思います。
一方、単純な和音ではなく、「音作り」にこだわりたい人にとってはMozziの豊富な音関係の関数群は非常に強い武器となるでしょう。
2.1.2 PWMDAC_Synth
PWMDAC_Synthはまさに「Arduinoで和音を鳴らす」ことに特化したライブラリです。6重和音を扱うことができ、さらにエンベロープも設定できます。
https://ja.osdn.net/users/kamide/pf/PWMDAC_Synth/wiki/FrontPage
2.1.3 VRA8-Pシリーズ
VRA8-Pで利用できる3音を扱えるシンセサイザーのライブラリです。ArduinoのCPUをフルに使うことで、ほかのライブラリを圧倒する高音質を実現しています。
ただし、この音源はArduinoのメモリ・CPUをかなり占有してしまうため、何かのロジックと組み合わせる場合は注意が必要です。場合によってはArduinoを2つ並べるなどして、片方は制御、片方はVRA8-Pを用いた音源、というような使い方のほうが良いかもしれません。
2.2 自分で和音を鳴らす処理を書く
既存のライブラリでは「痒い所」に手が届かない場合や、メモリ・CPU使用量を細かく調整したい場合、和音の合成処理自体に興味があるような場合は、自分で和音を鳴らす処理を書く事のがおすすめです。
ということで、以降RakuChordの音源での和音の処理について解説していきます。
RakuChordの音源のソースコード
RakuChordのファームウェアはオープンソースで公開しています。
音源周りの処理は https://github.com/inajob/rakuchord/blob/cc088ecfc711f7f432e1a3cc55a3f5a8bb0b9c37/firmware/src/MultiTunes.cpp にまとめています。
ここからは、このソースコードを眺めながらArduinoで和音を鳴らす方法を紹介します。
soundSetup
この関数が音を鳴らすための準備をするものです。起動時に1度だけ呼び出すことを想定しています。
内部では和音合成に必要なタイマーのセットアップを行っています。
RakuChordで使うのはArduinoのピンのうちDigitalの9番ピンです。まずは9番ピンをOUTPUTに設定しています。
次にタイマーの設定を行います。このタイマーは9番ピンのPWMの制御と、和音の波形の計算のタイミング制御のために利用しています。
Arduinoが使用しているマイコンATMega328には複数のタイマーが存在しますが、ここではTimer1を使用します。
Timer1の設定のためにはTCCR1AとTCCR1Bという2つのレジスタを用います。このレジスタの特定のビットを上げたり、下げたりすることでTimer1の挙動を設定します。
ここでは下記のような設定をしています。
- WGM13~10 = 0001
- 8bit位相基準PWM
- COM1A1~0 = 10
- カウントアップ比較一致でLOW、カウントダウン比較一致でHIGH
- CS12~10 = 001
- 分周なし
ちょっとこれだけでは何を言っているかわかりませんね・・もう少し解説します。
分周比
Timer1はTCNT1の値を適当な間隔でインクリメントしたりデクリメントしたりする仕組みです。具体的にどのようにTCNT1を動かすのかは後ほど説明しますが、その間隔について指定するときに用いるのが分周比です。
ATMega328が動作している周波数に対しての比率でこの間隔を指定します。
今回は「分周なし」の指定なので、Arduinoに搭載されている水晶発振子である16MHzの間隔でTCNT1を動かすことになります。
8bit位相基準PWM と 比較一致
WGM13~10によりTCNT1をどのように動作させるかを指定しています。
今回は8bit位相基準PWMという指定です。このモードではTCNT1は0から1ずつカウントアップしていき0xff(8bitの上限)までカウントアップしたら次は1ずつカウントダウンしていきます。そして0まで下がったらあとは繰り返しです。
オーバーフロー割込みは0xffに達したタイミングで実行されます。
つまり図にするとこのような感じです。
COM1A1~0,はTCNT1の動作と連動してデジタル9番ピンにどのような信号を出力するかを設定しています。今回は「カウントアップ比較一致でLOW、カウントダウン比較一致でHIGH」を指定しています。
比較に用いる値はOCR1Aレジスタの値です。
つまり図にするとこのような感じです。
このようにタイマーと連動してデジタル9番ピンを制御することで、非常に高速にピンをHIGH/LOWに切り替えることができます。またOCR1Aの値を変更することで、ON/OFF時間の比率を変更することができます。
これがPWM(Pulse Width Modulation)というテクニックです。このように短い間隔でHIGH/LOWを切り替えることで、HIGH(5V)とLOW(0V)という2種類の値しか出力できないデジタル回路においてアナログっぽい値を作ることができます。(厳密にはアナログとは違いますが・・)
このPWMを使うことで、単なる矩形波よりも複雑な様々な波形を生成することができます。もちろん「和音」も「波形」であらわすことができるため、そのような信号をPWM作ることで和音を鳴らすことができます。
タイマー割込みの設定
最後にこれらのタイマーを有効にするためにTIMASK1にTOIE1のビットを設定します。これでTCNT1が上限に達したときに割込みが発生します。
TIMER1_OVF_vect
今回利用するTimer1のオーバーフロー割込みはこの名前で呼び出されます。
ここまでの話を総合すると16MHzでTCNT1が0 → 0xFF → 0と繰り返し変化していきます。 これを1周期とするとこの1周期にかかる時間は
となります。
(ソースコード上ここでTCNT2に値を代入しているが、これは何の意味もなさそう・・・)
基本的にはこの割込みルーチンの中でdn[x]にd[x]を足していきます。xは0~4の5通りあり、これがオシレータを表しています。つまり独立した5つの音を制御しているということです。
最終的にはこのdn[x]から波形テーブルを引いて、5つあるそれぞれの結果をすべて足し合わせてOCR1Aレジスタの値として設定しています。
dn,dと音程
dn[x]とd[x]はunsigned intです。つまり16ビットの値です。
先ほど見たように割込みルーチンの中でdn[x]にd[x]をどんどん足していきます。
このd[x]はRakuChordの鍵盤の押下により「音程に対応した値」が代入されています。
「音程に対応した値」というのはhttps://github.com/inajob/rakuchord/blob/cc088ecfc711f7f432e1a3cc55a3f5a8bb0b9c37/firmware/src/tones.hに定義しています。
このソースコードによると「音程に対応した値」というのは周波数そのものとなっています。(NOTE_A4が440、NOTE_A5が880となっていることからも、これが良く知っている周波数と同じであることがわかります。)
一方、d[n]の値は割込みルーチンの中で、上位6ビットを使ってテーブルを引いています。
波形テーブルは64サンプルで1波形を表しています。dn[x]は16ビットですが、その上位6ビット(0~63)を取り出すことでこのサンプルを読みだしています。
d[x]の値を変数としてdn[x]が何回の割込みで1周するかを計算します。
割込みの発生間隔は上で求めたので、これを掛け合わせることで波形1周期にかかる時間が計算できます。
式を変形します
波形1周期にかかる周波数を求めます(↑の逆数)
これによりd[n]の値は周波数をだいたい2で割ったものとなります。
つまり実際にはd[n]の値の表す周波数より1オクターブ低く、かつ少し誤差のある音が鳴っていることがわかります。
オーディオアンプ
どの方法をとったとしても、Arduinoのピンから出てくる信号のレベルは低いものです。イアホンや小さなスピーカであれば何とか音が鳴りますが、大きな音を鳴らしたい場合は、Arduinoとは別にオーディオアンプが必要です。
RakuChordではLM386というICを利用してこれを実現しています。
調べるとLM386を採用したオーディオアンプのモジュールも販売されているため、回路を作るのが面倒な人は、これらを使うのが良いでしょう。
まとめ
Arduinoで和音を生成する方法を紹介しました。
この方法は私が様々なライブラリや、タイマーの仕様を読んで試行錯誤して作成したもので、この方法が唯一の方法ではありませんが、Arduinoで1から和音を生成したい人にとっては試行錯誤の足掛かりとなるのでは、と考えています。
(もっと良い方法や、間違いなどがあれば教えてください。→https://twitter.com/ina_ani)
Arduinoでちょっとリッチな音が鳴るようなガジェットを作りたい人の助けになれば幸いです。