/libymfm.wasm

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

Primary LanguageRustBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

libymfm.wasm

This repository is an experimental WebAssembly build of the ymfm Yamaha FM sound cores library.

aaronsgiles / ymfm

BSD-licensed Yamaha FM sound cores (OPM, OPN, OPL, and others)

libymfm.wasm provide high-level and low-level WebAssembly interfaces to ymfm's sound chips and additional sound chips.

The high-level interface provides the vgm/xgm sequencer, while the low-level interface provides direct access to the sound chip. Both can retrieve PCM binary at a given sample rate and number of frames.

The WebAssembly interface can be called from many computer languages by using Wasmer.

Supported Sound Chips

chip from note
YM2149 ymfm
YM2151 ymfm
YM2203 ymfm
YM2413 ymfm
YM2608 ymfm
YM2610/YM2610B ymfm
YM2612 ymfm
YM3526 ymfm
Y8950 ymfm
YM3812 ymfm
YMF262 ymfm
YMF278B ymfm
SN76489 MAME Rust ports
SEGAPCM MAME Rust ports
PWM MAME Rust ports
OKIM6258 MAME Rust ports
C140/C219 MAME Rust ports
OKIM6295 MAME Rust ports

Special Thanks

License

BSD 3-Clause License

Web Browser Interface

WebAssembly VGM Player

Firefox or Chromium or Safari 16 or higher is recommended.

Source code:

https://github.com/h1romas4/libymfm.wasm/tree/main/examples/web

WASI Commnad Line Interface

Options

$ wasmer run libymfm-cli.wasm -- -h
libymfm-cli 0.18.0
Hiromasa Tanaka <h1romas4@gmail.com>
libymfm CLI

USAGE:
    libymfm-cli.wasm [OPTIONS] <filename>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --loop <loop>                 Loop count
    -o, --output <output filepath>    Output file path
    -r, --rate <rate>                 Output sampling rate

ARGS:
    <filename>    Play .vgm/.vzg/.xgm/.xgz file path

Example 1 - Specify output file name

$ wasmer run libymfm-cli.wasm --mapdir /:./docs/vgm -- /ym2612.vgm -o ym2612.pcm
$ ffplay -f f32le -ar 44100 -ac 2 ./docs/vgm/ym2612.pcm

Example 2 - Direct play

$ wasmer run libymfm-cli.wasm --mapdir /:./docs/vgm -- /ym2612.vgm | ffplay -f f32le -ar 44100 -ac 2 -i -

Example 3 - Specify samplig rate

$ wasmer run libymfm-cli.wasm --mapdir /:./docs/vgm -- /ym2612.vgm -r 96000 | ffplay -f f32le -ar 96000 -ac 2 -i -

Source code:

https://github.com/h1romas4/libymfm.wasm/tree/main/examples/libymfm-cli

Python Binding

Install dependencies

cd examples/python
pip install -r requirements.txt

Run examples

# Simple VGM Player
python src/sample_vgmplay.py
# Simple XGM Player
python src/sample_xgmplay.py
# Sound chip direct access example
python src/sample_direct.py
# Pyxel impliments example
python src/sample_pyxel.py

VGM Play Example: sample_vgmplay.py

#
# VGM Play Example
#
import pygame
from wasm.chipstream import ChipStream

# VGM instance index
VGM_INDEX = 0
# Output sampling rate settings
SAMPLING_RATE = 44100
SAMPLING_CHUNK_SIZE = 4096

# Sound device init (signed 16bit)
pygame.mixer.pre_init(frequency=SAMPLING_RATE, size=-16, channels=2, buffer=SAMPLING_CHUNK_SIZE)
pygame.init()

# 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

# PyGame quit
pygame.quit()

# Drop instance
chip_stream.drop_vgm_instance(VGM_INDEX)

Bindings from other computer languages

libymfm.wasm has a super basic extern c Wasm interface.

src/rust/wasm/basic.rs

