2007年06月13日

シンプル版の製作とテスト

前回検討した 回路のうち シンプル版(I2CSND-S) を作ってテストした。

写真は改造後のもの。スイッチを後で追加している。

回路は、回路図そのものにした。回路図に現れないところの工夫では、丸ピンICソケット を割ったものを使って、出力の制限抵抗R2,R3 の交換とフィルター用 コイル L1,L2 の交換、それにフィルター用のコンデンサ C4,C6 の容量追加 が できるようにしている。
ちなみに、丸ピンICソケットは
シングルのもの
が綺麗に仕上る...のだが、安く買い置きしてある 40pin のものをニッパで割って使っている。
フィルター用のコイルは、LAL02NA470Kにして、フィルター用のコンデンサ には、積層セラミックの 10uF (±10%)使った
あと、出力の制限抵抗 R2,R3 は、75 Ωでは音が大きすぎるので、150 Ωにしている。(これでもまだ若干大きい。自分的には、3.3V のとき 220 Ωが良いかも知れない。)

この回路、そのままでは、ISP で書き込みできないという問題点がある。
MISO の先に LC フィルタが付いているため、波形が歪んで ISP の通信ができない。まだステレオ対応はしていないので、R 側の コイルを取ってしまったが、スライドスイッチで LC フィルタを切り離せるようにすべきだ。
L 側は、周波数カウンタにつなげて、クロックのキャリブレーションができるようにしているが、こちらは 無音 (PWM は ON だが 一定値) か 音が小さいときならちゃんと動作するようだ。

(後述するが)CPU 使用率測定機能も 値がばらつくものの有用なことを確認できた。

はまった点

EG の ATTACK の上限処理で 次のようにしていた。

if ((uint32_t)eg.intval + eg.attack >= (255<<8))

これが、(Windows/Linux で動いても) AVR (gcc-3.4.0) では動かない。ちなみに

if ((uint16_t)eg.intval + eg.attack < eg.intval)

は、Windows/Linux で動かない。

if ((uint16_t)(eg.intval + eg.attack) < eg.intval)

これは動くのだが、コード効率も悪そうなので、今は、

uint16_t next = eg.intval + eg.attack;
if (next < eg.intval)

という風にしている。どこが動かないのか すぐにはわからずに、結構はまった。

乗算のコスト

さて、このはまった過程で たかが 8bitX8bit 乗算 が非常に重いことを目のあたりにした。


CPU使用率は、電圧として出てくるので、USB910A の機能で簡単に読み取ることができる。

fcnt result 244625 x4 978500
06:VCC AUTO : 0021772 0x550c 3401.875
02:AIN3 AUTO : 0002304 0x0900 360.000
fcnt result 244625 x4 978500
06:VCC AUTO : 0021772 0x550c 3401.875
02:AIN3 AUTO : 0002336 0x0920 365.000
fcnt result 244625 x4 978500
06:VCC AUTO : 0021772 0x550c 3401.875
02:AIN3 AUTO : 0002304 0x0900 360.000
fcnt result 244625 x4 978500
06:VCC AUTO : 0021772 0x550c 3401.875
02:AIN3 AUTO : 0002192 0x0890 342.500

244625 というのは、PWM 周波数で、AIN3 が CPU使用率をあらわす電圧。こんなふうに多少ばらつく。以下精度が 0.1% のように書いているが、自分の感覚で読み取ったものであり、厳密ではない。


EG なし LFO ありの状態だと CPU 使用率が 6.3 % 前後なのだが、サンプリングレート(8000 Hz) 毎に 8bitX8bit 乗算 fmusu8_8() を入れるだけで15.8 % にまでなるのだ。

元のコードは、

static inline int8_t fmulsu8_8(int8_t a, uint8_t b) {
int16_t v;
v = (int16_t)a * b;
return (int8_t)(v >> 8);
}

で、8bitX8bit をして、上位 8 bit だけ取り出すもの。固定小数点演算の意味で fmulxx としている 。C 的には、16bitX16bit にするしかないので重いわけだ。
アセンブラ化しようと思ったのだが、なかなか面倒。8bit×8bit 乗算(符号なし/符号あり)などを眺めていたのだが、ふと 思い付いて アセンブラでやっていることをC で書いてみた。

static inline int8_t fmulsu8_8(int8_t a, uint8_t b) {
uint8_t i;
int16_t __c , __a;
__c = 0;
__a = a;

for (i=0; i<8; i++) {
if (b & 1) {
__c += __a;
}
b>>=1;
__a <<= 1;
}
return __c>>8;
}

これで 15.8 % が 10.3 % 程度まで下がった。
サンプリングレート毎に行う 8bitX8bit 乗算のコスト が 9.5 % から 4.0 % にまで下がったことになる。乗数が 16bit から 8bit に半減したおかげだ。(ちなみに、16bitx8bit 乗算もよくつかっているが同じコスト)。
おなじようにして、C 版の 16bitX16bit 乗算 のコストを調べたところ 12.1 % になった。被乗数 の方も 32bit に増えているので、3 倍というのは妥当だろう。サンプリング毎のCPUクロック数 は、2000 なので、12.1 % は、242 クロック(平均)ということになる。

LFO/EG のコスト

EG を入れると、DECAY が効いている状態で 11.8 % になった。もとが 10.3 % なので、1.5 % という計算。16bitX16bit と 16bitx8bit 乗算が 1/16 の頻度だから (12% + 4% )/16 = 1.0% 。+それ以外の処理で 1.5 % は妥当なところか。

ステレオ化のコスト

