Rust/WebAssembly でレトロシンセをエミュレートする

以前から WebAssembly を使ってレトロシンセ音源をエミュレートしてブラウザーで発声させてみたいと思っていたのですが、Rust が WebAssembly に直接コンパイルできるようになったのをきっかけに挑戦し、なんとか動かすことができました。

以下からデモを見ることができます。 🙂

WebAssembly 非対応の IE を除く、PC とモバイルのほとんどのブラウザーで動作すると思います。(なお、iOS 11 Safari と Android Chrome はサンプリングレートを無視してしまう処理があるようで高め・速めで再生されています。iOS 12 Safari では修正されたようです。)

https://h1romas4.github.io/rust-synth-emulation/index.html

ソースコードも github にコミットしました。

https://github.com/h1romas4/rust-synth-emulation

PSG(SN76489) VGM player by Rust

さて、初めての Rust だったこともあり製作に結構時間がかかりましたので顛末でも…。

何はなくとも Rust 言語を覚えなければということで、ちょうどオライリーから「プログラミング言語 Rust」が発売されたので購入。

読み進めるも若干飛ばし気味に進む展開に、まだ早かった…と先に公式ドキュメント版プログラミング言語 Rust を読みました。

プログラミング言語Rust: 2nd Edition”の日本語版PDFを作成した

プログラミング言語Rust: 2nd Editionの日本語版PDFを公開しました!

550ページ以上の素晴らしい翻訳と組版で本当に感謝しかありません…。2週間ほどかかりましたが最後まで通して読むことができました(オライリーは少しできるようになってから読んだほうがいいかもしれません)

合わせて、海外のハッカーさんが Rust でライブコーディングしている youtube 動画を見ながらプログラムの組み立て方などを覚えています。こちらも非常に参考になりました。

そんなこんなで半月ほどかけて、C 言語でかかれた SN76498(PSG)エミュレーターを Rust に移植し、PCM サンプリングファイルを出力させることに成功。 🙂

このプログラムを元に WebAssembly 化していきました。

Rust 側での状態の保持

WebAssembly は JavaScript と WebAssembly 間で関数を公開し、互いに呼び出すことができますが、WebAssembly(Rust)側で状態を保持したいことがあります。

今回のプログラムの構成は JavaScript 側で AudioContext イベントを回し、発声バッファが必要になったタイミングで Rust 側で PSG をエミュレーションし 2048 サンプルごとに渡すようなロジックになっていますが、Rust 側では楽曲のどこまで再生したかなどなどを覚えている必要があります。

保持したいデーターをつめた Rust 側の構造体は次のようにしました。

struct VgmPlay { sn76489: SN76489, vgmpos: usize, remain_frame_size: usize, vgmend: bool, buffer: [f32; MAX_BUFFRE_SIZE], vgmdata: [u8; 65536] }

C言語であれば static にしておけば OK ですが、Rust の static は実行前に大きさが決まっていないとコンパイルエラーとなるため、lazy_static! マクロを用いて Mutex 内にこの構造体を保持しています。

lazy_static! { static ref DATA: Mutex<VgmPlay> = Mutex::new(VgmPlay::from()); } 

JavaScript に公開する関数では Mutex をロックした上で中の構造体を取得し、構造体に impl した関数を呼び出します。

#[no_mangle] pub unsafe extern "C" fn play() -> f32 { let vgmplay = &mut DATA.lock().unwrap(); vgmplay.play() as f32 }

ブラウザに実装された JavaScript はシングルスレッドであるため、関数に再入がかかったり同じ構造体を使う別な関数が呼び出されることはありませんが、Mutex につめておくと安心ですね。 このあたりは次の記事が大変参考になりました。

Rocket – A Rust game running on WASM Technically, this isn’t necessary in the case of Javascript, since there will only be one thread. Still, the type system knows nothing about that… Hence the mutex.

感謝。

メモリーの共有

JavaScript/WebAssembly 間でメモリーのポインタを共有をすることができます。今回はサンプリングバッファを割当て、Rust 側で PSG をレンダリングしたメモリーをそのまま JavaScript からアクセスして AudioContext に書き込むことで発声させています。

Rust 側でメモリーの位置を返却。

#[no_mangle] pub unsafe extern "C" fn get_audio_buffer() -> *const f32 { let vgmplay = &mut DATA.lock().unwrap(); &(vgmplay.buffer[0]) }

JavaScript 側で ArrayBuffer としてアクセス。