#[no_mangle]
pub extern "C" fn vgm_create(
    vgm_index_id: u32,
    output_sampling_rate: u32,
    output_sample_chunk_size: u32,
    memory_index_id: u32,
) -> bool {
    let vgmplay = VgmPlay::new(
        SoundSlot::new(
            driver::VGM_TICK_RATE,
            output_sampling_rate,
            output_sample_chunk_size as usize,
        ),
        get_memory_bank()
            .borrow_mut()
            .get(memory_index_id as usize)
            .unwrap(),
    );
    if vgmplay.is_err() {
        return false;
    }
    get_vgm_bank()
        .borrow_mut()
        .insert(vgm_index_id as usize, vgmplay.unwrap());
    true
}

As with the Python binding example, you could easily create an interface. It would also be possible to create a more type-structured interface.

examples/python/src/wasm/chipstream.py

Build

Setup Rust toolchaine

Build require Rust 2021 edition and +nightly.

rustup install nightly
rustup target add wasm32-wasi

Setup wasi-sdk

Setup wasi-sdk-20 - wasi-sdk-20.0-linux.tar.gz requires Ubuntu 22.04.

Setup enviroment values:

.bashrc

export WASI_SDK_PATH=/home/hiromasa/devel/toolchain/wasi-sdk-20.0
export CARGO_TARGET_WASM32_WASI_LINKER=${WASI_SDK_PATH}/bin/lld
export CARGO_TARGET_WASM32_WASI_RUSTFLAGS="-L ${WASI_SDK_PATH}/share/wasi-sysroot/lib/wasm32-wasi"

Verify:

$ echo ${WASI_SDK_PATH}
/home/hiromasa/devel/toolchain/wasi-sdk-20.0
$ ls -alF ${WASI_SDK_PATH}
drwxr-xr-x 2 hiromasa hiromasa 4096 12月  3  2020 bin/
drwxr-xr-x 3 hiromasa hiromasa 4096 12月  3  2020 lib/
drwxr-xr-x 6 hiromasa hiromasa 4096 12月  3  2020 share/
$ ${WASI_SDK_PATH}/bin/clang -v
clang version 16.0.0
Target: wasm32-unknown-wasi
Thread model: posix
InstalledDir: /home/hiromasa/devel/toolchain/wasi-sdk-20.0/bin

Clone source

Require --recursive

git clone --recursive https://github.com/h1romas4/libymfm.wasm
cd libymfm.wasm

Build C/C++ (ymfm)

cmake -DCMAKE_TOOLCHAIN_FILE=./cmake/wasi.cmake -S . -B build
cmake --build build --parallel $(nproc)
ls -laF dist/ | grep libymfm
-rw-rw-r--  1 hiromasa hiromasa 480942  5月 25 13:21 libymfm.a

Build Rust

Web Browser Interface (examples/web)

Install wasm-bindgen require (--version 0.2.78)

cargo install wasm-bindgen-cli --version 0.2.78

Rust build and wasm-bindgen

Always add the +nightly flag. (-Z wasi-exec-model=reactor flag is used, so nightly must be specified)

cargo +nightly build --release --target wasm32-wasi --features bindgen
wasm-bindgen target/wasm32-wasi/release/libymfm.wasm --out-dir ./examples/web/src/wasm/

npm

cd examples/web
npm install
npm run start

Python Binding (examples/python)

Rust build and copy .wasm to Python project

Always add the +nightly flag. (-Z wasi-exec-model=reactor flag is used, so nightly must be specified)

cargo +nightly build --release --target wasm32-wasi
cp -p target/wasm32-wasi/release/libymfm.wasm ./examples/python/src/wasm/

WASI Commnad Line Interface (examples/libymfm-cli)

Building the WASI command-line interface requires disabling WASI reactor mode of the library, so the build requires a patch to the source code.

Pacth Cargo.toml

