Skip to content
Open
92 changes: 86 additions & 6 deletions src/browser/speaker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
MIXER_CHANNEL_BOTH, MIXER_CHANNEL_LEFT, MIXER_CHANNEL_RIGHT,
MIXER_SRC_PCSPEAKER, MIXER_SRC_DAC, MIXER_SRC_MASTER,
MIXER_SRC_PCSPEAKER, MIXER_SRC_DAC, MIXER_SRC_MASTER, MIXER_SRC_OPL,
} from "../const.js";
import { dbg_assert, dbg_log } from "../log.js";
import { OSCILLATOR_FREQ } from "../pit.js";
Expand All @@ -18,8 +18,9 @@ const AUDIOBUFFER_MINIMUM_SAMPLING_RATE = 8000;
/**
* @constructor
* @param {!BusConnector} bus
* @param {Object=} options
*/
export function SpeakerAdapter(bus)
export function SpeakerAdapter(bus, options)
{
if(typeof window === "undefined")
{
Expand All @@ -46,6 +47,8 @@ export function SpeakerAdapter(bus)

this.dac = new SpeakerDAC(bus, this.audio_context, this.mixer);

this.opl2 = new OPL2Source(bus, this.audio_context, this.mixer, options && options["opl2_worklet_url"]);

this.pcspeaker.start();

bus.register("emulator-stopped", function()
Expand Down Expand Up @@ -113,6 +116,20 @@ function SpeakerMixer(bus, audio_context)
this.node_gain_left = this.audio_context.createGain();
this.node_gain_right = this.audio_context.createGain();

// SBPro analogue output filter (mixer 0x0E bit 5, active-low).
// Mono cutoff ~3.2 kHz, stereo cutoff ~8 kHz; disabled = Nyquist (passthrough).
this.node_sbpro_filter_left = this.audio_context.createBiquadFilter();
this.node_sbpro_filter_right = this.audio_context.createBiquadFilter();
this.node_sbpro_filter_left.type = "lowpass";
this.node_sbpro_filter_right.type = "lowpass";
// Default: filter disabled — use Nyquist so the node is transparent.
var nyquist = this.audio_context.sampleRate / 2;
this.node_sbpro_filter_left.frequency.setValueAtTime(nyquist, this.audio_context.currentTime);
this.node_sbpro_filter_right.frequency.setValueAtTime(nyquist, this.audio_context.currentTime);
// Q ≈ 0.5 approximates the first-order RC slope of the real hardware filter.
this.node_sbpro_filter_left.Q.setValueAtTime(0.5, this.audio_context.currentTime);
this.node_sbpro_filter_right.Q.setValueAtTime(0.5, this.audio_context.currentTime);

this.node_merger = this.audio_context.createChannelMerger(2);

// Graph
Expand All @@ -122,11 +139,13 @@ function SpeakerMixer(bus, audio_context)

this.node_treble_left.connect(this.node_bass_left);
this.node_bass_left.connect(this.node_gain_left);
this.node_gain_left.connect(this.node_merger, 0, 0);
this.node_gain_left.connect(this.node_sbpro_filter_left);
this.node_sbpro_filter_left.connect(this.node_merger, 0, 0);

this.node_treble_right.connect(this.node_bass_right);
this.node_bass_right.connect(this.node_gain_right);
this.node_gain_right.connect(this.node_merger, 0, 1);
this.node_gain_right.connect(this.node_sbpro_filter_right);
this.node_sbpro_filter_right.connect(this.node_merger, 0, 1);

this.node_merger.connect(this.audio_context.destination);

Expand Down Expand Up @@ -188,6 +207,16 @@ function SpeakerMixer(bus, audio_context)
bus.register("mixer-treble-right", create_gain_handler(this.node_treble_right), this);
bus.register("mixer-bass-left", create_gain_handler(this.node_bass_left), this);
bus.register("mixer-bass-right", create_gain_handler(this.node_bass_right), this);

bus.register("mixer-sbpro-filter", function(data)
{
// data[0]: filter enabled (bit 5 = 0), data[1]: stereo
var enabled = data[0];
var stereo = data[1];
var cutoff = enabled ? (stereo ? 8000 : 3200) : this.audio_context.sampleRate / 2;
this.node_sbpro_filter_left.frequency.setValueAtTime(cutoff, this.audio_context.currentTime);
this.node_sbpro_filter_right.frequency.setValueAtTime(cutoff, this.audio_context.currentTime);
}, this);
}

/**
Expand Down Expand Up @@ -393,6 +422,7 @@ SpeakerMixerSource.prototype.set_volume = function(value, channel)
SpeakerMixerSource.prototype.set_gain_hidden = function(value)
{
this.gain_hidden = value;
this.update();
};

/**
Expand Down Expand Up @@ -448,6 +478,56 @@ PCSpeaker.prototype.start = function()
this.node_oscillator.start();
};

/**
* OPL2/OPL3 FM synthesizer source wired through the SpeakerMixer.
* Receives OPL register writes from SB16 via the bus and forwards
* them to the AudioWorklet running in opl2-worklet.js.
*
* @constructor
* @param {!BusConnector} bus
* @param {!AudioContext} audio_context
* @param {!SpeakerMixer} mixer
* @param {string=} worklet_url
*/
function OPL2Source(bus, audio_context, mixer, worklet_url)
{
var _port = null;
var _queue = [];

function send(msg)
{
if(_port) _port.postMessage(msg);
else _queue.push(msg);
}

// Placeholder pass-through node; registered synchronously so mixer-volume
// events from mixer_full_update() find the source immediately.
var node_output = audio_context.createGain();
var mixer_connection = mixer.add_source(node_output, MIXER_SRC_OPL);
// OPL output amplitude is ~2/3 of full-scale PCM; scale up to match.
mixer_connection.set_gain_hidden(1.5);

if(window.AudioWorklet && worklet_url)
{
audio_context.audioWorklet.addModule(/** @type{string} */ (worklet_url)).then(function()
{
var node = new AudioWorkletNode(audio_context, "opl2", { outputChannelCount: [2] });
node.connect(node_output);
_port = node.port;
for(var i = 0; i < _queue.length; i++) _port.postMessage(_queue[i]);
_queue.length = 0;
}).catch(function(e)
{
console.error("[OPL2] AudioWorklet load failed:", e);
});
}

bus.register("opl2-reg-write", function(data)
{
send({ t: "w", r: data[0], v: data[1] });
}, null);
}

/**
* @constructor
* @param {!BusConnector} bus
Expand Down Expand Up @@ -809,7 +889,7 @@ function SpeakerWorkletDAC(bus, audio_context, mixer)
// Interface

this.mixer_connection = mixer.add_source(this.node_output, MIXER_SRC_DAC);
this.mixer_connection.set_gain_hidden(3);
this.mixer_connection.set_gain_hidden(1);

bus.register("dac-send-data", function(data)
{
Expand Down Expand Up @@ -908,7 +988,7 @@ function SpeakerBufferSourceDAC(bus, audio_context, mixer)
this.node_output = this.node_lowpass;

this.mixer_connection = mixer.add_source(this.node_output, MIXER_SRC_DAC);
this.mixer_connection.set_gain_hidden(3);
this.mixer_connection.set_gain_hidden(1);

bus.register("dac-send-data", function(data)
{
Expand Down
2 changes: 1 addition & 1 deletion src/browser/starter.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ V86.prototype.continue_init = async function(emulator, options)

if(!options.disable_speaker)
{
this.speaker_adapter = new SpeakerAdapter(this.bus);
this.speaker_adapter = new SpeakerAdapter(this.bus, options);
}

// ugly, but required for closure compiler compilation
Expand Down
1 change: 1 addition & 0 deletions src/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,4 @@ export const MIXER_CHANNEL_BOTH = 2;
export const MIXER_SRC_MASTER = 0;
export const MIXER_SRC_PCSPEAKER = 1;
export const MIXER_SRC_DAC = 2;
export const MIXER_SRC_OPL = 3;
Loading