結局 8bitX8bit 乗算 がもうひとつ増えるので、4.0 % 増える計算 。EG も乗算 2 つ余計に入る予定で 1.0 % ぐらい増えるはず。

フィルターのコスト

AVRマイコンによるMIDIシンセサイザーの製作 にある フィルターだと

y[i] = y[i-1] + m * (x[i] - y[i-1])

で サンプリングレート(8000 Hz) 毎に 16bitx16bit 乗算がはいりそうだ。この場合 12.1 % のコストで、16bitx8bitで済ませれば 4.0 % のコスト。

どのように拡張できそうか

ここまで合計すると、EG あり 11.8 % + ステレオ 5.0 % + フィルタ 12.1 % で 合計 28.9 % 。

このバランスだと サンプリング周波数を 倍の 16kHz にしてもいけそうだが ... フィルタを凝れば 8KHz でも厳しいか。もともとフィルターを凝るのが目的なので、サンプリングレートをあげるのは二の次にして、進めていきたい。

ところで、FM音源化について考察しておく。各オペレータには、EG が付く。EG あり LFO なしだと だいたい 10.5% ぐらいだろう。これに対して 16bitx8bit 乗算が1つ余分に付く見込みで、合計はだいたい15% 。あと最終的な出力のボリューム 8bitx8bit 乗算が 2 つほど必要なので 8% がさらに必要。計算上は 6オペレータ分はいるが、たぶん無理で 4 オペレータが妥当そうだ。

ホスト側のプログラム

ホストには、USB910A を使う。以前使った コードを改造してi2c_send()/i2c_recv()を作り、sndtest.c が使えるようにした。
USB910A の FW は、version 0.5 ではうまくない。ちゃんとつながるのだが、いったん CLOSE すると 再接続ができない。、version 0.4 を使えば ちゃんと動く。
0.5 は、機能的には 0.4 と変わらないのだが、ベースコードを最新にしていて コードが 小さくなっている。原因はボチボチ調べることにして、とりあえず、version 0.4 を使うことにする。

試聴

まずノイズについて、USB の 5V で動かすと たいへんひどい。本体 PC でエディットするときのカーソル移動がノイズになってあらわれるぐらい。3.3V もあまり期待していなかったのだが ... こちらは全然ノイズが聞こえない。これぐらいなら十分実用になりそうだ。

スプリアスについて

正弦波の場合、高調波が(あまり)ないので、結構高音までいける。
三角波も おなじような感じ。それに反して のこぎり波は、ちょっと高音になると スプリアスのために、変な音がまじってくる。
逆に 正弦波でかなり低音にすると、(時間軸の)量子化ノイズのために、へんな音がまじってくる。
サンプリングレートを倍にすれば、1 オクターブ上まで OK ということになるのだろう。ただし、低音の問題は解決しない。

PC(の 8bit 8Khz) と比較して

なんだか音がはっきりしている気がする。余計なものが一切なくダイレクトにヘッドホンを駆動しているせいかも知れない。同じコードで同じ波形を作っているので、それぐらいしか違いはわからなかった。

判断するだけの経験がないので、あまり具体的にはかけなかった。興味があるかたは、ぜひ自作してみてほしい。ここまでのコードは、→ i2csnd-0.2.tar.gz

おわりに

これで、実機の環境も Windows/Linux に追いついたことになる。これをベースに次は完成向けて機能を作りこんでいこうと思う。その際 個別機能の構造体 VCO とかは、ひとつの構造体(名前は OPERATOR ?)に押し込めて、整理しようと思う。こうしないと評価環境でのマルチチップシミュレーションが作りづらい。実機でのポリフォニックは(あまり)考えていないが、FM音源版の準備という要素もある。
posted by すz at 16:14| Comment(0) | TrackBack(0) | I2CSND

2007年06月11日

回路の検討

ターゲットのチップは、ATtiny45 ということにした。次に回路を検討することにする。
まず最初に作る チップ1個のシンプル版は次のようにしようと思う。

シンプル版(I2CSND-S)



シリアルプログラミングできる範囲のピンのみを使う。... とすれば使えるピンは、RESET/VCC/GND あと I2C 用の SCL/SDA を除いて 3つのみとなる。ステレオにするつもりなので、出力は、OC1B/OC1A の2つに決まる。あとひとつは、CPU 使用率測定用。このピンは、CLKI 機能もあるので、外部クロック化にも使える。クロックソースは、マスタを水晶発振にして、その CLKOUT にすれば良いだろう。これで全部埋まった。機能として 周波数キャリブレーションも作るつもりだが、これは OC1B を兼用する。-- PWM の周波数を測定するので、無音レベルでも測定できるはず。

さて、どうやって DA変換するか ... アンプ を使わない簡単なやりかたがある。LC フィルタをかませて DA 変換し、コンデンサで直流分を除いて 出力制限抵抗で 出力レベルを調整すれば良い。
16 Ωの ヘッドホンを使うとして ... 75 Ωの 出力制限抵抗を入れれば、最大出力が 34 mW になる。最大電流も 19 mA と 問題ないレベルに収まる。低抵抗のボリュームは持っていないので、出力制限抵抗は差し替えて変更するつもり、最初決めたら変更することはないと思うので、それで困らないはず。

