#ym2149 #browser #chiptune

ym2149-wasm

WebAssembly bindings for YM2149 PSG emulator - play YM files in the browser

5 unstable releases

Uses new Rust 2024

0.8.0 Dec 8, 2025
0.7.1 Dec 4, 2025
0.7.0 Dec 3, 2025
0.6.1 Nov 21, 2025
0.6.0 Nov 19, 2025

#777 in WebAssembly

MIT license

1.5MB
16K SLoC

Contains (Zip file, 7KB) examples/JustAddCream.aks, (Zip file, 4KB) examples/LopEars.aks

ym2149-wasm

WebAssembly bindings for the YM2149 PSG emulator - play YM chiptunes, Arkos Tracker projects, and Project AY rips directly in your browser!

Features

  • 🎵 Play YM2–YM6, Arkos Tracker .aks, and ZXAY/EMUL .ay files in the browser
  • 🎮 Full playback control (play, pause, stop, seek)
  • 🔊 Volume control and channel muting
  • 📊 Real-time waveform data for visualizations
  • 📝 Metadata extraction (title, author, comments)
  • ⚡ High-performance cycle-accurate emulation
  • 🎨 Web Audio API integration

Installation

Build from Source

# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# Build the WASM module
cd crates/ym2149-wasm
wasm-pack build --target web --out-dir pkg

# Or for bundler (webpack, rollup, etc.)
wasm-pack build --target bundler --out-dir pkg

# Shortcut: rebuild + copy into examples/pkg
./scripts/build-wasm-examples.sh --release

Quick Start

Basic Usage

import init, { Ym2149Player } from './ym2149_wasm.js';

async function playYmFile(fileData) {
    // Initialize WASM module
    await init();

    // Create player from YM/AKS/AY file data
    const player = new Ym2149Player(fileData);

    // Get metadata
    const metadata = player.metadata;
    console.log(`Playing: ${metadata.title} by ${metadata.author}`);
    console.log(`Duration: ${metadata.duration_seconds}s`);

    // Start playback
    player.play();

    // Generate audio samples for Web Audio API
    const sampleRate = 44100;
    const samplesPerFrame = 882; // At 50Hz frame rate
    const samples = player.generateSamples(samplesPerFrame);

    // Use samples with Web Audio API (see examples below)
}

Web Audio API Integration

import init, { Ym2149Player } from './ym2149_wasm.js';

class YmWebPlayer {
    constructor() {
        this.audioContext = null;
        this.player = null;
        this.isPlaying = false;
    }

    async init() {
        await init();
        this.audioContext = new AudioContext({ sampleRate: 44100 });
    }

    async loadFile(fileData) {
        this.player = new Ym2149Player(fileData);
        console.log('Loaded:', this.player.metadata.title);
    }

    play() {
        if (!this.player || this.isPlaying) return;

        this.isPlaying = true;
        this.player.play();
        this.scheduleNextBuffer();
    }

    pause() {
        this.isPlaying = false;
        if (this.player) this.player.pause();
    }

    scheduleNextBuffer() {
        if (!this.isPlaying) return;

        const samplesPerFrame = 882; // 44.1kHz at 50Hz
        const samples = this.player.generateSamples(samplesPerFrame);

        // Create AudioBuffer
        const buffer = this.audioContext.createBuffer(
            1, // mono
            samples.length,
            this.audioContext.sampleRate
        );

        // Fill buffer
        buffer.getChannelData(0).set(samples);

        // Create and schedule source
        const source = this.audioContext.createBufferSource();
        source.buffer = buffer;
        source.connect(this.audioContext.destination);
        source.start();

        // Schedule next buffer
        setTimeout(() => this.scheduleNextBuffer(), 20); // 50Hz = 20ms
    }
}

// Usage
const player = new YmWebPlayer();
await player.init();

// Load file from user input
const input = document.getElementById('file-input');
input.addEventListener('change', async (e) => {
    const file = e.target.files[0];
    const arrayBuffer = await file.arrayBuffer();
    const uint8Array = new Uint8Array(arrayBuffer);
    await player.loadFile(uint8Array);
    player.play();
});

Playback Control

// Play/Pause
if (player.is_playing()) {
    player.pause();
} else {
    player.play();
}

// Volume control (0.0 to 1.0)
player.set_volume(0.5);

// Seek to position
player.seek_to_percentage(0.5); // Seek to 50%
player.seek_to_frame(1000);     // Seek to frame 1000

// Channel muting (for karaoke-style playback)
player.set_channel_mute(0, true);  // Mute channel A
player.set_channel_mute(1, false); // Unmute channel B
player.set_channel_mute(2, false); // Unmute channel C

// Get playback position
console.log(`Position: ${player.position_percentage() * 100}%`);
console.log(`Frame: ${player.frame_position()} / ${player.frame_count()}`);

