1#![warn(missing_docs)]
41
42mod metadata;
43mod players;
44
45use wasm_bindgen::prelude::*;
46use ym2149_arkos_replayer::{ArkosPlayer, load_aks};
47use ym2149_ay_replayer::{AyPlayer, CPC_UNSUPPORTED_MSG};
48use ym2149_sndh_replayer::is_sndh_data;
49use ym2149_ym_replayer::{PlaybackState, load_song};
50
51use metadata::{YmMetadata, metadata_from_summary};
52use players::{BrowserSongPlayer, arkos::ArkosWasmPlayer, ay::AyWasmPlayer, sndh::SndhWasmPlayer};
53use ym2149_common::DEFAULT_SAMPLE_RATE;
54
55pub const YM_SAMPLE_RATE_F32: f32 = DEFAULT_SAMPLE_RATE as f32;
57
58#[wasm_bindgen(start)]
60pub fn init_panic_hook() {
61 console_error_panic_hook::set_once();
62}
63
64macro_rules! console_log {
66 ($($t:tt)*) => {
67 web_sys::console::log_1(&format!($($t)*).into());
68 }
69}
70
71#[wasm_bindgen]
76pub struct Ym2149Player {
77 player: BrowserSongPlayer,
78 metadata: YmMetadata,
79 volume: f32,
80}
81
82#[wasm_bindgen]
83impl Ym2149Player {
84 #[wasm_bindgen(constructor)]
96 pub fn new(data: &[u8]) -> Result<Ym2149Player, JsValue> {
97 console_log!("Loading file ({} bytes)...", data.len());
98
99 let (player, metadata) = load_browser_player(data).map_err(|e| {
100 JsValue::from_str(&format!(
101 "Failed to load chiptune file ({} bytes): {}",
102 data.len(),
103 e
104 ))
105 })?;
106
107 console_log!("Song loaded successfully");
108 console_log!(" Title: {}", metadata.title);
109 console_log!(" Format: {}", metadata.format);
110
111 Ok(Ym2149Player {
112 player,
113 metadata,
114 volume: 1.0,
115 })
116 }
117
118 #[wasm_bindgen(getter)]
120 pub fn metadata(&self) -> YmMetadata {
121 self.metadata.clone()
122 }
123
124 pub fn play(&mut self) {
126 self.player.play();
127 }
128
129 pub fn pause(&mut self) {
131 self.player.pause();
132 }
133
134 pub fn stop(&mut self) {
136 self.player.stop();
137 }
138
139 pub fn restart(&mut self) {
141 self.player.stop();
142 self.player.play();
143 }
144
145 pub fn is_playing(&self) -> bool {
147 self.player.state() == PlaybackState::Playing
148 }
149
150 pub fn state(&self) -> String {
152 format!("{:?}", self.player.state())
153 }
154
155 pub fn set_volume(&mut self, volume: f32) {
157 self.volume = volume.clamp(0.0, 1.0);
158 }
159
160 pub fn volume(&self) -> f32 {
162 self.volume
163 }
164
165 pub fn frame_position(&self) -> u32 {
167 self.player.frame_position() as u32
168 }
169
170 pub fn frame_count(&self) -> u32 {
172 self.player.frame_count() as u32
173 }
174
175 pub fn position_percentage(&self) -> f32 {
177 self.player.playback_position()
178 }
179
180 pub fn seek_to_frame(&mut self, frame: u32) {
182 let _ = self.player.seek_frame(frame as usize);
183 }
184
185 pub fn seek_to_percentage(&mut self, percentage: f32) {
187 let total_frames = self.player.frame_count().max(1);
188 let clamped = percentage.clamp(0.0, 1.0);
189 let target = ((total_frames as f32 - 1.0) * clamped).round() as usize;
190 let _ = self.player.seek_frame(target);
191 }
192
193 pub fn set_channel_mute(&mut self, channel: usize, mute: bool) {
195 self.player.set_channel_mute(channel, mute);
196 }
197
198 pub fn is_channel_muted(&self, channel: usize) -> bool {
200 self.player.is_channel_muted(channel)
201 }
202
203 #[wasm_bindgen(js_name = generateSamples)]
210 pub fn generate_samples(&mut self, count: usize) -> Vec<f32> {
211 let mut samples = self.player.generate_samples(count);
212 if self.volume != 1.0 {
213 for sample in &mut samples {
214 *sample *= self.volume;
215 }
216 }
217 samples
218 }
219
220 #[wasm_bindgen(js_name = generateSamplesInto)]
224 pub fn generate_samples_into(&mut self, buffer: &mut [f32]) {
225 self.player.generate_samples_into(buffer);
226 if self.volume != 1.0 {
227 for sample in buffer.iter_mut() {
228 *sample *= self.volume;
229 }
230 }
231 }
232
233 pub fn get_registers(&self) -> Vec<u8> {
235 self.player.dump_registers().to_vec()
236 }
237
238 #[wasm_bindgen(js_name = getChannelStates)]
251 pub fn get_channel_states(&self) -> JsValue {
252 use ym2149_common::ChannelStates;
253
254 let regs = self.player.dump_registers();
255 let states = ChannelStates::from_registers(®s);
256
257 let obj = js_sys::Object::new();
259
260 let channels = js_sys::Array::new();
262 for ch in &states.channels {
263 let ch_obj = js_sys::Object::new();
264 js_sys::Reflect::set(
265 &ch_obj,
266 &"frequency".into(),
267 &ch.frequency_hz.unwrap_or(0.0).into(),
268 )
269 .ok();
270 js_sys::Reflect::set(
271 &ch_obj,
272 &"note".into(),
273 &ch.note_name.unwrap_or("--").into(),
274 )
275 .ok();
276 js_sys::Reflect::set(
277 &ch_obj,
278 &"amplitude".into(),
279 &ch.amplitude_normalized.into(),
280 )
281 .ok();
282 js_sys::Reflect::set(&ch_obj, &"toneEnabled".into(), &ch.tone_enabled.into()).ok();
283 js_sys::Reflect::set(&ch_obj, &"noiseEnabled".into(), &ch.noise_enabled.into()).ok();
284 js_sys::Reflect::set(
285 &ch_obj,
286 &"envelopeEnabled".into(),
287 &ch.envelope_enabled.into(),
288 )
289 .ok();
290 channels.push(&ch_obj);
291 }
292 js_sys::Reflect::set(&obj, &"channels".into(), &channels).ok();
293
294 let env_obj = js_sys::Object::new();
296 js_sys::Reflect::set(&env_obj, &"period".into(), &states.envelope.period.into()).ok();
297 js_sys::Reflect::set(&env_obj, &"shape".into(), &states.envelope.shape.into()).ok();
298 js_sys::Reflect::set(
299 &env_obj,
300 &"shapeName".into(),
301 &states.envelope.shape_name.into(),
302 )
303 .ok();
304 js_sys::Reflect::set(&obj, &"envelope".into(), &env_obj).ok();
305
306 obj.into()
307 }
308
309 pub fn set_color_filter(&mut self, enabled: bool) {
311 self.player.set_color_filter(enabled);
312 }
313
314 #[wasm_bindgen(js_name = subsongCount)]
316 pub fn subsong_count(&self) -> usize {
317 self.player.subsong_count()
318 }
319
320 #[wasm_bindgen(js_name = currentSubsong)]
322 pub fn current_subsong(&self) -> usize {
323 self.player.current_subsong()
324 }
325
326 #[wasm_bindgen(js_name = setSubsong)]
328 pub fn set_subsong(&mut self, index: usize) -> bool {
329 self.player.set_subsong(index)
330 }
331}
332
333fn load_browser_player(data: &[u8]) -> Result<(BrowserSongPlayer, YmMetadata), String> {
335 if data.is_empty() {
336 return Err("empty file data".to_string());
337 }
338
339 if is_sndh_data(data) {
342 let (wrapper, metadata) = SndhWasmPlayer::new(data)?;
343 return Ok((BrowserSongPlayer::Sndh(Box::new(wrapper)), metadata));
344 }
345
346 if let Ok((player, summary)) = load_song(data) {
348 let metadata = metadata_from_summary(&player, &summary);
349 return Ok((BrowserSongPlayer::Ym(Box::new(player)), metadata));
350 }
351
352 if let Ok(song) = load_aks(data) {
354 let arkos_player =
355 ArkosPlayer::new(song, 0).map_err(|e| format!("Arkos player init failed: {e}"))?;
356 let (wrapper, metadata) = ArkosWasmPlayer::new(arkos_player);
357 return Ok((BrowserSongPlayer::Arkos(Box::new(wrapper)), metadata));
358 }
359
360 if let Ok((wrapper, metadata)) = SndhWasmPlayer::new(data) {
362 return Ok((BrowserSongPlayer::Sndh(Box::new(wrapper)), metadata));
363 }
364
365 let (player, meta) = AyPlayer::load_from_bytes(data, 0)
367 .map_err(|e| format!("unrecognized format (AY parse error: {e})"))?;
368 if player.requires_cpc_firmware() {
369 return Err(CPC_UNSUPPORTED_MSG.to_string());
370 }
371 let (wrapper, metadata) = AyWasmPlayer::new(player, &meta);
372 Ok((BrowserSongPlayer::Ay(Box::new(wrapper)), metadata))
373}
374
375#[wasm_bindgen]
377extern "C" {
378 #[wasm_bindgen(js_namespace = console)]
379 fn log(s: &str);
380}