LC LPF の周波数は、(将来的にサンプリング周波数を 16kHzにしたいので)8kHz 弱にしてみた。いまのバージョンではサンプリング周波数は 8KHz なのでちょっとやばいかも知れない。ただ、PWM の周波数は、250 kHz なので、内部の処理次第でなんとかなるような気がする。なんともならないなら、すなおに、LPF の周波数を下げれば良い。コンデンサを 47 uF にすればよさそうだ。
コイルは、秋月で10個100円の LAL02NA470K が使えるかも知れない。max 70mA / 5.8 Ω の定格なので、電流的には大丈夫。それで全然ダメなようなら、千石で10個270円の LHLZ06NBを使ってみたい。こういったフェライトコアのコイルは、波形が歪むらしい。ただ、歪みも含めて(この装置の)特性だと割り切ってしまえばいいと思うので気にしないことにする。

ところで、このシンプル版は、出力に電源ノイズが載る。電源があまりに弱いと(USB の 5V を使う予定でたぶんすごく弱い)CPU の処理の変化までも、音となって出力されて来そうだ。3.3V のレギュレータを通して使えば、多少マシになるかもしれない。ただ、この回路はあくまで開発テスト用と位置づけて、あまり期待しないほうがよさそうだ。

ついでに小型スピーカーを鳴らすこともオプションとして考えておく。


2 つの出力を使って逆相にして 出力を得る。(BTL) 。この場合、直流分をカットするコンデンサは必要ない。16 Ωの 小型スピーカを使うとして ... 75 Ωの 出力制限抵抗を入れれば、最大電流は、38 mA と ぎりぎり OK。最大出力は 4 倍 の 137 mW になる。
もちろんプログラムで対応しなければならないが、これぐらいの改造でスピーカが鳴らせるのなら、対応する意味もあるだろう。

ちなみに、相手がスピーカなので、ELM - WaveTable電子オルゴール の回路のように LC LPF を取ってしまっても良いかも知れない。

マルチチップ版(I2CSND-D)

2 個のデバイスを使うマルチチップ版(i2CSND-D) の回路も考えてみた。



全部の ISP は付けるつもりはないが、1 つぐらいはあったほうが良いだろうということで、最初のチップだけ ISP をサポート。あと、I2C は終端抵抗をちゃんと付けることにする。周波数キャリブレーションもサポートしたいので、抵抗を間に入れて、MIX することにする。マルチチップ版では、PWM の出力も止めないといけないのでプログラムで対応が必要。

マルチチップ版では、デジタルの出力を アナログ部にある Buffer まで引っ張る。ここで波形を整えて、DA 変換する。DA 変換の際に
( オペアンプに高い周波数が渡らないように ) RC LPF もかけておく。オペアンプの後 LC フィルタを入れようと思う。これで良いのかどうか ... 不安なところもあるが、コイルやコンデンサを選択する楽しみもあるかも知れないので、これで行こうと思う。

この回路を N 個に拡張するのは簡単だ。ATtiny45 をデイジーチェーンして、Buffer に入力すれば良い。Buffer に使うチップは、VHC/VHCT シリーズである必要がある。HC シリーズだと クランプダイオードが入っているので、出力レベルが入力のレベルの影響を受ける。入力レベルを 落とす工夫が必要で、抵抗2つ使って 1/2 とかに分圧すればよいのだが、配線はそれなりに面倒。(参考:東芝のFAQ) ちなみに、(負論理の)インバーターでも(正論理の)バッファーでも良い。秋月で扱っているのは、VHCT540AFVHC244F。ちなみに VHC04F(フラット) は、千石扱っている。気合を入れて作るなら 一方向に 8 つ並んでいる VHCT540AF が良さそうだが、VHC244Fの片方向だけ使うのも手配線が楽そうで悪くはない。


次のようにすれば、1ピンを浮かすだけで ユニバーサル基板に半田づけできて、4bit の Buffer として使えるのだ。



オペアンプは、NJM4580D を使うのが無難。秋月で安く売っている LM358 でも十分かも知れない。スピーカーを鳴らしたいなら、オペアンプの変わりに秋月で安く売っている MC34119 を使うのも良いかも知れない。

マルチチップ版の回路は、いまのところ考えただけ。プログラムに影響があるところだけ、決めておけばよく、実際につくるときに、検討しなおすかも知れない。
posted by すz at 14:44| Comment(0) | TrackBack(0) | I2CSND

2007年06月08日

Server-Client化と通信プロトコル

いままで作ってきたプログラムは、サウンドジェネレータの動作確認のためのものだった。これをもとに、AVR で I2Cスレーブとして動くプログラムと、それをコントロールするプログラムを作っていくことにする。もちろん、実機だけで動くものではなく、Linux/Windows で評価できるものにしていくのが前提。
AVR側 は、I2CデバイスドライバのAPI でコマンドパケットを受け取り処理する構造にして、コントローラ側は、コマンドパケットを生成して送信する構造にすることになる。そして ... (評価環境では)その間をつなぐものが必要。
このつなぐところを、どうするか...というと TCP/IP による通信にしてしまうのが、実は最も簡単で便利。便利というのは、Socket API は、Linux , Windows で同じように使えるのでコードを1つ作ればよいというのが理由。コントローラ側/デバイス側の構造を変に変えなくて良いという理由もある。

コントローラ側の処理の流れ

メインループは次のように キーが入力されるとそれに対応した処理になるようにしている。もはや AVR-CDC とは関係ないのだが、getchar() に相当する関数が、いままでの経緯で usbcdc_getc() になっている。( ちなみに、プログラムを2つに分けた利点がもう1つあある。前のコードは、MinGW の環境で問題があったが、このコードなら Linux と同じように動く。)

for (;;) {
c = usbcdc_getc();
change_note(c);
}


change_note() はなにするかというと、キーに応じて関数を call するだけのもの。

