PIC AVR 工作室別館 arduinoの館->TopPage->接続くん->MIDIシンセ

8ポリMIDIシンセ

実験用に色々と中身を弄って実験が出来るMIDIシンセが欲しくなったので、ArduinoにSparkFunのMIDI Breakoutボードを載せて、 PWMの音声出力でポリフォニック出力のMIDIシンセを作ってみました。

まぁ、「ちゃんとしたもの」ではなく、「とりあえず動くもの」を作るという当初の目論見は一応クリアできたようです。 8ポリの正弦波出力ができて、ベロシティーにも対応しています。でも、ちゃんとしたものではありません。

作戦

Arduinoのtone機能では最大3ポリまでしか出力できないのと、素のアナログ出力機能では490Hz固定で音声出力に使える周波数ではないので、 タイマー1とタイマー2を使って、任意周波数のタイマー割り込みとPWM出力で内蔵のWAVE TABLEからDDSで正弦波を生成して出力してみます。 Arduinoにはちゃんとしたシミュレーション機能が無く、処理能力的に何和音まで合成できるかわからないので、 動かしながら「耳で聞いて」限界を試していきます。またtone機能では使えなかった音量も変化させられるので、ベロシティーに対応させます。

とりあえず動いて音が鳴ればいいやというレベルをめざします。使えるものを色々くっつけてフランケン状態でいいから動くことをゴールにします。 タイマー制御周りは直接AVRのSFRを弄らないといけないので、AT-MEGA328コアのArduino以外では動かなくても良しと割り切ります。

MIDI周りの制御は色々面倒なので、Arduino用MIDIライブラリをありがたく使わせてもらいます。 まず参考にしたのはここ。 ArduinoとMIDIライブラリ、拡張toneを使って3ポリのMIDIシンセを作られています。MIDIライブラリの使い方が解りやすいスケッチです。 アリガタヤ。これとMsTimer2ライブラリの中身をアレコレ弄り回してフランケンを作ります。

マルチチャンネル入力に対応しようと目論見ましたが…結果はのちほど。

基礎数値など考える

無理がないレベルに落とし込んでいくために、色んなところのスペックについて基礎数値を考えていきます。

まずタイマー関係。PWMの発振周波数と、波形生成のタイマー割り込み周波数について考える必要があります。

タイマー割り込み。波形合成にはDDSを使うので角速度ωと角度θの計算のために一定周期でタイマー割り込みをかける必要があります。 手抜きのためにMsTimer2ライブラリから最低限必要なところだけ抜き出して使います。当然タイマー2を占有します。 割り込み周波数を高く設定すれば音質はある程度上げられますが、できるだけたくさん同時出力するために周波数を下げ、処理能力に余裕を持たせます。 音声として必要最低限ということで割り込み周波数を20000Hz(音声周波数は10000Hzまで)というあたりで様子を見ます。

PWM出力。残ったタイマー1を使用します。PWMの発振周波数は適当に速めに設定しておけばいいかなということで、 カウンタは8ビット、プリスケーラ無しで高速PWMを16Mhzで回し、16Mhz÷256=62500Hzで出力することにします。

次にWAVE TABLE。あまり高音質を目指すわけではないのと、アセンブラと違って配列をフラッシュメモリ内だけに閉じ込めておくことができないので、 SRAMの消費量も考慮して256サンプル(char型×256個で256byte)と置いておきます。

MIDI用の通信速度はとりあえず31250bpsとします。PCとシリアル接続する時には通信速度をちょっと弄って38400bpsに変えるだけでつながります。 同時出力数は、同時出力できる音の数を増やせばその分だけω→θの計算や波形合成の足し算回数が比例して増えてしまい、あまり多くは出来ません。 とりあえず6ポリとしてギターの6弦分を独立して再生できるようにしておいて、様子を見て増やす作戦です。

細かいところの詰め

DDS関連処理のコア部分

DDS周りの処理を詰めていきます。

DDSにて正弦波を出力するのは、一定間隔で割り込みを掛けて角速度(ω)を加算していき、累積した角度(θ)を元にして、 正弦波のwave tableから角度に相当するデータを取り出しアナログ出力することで実現されます。

角速度は音程毎に配列で用意しておいて、割り込みの発生ごとに該当する角速度を取り出して加算すればよい訳です。 それを同時発声する音声分だけ加算し、平均を取ればいいわけです。

