#![warn(missing_docs)]
mod metadata;
mod players;
use wasm_bindgen::prelude::*;
use ym2149_arkos_replayer::{ArkosPlayer, load_aks};
use ym2149_ay_replayer::{AyPlayer, CPC_UNSUPPORTED_MSG};
use ym2149_sndh_replayer::is_sndh_data;
use ym2149_ym_replayer::{PlaybackState, load_song};
use metadata::{YmMetadata, metadata_from_summary};
use players::{BrowserSongPlayer, arkos::ArkosWasmPlayer, ay::AyWasmPlayer, sndh::SndhWasmPlayer};
use ym2149_common::DEFAULT_SAMPLE_RATE;
pub const YM_SAMPLE_RATE_F32: f32 = DEFAULT_SAMPLE_RATE as f32;
#[wasm_bindgen(start)]
pub fn init_panic_hook() {
console_error_panic_hook::set_once();
}
macro_rules! console_log {
($($t:tt)*) => {
web_sys::console::log_1(&format!($($t)*).into());
}
}
#[wasm_bindgen]
pub struct Ym2149Player {
player: BrowserSongPlayer,
metadata: YmMetadata,
volume: f32,
}
#[wasm_bindgen]
impl Ym2149Player {
#[wasm_bindgen(constructor)]
pub fn new(data: &[u8]) -> Result<Ym2149Player, JsValue> {
console_log!("Loading file ({} bytes)...", data.len());
let (player, metadata) = load_browser_player(data).map_err(|e| {
JsValue::from_str(&format!(
"Failed to load chiptune file ({} bytes): {}",
data.len(),
e
))
})?;
console_log!("Song loaded successfully");
console_log!(" Title: {}", metadata.title);
console_log!(" Format: {}", metadata.format);
Ok(Ym2149Player {
player,
metadata,
volume: 1.0,
})
}
#[wasm_bindgen(getter)]
pub fn metadata(&self) -> YmMetadata {
self.metadata.clone()
}
pub fn play(&mut self) {
self.player.play();
}
pub fn pause(&mut self) {
self.player.pause();
}
pub fn stop(&mut self) {
self.player.stop();
}
pub fn restart(&mut self) {
self.player.stop();
self.player.play();
}
pub fn is_playing(&self) -> bool {
self.player.state() == PlaybackState::Playing
}
pub fn state(&self) -> String {
format!("{:?}", self.player.state())
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.clamp(0.0, 1.0);
}
pub fn volume(&self) -> f32 {
self.volume
}
pub fn frame_position(&self) -> u32 {
self.player.frame_position() as u32
}
pub fn frame_count(&self) -> u32 {
self.player.frame_count() as u32
}
pub fn position_percentage(&self) -> f32 {
self.player.playback_position()
}
pub fn seek_to_frame(&mut self, frame: u32) {
let _ = self.player.seek_frame(frame as usize);
}
pub fn seek_to_percentage(&mut self, percentage: f32) -> bool {
self.player.seek_percentage(percentage)
}
pub fn duration_seconds(&self) -> f32 {
self.player.duration_seconds()
}
#[wasm_bindgen(js_name = hasDurationInfo)]
pub fn has_duration_info(&self) -> bool {
self.player.has_duration_info()
}
pub fn set_channel_mute(&mut self, channel: usize, mute: bool) {
self.player.set_channel_mute(channel, mute);
}
pub fn is_channel_muted(&self, channel: usize) -> bool {
self.player.is_channel_muted(channel)
}
#[wasm_bindgen(js_name = generateSamples)]
pub fn generate_samples(&mut self, count: usize) -> Vec<f32> {
let mut samples = self.player.generate_samples(count);
if self.volume != 1.0 {
for sample in &mut samples {
*sample *= self.volume;
}
}
samples
}
#[wasm_bindgen(js_name = generateSamplesInto)]
pub fn generate_samples_into(&mut self, buffer: &mut [f32]) {
self.player.generate_samples_into(buffer);
if self.volume != 1.0 {
for sample in buffer.iter_mut() {
*sample *= self.volume;
}
}
}
#[wasm_bindgen(js_name = generateSamplesStereo)]
pub fn generate_samples_stereo(&mut self, frame_count: usize) -> Vec<f32> {
let mut samples = self.player.generate_samples_stereo(frame_count);
if self.volume != 1.0 {
for sample in &mut samples {
*sample *= self.volume;
}
}
samples
}
#[wasm_bindgen(js_name = generateSamplesIntoStereo)]
pub fn generate_samples_into_stereo(&mut self, buffer: &mut [f32]) {
self.player.generate_samples_into_stereo(buffer);
if self.volume != 1.0 {
for sample in buffer.iter_mut() {
*sample *= self.volume;
}
}
}
pub fn get_registers(&self) -> Vec<u8> {
self.player.dump_registers().to_vec()
}
#[wasm_bindgen(js_name = getChannelStates)]
pub fn get_channel_states(&self) -> JsValue {
use ym2149_common::ChannelStates;
let regs = self.player.dump_registers();
let states = ChannelStates::from_registers(®s);
let obj = js_sys::Object::new();
let channels = js_sys::Array::new();
for ch in &states.channels {
let ch_obj = js_sys::Object::new();
js_sys::Reflect::set(
&ch_obj,
&"frequency".into(),
&ch.frequency_hz.unwrap_or(0.0).into(),
)
.ok();
js_sys::Reflect::set(
&ch_obj,
&"note".into(),
&ch.note_name.unwrap_or("--").into(),
)
.ok();
js_sys::Reflect::set(
&ch_obj,
&"amplitude".into(),
&ch.amplitude_normalized.into(),
)
.ok();
js_sys::Reflect::set(&ch_obj, &"toneEnabled".into(), &ch.tone_enabled.into()).ok();
js_sys::Reflect::set(&ch_obj, &"noiseEnabled".into(), &ch.noise_enabled.into()).ok();
js_sys::Reflect::set(
&ch_obj,
&"envelopeEnabled".into(),
&ch.envelope_enabled.into(),
)
.ok();
channels.push(&ch_obj);
}
js_sys::Reflect::set(&obj, &"channels".into(), &channels).ok();
let env_obj = js_sys::Object::new();
js_sys::Reflect::set(&env_obj, &"period".into(), &states.envelope.period.into()).ok();
js_sys::Reflect::set(&env_obj, &"shape".into(), &states.envelope.shape.into()).ok();
js_sys::Reflect::set(
&env_obj,
&"shapeName".into(),
&states.envelope.shape_name.into(),
)
.ok();
js_sys::Reflect::set(&obj, &"envelope".into(), &env_obj).ok();
obj.into()
}
pub fn set_color_filter(&mut self, enabled: bool) {
self.player.set_color_filter(enabled);
}
#[wasm_bindgen(js_name = subsongCount)]
pub fn subsong_count(&self) -> usize {
self.player.subsong_count()
}
#[wasm_bindgen(js_name = currentSubsong)]
pub fn current_subsong(&self) -> usize {
self.player.current_subsong()
}
#[wasm_bindgen(js_name = setSubsong)]
pub fn set_subsong(&mut self, index: usize) -> bool {
self.player.set_subsong(index)
}
}
fn load_browser_player(data: &[u8]) -> Result<(BrowserSongPlayer, YmMetadata), String> {
if data.is_empty() {
return Err("empty file data".to_string());
}
if is_sndh_data(data) {
let (wrapper, metadata) = SndhWasmPlayer::new(data)?;
return Ok((BrowserSongPlayer::Sndh(Box::new(wrapper)), metadata));
}
if let Ok((player, summary)) = load_song(data) {
let metadata = metadata_from_summary(&player, &summary);
return Ok((BrowserSongPlayer::Ym(Box::new(player)), metadata));
}
if let Ok(song) = load_aks(data) {
let arkos_player =
ArkosPlayer::new(song, 0).map_err(|e| format!("Arkos player init failed: {e}"))?;
let (wrapper, metadata) = ArkosWasmPlayer::new(arkos_player);
return Ok((BrowserSongPlayer::Arkos(Box::new(wrapper)), metadata));
}
if let Ok((wrapper, metadata)) = SndhWasmPlayer::new(data) {
return Ok((BrowserSongPlayer::Sndh(Box::new(wrapper)), metadata));
}
let (player, meta) = AyPlayer::load_from_bytes(data, 0)
.map_err(|e| format!("unrecognized format (AY parse error: {e})"))?;
if player.requires_cpc_firmware() {
return Err(CPC_UNSUPPORTED_MSG.to_string());
}
let (wrapper, metadata) = AyWasmPlayer::new(player, &meta);
Ok((BrowserSongPlayer::Ay(Box::new(wrapper)), metadata))
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}