static void change_note(uint8_t c) {
const uint8_t note_base = 36+24; // C1
static int oct;
switch(c) {
case 'z': oct --; break;
case 'x': oct ++; break;
case 'q': exit(0);
case 'w': set_param(PARAM_VCO_WAVE, WAVE_SIN); break;
case 'e': set_param(PARAM_VCO_WAVE, WAVE_TRI); break;
case 'r': set_param(PARAM_VCO_WAVE, WAVE_SAW); break;
case 'a': note_on(note_base + 12*oct +0, 127); break;
case 's': note_on(note_base + 12*oct +2, 127); break;
case 'd': note_on(note_base + 12*oct +4, 127); break;
case 'f': note_on(note_base + 12*oct +5, 127); break;
case 'g': note_on(note_base + 12*oct +7, 127); break;
case 'h': note_on(note_base + 12*oct +9, 127); break;
case 'j': note_on(note_base + 12*oct +11, 127); break;
case 'k': note_on(note_base + 12*oct +12, 127); break;
case 'l': note_on(note_base + 12*oct +14, 127); break;
}
}


で、たとえば note_on() は何をするかというと、パケットを作って、i2c_send() で 送信する。

static void note_on(uint8_t note, uint8_t verosity) {
ext_buf[0] = CMD_NOTEON;
ext_buf[1] = note;
ext_buf[2] = verosity;
ext_buf[3] = 0;
i2c_send(0x20, 4);
}


i2c_send は、以前作った I2Cホストドライバの API で、これにあわせて、TCP/IP で通信するモジュールを作る。この API にあわせて USB910A の I2C 機能を利用するモジュールを作れば、そのまま実機テストできるし、AVR のコントローラを作った場合でも 利用できるのだ。

ちなみに、コマンドは MIDI もどきにする。CMD_NOTEON は、0x90 で、ローカルな機能には、MIDI に決して MAP されない 0x00 〜 0x7f を使う。

デバイス側(AVR 側)の処理の流れ


for (;;) {
if (can_sndout()) {
generate_snd();
continue;
}
if (i2cdev_can_getc(1)) {
command_proc();
i2cdev_flush();
}
}


メインループはこんなもの。バッファーが FULL でなければサウンドを生成して詰め込み、I2C経由でコマンドがくればその処理をする。

さて、このコードは バッファーが FULL になった状態でコマンドがこなければ無限に回ってしまう。AVR のときは、それでよいのだが、Linux/Windows のときは、それではまずい。CPU を離すしくみを入れる必要があり、それを入れるとすれば、i2cdev_can_getc() 以外にない。
実際どうしているかというと、select() を使って通信がないときも、0.05 秒だけ待ち合わせるようにしている。逆にここで止まるので、音が途切れないだけの量は、バッファーに溜めておかなければならない。


static void command_proc() {
uint8_t cmd,p1,p2,p3;
cmd = i2cdev_getc() & 0xf0;
p1 = i2cdev_getc();
p2 = i2cdev_getc();
p3 = i2cdev_getc();
if (cmd == CMD_NOTEON) {
note_on(p1,p2);
} else if (cmd == CMD_NOTEOFF) {
note_off();
} else if (cmd == CMD_SETPARAM) {
switch(p1) {
case PARAM_LFO_FREQ:
lfo.wave_inc = ((int16_t)p3 << 8)|p2;
break;
case PARAM_LFO_INTENSITY:
lfo.intensity = p2; break;
case PARAM_VCO_WAVE:
vco.wave_type = p2; break;
        :
:
}
}
}


さて、コマンド処理はこんなかんじ。いまのところ 4 バイト固定のパケットが来るとして処理している。あと、内部に持っているパラメータは、CMD_SETPARAMで全部設定可能なようにしている。他のコマンド/機能はまだ実装していないが、簡単に追加していけそうだ。

AVR 側の処理とサイズ

ずいぶん回り道した感があるが、実は AVR 固有の機能で作らないといけないのは、PWM で 出力する部分だけ。サンプリングレートで割り込みが起きるようにして、PWM のレジスタにデータをセットするだけのものを作れば、Linux/Windows 版と同等になる。

いきなり動作するとは思えないが、一応作ってみてサイズを見てみた。

text data bss dec hex filename
2032 0 89 2121 849 i2csnd.elf


なんと..まだ 2KB 。これなら機能を充実させていっても ATtiny45 に入りそうだ。...ちなみに これなら ATtiny2313で何か作れるのでは?と考えては決していけない。最小限度必要なものを埋めていって 2KB ... スタート地点ですでに 2KB なのだから、なにかが作れたとしてもコードを縮小することばかりに追われてしまう。

プログラムについて

ここまでのプログラムを i2csnd-0.1.tar.gzにおいておく。今度は大丈夫だと思うので、どんな音が出るか興味がある人は動かしてみてほしい。

linux でビルドする場合は、cd lin; make。 MinGW+MSYS の場合は、cd lin; make -f win.mk 。
動かし方は、Window を2つ用意して、1つで i2csnd を実行し、もうひとつで、sndtest を実行する。i2csnd は、MSYS コンソールでも動作するが、sndtest は、コマンドプロンプトでしか動作しない。
posted by すz at 18:56| Comment(0) | TrackBack(0) | I2CSND

2007年06月07日

LFOとEGの設計とテスト

