Bring in SB16/WSS support (bufferless SB DSP, AD1848, SB mixer, IRQ/DMA configuration) and 0.12 fixed-point volume scaling from wss-sb16, and combine with stereo OPL3 pipeline and lock-free SPSC audio FIFO from sb16_merge.
Conflict resolutions:
- volctrl.h: use 0.12 scale from wss-sb16 (no int64_t)
- sbplay.cpp: keep stereo opl_resample_tick() with clamp16 before resampler buffers; lock-free SPSC FIFO reads with stereo volume; add sbdsp_process() from wss-sb16
- pgusinit.c: use wss-sb16's full BLASTER parser (A/I/D/H/T fields)
Adds ymfm as a new OPL synthesis backend alongside the existing emu8950
implementation, selectable at compile time via USE_YMFM_OPL / USE_YMF3812.
- ymf262 (OPL3): 18-channel true stereo via data[0]/data[1] (L1/R1)
- ym3812 (OPL2): mono, duplicated to both channels
- Pre-generation buffer (128 samples) for cache-efficient synthesis
- Stereo FIFO pipeline in sbplay: interleaved L/R pairs, proper resampling
from 49716 Hz to 44100 Hz via Q16.16 phase accumulator
- OPL3 bank 2 port handling in SOUND_SB path (ports +0xA/+0xB) and
SOUND_OPL path (port mask widened to 0x3fc, case 2/3 added)
- opl_addr widened to uint16_t to carry the 0x100 bank 2 flag
- OPL3 bank 2 status read port (oplBasePort+2) added to IOR handler
- Panning bits default to 0x30 (L1+R1) for 0xC0-0xC8 regs when unset,
preventing channels from going silent after newflag (reg 0x105) is set
- pgusinit: BLASTER T type now accepts T3-T6 (adds SB16 OPL3 / AWE32)
- Removed OPL cmd buffer drain spin-wait from IOR path (timer regs are
handled on core 0, no drain needed)
fifo_add_sample() increments samples_in_fifo with a non-atomic
load-add-store. The PWM ISR (audio consumer) runs on the same core
and can preempt the main loop (OPL producer) between the load and
store, decrementing the counter mid-update. The ISR's decrement is
then overwritten by the main loop's store, inflating samples_in_fifo
by 1. Over time this drift could cause the producer to throttle itself
early or the consumer to read a stale sample.
Wrap the increment and start-threshold check in fifo_add_sample() with
save_and_disable_interrupts()/restore_interrupts(). The IRQ disable
window is minimal (~4 cycles) and only covers the counter update — the
buffer write and write_idx advance are outside the critical section
since write_idx is single-writer (producer only) and the ISR never
reads it.
This was an audible in games like Doom. It would cause an occasional small interruption when picking up items.
Core 0 (bus ISR) was calling OPL_writeReg() directly on the emu8950
OPL struct while Core 1 was simultaneously reading it in OPL_calc_buffer()
to generate audio. With no synchronization between the cores, a register
write landing mid-calculation produces torn state — a mix of old and new
operator fields — causing occasional clicks or pops during OPL music.
The OPL_CMD_BUFFER mechanism was already implemented in picogus.cpp and
sbplay.cpp but never enabled. Enable it in CMakeLists.txt for all OPL
targets (pg-adlib, pg-sb).
With OPL_CMD_BUFFER enabled, Core 0 enqueues raw (addr, data) pairs into
a 256-entry SPSC ring buffer. Core 1 drains the queue at the top of each
main loop iteration, before the resampler fill loop, so OPL_writeReg()
only ever runs on Core 1 and never races with OPL_calc_buffer().
Timer registers (0x02–0x04) are carved out and still handled immediately
on Core 0. This preserves the existing invariant that OPL_Pico_PortRead()
and the timer state are both Core 0 only, avoiding a new cross-core race
on the timer structs.
In OPL_Pico_WriteRegister(), the register 0x04 (TIMER_CTRL) handler
had a copy-paste error in the Timer 2 enable path: it wrote the Timer 2
start bit (0x02) into timer1.enabled instead of timer2.enabled.
This meant timer2.enabled was always 0, so OPLTimer_CalculateEndTime()
never set timer2.expire_time, and the status register port read never
set bit 5 (Timer 2 expired). The bug also had a secondary effect:
writing 0x22 (enable Timer 2) would accidentally re-enable timer1.
The canonical OPL detection sequence used by the Doom engine and many
other DOS titles tests both timers — it expects bit 6 (Timer 1) and
bit 5 (Timer 2) to be set in sequence. With Timer 2 broken, games
running this detection would see it fail and potentially fall back to
PC speaker or no music. Music drivers that poll Timer 2 for their
playback interrupt would also hang silently.
The TLL (Total Level Lookup) table is 128KB — too large for the RP2040's
256KB RAM when the firmware also runs code from RAM. Previous attempts to
use a `const` array failed because the firmware uses a copy_to_ram binary
type, which copies ALL sections including `.rodata` into RAM at boot.
The fix: place the pre-computed table in the `.flashdata.tll_table` linker
section. Both pico-sdk's `memmap_copy_to_ram.ld` and the local
`fw_memmap_c2ram.ld` explicitly capture `.flashdata*` into the FLASH
`.rodata` region, which is accessed via XIP cache and never copied to RAM.
- gen_tll_table.py: new script that pre-computes makeTllTable() using the
EMU8950_NO_FLOAT integer path, outputting tll_table_flash.c with the
`.flashdata.tll_table` section attribute
- tll_table_flash.c: generated 128KB table (32,768 uint32_t values)
- emu8950.c: support EMU8950_TLL_FLASH — use extern const declaration and
skip makeTllTable() at runtime when table is pre-computed in flash
- CMakeLists: replace EMU8950_NO_TLL=1 with EMU8950_TLL_FLASH=1
TLL improves OPL2 accuracy by enabling the full Key Scale Level (KSL)
attenuation lookup table rather than the inline approximation.
Replace the int16_t intermediate in the OPL output path with int32_t
throughout: emu8950 mix_output_raw, OPL_calc_buffer, OPL_Pico_simple,
and get_opl_sample.
Remove the >> 1 attenuation in _MO() that was required to prevent
int16_t overflow on multi-channel sums. This eliminates 1 bit of
permanent precision loss per channel sample.
Use int64_t intermediate in scale_sample() to prevent int32_t overflow
when opl_volume is boosted above 1.0x (was undefined behaviour at high
volumes with the old approach).
Apply a 6x OPL gain constant in CMD_OPLVOL to compensate for the
removed attenuation and provide comfortable headroom. Per-channel peak
rises from ~25% to ~75% of full scale at 100% oplvol; the recommended
operating point is ~87% oplvol, leaving ~13% of range for boosting
quiet music. The volume setting now correctly attenuates the full-
precision signal rather than scaling an already-clipped intermediate.
When a CUE track has no INDEX 00 (no explicit pregap), prestart was
left at 0, causing skip = cur->start - 0 = full absolute frame position.
This made cdi_add_track compute a negative (wrapping) prev->length and
corrupted skip offsets for all subsequent tracks.
Fix: track whether INDEX 00 was actually seen per-track (has_prestart).
When absent, pass trk.start as prestart so skip computes to 0. Affects
all three cdi_add_track call sites in cdi_load_cue. CUEs with INDEX 00
are unaffected.
CMD_CDLIST now queues image list when status is ERROR, not just IDLE,
fixing permanent stuck state after a failed /cdload or /cdloadname
isCDImage: fix len<=4 guard so ".iso"/".cue" are rejected; hide all
dot-files instead of only macOS ._-prefixed files
Replace strcpy with strncpy for image_path to prevent buffer overrun
Ensures seek_pos, cd_end, and fifo_reset are all visible to core 1 before cd_status is set to PLAYING, preventing core 1 from starting audio consumption against a partially-updated state.
Seven correctness fixes across cdrom.c and mke.c:
cdrom_status(): initialize status to 0 to prevent random stack bits in the status byte sent to DOS
cdrom_disc_capacity(): guard ti.f - 1 against uint8 underflow when frame is 0
CMD1_READ_VER: pad response to 12 bytes as expected by SBPCD driver
CMD1_GETMODE: set data mode byte to 0x01 (Mode 1) instead of 0x00
CMD1_ABORT: clear full command buffer (bytes 0–6), not just byte 0
CMD1_READSUBQ: move cdrom_fifo_clear() to before the subchannel read, not after
CMD1_READTOC: check return value of cdrom_read_toc() before writing to FIFO
Prevents cdrom_tasks() from being starved when the OPL FIFO is nearly empty. The 4096-sample FIFO (~93ms) provides sufficient headroom to absorb the capped fill rate without risk of OPL underrun.
Fixes three reliability issues in msc_app.c:
Add 2-second timeout to wait_for_disk_io() to prevent firmware lockup when a USB drive stops responding mid-transfer
Propagate SCSI errors from disk_io_complete() — disk_read()/disk_write() now return RES_ERROR on failure instead of always returning RES_OK
Guard tuh_msc_umount_cb() against a second MSC device disconnect tearing down the active drive's filesystem
Several bugs caused CD audio to crackle, distort, and stutter when a USB HID device was connected.
usbplay.cpp, psgplay.cpp: Switch take_audio_buffer() from blocking to non-blocking with a tuh_task() poll loop, matching the existing pattern in gusplay.cpp. Add 500ms boot-time USB pump. Prevents TinyUSB keep-alive timeouts from colliding with audio DMA fills.
sbplay.cpp: Remove private cd_index and opl_out_index static cursors in the PWM IRQ handler. Both were decoupled from their FIFO's read_idx and drifted after any fifo_reset() (seek/pause/stop), causing stale/out-of-order sample reads. Now reads directly from fifo->buffer[fifo->read_idx] before advancing.
audio_fifo.h, sbplay.cpp: Increase AUDIO_FIFO_SIZE from 1024 to 4096 samples (~93ms). One CD sector is 1176 samples; the old buffer was smaller than a single sector, so slow FAT/USB reads caused underruns. Fill target updated from hardcoded 1024 to AUDIO_FIFO_SIZE - SAMPLES_PER_SECTOR.
cdrom.c: Don't call fifo_reset() on resume in cdrom_audio_pause_resume() — only flush on pause/stop. Flushing on resume discarded buffered audio and caused a gap.
cdrom.c: Fix cdrom_get_subq() to report the currently-audible position rather than the next-to-read sector. Subtracts buffered FIFO sectors from seek_pos before calling get_subchannel().
audio_fifo.h: Lower AUDIO_FIFO_START_THRESHOLD from 2048 (~46ms) to 512 (~11ms) to reduce post-seek silence.