Many changes

* MIDI
  * Send MIDI key release for v2 of vail adapter
  * Now handles hot-plugging MIDI devices!
* Iambic input
  * Typeahead is now a toggle
  * Implemented iambic mode A, also as a toggle (#31)
* If you set the repeater to "debug", it spews out debugging messages
* Made it a bit easier on myself to update my instance
This commit is contained in:
Neale Pickett 2022-04-19 22:41:09 -06:00
parent ca9f51c621
commit 74ad07174a
6 changed files with 102 additions and 27 deletions

View File

@ -1,9 +1,11 @@
#! /bin/sh #! /bin/sh
cd $(dirname $0)
case "$1" in case "$1" in
-prod|--prod) -prod|--prod)
echo "Push to main branch, then update stack." echo "Push to main branch, then update stack."
#rsync -va static melville.woozle.org:/srv/vail/ docker -H ssh://melville.woozle.org service update --image ghcr.io/nealey/vail:main melville_vail
;; ;;
"") "")
rsync -va static/ melville.woozle.org:/srv/vail/testing/ rsync -va static/ melville.woozle.org:/srv/vail/testing/

View File

@ -218,7 +218,7 @@
</div> </div>
<div class="mdl-card__supporting-text"> <div class="mdl-card__supporting-text">
<p> <p>
Dit length (iambic): Iambic Dit length:
<output id="iambic-duration-value"></output>ms <output id="iambic-duration-value"></output>ms
/ /
<output id="iambic-duration-wpm"></output> WPM <output id="iambic-duration-wpm"></output> WPM
@ -230,6 +230,16 @@
max="255" max="255"
value="100"> value="100">
</p> </p>
<p>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="iambic-mode-b">
<input type="checkbox" id="iambic-mode-b" class="mdl-switch__input" checked>
<span class="mdl-switch__label">Iambic mode B</span>
</label>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="iambic-typeahead">
<input type="checkbox" id="iambic-typeahead" class="mdl-switch__input" checked>
<span class="mdl-switch__label">Iambic typeahead</span>
</label>
</p>
<p> <p>
Receive delay: Receive delay:
<output id="rx-delay-value"></output>ms <output id="rx-delay-value"></output>ms

View File