ddstest1.c に LFOとEGを組み込んでみた。→ ddstest2.c(修正版:ddstest2a.c
EG サンプル↓


LFO と EG は高い頻度で動かす必要がない(と思った)ので、↓のように、1/16 の頻度に落としてCPUを節約している。(あと負荷を分散させるよう工夫もしている)
ただ、LFO もサンプリングレートで動かしてやれば、2オペレータのFM音源にもなるかも知れない。(参考:JO-MIDI-FM)
今は、VCFをどう入れられるかが興味の対象なのだが、一段落したらFM音源化にもチャレンジしてみたい。


static void generate_snd() {
static uint8_t cnt;
:
cnt++;
if (cnt >= 16) {
do_lfo();
cnt = 0;
} else if (cnt == 8) {
do_eg();
}
}


LFO の本体はこんな具合。VCO を 正弦波専用に単純化して、VCOのパラメータを変更するコード。

static void do_lfo() {
uint8_t pos;
int8_t rev = 0;
int8_t v;
int16_t v2;
lfo.wave_pos += lfo.wave_inc;
pos = lfo.wave_pos >> 8;
if (pos >= 128) {
rev = 1;
pos = 255 - pos;
}
if (pos >= 64) {
pos = 127 - pos;
}
v = __LPM(sintab+pos);
if (rev) v = -v;
v2 = (int16_t)v * lfo.intensity;
vco.lfo_inc = fmulsu16(v2, vco.wave_inc)>>4;
}


EG はちょっと長いけれども、全部載せるとこんなかんじ。


static void do_eg() {
uint8_t state = eg.state;
if (state == EG_NOTEOFF) {
eg.intval = (0 << 8);
}
if (state == EG_NOTEON) {
eg.sustain_cnt = 0;
if (eg.attack == 0) {
eg.intval = eg.sustain_lvl;
eg.state = state = EG_SUSTAIN;
} else {
eg.intval = 0;
eg.state = state = EG_ATTACK;
}
}
if (state == EG_ATTACK) {
if ((uint32_t)eg.intval + eg.attack >=
(255<<8)) { // overflow
eg.intval = (255 << 8);
if (eg.sustain_lvl == 0) {
eg.state = EG_RELEASE;
} else {
eg.state = EG_DECAY;
}
} else {
eg.intval += eg.attack;
}
} else if (state == EG_DECAY) {
eg.intval = fmul16(eg.intval, eg.decay);
if (eg.intval <= eg.sustain_lvl) {
eg.intval = eg.sustain_lvl;
if (eg.sustain_time == 0) {
eg.state = EG_RELEASE;
} else {
eg.state = EG_SUSTAIN;
}
}
} else if (state == EG_SUSTAIN) {
eg.sustain_cnt ++;
if (eg.sustain_cnt == eg.sustain_time) {
eg.state = EG_RELEASE;
}
} else if (state == EG_RELEASE) {
eg.intval = fmul16(eg.intval, eg.release);
if (eg.intval < (1 << 8)) {
eg.intval = 0;
eg.state = EG_NOTEOFF;
}
}
eg.out = eg.intval << 8;
}


内部的には、16bit で値を計算して、上位8bit だけを出力としている。
ATTACK は、(計算が楽なので)線形で立ち上げることにした。DECAYは、減衰曲線で sustain_lvlになるまで減衰させる。SUSTAIN が終われば、RELEASEで減衰させるが、一応 DECAY と別のパラメータにした。
特別な処理としては、ATTACK のパラメータが 0 なら、SUSTAIN から入る。とか sustain_lvl が 0 なら ATTACK - RELEASE になるとか。ちなみに、CASE 文を使わないのは、(今使っている) avr-gcc(version 3.4) ではコード量が大きくなってしまうから。(CASE 文を使わないくせがついてしまった)

あと、fmul16()とかfmulsu16()とか出てくるが、実機では、アセンブラ化するつもりで関数化している。

おわりに

EG が付いたので、なんだか楽器らしくなってきて音を鳴らすのが楽しくなってきた。デジタルフィルタの VCF を作れば、一応コンポーネントは揃うことになる。もう一息なのだが、VCF は計算量が多く実機とすりあわせていかないといけないので、作るのはひとまずおいておいて ... 実機のコードと それにあわせた評価環境を作ろうと思う。
今のコードは、簡単なUIとサウンドジェネレータが一体になっているので、これを2つに分ける。Linux や Windows でもテストしたいので、プログラム間は、TCP/IP で通信する予定。

追記:
Linux/Windows の両方でバグがあったので、ddstest2.c を修正して、ddstest2a.c にアップデートしました。
posted by すz at 20:42| Comment(0) | TrackBack(0) | I2CSND

2007年06月06日

VCOの設計とテスト

正弦波と三角波、鋸波を生成するVCOが一応できた。


コードは次のようなもの。


prog_char sintab[64] = {
0, 3, 6, 9, 12, 15, 18, 21,
24, 27, 30, 33, 36, 39, 42, 45,
48, 51, 54, 57, 59, 62, 65, 67,
70, 73, 75, 78, 80, 82, 85, 87,
89, 91, 94, 96, 98, 100, 102, 103,
105, 107, 108, 110, 112, 113, 114, 116,
117, 118, 119, 120, 121, 122, 123, 123,
124, 125, 125, 126, 126, 126, 126, 126,
};

struct VCO {
uint16_t wave_pos;
int16_t wave_inc;
int8_t lfo_inc;
uint8_t wave_type;
} vco;

static void generate_snd() {
uint8_t pos;
int8_t rev = 0;
int8_t v;
vco.wave_pos += vco.wave_inc + vco.lfo_inc;
pos = vco.wave_pos >> 8;
if (pos >= 128) {
rev = 1;
pos = 255 - pos;
}
if (vco.wave_type == WAVE_SAW) {
v = 127 - pos;
} else {
if (pos >= 64) {
pos = 127 - pos;
}
if (vco.wave_type == WAVE_TRI) {
v = pos*2;
} else
v = __LPM(sintab+pos);
}
if (rev) v = -v;
sndout(v);
}


サンプリングレートで、この関数を call すると、そのときの出力を計算して sndout() で出力する。アルゴリズムは、DDS(ダイレクトデジタルシンサイザ)。

メインループはこんなかんじ↓。can_sndout() で バッファーが空いていれば、generate_snd() で次のデータを作ってバッファーに詰め込む。

for (;;) {
if (can_sndout()) {
generate_snd();
continue;
}
if (usbcdc_can_getc(1)) {
c = usbcdc_getc();
change_note(c);
}
}


周波数 HZ の波形を出力したい場合は、

vco.wave_inc = HZ * 256 * 256 / SAMPLE_RATE;

となるようにすれば良い。
ちなみに、vco.lfo_inc は、LFO で周波数を揺らすためのものだが LFOはまだできていない。

もちろん、音階に対応した vco.wave_inc の値はあらかじめ作っておく。

prog_uint16_t note_tab[12] = {
8572944/ SAMPLE_RATE,
9082717/ SAMPLE_RATE,
9622802/ SAMPLE_RATE,
10195003/ SAMPLE_RATE,
10801229/ SAMPLE_RATE,
11443502/ SAMPLE_RATE,
12123967/ SAMPLE_RATE,
12844895/ SAMPLE_RATE,
13608690/ SAMPLE_RATE,
14417904/ SAMPLE_RATE, // A3 220 Hz
15275236/ SAMPLE_RATE,
16183547/ SAMPLE_RATE,
};


ちなみにこの値は、220 Hz * 256 * 256 をもとに、2の12乗根 1.059463 を乗除して作ったもの。uint16_t に収まる必要があるので、表現できる最大の周波数は、SAMPLE_RATE ということになる。

ずいぶんはしょってしまったが、本題はこれから。はたして、これで進めてしまってよいのだろうか? グラフ化した波形(先頭のグラフ)はそれなりにまともに見えるが、いったいどんな音になるのだろう -- 確かめてから先に進みたい。

Linuxのサウンド出力

というわけで、Linux でどのようにサウンドを出力するのか調べて作ってみたのが次のコード(説明のためにエラー処理はすべて除いた)。

int sndout_fd;
int audio_buf_size;

static void sndout_init() {
int arg;
struct audio_buf_info info;

sndout_fd = open("/dev/dsp",O_RDWR);
arg = 16;
ioctl(sndout_fd, SOUND_PCM_WRITE_BITS, &arg);
arg = 2;
ioctl(sndout_fd, SOUND_PCM_WRITE_CHANNELS, &arg);
arg = SAMPLE_RATE;
ioctl(sndout_fd, SOUND_PCM_WRITE_RATE, &arg);
arg = 0x0008000c; // fragstotal 8 fragsize 4096
ioctl(sndout_fd, SOUND_PCM_SETFRAGMENT, &arg);
ioctl(sndout_fd, SOUND_PCM_GETOSPACE, &info);
audio_buf_size = info.bytes;
}

static int can_sndout() {
struct audio_buf_info info;
r = ioctl(sndout_fd, SOUND_PCM_GETOSPACE, &info);
if (audio_buf_size - info.bytes >= info.fragsize * 2) {
return 0;
}
return 1;
}

static void sndout(int8_t d) {
int16_t buf[2];
buf[0] = (int16_t)d <<8;
buf[1] = (int16_t)d <<8;
write(sndout_fd, buf, 4);

}


ポイントは、fragsizeを小さくした上で、バッファーが一杯残っている段階で FULL と認識して詰め込みすぎないようにすること。そうしないと、パラメータを変更したときの追従性が悪くなってしまう。
参考にしたのは、
Linux Sound programming with OSS API(リアルタイム性を考慮する)
Linux Sound programming with OSS API(/dev/dsp の機能を調べる)

追記:
私の環境では、どうも 2ch/16 BitParSample (S16LE) 以外設定できないようで、4倍の周波数(32K Hz)になってしまっていました。なので、S16LE 用のコードに変更しました。


Windowsのサウンド出力


#include <mmsystem.h>

#define WAVEBUF_SIZE 1024
#define NR_WAVEHDR 8

HWAVEOUT HWaveOut = NULL;
uint8_t *WaveBuf;
WAVEHDR WaveHdr[NR_WAVEHDR];
int cur_wavehdr = 0;
int cur_wavebuf_len = 0;
WAVEFORMATEX waveFmt;

static void sndout_init() {
int r,i;
waveFmt.wFormatTag = (WORD)0x0001; // PCM
waveFmt.nChannels = 1;
waveFmt.nSamplesPerSec = SAMPLE_RATE;
waveFmt.nAvgBytesPerSec = SAMPLE_RATE*1;
waveFmt.nBlockAlign = 1;
waveFmt.wBitsPerSample = 8;
waveFmt.cbSize = (WORD)0;

WaveBuf = GlobalAlloc(GPTR, NR_WAVEHDR * WAVEBUF_SIZE);
memset(WaveHdr, 0, sizeof(WAVEHDR) * NR_WAVEHDR);
for (i=0; i<NR_WAVEHDR; i++) {
WaveHdr[i].lpData = WaveBuf+ i * WAVEBUF_SIZE;
WaveHdr[i].dwBufferLength = WAVEBUF_SIZE;
}

waveOutOpen(&HWaveOut, WAVE_MAPPER, &waveFmt, 0, 0, CALLBACK_NULL);
for (i=0; i<NR_WAVEHDR; i++) {
waveOutPrepareHeader(HWaveOut,WaveHdr + i, sizeof(WAVEHDR));
}
}

static void sndout(int8_t d) {
int next;

next = cur_wavehdr + 1;
if (next >= NR_WAVEHDR) next -= NR_WAVEHDR;
if (cur_wavebuf_len >= WAVEBUF_SIZE) {
while (!(WaveHdr[next].dwFlags & WHDR_DONE)
&& (WaveHdr[next].dwFlags != WHDR_PREPARED)) {
fprintf(stderr,"wave buffer overflow\n");
exit(1);
}
waveOutWrite(HWaveOut, WaveHdr + cur_wavehdr, sizeof(WAVEHDR));
cur_wavehdr = next;
cur_wavebuf_len = 0;
}
WaveHdr[cur_wavehdr].lpData[cur_wavebuf_len++] = d+128;
}

static int can_sndout() {
int prev;

prev = cur_wavehdr - 3;
if (prev < 0) prev += NR_WAVEHDR;
if (WaveHdr[prev].dwFlags == WHDR_PREPARED) return 1;
if (WaveHdr[prev].dwFlags & WHDR_DONE) return 1;
return 0;
}


Windows の場合は、自分で fragment を作らないといけないので若干面倒。あと、fragment の数とか サイズ、詰め込みすぎないようにするところとか、Linux 版にあわせてみた。

Windows版で参考にしたのは、oggdecのソースコードとかportaudioとか
追記:
バグがありました。nBlockAlignには、waveFmt.nChannels * waveFmt.wBitsPerSample/8 (=1) を入れるべきで、あと、8 BitPerSample では、値が 0 〜 255 なので +128 してやる必要があります。(参考:WAVEデータの作成と再生


視聴してみて

聞いてみたらそれなりに聞こえたので、8bit から拡張するとか補完を入れて精度を上げるとかあまり考えずにゴリゴリいきたいと思う。そういうことはできてから考えれば良さそうだ。

おわりに

なんかコードばっかりのページになってしまった。ほとんどおまじないみたいなものなので、解説も微妙に書きにくい。もしもっと知りたいなら、ここに出てきたキーワードでググッてみてほしい。

ちなみに、実行可能なコードのソース ddstest1.cも置いておくので、どんな音になるか興味がある人は動かしてみてほしい。(注意: ddstest1 はバグっています。ddstest2a.cが機能拡張+バグ修正版なのですが、(キー入力の問題で)MinGWの環境ではまともに動きません。)
posted by すz at 23:07| Comment(2) | TrackBack(0) | I2CSND

2007年05月25日

I2CSND:構想

マスターコントローラ

当面開発用 マスターコントローラには、USB910A を使う。これを使って 肝心のデバイス側をデバッグする。スタンドアローンにするとか、MIDI インターフェイスを持たせるとかは、スレーブができてしまったから考えればよいだろう。

USB910A をつかえば、スレーブ側の発振周波数のキャリブレーションもできるし、スレーブ側のCPU使用率も測定可能だ。

スレーブ側のCPU使用率を測定するのは簡単で、ポートを1つ使って、実行しているとき H 、そうでないときに L にするようにしておいて、RC LPFを通した後の電圧を測定すればよい。
スレーブ側が間に合わなければ、想定した音にならない。それを判断するのに
、スレーブ側のCPU使用率を測定できることは重要ではないかと思う。

スレーブ側AVRの選択

サウンドを生成する処理では、非常に多くのかけ算を使う。なので、8bit 乗算命令があるMEGAシリーズを選ぶのが妥当なのだが ... いくつか大きな問題がある。

1) TWI 版 I2C デバイスドライバのコードを持っていない。
MEGA シリーズは、USI は持っていなくて、TWI インターフェイスを使う。なので、TWI 版のドライバを作るところから始めないといけない。そこから始めるのはちょっとつらい。ある程度できあがって、勢いがついたところで ドライバを作りMEGA に移行したいところ。

2) 高周波 PWM を持っていない。
最初から DAC を付けることは考えていなくて PWM を使おうと思っている。PWM の周波数は高ければ高いほど良いのだが、64MHz の 高周波 PWM を持っているのは、8 pin の ATtiny45系 と 20 pin の ATtiny461系 。これでいったいどんな音がでるのか興味があるのだ。

