vail/static/keyer.mjs

683 lines
14 KiB
JavaScript
Raw Normal View History

2022-05-08 11:33:25 -06:00
/**
* A number of keyers.
*
* The document "All About Squeeze-Keying" by Karl Fischer, DJ5IL, was
* absolutely instrumental in correctly (I hope) implementing everything more
* advanced than the bug keyer.
*/
2021-04-26 14:33:48 -06:00
/** Silent period between words */
const PAUSE_WORD = -7
/** Silent period between letters */
const PAUSE_LETTER = -3
/** Silent period between dits and dash */
const PAUSE = -1
2022-05-08 14:49:41 -06:00
/** Length of a dit */
2021-04-26 14:33:48 -06:00
const DIT = 1
2022-05-08 14:49:41 -06:00
/** Length of a dah */
2021-04-26 14:33:48 -06:00
const DAH = 3
2022-05-08 11:33:25 -06:00
/**
* A time duration.
*
* JavaScript uses milliseconds in most (but not all) places.
* I've found it helpful to be able to multiply by a unit, so it's clear what's going on.
*
* @typedef {number} Duration
*/
/** @type {Duration} */
const Millisecond = 1
/** @type {Duration} */
const Second = 1000 * Millisecond
/** @type {Duration} */
const Minute = 60 * Second
/** @type {Duration} */
const Hour = 60 * Minute
2021-04-26 14:33:48 -06:00
const MorseMap = {
"\x04": ".-.-.", // End Of Transmission
"\x18": "........", // Cancel
"0": "-----",
"1": ".----",
"2": "..---",
"3": "...--",
"4": "....-",
"5": ".....",
"6": "-....",
"7": "--...",
"8": "---..",
"9": "----.",
"a": ".-",
"b": "-...",
"c": "-.-.",
"d": "-..",
"e": ".",
"f": "..-.",
"g": "--.",
"h": "....",
"i": "..",
"j": ".---",
"k": "-.-",
"l": ".-..",
"m": "--",
"n": "-.",
"o": "---",
"p": ".--.",
"q": "--.-",
"r": ".-.",
"s": "...",
"t": "-",
"u": "..-",
"v": "...-",
"w": ".--",
"x": "-..-",
"y": "-.--",
"z": "--..",
".": ".-.-.-",
",": "--..--",
"?": "..--..",
"'": ".----.",
"!": "-.-.--",
"/": "-..-.",
"(": "-.--.",
")": "-.--.-",
"&": ".-...",
":": "---...",
";": "---...",
"=": "-...-",
"+": ".-.-.",
"-": "-....-",
"_": "--..-.",
"\"": ".-..-.",
"$": "...-..-",
"@": ".--.-.",
}
/**
* Return the inverse of the input.
* If you give it dit, it returns dah, and vice-versa.
*
* @param ditdah What to invert
* @returns The inverse of ditdah
*/
2022-05-08 11:33:25 -06:00
function not(ditdah) {
if (ditdah == DIT) {
return DAH
}
return DIT
}
2022-05-08 14:49:41 -06:00
/**
* Queue Set: A Set you can shift and pop.
*
* Performance of this implementation may be bad for large sets.
*/
class QSet extends Set {
shift() {
let r = [...this].shift()
this.delete(r)
return r
}
pop() {
let r = [...this].pop()
this.delete(r)
return r
}
}
2021-04-26 14:33:48 -06:00
/**
* A callback to start or stop transmission
*
* @callback TxControl
*/
2022-05-08 11:33:25 -06:00
/**
* A straight keyer.
*
* This is one or more relays wired in parallel. Each relay has an associated
* input key. You press any key, and it starts transmitting until all keys are
* released.
*/
class StraightKeyer {
/**
* @param {TxControl} beginTxFunc Callback to begin transmitting
* @param {TxControl} endTxFunc Callback to end transmitting
*/
constructor(beginTxFunc, endTxFunc) {
this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.Reset()
}
/**
* Returns a list of names for keys supported by this keyer.
*
* @returns {Array.<string>} A list of key names
*/
KeyNames() {
return ["Key"]
}
/**
* Reset state and stop all transmissions.
*/
Reset() {
this.endTxFunc()
this.txRelays = []
}
/**
* Returns the state of a single transmit relay.
*
* If n is not provided, return the state of all relays wired in parallel.
*
* @param {number} n Relay number
* @returns {bool} True if relay is closed
*/
TxClosed(n=null) {
if (n == null) {
return this.txRelays.some(Boolean)
}
return this.txRelays[n]
}
/**
* Close a transmit relay.
*
* In most of these keyers, you have multiple things that can transmit. In
* the circuit, they'd all be wired together in parallel. We instead keep
* track of relay state here, and start or stop transmitting based on the
* logical of of all relays.
*
* @param {number} n Relay number
* @param {bool} closed True if relay should be closed
*/
Tx(n, closed) {
let wasClosed = this.TxClosed()
this.txRelays[n] = closed
let nowClosed = this.TxClosed()
if (wasClosed != nowClosed) {
if (nowClosed) {
this.beginTxFunc()
} else {
this.endTxFunc()
}
}
}
/**
* React to a key being pressed.
*
* @param {number} key Which key was pressed
* @param {bool} pressed True if the key was pressed
*/
Key(key, pressed) {
this.Tx(key, pressed)
}
}
/**
* A "Cootie" or "Double Speed Key" is just two straight keys in parallel.
*/
class CootieKeyer extends StraightKeyer {
KeyNames() {
return ["Key", "Key"]
}
}
/**
* A Vibroplex "Bug".
*
* Left key send dits over and over until you let go.
* Right key works just like a stright key.
*/
class BugKeyer extends StraightKeyer {
KeyNames() {
return ["· ", "Key"]
}
Reset() {
super.Reset()
this.SetDitDuration(100 * Millisecond)
if (this.pulseTimer) {
clearInterval(this.pulseTimer)
this.pulseTimer = null
}
this.keyPressed = []
}
/**
* Set the duration of dit.
*
* @param {Duration} d New dit duration
*/
SetDitDuration(d) {
this.ditDuration = d
}
Key(key, pressed) {
this.keyPressed[key] = pressed
if (key == 0) {
this.beginPulsing()
} else {
super.Key(key, pressed)
}
}
/**
* Begin a pulse if it hasn't already begun
*/
beginPulsing() {
if (!this.pulseTimer) {
this.pulse()
}
}
pulse() {
if (this.TxClosed(0)) {
// If we were transmitting, pause
this.Tx(0, false)
} else if (this.keyPressed[0]) {
// If the key was pressed, transmit
this.Tx(0, true)
} else {
// If the key wasn't pressed, stop pulsing
this.pulseTimer = null
return
}
this.pulseTimer = setTimeout(() => this.pulse(), this.ditDuration)
}
}
/**
* Electronic Bug Keyer
*
* Repeats both dits and dahs, ensuring proper pauses.
*
* I think the original ElBug Keyers did not have two paddles, so I've taken the
* liberty of making it so that whatever you pressed last is what gets repeated,
* similar to a modern computer keyboard.
*/
class ElBugKeyer extends BugKeyer {
KeyNames() {
return ["· ", ""]
}
Reset() {
super.Reset()
this.lastPressed = -1
}
Key(key, pressed) {
this.keyPressed[key] = pressed
if (pressed) {
this.lastPressed = key
} else {
this.lastPressed = this.keyPressed.findIndex(Boolean)
}
this.beginPulsing()
}
2022-05-08 14:49:41 -06:00
/**
* Computes transmission duration for a given key.
*
* @param {number} key Key to calculate
* @returns {Duration} Duration of transmission
*/
keyDuration(key) {
switch (key) {
case 0:
return DIT * this.ditDuration
case 1:
return DAH * this.ditDuration
}
return 0
}
2022-05-08 11:33:25 -06:00
/**
* Calculates the duration of the next transmission to send.
*
* If there is nothing to send, returns 0.
*
* @returns {Duration} Duration of next transmission
*/
nextTxDuration() {
2022-05-08 14:49:41 -06:00
if (!this.keyPressed.some(Boolean)) {
return 0
2022-05-08 11:33:25 -06:00
}
2022-05-08 14:49:41 -06:00
return this.keyDuration(this.lastPressed)
2022-05-08 11:33:25 -06:00
}
pulse() {
let nextPulse = 0
// This keyer only drives one transmit relay
if (this.TxClosed()) {
// If we're transmitting at all, pause
nextPulse = this.ditDuration
2022-05-08 14:49:41 -06:00
this.Tx(0, false)
} else if ((nextPulse = this.nextTxDuration()) > 0) {
2022-05-08 11:33:25 -06:00
this.Tx(0, true)
}
if (nextPulse) {
this.pulseTimer = setTimeout(() => this.pulse(), nextPulse)
} else {
this.pulseTimer = null
}
}
}
/**
* Single dot memory keyer.
*
* If you tap dit while a dah is sending, it queues up a dit to send, even if
* the dit key is no longer being held at the start of the next cycle.
*/
class SingleDotKeyer extends ElBugKeyer {
Reset() {
super.Reset()
2022-05-08 14:49:41 -06:00
this.queue = new QSet()
2022-05-08 11:33:25 -06:00
}
Key(key, pressed) {
2022-05-08 14:49:41 -06:00
if (pressed && (key == 0)) {
this.queue.add(0)
2022-05-08 11:33:25 -06:00
}
2022-05-08 14:49:41 -06:00
super.Key(key, pressed)
2022-05-08 11:33:25 -06:00
}
nextTxDuration() {
2022-05-08 14:49:41 -06:00
let key = this.queue.shift()
if (key != null) {
return this.keyDuration(key)
2022-05-08 11:33:25 -06:00
}
return super.nextTxDuration()
}
}
2022-05-08 14:49:41 -06:00
class UltimaticKeyer extends SingleDotKeyer {
Key(key, pressed) {
if (pressed) {
this.queue.add(key)
}
super.Key(key, pressed)
}
}
2021-04-26 14:33:48 -06:00
/**
2021-04-27 17:30:16 -06:00
* Keyer class. This handles iambic and straight key input.
2021-04-26 14:33:48 -06:00
*
* This will handle the following things that people appear to want with iambic input:
*
* - Typematic: you hold the key down and it repeats evenly-spaced tones
* - Typeahead: if you hit a key while it's still transmitting the last-entered one, it queues up your next entered one
*/
2022-05-08 11:33:25 -06:00
class OldKeyer {
2021-04-26 14:33:48 -06:00
/**
* Create a Keyer
2021-04-26 14:33:48 -06:00
*
2021-04-27 18:37:25 -06:00
* @param {TxControl} beginTxFunc Callback to begin transmitting
* @param {TxControl} endTxFunc Callback to end transmitting
2021-04-26 14:33:48 -06:00
* @param {number} intervalDuration Dit duration (milliseconds)
2021-04-27 18:37:25 -06:00
* @param {number} pauseMultiplier How long to stretch out inter-letter and inter-word pauses
2021-04-26 14:33:48 -06:00
*/
constructor(beginTxFunc, endTxFunc, {intervalDuration=100, pauseMultiplier=1}={}) {
2021-04-26 14:33:48 -06:00
this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.intervalDuration = intervalDuration
this.pauseMultiplier = pauseMultiplier
2021-04-26 14:33:48 -06:00
this.ditDown = false
this.dahDown = false
this.typeahead = false
this.iambicModeB = true
2021-04-26 14:33:48 -06:00
this.last = null
this.queue = []
this.pulseTimer = null
}
pulse() {
if (this.queue.length == 0) {
let next = this.typematic()
if (next) {
// Barkeep! Another round!
this.Enqueue(next)
} else {
// Nothing left on the queue, stop the machine
this.pulseTimer = null
return
}
}
let next = this.queue.shift()
if (next < 0) {
next *= -1
if (next > 1) {
// Don't adjust spacing within a letter
next *= this.pauseMultiplier
} else {
this.endTxFunc()
2022-04-24 17:13:56 -06:00
if (this.txChart) {
this.txChart.Add(Date.now(), 0)
}
}
2021-04-26 14:33:48 -06:00
} else {
this.last = next
this.beginTxFunc()
2022-04-24 17:13:56 -06:00
if (this.txChart) {
this.txChart.Add(Date.now(), 1)
}
2021-04-26 14:33:48 -06:00
}
this.pulseTimer = setTimeout(() => this.pulse(), next * this.intervalDuration)
}
maybePulse() {
// If there's no timer running right now, restart the pulse
if (!this.pulseTimer) {
this.pulse()
}
}
typematic() {
if (this.ditDown && this.dahDown) {
this.modeBQueue = this.last
2022-05-08 11:33:25 -06:00
this.last = not(this.last)
2021-04-26 14:33:48 -06:00
} else if (this.ditDown) {
this.modeBQueue = null
2021-04-26 14:33:48 -06:00
this.last = DIT
} else if (this.dahDown) {
this.modeBQueue = null
2021-04-26 14:33:48 -06:00
this.last = DAH
} else if (this.modeBQueue && this.iambicModeB) {
this.last = this.modeBQueue
this.modeBQueue = null
2021-04-26 14:33:48 -06:00
} else {
this.last = null
this.modeBQueue = null
2021-04-26 14:33:48 -06:00
}
return this.last
}
/**
* Return true if we are currently playing out something
*/
Busy() {
return this.pulseTimer
}
2021-04-27 17:30:16 -06:00
2021-04-26 14:33:48 -06:00
/**
* Set a new dit interval (transmission rate)
*
* @param {number} duration Dit duration (milliseconds)
*/
SetIntervalDuration(duration) {
this.intervalDuration = duration
}
/**
* Set a new pause multiplier.
*
* This slows down the inter-letter and inter-word pauses,
* which can aid in learning.
*
* @param {number} multiplier Pause multiplier
*/
SetPauseMultiplier(multiplier) {
this.pauseMultiplier = multiplier
}
/**
* Set Iambic mode B.
*
* Near as I can tell, B sends one more tone than was entered, when
* both keys are held down.
* This logic happens in the typematic code.
*
* Dit key
*
* Dah key
*
* Mode A output
*
* Mode B output
*
* @param {boolean} value True to set mode to B
*/
SetIambicModeB(value) {
this.iambicModeB = Boolean(value)
}
/**
* Enable/disable typeahead.
*
* Typeahead maintains a key buffer, so you can key in dits and dahs faster than the
* Iambic keyer can play them out.
*
* Some people apparently expect this behavior, and have trouble if it isn't enabled.
* For others, having this enabled makes it feel like they have a "phantom keyer"
* entering keys they did not send.
*
* @param value True to enable typeahead
*/
SetTypeahead(value) {
this.typeahead = value
}
2021-04-28 14:28:59 -06:00
/**
* Delete anything left on the queue.
*/
Flush() {
this.queue.splice(0)
}
2021-04-26 14:33:48 -06:00
/**
* Add to the output queue, and start processing the queue if it's not currently being processed.
*
* @param {number} key A duration, in dits. Negative durations are silent.
*/
Enqueue(key) {
this.queue.push(key)
if (key > 0) {
this.queue.push(PAUSE)
}
this.maybePulse()
}
2021-04-27 18:37:25 -06:00
/**
* Enqueue a morse code string (eg "... --- ...")
*
* @param {string} ms String to enqueue
*/
2021-04-26 14:33:48 -06:00
EnqueueMorseString(ms) {
for (let mc of ms) {
switch (mc) {
case ".":
this.Enqueue(DIT)
break
case "-":
this.Enqueue(DAH)
break
case " ":
this.Enqueue(PAUSE_LETTER)
break
}
}
}
2021-04-27 18:37:25 -06:00
/**
* Enqueue an ASCII string (eg "SOS help")
*
* @param {string} s String to enqueue
*/
EnqueueAsciiString(s, {pauseLetter = PAUSE_LETTER, pauseWord = PAUSE_WORD} = {}) {
2021-04-26 14:33:48 -06:00
for (let c of s.toLowerCase()) {
let m = MorseMap[c]
if (m) {
this.EnqueueMorseString(m)
this.Enqueue(pauseLetter)
2021-04-26 14:33:48 -06:00
continue
}
switch (c) {
case " ":
case "\n":
case "\t":
this.Enqueue(pauseWord)
2021-04-26 14:33:48 -06:00
break
default:
console.warn("Unable to encode '" + c + "'!")
break
}
}
}
/**
2021-04-27 17:30:16 -06:00
* Do something to the straight key
*
* @param down True if key was pressed
*/
Straight(down) {
if (down) {
this.beginTxFunc()
} else {
this.endTxFunc()
2021-04-26 14:33:48 -06:00
}
2021-04-27 17:30:16 -06:00
}
/**
* Do something to the dit key
*
* @param down True if key was pressed
*/
Dit(down) {
this.ditDown = down
2022-04-23 21:23:05 -06:00
if (down) {
if (this.typeahead
|| !this.Busy()
|| (this.iambicModeB && (this.last == DAH))) {
this.Enqueue(DIT)
}
2021-04-27 17:30:16 -06:00
}
}
2021-04-26 14:33:48 -06:00
2021-04-27 17:30:16 -06:00
/**
* Do something to the dah key
*
* @param down True if key was pressed
*/
Dah(down) {
this.dahDown = down
2022-04-23 21:23:05 -06:00
if (down) {
if (this.typeahead
|| !this.Busy()
|| (this.iambicModeB && (this.last == DIT))) {
this.Enqueue(DAH)
}
2021-04-26 14:33:48 -06:00
}
2021-04-27 17:30:16 -06:00
}
2021-04-26 14:33:48 -06:00
}
2022-05-08 14:49:41 -06:00
export {StraightKeyer, CootieKeyer, BugKeyer, ElBugKeyer, SingleDotKeyer, UltimaticKeyer}