@ -102,16 +102,25 @@ export class MIDI {
} }
async midiInit(access) { async midiInit(access) {
this.inputs = []
this.midiAccess = await navigator.requestMIDIAccess() this.midiAccess = await navigator.requestMIDIAccess()
for (let input of this.midiAccess.inputs.values()) {
input.addEventListener("midimessage", e => this.midiMessage(e))
}
this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e)) this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e))
this.midiStateChange()
} }
midiStateChange(event) { midiStateChange(event) {
// XXX: it's not entirely clear how to handle new devices showing up. // Go through this.midiAccess.inputs and only listen on new things
// XXX: possibly we go through this.midiAccess.inputs and somehow only listen on new things for (let input of this.midiAccess.inputs.values()) {
if (!this.inputs.includes(input)) {
input.addEventListener("midimessage", e => this.midiMessage(e))
this.inputs.push(input)
}
}
// 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
}
} }
midiMessage(event) { midiMessage(event) {

View File

@ -90,7 +90,7 @@ if (!window.AudioContext) {
*/ */
class Keyer { class Keyer {
/** /**
* Create an Keyer * Create a Keyer
* *
* @param {TxControl} beginTxFunc Callback to begin transmitting * @param {TxControl} beginTxFunc Callback to begin transmitting
* @param {TxControl} endTxFunc Callback to end transmitting * @param {TxControl} endTxFunc Callback to end transmitting
@ -104,6 +104,8 @@ class Keyer {
this.pauseMultiplier = pauseMultiplier this.pauseMultiplier = pauseMultiplier
this.ditDown = false this.ditDown = false
this.dahDown = false this.dahDown = false
this.typeahead = false
this.iambicModeB = true
this.last = null this.last = null
this.queue = [] this.queue = []
this.pulseTimer = null this.pulseTimer = null
@ -146,10 +148,14 @@ class Keyer {
typematic() { typematic() {
if (this.ditDown && this.dahDown) { if (this.ditDown && this.dahDown) {
if (this.last == DIT) { if (this.iambicModeB) {
this.last = DAH if (this.last == DIT) {
this.last = DAH
} else {
this.last = DIT
}
} else { } else {
this.last = DIT this.last = this.last // Mode A = keep on truckin'
} }
} else if (this.ditDown) { } else if (this.ditDown) {
this.last = DIT this.last = DIT
@ -189,6 +195,34 @@ class Keyer {
this.pauseMultiplier = multiplier this.pauseMultiplier = multiplier
} }
/**
* Set Iambic mode B.
*
* If true, holding both keys will alternate between dit and dah.
* If false, holding both keys sends whatever key was depressed first.
*
* @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
}
/** /**
* Delete anything left on the queue. * Delete anything left on the queue.
*/ */
@ -277,8 +311,8 @@ class Keyer {
*/ */
Dit(down) { Dit(down) {
this.ditDown = down this.ditDown = down
if (down) { if (down && this.typeahead || !this.Busy()) {
this.Enqueue(DIT) this.Enqueue(DIT, this.typeahead)
} }
} }
@ -289,8 +323,8 @@ class Keyer {
*/ */
Dah(down) { Dah(down) {
this.dahDown = down this.dahDown = down
if (down) { if (down && this.typeahead || !this.Busy()) {
this.Enqueue(DAH) this.Enqueue(DAH, this.typeahead)
} }
} }
} }

View File

@ -45,6 +45,7 @@ export class Vail {
console.error(err, jmsg) console.error(err, jmsg)
return return
} }
let beginTxTime = msg[0] let beginTxTime = msg[0]
let durations = msg.slice(1) let durations = msg.slice(1)
@ -62,9 +63,16 @@ export class Vail {
this.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration) this.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration)
this.lagDurations.splice(20, 2) this.lagDurations.splice(20, 2)
this.rx(0, 0, this.stats()) this.rx(0, 0, this.stats())
if (this.name == "debug") {
console.debug("Vail.wsMessage() SQUELCH", msg)
}
return return
} }
if (this.name == "debug") {
console.debug("Vail.wsMessage()", msg)
}
// The very first packet is the server telling us the current time // The very first packet is the server telling us the current time
if (durations.length == 0) { if (durations.length == 0) {
if (this.clockOffset == 0) { if (this.clockOffset == 0) {

View File

@ -64,15 +64,20 @@ class VailClient {
e.addEventListener("click", e => this.test()) e.addEventListener("click", e => this.test())
} }
// Set up sliders // Set up inputs
this.sliderInit("#iambic-duration", e => { this.inputInit("#iambic-duration", e => {
this.keyer.SetIntervalDuration(e.target.value) this.keyer.SetIntervalDuration(e.target.value)
this.roboKeyer.SetIntervalDuration(e.target.value) this.roboKeyer.SetIntervalDuration(e.target.value)
}) })
this.sliderInit("#rx-delay", e => { this.inputInit("#rx-delay", e => {
this.rxDelay = Number(e.target.value) this.rxDelay = Number(e.target.value)
}) })
this.inputInit("#iambic-mode-b", e => {
this.keyer.SetIambicModeB(e.target.checked)
})
this.inputInit("#iambic-typeahead", e => {
this.keyer.SetTypeahead(e.target.checked)
})
// Fill in the name of our repeater // Fill in the name of our repeater
let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim())) let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
@ -163,33 +168,40 @@ class VailClient {
} }
/** /**
* Set up a slider. * Set up an input.
* *
* This reads any previously saved value and sets the slider to that. * This reads any previously saved value and sets the input value to that.
* When the slider is updated, it saves the value it's updated to, * When the input is updated, it saves the value it's updated to,
* and calls the provided callback with the new value. * and calls the provided callback with the new value.
* *
* @param {string} selector CSS path to the element * @param {string} selector CSS path to the element
* @param {function} callback Callback to call with any new value that is set * @param {function} callback Callback to call with any new value that is set
*/ */
sliderInit(selector, callback) { inputInit(selector, callback) {
let element = document.querySelector(selector) let element = document.querySelector(selector)
if (!element) { if (!element) {
return return
} }
let storedValue = localStorage[element.id] let storedValue = localStorage[element.id]
if (storedValue) { if (storedValue != null) {
element.value = storedValue element.value = storedValue
element.checked = JSON.parse(storedValue)
} }
let outputElement = document.querySelector(selector + "-value") let outputElement = document.querySelector(selector + "-value")
let outputWpmElement = document.querySelector(selector + "-wpm") let outputWpmElement = document.querySelector(selector + "-wpm")
element.addEventListener("input", e => { element.addEventListener("input", e => {
localStorage[element.id] = element.value let value = element.value
if (element.hasAttribute("checked")) {
value = element.checked
}
localStorage[element.id] = value
if (outputElement) { if (outputElement) {
outputElement.value = element.value outputElement.value = value
} }
if (outputWpmElement) { if (outputWpmElement) {
outputWpmElement.value = (1200 / element.value).toFixed(1) outputWpmElement.value = (1200 / value).toFixed(1)
} }
if (callback) { if (callback) {
callback(e) callback(e)