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:
parent
d8e329b12c
commit
33cabfc348
9 changed files with 2119 additions and 1991 deletions
|
|
@ -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)
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue