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()
}
/**