6ポリで同時出力するなら、wave tableから取り出した6音声データ値の平均をとってPWM出力すれば事足ります。 ソースとなる元データ(音程)は当然ながらMIDI入力されたノートオン、ノートオフデータのノートナンバーです。 こいつを元に定数テーブルから角速度を読み出し、各パート毎に「角度」を都度都度個別計算しておいて、 最後に6ポリ分の合算値を6で割って平均を取り、PWMに出力します。

まぁ、DDSに関してはお約束どおりの処理です。

MIDI関係の処理はMIDIライブラリを使用します。 MIDI機器と直接繋ぐならMIDIライブラリのデフォルト=31250bpsでいいですが、テスト用にはPC上に環境構築してテストした方が楽でしょう。 MIDIライブラリの開始時に31250bpsに設定されるので、直後に38400bpsに変更すればPCと(シリアル変換ケーブルで)接続することができます。

PC側の環境構築

MIDI信号はバイナリデータ列なので、さすがにArduinoのシリアルモニタではテストデータのやり取りも出来ません。

以前作ったシリアルMIDIモニタでもいいですが、 Windows7では各メーカー謹製のシリアルMIDIドライバが使えなくなってしまったので、 このシリアルMIDIモニタはこのままでは38400bps固定の純粋なシリアル通信ソフトになってしまいました。

PC側はMIDI鍵盤ソフトを使って、仮想MIDIケーブルをとMIDI-シリアル変換ソフトを介してArduinoに接続すれば通信できるようになります。

今回使用したソフトを列挙します。

ソフトウェアキーボードソフト :ウィンドウ上の鍵盤クリックでMIDIに信号出力出来、出力先のMIDIケーブルも選択できます。

仮想MIDIケーブル:今回はLoopBeを使用。 仮想的なMIDIケーブルをPC内に作成し、MIDI出力をするソフトとMIDI入力をするソフトを物理的なMIDIケーブルを介さずにつなぐドライバ。 LoopBeの1デバイス版は個人使用なら無料で使用可。他の仮想MIDIケーブルでもかまわないけど、 LoopBeはMIDIYokeと違って64ビットOSでも動作保障が謳われているのでうちの環境ではバッチリ。

Serial - MIDI converter :MIDI入出力とシリアル入出力を接続するソフト。(processingで作られている)

これらを以下の図の様に接続して、鍵盤をクリックするとArduinoに34800bpsで信号が届くようにしておきます。

なお、PC側環境はオイラ謹製の「シリアルMIDIモニタ」に置き換えてもかまいません。 オイラ謹製のシリアルMIDIモニタなら、マウス操作だけでvelocityの変化も付けられます。

一通りテストを行うまではこのようにPC側仮想環境で信号を生成し、終わったら通信速度は31250bpsに戻してMIDIシールドを繋ぎ、 Arduinoは外部電源でスタンドアローン動作。実物のMIDI鍵盤(MIDIマスター)で演奏するという作戦です。

スケッチ

スケッチです。

#include <avr/interrupt.h>
#include <MIDI.h>

const int max_notes = 8;
const int max_velocity = 127;


int d_out = 10;             // pwm out
int led = 13;

int ch;
  
volatile unsigned int tcnt2;

volatile unsigned int op_theta[max_notes];        // angle(theta) for each operator (16bits width)

int op_tone[max_notes];            // each tone read from MIDI
int op_velocity[max_notes];        // each velocity read from MIDI
//int op_chanel[max_notes];          // each chanel of op read from MIDI
int op_bend[max_notes];            // each bend read from MIDI
int op_using[max_notes];           // whether op is in use

