mirror of https://github.com/nealey/vail.git
Output MIDI notes + refactoring
This commit is contained in:
parent
4ef1ff7517
commit
b7de5cf8cb
|
@ -72,7 +72,7 @@
|
|||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<!-- This appears as a little light that turns on when someone's sending -->
|
||||
<span class="tag" id="recv">
|
||||
<span class="tag" class="recv-lamp">
|
||||
<output class="has-text-info" id="note"></output>
|
||||
<i class="mdi mdi-volume-off" id="muted"></i>
|
||||
</span>
|
||||
|
|
|
@ -2,9 +2,14 @@ class Input {
|
|||
constructor(keyer) {
|
||||
this.keyer = keyer
|
||||
}
|
||||
|
||||
SetDitDuration(delay) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
SetKeyerMode(mode) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
|
||||
export class HTML extends Input{
|
||||
|
@ -117,11 +122,17 @@ export class MIDI extends Input{
|
|||
this.midiStateChange()
|
||||
}
|
||||
|
||||
SetIntervalDuration(delay) {
|
||||
SetDitDuration(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])
|
||||
output.send([0xB0, 0x01, delay/2])
|
||||
}
|
||||
}
|
||||
|
||||
SetKeyerMode(mode) {
|
||||
for (let output of this.midiAccess.outputs.values()) {
|
||||
output.send([0xC0, mode])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +147,7 @@ export class MIDI extends Input{
|
|||
|
||||
// Tell the Vail adapter to disable keyboard events: we can do MIDI!
|
||||
for (let output of this.midiAccess.outputs.values()) {
|
||||
output.send([0x8B, 0x00, 0x00]) // Turn off keyboard mode
|
||||
output.send([0xB0, 0x00, 0x00]) // Turn off keyboard mode
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,16 +240,36 @@ export class Gamepad extends Input{
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up all input methods
|
||||
class Collection {
|
||||
constructor(keyer) {
|
||||
this.html =new HTML(keyer)
|
||||
this.keyboard =new Keyboard(keyer)
|
||||
this.midi =new MIDI(keyer)
|
||||
this.gamepad =new Gamepad(keyer)
|
||||
this.collection = [this.html, this.keyboard, this.midi, this.gamepad]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set duration of all inputs
|
||||
*
|
||||
* @param keyer Keyer object for everyone to use
|
||||
* @param duration Duration to set
|
||||
*/
|
||||
export function SetupAll(keyer) {
|
||||
return {
|
||||
HTML: new HTML(keyer),
|
||||
Keyboard: new Keyboard(keyer),
|
||||
MIDI: new MIDI(keyer),
|
||||
Gamepad: new Gamepad(keyer),
|
||||
SetDitDuration(duration) {
|
||||
for (let e of this.collection) {
|
||||
e.SetDitDuration(duration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set keyer mode of all inputs
|
||||
*
|
||||
* @param mode Keyer mode to set
|
||||
*/
|
||||
SetKeyerMode(mode) {
|
||||
for (let e of this.collection) {
|
||||
e.SetKeyerMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {Collection}
|
||||
|
|
|
@ -53,10 +53,17 @@ class QSet extends Set {
|
|||
}
|
||||
|
||||
/**
|
||||
* A callback to start or stop transmission
|
||||
* Definition of a transmitter type.
|
||||
*
|
||||
* @callback TxControl
|
||||
* The VailClient class implements this.
|
||||
*/
|
||||
class Transmitter {
|
||||
/** Begin transmitting */
|
||||
BeginTx() {}
|
||||
|
||||
/** End transmitting */
|
||||
EndTx() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A straight keyer.
|
||||
|
@ -67,12 +74,10 @@ class QSet extends Set {
|
|||
*/
|
||||
class StraightKeyer {
|
||||
/**
|
||||
* @param {TxControl} beginTxFunc Callback to begin transmitting
|
||||
* @param {TxControl} endTxFunc Callback to end transmitting
|
||||
* @param {Transmitter} output Transmitter object
|
||||
*/
|
||||
constructor(beginTxFunc, endTxFunc) {
|
||||
this.beginTxFunc = beginTxFunc
|
||||
this.endTxFunc = endTxFunc
|
||||
constructor(output) {
|
||||
this.output = output
|
||||
this.Reset()
|
||||
}
|
||||
|
||||
|
@ -89,7 +94,7 @@ class StraightKeyer {
|
|||
* Reset state and stop all transmissions.
|
||||
*/
|
||||
Reset() {
|
||||
this.endTxFunc()
|
||||
this.output.EndTx()
|
||||
this.txRelays = []
|
||||
}
|
||||
|
||||
|
@ -140,9 +145,9 @@ class StraightKeyer {
|
|||
|
||||
if (wasClosed != nowClosed) {
|
||||
if (nowClosed) {
|
||||
this.beginTxFunc()
|
||||
this.output.BeginTx()
|
||||
} else {
|
||||
this.endTxFunc()
|
||||
this.output.EndTx()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -471,6 +476,19 @@ const Keyers = {
|
|||
robo: RoboKeyer.Keyer,
|
||||
}
|
||||
|
||||
export {
|
||||
Keyers,
|
||||
const Numbers = {
|
||||
straight: 1,
|
||||
cootie: 1,
|
||||
bug: 2,
|
||||
elbug: 3,
|
||||
singledot: 4,
|
||||
ultimatic: 5,
|
||||
iambic: 6,
|
||||
iambica: 7,
|
||||
iambicb: 8,
|
||||
keyahead: 9,
|
||||
}
|
||||
|
||||
export {
|
||||
Keyers, Numbers,
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ const Second = 1000 * Millisecond
|
|||
const OscillatorRampDuration = 5*Millisecond
|
||||
|
||||
console.warn("Chrome will now complain about an AudioContext not being allowed to start. This is normal, and there is no way to make Chrome stop complaining about this.")
|
||||
const BuzzerAudioContext = new AudioContext()
|
||||
const BuzzerAudioContext = new AudioContext({
|
||||
latencyHint: 0,
|
||||
})
|
||||
/**
|
||||
* Compute the special "Audio Context" time
|
||||
*
|
||||
|
@ -42,13 +44,6 @@ function BuzzerAudioContextTime(when) {
|
|||
return Math.max(when - acOffset, 0) / Second
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the audio system is able to start making noise.
|
||||
*/
|
||||
async function Ready() {
|
||||
await BuzzerAudioContext.resume()
|
||||
}
|
||||
|
||||
class Oscillator {
|
||||
/**
|
||||
* Create a new oscillator, and encase it in a Gain for control.
|
||||
|
@ -155,7 +150,7 @@ class Buzzer {
|
|||
* @param {boolean} tx Transmit or receive tone
|
||||
* @param {number} when Time to begin, in ms (0=now)
|
||||
*/
|
||||
Buzz(tx, when=0) {
|
||||
async Buzz(tx, when=0) {
|
||||
console.log("Buzz", tx, when)
|
||||
}
|
||||
|
||||
|
@ -165,7 +160,7 @@ class Buzzer {
|
|||
* @param {boolean} tx Transmit or receive tone
|
||||
* @param {number} when Time to end, in ms (0=now)
|
||||
*/
|
||||
Silence(tx, when=0) {
|
||||
async Silence(tx, when=0) {
|
||||
console.log("Silence", tx, when)
|
||||
}
|
||||
|
||||
|
@ -210,6 +205,10 @@ class ToneBuzzer extends AudioBuzzer {
|
|||
|
||||
this.rxOsc = new Oscillator(lowFreq, txGain)
|
||||
this.txOsc = new Oscillator(highFreq, txGain)
|
||||
|
||||
// Keep the speaker going always. This keeps the browser from "swapping out" our audio context.
|
||||
this.bgOsc = new Oscillator(1, 0.001)
|
||||
this.bgOsc.SoundAt()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -218,7 +217,7 @@ class ToneBuzzer extends AudioBuzzer {
|
|||
* @param {boolean} tx Transmit or receive tone
|
||||
* @param {number} when Time to begin, in ms (0=now)
|
||||
*/
|
||||
Buzz(tx, when = null) {
|
||||
async Buzz(tx, when = null) {
|
||||
let osc = tx?this.txOsc:this.rxOsc
|
||||
osc.SoundAt(when)
|
||||
}
|
||||
|
@ -229,7 +228,7 @@ class ToneBuzzer extends AudioBuzzer {
|
|||
* @param {boolean} tx Transmit or receive tone
|
||||
* @param {number} when Time to end, in ms (0=now)
|
||||
*/
|
||||
Silence(tx, when = null) {
|
||||
async Silence(tx, when = null) {
|
||||
let osc = tx?this.txOsc:this.rxOsc
|
||||
osc.HushAt(when)
|
||||
}
|
||||
|
@ -249,7 +248,7 @@ class TelegraphBuzzer extends AudioBuzzer{
|
|||
this.openSample = new Sample("telegraph-b.mp3")
|
||||
}
|
||||
|
||||
Buzz(tx, when=0) {
|
||||
async Buzz(tx, when=0) {
|
||||
if (tx) {
|
||||
this.hum.SoundAt(when)
|
||||
} else {
|
||||
|
@ -257,7 +256,7 @@ class TelegraphBuzzer extends AudioBuzzer{
|
|||
}
|
||||
}
|
||||
|
||||
Silence(tx ,when=0) {
|
||||
async Silence(tx ,when=0) {
|
||||
if (tx) {
|
||||
this.hum.HushAt(when)
|
||||
} else {
|
||||
|
@ -266,29 +265,174 @@ class TelegraphBuzzer extends AudioBuzzer{
|
|||
}
|
||||
}
|
||||
|
||||
class Lamp extends Buzzer {
|
||||
constructor(element) {
|
||||
class LampBuzzer extends Buzzer {
|
||||
constructor() {
|
||||
super()
|
||||
this.element = element
|
||||
this.elements = document.querySelectorAll(".recv-lamp")
|
||||
}
|
||||
|
||||
Buzz(tx, when=0) {
|
||||
async Buzz(tx, when=0) {
|
||||
if (tx) return
|
||||
|
||||
let ms = when?when - Date.now():0
|
||||
setTimeout(
|
||||
() =>{
|
||||
this.element.classList.add("rx")
|
||||
for (let e of this.elements) {
|
||||
e.classList.add("rx")
|
||||
}
|
||||
},
|
||||
ms,
|
||||
)
|
||||
}
|
||||
Silence(tx, when=0) {
|
||||
async Silence(tx, when=0) {
|
||||
if (tx) return
|
||||
|
||||
let ms = when?when - Date.now():0
|
||||
setTimeout(() => this.element.classList.remove("rx"), ms)
|
||||
setTimeout(
|
||||
() => {
|
||||
for (let e of this.elements) {
|
||||
e.classList.remove("rx")
|
||||
}
|
||||
},
|
||||
ms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export {Ready, ToneBuzzer, TelegraphBuzzer, Lamp}
|
||||
class MIDIBuzzer extends Buzzer {
|
||||
constructor() {
|
||||
super()
|
||||
this.SetNote(69) // A4; 440Hz
|
||||
|
||||
this.midiAccess = {outputs: []} // stub while we wait for async stuff
|
||||
if (navigator.requestMIDIAccess) {
|
||||
this.midiInit()
|
||||
}
|
||||
}
|
||||
|
||||
async midiInit(access) {
|
||||
this.outputs = new Set()
|
||||
this.midiAccess = await navigator.requestMIDIAccess()
|
||||
this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e))
|
||||
this.midiStateChange()
|
||||
}
|
||||
|
||||
midiStateChange(event) {
|
||||
let newOutputs = new Set()
|
||||
for (let output of this.midiAccess.outputs.values()) {
|
||||
console.log(output.state)
|
||||
if ((output.state != "connected") || (output.name.includes("Through"))) {
|
||||
continue
|
||||
}
|
||||
newOutputs.add(output)
|
||||
}
|
||||
this.outputs = newOutputs
|
||||
}
|
||||
|
||||
sendAt(when, message) {
|
||||
let ms = when?when - Date.now():0
|
||||
setTimeout(
|
||||
() => {
|
||||
for (let output of this.outputs) {
|
||||
output.send(message)
|
||||
}
|
||||
},
|
||||
ms,
|
||||
)
|
||||
}
|
||||
|
||||
async Buzz(tx, when=0) {
|
||||
if (tx) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sendAt(when, [0x90, this.note, 0x7f])
|
||||
}
|
||||
|
||||
async Silence(tx, when=0) {
|
||||
if (tx) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sendAt(when, [0x80, this.note, 0x7f])
|
||||
}
|
||||
|
||||
/*
|
||||
* Set note to transmit
|
||||
*/
|
||||
SetNote(tx, note) {
|
||||
if (tx) {
|
||||
return
|
||||
}
|
||||
this.note = note
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the audio system is able to start making noise.
|
||||
*/
|
||||
async function AudioReady() {
|
||||
await BuzzerAudioContext.resume()
|
||||
}
|
||||
|
||||
class Collection {
|
||||
constructor() {
|
||||
this.tone = new ToneBuzzer()
|
||||
this.telegraph = new TelegraphBuzzer()
|
||||
this.lamp = new LampBuzzer()
|
||||
this.midi = new MIDIBuzzer()
|
||||
this.collection = new Set([this.tone, this.lamp, this.midi])
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio output type.
|
||||
*
|
||||
* @param {string} audioType "telegraph" for telegraph mode, otherwise tone mode
|
||||
*/
|
||||
SetAudioType(audioType) {
|
||||
this.collection.delete(this.telegraph)
|
||||
this.collection.delete(this.tone)
|
||||
if (audioType == "telegraph") {
|
||||
this.collection.add(this.telegraph)
|
||||
} else {
|
||||
this.collection.add(this.tone)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buzz all outputs.
|
||||
*
|
||||
* @param tx True if transmitting
|
||||
*/
|
||||
Buzz(tx=False) {
|
||||
for (let b of this.collection) {
|
||||
b.Buzz(tx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Silence all outputs.
|
||||
*
|
||||
* @param tx True if transmitting
|
||||
*/
|
||||
Silence(tx=False) {
|
||||
for (let b of this.collection) {
|
||||
b.Silence(tx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buzz for a certain duration at a certain time
|
||||
*
|
||||
* @param tx True if transmitting
|
||||
* @param when Time to begin
|
||||
* @param duration How long to buzz
|
||||
*/
|
||||
BuzzDuration(tx, when, duration) {
|
||||
for (let b of this.collection) {
|
||||
b.BuzzDuration(tx, when, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {AudioReady, Collection}
|
|
@ -16,7 +16,7 @@
|
|||
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */
|
||||
}
|
||||
|
||||
#recv.rx {
|
||||
.recv-lamp.rx {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Keyers} from "./keyers.mjs"
|
||||
import * as Buzzer from "./buzzer.mjs"
|
||||
import * as Keyers from "./keyers.mjs"
|
||||
import * as Outputs from "./outputs.mjs"
|
||||
import * as Inputs from "./inputs.mjs"
|
||||
import * as Repeaters from "./repeaters.mjs"
|
||||
import * as Chart from "./chart.mjs"
|
||||
|
@ -10,7 +10,7 @@ const Second = 1000 * Millisecond
|
|||
const Minute = 60 * Second
|
||||
|
||||
/**
|
||||
* Pop up a message, using an notification..
|
||||
* Pop up a message, using an notification.
|
||||
*
|
||||
* @param {string} msg Message to display
|
||||
*/
|
||||
|
@ -37,16 +37,17 @@ class VailClient {
|
|||
this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps
|
||||
this.beginTxTime = null // Time when we began transmitting
|
||||
|
||||
// Make helpers
|
||||
this.lamp = new Buzzer.Lamp(document.querySelector("#recv"))
|
||||
this.buzzer = new Buzzer.ToneBuzzer()
|
||||
this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
|
||||
this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
|
||||
this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence())
|
||||
// Outputs
|
||||
this.outputs = new Outputs.Collection()
|
||||
|
||||
// Keyers
|
||||
this.straightKeyer = new Keyers.Keyers.straight(this)
|
||||
this.keyer = new Keyers.Keyers.straight(this)
|
||||
this.roboKeyer = new Keyers.Keyers.robo(() => this.Buzz(), () => this.Silence())
|
||||
|
||||
// Set up various input methods
|
||||
// Send this as the keyer so we can intercept dit and dah events for charts
|
||||
this.inputs = Inputs.SetupAll(this)
|
||||
this.inputs = new Inputs.Collection(this)
|
||||
|
||||
// Maximize button
|
||||
for (let e of document.querySelectorAll("button.maximize")) {
|
||||
|
@ -69,9 +70,7 @@ class VailClient {
|
|||
}
|
||||
this.keyer.SetDitDuration(this.ditDuration)
|
||||
this.roboKeyer.SetDitDuration(this.ditDuration)
|
||||
for (let i of Object.values(this.inputs)) {
|
||||
i.SetDitDuration(this.ditDuration)
|
||||
}
|
||||
this.inputs.SetDitDuration(this.ditDuration)
|
||||
})
|
||||
this.inputInit("#rx-delay", e => {
|
||||
this.rxDelay = e.target.value * Second
|
||||
|
@ -89,7 +88,7 @@ class VailClient {
|
|||
this.setTimingCharts(true)
|
||||
|
||||
// Turn off the "muted" symbol when we can start making noise
|
||||
Buzzer.Ready()
|
||||
Outputs.AudioReady()
|
||||
.then(() => {
|
||||
console.log("Audio context ready")
|
||||
document.querySelector("#muted").classList.add("is-hidden")
|
||||
|
@ -117,12 +116,13 @@ class VailClient {
|
|||
}
|
||||
|
||||
setKeyer(keyerName) {
|
||||
let newKeyerClass = Keyers[keyerName]
|
||||
let newKeyerClass = Keyers.Keyers[keyerName]
|
||||
let newKeyerNumber = Keyers.Numbers[keyerName]
|
||||
if (!newKeyerClass) {
|
||||
console.error("Keyer not found", keyerName)
|
||||
return
|
||||
}
|
||||
let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx())
|
||||
let newKeyer = new newKeyerClass(this)
|
||||
let i = 0
|
||||
for (let keyName of newKeyer.KeyNames()) {
|
||||
let e = document.querySelector(`.key[data-key="${i}"]`)
|
||||
|
@ -132,24 +132,23 @@ class VailClient {
|
|||
this.keyer.Release()
|
||||
this.keyer = newKeyer
|
||||
|
||||
this.inputs.SetKeyerMode(newKeyerNumber)
|
||||
|
||||
document.querySelector("#keyer-rate").dispatchEvent(new Event("input"))
|
||||
}
|
||||
|
||||
Buzz() {
|
||||
this.buzzer.Buzz()
|
||||
this.lamp.Buzz()
|
||||
this.outputs.Buzz(false)
|
||||
if (this.rxChart) this.rxChart.Set(1)
|
||||
}
|
||||
|
||||
Silence() {
|
||||
this.buzzer.Silence()
|
||||
this.lamp.Silence()
|
||||
this.outputs.Silence()
|
||||
if (this.rxChart) this.rxChart.Set(0)
|
||||
}
|
||||
|
||||
BuzzDuration(tx, when, duration) {
|
||||
this.buzzer.BuzzDuration(tx, when, duration)
|
||||
this.lamp.BuzzDuration(tx, when, duration)
|
||||
this.outputs.BuzzDuration(tx, when, duration)
|
||||
|
||||
let chart = tx?this.txChart:this.rxChart
|
||||
if (chart) {
|
||||
|
@ -163,10 +162,11 @@ class VailClient {
|
|||
*
|
||||
* Called from the keyer.
|
||||
*/
|
||||
beginTx() {
|
||||
BeginTx() {
|
||||
this.beginTxTime = Date.now()
|
||||
this.buzzer.Buzz(true)
|
||||
this.outputs.Buzz(true)
|
||||
if (this.txChart) this.txChart.Set(1)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -174,13 +174,13 @@ class VailClient {
|
|||
*
|
||||
* Called from the keyer
|
||||
*/
|
||||
endTx() {
|
||||
EndTx() {
|
||||
if (!this.beginTxTime) {
|
||||
return
|
||||
}
|
||||
let endTxTime = Date.now()
|
||||
let duration = endTxTime - this.beginTxTime
|
||||
this.buzzer.Silence(true)
|
||||
this.outputs.Silence(true)
|
||||
this.repeater.Transmit(this.beginTxTime, duration)
|
||||
this.beginTxTime = null
|
||||
if (this.txChart) this.txChart.Set(0)
|
||||
|
@ -222,10 +222,10 @@ class VailClient {
|
|||
*/
|
||||
setTelegraphBuzzer(enable) {
|
||||
if (enable) {
|
||||
this.buzzer = new Buzzer.TelegraphBuzzer()
|
||||
this.outputs.SetAudioType("telegraph")
|
||||
toast("Telegraphs only make sound when receiving!")
|
||||
} else {
|
||||
this.buzzer = new Buzzer.ToneBuzzer()
|
||||
this.outputs.SetAudioType()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -343,7 +343,7 @@ class VailClient {
|
|||
*/
|
||||
error(msg) {
|
||||
toast(msg)
|
||||
this.buzzer.Error()
|
||||
this.outputs.Error()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue