mirror of https://github.com/nealey/vail.git
Multiple fixes
* The little light now shows for telegraph sounds * Telegraph no longer plays the "off" sound constantly * The little light now shows for Fortunes repeaters * MIDI changes * No longer sends note off to disable keyboard mode * Now sends dit length/2 as MIDI control 1
This commit is contained in:
parent
870c318d96
commit
fc537d7341
|
@ -1,7 +1,15 @@
|
||||||
|
class Input {
|
||||||
export class HTML {
|
|
||||||
constructor(keyer) {
|
constructor(keyer) {
|
||||||
this.keyer = keyer
|
this.keyer = keyer
|
||||||
|
}
|
||||||
|
SetIntervalDuration(delay) {
|
||||||
|
// Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HTML extends Input{
|
||||||
|
constructor(keyer) {
|
||||||
|
super(keyer)
|
||||||
|
|
||||||
// Listen to HTML buttons
|
// Listen to HTML buttons
|
||||||
for (let e of document.querySelectorAll("button.key")) {
|
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) {
|
constructor(keyer) {
|
||||||
this.keyer = keyer
|
super(keyer)
|
||||||
|
|
||||||
// Listen for keystrokes
|
// Listen for keystrokes
|
||||||
document.addEventListener("keydown", e => this.keyboard(e))
|
document.addEventListener("keydown", e => this.keyboard(e))
|
||||||
|
@ -92,10 +100,11 @@ export class Keyboard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MIDI {
|
export class MIDI extends Input{
|
||||||
constructor(keyer) {
|
constructor(keyer) {
|
||||||
this.keyer = keyer
|
super(keyer)
|
||||||
|
|
||||||
|
this.midiAccess = {outputs: []} // stub while we wait for async stuff
|
||||||
if (navigator.requestMIDIAccess) {
|
if (navigator.requestMIDIAccess) {
|
||||||
this.midiInit()
|
this.midiInit()
|
||||||
}
|
}
|
||||||
|
@ -108,6 +117,14 @@ export class MIDI {
|
||||||
this.midiStateChange()
|
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) {
|
midiStateChange(event) {
|
||||||
// Go through this.midiAccess.inputs and only listen on new things
|
// Go through this.midiAccess.inputs and only listen on new things
|
||||||
for (let input of this.midiAccess.inputs.values()) {
|
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!
|
// Tell the Vail adapter to disable keyboard events: we can do MIDI!
|
||||||
for (let output of this.midiAccess.outputs.values()) {
|
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) {
|
constructor(keyer) {
|
||||||
this.keyer = keyer
|
super(keyer)
|
||||||
|
|
||||||
// Set up for gamepad input
|
// Set up for gamepad input
|
||||||
window.addEventListener("gamepadconnected", e => this.gamepadConnected(e))
|
window.addEventListener("gamepadconnected", e => this.gamepadConnected(e))
|
||||||
|
|
193
static/morse.mjs
193
static/morse.mjs
|
@ -130,8 +130,9 @@ class Keyer {
|
||||||
if (next > 1) {
|
if (next > 1) {
|
||||||
// Don't adjust spacing within a letter
|
// Don't adjust spacing within a letter
|
||||||
next *= this.pauseMultiplier
|
next *= this.pauseMultiplier
|
||||||
|
} else {
|
||||||
|
this.endTxFunc()
|
||||||
}
|
}
|
||||||
this.endTxFunc()
|
|
||||||
} else {
|
} else {
|
||||||
this.last = next
|
this.last = next
|
||||||
this.beginTxFunc()
|
this.beginTxFunc()
|
||||||
|
@ -329,31 +330,62 @@ class Keyer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A (mostly) virtual class defining a buzzer.
|
||||||
|
*/
|
||||||
class Buzzer {
|
class Buzzer {
|
||||||
// Buzzers keep two oscillators: one high and one low.
|
/**
|
||||||
// They generate a continuous waveform,
|
* Signal an error
|
||||||
// and we change the gain to turn the pitches off and on.
|
*/
|
||||||
//
|
Error() {
|
||||||
// This also implements a very quick ramp-up and ramp-down in gain,
|
console.log("Error")
|
||||||
// in order to avoid "pops" (square wave overtones)
|
}
|
||||||
// that happen with instant changes in gain.
|
|
||||||
|
|
||||||
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.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()
|
this.ac.resume()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
document.querySelector("#muted").classList.add("hidden")
|
document.querySelector("#muted").classList.add("hidden")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.errorGain = this.create(errorFreq, "square")
|
||||||
|
this.errorFreq = errorFreq
|
||||||
}
|
}
|
||||||
|
|
||||||
create(frequency, type = "sine") {
|
create(frequency, type = "sine") {
|
||||||
|
@ -368,6 +400,53 @@ class Buzzer {
|
||||||
return gain
|
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
|
// Generate some noise to prevent the browser from putting us to sleep
|
||||||
whiteNoise() {
|
whiteNoise() {
|
||||||
let bufferSize = 17 * this.ac.sampleRate
|
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
|
* Set gain
|
||||||
|
@ -429,14 +493,6 @@ class Buzzer {
|
||||||
this.txGain = gain
|
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
|
* Begin buzzing at time
|
||||||
*
|
*
|
||||||
|
@ -444,14 +500,6 @@ class Buzzer {
|
||||||
* @param {number} when Time to begin, in ms (null=now)
|
* @param {number} when Time to begin, in ms (null=now)
|
||||||
*/
|
*/
|
||||||
Buzz(tx, when = null) {
|
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 gain = this.gain(tx)
|
||||||
let acWhen = this.acTime(when)
|
let acWhen = this.acTime(when)
|
||||||
this.ac.resume()
|
this.ac.resume()
|
||||||
|
@ -467,34 +515,14 @@ class Buzzer {
|
||||||
* @param {number} when Time to end, in ms (null=now)
|
* @param {number} when Time to end, in ms (null=now)
|
||||||
*/
|
*/
|
||||||
Silence(tx, when = null) {
|
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 gain = this.gain(tx)
|
||||||
let acWhen = this.acTime(when)
|
let acWhen = this.acTime(when)
|
||||||
|
|
||||||
gain.setTargetAtTime(0, acWhen, this.ramp)
|
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) {
|
constructor(gain=0.6) {
|
||||||
super()
|
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 {DIT, DAH, PAUSE, PAUSE_WORD, PAUSE_LETTER}
|
||||||
export {Keyer, Buzzer, TelegraphBuzzer}
|
export {Keyer, ToneBuzzer, TelegraphBuzzer, Lamp}
|
||||||
|
|
|
@ -2,10 +2,9 @@ import * as Morse from "./morse.mjs"
|
||||||
import * as Inputs from "./inputs.mjs"
|
import * as Inputs from "./inputs.mjs"
|
||||||
import * as Repeaters from "./repeaters.mjs"
|
import * as Repeaters from "./repeaters.mjs"
|
||||||
|
|
||||||
const DefaultRepeater = "General Chaos"
|
const DefaultRepeater = "General"
|
||||||
const Millisecond = 1
|
const Millisecond = 1
|
||||||
const Second = 1000 * Millisecond
|
const Second = 1000 * Millisecond
|
||||||
const Minute = 60 * Second
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pop up a message, using an MDL snackbar.
|
* Pop up a message, using an MDL snackbar.
|
||||||
|
@ -30,14 +29,24 @@ class VailClient {
|
||||||
this.lagTimes = [0]
|
this.lagTimes = [0]
|
||||||
this.rxDurations = [0]
|
this.rxDurations = [0]
|
||||||
this.clockOffset = null // How badly our clock is off of the server's
|
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.beginTxTime = null // Time when we began transmitting
|
||||||
this.debug = localStorage.debug
|
this.debug = localStorage.debug
|
||||||
|
|
||||||
// Make helpers
|
// 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.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
|
// Set up various input methods
|
||||||
this.inputs = Inputs.SetupAll(this.keyer)
|
this.inputs = Inputs.SetupAll(this.keyer)
|
||||||
|
@ -59,6 +68,9 @@ class VailClient {
|
||||||
this.inputInit("#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)
|
||||||
|
for (let i of Object.values(this.inputs)) {
|
||||||
|
i.SetIntervalDuration(e.target.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.inputInit("#rx-delay", e => {
|
this.inputInit("#rx-delay", e => {
|
||||||
this.rxDelay = Number(e.target.value)
|
this.rxDelay = Number(e.target.value)
|
||||||
|
@ -88,7 +100,7 @@ class VailClient {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
this.buzzer = new Morse.TelegraphBuzzer()
|
this.buzzer = new Morse.TelegraphBuzzer()
|
||||||
} else {
|
} else {
|
||||||
this.buzzer = new Morse.Buzzer()
|
this.buzzer = new Morse.ToneBuzzer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +238,7 @@ class VailClient {
|
||||||
*/
|
*/
|
||||||
error(msg) {
|
error(msg) {
|
||||||
toast(msg)
|
toast(msg)
|
||||||
this.buzzer.ErrorTone()
|
this.buzzer.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -267,6 +279,7 @@ class VailClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buzzer.BuzzDuration(false, when, duration)
|
this.buzzer.BuzzDuration(false, when, duration)
|
||||||
|
this.lamp.BuzzDuration(false, when, duration)
|
||||||
|
|
||||||
this.rxDurations.unshift(duration)
|
this.rxDurations.unshift(duration)
|
||||||
this.rxDurations.splice(20, 2)
|
this.rxDurations.splice(20, 2)
|
||||||
|
|
Loading…
Reference in New Issue