AssemblyScript で WebAssembly を知る

毎年恒例 ゆるWeb勉強会@札幌 Advent Calendar 2022 への投稿です…!

札幌で開催しているWeb系勉強会、「ゆるWeb勉強会@札幌」の2022年アドベントカレンダーです。

過去発表内容のまとめ、発表する機会の無かったネタ、勉強会に参加したいけどできなかったので、などなど、Webに関するキーワードを中心としつつ自由に書いていってください!

毎年 WebAssembly 関連を投稿させていただいておりますが、今年は WebAssembly が理解の手助けとなるような、入門記事を書いてみたいと思います。

AssemblyScript 言語

最初に WebAssembly のおおよそですが、 プログラムソースコードをビルドすることにより Wasm バイナリー(.wasm)を出力し、このファイルを WebAssembly ランタイムに読ませることでプログラムを動作させる技術です。

この記事で取り上げる AssemblyScript 言語は、TypeScript Like なソースコードを Wasm バイナリーにビルドできるコンパイラーです。npm で簡単にツールチェインが導入でき、また WebAssembly 専用の言語となっているため、端的に理解しやすく、入門にも最適だと感じます。

The AssemblyScript Book

AssemblyScript compiles a variant of TypeScript (opens new window) (a typed superset of JavaScript) to WebAssembly (opens new window) using Binaryen (opens new window), looking like:

WebAssembly ランタイムを持つ主要ユーザのひとつはウェブブラウザーで、モバイルを含む主要 4 ウェブブラウザーで .wasm を実行可能です。また、ウェブブラウザーから JavaScript ランタイムが node.js として取り出されたように、ウェブブラウザー外で .wasm を動かす動きも近年活発です。

なお、.wasm のビルドに対応しているプログラム言語は、大きく 2種類のパターンがあり、直接 .wasm にビルドするものと、言語のインタープリタ(ランタイム)を .wasm にして、ソースコード(もしくは中間コード)をそのまま与えて動作するものに分けられます。

前者の代表は、Rust、C/C++、TinyGo などなど、そして AssemblyScript もこの仲間です。

後者は、Python(Pyodide)、Ruby、.NET/C# などが該当します。つまり、これらのインタープリタやランタイムが C/C++ であるということに立脚すると、前者の方式で Wasm ビルドできればスクリプトファイルが実行できる、というイメージです。

というわけで、本記事では WebAssembly 自身の理解の手助けとなるよう、直接プログラムが .wasm となり、ランタイムとのインターフェースもシンプルで分かりやすい AssemblyScript を使って解説を進めていきます。

サンプルプログラム

AssemblyScript/WebAssembly を使って時計を描画するプログラムをつくってみました。

次のような要素が含まれています。

  • WebAssembly から直接画面描画をすることができないので、WebAssembly ランタイムから import した、指定した位置に「点」を描画する関数を呼び出し時計を描いている。
  • 上記のような特性を逆に利用すると、ランタイム環境に依存する処理が差し替えられるため、同一 WebAssembly プログラムを、ウェブブラウザーとマイコン(M5Stack) で動作させるマルチプラットフォームの例ともなっています。

ソースコード:

https://github.com/h1romas4/m5stack-core2-wasm3-as

M5Stack Core2 With Wasm3/AssemblyScript Demo

WebAssembly プログラムとランタイムのインターフェース

WebAssembly の上のプログラムはよく「計算しかできない」と言われます。これは Wasm がサンドボックスで動作し、その外側のリソースにアクセスできないためです。

外界との接続との唯一のインターフェースは、それぞれの関数の export/import と、メモリーのポインターです。

JavaScript からみた、.wasm は引数の型が不自由な関数が追加されているように見えます。

:ランタイムに関数を公開する AssemblyScript のソースコード:

– 引数の型が u32 という見慣れない型になっている。

export function clock(x: u32, y: u32, r: u32): void {
    analogClock = new AnalogClock(x, y, r);
}

export function tick(): void {
    if(analogClock != null) {
        analogClock.tick();
    }
}

