vail/static/vail.js

699 lines
16 KiB
JavaScript
Raw Normal View History

2020-04-20 22:12:30 -06:00
// jshint asi:true
2020-04-09 23:09:33 -06:00
2020-04-26 15:46:37 -06:00
const lowFreq = 660
const highFreq = lowFreq * 6 / 5 // Perfect minor third
2020-04-26 21:43:55 -06:00
const errorFreq = 30
2020-04-20 22:12:30 -06:00
const DIT = 1
const DAH = 3
2020-04-20 22:12:30 -06:00
// iOS kludge
2021-01-18 14:32:48 -07:00
if (!window.AudioContext) {
window.AudioContext = window.webkitAudioContext
}
function toast(msg) {
let el = document.querySelector("#snackbar")
el.MaterialSnackbar.showSnackbar({
message: msg,
timeout: 2000
})
}
class Iambic {
2020-05-01 15:07:09 -06:00
constructor(beginTxFunc, endTxFunc) {
this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.intervalDuration = null
this.state = this.stateBegin
this.ditDown = false
this.dahDown = false
2020-05-01 15:07:09 -06:00
}
/**
* Set a new interval (transmission rate)
*
* @param {number} duration New interval duration, in ms
*/
SetIntervalDuration(duration) {
this.intervalDuration = duration
if (this.interval) {
clearInterval(this.interval)
this.interval = setInterval(e => this.pulse(), duration)
}
2020-05-01 15:07:09 -06:00
}
// An interval has passed, call whatever the current state function is
pulse(event) {
this.state()
}
stateBegin() {
if (this.ditDown) {
this.stateDit()
} else if (this.dahDown) {
this.stateDah()
} else {
clearInterval(this.interval)
this.interval = null
}
2020-05-01 15:07:09 -06:00
}
2021-01-18 14:32:48 -07:00
2020-05-01 15:07:09 -06:00
stateDit() {
// Send a dit
this.beginTxFunc()
this.state = this.stateDitEnd
}
stateDitEnd() {
this.endTxFunc()
this.state = this.stateDitNext
}
stateDitNext() {
if (this.dahDown) {
this.state = this.stateDah
} else {
this.state = this.stateBegin
}
this.state()
2020-05-01 15:07:09 -06:00
}
2020-05-01 15:07:09 -06:00
stateDah() {
// Send a dah
this.beginTxFunc()
this.state = this.stateDah2
}
stateDah2() {
this.state = this.stateDah3
}
stateDah3() {
this.state = this.stateDahEnd
2020-05-01 15:07:09 -06:00
}
stateDahEnd() {
2020-05-01 15:07:09 -06:00
this.endTxFunc()
this.state = this.stateDahNext
2020-05-01 15:07:09 -06:00
}
stateDahNext() {
if (this.ditDown) {
this.state = this.stateDit
} else {
this.state = this.stateBegin
}
this.state()
}
2021-01-18 14:32:48 -07:00
2020-05-01 15:07:09 -06:00
/**
* Edge trigger on key press or release
*
* @param {boolean} down True if key was pressed, false if released
* @param {number} key DIT or DAH
*/
Key(down, key) {
if (key == DIT) {
this.ditDown = down
2020-05-01 15:07:09 -06:00
} else if (key == DAH) {
this.dahDown = down
2020-05-01 15:07:09 -06:00
}
2021-01-18 14:32:48 -07:00
// Not pulsing yet? Start right away!
2021-01-18 14:32:48 -07:00
if (!this.interval) {
this.interval = setInterval(e => this.pulse(), this.intervalDuration)
this.pulse()
}
2020-05-01 15:07:09 -06:00
}
2020-04-20 22:12:30 -06:00
}
class Buzzer {
2020-05-01 15:07:09 -06:00
// 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.
2021-01-18 14:32:48 -07:00
constructor(txGain = 0.6) {
2020-05-01 15:07:09 -06:00
this.txGain = txGain
this.ac = new AudioContext()
this.lowGain = this.create(lowFreq)
this.highGain = this.create(highFreq)
this.errorGain = this.create(errorFreq, "square")
2020-05-30 15:00:05 -06:00
this.noiseGain = this.whiteNoise()
this.ac.resume()
2021-01-18 14:32:48 -07:00
.then(() => {
document.querySelector("#muted").classList.add("hidden")
})
2020-05-01 15:07:09 -06:00
}
2021-01-18 14:32:48 -07:00
create(frequency, type = "sine") {
2020-05-01 15:07:09 -06:00
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
}
2021-01-18 14:32:48 -07:00
2020-05-30 15:00:05 -06:00
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;
}
2021-01-18 14:32:48 -07:00
2020-05-30 15:00:05 -06:00
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
2021-01-18 14:32:48 -07:00
2020-05-30 15:00:05 -06:00
let gain = this.ac.createGain()
gain.gain.value = 0.1
2021-01-18 14:32:48 -07:00
2020-05-30 15:00:05 -06:00
whiteNoise.connect(filter)
filter.connect(gain)
gain.connect(this.ac.destination)
2021-01-18 14:32:48 -07:00
2020-05-30 15:00:05 -06:00
return gain
2021-01-18 14:32:48 -07:00
}
2020-05-01 15:07:09 -06:00
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) {
2021-01-18 14:32:48 -07:00
if (!when) {
2020-05-01 15:07:09 -06:00
return this.ac.currentTime
}
2021-01-18 14:32:48 -07:00
let acOffset = Date.now() - this.ac.currentTime * 1000
2020-05-01 15:07:09 -06:00
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)
2020-05-01 15:07:09 -06:00
*/
2021-01-18 14:32:48 -07:00
Buzz(tx, when = null) {
if (!tx) {
let recv = document.querySelector("#recv")
let ms = when - Date.now()
setTimeout(e => {
recv.classList.add("rx")
}, ms)
}
2020-05-01 15:07:09 -06:00
let gain = this.gain(tx)
let acWhen = this.acTime(when)
2020-05-01 15:07:09 -06:00
this.ac.resume()
2021-01-18 14:32:48 -07:00
.then(() => {
gain.setTargetAtTime(this.txGain, acWhen, 0.001)
})
2020-05-01 15:07:09 -06:00
}
/**
* End buzzing at time
*
* @param {boolean} tx Transmit or receive tone
* @param {number} when Time to end, in ms (null=now)
2020-05-01 15:07:09 -06:00
*/
2021-01-18 14:32:48 -07:00
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)
2020-05-01 15:07:09 -06:00
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)
2021-01-18 14:32:48 -07:00
this.Silence(high, when + duration)
2020-05-01 15:07:09 -06:00
}
2020-04-09 23:09:33 -06:00
}
class Vail {
2020-05-01 15:07:09 -06:00
constructor() {
this.sent = []
this.lagTimes = [0]
this.rxDurations = [0]
this.clockOffset = 0 // How badly our clock is off of the server's
2020-05-01 15:07:09 -06:00
this.rxDelay = 0 // Milliseconds to add to incoming timestamps
this.beginTxTime = null // Time when we began transmitting
2020-06-29 19:28:51 -06:00
this.debug = localStorage.debug
2020-05-01 15:07:09 -06:00
this.openSocket()
2020-05-01 15:07:09 -06:00
// Listen to HTML buttons
for (let e of document.querySelectorAll("button.key")) {
2021-01-18 14:32:48 -07:00
e.addEventListener("contextmenu", e => { e.preventDefault(); return false })
e.addEventListener("touchstart", e => this.keyButton(e))
e.addEventListener("touchend", e => this.keyButton(e))
2020-05-01 15:07:09 -06:00
e.addEventListener("mousedown", e => this.keyButton(e))
e.addEventListener("mouseup", e => this.keyButton(e))
}
2020-05-05 20:10:16 -06:00
for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e))
}
2020-05-01 15:07:09 -06:00
// Listen for keystrokes
2020-05-19 08:21:33 -06:00
document.addEventListener("keydown", e => this.keyboard(e))
document.addEventListener("keyup", e => this.keyboard(e))
2020-05-01 15:07:09 -06:00
// Make helpers
this.iambic = new Iambic(() => this.beginTx(), () => this.endTx())
this.buzzer = new Buzzer()
// Listen for slider values
this.inputInit("#iambic-duration", e => this.iambic.SetIntervalDuration(e.target.value))
2021-01-18 14:32:48 -07:00
this.inputInit("#rx-delay", e => { this.rxDelay = Number(e.target.value) })
2020-05-01 15:07:09 -06:00
// Show what repeater we're on
2020-05-05 20:10:16 -06:00
let repeater = (new URL(location)).searchParams.get("repeater") || "General Chaos"
2020-05-01 15:07:09 -06:00
document.querySelector("#repeater").textContent = repeater
2021-01-18 14:32:48 -07:00
// Request MIDI access
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess()
2021-01-18 14:32:48 -07:00
.then(a => this.midiInit(a))
}
2021-01-18 14:32:48 -07:00
2020-05-19 08:21:33 -06:00
// Set up for gamepad input
window.addEventListener("gamepadconnected", e => this.gamepadConnected(e))
2020-05-05 20:10:16 -06:00
}
2021-01-18 14:32:48 -07:00
openSocket() {
// Set up WebSocket
let wsUrl = new URL(window.location)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
wsUrl.pathname += "chat"
this.socket = new WebSocket(wsUrl)
this.socket.addEventListener("message", e => this.wsMessage(e))
this.socket.addEventListener("close", e => this.openSocket())
2020-05-01 15:07:09 -06:00
}
inputInit(selector, func) {
let element = document.querySelector(selector)
let storedValue = localStorage[element.id]
if (storedValue) {
element.value = storedValue
}
let outputElement = document.querySelector(selector + "-value")
element.addEventListener("input", e => {
localStorage[element.id] = element.value
if (outputElement) {
outputElement.value = element.value
}
func(e)
})
element.dispatchEvent(new Event("input"))
}
2021-01-18 14:32:48 -07:00
midiInit(access) {
this.midiAccess = access
for (let input of this.midiAccess.inputs.values()) {
input.addEventListener("midimessage", e => this.midiMessage(e))
}
this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e))
2021-01-18 14:32:48 -07:00
}
midiStateChange(event) {
// XXX: it's not entirely clear how to handle new devices showing up.
// XXX: possibly we go through this.midiAccess.inputs and somehow only listen on new things
}
2021-01-18 14:32:48 -07:00
midiMessage(event) {
let data = Array.from(event.data)
2021-01-18 14:32:48 -07:00
let begin
2020-05-02 17:55:17 -06:00
let cmd = data[0] >> 4
let chan = data[0] & 0xf
switch (cmd) {
case 9:
begin = true
break
2020-05-02 17:55:17 -06:00
case 8:
begin = false
break
default:
return
}
2021-01-18 14:32:48 -07:00
switch (data[1] % 12) {
case 0: // C
this.straightKey(begin)
break
case 1: // C#
this.iambic.Key(begin, DIT)
break
case 2: // D
this.iambic.Key(begin, DAH)
break
default:
return
2021-01-18 14:32:48 -07:00
}
}
2021-01-18 14:32:48 -07:00
2020-05-01 15:07:09 -06:00
error(msg) {
2020-05-19 08:21:33 -06:00
toast(msg)
2020-05-01 15:07:09 -06:00
this.buzzer.ErrorTone()
}
beginTx() {
this.beginTxTime = Date.now()
this.buzzer.Buzz(true)
}
endTx() {
let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime
this.buzzer.Silence(true)
this.wsSend(this.beginTxTime, duration)
this.beginTxTime = null
}
updateReading(selector, value) {
let e = document.querySelector(selector)
if (e) {
e.value = value
}
}
updateReadings() {
2021-01-18 14:32:48 -07:00
let avgLag = this.lagTimes.reduce((a, b) => (a + b)) / this.lagTimes.length
let longestRx = this.rxDurations.reduce((a, b) => Math.max(a, b))
2020-05-01 15:07:09 -06:00
let suggestedDelay = (avgLag + longestRx) * 1.2
this.updateReading("#lag-value", avgLag.toFixed())
this.updateReading("#longest-rx-value", longestRx)
this.updateReading("#suggested-delay-value", suggestedDelay.toFixed())
this.updateReading("#clock-off-value", this.clockOffset)
2020-05-01 15:07:09 -06:00
}
addLagReading(duration) {
this.lagTimes.push(duration)
while (this.lagTimes.length > 20) {
this.lagTimes.shift()
}
this.updateReadings()
}
addRxDuration(duration) {
this.rxDurations.push(duration)
while (this.rxDurations.length > 20) {
this.rxDurations.shift()
}
this.updateReadings()
}
wsSend(time, duration) {
2020-06-29 19:28:51 -06:00
let msg = [time - this.clockOffset, duration]
2020-05-01 15:07:09 -06:00
let jmsg = JSON.stringify(msg)
this.socket.send(jmsg)
this.sent.push(jmsg)
}
wsMessage(event) {
let now = Date.now()
let jmsg = event.data
let msg
try {
msg = JSON.parse(jmsg)
}
2021-01-18 14:32:48 -07:00
catch (err) {
console.log(err, msg)
return
}
2020-05-01 15:07:09 -06:00
let beginTxTime = msg[0]
let durations = msg.slice(1)
2021-01-18 14:32:48 -07:00
2020-06-29 19:28:51 -06:00
if (this.debug) {
console.log("recv", beginTxTime, durations)
2020-05-30 22:23:53 -06:00
}
2021-01-18 14:32:48 -07:00
2020-05-01 15:07:09 -06:00
let sent = this.sent.filter(e => e != jmsg)
if (sent.length < this.sent.length) {
// We're getting our own message back, which tells us our lag.
// We shouldn't emit a tone, though.
2021-01-18 14:32:48 -07:00
let totalDuration = durations.reduce((a, b) => a + b)
2020-05-01 15:07:09 -06:00
this.sent = sent
this.addLagReading(now - beginTxTime - totalDuration)
return
}
2020-06-29 19:28:51 -06:00
// Server is telling us the current time
if (durations.length == 0) {
let offset = now - beginTxTime
if (this.clockOffset == 0) {
this.clockOffset = offset
this.updateReadings()
}
return
}
2020-05-01 15:07:09 -06:00
2020-06-29 19:28:51 -06:00
// Why is this happening?
if (beginTxTime == 0) {
return
}
2021-01-18 14:32:48 -07:00
2020-06-29 19:28:51 -06:00
// Add rxDelay
2021-01-18 14:32:48 -07:00
let adjustedTxTime = beginTxTime + this.rxDelay
2020-05-01 15:07:09 -06:00
if (adjustedTxTime < now) {
2020-06-29 19:28:51 -06:00
console.log("adjustedTxTime: ", adjustedTxTime, " now: ", now)
2020-05-01 15:07:09 -06:00
this.error("Packet requested playback " + (now - adjustedTxTime) + "ms in the past. Increase receive delay!")
return
}
// Every other value is a silence duration
let tx = true
for (let duration of durations) {
duration = Number(duration)
if (tx && (duration > 0)) {
this.buzzer.BuzzDuration(false, adjustedTxTime, duration)
this.addRxDuration(duration)
}
adjustedTxTime = Number(adjustedTxTime) + duration
tx = !tx
}
}
straightKey(begin) {
if (begin) {
this.beginTx()
} else {
this.endTx()
}
}
2020-05-19 08:21:33 -06:00
iambicDit(begin) {
this.iambic.Key(begin, DIT)
}
iambicDah(begin) {
this.iambic.Key(begin, DAH)
}
2021-01-18 14:32:48 -07:00
2020-05-19 08:21:33 -06:00
keyboard(event) {
2020-05-01 15:07:09 -06:00
if (event.repeat) {
// Ignore key repeats generated by the OS, we do this ourselves
return
}
let begin = event.type.endsWith("down")
2021-01-18 14:32:48 -07:00
if ((event.code == "KeyX") ||
(event.code == "Period") ||
(event.code == "ControlLeft") ||
(event.code == "BracketLeft") ||
(event.key == "[")) {
2020-05-01 15:07:09 -06:00
event.preventDefault()
2020-05-19 08:21:33 -06:00
this.iambicDit(begin)
2020-05-01 15:07:09 -06:00
}
2021-01-18 14:32:48 -07:00
if ((event.code == "KeyZ") ||
(event.code == "Slash") ||
(event.code == "ControlRight") ||
(event.code == "BracketRight") ||
(event.key == "]")) {
2020-05-01 15:07:09 -06:00
event.preventDefault()
2020-05-19 08:21:33 -06:00
this.iambicDah(begin)
2020-05-01 15:07:09 -06:00
}
2021-01-18 14:32:48 -07:00
if ((event.code == "KeyC") ||
(event.code == "Comma") ||
(event.key == "Shift") ||
(event.key == "Enter") ||
(event.key == "NumpadEnter")) {
2020-05-01 15:07:09 -06:00
event.preventDefault()
2020-05-19 08:21:33 -06:00
this.straightKey(begin)
2020-05-01 15:07:09 -06:00
}
}
keyButton(event) {
let begin = event.type.endsWith("down") || event.type.endsWith("start")
event.preventDefault()
2020-05-01 15:07:09 -06:00
if (event.target.id == "dah") {
2020-05-19 08:21:33 -06:00
this.iambicDah(begin)
2020-05-01 15:07:09 -06:00
} else if ((event.target.id == "dit") && (event.button == 2)) {
2020-05-19 08:21:33 -06:00
this.iambicDah(begin)
2020-05-01 15:07:09 -06:00
} else if (event.target.id == "dit") {
2020-05-19 11:54:44 -06:00
this.iambicDit(begin)
2020-05-01 15:07:09 -06:00
} else if (event.target.id == "key") {
this.straightKey(begin)
} else if ((event.target.id == "ck") && begin) {
2020-05-01 15:07:09 -06:00
this.Test()
}
}
2020-05-19 08:21:33 -06:00
gamepadConnected(event) {
// Polling could be computationally expensive,
// especially on devices with a power budget, like phones.
// To be considerate, we only start polling if a gamepad appears.
2021-01-18 14:32:48 -07:00
if (!this.gamepadButtons) {
2020-05-19 08:21:33 -06:00
this.gamepadButtons = {}
this.gamepadPoll(event.timeStamp)
}
}
2021-01-18 14:32:48 -07:00
2020-05-19 08:21:33 -06:00
gamepadPoll(timestamp) {
let currentButtons = {}
for (let gp of navigator.getGamepads()) {
if (gp == null) {
continue
}
2020-05-21 20:32:23 -06:00
for (let i in gp.buttons) {
let pressed = gp.buttons[i].pressed
if (i < 2) {
currentButtons.key |= pressed
} else if (i % 2 == 0) {
currentButtons.dit |= pressed
} else {
currentButtons.dah |= pressed
}
}
2020-05-19 08:21:33 -06:00
}
if (currentButtons.key != this.gamepadButtons.key) {
this.straightKey(currentButtons.key)
}
if (currentButtons.dit != this.gamepadButtons.dit) {
this.iambicDit(currentButtons.dit)
}
if (currentButtons.dah != this.gamepadButtons.dah) {
this.iambicDah(currentButtons.dah)
}
this.gamepadButtons = currentButtons
requestAnimationFrame(e => this.gamepadPoll(e))
}
2020-05-01 15:07:09 -06:00
/**
* Send "CK" to server, and don't squelch the repeat
*/
Test() {
let dit = Number(document.querySelector("#iambic-duration-value").value)
let dah = dit * 3
let s = dit
let msg = [
Date.now(),
dah, s, dit, s, dah, s, dit,
s * 3,
dah, s, dit, s, dah
]
this.wsSend(Date.now(), 0) // Get round-trip time
this.socket.send(JSON.stringify(msg))
}
2020-05-19 08:21:33 -06:00
maximize(e) {
let element = e.target
2021-01-18 14:32:48 -07:00
while (!element.classList.contains("mdl-card")) {
2020-05-19 08:21:33 -06:00
element = element.parentElement
2021-01-18 14:32:48 -07:00
if (!element) {
2020-05-19 08:21:33 -06:00
console.log("Maximize button: couldn't find parent card")
return
}
}
element.classList.toggle("maximized")
console.log(element)
}
2021-01-18 14:32:48 -07:00
2020-05-19 08:21:33 -06:00
2020-04-10 08:59:15 -06:00
}
function vailInit() {
2020-05-26 20:52:48 -06:00
if (navigator.serviceWorker) {
navigator.serviceWorker.register("sw.js")
}
try {
window.app = new Vail()
} catch (err) {
console.log(err)
toast(err)
}
2020-04-09 23:09:33 -06:00
}
if (document.readyState === "loading") {
2020-05-01 15:07:09 -06:00
document.addEventListener("DOMContentLoaded", vailInit)
2020-04-09 23:09:33 -06:00
} else {
2020-05-01 15:07:09 -06:00
vailInit()
2020-04-09 23:09:33 -06:00
}
// vim: noet sw=2 ts=2