From b7de5cf8cb92ac654ec31628e4253458294ce817 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 22 May 2022 21:37:36 -0600 Subject: [PATCH] Output MIDI notes + refactoring --- static/index.html | 2 +- static/inputs.mjs | 59 ++++++--- static/keyers.mjs | 42 +++++-- static/{buzzer.mjs => outputs.mjs} | 188 +++++++++++++++++++++++++---- static/vail.css | 4 +- static/vail.mjs | 58 ++++----- 6 files changed, 273 insertions(+), 80 deletions(-) rename static/{buzzer.mjs => outputs.mjs} (67%) diff --git a/static/index.html b/static/index.html index 07f5d82..970e203 100644 --- a/static/index.html +++ b/static/index.html @@ -72,7 +72,7 @@
- + diff --git a/static/inputs.mjs b/static/inputs.mjs index 362f4d3..290e62c 100644 --- a/static/inputs.mjs +++ b/static/inputs.mjs @@ -2,9 +2,14 @@ class Input { constructor(keyer) { this.keyer = keyer } + SetDitDuration(delay) { // Nothing } + + SetKeyerMode(mode) { + // Nothing + } } export class HTML extends Input{ @@ -117,11 +122,17 @@ export class MIDI extends Input{ this.midiStateChange() } - SetIntervalDuration(delay) { + SetDitDuration(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]) + output.send([0xB0, 0x01, delay/2]) + } + } + + SetKeyerMode(mode) { + for (let output of this.midiAccess.outputs.values()) { + output.send([0xC0, mode]) } } @@ -136,7 +147,7 @@ export class MIDI extends Input{ // Tell the Vail adapter to disable keyboard events: we can do MIDI! for (let output of this.midiAccess.outputs.values()) { - output.send([0x8B, 0x00, 0x00]) // Turn off keyboard mode + output.send([0xB0, 0x00, 0x00]) // Turn off keyboard mode } } @@ -229,16 +240,36 @@ export class Gamepad extends Input{ } } -/** - * Set up all input methods - * - * @param keyer Keyer object for everyone to use - */ -export function SetupAll(keyer) { - return { - HTML: new HTML(keyer), - Keyboard: new Keyboard(keyer), - MIDI: new MIDI(keyer), - Gamepad: new Gamepad(keyer), +class Collection { + constructor(keyer) { + this.html =new HTML(keyer) + this.keyboard =new Keyboard(keyer) + this.midi =new MIDI(keyer) + this.gamepad =new Gamepad(keyer) + this.collection = [this.html, this.keyboard, this.midi, this.gamepad] + } + + /** + * Set duration of all inputs + * + * @param duration Duration to set + */ + SetDitDuration(duration) { + for (let e of this.collection) { + e.SetDitDuration(duration) + } + } + + /** + * Set keyer mode of all inputs + * + * @param mode Keyer mode to set + */ + SetKeyerMode(mode) { + for (let e of this.collection) { + e.SetKeyerMode(mode) + } } } + +export {Collection} diff --git a/static/keyers.mjs b/static/keyers.mjs index 2bed7d8..e9d3874 100644 --- a/static/keyers.mjs +++ b/static/keyers.mjs @@ -53,10 +53,17 @@ class QSet extends Set { } /** - * A callback to start or stop transmission + * Definition of a transmitter type. * - * @callback TxControl + * The VailClient class implements this. */ +class Transmitter { + /** Begin transmitting */ + BeginTx() {} + + /** End transmitting */ + EndTx() {} +} /** * A straight keyer. @@ -67,12 +74,10 @@ class QSet extends Set { */ class StraightKeyer { /** - * @param {TxControl} beginTxFunc Callback to begin transmitting - * @param {TxControl} endTxFunc Callback to end transmitting + * @param {Transmitter} output Transmitter object */ - constructor(beginTxFunc, endTxFunc) { - this.beginTxFunc = beginTxFunc - this.endTxFunc = endTxFunc + constructor(output) { + this.output = output this.Reset() } @@ -89,7 +94,7 @@ class StraightKeyer { * Reset state and stop all transmissions. */ Reset() { - this.endTxFunc() + this.output.EndTx() this.txRelays = [] } @@ -140,9 +145,9 @@ class StraightKeyer { if (wasClosed != nowClosed) { if (nowClosed) { - this.beginTxFunc() + this.output.BeginTx() } else { - this.endTxFunc() + this.output.EndTx() } } } @@ -471,6 +476,19 @@ const Keyers = { robo: RoboKeyer.Keyer, } -export { - Keyers, +const Numbers = { + straight: 1, + cootie: 1, + bug: 2, + elbug: 3, + singledot: 4, + ultimatic: 5, + iambic: 6, + iambica: 7, + iambicb: 8, + keyahead: 9, +} + +export { + Keyers, Numbers, } diff --git a/static/buzzer.mjs b/static/outputs.mjs similarity index 67% rename from static/buzzer.mjs rename to static/outputs.mjs index 925c990..6cfa56d 100644 --- a/static/buzzer.mjs +++ b/static/outputs.mjs @@ -27,7 +27,9 @@ const Second = 1000 * Millisecond const OscillatorRampDuration = 5*Millisecond console.warn("Chrome will now complain about an AudioContext not being allowed to start. This is normal, and there is no way to make Chrome stop complaining about this.") -const BuzzerAudioContext = new AudioContext() +const BuzzerAudioContext = new AudioContext({ + latencyHint: 0, +}) /** * Compute the special "Audio Context" time * @@ -42,13 +44,6 @@ function BuzzerAudioContextTime(when) { return Math.max(when - acOffset, 0) / Second } -/** - * Block until the audio system is able to start making noise. - */ -async function Ready() { - await BuzzerAudioContext.resume() -} - class Oscillator { /** * Create a new oscillator, and encase it in a Gain for control. @@ -155,7 +150,7 @@ class Buzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to begin, in ms (0=now) */ - Buzz(tx, when=0) { + async Buzz(tx, when=0) { console.log("Buzz", tx, when) } @@ -165,7 +160,7 @@ class Buzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to end, in ms (0=now) */ - Silence(tx, when=0) { + async Silence(tx, when=0) { console.log("Silence", tx, when) } @@ -210,6 +205,10 @@ class ToneBuzzer extends AudioBuzzer { this.rxOsc = new Oscillator(lowFreq, txGain) this.txOsc = new Oscillator(highFreq, txGain) + + // Keep the speaker going always. This keeps the browser from "swapping out" our audio context. + this.bgOsc = new Oscillator(1, 0.001) + this.bgOsc.SoundAt() } /** @@ -218,7 +217,7 @@ class ToneBuzzer extends AudioBuzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to begin, in ms (0=now) */ - Buzz(tx, when = null) { + async Buzz(tx, when = null) { let osc = tx?this.txOsc:this.rxOsc osc.SoundAt(when) } @@ -229,7 +228,7 @@ class ToneBuzzer extends AudioBuzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to end, in ms (0=now) */ - Silence(tx, when = null) { + async Silence(tx, when = null) { let osc = tx?this.txOsc:this.rxOsc osc.HushAt(when) } @@ -249,7 +248,7 @@ class TelegraphBuzzer extends AudioBuzzer{ this.openSample = new Sample("telegraph-b.mp3") } - Buzz(tx, when=0) { + async Buzz(tx, when=0) { if (tx) { this.hum.SoundAt(when) } else { @@ -257,7 +256,7 @@ class TelegraphBuzzer extends AudioBuzzer{ } } - Silence(tx ,when=0) { + async Silence(tx ,when=0) { if (tx) { this.hum.HushAt(when) } else { @@ -266,29 +265,174 @@ class TelegraphBuzzer extends AudioBuzzer{ } } -class Lamp extends Buzzer { - constructor(element) { +class LampBuzzer extends Buzzer { + constructor() { super() - this.element = element + this.elements = document.querySelectorAll(".recv-lamp") } - Buzz(tx, when=0) { + async Buzz(tx, when=0) { if (tx) return let ms = when?when - Date.now():0 setTimeout( () =>{ - this.element.classList.add("rx") + for (let e of this.elements) { + e.classList.add("rx") + } }, ms, ) } - Silence(tx, when=0) { + async Silence(tx, when=0) { if (tx) return let ms = when?when - Date.now():0 - setTimeout(() => this.element.classList.remove("rx"), ms) + setTimeout( + () => { + for (let e of this.elements) { + e.classList.remove("rx") + } + }, + ms, + ) } } -export {Ready, ToneBuzzer, TelegraphBuzzer, Lamp} +class MIDIBuzzer extends Buzzer { + constructor() { + super() + this.SetNote(69) // A4; 440Hz + + this.midiAccess = {outputs: []} // stub while we wait for async stuff + if (navigator.requestMIDIAccess) { + this.midiInit() + } + } + + async midiInit(access) { + this.outputs = new Set() + this.midiAccess = await navigator.requestMIDIAccess() + this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e)) + this.midiStateChange() + } + + midiStateChange(event) { + let newOutputs = new Set() + for (let output of this.midiAccess.outputs.values()) { + console.log(output.state) + if ((output.state != "connected") || (output.name.includes("Through"))) { + continue + } + newOutputs.add(output) + } + this.outputs = newOutputs + } + + sendAt(when, message) { + let ms = when?when - Date.now():0 + setTimeout( + () => { + for (let output of this.outputs) { + output.send(message) + } + }, + ms, + ) + } + + async Buzz(tx, when=0) { + if (tx) { + return + } + + this.sendAt(when, [0x90, this.note, 0x7f]) + } + + async Silence(tx, when=0) { + if (tx) { + return + } + + this.sendAt(when, [0x80, this.note, 0x7f]) + } + + /* + * Set note to transmit + */ + SetNote(tx, note) { + if (tx) { + return + } + this.note = note + } +} + +/** + * Block until the audio system is able to start making noise. + */ +async function AudioReady() { + await BuzzerAudioContext.resume() +} + +class Collection { + constructor() { + this.tone = new ToneBuzzer() + this.telegraph = new TelegraphBuzzer() + this.lamp = new LampBuzzer() + this.midi = new MIDIBuzzer() + this.collection = new Set([this.tone, this.lamp, this.midi]) + } + + /** + * Set the audio output type. + * + * @param {string} audioType "telegraph" for telegraph mode, otherwise tone mode + */ + SetAudioType(audioType) { + this.collection.delete(this.telegraph) + this.collection.delete(this.tone) + if (audioType == "telegraph") { + this.collection.add(this.telegraph) + } else { + this.collection.add(this.tone) + } + } + + /** + * Buzz all outputs. + * + * @param tx True if transmitting + */ + Buzz(tx=False) { + for (let b of this.collection) { + b.Buzz(tx) + } + } + + /** + * Silence all outputs. + * + * @param tx True if transmitting + */ + Silence(tx=False) { + for (let b of this.collection) { + b.Silence(tx) + } + } + + /** + * Buzz for a certain duration at a certain time + * + * @param tx True if transmitting + * @param when Time to begin + * @param duration How long to buzz + */ + BuzzDuration(tx, when, duration) { + for (let b of this.collection) { + b.BuzzDuration(tx, when, duration) + } + } +} + +export {AudioReady, Collection} diff --git a/static/vail.css b/static/vail.css index 4723afc..f35723b 100644 --- a/static/vail.css +++ b/static/vail.css @@ -16,7 +16,7 @@ -webkit-user-select: none; /* 2022-04-26 Safari still needs this */ } -#recv.rx { +.recv-lamp.rx { background-color: orange; } @@ -83,4 +83,4 @@ code { #charts canvas { height: 0.5em; width: 100%; -} \ No newline at end of file +} diff --git a/static/vail.mjs b/static/vail.mjs index 6473e4a..508ed80 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -1,5 +1,5 @@ -import {Keyers} from "./keyers.mjs" -import * as Buzzer from "./buzzer.mjs" +import * as Keyers from "./keyers.mjs" +import * as Outputs from "./outputs.mjs" import * as Inputs from "./inputs.mjs" import * as Repeaters from "./repeaters.mjs" import * as Chart from "./chart.mjs" @@ -10,7 +10,7 @@ const Second = 1000 * Millisecond const Minute = 60 * Second /** - * Pop up a message, using an notification.. + * Pop up a message, using an notification. * * @param {string} msg Message to display */ @@ -37,16 +37,17 @@ class VailClient { this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps this.beginTxTime = null // Time when we began transmitting - // Make helpers - this.lamp = new Buzzer.Lamp(document.querySelector("#recv")) - this.buzzer = new Buzzer.ToneBuzzer() - this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx()) - this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx()) - this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence()) + // Outputs + this.outputs = new Outputs.Collection() + + // Keyers + this.straightKeyer = new Keyers.Keyers.straight(this) + this.keyer = new Keyers.Keyers.straight(this) + this.roboKeyer = new Keyers.Keyers.robo(() => this.Buzz(), () => this.Silence()) // Set up various input methods // Send this as the keyer so we can intercept dit and dah events for charts - this.inputs = Inputs.SetupAll(this) + this.inputs = new Inputs.Collection(this) // Maximize button for (let e of document.querySelectorAll("button.maximize")) { @@ -69,9 +70,7 @@ class VailClient { } this.keyer.SetDitDuration(this.ditDuration) this.roboKeyer.SetDitDuration(this.ditDuration) - for (let i of Object.values(this.inputs)) { - i.SetDitDuration(this.ditDuration) - } + this.inputs.SetDitDuration(this.ditDuration) }) this.inputInit("#rx-delay", e => { this.rxDelay = e.target.value * Second @@ -89,7 +88,7 @@ class VailClient { this.setTimingCharts(true) // Turn off the "muted" symbol when we can start making noise - Buzzer.Ready() + Outputs.AudioReady() .then(() => { console.log("Audio context ready") document.querySelector("#muted").classList.add("is-hidden") @@ -117,12 +116,13 @@ class VailClient { } setKeyer(keyerName) { - let newKeyerClass = Keyers[keyerName] + let newKeyerClass = Keyers.Keyers[keyerName] + let newKeyerNumber = Keyers.Numbers[keyerName] if (!newKeyerClass) { console.error("Keyer not found", keyerName) return } - let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx()) + let newKeyer = new newKeyerClass(this) let i = 0 for (let keyName of newKeyer.KeyNames()) { let e = document.querySelector(`.key[data-key="${i}"]`) @@ -132,24 +132,23 @@ class VailClient { this.keyer.Release() this.keyer = newKeyer + this.inputs.SetKeyerMode(newKeyerNumber) + document.querySelector("#keyer-rate").dispatchEvent(new Event("input")) } Buzz() { - this.buzzer.Buzz() - this.lamp.Buzz() + this.outputs.Buzz(false) if (this.rxChart) this.rxChart.Set(1) } Silence() { - this.buzzer.Silence() - this.lamp.Silence() + this.outputs.Silence() if (this.rxChart) this.rxChart.Set(0) } BuzzDuration(tx, when, duration) { - this.buzzer.BuzzDuration(tx, when, duration) - this.lamp.BuzzDuration(tx, when, duration) + this.outputs.BuzzDuration(tx, when, duration) let chart = tx?this.txChart:this.rxChart if (chart) { @@ -163,10 +162,11 @@ class VailClient { * * Called from the keyer. */ - beginTx() { + BeginTx() { this.beginTxTime = Date.now() - this.buzzer.Buzz(true) + this.outputs.Buzz(true) if (this.txChart) this.txChart.Set(1) + } /** @@ -174,13 +174,13 @@ class VailClient { * * Called from the keyer */ - endTx() { + EndTx() { if (!this.beginTxTime) { return } let endTxTime = Date.now() let duration = endTxTime - this.beginTxTime - this.buzzer.Silence(true) + this.outputs.Silence(true) this.repeater.Transmit(this.beginTxTime, duration) this.beginTxTime = null if (this.txChart) this.txChart.Set(0) @@ -222,10 +222,10 @@ class VailClient { */ setTelegraphBuzzer(enable) { if (enable) { - this.buzzer = new Buzzer.TelegraphBuzzer() + this.outputs.SetAudioType("telegraph") toast("Telegraphs only make sound when receiving!") } else { - this.buzzer = new Buzzer.ToneBuzzer() + this.outputs.SetAudioType() } } @@ -343,7 +343,7 @@ class VailClient { */ error(msg) { toast(msg) - this.buzzer.Error() + this.outputs.Error() } /**