Metadata Access

const metadata = player.metadata;

console.log(`Title: ${metadata.title}`);
console.log(`Author: ${metadata.author}`);
console.log(`Comments: ${metadata.comments}`);
console.log(`Format: ${metadata.format}`);
console.log(`Frames: ${metadata.frame_count}`);
console.log(`Frame Rate: ${metadata.frame_rate} Hz`);
console.log(`Duration: ${metadata.duration_seconds} seconds`);

Visualization

// Get current register values for visualization
const registers = player.get_registers(); // Returns Uint8Array[16]

// Register layout:
// R0-R1:   Channel A period
// R2-R3:   Channel B period
// R4-R5:   Channel C period
// R6:      Noise period
// R7:      Mixer control
// R8-R10:  Channel volumes
// R11-R12: Envelope period
// R13:     Envelope shape
// R14-R15: I/O ports

// Calculate frequencies
const channelAPeriod = registers[0] | (registers[1] << 8);
const frequencyA = 2000000 / (16 * channelAPeriod); // Master clock / (16 * period)

// Draw waveform visualization
function drawWaveform(samples, canvas) {
    const ctx = canvas.getContext('2d');
    const width = canvas.width;
    const height = canvas.height;

    ctx.clearRect(0, 0, width, height);
    ctx.strokeStyle = '#00ff00';
    ctx.beginPath();

    for (let i = 0; i < samples.length; i++) {
        const x = (i / samples.length) * width;
        const y = ((samples[i] + 1) / 2) * height; // Normalize -1..1 to 0..height
        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
    }

    ctx.stroke();
}

API Reference

Ym2149Player

Constructor

constructor(data: Uint8Array): Ym2149Player

Creates a new player from YM file data.

Properties

  • metadata: YmMetadata - Song metadata (read-only)

Methods

  • play(): void - Start playback
  • pause(): void - Pause playback
  • stop(): void - Stop and reset to beginning
  • restart(): void - Restart from beginning
  • is_playing(): boolean - Check if currently playing
  • state(): string - Get playback state as string

Volume Control:

  • set_volume(volume: number): void - Set volume (0.0-1.0)
  • volume(): number - Get current volume

Seeking:

  • seek_to_frame(frame: number): void - Seek to specific frame
  • seek_to_percentage(percentage: number): void - Seek to percentage (0.0-1.0)
  • frame_position(): number - Get current frame
  • frame_count(): number - Get total frames
  • position_percentage(): number - Get position as percentage

Channel Control:

  • set_channel_mute(channel: number, mute: boolean): void - Mute/unmute channel (0-2)
  • is_channel_muted(channel: number): boolean - Check if channel is muted

Audio Generation:

  • generateSamples(count: number): Float32Array - Generate audio samples
  • generateSamplesInto(buffer: Float32Array): void - Generate into buffer (zero-alloc)

Visualization:

  • get_registers(): Uint8Array - Get current PSG register values (16 bytes)

Effects:

  • set_color_filter(enabled: boolean): void - Enable/disable ST color filter

YmMetadata

interface YmMetadata {
    title: string;          // Song title
    author: string;         // Composer/author
    comments: string;       // Song comments
    format: string;         // YM format version (e.g., "YM6")
    frame_count: number;    // Total frames
    frame_rate: number;     // Frame rate in Hz (typically 50)
    duration_seconds: number; // Duration in seconds
}

Examples

See the examples/ directory for complete working examples:

  • simple-player.html - Minimal web player
  • advanced-player.html - Full-featured player with UI
  • visualizer.html - Player with oscilloscope and spectrum analyzer
  • bundler-example/ - Example using webpack/rollup

Performance

The WASM module is highly optimized:

  • ⚡ ~6ns per emulator clock cycle
  • 🎵 Real-time generation of 44.1kHz audio
  • 📦 Small bundle size (~100KB gzipped)
  • 🔋 Minimal CPU usage (<1% on modern hardware)

Browser Support

Works in all modern browsers that support:

  • WebAssembly
  • Web Audio API
  • ES6 Modules (or use a bundler)

Tested on:

  • ✅ Chrome/Edge 90+
  • ✅ Firefox 88+
  • ✅ Safari 15+
  • ✅ Mobile browsers (iOS Safari, Chrome Mobile)

Building

Development Build

wasm-pack build --dev --target web

Production Build

wasm-pack build --release --target web

Build Options

# Target web (ES modules)
wasm-pack build --target web

# Target bundler (webpack, rollup, parcel)
wasm-pack build --target bundler

# Target Node.js
wasm-pack build --target nodejs

# With features
wasm-pack build --features effects,tracker,digidrums

License

MIT - See main repository for details.

Dependencies

~15–23MB
~430K SLoC