... というわけで、はじめは、ATtiny45系 と 20 pin の ATtiny461系 を
ターゲットにしようと思う。

手持ちは、ATtiny45 と ATtiny861。ATtiny45 で動けば、複数デバイスにしたとき楽なので、ATtiny45 からスタートしようかと思う。そして、プログラムがあふれたり、10 bit 高周波 PWM をためしたくなれば、ATtiny861 に移行して、複数のデバイスでのテストは、ATtiny45/85 にもどるつもり。

8bit 乗算命令がないというデメリットは、できるだけ周波数を上げることで 少しでもカバーしたい。ひょっとしたら 32MHz で動くかもしれないので、それもためしてみたい。

ところで、外部から正確なクロックを入力できるようにするかどうかに
ついては保留。当面は 内部RC発振器を使うことにしたい。

サウンド出力

1つのデバイスは、単音のみしか考えないことにする。もともと複数付けるつもりなのだし、ポリフォニックにする余裕があるなら、スペックを上げた方が良いだろう。インターフェイス的にも割り切った方がすっきりする。
単音だけだが、アナログミキサなんて考えたくないので、ステレオ出力にはしようと思う。

アナログ部

PWM は、0V - 電源の間の矩形波を生成するので、電源の変動が出力に載って来てしまう。それでは面白くないので、PWM と アナログ変換(RC LPF) の間にバッファを入れ、バッファの電源は専用のもの(3V前後)を用意する。それを オペアンプで増幅して ヘッドホン出力にする予定。オペアンプには負帰還がかかるので電源に気を使う必要はなく、5V を使う。

