1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
//! SNDH file WASM player wrapper.
//!
//! Wraps `SndhPlayer` to provide a consistent interface for the browser player.
use ym2149::Ym2149Backend;
use ym2149_common::{ChiptunePlayer, ChiptunePlayerBase, MetadataFields, PlaybackState};
use ym2149_sndh_replayer::{SndhPlayer, load_sndh};
use crate::YM_SAMPLE_RATE_F32;
use crate::metadata::YmMetadata;
/// SNDH player wrapper for WebAssembly.
pub struct SndhWasmPlayer {
player: SndhPlayer,
/// Cached STE detection (preserved across subsong changes)
uses_ste: bool,
}
impl SndhWasmPlayer {
/// Create a new SNDH WASM player wrapper from raw data.
pub fn new(data: &[u8]) -> Result<(Self, YmMetadata), String> {
let sample_rate = YM_SAMPLE_RATE_F32 as u32;
let mut player =
load_sndh(data, sample_rate).map_err(|e| format!("Failed to load SNDH: {e}"))?;
// Initialize default subsong
let default_subsong = player.default_subsong();
player
.init_subsong(default_subsong)
.map_err(|e| format!("Failed to init SNDH subsong: {e}"))?;
// Warm-up: Generate audio to detect STE hardware usage at runtime.
// Some drivers don't enable DMA until actual playback starts.
// We generate ~500ms of audio and discard the output.
// Buffer size: 44100 floats = 22050 stereo frames = 500ms at 44.1kHz
// Use heap allocation to avoid stack overflow in WASM.
{
let mut discard_buffer = vec![0.0f32; 44100];
player.render_f32_stereo(&mut discard_buffer);
}
// Capture STE detection state BEFORE re-init (reset would clear it)
let uses_ste = player.uses_ste_features();
// Re-initialize to reset position to beginning (clean state)
let _ = player.init_subsong(default_subsong);
let metadata = metadata_from_player(&player);
Ok((Self { player, uses_ste }, metadata))
}
/// Start playback.
pub fn play(&mut self) {
ChiptunePlayerBase::play(&mut self.player);
}
/// Pause playback.
pub fn pause(&mut self) {
ChiptunePlayerBase::pause(&mut self.player);
}
/// Stop playback and reset.
pub fn stop(&mut self) {
ChiptunePlayerBase::stop(&mut self.player);
}
/// Get current playback state.
pub fn state(&self) -> PlaybackState {
ChiptunePlayerBase::state(&self.player)
}
/// Get current frame position.
pub fn frame_position(&self) -> usize {
self.player.current_frame() as usize
}
/// Get total frame count.
///
/// Returns 0 if duration is unknown (from FRMS tag or TIME fallback).
pub fn frame_count(&self) -> usize {
self.player.total_frames() as usize
}
/// Get playback position as percentage (0.0 to 1.0).
pub fn playback_position(&self) -> f32 {
self.player.progress()
}
/// Get the number of times the song has looped.
pub fn loop_count(&self) -> u32 {
self.player.loop_count()
}
/// Seek to a specific frame.
///
/// Returns true on success. Seeking re-initializes and fast-forwards.
pub fn seek_frame(&mut self, frame: usize) -> bool {
self.player.seek_to_frame(frame as u32).is_ok()
}
/// Seek to a percentage position (0.0 to 1.0).
///
/// Returns true on success. Works for all SNDH files (uses fallback duration for older files).
pub fn seek_percentage(&mut self, position: f32) -> bool {
ChiptunePlayerBase::seek(&mut self.player, position)
}
/// Get duration in seconds.
///
/// For SNDH < 2.2 without FRMS/TIME, returns 300 (5 minute fallback).
pub fn duration_seconds(&self) -> f32 {
ChiptunePlayerBase::duration_seconds(&self.player)
}
/// Check if the duration is from actual metadata (FRMS/TIME) or estimated.
///
/// Returns false for older SNDH files using the 5-minute fallback.
pub fn has_duration_info(&self) -> bool {
self.player.has_duration_info()
}
/// Generate mono audio samples.
pub fn generate_samples(&mut self, count: usize) -> Vec<f32> {
ChiptunePlayerBase::generate_samples(&mut self.player, count)
}
/// Generate mono audio samples into a pre-allocated buffer.
pub fn generate_samples_into(&mut self, buffer: &mut [f32]) {
ChiptunePlayerBase::generate_samples_into(&mut self.player, buffer);
}
/// Generate stereo audio samples (interleaved L/R).
///
/// Returns stereo samples with STE DAC and LMC1992 audio processing.
pub fn generate_samples_stereo(&mut self, frame_count: usize) -> Vec<f32> {
let mut buffer = vec![0.0f32; frame_count * 2];
self.player.render_f32_stereo(&mut buffer);
buffer
}
/// Generate stereo audio samples into a pre-allocated buffer (interleaved L/R).
///
/// Buffer length must be even (frame_count * 2).
pub fn generate_samples_into_stereo(&mut self, buffer: &mut [f32]) {
self.player.render_f32_stereo(buffer);
}
/// Mute or unmute a channel.
///
/// SNDH has 5 logical channels:
/// - 0, 1, 2: YM2149 channels A, B, C
/// - 3: STE DAC Left
/// - 4: STE DAC Right
pub fn set_channel_mute(&mut self, channel: usize, mute: bool) {
match channel {
0..=2 => {
// YM2149 channels
ChiptunePlayerBase::set_channel_mute(&mut self.player, channel, mute);
}
3 => {
// DAC Left
self.player.set_dac_mute_left(mute);
}
4 => {
// DAC Right
self.player.set_dac_mute_right(mute);
}
_ => {}
}
}
/// Check if a channel is muted.
///
/// SNDH has 5 logical channels:
/// - 0, 1, 2: YM2149 channels A, B, C
/// - 3: STE DAC Left
/// - 4: STE DAC Right
pub fn is_channel_muted(&self, channel: usize) -> bool {
match channel {
0..=2 => ChiptunePlayerBase::is_channel_muted(&self.player, channel),
3 => self.player.is_dac_left_muted(),
4 => self.player.is_dac_right_muted(),
_ => false,
}
}
/// Get the number of channels.
///
/// Always returns 5 for SNDH (3 YM2149 + 2 DAC).
/// DAC channels will show zero activity for non-STE songs.
pub fn channel_count(&self) -> usize {
5 // Always show all channels: 3 YM + 2 DAC (L/R)
}
/// Check if this SNDH uses STE hardware features.
pub fn uses_ste_features(&self) -> bool {
self.uses_ste
}
/// Get current DAC levels for visualization (normalized 0.0 to 1.0).
///
/// Returns (left, right) amplitude values.
pub fn get_dac_levels(&self) -> (f32, f32) {
self.player.get_dac_levels()
}
/// Dump current PSG register values.
pub fn dump_registers(&self) -> [u8; 16] {
self.player.ym2149().dump_registers()
}
/// Get current per-channel audio outputs.
///
/// Returns the actual audio output values (A, B, C) updated at sample rate.
pub fn get_channel_outputs(&self) -> (f32, f32, f32) {
self.player.ym2149().get_channel_outputs()
}
/// Generate samples with per-sample channel outputs for visualization.
///
/// Fills the mono buffer with mixed samples and channels buffer with
/// per-sample channel outputs. For SNDH, we capture YM2149 channels
/// and STE DAC levels.
///
/// Channel layout: [A, B, C, DAC_L, DAC_R] (always 5 channels for SNDH).
pub fn generate_samples_with_channels_into(&mut self, mono: &mut [f32], channels: &mut [f32]) {
let channel_count = self.channel_count();
// Generate samples in small batches and capture channel state after each
// Using batch size of 1 stereo frame (2 samples) for best accuracy
let mut stereo_buf = [0.0f32; 2];
let mut idx = 0;
while idx < mono.len() {
// Render one stereo frame
self.player.render_f32_stereo(&mut stereo_buf);
mono[idx] = (stereo_buf[0] + stereo_buf[1]) * 0.5;
// Capture YM2149 channel outputs
let (a, b, c) = self.player.ym2149().get_channel_outputs();
let base = idx * channel_count;
channels[base] = a;
channels[base + 1] = b;
channels[base + 2] = c;
// Capture DAC levels
let (dac_l, dac_r) = self.player.get_dac_levels();
channels[base + 3] = dac_l;
channels[base + 4] = dac_r;
idx += 1;
}
}
/// Enable or disable the color filter.
pub fn set_color_filter(&mut self, _enabled: bool) {
// Not applicable for SNDH (uses real 68000 code)
}
/// Get number of subsongs.
pub fn subsong_count(&self) -> usize {
self.player.subsong_count()
}
/// Get current subsong (1-based).
pub fn current_subsong(&self) -> usize {
self.player.current_subsong()
}
/// Set subsong (1-based). Returns true on success.
///
/// Valid range: 1 to `subsong_count()`.
pub fn set_subsong(&mut self, index: usize) -> bool {
if index < 1 || index > self.subsong_count() {
return false;
}
self.player.init_subsong(index).is_ok()
}
/// Get LMC1992 master volume in dB (-80 to 0).
pub fn lmc1992_master_volume_db(&self) -> i8 {
self.player.lmc1992_master_volume_db()
}
/// Get LMC1992 left volume in dB (-40 to 0).
pub fn lmc1992_left_volume_db(&self) -> i8 {
self.player.lmc1992_left_volume_db()
}
/// Get LMC1992 right volume in dB (-40 to 0).
pub fn lmc1992_right_volume_db(&self) -> i8 {
self.player.lmc1992_right_volume_db()
}
/// Get LMC1992 bass in dB (-12 to +12).
pub fn lmc1992_bass_db(&self) -> i8 {
self.player.lmc1992_bass_db()
}
/// Get LMC1992 treble in dB (-12 to +12).
pub fn lmc1992_treble_db(&self) -> i8 {
self.player.lmc1992_treble_db()
}
/// Get LMC1992 master volume raw value (0-40).
pub fn lmc1992_master_volume_raw(&self) -> u8 {
self.player.lmc1992_master_volume_raw()
}
/// Get LMC1992 left volume raw value (0-20).
pub fn lmc1992_left_volume_raw(&self) -> u8 {
self.player.lmc1992_left_volume_raw()
}
/// Get LMC1992 right volume raw value (0-20).
pub fn lmc1992_right_volume_raw(&self) -> u8 {
self.player.lmc1992_right_volume_raw()
}
/// Get LMC1992 bass raw value (0-12).
pub fn lmc1992_bass_raw(&self) -> u8 {
self.player.lmc1992_bass_raw()
}
/// Get LMC1992 treble raw value (0-12).
pub fn lmc1992_treble_raw(&self) -> u8 {
self.player.lmc1992_treble_raw()
}
}
/// Convert SNDH player metadata to YmMetadata for WASM.
fn metadata_from_player(player: &SndhPlayer) -> YmMetadata {
let meta = ChiptunePlayer::metadata(player);
let frame_count = player.total_frames();
let frame_rate = meta.frame_rate();
let duration_seconds = if frame_count > 0 && frame_rate > 0 {
frame_count as f32 / frame_rate as f32
} else {
0.0
};
YmMetadata {
title: if meta.title().is_empty() {
"(unknown)".to_string()
} else {
meta.title().to_string()
},
author: if meta.author().is_empty() {
"(unknown)".to_string()
} else {
meta.author().to_string()
},
comments: meta.comments().to_string(),
format: "SNDH".to_string(),
frame_count,
frame_rate,
duration_seconds,
}
}