PIC AVR 工作室別館 arduinoの館->TopPage->接続くん2->ノイズ生成
ノイズ生成
Arduinoにスピーカ(圧電スピーカとか)を繋いで、80年代マイコンのPSG/SSGテイストのノイズを出そうという作戦です。
「シュワー」とか「ジュゥゥォォー」とか「ゴォーーーー」とかいった感じの音が出ます。 昔のPC-6001やファミコンっぽい「ノイズ」の音になりました。
秋月互換基板使いました。
音源チップとノイズ
80年代のPC(当時のマイコン)には、PSG と呼ばれる音源チップが搭載されていたり、PSG同等の機能がFM音源チップのSSGとして実現されていたりしました。 PSGやSSGは3和音のトーン生成やエンベロープに加え「ノイズ生成機能」がついていて、FM音源ほどじゃないけど、 ゲームなどの効果音やBGMにはそこそこの雰囲気を表現できました。 生産は終わってしまっているかもしれませんが、チップ単体は今でも入手は出来るようです。 最近ではチップチューン などの材料としても、オリジナルのチップやソフトウェアエミュレーションしたものが使われているようです。
当時はPC-6001用を使ってプログラムを書いて、ゲームの効果音などとして鳴らしてみたことがあったのですが、 当時から一つ気になっていました。ノイズ生成に関するパラメタに「周波数」というものがあったことです。
ノイズの代表例といえば「ホワイトノイズ」 ですが、ホワイトノイズは特定の周波数にピークが無く、周波数を指定するというのはなんだか変だなぁと。 「ピンクノイズ」 というわけでもないようです。
調べてみると、どうやらPSGやSSGのノイズ発生の仕組みはホワイトノイズのようなものではないようです。 先人の調査結果や実験結果を参考にさせていただきました。
↓参考にしたサイトなど。(大感謝!)
- 「ひろゆきのホームページ」PSG実験室
- 「MSX2 Technical Hand Book」1章 PSGと音声出力
- 「タケダノヲト」Vol20.ソフトウエアノイズジェネレーター
などなど。(敬称略)
Arduino(というよりはMEGA168/328)の機能を使って、このノイズをソフトウェアでエミュレーションしてみることにしました。
動作原理の調査
一般にノイズ(ホワイトノイズ)というのは特定の周波数を持たず、「サーーーー」とか「シャーーーー」とかいう感じの音として聴こえます。 特定の周波数をもたず、音楽ならメロディーと協和音も不協和音も生じないので、パーカッションとして用いられたりします。
ところが、PSGやSSGから出力されるノイズはそのパラメタ内に「周波数」を指定するようになっており、 実際この周波数の高低によって実際音程の高低をコントロールすることができました。 ということは、PSGのノイズはいわゆるホワイトノイズとは違う原理でなっているんだろうということが想像できます。
で、「なんじゃこりゃ?」と思うわけです。
色々調べてみると、どうやら擬似乱数 のロジックのうち「線形帰還シフトレジスタ」という方法を使って、そのうちの1ビットを取り出してデジタル出力(=短いを矩形波出力) することで実現していることがわかりました。ただ、矩形波といっても周期が1波1波ごとにランダムで変化するので、 それを構成する倍音成分は(ほぼ)ランダムとなり、いわゆる特定周波数をもたない「ノイズ」として聴こえるようです。
実際、武田さんの「タケダノヲト」のページにあるmp3ファイルを聴いてみると、周波数の高低を伴ったノイズとして聴くことができました。
サンプルプログラムを眺めてみると、やはりwikipediaに書かれているような、周期を擬似乱数で散らした「0」と「1」 の羅列で表現していることが解りました。
作戦
そのような計算ロジックを組めば音程的な高低を伴ったノイズとして聴けそうなことは判りました。
一方、Arduinoでタイマー割込みといえばライブラリ化されている中ではMsTimer2がメジャーどころ。 しかしこのライブラリは最も細かくても1ミリ秒単位でしか周期を設定できません。
参考にした武田さんのタケダノヲトに挙げられているサンプルプログラムでは、割り込み周期はおよそ60000回/秒。 つまり毎秒約60000回ほど乱数発生とデジタル出力を行っているということです。このあたりを参考数値としてロジックに落とし込んでいきます。
で、タイマー割込みを使ってノイズ生成を行うんですが、タイマー割込みの処理はこの間作った Arduinoの簡易MIDI音源 を流用しちゃえば簡単そうです。例によってAT-MEGA168/328シリーズのSFRを直接弄るので、Arduinoっぽくないスケッチになっちゃいます。
それと、武田さんのプログラムが60000回/秒に対してこの間オイラが作ったタイマー割込み処理は20000回/秒なので、 スペック的にはすでに1/3なのは見えちゃってるんだけど、SFRの設定弄りだすと色々手間なのでとりあえず「そのまま動かしてみる」ことに。 きっと山南艦長もそうしたはず。
鳴らしてみたら、波形をFFT掛けて眺めてみます。
ソフトウェア処理
まず必要になるのは乱数発生処理。線形帰還シフトレジスタ方式で乱数を発生させることにします。 計算のビット幅は16ビット(int型)として、右シフトさせながら途中の2ビットを抜き出してxorを取ることにします。
割り込みは、この間作ったパチモノMIDIシンセのDDS処理で使った20000回/秒のタイマ割込みを流用します。 もっと狭い間隔で割り込みかけたいところなんだけど、20000回でも1回あたり800クロック(@16Mhz)しかないし、 Arduinoはシミュレータ環境がないので、とりあえずこの程度でお茶を濁します。
割り込み発生ごとにカウンタを1進めておいて、アナログ入力から入力した数値と比較して、 カウンタがアナログ入力値よりも大きくなったらカウンタ値をゼロに戻すと共に、乱数を発生させて1ビット分出力します。
アナログ入力は可変抵抗にでも繋いでおいて、入力値がモニターできるように数値の変化時にシリアルモニター出力します。
というわけで、外付け部品は最低限の可変抵抗と圧電スピーカくらいで済ませます。
スケッチ
#include <avr/interrupt.h> int d_out = 9; // 1bit out int led = 13; int a_in = 0; // analog in pin volatile unsigned char tcnt2; volatile unsigned char in_value; /* 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; // tcnt2 = (int)((float)F_CPU * 0.0000125 / 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 out_data; tcnt2++; if (tcnt2 >= in_value) { tcnt2 = 0; out_data = random_num(); digitalWrite(d_out,out_data & 1); digitalWrite(led,out_data & 1); } } /* making random num */ int random_num() { static int seed = 12345; //random seed int random_value; // random_value = (seed>>1) + (bitRead(seed, 16) xor bitRead(seed, 13) xor 1)*0x8000; random_value = (seed>>1) + (bitRead(seed, 13) xor bitRead(seed, 4) xor 1)*0x8000; seed = random_value; return seed; } unsigned char read_value() { static unsigned char v; int a; a = analogRead(a_in) / 4; if (a != v) { v = a; Serial.println(v); } return v; } /*** ***/ /*** main logic ***/ /*** ***/ void setup() { /* pin setting */ pinMode(d_out, OUTPUT); pinMode(led, OUTPUT); pinMode(a_in,INPUT); /* set up timers */ timer2_set(); //timer2_stop(); timer2_start(); in_value = 0; Serial.begin(9600); // for conect to PC } void loop() { /* main loop */ in_value = read_value(); }
簡単な説明。アナログ入力0に可変抵抗でVccとGNDを分圧した電圧を入力して、デジタル9ピンとGNDに圧電スピーカを繋いで使います。
MsTimer2では割り込み頻度が足らないので、もっと細かい頻度で割り込みできるようにSFRを直接弄ります。 毎秒20000回割り込みを発生させて、カウンタを1アップします。そのカウンタ値とアナログ入力からの入力値を4で割た値と比較して、 カウンタ値がアナログ入力値に追いついていたら、カウンタをクリアして、1ビット分の乱数を出力します。その繰り返し。
ちなみに、アナログ入力からの入力値(÷4)が255の場合でおよそ毎秒78回、 0(これは1と一緒)の場合でおよそ20000回、「0」か「1」がランダムに出力されることになります。
途中コメントアウトしてあるところのうち、「tcnt2」に数値を設定しているところは割り込み周期の計算です。 ここのコメント文を入れ替えてやると毎秒80000回で割込みが掛かるようになるんですが、 なぜかこの処理に変えると微妙にバグるのんだけど、割込み周期が短すぎて処理が追いつかないのか、バグが残っているのか…
Arduinoはシミュレータが無いので詳細は良くわかんないんですが、このスケッチ自体は習作だし、 あとでアセンブラ使ってTINY2313でPSGエミュレータを作る実験って位置づけなので、このまま放置します。
もう一箇所コメントアウトしてあるところは、参考サイトに挙げた「ひろゆきのホームページ」PSG実験室で解析されていた計算を反映したものですが、 このスケッチではカウンタが16ビット幅(int型)であふれてしまうので、代わりにビット4、ビット13を取り出してxorすることにしました。 とりあえずこの2ビットを取り出すことでノイズっぽい音が出ているので、それなりにランダム性は担保されているようです。
アップロードしたら、圧電スピーカと可変抵抗を繋いで、可変抵抗をグリグリ弄り回してみてください。
FFTに掛けてみる
PSGのノイズジェネレータは、平均周波数を上下できるようになっているんですが、昔PC-6001で初めてPSGを弄ってみたとき、 発生するノイズの周波数はこんなイメージだと思ってました。
でも、動作原理を見てみる限り、そんな出力波形にはなりそうにありません。実際、波形をFFTに掛けてみました。
まずは200Hz/Divで。毎秒78回~5000回まで切り替えながら波形の変化を見てみると、こんな風に右下がり逓減の波形に、 周期的にディップが生じた波形になるみたい。
同じように5000Hz/Divで見てみる。
どうやらピークが上下するんじゃなくて、右下がりの角度が「低音」側で鋭く、「高音」側で水平に近づいていく感じになるっぽい。 まぁ、たしかに平均の周波数は上下することにはなるな。(可聴域内においては)
とりあえず、ノイズの音に高低がつく理由はわかったんだけど、ディップが出るのは何でだろう。矩形波の周波数をランダムにしているからか? じゃぁ、矩形波じゃなくてランダムなアナログ値を同じように出力周期を上下させてたらどうなるんだろう。
周波数の指定によって傾斜が変化するのか、それとも単調なホワイトノイズになっちゃうのか?やっぱりディップが出ちゃうのか?
アナログ出力でやってみた
矩形波の周波数をランダムにするだけじゃなくて、PWMで出力レベルも可変にしてみたらどうなるか、 ディップが無くなるかなぁ?とか思ってやってみました。
出力方法をちょっと変更。これまで「0」か「1」の2値だったのを、PWM出力を使って256段階の乱数値に。 PWM出力自体は16Mhz÷256サンプル=毎秒62500サンプル出力としておいて、別途20000回/秒で割込みをかけてアナログ入力値と比較し、 アナログ入力値に達していたら新しい乱数値を計算してまたPWM出力…という具合。一応20000Hz程度のCRフィルタを通したのがこんな波形。
ノコギリの歯のように見える小さいギザギザがPWMによるもの。CRフィルタの傾斜もイマイチだし、折り返しノイズもあるだろうし、 キレイに取り除けなかったけどまぁ放置。階段状になっているのが、アナログ入力値に合わせて毎秒78回~20000回程度で変化する乱数値。
それ以外はスケッチをそのまま流用。↓こんな感じ。
#include <avr/interrupt.h> int d_out = 10; // pwm out int led = 13; int a_in = 0; // analog in pin volatile unsigned char tcnt2; volatile unsigned char in_value; /* 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; // tcnt2 = (int)((float)F_CPU * 0.0000125 / 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 out_data; tcnt2++; if (tcnt2 >= in_value) { tcnt2 = 0; out_data = random_num(); timer1_pwm_out(out_data & 0x0ff); //digitalWrite(d_out,out_data & 1); } } /* 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) } /* making random num */ int random_num() { static int seed = 12345; //random seed int random_value; // random_value = (seed>>1) + (bitRead(seed, 16) xor bitRead(seed, 13) xor 1)*0x8000; random_value = (seed>>1) + (bitRead(seed, 13) xor bitRead(seed, 4) xor 1)*0x8000; seed = random_value; return seed; } unsigned char read_value() { static unsigned char v; int a; a = analogRead(a_in) / 4; if (a != v) { v = a; Serial.println(v); } return v; } /*** ***/ /*** main logic ***/ /*** ***/ void setup() { /* pin setting */ pinMode(d_out, OUTPUT); pinMode(led, OUTPUT); pinMode(a_in,INPUT); /* set up timers */ timer2_set(); //timer2_stop(); timer2_start(); /* timer2 setup */ timer1_set(); in_value = 0; Serial.begin(9600); // for conect to PC } void loop() { /* main loop */ in_value = read_value(); }
タイマ1でPWMを62500spsで出力する為に、例によってまたAVRのSFRを直接弄っちゃってるんだけど、まぁ仕方なし。
出力値が本当に乱数かというと、確かに最上位ビットは1と0のどっちが入るかは乱数だけど、 0が入った場合は必ず値が直前の半分になるという規則性アリ。ちょっと乱数というには厳しいかも… でもまぁ、波形見た感じは乱数っぽい動き。
で、実行結果。まずは50Hz/divで。
やはり等間隔にディップが見える感じ。さらに別の定常波みたいのが見える。
さらに5000Hz/divで。
やっぱディップ見たいのが見て取れるし、14Khzくらいのところに定常波。
もうちょっと高い周波数まで眺めてみると…
PWMの周波数=62500Hzを中心にその1/8周波数ごとに規則的にピークが載ってる。まぁ、原因はわかったかな。PWMによるノイズ。
考察みたいなもの
0か1の2値の矩形波をランダム周期で出力することで「ブラウン(レッド)ノイズ~ピンクノイズ~ホワイトノイズ」 のような周波数パターンを制御できるっぽいことがわかった。どうやら、矩形波の持つ倍音成分も積極的に足し合わせることで、 ホワイトノイズっぽく仕立て上げているみたい。んで、ブラウン~ホワイト間でシフトすることで平均周波数が上下できるみたい。
だけど、矩形波を使っているせいか周期的なディップが生じるみたい。 じゃぁ、2値ではなく出力値をランダムで変化させたらディップがなくならないかなぁと思ったら、甘かった。 PWMで階段状に乱数で出力しても、結局それは矩形波の組み合わせにしか見えないわけで、結局ディップが生じることに。 PWM使うだけ面倒が増えるだけなので、やっても損なだけ。
乱数で値を上下させるにしても、矩形波っぽい形ではなくもっと連続的に変化する波形の変化具合(速度)に緩急つけるしかなさそうな予感。
それ以上のことは、数学的知識も計測機材も脳ミソもないのでわかんなかった、というところ。
まぁ、昔考えていたような「ノイズ成分の山が上下する」んじゃなく、「ブラウンノイズっぽいのからホワイトノイズっぽいのにシフトする」 っていう感じだけわかったので、それだけでも収穫として実験は終了にします。
(追記:ノイズ生成の処理方法を利用して、似非PSG音源を作りました)