[lib]
# https://github.com/rust-lang/rust/pull/79997
# https://github.com/bazelbuild/rules_rust/issues/771
# crate-type = ["bin"] # disable this line
crate-type = ["cdylib", "rlib"] # enable this line
path = "src/rust/lib.rs"

Pacth src/rust/lib.rs

// #![no_main] // disable this line

Pacth .cargo/config

[target.wasm32-wasi]
rustflags = [
  "-Ctarget-feature=+bulk-memory",
  # "-Z", "wasi-exec-model=reactor", # disable this line

Build

cd examples/libymfm-cli
cargo +nightly build --target=wasm32-wasi --release

Verify:

ls -laF target/wasm32-wasi/release/*.wasm
-rwxrwxr-x 2 hiromasa hiromasa 2924223  5月 21 14:56 target/wasm32-wasi/release/libymfm-cli.wasm*

Native Debug & Test

Since Rust currently does not allow create-type switching, the following modification to the source code is required for native debugging.

Cargo --crate-type CLI Argument

It is also required if you want to use this library as a simple native library.

Pacth Cargo.toml

[lib]
# https://github.com/rust-lang/rust/pull/79997
# https://github.com/bazelbuild/rules_rust/issues/771
# crate-type = ["bin"] # disable this line
crate-type = ["cdylib", "rlib"] # enable this line
path = "src/rust/lib.rs"

Pacth src/rust/lib.rs

// #![no_main] // disable this line

Buile or test on native

cmake -DCMAKE_TOOLCHAIN_FILE=./cmake/x86-64.cmake -S . -B build
cmake --build build --parallel $(nproc)
ls -laF dist/ | grep libymfm
-rw-rw-r--  1 hiromasa hiromasa 680784  5月 25 13:23 libymfm.a

Native debugging can now be performed.

cargo build --release
cargo test ym2612_1 -- --nocapture

Build Note

WASI Library

Essentially, wasm-bindgen is incompatible with wasm32-wasi.

improve panic message when compiling to wasi #2554

panicked at 'unknown instruction LocalTee

TODO / Known Issues

  • System
    • Fix ROM bus architecture.
    • Add support sound mixers with multi-channel output.
    • Remove the dependency on wasm-bindgen to have only extern "C" interface.
    • Allow the header meta parser to be used independently.
    • Split the sequence parser and player.
  • VGM driver
    • YM2141 clock worng?
    • Is there a problem with the file parser? The beginning of the song may be wrong.
    • Support all data stream (now only support YM2612 and OKIM6285)
    • Support dual chip ROM blocks.
    • Add support parse v1.70 extra header.
    • Respect the sound chip volume value of the extra header.
    • Respect seccond sound chip clock value of the extra header.
    • Implement more of the unimplemented.
  • XGM driver
    • There is still a bug with multi-channel PCM.
  • Multilingual Interface
    • CLI
    • Web/JavaScript
    • Python wasmer-python
    • Add an interface that does not depend on wasm-bindgen
  • ymfm
    • Add direct ymfm intarfece
    • Support yfmf's all sound chips
  • Refactoring
    • Better upsampling
    • Separate the sound stream from the sound driver.
    • Support for arbitrary input tick rate and output sampling rate.
    • Support data stream.
  • Add support sound chip
    • Fix SEGAPCM
    • OKIM6285
    • C140
    • C219
    • OKIM6295
    • RF5C164
    • Next to be determined
  • Examples source
    • Web Frontend: Safari now supports SharedArrayBuffer, but it does not work well. SharedArrayBuffer posted to AudioWorkletProcessor is not actually shared with the main thread
    • Web Frontend: Support YM2608 ADPCM ROM (wasmer-js WASI fopen)
    • Web Frontend: Remove the wasm-bindgen dependency. Provide a TypeScript-based API wrapper.
    • Web Frontend: AudioWorklet
    • Web Frontend: Web Worker AudioWorklet and SharedArrayBuffer (The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers cannot be set in github pages, so they cannot be deployed)
    • Web Frontend: Add buffering mode
    • CLI: Support loop