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
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.ayfiles 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 playbackpause(): void- Pause playbackstop(): void- Stop and reset to beginningrestart(): void- Restart from beginningis_playing(): boolean- Check if currently playingstate(): 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 frameseek_to_percentage(percentage: number): void- Seek to percentage (0.0-1.0)frame_position(): number- Get current frameframe_count(): number- Get total framesposition_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 samplesgenerateSamplesInto(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 playeradvanced-player.html- Full-featured player with UIvisualizer.html- Player with oscilloscope and spectrum analyzerbundler-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.
Links
- Main Repository
- Documentation
- NPM Package (coming soon)
- Examples
Dependencies
~15–23MB
~430K SLoC