sb16: OPL2/OPL3 via AudioWorklet, ADPCM, MPU-401 UART, AdLib compat ports, and mixer/DSP fixes#1517
Open
luckyyyyy wants to merge 9 commits intocopy:masterfrom
Open
sb16: OPL2/OPL3 via AudioWorklet, ADPCM, MPU-401 UART, AdLib compat ports, and mixer/DSP fixes#1517luckyyyyy wants to merge 9 commits intocopy:masterfrom
luckyyyyy wants to merge 9 commits intocopy:masterfrom
Conversation
cbfb8f0 to
01eb760
Compare
Replace the ScriptProcessorNode-based OPL2 synthesizer with an AudioWorklet-based implementation for lower latency and better browser compatibility. OPL2/OPL3 synthesizer: - Port DBOPL FM synthesizer from DOSBox (GPL v2+) as an AudioWorklet - Implement full OPL2 operator: ADSR envelope, KSR, tremolo, vibrato, waveform selection (all 8 waveforms), MASK_SUSTAIN - Add OPL3 mode: 4-op channels, panning (left/right/both), extended stereo synthesis modes (FM/AM combinations), OPL3 enable flag - OPL2 mono output: copy left channel to right for stereo playback - Wire OPL through SpeakerMixer for per-channel volume control Creative ADPCM decoding (DSP commands): - 0x16/0x17/0x1F: 2-bit ADPCM (4 samples/byte), single-cycle and auto-init - 0x74/0x75/0x7D: 4-bit ADPCM (2 samples/byte), single-cycle and auto-init - 0x76/0x77/0x7F: 3-bit ADPCM (3 samples/byte), single-cycle and auto-init - Reference byte support for commands 0x17/0x1F/0x75/0x7D/0x77/0x7F MPU-401 (port 0x330/0x331): - Implement UART mode (command 0x3F) and reset (0xFF) with ACK - Status register: bits 0-5 always set, bit 7 = input-not-ready - MIDI data write silently discarded (no external MIDI output) Mixer fixes: - Default SB16 volume registers 0x30-0x35 set to 31<<3 (0 dB) matching the hardware reset state - Volume formula corrected: db = (31-amount)*2, subtract 1 when count>20 matching SB16 hardware attenuation curve - OPL gain_hidden=1.5 matching adlib mixer scale factor - DAC gain_hidden=1 (PCM samples already normalised to -1..+1) - set_gain_hidden() immediately applies gain to the Web Audio GainNode - AudioWorkletNode created with outputChannelCount:[2] for stereo output
509e5ae to
774f21d
Compare
… mixer 0x0A/0x0E/0x36-0x43 DSP commands: - 0x04: ASP set mode register, track asp_init_in_progress flag - 0x05: ASP set codec parameter (2-byte consume, no-op like DOSBox) - 0x08: ASP get version (sub=0x03 returns 0x18, matching DOSBox) - 0x0F: ASP get register; reg 0x83 toggles when init in progress - 0x24: faked 8-bit DMA ADC — fill DMA memory with 0x80 silence then raise SB_IRQ_8BIT, mirroring DOSBox DSP_ADC_CallBack; respects channel-mask/unmask via dma_on_unmask - 0x2C: add explicit handler with log (was silent no-op) - 0x38: consume the data byte (was registered with size 0) - 0x80: Silence DAC — fire SB_IRQ_8BIT after computed delay via setTimeout - 0xF8: undocumented pre-SB16 command returns 0x00 (matches DOSBox) FM / AdLib ports: - port2x2_read (0x222): return 0x00; on OPL3 bits 1-2 are LOW — used by programs to distinguish OPL3 from OPL2 during chip detection - port2x8_read (0x228): delegate to port2x0_read() so the OPL2-compat status/timer register reads correctly (was returning 0xFF) - port2x8_write (0x228): set fm_current_address0 (AdLib compat address) - port2x9_write (0x229): dispatch FM_HANDLERS like port2x1_write (AdLib compat data); was a complete no-op Mixer registers: - 0x0A: Mic Level — read (mic>>2)&7, write mic=(val&7)<<2|1 sharing the 0x3A storage field, matching DOSBox SBT_16 behaviour - 0x0E: SBPro Stereo/Filter Select — bit 1 drives dsp_stereo, bit 5 (filter) stored for read-back but not applied (no analogue filter) - 0x36/0x37: CD Volume L/R — mask bits 2:0 to zero on write (DOSBox cda[x]=val>>3 / read cda[x]<<3) - 0x38/0x39: Line-in Volume L/R — same masking scheme - 0x3A: Mic Volume (SB16 5-bit field, val>>3 / read val<<3) - 0x3C: Output Mixer Switches — stored for read-back, no audio source - 0x3D/0x3E: Input Mixer Switches — same - 0x3F/0x40: Input Gain L/R — stored - 0x41/0x42: Output Gain L/R — stored (already had 0x41 handler, added 0x42) - 0x43: Mic AGC — stored State serialisation: persist dma_adc_left in state slot 36 and asp_init_in_progress is reset in dsp_reset().
Three bugs prevented OPL3 from ever being activated: - port2x3_write (bank1 data): pass 0x100|address to handler so bank1 registers carry the high bit the worklet uses to distinguish banks. Without this, all bank1 operator/channel writes were silently routed to bank0. - reg 0x104 (four-operator enable): the case-1 branch was a no-op comment; now sends opl2-reg-write so the worklet updates reg104 and calls updateSynths(). - reg 0x105 (OPL3 mode enable): the else branch was a no-op comment; now sends opl2-reg-write so the worklet sets opl3Active=0xff and switches to 18-channel mode.
0x91 is the high-speed single-cycle DAC output, symmetric with 0x90 (auto-init variant). Unlike 0x90, dma_autoinit is false so the DMA transfer does not auto-restart after completion. Also corrects the erroneous comment that labelled 0x91 as an ADC input command. Per the SB2.0 programmer's guide and DOSBox source: 0x90 – high-speed auto-init DMA DAC (already implemented) 0x91 – high-speed single-cycle DMA DAC (now implemented) 0x98 – high-speed auto-init DMA ADC (stub, input not supported) 0x99 – high-speed single-cycle DMA ADC (stub, input not supported)
Register 0x0E bit 5 is active-low: 0 = filter ON, 1 = filter OFF. DOSBox models this as a first-order RC lowpass with cutoff ~3.2 kHz in mono mode and ~8 kHz in stereo mode. - Add node_sbpro_filter_left/right (BiquadFilterNode, type=lowpass, Q=0.5) to SpeakerMixer, inserted between the gain nodes and the channel merger. - Default frequency set to Nyquist so the node is transparent when the filter is disabled. - Register 'mixer-sbpro-filter' bus handler in SpeakerMixer to update the cutoff frequency on demand. - On mixer 0x0E write, send 'mixer-sbpro-filter' with the enabled flag and the current stereo state; mixer_full_update() at reset triggers the initial state automatically.
…ignment Two bugs causing muffled audio in DOS games: 1. mixer_registers[0x0E] was uninitialized (Uint8Array defaults to 0). mixer_full_update() then called mixer_write(0x0E, 0), which sent 'mixer-sbpro-filter' with enabled=true (bit 5 = 0 = active-low), engaging the 3.2 kHz mono low-pass filter on ALL audio at startup. Root cause: DOSBox's CTMIXER_Reset() never resets 'filtered', and DosBo x.mixer.filtered is structurally zero-initialized (false) AND DOSBox never actually wires this flag into the audio graph anyway. Fix: set mixer_registers[0x0E] = 0x20 (bit 5 = 1 = filter bypass) in mixer_reset() so the filter defaults to transparent (Nyquist). 2. Register 0x46 (Bass Left) was sending to 'mixer-bass-right' instead of 'mixer-bass-left', swapping left and right bass channels. Fix: correct bus event name to 'mixer-bass-left'.
…obals) - Remove 'use strict' directive (unnecessary inside ES modules) - Remove spaces after if/for/while/switch keywords (keyword-spacing rule) - Add /* global sampleRate, registerProcessor, AudioWorkletProcessor */ comment so ESLint recognises AudioWorklet-specific globals
…sure Compiler compat Closure Compiler v20210601 does not support import.meta, causing build/libv86-debug.js to fail with JSC_CANNOT_CONVERT. Fix by accepting opl2_worklet_url as a V86 option (similar to wasm_path) and forwarding it through SpeakerAdapter -> OPL2Source. ES-module users (e.g. pal.html) pass the URL explicitly via new URL(..., import.meta.url).
Author
|
@copy done |
- Guard addModule() call with '&& worklet_url' to handle undefined case (fixes JSC_TYPE_MISMATCH: actual parameter 1 of addModule does not match formal parameter - string vs string|undefined) - Cast worklet_url to string with @type annotation for Closure Compiler - Add missing third argument 'null' to bus.register() in OPL2Source (fixes JSC_WRONG_ARGUMENT_COUNT: Function requires 3 arguments)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace the ScriptProcessorNode-based OPL2 synthesizer with an AudioWorklet and implement a large set of previously missing SB16 features.
OPL2/OPL3 Synthesizer (
opl2-worklet.js)AudioWorkletProcessorMASK_SUSTAINSpeakerMixerso hardware FM volume registers (0x34/0x35) take effectaddr | 0x100so the worklet routes them to the correct OPL3 operator/channel setAdLib Compatible Ports
port2x0_read()— correct timer/status valuefm_current_address0(AdLib compat address latch)FM_HANDLERSlike port 0x221 (AdLib compat data)Creative ADPCM Decoding
Reference-byte and auto-init variants all supported.
DSP Commands
asp_init_in_progressDSP_ADC_CallBack)setTimeoutMPU-401 (port 0x330/0x331)
Mixer Registers
(mic>>2)&7, writemic=(val&7)<<2|1, shares 0x3A storagedsp_stereo; bit 5 stored for read-backval>>3/ readval<<3, shared with 0x0A31<<3(0 dB), correct attenuation formulaRemaining Known Gaps (vs DOSBox)