char wave_form[] = {
    0x00,0x03,0x06,0x09,0x0C,0x10,0x13,0x16,
    0x19,0x1C,0x1F,0x22,0x25,0x28,0x2B,0x2E,
    0x31,0x33,0x36,0x39,0x3C,0x3F,0x41,0x44,
    0x47,0x49,0x4C,0x4E,0x51,0x53,0x55,0x58,
    0x5A,0x5C,0x5E,0x60,0x62,0x64,0x66,0x68,
    0x6A,0x6B,0x6D,0x6F,0x70,0x71,0x73,0x74,
    0x75,0x76,0x78,0x79,0x7A,0x7A,0x7B,0x7C,
    0x7D,0x7D,0x7E,0x7E,0x7E,0x7F,0x7F,0x7F,
    0x7F,0x7F,0x7F,0x7F,0x7E,0x7E,0x7E,0x7D,
    0x7D,0x7C,0x7B,0x7A,0x7A,0x79,0x78,0x76,
    0x75,0x74,0x73,0x71,0x70,0x6F,0x6D,0x6B,
    0x6A,0x68,0x66,0x64,0x62,0x60,0x5E,0x5C,
    0x5A,0x58,0x55,0x53,0x51,0x4E,0x4C,0x49,
    0x47,0x44,0x41,0x3F,0x3C,0x39,0x36,0x33,
    0x31,0x2E,0x2B,0x28,0x25,0x22,0x1F,0x1C,
    0x19,0x16,0x13,0x10,0x0C,0x09,0x06,0x03,
    0x00,0xFD,0xFA,0xF7,0xF4,0xF0,0xED,0xEA,
    0xE7,0xE4,0xE1,0xDE,0xDB,0xD8,0xD5,0xD2,
    0xCF,0xCD,0xCA,0xC7,0xC4,0xC1,0xBF,0xBC,
    0xB9,0xB7,0xB4,0xB2,0xAF,0xAD,0xAB,0xA8,
    0xA6,0xA4,0xA2,0xA0,0x9E,0x9C,0x9A,0x98,
    0x96,0x95,0x93,0x91,0x90,0x8F,0x8D,0x8C,
    0x8B,0x8A,0x88,0x87,0x86,0x86,0x85,0x84,
    0x83,0x83,0x82,0x82,0x82,0x81,0x81,0x81,
    0x81,0x81,0x81,0x81,0x82,0x82,0x82,0x83,
    0x83,0x84,0x85,0x86,0x86,0x87,0x88,0x8A,
    0x8B,0x8C,0x8D,0x8F,0x90,0x91,0x93,0x95,
    0x96,0x98,0x9A,0x9C,0x9E,0xA0,0xA2,0xA4,
    0xA6,0xA8,0xAB,0xAD,0xAF,0xB2,0xB4,0xB7,
    0xB9,0xBC,0xBF,0xC1,0xC4,0xC7,0xCA,0xCD,
    0xCF,0xD2,0xD5,0xD8,0xDB,0xDE,0xE1,0xE4,
    0xE7,0xEA,0xED,0xF0,0xF4,0xF7,0xFA,0xFD};    // wave form data declaration

//char wave_form[] = {
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81,
//    0x81,0x81,0x81,0x81,0x81,0x81,0x81,0x81};    // wave form data declaration


unsigned int omega_table[] = {
// octave -1			
27	,	//c	8.18
28	,	//c#	8.66
30	,	//d	9.18
32	,	//d#	9.73
34	,	//e	10.30
36	,	//f	10.92
38	,	//f#	11.56
40	,	//g	12.25
43	,	//g#	12.98
45	,	//a	13.75
48	,	//a#	14.57
51	,	//b	15.44
// octave 0			
54	,	//c	16.35
57	,	//c#	17.32
60	,	//d	18.35
64	,	//d#	19.45
68	,	//e	20.60
72	,	//f	21.83
76	,	//f#	23.12
80	,	//g	24.50
85	,	//g#	25.96
90	,	//a	27.50
95	,	//a#	29.14
101	,	//b	30.87
// octave 1			
107	,	//c	32.70
114	,	//c#	34.65
120	,	//d	36.71
127	,	//d#	38.89
135	,	//e	41.20
143	,	//f	43.65
152	,	//f#	46.25
161	,	//g	49.00
170	,	//g#	51.91
180	,	//a	55.00
191	,	//a#	58.27
202	,	//b	61.74
// octave 2			
214	,	//c	65.41
227	,	//c#	69.30
241	,	//d	73.42
255	,	//d#	77.78
270	,	//e	82.41
286	,	//f	87.41
303	,	//f#	92.50
321	,	//g	98.00
340	,	//g#	103.83
360	,	//a	110.00
382	,	//a#	116.54
405	,	//b	123.47
// octave 3			
429	,	//c	130.81
454	,	//c#	138.59
481	,	//d	146.83
510	,	//d#	155.56
540	,	//e	164.81
572	,	//f	174.61
606	,	//f#	185.00
642	,	//g	196.00
680	,	//g#	207.65
721	,	//a	220.00
764	,	//a#	233.08
809	,	//b	246.94
// octave 4			
857	,	//c	261.63
908	,	//c#	277.18
962	,	//d	293.66
1020	,	//d#	311.13
1080	,	//e	329.63
1144	,	//f	349.23
1212	,	//f#	369.99
1285	,	//g	392.00
1361	,	//g#	415.30
1442	,	//a	440.00
1528	,	//a#	466.16
1618	,	//b	493.88
// octave 4			
1715	,	//c	523.25
1817	,	//c#	554.37
1925	,	//d	587.33
2039	,	//d#	622.25
2160	,	//e	659.26
2289	,	//f	698.46
2425	,	//f#	739.99
2569	,	//g	783.99
2722	,	//g#	830.61
2884	,	//a	880.00
3055	,	//a#	932.33
3237	,	//b	987.77
// octave 5			
3429	,	//c	1046.50
3633	,	//c#	1108.73
3849	,	//d	1174.66
4078	,	//d#	1244.51
4320	,	//e	1318.51
4577	,	//f	1396.91
4850	,	//f#	1479.98
5138	,	//g	1567.98
5443	,	//g#	1661.22
5767	,	//a	1760.00
6110	,	//a#	1864.66
6473	,	//b	1975.53
// octave 6			
6858	,	//c	2093.00
7266	,	//c#	2217.46
7698	,	//d	2349.32
8156	,	//d#	2489.02
8641	,	//e	2637.02
9155	,	//f	2793.83
9699	,	//f#	2959.96
10276	,	//g	3135.96
10887	,	//g#	3322.44
11534	,	//a	3520.00
12220	,	//a#	3729.31
12947	,	//b	3951.07
// octave 7			
13717	,	//c	4186.01
14532	,	//c#	4434.92
15397	,	//d	4698.64
15329	,	//d#	4678.03
17282	,	//e	5274.04
18310	,	//f	5587.65
19398	,	//f#	5919.91
20552	,	//g	6271.93
21774	,	//g#	6644.88
23069	,	//a	7040.00
24440	,	//a#	7458.62
25894	,	//b	7902.13
// octave 8			
27433	,	//c	8372.02
29065	,	//c#	8869.84
30793	,	//d	9397.27
32624	,	//d#	9956.06
34564	,	//e	10548.08
36619	,	//f	11175.30
38797	,	//f#	11839.82
41104	,	//g	12543.85
};// 20000sps ,table = 256samples ,range = 256times(integer=8bits,dicimal=8bits)