:export function が入った AssemblyScript を .wasm にビルドしたアセンブリーが出力(抜粋):

– 引数が i32 という WebAssembly の型になっている。

  (export "clock" (func 16))
  (export "tick" (func 28))

  (func (;16;) (type 1) (param i32 i32 i32)
  (func (;28;) (type 2)

そしてこの関数は JavaScript から次のように呼び出すことができます。

.wasm をロードする JavaScript のソースコード:

instance.exports.wasm に定義された関数を取得できる。

async function loadWasm() {
    const response = await fetch(new URL('../dist/app.wasm', import.meta.url));
    const responseArrayBuffer = new Uint8Array(await response.arrayBuffer());
    const wasm_bytes = new Uint8Array(responseArrayBuffer).buffer;
    let module = await WebAssembly.compile(wasm_bytes);
    const instance = await WebAssembly.instantiate(module, {
        ...createImports()
    });
    wasmExports = instance.exports;
};

.wasm でエクスポートされた関数を JavaScript からコールするソースコード:

instance.exports にエクスポートされた関数の引数に number 型を渡している。

(async function() {
    await loadWasm();
    wasmExports.clock(160, 120, 120);
    setInterval(() => {
        wasmExports.tick();
        wasmExports.__collect() // clean up all garbage
    }, 500);
})();

ポイントは .wasm 関数の引数には i32, i64, f32, f64 といった WebAssembly で使える(数値)型しかとれないことです。 JavaScript のオブジェクトのようなものや、文字列、もちろん DOM などの WebIDL の型も(現在のところ)そのまま渡して処理はできません。

AssemblyScript inherits WebAssembly’s more specific integer, floating point and reference types:

逆もしかりで、.wasm 側から JavaScript の関数を呼び出す際もこれらの引数型しかとることができません。

:JavaScript の関数を import する AssemblyScript のコード:

export declare function draw_pixel(x: i32, y: i32, color: i32): void;

export declare function をビルドした .wasm のアセンブリー(抜粋):

  (type (;1;) (func (param i32 i32 i32)))
  (import "c3dev" "draw_pixel" (func (;0;) (type 1)))

:AssemblyScript に関数を公開する JavaScript のコード:

async function loadWasm() {
    // ..snip...
    const instance = await WebAssembly.instantiate(module, {
        ...createImports()
    });
    // ..snip...
};

function createImports() {
    let imports = [];
    // ..snip...
    imports['c3dev'] = {
        'draw_pixel': (x, y, color) => {
            canvasContext.fillStyle = convertRGB565toStyle(color);
            canvasContext.fillRect(x, y, 1, 1);
        },
    // ..snip...
}

.wasm 側で import した i32 という数値型 を持つ draw_pixel 関数を呼び出し、最終的に ウェブブラウザーの Canvas に指定座標にひとつ点を描いています。マイコン動作ではこの部分が LCD に点をひとつ描く C の関数が呼び出すようにしています。

(ちなみに、点が描ければ円も線も計算で描けるということで、サンプルプログラムの時計は AssemblyScript の演算処理により全て点だけで描画を行っています)

インターフェースをつくる

以上のように、WebAssembly でプログラムをかく場合は、WebAssembly ランタイムをホストしている環境との連携インターフェースの設計が肝で、特性を考慮して、どのような処理単位にすると効率的に処理できるのかを考えていきます。

また、今回の例では用いていませんが、メモリーのポインター値を共有することで、VRAM や音声波形など大きな演算結果(配列)をインターフェースすることもでき、Wasm 内の演算結果を外部に持ち出すことができます。

このようなホストへのインターフェースの標準化の実装のひとつとして WASI があり、システム時間や乱数シード、ファイルシステムやネットワークへのアクセスが規定されています。(ただしウェブブラウザーには WASI API は実装されていません)

WASI も WebAssembly 型の関数が沢山あるだけですので、覗いてみると理解しやすいかと思います。

また、Rust の wasm-pack(wasm-bindgen) や Emscripten(C/C++) など他の WebAssembly ツールチェインは、Wasm 型との引数合わせやラップ、JavaScript の呼び出しをソースコードジェネレートなどを、自動でやってくれるものとして捉えると考えやすいです。

WebAssembly のインターフェースを知ることで周辺技術が何をしているのかが見えてきそうだな、ということで本記事は書かれました。

処理を速くしたい部分や、JavaScript 外で便利なライブラリー、、圧縮展開、画像動画音声処理、言語解析器などなどを見つけたら、「WebAssembly(型) なインターフェース」を追加して呼び出す。まずは WebAssembly の実用的なユースケースのひとつと思いますので、ぜひお試しください。

関連

https://twitter.com/h1romas4/status/1612348626248044544

WebAssembly で動作する FM 音源ライブラリー libymfm.wasm

去年くらいからつくりはじめていた、libymfm.wasm ですが、GitHub のリポジトリーにコミットするだけで、ブログにあまりあれこれ書いていませんでした…!(ので書いてみます)

libymfm.wasm は WebAssembly 上で動作する(主に) FM 音源シンセサイザーをエミュレートして PCM を生成するライブラリーです。ゲームなどのプログラムへの組み込みを考えて作成されました。

https://github.com/h1romas4/libymfm.wasm

This repository is an experimental WebAssembly build of the [ymfm](https://github.com/aaronsgiles/ymfm) Yamaha FM sound cores library.

FM 音源エミュレータコアとしては、多目的エミュレーションフレームワーク MAME の新 YAMAHA FM 音源エミュレーションコアを由来とする C++ でかかれた ymfm を使わせて頂いています。

ymfm の作者は MAME の中の人のアーロンさんで、元々 MAME 内での実装だった新 YAMAHA FM 音源コアを 3rdparty ライブラリー化したのが ymfm となります。

従来の MAME の FM 音源エミュレーションコアは YM2151 や YM2203、YM2612 などなど別チップは別エミュレーションの実装となっていましたが、機能差分以外は同じ回路が使われているのではないかという仮説から、 decap 解析などとの比較を経て完成したライブラリーです。ということで、ymfm は多くの YM 系のチップをそのひとつでサポートしています。

さて、表題の libymfm.wasm ですが、ymfm を使いながら、Rust でかかれた次のような実装を加えています。

VGM/XGM 形式のシーケンサーを搭載

VGM/XGM 形式の演奏形式をサポートしています。ファイルを渡すだけで発音可能です。

VGM はサポートしているサウンドチップのみ、またデーターブロックの圧縮が未サポートなどフル実装ではありません。XGM は PCM に不具合ありで修正予定です。 (多くのデータでテストはされていませんが修正済み)

サンプリングレートコンバート

各サウンドチップが出力するネイティブサンプリングレートはまちまちなので(YM2151 が 3.58MHz 動作で 55.9kHz 等々特殊です)、扱いやすいように指定したサンプリングレートにアップ、ダウンサンプリングで統一して PCM 出力します。

クロックの制御

ライブラリーに、出力サンプリングレートに対して何サンプル分の PCM が欲しいのか(時間をどれくらい進めるのか)を指定できます。これはゲーム組み込み用などで、1フレーム分のサンプルが欲しいケースや、バッファリング再生したい時に便利です。

WebAssembly 向けのインターフェース関数

ハイレベルインターフェースとして vgmplay xgmplay 関数、ローレベルインターフェースとして各サウンドチップに直接レジスターライトして、結果を任意のサンプリングレートとフレーム数で PCM 取得できます。

追加のサウンドチップ

ymfm サポート以外のサウンドチップも MAME からの移植でいくつか実装。主にメガドライブ、セガアーケード、X68K 構成を想定したチョイスです。

ライセンス

ymfm や追加音源、libymfm.wasm 全て BSD ライセンスになっています。


WebAssembly ライブラリー形式となっていますので、同一 .wasm シングルバイナリー(3MB 程度です)で Wasmer などの各言語向け WebAssembly バインディングを使うことで、ほとんどの OS、コンピュータ言語からコールして容易に使うことができます。 .wasm ファイルひとつで全環境動くので扱いやすいです。もちろんウェブブラウザーでも…!

リポジトリには Python からコールする例を入れています。リンク先の手順ですぐ発音すると思いますので、良ければ遊んでみてください〜。

VGM/XGM 再生

関数に VGM/XGM ファイルを与えると指定したチャンクサイズで PCM を取得できます。

sample_vgmplay.py 抜粋

# Output sampling rate settings
SAMPLING_RATE = 44100
SAMPLING_CHUNK_SIZE = 4096

# ...snip...

# Create Wasm instance
chip_stream = ChipStream()

# Setup VGM
header, gd3 = chip_stream.create_vgm_instance(VGM_INDEX, "./vgm/ym2612.vgm", SAMPLING_RATE, SAMPLING_CHUNK_SIZE)
# Print VGM meta
print(header)
print(gd3)

# Play
while chip_stream.vgm_play(VGM_INDEX) == 0:
    # Get sampling referance
    s16le = chip_stream.vgm_get_sampling_ref(VGM_INDEX)
    # Sounds
    sample = pygame.mixer.Sound(buffer=s16le)
    pygame.mixer.Sound.play(sample)
    # Wait pygame mixer
    while pygame.mixer.get_busy() == True:
        pass

サウンドチップダイレクトコール

以下の例は あぶり6800 さんの Z80 MSX サウンドドライバー(1/60 tick) の Python によるシミュレートで、YM2149 の SSG を発音させています。

ソースコードでは YM2149 ひとつを扱っていますが、サウンドスロットにぽこぽこ複数の音源を追加してレジスターライトして PCM を取得できるイメージです。

前述の vgmplay(xgmplay) で使っているインターフェースは全て Wasm 側にも公開しているので、PCM ROM がある音源の操作も含め、原理的には自前で vgmplay をかけるスペックになっています。

sample_direct.py 抜粋

# Create Wasm instance
chip_stream = ChipStream()

# Setup sound slot
chip_stream.sound_slot_create(SOUND_SLOT_INDEX, SOUND_DRIVER_TICK_RATE, SAMPLING_RATE, SAMPLING_CHUNK_SIZE)

# Add "one" YM2149 sound chip in sound slot
chip_stream.sound_slot_add_sound_device(SOUND_SLOT_INDEX, SoundChipType.YM2149, 1, YM2149_CLOCK)

# YM2149 initialize (write reg: 0x7, data: 0b10111000)
mixing = 0b10111000
chip_stream.sound_slot_write(SOUND_SLOT_INDEX, SoundChipType.YM2149, 0, 0x7, mixing)

# ...snip...

# Write YM2149
chip_stream.sound_slot_write(SOUND_SLOT_INDEX, SoundChipType.YM2149, 0, track + 0x8, volume)

さて、libymfm.wasm のプログラム部分についてですが、Rust でかいており、C/C++ の ymfm を wasi-sdk でビルドした上で、Rust の wasm32-wasi とリンクする形でビルドしています。

このことから、.wasm は WASI の形になっています。(とはいえ、現在のところは WASI の機能はほとんど使っていません。一部、YM2608 の ROM ファイル読み込みで機能しています)

ウェブブラウザーインターフェースでは、WASI をブラウザー上で動作させるため、wasmer-js を使い、WASI バインディング関数をシミュレートして動作させています。

ウェブブラウザーインターフェースは次のリンクから試すことができ、演奏ファイルのドラッグアンドドロップ(複数可能)で楽曲が再生されるはずです。(画面クリックでもデモ曲が演奏されます)

https://chipstream.netlify.app

libymfm.wasm とは直接関係ありませんが、ウェブブラウザーの JavaScript の実装的には AudioWorklet と Woker と SharedArrayBuffer を使った音声出力の実装になっています。

iOS/macOS の Safari は現時点で SharedArrayBuffer に対応しているものの、どうも SharedArrayBuffer の notify がうまく飛ばないようで発音しません。どうやら AudioWorklet に渡された際に共有ではなくコピーになるようです。Safari 16 で修正されそうです(SharedArrayBuffer posted to AudioWorkletProcessor is not actually shared with the main thread

(2022/8 追記。Safari Technology Preview 149 で修正されているのを確認しました)

Windows/macOS/Linux の各ブラウザーで音切れしないように確認しながらつくっていますが、まだだめな環境があるかもです。。結構苦戦しました。。ちなみに手元の環境では、Linux の Firefox が一番素直に動作するようです。

てなわけで、みなさまのアプリに組み込みが容易になっている libymfm.wasm の紹介でした。新作ゲームに FM 音源ミュージックのリアルタイム再生など、いかがでしょうか…!

JavaScript、Python のサンプル実装含めてソースコードは、GitHub にコミットしてありますので、良ければ見てみてください。

https://github.com/h1romas4/libymfm.wasm

This repository is an experimental WebAssembly build of the [ymfm](https://github.com/aaronsgiles/ymfm) Yamaha FM sound cores library.

時折あるアップデート通知は https://twitter.com/h1romas4 のツイッターにて〜 😀

関連

MSX ゲーム開発 2022年

1980年代から90年にかけて家庭に広く普及した MSX パソコン(当時はマイコンと言ってましたね!)にて、ゲームをつくる活動を 2022年に復活してみました…!

当時の開発は MSX-BASIC もしくは Z80 アセンブラをつかったものでしたが、今回は さまざまな 8bit Z80 系のレトロコンピュータに対応する Z88DK ツールチェインによる C 言語を使っています。

Home: z88dk

z88dk is the only C and assembler development kit that comes ready out-of-the-box to create programs for over 100 z80-family machines.

Z88DK のセットアップやサンプルコードについては、以下の文書にまとめています。C 言語ですが、BASIC より簡単かも、、ですので良ければ遊んでみてください。この記事で紹介しているゲームのソースコードへのリンクもつけています。

Z88DK を使って MSX のゲームをつくるための環境構築メモ

この文書は、Z80 を CPU に持つコンピュータ向けの C コンパイラ・アセンブラツールチェーンである Z88DK を使って MSX のゲームをつくるための環境構築メモです。

てなわけで、この Z88DK を使いまして、新作ゲーム(?) をふたつつくってみましたので紹介したいと思います。ゲームはウェブブラウザーで動作する MSX エミュレータから楽しめますので合わせてリンクをしています。

PONPON for MSX

80年代のコンピュータ誌に投稿された PONPON という名前の投稿プログラムの MSX クローンです。自分はプログラムポシェット誌でみて打ち込んで楽しんだ覚えがあります。

自動的に上下に移動する主人公「◯氏」を赤ブロックに激突しないように左右に操作して $ を取得して点数を競うゲームです。

次のリンクからウェブブラウザで遊べます。

WebMSX で PONPON を遊んでみる…!

記憶だけを頼りにつくっていますが、この MSX 版はプログラムポシェット掲載 PC88 PONPON の “改造版” の移植です。オリジナルは 40 * 25 桁(WIDTH 40,25) でもっと綺麗に壁や赤ブロックが並んでいたと思います。

当時小学生だった自分はごちゃごちゃ改造していて、確かこのような感じになった気がします、、懐かしいです。。

NOBORUNOCA for MSX

MSX の VDP はいわゆるスクロール機能を持たず、スクロールをしたい場合は通常 VRAM ブロック転送による PSG(8ドット) 単位スクロールとなりますが、これを PCG キャラクターをドットずらしで用意することでスムーズスクロールを実装する技がありました。

当時の自分はスムーズスクロールしてみたくても、プログラミング技術もあまりなく実装を諦めており、これまで数十年間心の何処かにひっかかっていた課題のひとつだったのですが、2022年になってようやく実装することができました。

というわけで、構想数十年、製作 5日の NOBORUNOCA です。

強制縦スクロールのワンキーためジャンプアクションゲームです。使うのは SPACE キーのみ…!のぼるのか…のぼらないのか…

次のリンクからウェブブラウザで遊べます。

WebMSX で NOBORUNOCA を遊んでみる…!

操作はシンプル、割と奥が深いを目指してつくってみました。ゲーム特有の落ち着けばなんとかなる…!がうまくつくれたと思いますので、良ければ遊んでみてください…!(ツイッター #NOBORUNOCA タグでみなさまのハイスコアを拝見しております… 😀

裏技的な攻略情報がいくつかあるので書いておきます。

  • 足場生成に関わるゲームの乱数シードはタイトル画面のパワーゲージ値によります。
  • レベルエクステンド時に、必ず穴がない休憩足場が生成されます。
  • ジャンプ上昇中、ジャンプ落下、歩き落下中もパワーゲージがチャージできます。
  • 落下中から、着地前直前十数フレームで、その場再ジャンプが可能な猶予フレームがあります。これは足場がない最下段でも適用可能なので、目押しスペース離しで復活できることがあります。また、成立するとボーナス点が入ってます。
  • ジャンプ落下中は足場判定が横に広がっています。このため左右の縦壁でも再ジャンプ可能なパターンがあります。

いろいろ実装していましたら、当時の BASIC ゲームも面白くなるようにいろいろ調整して楽しんでいたことを思い出してノスタルジー。

ゲームミュージック

マイコンゲームに音楽をつけるのも当時の課題のひとつで、サウンドドライバー問題とシーケンサーどうする問題がありましたが、今回 @aburi6800 さん の MSX Z80 サウンドドライバーと、いちまるまるゲームズさんの、Lovely Composer & あぶり6800さんコンバータにより、見事、課題解決することができました…!

ありがとうございました…!

Lovely Composer (ラブリーコンポーザ)

家庭用ゲーム機の作曲ソフトの方向性を受け継いだ、かわいい作曲ツール!
レトロゲームのようなピコピコサウンドの音楽や効果音を、楽しく手軽に作れます。

https://github.com/aburi6800/msx-PSGSoundDriver

MSX用のPSGサウンドドライバです。
z88dkのz80asmでコンパイルできる形にしています。

楽曲は YAMAHA MODX シンセで曲のスケッチをかいて、Lovely Composer に打ち込む形で楽曲をつくり、lc2asm コンバータでゲームに取り込む手法でつくっています。

今回は MSX 向けで矩形波 2/3ch だけの打ち込みとしていますが、Lovely Composer は矩形波以外も波形メモリ音源的な音などなどピコピコサウンドを手軽なプリセットと操作系で使え楽しいです。:D

MSX ゲーム取り込み前には、YM2149 での鳴り具合を確認するために、自分が以前からつくっていました WebAssembly の ymfm エミュレータを Python からコールして、サウンドドライバー互換で発音させるスクリプトも利用しています。

https://github.com/h1romas4/noborunoca/tree/main/tools/lcconv

楽曲のほうですが、自分は音楽の方が楽器は持っているもののほとんど素人ですが、、レトロっぽいコミカルさが出るように、少ない小節数にすると心に決め、メロディーのリズムに気をつけてかきました。

密かにちょっと気に入っていますので良ければ聴いてみてください…!

最後に

残念ながら手元に MSX 実機がないためまだ実機動作を自分で見れていないのですが、Simple ROM Cartridge を使わせていただいて、いつか動作させたい…!夢の ROM 版ゲーム…

MSX用カートリッジ64K Simple ROM Cartridge

ちなみに PONPON は 16KB で NOBORUNCA はほんの少しだけはみ出て 32KB ROM になっています。

書き込み準備ヨシ…!(ちなみに写っている PSP 版は fMSX エミュレータによる動作です)

ゲームのソースコード

両ゲームとも GitHub Actions で ROM のビルドができるように仕込んでいます。また、リリースページに .rom ファイルを置いています。

PONPON

https://github.com/h1romas4/z88dk-msx-template

NOBORUNOCA

https://github.com/h1romas4/noborunoca


関連