Found a work-around for preventing browsers from "easing" (aka de-zippering) the sound from an oscillator when its frequency is altered; also attempted to provide a mechanism for starting the audio in response to a user-generated (eg, touchstart) event, in an effort to enable PC speaker sounds on an iPad, but iOS devices are a real pain in the butt....

This commit is contained in:
Jeff Parsons 2017-08-12 12:15:24 -07:00 committed by Jeff Parsons
commit 33cabfc348
9 changed files with 2119 additions and 1991 deletions

View file

@ -56,7 +56,7 @@ class ChipSet extends Component {
* model: eg, "5150", "5160", "5170", "deskpro386" (should be a member of ChipSet.MODELS)
* sw1: 8-character binary string representing the SW1 DIP switches (SW1[1-8]); see Switches Overview
* sw2: 8-character binary string representing the SW2 DIP switches (SW2[1-8]) (MODEL_5150 only)
* sound: true to enable (experimental) sound support (default); false to disable
* sound: true (or non-zero) to enable sounds (default), false (or 0) to disable; number used as initial gain
* scaleTimers: true to divide timer cycle counts by the CPU's cycle multiplier (default is false)
* floppies: array of floppy drive sizes in Kb (default is "[360, 360]" if no sw1 value provided)
* monitor: none|tv|color|mono|ega|vga (if no sw1 value provided, default is "ega" for 5170, "mono" otherwise)
@ -150,10 +150,15 @@ class ChipSet extends Component {
* I know about sound generation, using the API to make the same noises as the IBM PC speaker seems
* straightforward.
*
* To start, we create an audio context, unless the 'sound' parameter has been explicitly set to false.
* To start, we create an audio context, unless the 'sound' parameter has been explicitly set to false
* or 0; the boolean value true (along with any illegal number) now defaults to 0.5 instead of 1.0.
*/
this.fSpeaker = false;
if (parmsChipSet['sound']) {
this.gainInit = parmsChipSet['sound'];
if (this.gainInit) {
if (typeof this.gainInit != "number" || this.gainInit < 0 || this.gainInit > 1) {
this.gainInit = 0.5;
}
this.classAudio = this.contextAudio = null;
if (window) {
this.classAudio = window['AudioContext'] || window['webkitAudioContext'];
@ -191,10 +196,16 @@ class ChipSet extends Component {
this.cmp = cmp;
this.fpu = cmp.getMachineComponent("FPU");
this.setDIPSwitches(ChipSet.SWITCH_TYPE.FPU, this.fpu?1:0, true);
this.setDIPSwitches(ChipSet.SWITCH_TYPE.FPU, this.fpu? 1 : 0, true);
this.kbd = cmp.getMachineComponent("Keyboard");
var gain = cmp.getMachineParm("sound");
if (gain != null) {
var gainValue = +gain || 0;
this.gainInit = (gain == "true" || gainValue < 0 || gainValue > 1? 0.5 : gainValue);
}
/*
* This divisor is invariant, so we calculate it as soon as we're able to query the CPU's base speed.
*/
@ -4585,45 +4596,76 @@ class ChipSet extends Component {
setSpeaker(fOn)
{
if (this.contextAudio) {
try {
if (fOn !== undefined) {
this.fSpeaker = fOn;
} else {
fOn = !!(this.fSpeaker && this.cpu && this.cpu.isRunning());
}
var freq = Math.round(ChipSet.TIMER_TICKS_PER_SEC / this.getTimerInit(ChipSet.PIT0.TIMER2));
if (fOn !== undefined) {
this.fSpeaker = fOn;
} else {
fOn = !!(this.fSpeaker && this.cpu && this.cpu.isRunning());
}
var freq = Math.round(ChipSet.TIMER_TICKS_PER_SEC / this.getTimerInit(ChipSet.PIT0.TIMER2));
if (freq < 20 || freq > 20000) {
/*
* Treat frequencies outside the normal hearing range (below 20hz or above 20Khz) as a clever attempt
* to turn sound off; we have to explicitly turn the sound off in those cases, to prevent the Audio API
* from "easing" the audio to the target frequency and creating odd sound effects.
* Treat frequencies outside the normal hearing range (below 20hz or above 20Khz) as a clever
* attempt to turn sound off.
*/
if (freq < 20 || freq > 20000) fOn = false;
if (fOn) {
if (!this.oscillatorAudio) {
this.oscillatorAudio = this.contextAudio['createOscillator']();
this.gainAudio = this.contextAudio['createGain']();
this.oscillatorAudio['connect'](this.gainAudio);
this.gainAudio['connect'](this.contextAudio['destination']);
this.oscillatorAudio['frequency']['value'] = freq;
this.oscillatorAudio['type'] = "square";
this.oscillatorAudio['start'](0);
}
this.oscillatorAudio['frequency']['value'] = freq;
this.gainAudio['gain']['value'] = 1;
if (this.messageEnabled(Messages.SPEAKER)) this.printMessage("speaker on at " + freq + "hz", true);
} else if (this.oscillatorAudio) {
this.gainAudio['gain']['value'] = 0;
if (this.messageEnabled(Messages.SPEAKER)) this.printMessage("speaker off at " + freq + "hz", true);
}
} catch(e) {
this.notice("AudioContext exception: " + e.message);
this.contextAudio = null;
fOn = false;
}
if (fOn && this.startAudio()) {
/*
* Instead of setting the frequency's 'value' property directly, as we used to do, we use the
* setValueAtTime() method, with a time of zero, as a work-around to avoid the "easing" (aka
* "de-zippering") of the frequency that browsers like to do. Supposedly de-zippering is an
* attempt to avoid "pops" if the frequency is altered while the wave is still rising or falling.
* We'll see.
*/
this.oscillatorAudio['frequency']['setValueAtTime'](freq, 0);
this.gainAudio['gain']['value'] = this.gainInit;
if (this.messageEnabled(Messages.SPEAKER)) this.printMessage("speaker on at " + freq + "hz", true);
} else if (this.gainAudio) {
this.gainAudio['gain']['value'] = 0;
if (this.messageEnabled(Messages.SPEAKER)) this.printMessage("speaker off at " + freq + "hz", true);
}
} else if (fOn) {
this.printMessage("BEEP", Messages.SPEAKER);
}
}
/**
* startAudio(event)
*
* @this {ChipSet}
* @param {Event} [event] object from a 'touch' event, if any
* @return {boolean}
*/
startAudio(event)
{
if (this.contextAudio) {
/*
* NOTE: If the machine happened to enable its speaker *before* the user generated an event
* (eg, touchstart) that resulted in a call here, then we're too late -- at least as far as iOS
* devices are concerned, because those devices require the oscillator's start() method to be
* called in the context of a user-initiated event.
*
* One possible corrective solution would be to simply recreate the oscillator and gain objects
* every time we're called with an event object, but I'm not prepared to go that far yet....
*/
if (this.oscillatorAudio) return true;
try {
this.oscillatorAudio = this.contextAudio['createOscillator']();
this.gainAudio = this.contextAudio['createGain']();
this.oscillatorAudio['connect'](this.gainAudio);
this.gainAudio['connect'](this.contextAudio['destination']);
this.gainAudio['gain']['value'] = 0;
this.oscillatorAudio['type'] = "square";
this.oscillatorAudio['start'](0);
return true;
} catch(e) {
this.notice("AudioContext exception: " + e.message);
this.contextAudio = null;
}
}
return false;
}
/**
* messageBitsDMA(iChannel)
*

View file

@ -2503,6 +2503,15 @@ class Video extends Component {
if (this.kbd) this.captureTouch(Video.TOUCH.KEYGRID);
}
/*
* If no touch support was required or requested, we still want to do some minimal touch event processing;
* eg, notifying the ChipSet component whenever a touchstart occurs, so that it can enable audio in response
* to a user action on iOS devices.
*/
if (!this.nTouchConfig) {
this.captureTouch(Video.TOUCH.DEFAULT);
}
if (this.sFileURL) {
var sProgress = "Loading " + this.sFileURL + "...";
Web.getResource(this.sFileURL, null, true, function(sURL, sResponse, nErrorCode) {
@ -2783,16 +2792,25 @@ class Video extends Component {
if (control) {
var video = this;
if (!this.nTouchConfig) {
this.nTouchConfig = nTouchConfig;
control.addEventListener(
'touchstart',
function onTouchStart(event) { video.onTouchStart(event); },
false // we'll specify false for the 'useCapture' parameter for now...
);
if (nTouchConfig == Video.TOUCH.DEFAULT) {
return;
}
control.addEventListener(
'touchmove',
function onTouchMove(event) { video.onTouchMove(event); },
true
);
control.addEventListener(
'touchend',
function onTouchEnd(event) { video.onTouchEnd(event); },
@ -2822,7 +2840,6 @@ class Video extends Component {
// this.log("touch events captured");
this.nTouchConfig = nTouchConfig;
this.xTouch = this.yTouch = this.timeTouch = -1;
/*
@ -2879,6 +2896,8 @@ class Video extends Component {
onTouchStart(event)
{
if (DEBUG) this.printMessage("onTouchStart()");
this.chipset.startAudio(event);
if (this.nTouchConfig == Video.TOUCH.DEFAULT) return;
this.processTouchEvent(event, true);
}
@ -7432,8 +7451,9 @@ Video.cardSpecs[Video.CARD.VGA] = ["VGA", Card.CGA.CRTC.INDX.PORT, 0xB8000, 0x04
*/
Video.TOUCH = {
NONE: 0,
KEYGRID: 1,
MOUSE: 2
DEFAULT: 1,
KEYGRID: 2,
MOUSE: 3
};
/*