#![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());
}
}
#[inline]
fn apply_volume(samples: &mut [f32], volume: f32) {
if volume != 1.0 {
for sample in samples.iter_mut() {
*sample *= volume;
}
}
}
#[inline]
fn set_js_prop(obj: &js_sys::Object, key: &str, value: impl Into<JsValue>) {
let _ = js_sys::Reflect::set(obj, &key.into(), &value.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 loop_count(&self) -> u32 {
self.player.loop_count()
}
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()
}
#[wasm_bindgen(js_name = setChannelMute)]
pub fn set_channel_mute(&mut self, channel: usize, mute: bool) {
self.player.set_channel_mute(channel, mute);
}
#[wasm_bindgen(js_name = isChannelMuted)]
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);
apply_volume(&mut samples, self.volume);
samples
}
#[wasm_bindgen(js_name = generateSamplesInto)]
pub fn generate_samples_into(&mut self, buffer: &mut [f32]) {
self.player.generate_samples_into(buffer);
apply_volume(buffer, 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);
apply_volume(&mut samples, 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);
apply_volume(buffer, 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 all_regs = self.player.dump_all_registers();
let obj = js_sys::Object::new();
let channels = js_sys::Array::new();
let envelopes = js_sys::Array::new();
for regs in &all_regs {
let states = ChannelStates::from_registers(regs);
for ch in &states.channels {
let ch_obj = js_sys::Object::new();
set_js_prop(&ch_obj, "frequency", ch.frequency_hz.unwrap_or(0.0));
set_js_prop(&ch_obj, "note", ch.note_name.unwrap_or("--"));
set_js_prop(&ch_obj, "amplitude", ch.amplitude_normalized);
set_js_prop(&ch_obj, "toneEnabled", ch.tone_enabled);
set_js_prop(&ch_obj, "noiseEnabled", ch.noise_enabled);
set_js_prop(&ch_obj, "envelopeEnabled", ch.envelope_enabled);
channels.push(&ch_obj);
}
let env_obj = js_sys::Object::new();
set_js_prop(&env_obj, "period", states.envelope.period);
set_js_prop(&env_obj, "shape", states.envelope.shape);
set_js_prop(&env_obj, "shapeName", states.envelope.shape_name);
envelopes.push(&env_obj);
}
if let BrowserSongPlayer::Sndh(sndh_player) = &self.player {
if sndh_player.uses_ste_features() {
let (dac_left, dac_right) = sndh_player.get_dac_levels();
let dac_l_obj = js_sys::Object::new();
set_js_prop(&dac_l_obj, "frequency", 0.0f64);
set_js_prop(&dac_l_obj, "note", "DAC");
set_js_prop(&dac_l_obj, "amplitude", dac_left as f64);
set_js_prop(&dac_l_obj, "toneEnabled", false);
set_js_prop(&dac_l_obj, "noiseEnabled", false);
set_js_prop(&dac_l_obj, "envelopeEnabled", false);
set_js_prop(&dac_l_obj, "isDac", true);
channels.push(&dac_l_obj);
let dac_r_obj = js_sys::Object::new();
set_js_prop(&dac_r_obj, "frequency", 0.0f64);
set_js_prop(&dac_r_obj, "note", "DAC");
set_js_prop(&dac_r_obj, "amplitude", dac_right as f64);
set_js_prop(&dac_r_obj, "toneEnabled", false);
set_js_prop(&dac_r_obj, "noiseEnabled", false);
set_js_prop(&dac_r_obj, "envelopeEnabled", false);
set_js_prop(&dac_r_obj, "isDac", true);
channels.push(&dac_r_obj);
}
}
set_js_prop(&obj, "channels", &channels);
set_js_prop(&obj, "envelopes", &envelopes);
if let Some(first_env) = all_regs.first() {
let states = ChannelStates::from_registers(first_env);
let env_obj = js_sys::Object::new();
set_js_prop(&env_obj, "period", states.envelope.period);
set_js_prop(&env_obj, "shape", states.envelope.shape);
set_js_prop(&env_obj, "shapeName", states.envelope.shape_name);
set_js_prop(&obj, "envelope", &env_obj);
}
obj.into()
}
#[wasm_bindgen(js_name = getLmc1992State)]
pub fn get_lmc1992_state(&self) -> JsValue {
if let BrowserSongPlayer::Sndh(sndh_player) = &self.player {
let obj = js_sys::Object::new();
set_js_prop(&obj, "masterVolume", sndh_player.lmc1992_master_volume_db() as i32);
set_js_prop(&obj, "leftVolume", sndh_player.lmc1992_left_volume_db() as i32);
set_js_prop(&obj, "rightVolume", sndh_player.lmc1992_right_volume_db() as i32);
set_js_prop(&obj, "bass", sndh_player.lmc1992_bass_db() as i32);
set_js_prop(&obj, "treble", sndh_player.lmc1992_treble_db() as i32);
set_js_prop(&obj, "masterVolumeRaw", sndh_player.lmc1992_master_volume_raw() as i32);
set_js_prop(&obj, "leftVolumeRaw", sndh_player.lmc1992_left_volume_raw() as i32);
set_js_prop(&obj, "rightVolumeRaw", sndh_player.lmc1992_right_volume_raw() as i32);
set_js_prop(&obj, "bassRaw", sndh_player.lmc1992_bass_raw() as i32);
set_js_prop(&obj, "trebleRaw", sndh_player.lmc1992_treble_raw() as i32);
obj.into()
} else {
JsValue::NULL
}
}
#[wasm_bindgen(js_name = getChannelOutputs)]
pub fn get_channel_outputs(&self) -> Vec<f32> {
let outputs = self.player.get_channel_outputs();
outputs.into_iter().flat_map(|[a, b, c]| [a, b, c]).collect()
}
#[wasm_bindgen(js_name = generateSamplesWithChannels)]
pub fn generate_samples_with_channels(&mut self, count: usize) -> JsValue {
let (mut mono, channels) = self.player.generate_samples_with_channels(count);
if self.volume != 1.0 {
for sample in &mut mono {
*sample *= self.volume;
}
}
let obj = js_sys::Object::new();
let mono_arr = js_sys::Float32Array::from(&mono[..]);
let channels_arr = js_sys::Float32Array::from(&channels[..]);
js_sys::Reflect::set(&obj, &"mono".into(), &mono_arr).ok();
js_sys::Reflect::set(&obj, &"channels".into(), &channels_arr).ok();
js_sys::Reflect::set(&obj, &"channelCount".into(), &(self.player.channel_count() as u32).into()).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 = channelCount)]
pub fn channel_count(&self) -> usize {
self.player.channel_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 psg_count = song.subsongs.first().map(|s| s.psgs.len()).unwrap_or(0);
console_log!("Arkos: loaded song with {} PSGs ({} channels)", psg_count, psg_count * 3);
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);
}