DE0で作った音源にエンベロープを付けた。
前回、DE0で作った音源をMIDIキーボードで鳴らしてみた。のつづき。ポリ化のつぎは、シンセになくてはならないエンベロープを付けてみる。つまり、一音一音の音量の調節、ADSRってやつだ。
これまではMIDIキーボードで指定された音程を固定の音量で鳴らしていたので、今回はADSRの各パラメータの大きさに応じて音量を調節するしくみを作る。デジタル音声信号の音量調節とは、つまりは掛け算だ。乗算器ってどんな仕組みだっけ、、と久々にパタヘネ本を引っ張りだして読み返したりしたけど、HDLでは乗算器を自分で書く必要はなくて、単にこんなコードを書くだけで済んだ:
wire [23:0] outvol = cur_vol * cur_velocity * 2 * wavedat; assign DAT = outvol[23:16];
ここで、wavedatには正弦波のデジタル信号が8ビットの整数値として入る。これに、エンベロープで制御された音量cur_volと、MIDIキーボードを叩いた強さ(ベロシティ)cur_velocityを掛け合わせる。8ビットの値を3つ掛けるので答えは8 x 3 = 24ビットのデータになるけど、DAコンバーターには12ビットしか入らないので、24ビット中の上位12ビットだけを取り出してDAコンバーターへの出力DATに入れている(そのせいか小さな音の音質があんまりよくないのだけど。。これを直すには正弦波のビット数を上げるしかないのかな。。?)
乗算回路だって除算回路だって1行で書けるHDLって、便利だなぁ。。
カウンタでADSRをつくる
ADSRの各パラメータにしたがってcur_volを上下させるコードはこんなふうに書いてWaveGenに追加した。A, D/S, R, 無音のそれぞれにひとつずつ状態を割り当てて、ノートオン時にAを始め、Aが終わったらD/S、ノートオフでR、、って感じの状態遷移を行う。HDLプログラミングって、こういったカウンタや状態遷移ばっかりだね。それと、always文がどんどんでかくなってきて、アプリプログラマーとしてはリファクタリングしたくてたまらないのだけど、なかなかうまく小分けにできない。。
case (env_st) 0:; // note off 1: // attack begin if (cur_vol == 255) begin env_st <= 2; env_cnt_speed <= dec_val; end else cur_vol <= cur_vol + 1; end 2: // decay/sustain begin if (cur_vol > sus_val) cur_vol <= cur_vol - 1; end 3: // release begin if (cur_vol > 0) cur_vol <= cur_vol - 1; else env_st <= 0; end endcase
シリアルUSBとDE0をつなぐ
こんな感じで、エンベロープ機能そのものはそれほど難しくなく実装できた。問題は、ADSRの各パラメータ(それぞれ8ビット)を設定するためのユーザーインターフェースをどうするかだ。DE0に付いてるボタンやLEDでちまちま入力するってインタフェースはとっても使いにくいしHDL書くのが面倒。PC上でGUIを作ってDE0と通信させることにした。
通信手段をいろいろ探してたら、こんな製品を見つけた。
FTDIのUSBシリアル変換ケーブルってやつ。一見すると単なるケーブルに見えるけど、USBコネクタの中にシリアル通信用のICであるFT232RLが内蔵されてて、PCからはシリアルポートとして認識される。このシリアルポートにデータを流すと、ケーブルの反対側からはMIDIやRS-232Cと同じUARTの非同期シリアル信号として出力される。前に作ったMIDIデコード用のHDLをそのまま使える!ってことで、さっそくこのケーブルを購入。MIDIデコードのコードをコピペして921.6kbps用にタイミング合わせたら、すんなり動いた。シリアルデータをバイト単位で切り出すSerialIfモジュールとして追加した。
さらに、そこから流れてくるバイト列を解釈するUIControllerモジュールを追加。どんなプロトコルにしようかな?エンベロープ以外にも使えるよう汎用性持たせたいな?と考えて、まあとりあえず、
- 1バイト目は0xFF
- 2バイト目はコマンドを表す(今回はエンベロープ設定用の0x01だけ使用)
- 3〜6バイト目には32ビットデータを入れる
って固定長のプロトコルにしておいた。デコード簡単だし。例えばADSRの各パラメータを送るには、FF 01 xx xx xx xx って感じの6バイトをUSBシリアルに流す(xx部分にはADSRのそれぞれが1バイトずつ入る)。するとUIControllerモジュール側でコマンドを解釈して、ADSRのパラメータを更新し、WaveGenに供給する仕組み。
ChromeのSerial APIでシリアル通信
あとはPC側のGUIだけど、シリアル通信できるGUIアプリってどう作るか。俺がすぐ作れるのってJavaアプリとかAdobe AIRとかだけど……今さら感が漂う。できればHTML5がいいな。最近ChromeってJSでなんでも書けそうな勢いなのでシリアル通信用APIもあるだろ?ってググったら、あった。さすがChrome。
もっとも、こういったネーティブリソースにアクセスするには、ChromeのPackaged Appsという形式でHTML5アプリを作る必要がある。manifest.jsonっていうファイルにアプリケーション情報を記述したりChromeに登録したりすると、Serial APIはじめネーティブアプリ的なAPIが利用できるようになる。Chromeのドキュメントを見ながらそのあたりの手順にしたがってPackaged Appを作成した。シリアルポート一覧を取得するサンプルコードを動かしてみたら、うまいこと一覧が取れた(でも、実はSerial APIってWindowsでは動いてないらしく、最初はWindows環境でテストしてて動かずしばらく悩んでた…)。
ここで、/dev/cu.usbserial-FTG3ZBV4っていうのがFTDIのUSBシリアル変換ケーブルのシリアルポート。ちなみに、UNIXでは古来よりシリアルポートへの送信に/dev/cu、受信に/dev/ttyというデバイスファイル名を使うのが習わしだそうな。
さて、このWebUIアプリにはADSRのそれぞれをコントロールするノブを4つ置きたい。いろいろ探して、Canvasベースのノブを簡単に作れるjQuery Knobってのを見つけた。
このノブを4つ並べて、値が変化したときにコールバックされるfunctionを以下のように記述。
function updateAdsr() { chrome.serial.open("/dev/cu.usbserial-FTG3ZBV4", {bitrate: 921600}, onOpen); }
こんな感じで、USBシリアル変換ケーブルのポートをボーレート921.6kbpsでオープンする。開いたらonOpenが呼ばれるので、
var onOpen = function(connInfo) { // get conn id connId = connInfo.connectionId; console.log("opened: " + connId); // send env command var buf = new ArrayBuffer(6); <ここで送信するデータをbufに入れる> // write and close the connection chrome.serial.write(connId, buf, function() { console.log("written."); chrome.serial.flush(connId, function() { console.log("flushed."); chrome.serial.close(connId, function() { console.log("closed."); }); }); }); }
といった具合にJSっぽいコールバックだらけのコードを書く。ポートがオープンしたらconnectionIdが取れるので、それを使ってwrite()を呼び出し、ArrayBufferに入れたバイト列を書きだすだけ。ちゃんと即時送信されるようにflush()も呼んでおく。それと今回は1回コマンドを送るたびにポートをclose()するようにした(この書き方だとclose漏れが起きるかも。。?)。
一方、write()に渡すArrayBufferはこんなふうにして作る。
var bufView = new Uint8Array(buf); bufView[0] = 0xff; // header bufView[1] = 0x01; // enverope command bufView[2] = $("#env_a").attr("value"); bufView[3] = $("#env_d").attr("value"); bufView[4] = $("#env_s").attr("value"); bufView[5] = $("#env_r").attr("value");
...いつのまにかJSにもバイト列をいじるAPIが完備されててびっくりしたよ。さきほどの4つのノブに付けたenv_a、env_d...というIDを使ってノブの値を読み出し、ArrayBufferに入れるだけ。
エンベロープ付きの音が出た!
そんなこんなで、DE0で作ってきた俺様音源にエンベロープが付いた!それっぽいデモを作ってみた(MIDIファイルはまたここからお借りした)。
おぉ、単なる正弦波なのに、エンベロープ付けるだけでそれっぽくなるなぁ。。しかしReleaseを長くすると8音じゃ足りなくなる。同時発音数増やしたい。
次の野望
フィルターは鬼門だなぁ。。デジタル信号処理の入門書がAmazonから届いたので、もし俺が数式等を理解できたならばもしかするとフィルタを実装してみるかもしれないがしないかもしれない。
現在のコード全体はこちら。