Coder Social home page Coder Social logo

h1romas4 / libymfm.wasm Goto Github PK

View Code? Open in Web Editor NEW
53.0 5.0 8.0 19.95 MB

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

Home Page: https://chipstream.netlify.app/

License: BSD 3-Clause "New" or "Revised" License

CMake 1.73% C++ 4.68% Rust 93.16% HTML 0.43%
ym2612 ym2151 ym2203 ym2149 vgm vgmplay webassembly wasi wasm wasm32-wasi wasi-sdk wasmer audioworklet webworker rust xgm

libymfm.wasm's Introduction

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 <[email protected]>
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

libymfm.wasm's People

Contributors

h1romas4 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

libymfm.wasm's Issues

Question on the FFI wrapper

Hi Hiromasa,

I'm trying to use your FFI wrapper to simply replay a YM registers dump file (captured every 50Hz) but the result is not good at all. I guess I don't use your wrapper correctly, can you tell me what is wrong ? I guess this is due to the frequency I call ymfm_generate but I don't undertstand why.

Here is the principle (in Zig but not really different from Rust):

(if not clear in the comments)

  1. I call ymfm_add_chip to set up a YM2149 at 2000000 Hz and get the sampling rate: 31250
  2. I loop over the blocks of 14 YM2149 registers contained in my YM dump file
    • I write the 14 registers using ymfm_write
    • I generate 31250/50 ticks with ymfm_generate
      • I write the first i32 of the buffer (mono channel) in a file for each tick
  3. Done!
pub fn main() !void {

    // YM2149 clock: 2MHz
    const YM2149_clock: u32 = 2e6;
    const fYM2149_clock: f32 = @intToFloat(f32, YM2149_clock);

    // Get YM2149 enum value, basically 0
    const ym2149: u16 = @enumToInt(ChipType.CHIP_YM2149);

    // create wav file
    const file = try std.fs.cwd().createFile(
        "sound.wav",
        .{ .read = true },
    );
    defer file.close();

    // Get a buffered writer to write in this file
    var buf_writer = std.io.bufferedWriter(file.writer());
    const writer = buf_writer.writer();    

    // add a YM2149, YM2149_clock == 
    var sampling_rate: u32 = ymfm.ymfm_add_chip(ym2149, YM2149_clock);

    std.debug.print("Sampling Rate: {} for master clock: {}\n", .{ sampling_rate, YM2149_clock });
    std.debug.print("Dump length: {} frames = {} seconds\n", .{ dump_b.len / 14, dump_b.len / 14 / 50 });
    std.debug.print("Tick per frame: {}\n", .{ sampling_rate / 50 });

    // counters for the YM dump file
    var counter: u64 = 0;
    var dump_loop: u32 = 0;

    // the YM dump file is composed of 50Hz frames capturing the first 14 YM2149 registers
    // loop on those frames
    while(dump_loop < dump_b.len / 14) : (dump_loop += 1) {

        // write the 14 registers
        var i: u32 = 0;
        while( i < 14) : ( i += 1) {
            ymfm.ymfm_write(ym2149, 0, i, dump_b[counter]);  
            counter += 1;
        }     

        // generate some sound for 50 Hz, so tick the YM (sampling_rate / 50) times
        // write every sample output to the file
        var tick: u32 = 0;
        var buffer: [2]i32 = undefined;
        while (tick < sampling_rate / 50) : ( tick += 1) {
            ymfm.ymfm_generate(ym2149, 0, &buffer);

            // store only left channel
            const slice = buffer[0];            
            try writer.writeAll(std.mem.asBytes(&slice));       
        }
                
        // std.debug.print("Frame {} done\n", .{ dump_loop } );
        
    }
    try buf_writer.flush();
}
Sampling Rate: 31250 for master clock: 2000000
Dump length: 8833 frames = 176 seconds
Tick per frame: 625

Any idea what I did wrong ?

More consistent location for the c++ library (native build) + another readme suggestion

I've found that I have to set build.rs to this if I want to use your library as a library in my project, because the CWD is actually set to the workspace root of my package for some reason:

let dir = env::var("CARGO_MANIFEST_DIR").unwrap();
println!("cargo:rustc-link-search=native={}/dist", dir);

Also the cmake build instructions could have been simpler:

cmake -DCMAKE_TOOLCHAIN_FILE=./cmake/x86-64.cmake -S . -B build
cmake --build build --parallel $(nproc)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.