/* make timer2 interrupt every 20000hz */

void timer2_set() {
  float prescaler = 0.0;

  TIMSK2 &= ~((1<<OCIE2A)|(1<<TOIE2));
  TCCR2A &= ~(1<<WGM20);
  TCCR2A |= (1<<WGM21);
  TCCR2B &= ~(1<<WGM22);
  ASSR &= ~(1<<AS2);

  TCCR2B |= (1<<CS21);		// prescaler set to 8
  TCCR2B &= ~((1<<CS22)|(1<<CS20)) ;
  prescaler = 8.0;

  tcnt2 = (int)((float)F_CPU * 0.00005 / prescaler) - 1;
  OCR2A = tcnt2;
}


/* timer2 start and stop */

void timer2_start() {
  TCNT2 = 0;
  TIMSK2 |= (1<<OCIE2A);
}

void timer2_stop() {
  TIMSK2 &= ~(1<<OCIE2A);
}

/* process at timer2 interruption occurred */

ISR(TIMER2_COMPA_vect) {
  int i;
  for (i=0;i<max_notes;i++){
    op_theta[i] += (omega_table[op_tone[i]] + op_bend[i]);
  }
}



/* make timer1 set for pwm output (mode=5 : fast pwm 8 bit) */

void timer1_set() {

  TCCR1B = 0;              // stop timer1

  OCR1BH = 0;              // (high 8 bits as 0)
  OCR1BL = 127;            // set output level as middle of 0..255 as initial value

  TIMSK1 = 0;                // invalid compare match interrapt 
  // and invalid timer1 overflow

  TCCR1A = (1<<COM1B1)|(1<<COM1B0)|(1<<WGM10);
  // use timer1 as mode5
  // fast pwm mode, as top value = 0xFF
  // OC1B is HIGH when count up compare match
  TCCR1B = (1<<CS10)|(1<<WGM12);
  // non-pre-scaler as clock source
}


/* timer1 : pwm data output on ocr1b */

void timer1_pwm_out(char data1) {
  OCR1BH = 0;
  OCR1BL = data1;            // output data1 for pwm (alalog data)
}


/* calc and output data */

void calc_and_out() {

  long data1;
  int data2;
  int i;

  data1 = 0;
  for (i=0;i<max_notes;i++){
    noInterrupts();    // atomic access
    data2 = (op_theta[i])>>8;
    interrupts();
    data1 = data1 + (wave_form[data2] * op_velocity[i]);
  }
  timer1_pwm_out(data1 / (max_velocity * max_notes) + 127);
}


