vail/static/vail.js

312 lines
7.4 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-20 22:12:30 -06:00
const DIT = 1
const DAH = 3
2020-04-20 22:12:30 -06:00
class Iambic {
2020-04-26 15:46:37 -06:00
constructor(beginTxFunc, endTxFunc) {
this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.interval = null
2020-04-26 15:46:37 -06:00
this.state = this.stateSpace
this.keyState = null
}
2020-04-26 15:46:37 -06:00
/**
* Set a new interval (transmission rate)
*
* @param {number} duration New interval duration, in ms
*/
SetInterval(duration) {
clearInterval(this.interval)
this.interval = setInterval(e => this.pulse(), duration)
}
// An interval has passed, call whatever the current state function is
pulse(event) {
this.state()
}
2020-04-26 15:46:37 -06:00
stateSpace() {
// Don't transmit for one interval.
this.state = this.keyState || this.stateSpace
}
2020-04-26 15:46:37 -06:00
stateDit() {
// Send a dit
this.beginTxFunc()
2020-04-26 15:46:37 -06:00
this.state = this.stateEnd
}
2020-04-26 15:46:37 -06:00
stateDah() {
// Send a dah
this.beginTxFunc()
2020-04-26 15:46:37 -06:00
this.state = this.stateDah2
}
2020-04-26 15:46:37 -06:00
stateDah2() {
this.state = this.stateDah3
}
2020-04-26 15:46:37 -06:00
stateDah3() {
this.state = this.stateEnd
}
2020-04-26 15:46:37 -06:00
stateEnd() {
// Stop sending
this.endTxFunc()
2020-04-26 15:46:37 -06:00
this.state = this.stateSpace
this.state()
2020-04-20 22:12:30 -06:00
}
2020-04-26 15:46:37 -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) {
// By setting keyState we request this state transition,
// the next time the transition is possible.
let keyState = null
if (key == DIT) {
2020-04-26 15:46:37 -06:00
keyState = this.stateDit
} else if (key == DAH) {
2020-04-26 15:46:37 -06:00
keyState = this.stateDah
}
2020-04-26 15:46:37 -06:00
if (down) {
this.keyState = keyState
} else if (keyState == this.keyState) {
// Only stop when we've released the right key
this.keyState = null
}
}
2020-04-20 22:12:30 -06:00
}
class Buzzer {
2020-04-26 15:46:37 -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.
constructor(txGain=0.5) {
this.txGain = txGain
this.ac = new AudioContext()
this.lowGain = this.ac.createGain()
2020-04-26 15:46:37 -06:00
this.lowGain.connect(this.ac.destination)
this.lowGain.gain.value = 0
this.lowOsc = this.ac.createOscillator()
2020-04-26 15:46:37 -06:00
this.lowOsc.connect(this.lowGain)
this.lowOsc.frequency.value = lowFreq
this.lowOsc.start()
this.highGain = this.ac.createGain()
2020-04-26 15:46:37 -06:00
this.highGain.connect(this.ac.destination)
this.highGain.gain.value = 0
this.highOsc = this.ac.createOscillator()
2020-04-26 15:46:37 -06:00
this.highOsc.connect(this.highGain)
this.highOsc.frequency.value = highFreq
this.highOsc.start()
}
2020-04-20 22:12:30 -06:00
gain(high) {
if (high) {
2020-04-26 15:46:37 -06:00
return this.highGain.gain
} else {
2020-04-26 15:46:37 -06:00
return this.lowGain.gain
}
2020-04-20 22:12:30 -06:00
}
2020-04-26 15:46:37 -06:00
/**
* 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
}
2020-04-26 15:46:37 -06:00
let acOffset = Date.now() - this.ac.currentTime*1000
return (when - acOffset) / 1000
}
2020-04-26 15:46:37 -06:00
/**
* Set gain
*
* @param {number} gain Value (0-1)
*/
SetGain(gain) {
this.txGain = gain
}
/**
* Begin buzzing at time
*
* @param {boolean} high High or low pitched tone
* @param {number} when Time to begin (null=now)
*/
Buzz(high, when=null) {
let gain = this.gain(high)
let acWhen = this.acTime(when)
this.ac.resume()
gain.setTargetAtTime(this.txGain, acWhen, 0.001)
}
2020-04-26 15:46:37 -06:00
/**
* End buzzing at time
*
* @param {boolean} high High or low pitched tone
* @param {number} when Time to begin (null=now)
*/
Silence(high, when=null) {
let gain = this.gain(high)
2020-04-26 15:46:37 -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)
this.Silence(high, when+duration)
}
2020-04-09 23:09:33 -06:00
}
class Vail {
constructor() {
this.beginTxTime = null // Time when we began transmitting
// Set up WebSocket
let wsUrl = new URL(window.location)
wsUrl.protocol = "ws:"
wsUrl.pathname += "chat"
window.socket = new WebSocket(wsUrl)
window.socket.addEventListener("message", this.wsMessage)
2020-04-20 22:12:30 -06:00
// Listen to HTML buttons
for (let e of document.querySelectorAll("button.key")) {
e.addEventListener("contextmenu", e => {e.preventDefault(); return false})
e.addEventListener("mousedown", e => this.keyButton(e))
e.addEventListener("mouseup", e => this.keyButton(e))
}
2020-04-20 22:12:30 -06:00
// Listen for keystrokes
document.addEventListener("keydown", e => this.key(e))
document.addEventListener("keyup", e => this.key(e))
2020-04-26 15:46:37 -06:00
// Make helpers
this.iambic = new Iambic(() => this.beginTx(), () => this.endTx())
2020-04-26 15:46:37 -06:00
this.buzzer = new Buzzer()
2020-04-26 15:46:37 -06:00
// Listen for slider values
this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value))
2020-04-10 08:27:35 -06:00
}
inputInit(selector, func) {
2020-04-26 15:46:37 -06:00
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)
})
2020-04-26 15:46:37 -06:00
element.dispatchEvent(new Event("input"))
2020-04-20 22:12:30 -06:00
}
2020-04-10 08:27:35 -06:00
2020-04-26 15:46:37 -06:00
beginTx() {
this.beginTxTime = Date.now()
this.buzzer.Buzz(true)
}
2020-04-26 15:46:37 -06:00
endTx() {
let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime
this.buzzer.Silence(true)
let msg = JSON.stringify([this.beginTxTime, duration])
window.socket.send(msg)
}
2020-04-26 15:46:37 -06:00
wsMessage(event) {
let msg = JSON.parse(event.data)
let beginTxTime = msg[0]
let duration = msg[1]
console.log(msg)
2020-04-26 15:46:37 -06:00
}
key(event) {
if (event.repeat) {
// Ignore key repeats generated by the OS, we do this ourselves
return
}
2020-04-26 15:46:37 -06:00
let begin = event.type.endsWith("down")
2020-04-26 15:46:37 -06:00
if ((event.code == "Period") || (event.key == "KeyZ")) {
event.preventDefault()
this.iambic.Key(begin, DIT)
}
2020-04-26 15:46:37 -06:00
if ((event.code == "Slash") || (event.code == "KeyX")) {
event.preventDefault()
this.iambic.Key(begin, DAH)
}
2020-04-26 15:46:37 -06:00
if ((event.key == "Shift")) {
event.preventDefault()
if (begin) {
this.beginTx()
} else {
this.endTx()
}
}
}
keyButton(event) {
let begin = event.type.endsWith("down")
if (event.target.id == "dah") {
this.iambic.Key(begin, DAH)
} else if ((event.target.id == "dit") && (event.button == 2)) {
this.iambic.Key(begin, DAH)
} else if (event.target.id == "dit") {
this.iambic.Key(begin, DIT)
} else if (event.target.id == "key") {
if (begin) {
this.beginTx()
} else {
this.endTx()
}
}
}
2020-04-10 08:59:15 -06:00
}
function vailInit() {
window.app = new Vail()
2020-04-09 23:09:33 -06:00
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", vailInit)
2020-04-09 23:09:33 -06:00
} else {
vailInit()
2020-04-09 23:09:33 -06:00
}