wasm_audio_buffer = new Float32Array( wasm_memory.buffer, wasm_exports.get_audio_buffer(), SAMPLE_LENGTH); // ... ev.outputBuffer.getChannelData(0).set(wasm_audio_buffer);

また、楽曲データーである .vgm ファイルを http して Rust のメモリーに格納することもしています。

Rust で上記のサンプリングバッファ同様 vgm_data のポインタを関数で公開した上で JavaScript 側から fetch した値を Uint8Array として set。

wasm_vgm_data = new Uint8Array( wasm_memory.buffer, wasm_exports.get_vgm_data(), MAX_VGM_DATA); // load vgm data fetch('./vgm/vgmsample.vgm') .then(response => response.arrayBuffer()) .then(bytes => wasm_vgm_data.set(new Uint8Array(bytes))) .then(results => { // ... });

JavaScript/WebAssembly でメモリーが共有できるため、余分なコピーが発生せず高速に処理することができました。今回は実装していませんが、Rust 側でサンプリングをフーリエ変換してフレームバッファメモリーにビジュアライズして書き込み、canvas に転送なんてこともできると思います。(Web Audio API にも FFT がありますが :D)

Web Audio API

WebAssembly だけの話ってわけでもないのですが、ブラウザの Web Audio API の使い方にちょっと困りました。

Rust 側としてはサンプリングを全て再生したタイミングで次のサンプリングを送り込みたいのですがそれを Rust 側で知るすべがなかったため、JavaScript 側の AudioContext の onaudioprocess イベントを使い(バッファを吐ききると発動する) Rust 側からサンプリングを渡す方式としています。

残念ながら現在 onaudioprocess イベントは JavaScript のメインスレッドを使いきる可能性があるということで非推奨となっており、Audio Workers を使えということのようです。

Audio Workers Audio workers を利用すると web worker のコンテキストで音声処理をおこなえます。Audio Workers は比較的新しいいくつかのインタフェース (2014 年 8 月 29 日に定義)によって定義されているため、これを実装したブラウザはまだありません。実装が完了すると、 ScriptProcessorNode, と JavaScript による音声処理 で述べた機能を置き換えることとなります。

が、まだ実装が進んでいないようです…。 とりあえず今のところ WebAssembly では、JavaScript のタイマーやイベントを契機に処理するという流れが良いかと感じました。

(追記:Audio Worker は仕様から消えて AudioWorklet になったようです)

(2022/8追記 AudioWorklet を使ってつくり直した実装は以下にあります)


というわけで、なんとなく WebAssembly でのプログラムの形がつかめてきました。 Rust も少しだけ分かってきましたのでまた何かつくってみようと思います。

WebAssembly はブラウザベンダー4社でつくってるだけあり、互換性もよく(今回 WebAssembly 的には何のトラブルもなく全てのブラウザで動作しました)、次のステップでは GC やスレッドも入ってくるということなので楽しみな技術です。

ついにブラウザーで好きなプログラムを動作させることができるようになって嬉しいす。 😀

関連

M5Stack/ESP32 でメガドライブ音源をエミュレート

勉強会で見せていただいて気になっていた M5Stack を買ってみました。  🙂

内蔵スピーカーもついていますので hello world がてら、 PSG 音源をエミュレーションしてみようと始めたところ、なかなか鳴ってくれなくて苦労しました…

「エンディングI/SORCERIAN/Copyright© Nihon Falcom Corporation」

追記。FM音源 + YM2612 の DAC もエミュレートできて無事メガドライブ(GENESIS/MEGADRIVE)音源になりました 😀

勢いがあるうちに忘れないようメモしておきます。

ソースは github で公開しています。(力尽きたので不出来、手抜きの部分は大目に見てください)

https://github.com/h1romas4/m5stack-synth-emulation

GENESIS/MEGADRIVE(YM2612+SN76496) VGM player on ESP32/M5Stack

開発環境

PSG エミュレーターを移植するのに gcc ツールチェインを使いたかったので開発環境は(Arduino ではなく) ESP-IDF を使いました。 リンク先の通りに SDK ファイルを展開して、 $IDF_PATH を設定してあげれば OK です。

今回は Linux を使ってやりましたが、Windows 10 の場合は WSL を使うと楽かもしれません。(未確認)

M5Stack ESP-IDF 用の雛形が github に提供されていますので、手順の通り git clone して make menuconfig してシリアルを設定して、make flash monitor すれば hello world できると思います。

