diff --git a/static/inputs.mjs b/static/inputs.mjs index be025e6..b0a8adb 100644 --- a/static/inputs.mjs +++ b/static/inputs.mjs @@ -1,7 +1,15 @@ - -export class HTML { +class Input { constructor(keyer) { this.keyer = keyer + } + SetIntervalDuration(delay) { + // Nothing + } +} + +export class HTML extends Input{ + constructor(keyer) { + super(keyer) // Listen to HTML buttons for (let e of document.querySelectorAll("button.key")) { @@ -30,9 +38,9 @@ export class HTML { } } -export class Keyboard { +export class Keyboard extends Input{ constructor(keyer) { - this.keyer = keyer + super(keyer) // Listen for keystrokes document.addEventListener("keydown", e => this.keyboard(e)) @@ -92,10 +100,11 @@ export class Keyboard { } } -export class MIDI { +export class MIDI extends Input{ constructor(keyer) { - this.keyer = keyer + super(keyer) + this.midiAccess = {outputs: []} // stub while we wait for async stuff if (navigator.requestMIDIAccess) { this.midiInit() } @@ -108,6 +117,14 @@ export class MIDI { this.midiStateChange() } + SetIntervalDuration(delay) { + // Send the Vail adapter the current iambic delay setting + for (let output of this.midiAccess.outputs.values()) { + // MIDI only supports 7-bit values, so we have to divide it by two + output.send([0x8B, 0x01, delay/2]) + } + } + midiStateChange(event) { // Go through this.midiAccess.inputs and only listen on new things for (let input of this.midiAccess.inputs.values()) { @@ -119,7 +136,7 @@ export class MIDI { // Tell the Vail adapter to disable keyboard events: we can do MIDI! for (let output of this.midiAccess.outputs.values()) { - output.send([0x80, 0x00, 0x00]) // Stop playing low C + output.send([0x8B, 0x00, 0x00]) // Turn off keyboard mode } } @@ -156,9 +173,9 @@ export class MIDI { } } -export class Gamepad { +export class Gamepad extends Input{ constructor(keyer) { - this.keyer = keyer + super(keyer) // Set up for gamepad input window.addEventListener("gamepadconnected", e => this.gamepadConnected(e)) diff --git a/static/morse.mjs b/static/morse.mjs index b1641aa..cfab03d 100644 --- a/static/morse.mjs +++ b/static/morse.mjs @@ -130,8 +130,9 @@ class Keyer { if (next > 1) { // Don't adjust spacing within a letter next *= this.pauseMultiplier + } else { + this.endTxFunc() } - this.endTxFunc() } else { this.last = next this.beginTxFunc() @@ -329,31 +330,62 @@ class Keyer { } } +/** + * A (mostly) virtual class defining a buzzer. + */ class Buzzer { - // Buzzers keep two oscillators: one high and one low. - // They generate a continuous waveform, - // and we change the gain to turn the pitches off and on. - // - // This also implements a very quick ramp-up and ramp-down in gain, - // in order to avoid "pops" (square wave overtones) - // that happen with instant changes in gain. + /** + * Signal an error + */ + Error() { + console.log("Error") + } - constructor({txGain=0.6, highFreq=660, lowFreq=550, errorFreq=30} = {}) { - this.txGain = txGain + /** + * Begin buzzing at time + * + * @param {boolean} tx Transmit or receive tone + * @param {number} when Time to begin, in ms (null=now) + */ + Buzz(tx, when = null) { + console.log("Buzz", tx, when) + } + + /** + * End buzzing at time + * + * @param {boolean} tx Transmit or receive tone + * @param {number} when Time to end, in ms (null=now) + */ + Silence(tx, when = null) { + console.log("Silence", tx, when) + } + + /** + * Buzz for a duration at time + * + * @param {boolean} tx Transmit or receive tone + * @param {number} when Time to begin (ms since 1970-01-01Z, null=now) + * @param {number} duration Duration of buzz (ms) + */ + BuzzDuration(tx, when, duration) { + this.Buzz(tx, when) + this.Silence(tx, when + duration) + } +} + +class AudioBuzzer extends Buzzer { + constructor(errorFreq=30) { + super() this.ac = new AudioContext() - this.ramp = 0.005 // Lead-in and lead-out time to avoid popping. This one is in seconds. - - this.lowGain = this.create(lowFreq) - this.highGain = this.create(highFreq) - this.errorGain = this.create(errorFreq, "square") - //this.noiseGain = this.whiteNoise() - this.ac.resume() .then(() => { document.querySelector("#muted").classList.add("hidden") }) + this.errorGain = this.create(errorFreq, "square") + this.errorFreq = errorFreq } create(frequency, type = "sine") { @@ -368,6 +400,53 @@ class Buzzer { return gain } + ready() { + return this.ac.state == "running" + } + + Error() { + this.errorGain.gain.setTargetAtTime(0.5, this.ac.currentTime, 0.001) + this.errorGain.gain.setTargetAtTime(0, this.ac.currentTime + 0.2, 0.001) + } + + /** + * Convert clock time to AudioContext time + * + * @param {number} when Clock time in ms + * @return {number} AudioContext offset time + */ + acTime(when) { + if (!when) { + return this.ac.currentTime + } + + let acOffset = Date.now() - this.ac.currentTime * 1000 + let acTime = (when - acOffset) / 1000 + return acTime + } +} + +class ToneBuzzer extends AudioBuzzer { + // Buzzers keep two oscillators: one high and one low. + // They generate a continuous waveform, + // and we change the gain to turn the pitches off and on. + // + // This also implements a very quick ramp-up and ramp-down in gain, + // in order to avoid "pops" (square wave overtones) + // that happen with instant changes in gain. + + constructor({txGain=0.6, highFreq=660, lowFreq=550} = {}) { + super() + this.txGain = txGain + + this.ac = new AudioContext() + this.ramp = 0.005 // Lead-in and lead-out time to avoid popping. This one is in seconds. + + this.lowGain = this.create(lowFreq) + this.highGain = this.create(highFreq) + //this.noiseGain = this.whiteNoise() + } + // Generate some noise to prevent the browser from putting us to sleep whiteNoise() { let bufferSize = 17 * this.ac.sampleRate @@ -404,21 +483,6 @@ class Buzzer { } } - /** - * Convert clock time to AudioContext time - * - * @param {number} when Clock time in ms - * @return {number} AudioContext offset time - */ - acTime(when) { - if (!when) { - return this.ac.currentTime - } - - let acOffset = Date.now() - this.ac.currentTime * 1000 - let acTime = (when - acOffset) / 1000 - return acTime - } /** * Set gain @@ -429,14 +493,6 @@ class Buzzer { this.txGain = gain } - /** - * Play an error tone - */ - ErrorTone() { - this.errorGain.gain.setTargetAtTime(this.txGain * 0.5, this.ac.currentTime, 0.001) - this.errorGain.gain.setTargetAtTime(0, this.ac.currentTime + 0.2, 0.001) - } - /** * Begin buzzing at time * @@ -444,14 +500,6 @@ class Buzzer { * @param {number} when Time to begin, in ms (null=now) */ Buzz(tx, when = null) { - if (!tx) { - let recv = document.querySelector("#recv") - let ms = when - Date.now() - setTimeout(e => { - recv.classList.add("rx") - }, ms) - } - let gain = this.gain(tx) let acWhen = this.acTime(when) this.ac.resume() @@ -467,34 +515,14 @@ class Buzzer { * @param {number} when Time to end, in ms (null=now) */ Silence(tx, when = null) { - if (!tx) { - let recv = document.querySelector("#recv") - let ms = when - Date.now() - setTimeout(e => { - recv.classList.remove("rx") - }, ms) - } - let gain = this.gain(tx) let acWhen = this.acTime(when) gain.setTargetAtTime(0, acWhen, this.ramp) } - - /** - * Buzz for a duration at time - * - * @param {boolean} tx Transmit or receive tone - * @param {number} when Time to begin (ms since 1970-01-01Z, null=now) - * @param {number} duration Duration of buzz (ms) - */ - BuzzDuration(tx, when, duration) { - this.Buzz(tx, when) - this.Silence(tx, when + duration) - } } -class TelegraphBuzzer extends Buzzer{ +class TelegraphBuzzer extends AudioBuzzer{ constructor(gain=0.6) { super() @@ -530,5 +558,30 @@ class TelegraphBuzzer extends Buzzer{ } } +class Lamp extends Buzzer { + constructor() { + super() + this.lamp = document.querySelector("#recv") + } + + Buzz(tx, when=0) { + if (tx) return + + let ms = when - Date.now() + setTimeout(e => { + recv.classList.add("rx") + }, ms) + } + Silence(tx, when=0) { + if (tx) return + + let recv = document.querySelector("#recv") + let ms = when - Date.now() + setTimeout(e => { + recv.classList.remove("rx") + }, ms) + } +} + export {DIT, DAH, PAUSE, PAUSE_WORD, PAUSE_LETTER} -export {Keyer, Buzzer, TelegraphBuzzer} +export {Keyer, ToneBuzzer, TelegraphBuzzer, Lamp} diff --git a/static/vail.mjs b/static/vail.mjs index c021df0..0f77241 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -2,10 +2,9 @@ import * as Morse from "./morse.mjs" import * as Inputs from "./inputs.mjs" import * as Repeaters from "./repeaters.mjs" -const DefaultRepeater = "General Chaos" +const DefaultRepeater = "General" const Millisecond = 1 const Second = 1000 * Millisecond -const Minute = 60 * Second /** * Pop up a message, using an MDL snackbar. @@ -30,14 +29,24 @@ class VailClient { this.lagTimes = [0] this.rxDurations = [0] this.clockOffset = null // How badly our clock is off of the server's - this.rxDelay = 0 // Milliseconds to add to incoming timestamps + this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps this.beginTxTime = null // Time when we began transmitting this.debug = localStorage.debug // Make helpers - this.buzzer = new Morse.Buzzer() + this.lamp = new Morse.Lamp() + this.buzzer = new Morse.ToneBuzzer() this.keyer = new Morse.Keyer(() => this.beginTx(), () => this.endTx()) - this.roboKeyer = new Morse.Keyer(() => this.buzzer.Buzz(), () => this.buzzer.Silence()) + this.roboKeyer = new Morse.Keyer( + () => { + this.buzzer.Buzz() + this.lamp.Buzz() + }, + () => { + this.buzzer.Silence() + this.lamp.Silence() + } + ) // Set up various input methods this.inputs = Inputs.SetupAll(this.keyer) @@ -59,6 +68,9 @@ class VailClient { this.inputInit("#iambic-duration", e => { this.keyer.SetIntervalDuration(e.target.value) this.roboKeyer.SetIntervalDuration(e.target.value) + for (let i of Object.values(this.inputs)) { + i.SetIntervalDuration(e.target.value) + } }) this.inputInit("#rx-delay", e => { this.rxDelay = Number(e.target.value) @@ -88,7 +100,7 @@ class VailClient { if (enable) { this.buzzer = new Morse.TelegraphBuzzer() } else { - this.buzzer = new Morse.Buzzer() + this.buzzer = new Morse.ToneBuzzer() } } @@ -226,7 +238,7 @@ class VailClient { */ error(msg) { toast(msg) - this.buzzer.ErrorTone() + this.buzzer.Error() } /** @@ -267,6 +279,7 @@ class VailClient { } this.buzzer.BuzzDuration(false, when, duration) + this.lamp.BuzzDuration(false, when, duration) this.rxDurations.unshift(duration) this.rxDurations.splice(20, 2)