バッファには、普通の 74HC シリーズではなく、入力の電源側にクランプダイオードが入っていない 74VHC シリーズが良いのではないかと思う。VHC04 とかなんでも良いのだが、74VHC244F ならば、秋月で5個100円だし。これを使えばどうか。専用電源には、秋月で扱っているSI91841DT-285が良さそう。

といっても、最初のうちは動作しているかモニタできれば良く、さらにミキシングなど考えなくても良いので、AVRのPWM 出力を 直接 LC フィルタ にかけるだけで済ませるかもしれない。

マスタースレーブ間の通信プロトコル

結局のところマスター側では MIDI をサポートするだろうから、MIDIそのものでなくとも MIDI から変換しやすいようなものにすべきだろう。

といっても、音色のパラメータテーブルのようなものは内部に持たせず、マスターからパラメータそのものを送り込む。プログラムメモリを食うし、どういう値にすればよいか当初は皆目わからないから。

Linux版/MinGW+MSYS版

例によって デバッグのために Linux版も作るつもり、できたら MinGW+MSYS版も作りたい。これができると PC で作る音とどれぐらい違うのか比較できるだろうし、新しい機能を入れるときに PC でまず試すことができる。( 実機だと チューニングしないと 入れたい処理がこなせないかも知れない )

サウンド生成処理