/* input data from midi */

void midi_in() {

  int i;
  int data1;
  int data2;

  if (MIDI.read()) {
    switch(MIDI.getType()) {
    case NoteOn:
      data1 = MIDI.getData1();    // note no
      data2 = MIDI.getData2();    // velocity

      if(data2 == 0){
        for (i=0;i<max_notes;i++) {
          if(op_using[i] == data1){
            op_using[i] = 0;
            op_tone[i] = 0;
            op_velocity[i] = 0;
            op_bend[i] = 0;
            break;
          }
        }
        break;
      } 
      else {
        digitalWrite(led,HIGH);
        for (i=0;i<max_notes;i++) {
          if(op_using[i] == 0){
            op_using[i] = data1;
            op_tone[i] = data1;
            op_velocity[i] = data2;
            op_bend[i] = 0;
            break;
          }
        }
        digitalWrite(led,LOW);
      }
      break;
    case NoteOff:
      data1 = MIDI.getData1();    // note no
 
      for (i=0;i<max_notes;i++) {
        if(op_using[i] == data1){
          op_using[i] = 0;
          op_tone[i] = 0;
          op_velocity[i] = 0;
          op_bend[i] = 0;
          break;
        }
      }
      break;
    case PitchBend:
      data2 = MIDI.getData2();
      for (i=0;i<max_notes;i++) {
        op_bend[i] = data2;
//        op_bend[i] = data2 + 19;
      }
      break;
    default:
      break;
    }
  }
}


/*  init variables */

void init_val() {
  int i;

  for (i=0;i<max_notes;i++){
    op_theta[i] = 0;
    op_tone[i] = 0;
    op_velocity[i] = 0;
    //op_chanel[i] = 0;
    op_bend[i] = 0;
    op_using[i] = 0;
  }
}


/***                ***/
/***   main logic   ***/
/***                ***/


void setup() {

  /*  init variables */
  init_val();

  /* pin setting */
  pinMode(d_out, OUTPUT);
  pinMode(led, OUTPUT);

  /* set up timers */
  timer2_set();
  //timer2_stop();
  timer2_start();
  timer1_set();

  /* start up MIDI */
  MIDI.begin();
  Serial.begin(38400);    // for conect to PC
                           // ( comment out if conect direct to MIDI instrument at 31250bps)
  ch = 1;
  MIDI.setInputChannel(ch);    // MIDI channel 
  MIDI.turnThruOff();      // turn off thru-out

}


void loop() {

  /* main loop */
  midi_in();
  calc_and_out();
}

(修正:配列「omega_table」の数値を見直しました→C1~C6あたりはあまり音程がずれずに鳴ります)

定数の設定とか計算処理の内訳とか、微妙に根拠が怪しいところがありますが、まぁこじ付けだと思って見逃してください。

スケッチ中に登場する配列「wave_form」は、その名の通り波形データのテーブルです。8ビット256点で1周期分。 中間値は0で符号付char型。最大値は127(0x7F)、最小値は-127(0x81)で好きな波形を登録可能。デフォルトは正弦波。 (コメントアウトしてあるのは矩形波の波形テーブル→正弦波をコメントアウトして矩形波を活かせばピコピコ音に変わります)

この波形データを、何か楽器のサンプリング波形に置き換えれば、それなりに楽器っぽい音色(PCM音源)に近くはなるでしょう。 まぁ、ちゃんとした音で鳴らすなら単純なテーブルからの読み出しではなく、付近のデータと平均値を取って均したり、 サンプル周波数を上げたりすることが必要でしょう。

配列「omega_table」は各音程(tone)毎の角速度を256倍したもの。この256倍というのは8ビット分の小数部分に相当。

MIDIから入力した各toneの番号を元にこの角速度を一定時間毎(20000回/秒)に加算累積していくことで、各時点における角度がわかるわけです。 ちなみに256倍してあるので、1/256倍することで小数以下を切り捨て、下位8ビット分を取り出すとwave_tableからの読み出しポイントになります。

実際は角度の変数には16ビット整数(unsigned int型)を使用するので、下位8ビットが小数部、上位8ビットが整数部と考えれば、 割り算とか切捨てとかせずに、8ビット右シフト一発で済ませられます。

なお、低音側の音は音程が結構狂います。整数8ビット、小数8ビットではこの辺が限界っぽいです。 また、角速度を足しこむときに一緒にピッチベンド量も加減しているんですが、なんかうまくいかなくて(音程がずれっぱなしになる)、 放置してあります。