https://github.com/m5stack/M5Stack-IDF
To use as a M5Stack component of ESP-IDF

ちなみに monitor の抜け方は ctrl + [ です。

自分がやったときは Makefile にバグがありいきなり、make flash できないという不具合に遭遇しています。

Detected overlap at address: 0xe000 for file:

これは Makefile にパッチする次のワークアラウンドで解消できました。(m5stack リポジトリは既に修正済みだと思います)

(esp-idf) make flash fails due to the overlapping of offset addresses #1724

https://github.com/espressif/arduino-esp32/issues/1724

static heap の上限

static heap をたくさん使おうとすると

region `dram0_0_seg' overflowed by 67900 bytes

とリンクエラーになります。 がびーん。

どうやら static heap にサイズ上限が決められているようなので、static で取得するのをやめて動的に heap_caps_malloc() して回避しました。

build/ 下に app-template.map というファイルができていて、static heap のサイズが分かります。(この場合 DECAY_TO_ATTACK 変数に 0x4000 取られています)

.bss._ZL15DECAY_TO_ATTACK
0x000000003ffbd1cc     0x4000 /home/..../libsynth.a(ym2612.o)

また、make size すると次のように現在のメモリーサイズが表示できます。

Total sizes:
 DRAM .data size:    6488 bytes
 DRAM .bss  size:  118632 bytes
Used static DRAM:  125120 bytes (  55616 available, 69.2% used)
Used static IRAM:   36140 bytes (  94932 available, 27.6% used)
      Flash code:  181978 bytes
    Flash rodata:   64104 bytes
Total image size:~ 288710 bytes (.bin may be padded larger)

static heap を回避しても malloc で失敗することもありますので、あちこち移動させてうまいことメモリーに載せていきます。 なお、heap_caps_get_free_size() で動作中の残りメモリーを知ることができるようです。

内蔵 DAC の鳴らし方

なかなか鳴らなくて PSG エミュレーションが悪いのか DAC の操作が悪いのかで苦労しました…。

I2S 経由の DAC は次のようなイメージで鳴ると思います。

i2s_config_t i2s_config = {
     .mode = static_cast(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
     .sample_rate = SAMPLING_RATE,
     .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
     .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
     .communication_format = static_cast(I2S_COMM_FORMAT_I2S_MSB),
     .intr_alloc_flags = 0,
     .dma_buf_count = 16,
     .dma_buf_len = 512,
     .use_apll = false,
     .tx_desc_auto_clear = true, // esp-idf 3.2 で追加
     .fixed_mclk = 0
};

i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
i2s_set_pin(i2s_num, NULL);

size_t bytes_written = 0;
uint16_t buf[2];
buf[0] = 0x0000; // right sample
buf[1] = 0x0000; // left  sample
i2s_write((i2s_port_t)i2s_num, buf, sizeof(uint16_t) * 2, &bytes_written, portMAX_DELAY);

buf には符号付き符号なしリトルエンディアンのステレオサンプリングを渡します。(内蔵 DAC の場合は符号なしでした… どうにも音が割れると思ったら…)上のサンプルソースは 1フレームですが、この config で 4096 フレーム * 2(ステレオ)が一気に渡せました。

flash への任意データーの書き込み

partitions.csv ファイルをつくり make menuconfig (sdkconfig) で指定してあげることにより、内蔵の flash メモリーの任意の位置に esptool.py でバイナリを書き込みプログラムから read できます。

esptool.py --chip esp32 --port "/dev/ttyUSB0" --baud 115200 write_flash -fs 4MB 0x211000 "バイナリファイル"

プログラムから読み込むときは、

esp_partition_find_first() して esp_partition_mmap() でポインターがもらえます。便利。 🙂

その他

  • 普通に printf すると make flash monitor するだけで実行してコンソール出力されるので嬉しい。
  • プログラムが大きくなったせいか途中から “Make error of dangerous relocation” と怒られてリンクできなくなりました。 CFLAGS に -mlongcalls をつけることで解消しました。
  • M5STACK の内蔵スピーカーはそこそこ音が割れます。 PSG くらいがちょうど良さそうです。
  • 実はがんばって FM音源も移植したのですが、いい音で鳴りませんでした… さすが ESP32 だけあって処理速度は十分です。 ソースに残骸がありますので、気になる方は鳴らしてみてください…)
  • FM音源(YM2612)も鳴らすことができました!
  • YM2612 + SN76489 + YM2612 の内蔵 PCM をエミュレートしたら CPU の 1コアを使い切っているようです。

以前から充電できるポケステがあったらいいのになぁと思っていたのですが、ちょうどでてきてくれた M5Stack さん。 久しぶりに夢中になってしまいました。

今回の hello world で一通りの困りそうなことは踏んだ気がしますので、引き続き何か作っていきたいと思います。 🙂

関連

Arduino で YAMAHA YM2151 を VGM ファイルで演奏させる

1990年代の多くのアーケードゲーム機に搭載されていた音源チップ YAMAHA YM2151 を Arduino からこの手でどうしても発声させてみたい。

電子工作を始めて Lチカもそこそこに製作を始めた”未来ガジェット”のひとつです。

結構前につくったものでしたが、どのように製作したか忘れぬよう github に Arduino スケッチと参考させていただいた資料・回路図のリンクを掲載してみました。

Play back the VGM format file with Arduino. (only YM2151)

https://github.com/h1romas4/arduino-vgmplayer

本スケッチでは VGM Format と呼ばれる、音源チップに流すデーターダンプ形式を、そのまま YM2151 に write して発声させる方式を採っています。

このことから Arduino のスケッチは 100行ほどの単純なものになり、まずは発声させてみたい方にはちょうど良い規模感のプログラムになっています。たとえば PSG を同時に鳴らしたい、なんて時も数行足してもらえれば実装できると思います。

演奏データとなる VGM ファイルは mml2vgm を使わせて頂き MML (Music Macro Language)で作成しました。MML の書き方は体が覚えていることでしょう 😀

https://github.com/kuma4649/mml2vgm

[概要]
このツールは、ユーザーが作成したMMLファイルを元にVGM/XGMファイルを作成します。

[機能、特徴]
[VGM]
・メガドライブ2台分の音源構成(YM2612 + SN76489 + RF5C164)*2にそったVGMを生成します。
(他にYM2151,YM2203,YM2608,YM2610B,SegaPCM,HuC6280に対応しています。)

github 上のサンプルは次のようにしてみました。(カエルの歌の3声輪唱です)

assets/vgmsaple.gwi

'@ M 110
   AR  DR  SR  RR  SL  TL  KS  ML  DT1 DT2 AME
'@ 022,005,000,004,005,041,000,001,005,000,000
'@ 016,008,008,008,002,000,001,002,005,000,000
'@ 031,018,000,004,010,044,000,008,009,000,000
'@ 031,009,007,008,002,003,001,001,009,000,000
   ALG FB
'@ 004,007

'X6 T160@110l4 r1r1r1r1 cdefedcrefgagfercrcrcrcrc8c8d8d8e8e8f8f8edc
'X7 T160@110l4 r1r1     cdefedcrefgagfercrcrcrcrc8c8d8d8e8e8f8f8edc
'X8 T160@110l4          cdefedcrefgagfercrcrcrcrc8c8d8d8e8e8f8f8edc

このテキストファイルを mml2vgm により .vgm 化して、Arduino のスケッチから PROGMEM で Flash メモリーに書き込んでいます。(music/vgmsample.h

曲の大きさによっては Arduino の 32KByte Flash に載りきらないため、作例では EEPROM を接続し書き込んだデーターを演奏させることもしています。(ちなみに EEPROM を単純に 1Byte 読みすると速度が追いつかなかったため、先読みのリングバッファのような実装を入れました)

ハードの方は Arduino UNO のシールドで実装しています。(まずはブレッドボードで動作を確認した方が良いと思います)

足の数に対して基板が結構小さく、おまけに全部ビニール線で繋いだため結構大変でした。。とはいえ、初心者の自分が、2日くらいハンダ付けをがんばれば完成しましたので、おそらくみなさまいけるかと思います。(部品は AliExpress で入手しています)

さて完成した機械から奏でられた音色は…

[tegaki]リーディングシュタイナー発動…!!![/tegaki]

苦労した甲斐もありまして、無事タイムリープに成功。 😀

子供の頃からゲームの音楽と言えば FM 音源で、なんとかいい音で聴こうと PC からラインを引っ張り出してステレオコンポに接続して、何度も何度も聴いていたことを思い出します。

この機械は、電源を入れればすぐ楽曲が奏でられますので「古の電子オルゴール」として現在も活躍中です。

2019/4 追記。 github 上のサンプルデーターをパッヘルベル カノンに差し替えました!

関連