Fail mode for toasts

This commit is contained in:
Neale Pickett 2021-04-26 14:33:48 -06:00
parent 1a088e71d1
commit f987cbdb46
4 changed files with 473 additions and 316 deletions

View File

@ -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>

View File

@ -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)
} }
} }

35
static/fortune.mjs Normal file
View File

@ -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()
}

413
static/morse.mjs Normal file
View File

@ -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}