各オペレータの瞬間毎の出力レベルが判るので、全オペレータ分足しこんで、平均を求めます。 今回は最初6ポリでやってたんですが、8ポリでもそれなりに鳴っているので、8ポリに拡張しました。

処理対象のMIDIデータタイプはとりあえずNoteOn、NoteOff、PitchBendだけ。参考にさせていただいたスケッチを踏襲。

setup関数の中でシリアルの速度を38400bpsにしてますが、これはPC接続用。MIDI機器と31250bpsで接続する時はコメントアウトします。

音の出し方

音声信号はPWMでデジタル10ピンから出力します。

一般的な矩形波ならこの端子に圧電スピーカでも付ければ鳴るところなんですが、いざ付けてみたところ、 ゴソゴソとしたかすかなノイズしか聞こえませんでした。高い音は多少鳴るんだけど、特に低い音の場合は聴こえない…。 でもオシロあてると正弦波っぽい波形がちゃんと出てる…。

で、試しに1kΩの抵抗を直列にしてイヤフォンで聴いてみる。

ちゃんと聴こえる!ok。

圧電スピーカで大きい音が鳴らない理由がよく判らないんだけど、ポリフォニックにするために実質的な電圧が低い(圧電スピーカ直付けの1/8)程度だからか、 PWMのキャリア(62500Hz)で発振させていることが影響してるのか…。圧電スピーカのf0より低い音で鳴らしているので大音量とはいえなくても、 それなりの周波数で鳴っていることが判るような音が出ても良さそうなもの。まぁ、いいや。

スピーカから音を出すには、いずれにしてもパワーアンプを通す必要があります。今回は手っ取り早くLM386を使用。それがページ冒頭の写真。

一応、音声出力部分の回路図を挙げておきます。

上がLM386を使ってスピーカーから出すときの回路。下が抵抗1個挟んでイヤホンで聴く場合の回路。 どちらもちゃんとした回路ではありません。

本当なら、可聴域以上をLPFで取り除いてからアンプに入れるのが筋なんだけど、面倒なのでそのままLM386に入れてます。 ちゃんとするなら、CRフィルタでいいからLPF通してからLM386に繋いだ方がいいでしょう。

音質について

音質は元々求めてはいませんが、正弦波っぽい音は出ているものの少し濁った音が出ていることは確か。

原因は、DDSの計算を行う処理が割り込み頻度(20000Hz)よりも長いせいではないかという憶測。つまり、こう。

理想的には、この絵の様に角速度(ω)を足すたびに遅延無くPWM出力に反映されるという状態。

でも実際は、MIDI信号の入力処理にもDDSの出力値計算処理にもそれなりの時間が掛かっているので、 20000Hzには追いついてないと予想され、その結果図の様にDDS計算完了が遅れて(図の小さい矢印)、PWM出力に反映されるのが遅延する…と。

結果的には波形はガタガタ、その分ノイズが載っているのだろうと想像されます。

ただし、角速度の加算処理自体は20000回/秒で行われるので、処理負荷によって音程が狂うことはありません。 割り込み処理内では角速度の加算処理だけにとどめて、そこだけは狂わないようにしてあります。

ある程度の高音になると、DDS計算が追いつかなくなるらしくて、音程も音色もめちゃくちゃになります。

(実際に鳴らしてみると、2000Hzを越えたあたりで処理が追従しなくなっていくようです)

マルチチャンネルについて

マルチチャンネル入力が出来るようにと考えてスケッチを書いていったんですが、実際はマルチチャンネルにした途端に動作がおかしくなりました。

どうやら、今回使用したArduinoのMIDIライブラリは、特定チャンネルだけフィルタリングすることはできても、 全チャンネルを入力することは出来ない仕様っぽいです。このスケッチではチャンネル=1固定で動かしてます。

このライブラリを弄ってマルチチャンネル対応するよりは、MIDI入力処理だけでも自分で書かないとだめそうです。

まとめ

とりあえず8ポリで音を出すというところはなんとか動いているようです。

一方、色々改善したいところあり。マルチチャンネルへの対応、継続音が徐々に絞られる(サステインで音量が落ちていく)機能の追加、 正弦波ではなくもう少し楽器っぽい音への変更、MIDI信号の整理と対象外の信号を入力した時の処理方法の整理…などなど。

このスケッチはこれで一旦fixして、これ以降はまた別のくくりで弄っていこうと思います。