mirror of https://github.com/nealey/vail.git
Fail mode for toasts
This commit is contained in:
parent
1a088e71d1
commit
f987cbdb46
|
@ -14,7 +14,8 @@
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="icon" href="vail.png" sizes="256x256" type="image/png">
|
<link rel="icon" href="vail.png" sizes="256x256" type="image/png">
|
||||||
<link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml">
|
<link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml">
|
||||||
<script src="dev.js"></script>
|
<script type="module" src="dev.mjs"></script>
|
||||||
|
<script type="module" src="fortune.mjs"></script>
|
||||||
<link rel="stylesheet" href="vail.css">
|
<link rel="stylesheet" href="vail.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -166,14 +167,26 @@
|
||||||
CK
|
CK
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button id="fortune" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
||||||
|
Fortune
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr class="mdl-card__supporting-text">
|
||||||
<td class="mdl-card__supporting-text">
|
<td>
|
||||||
<p>
|
<p>
|
||||||
Check (CK) round-trip times and audio functionality
|
Check (CK) round-trip times and audio functionality
|
||||||
by sending "CK" to the repeater and playing the returned signal.
|
by sending "CK" to the repeater and playing the returned signal.
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<p>
|
||||||
|
Fetch a fortune and play it locally.
|
||||||
|
This can help practice copying (hearing) Morse code,
|
||||||
|
without having to involve another person.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,308 +1,4 @@
|
||||||
// jshint asi:true
|
import * as Morse from "./morse.mjs"
|
||||||
|
|
||||||
const lowFreq = 660
|
|
||||||
const highFreq = lowFreq * 6 / 5 // Perfect minor third
|
|
||||||
const errorFreq = 30
|
|
||||||
|
|
||||||
const PAUSE = -1
|
|
||||||
const DIT = 1
|
|
||||||
const DAH = 3
|
|
||||||
|
|
||||||
// iOS kludge
|
|
||||||
if (!window.AudioContext) {
|
|
||||||
window.AudioContext = window.webkitAudioContext
|
|
||||||
}
|
|
||||||
|
|
||||||
function toast(msg) {
|
|
||||||
let el = document.querySelector("#snackbar")
|
|
||||||
el.MaterialSnackbar.showSnackbar({
|
|
||||||
message: msg,
|
|
||||||
timeout: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A callback to start or stop transmission
|
|
||||||
*
|
|
||||||
* @callback TxControl
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iambic input class.
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
class Iambic {
|
|
||||||
/**
|
|
||||||
* Create an Iambic control
|
|
||||||
*
|
|
||||||
* @param {TxControl} beginTxFunc Function to begin transmitting
|
|
||||||
* @param {TxControl} endTxFunc Function to end transmitting
|
|
||||||
* @param {number} intervalDuration Dit duration (milliseconds)
|
|
||||||
*/
|
|
||||||
constructor(beginTxFunc, endTxFunc, intervalDuration=100) {
|
|
||||||
this.beginTxFunc = beginTxFunc
|
|
||||||
this.endTxFunc = endTxFunc
|
|
||||||
this.intervalDuration = intervalDuration
|
|
||||||
this.ditDown = false
|
|
||||||
this.dahDown = false
|
|
||||||
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 = next * -1
|
|
||||||
this.endTxFunc()
|
|
||||||
} else {
|
|
||||||
this.last = next
|
|
||||||
this.beginTxFunc()
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
if (this.last == DIT) {
|
|
||||||
this.last = DAH
|
|
||||||
} else {
|
|
||||||
this.last = DIT
|
|
||||||
}
|
|
||||||
} else if (this.ditDown) {
|
|
||||||
this.last = DIT
|
|
||||||
} else if (this.dahDown) {
|
|
||||||
this.last = DAH
|
|
||||||
} else {
|
|
||||||
this.last = null
|
|
||||||
}
|
|
||||||
return this.last
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a new dit interval (transmission rate)
|
|
||||||
*
|
|
||||||
* @param {number} duration Dit duration (milliseconds)
|
|
||||||
*/
|
|
||||||
SetIntervalDuration(duration) {
|
|
||||||
this.intervalDuration = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add to the output queue, and start processing the queue if it's not currently being processed.
|
|
||||||
*
|
|
||||||
* @param {number} key DIT or DAH
|
|
||||||
*/
|
|
||||||
Enqueue(key) {
|
|
||||||
this.queue.push(key)
|
|
||||||
this.queue.push(PAUSE)
|
|
||||||
this.maybePulse()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edge trigger on key press or release
|
|
||||||
*
|
|
||||||
* @param {number} key DIT or DAH
|
|
||||||
* @param {boolean} down True if key was pressed, false if released
|
|
||||||
*/
|
|
||||||
Key(key, down) {
|
|
||||||
if (key == DIT) {
|
|
||||||
this.ditDown = down
|
|
||||||
} else if (key == DAH) {
|
|
||||||
this.dahDown = down
|
|
||||||
}
|
|
||||||
|
|
||||||
if (down) {
|
|
||||||
this.Enqueue(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Buzzer {
|
|
||||||
// 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) {
|
|
||||||
this.txGain = txGain
|
|
||||||
|
|
||||||
this.ac = new AudioContext()
|
|
||||||
|
|
||||||
this.lowGain = this.create(lowFreq)
|
|
||||||
this.highGain = this.create(highFreq)
|
|
||||||
this.errorGain = this.create(errorFreq, "square")
|
|
||||||
this.noiseGain = this.whiteNoise()
|
|
||||||
|
|
||||||
this.ac.resume()
|
|
||||||
.then(() => {
|
|
||||||
document.querySelector("#muted").classList.add("hidden")
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
create(frequency, type = "sine") {
|
|
||||||
let gain = this.ac.createGain()
|
|
||||||
gain.connect(this.ac.destination)
|
|
||||||
gain.gain.value = 0
|
|
||||||
let osc = this.ac.createOscillator()
|
|
||||||
osc.type = type
|
|
||||||
osc.connect(gain)
|
|
||||||
osc.frequency.value = frequency
|
|
||||||
osc.start()
|
|
||||||
return gain
|
|
||||||
}
|
|
||||||
|
|
||||||
whiteNoise() {
|
|
||||||
let bufferSize = 17 * this.ac.sampleRate
|
|
||||||
let noiseBuffer = this.ac.createBuffer(1, bufferSize, this.ac.sampleRate)
|
|
||||||
let output = noiseBuffer.getChannelData(0)
|
|
||||||
for (let i = 0; i < bufferSize; i++) {
|
|
||||||
output[i] = Math.random() * 2 - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let whiteNoise = this.ac.createBufferSource();
|
|
||||||
whiteNoise.buffer = noiseBuffer;
|
|
||||||
whiteNoise.loop = true;
|
|
||||||
whiteNoise.start(0);
|
|
||||||
|
|
||||||
let filter = this.ac.createBiquadFilter()
|
|
||||||
filter.type = "lowpass"
|
|
||||||
filter.frequency.value = 100
|
|
||||||
|
|
||||||
let gain = this.ac.createGain()
|
|
||||||
gain.gain.value = 0.1
|
|
||||||
|
|
||||||
whiteNoise.connect(filter)
|
|
||||||
filter.connect(gain)
|
|
||||||
gain.connect(this.ac.destination)
|
|
||||||
|
|
||||||
return gain
|
|
||||||
}
|
|
||||||
|
|
||||||
gain(high) {
|
|
||||||
if (high) {
|
|
||||||
return this.highGain.gain
|
|
||||||
} else {
|
|
||||||
return this.lowGain.gain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* @param {number} gain Value (0-1)
|
|
||||||
*/
|
|
||||||
SetGain(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
|
|
||||||
*
|
|
||||||
* @param {boolean} tx Transmit or receive tone
|
|
||||||
* @param {number} when Time to begin, in ms (null=now)
|
|
||||||
*/
|
|
||||||
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 acWhen = this.acTime(when)
|
|
||||||
this.ac.resume()
|
|
||||||
.then(() => {
|
|
||||||
gain.setTargetAtTime(this.txGain, acWhen, 0.001)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
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 acWhen = this.acTime(when)
|
|
||||||
|
|
||||||
gain.setTargetAtTime(0, acWhen, 0.001)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Buzz for a duration at time
|
|
||||||
*
|
|
||||||
* @param {boolean} high High or low pitched tone
|
|
||||||
* @param {number} when Time to begin (ms since 1970-01-01Z, null=now)
|
|
||||||
* @param {number} duration Duration of buzz (ms)
|
|
||||||
*/
|
|
||||||
BuzzDuration(high, when, duration) {
|
|
||||||
this.Buzz(high, when)
|
|
||||||
this.Silence(high, when + duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Vail {
|
class Vail {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -333,8 +29,8 @@ class Vail {
|
||||||
document.addEventListener("keyup", e => this.keyboard(e))
|
document.addEventListener("keyup", e => this.keyboard(e))
|
||||||
|
|
||||||
// Make helpers
|
// Make helpers
|
||||||
this.iambic = new Iambic(() => this.beginTx(), () => this.endTx())
|
this.iambic = new Morse.Iambic(() => this.beginTx(), () => this.endTx())
|
||||||
this.buzzer = new Buzzer()
|
this.buzzer = new Morse.Buzzer()
|
||||||
|
|
||||||
// Listen for slider values
|
// Listen for slider values
|
||||||
this.inputInit("#iambic-duration", e => this.iambic.SetIntervalDuration(e.target.value))
|
this.inputInit("#iambic-duration", e => this.iambic.SetIntervalDuration(e.target.value))
|
||||||
|
@ -415,10 +111,10 @@ class Vail {
|
||||||
this.straightKey(begin)
|
this.straightKey(begin)
|
||||||
break
|
break
|
||||||
case 1: // C#
|
case 1: // C#
|
||||||
this.iambic.Key(DIT, begin)
|
this.iambic.Key(Morse.DIT, begin)
|
||||||
break
|
break
|
||||||
case 2: // D
|
case 2: // D
|
||||||
this.iambic.Key(DAH, begin)
|
this.iambic.Key(Morse.DAH, begin)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
|
@ -426,7 +122,7 @@ class Vail {
|
||||||
}
|
}
|
||||||
|
|
||||||
error(msg) {
|
error(msg) {
|
||||||
toast(msg)
|
Morse.toast(msg)
|
||||||
this.buzzer.ErrorTone()
|
this.buzzer.ErrorTone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,11 +253,11 @@ class Vail {
|
||||||
}
|
}
|
||||||
|
|
||||||
iambicDit(begin) {
|
iambicDit(begin) {
|
||||||
this.iambic.Key(DIT, begin)
|
this.iambic.Key(Morse.DIT, begin)
|
||||||
}
|
}
|
||||||
|
|
||||||
iambicDah(begin) {
|
iambicDah(begin) {
|
||||||
this.iambic.Key(DAH, begin)
|
this.iambic.Key(Morse.DAH, begin)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard(event) {
|
keyboard(event) {
|
||||||
|
@ -701,7 +397,7 @@ function vailInit() {
|
||||||
window.app = new Vail()
|
window.app = new Vail()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
toast(err)
|
Morse.toast(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import * as Morse from "./morse.mjs"
|
||||||
|
|
||||||
|
class Fortune {
|
||||||
|
constructor() {
|
||||||
|
let button = document.querySelector("#fortune")
|
||||||
|
button.addEventListener("click", () => this.go())
|
||||||
|
this.buzzer = new Morse.Buzzer({highFreq: 440})
|
||||||
|
this.iambic = new Morse.Iambic(() => this.buzzer.Buzz(true), () => this.buzzer.Silence(true))
|
||||||
|
document.querySelector("#iambic-duration").addEventListener("input", e => this.iambic.SetIntervalDuration(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async go() {
|
||||||
|
let resp = await fetch("https://rot47.net/api/fortune/")
|
||||||
|
let fortune = await resp.json()
|
||||||
|
this.iambic.EnqueueAsciiString(fortune)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function fortuneInit() {
|
||||||
|
try {
|
||||||
|
window.fortune = new Fortune()
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
Morse.toast(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", fortuneInit)
|
||||||
|
} else {
|
||||||
|
fortuneInit()
|
||||||
|
}
|
|
@ -0,0 +1,413 @@
|
||||||
|
/** 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
|
||||||
|
/** Duration of a dit */
|
||||||
|
const DIT = 1
|
||||||
|
/** Duration of a dah */
|
||||||
|
const DAH = 3
|
||||||
|
|
||||||
|
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": "--..",
|
||||||
|
".": ".-.-.-",
|
||||||
|
",": "--..--",
|
||||||
|
"?": "..--..",
|
||||||
|
"'": ".----.",
|
||||||
|
"!": "-.-.--",
|
||||||
|
"/": "-..-.",
|
||||||
|
"(": "-.--.",
|
||||||
|
")": "-.--.-",
|
||||||
|
"&": ".-...",
|
||||||
|
":": "---...",
|
||||||
|
";": "---...",
|
||||||
|
"=": "-...-",
|
||||||
|
"+": ".-.-.",
|
||||||
|
"-": "-....-",
|
||||||
|
"_": "--..-.",
|
||||||
|
"\"": ".-..-.",
|
||||||
|
"$": "...-..-",
|
||||||
|
"@": ".--.-.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS kludge
|
||||||
|
if (!window.AudioContext) {
|
||||||
|
window.AudioContext = window.webkitAudioContext
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(msg) {
|
||||||
|
let el = document.querySelector("#snackbar")
|
||||||
|
if (!el || !el.MaterialSnackbar) {
|
||||||
|
console.warn(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
el.MaterialSnackbar.showSnackbar({
|
||||||
|
message: msg,
|
||||||
|
timeout: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback to start or stop transmission
|
||||||
|
*
|
||||||
|
* @callback TxControl
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iambic input class.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
class Iambic {
|
||||||
|
/**
|
||||||
|
* Create an Iambic control
|
||||||
|
*
|
||||||
|
* @param {TxControl} beginTxFunc Function to begin transmitting
|
||||||
|
* @param {TxControl} endTxFunc Function to end transmitting
|
||||||
|
* @param {number} intervalDuration Dit duration (milliseconds)
|
||||||
|
*/
|
||||||
|
constructor(beginTxFunc, endTxFunc, intervalDuration=100) {
|
||||||
|
this.beginTxFunc = beginTxFunc
|
||||||
|
this.endTxFunc = endTxFunc
|
||||||
|
this.intervalDuration = intervalDuration
|
||||||
|
this.ditDown = false
|
||||||
|
this.dahDown = false
|
||||||
|
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 = next * -1
|
||||||
|
this.endTxFunc()
|
||||||
|
} else {
|
||||||
|
this.last = next
|
||||||
|
this.beginTxFunc()
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (this.last == DIT) {
|
||||||
|
this.last = DAH
|
||||||
|
} else {
|
||||||
|
this.last = DIT
|
||||||
|
}
|
||||||
|
} else if (this.ditDown) {
|
||||||
|
this.last = DIT
|
||||||
|
} else if (this.dahDown) {
|
||||||
|
this.last = DAH
|
||||||
|
} else {
|
||||||
|
this.last = null
|
||||||
|
}
|
||||||
|
return this.last
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new dit interval (transmission rate)
|
||||||
|
*
|
||||||
|
* @param {number} duration Dit duration (milliseconds)
|
||||||
|
*/
|
||||||
|
SetIntervalDuration(duration) {
|
||||||
|
this.intervalDuration = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EnqueueAsciiString(s) {
|
||||||
|
for (let c of s.toLowerCase()) {
|
||||||
|
let m = MorseMap[c]
|
||||||
|
if (m) {
|
||||||
|
this.EnqueueMorseString(m)
|
||||||
|
this.Enqueue(PAUSE_LETTER)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case " ":
|
||||||
|
case "\n":
|
||||||
|
case "\t":
|
||||||
|
this.Enqueue(PAUSE_WORD)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn("Unable to encode '" + c + "'!")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge trigger on key press or release
|
||||||
|
*
|
||||||
|
* @param {number} key DIT or DAH
|
||||||
|
* @param {boolean} down True if key was pressed, false if released
|
||||||
|
*/
|
||||||
|
Key(key, down) {
|
||||||
|
if (key == DIT) {
|
||||||
|
this.ditDown = down
|
||||||
|
} else if (key == DAH) {
|
||||||
|
this.dahDown = down
|
||||||
|
}
|
||||||
|
|
||||||
|
if (down) {
|
||||||
|
this.Enqueue(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Buzzer {
|
||||||
|
// 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, errorFreq=30} = {}) {
|
||||||
|
this.txGain = txGain
|
||||||
|
|
||||||
|
this.ac = new AudioContext()
|
||||||
|
|
||||||
|
this.lowGain = this.create(lowFreq)
|
||||||
|
this.highGain = this.create(highFreq)
|
||||||
|
this.errorGain = this.create(errorFreq, "square")
|
||||||
|
//this.noiseGain = this.whiteNoise()
|
||||||
|
|
||||||
|
this.ac.resume()
|
||||||
|
.then(() => {
|
||||||
|
document.querySelector("#muted").classList.add("hidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
create(frequency, type = "sine") {
|
||||||
|
let gain = this.ac.createGain()
|
||||||
|
gain.connect(this.ac.destination)
|
||||||
|
gain.gain.value = 0
|
||||||
|
let osc = this.ac.createOscillator()
|
||||||
|
osc.type = type
|
||||||
|
osc.connect(gain)
|
||||||
|
osc.frequency.value = frequency
|
||||||
|
osc.start()
|
||||||
|
return gain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some noise to prevent the browser from putting us to sleep
|
||||||
|
whiteNoise() {
|
||||||
|
let bufferSize = 17 * this.ac.sampleRate
|
||||||
|
let noiseBuffer = this.ac.createBuffer(1, bufferSize, this.ac.sampleRate)
|
||||||
|
let output = noiseBuffer.getChannelData(0)
|
||||||
|
for (let i = 0; i < bufferSize; i++) {
|
||||||
|
output[i] = Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let whiteNoise = this.ac.createBufferSource();
|
||||||
|
whiteNoise.buffer = noiseBuffer;
|
||||||
|
whiteNoise.loop = true;
|
||||||
|
whiteNoise.start(0);
|
||||||
|
|
||||||
|
let filter = this.ac.createBiquadFilter()
|
||||||
|
filter.type = "lowpass"
|
||||||
|
filter.frequency.value = 100
|
||||||
|
|
||||||
|
let gain = this.ac.createGain()
|
||||||
|
gain.gain.value = 0.01
|
||||||
|
|
||||||
|
whiteNoise.connect(filter)
|
||||||
|
filter.connect(gain)
|
||||||
|
gain.connect(this.ac.destination)
|
||||||
|
|
||||||
|
return gain
|
||||||
|
}
|
||||||
|
|
||||||
|
gain(high) {
|
||||||
|
if (high) {
|
||||||
|
return this.highGain.gain
|
||||||
|
} else {
|
||||||
|
return this.lowGain.gain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @param {number} gain Value (0-1)
|
||||||
|
*/
|
||||||
|
SetGain(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
|
||||||
|
*
|
||||||
|
* @param {boolean} tx Transmit or receive tone
|
||||||
|
* @param {number} when Time to begin, in ms (null=now)
|
||||||
|
*/
|
||||||
|
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 acWhen = this.acTime(when)
|
||||||
|
this.ac.resume()
|
||||||
|
.then(() => {
|
||||||
|
gain.setTargetAtTime(this.txGain, acWhen, 0.001)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
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 acWhen = this.acTime(when)
|
||||||
|
|
||||||
|
gain.setTargetAtTime(0, acWhen, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buzz for a duration at time
|
||||||
|
*
|
||||||
|
* @param {boolean} high High or low pitched tone
|
||||||
|
* @param {number} when Time to begin (ms since 1970-01-01Z, null=now)
|
||||||
|
* @param {number} duration Duration of buzz (ms)
|
||||||
|
*/
|
||||||
|
BuzzDuration(high, when, duration) {
|
||||||
|
this.Buzz(high, when)
|
||||||
|
this.Silence(high, when + duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {DIT, DAH, PAUSE, PAUSE_WORD, PAUSE_LETTER}
|
||||||
|
export {toast, Iambic, Buzzer}
|
Loading…
Reference in New Issue