TetraHeadから TetraHead-v.1.0.tar.gz をダウンロードすると、一連のドキュメントが pdf で入っている。肝となるフィルターのアルゴリズムについても www.musicdsp.com の膨大なアーカイブからピックアップしたものが、filtres.pdf にまとめられている。LFOやVCOなどのコンポーネントの仕様も conseption.pdf にきっちり書いてある。これらのドキュメントを参考にして設計したいと思う。...といってもソースコードを流用するわけでも、アルゴリズムをそのまま使うわけでもない。ソースコードが Java だからではない。AVRでは力不足で使いたくても使えないのだ。

おわりに

とまぁ、好きなことを書いてみたらずいぶんな大風呂敷になってしまいました。こういうものを短期間で全部作るということではありません。こういう構想のもとに、(しょぼいかもしれない)サブセットからスタートして、興味が赴くままに拡張していくつもりです。あと、次回はなにか形になったら書くつもりなので、時間がかかりそう。
posted by すz at 18:20| Comment(0) | TrackBack(0) | I2CSND

2007年05月23日

I2CSND:はじめに

作ってみようと思っている装置で書いた

(4)I2C で制御する音を出すなにか
電子工作のテーマとして音を出すものを作るのは楽しい。I2Cドライバも作ったので、ATtiny45 (かひょっとすると 85) を複数個つかって音を出すなにかを作ってみたい。
プログラム次第で簡単なものから高度なものまで作れると思うが、バーチャルアナログシンセなんてものにもトライしたい。外部クロック入力にして正確なA音程を目指すのも良いが、RC発振器をキャリブレーションして使うというのも味があってよいかも知れない。電源ノイズが載るのだけは避けたいので、電源だけは専用のレギューレータをつけてやりたい。

についてボチボチ取り組みたいと思う。

バーチャルアナログシンセについて

一言で書くと、DSP でアナログシンセをシミューレートするタイプのシンセサイザ。WikiPedia には、こういう説明がある。

AVR でバーチャルアナログシンセを作った草分け的な作品は、SYN8535で、8MHz の AT90S8535 の上で作られていた。今は、ATmega16/ATmega32 に移植されて、AVRSYNに進化している。
AVRマイコンによるMIDIシンセサイザーの製作なんていうページもある。バーチャルアナログシンセについて、わかりやすく説明されているので紹介しておく。

どんなものを作りたいのか

USB や I2C が自由に扱えるようになったので、
AVRマイコンによるMIDIシンセサイザーの製作のようなPWMでサウンドを生成するものを、I2C スレーブとして実装して、複数のスレーブをマスターコントローラに接続し、USB で PCと接続して制御するようなものを作りたいと思う。

もちろん本格的なバーチャルアナログシンセを作るわけではない。音作りそのものを楽しむなら、PC で作ったほうがよさそうだ。ググるとZynAddSubFXなんていうオープンソースのものも見つかるし、他にもあるだろう。

本格的なものを作るなら、AVR では力不足で、AT90SAM7Sなどで作るのが良さそうだ。
ちなみに、AT90SAM7SはDMAを持っているので、シリアルDACに接続してもCPUの負荷にならないし、内蔵メモリも多い。AT90ARM7S開発ボードとしては、ベストテクロノジーの BTC090や Olimex の SAM7-H256(ソリトンウェーブが扱っている。)あたりがよさげ。


でも、AVRという限られたリソースでどれぐらいのものが作れるのか挑戦したい。そのために、まずはベースとなるものを作る。バーチャルアナログシンセそのものは、すぐには完成しないだろう。..というかいくらでも工夫の余地がありそうなので、完成しないかも知れない。

追記:
オープンソースのシンセサイザで、設計するとき参考になりそうなものをピックアップしてみた。
the Linux MIDI-HOWTOを見て、spiralmodular
Source Forge で検索してみつけた TetraHead Sound System。Java で書かれている。(英語ですらないが)pdf のドキュメントがあって、わかりやすくまとめられていて参考になりそうだ。
posted by すz at 21:38| Comment(0) | TrackBack(0) | I2CSND