Output MIDI notes + refactoring

This commit is contained in:
Neale Pickett 2022-05-22 21:37:36 -06:00
parent 4ef1ff7517
commit b7de5cf8cb
6 changed files with 273 additions and 80 deletions

View File

@ -72,7 +72,7 @@
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
<!-- This appears as a little light that turns on when someone's sending --> <!-- 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> <output class="has-text-info" id="note"></output>
<i class="mdi mdi-volume-off" id="muted"></i> <i class="mdi mdi-volume-off" id="muted"></i>
</span> </span>

View File

@ -2,9 +2,14 @@ class Input {
constructor(keyer) { constructor(keyer) {
this.keyer = keyer this.keyer = keyer
} }
SetDitDuration(delay) { SetDitDuration(delay) {
// Nothing // Nothing
} }
SetKeyerMode(mode) {
// Nothing
}
} }
export class HTML extends Input{ export class HTML extends Input{
@ -117,11 +122,17 @@ export class MIDI extends Input{
this.midiStateChange() this.midiStateChange()
} }
SetIntervalDuration(delay) { SetDitDuration(delay) {
// Send the Vail adapter the current iambic delay setting // Send the Vail adapter the current iambic delay setting
for (let output of this.midiAccess.outputs.values()) { for (let output of this.midiAccess.outputs.values()) {
// MIDI only supports 7-bit values, so we have to divide it by two // 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! // Tell the Vail adapter to disable keyboard events: we can do MIDI!
for (let output of this.midiAccess.outputs.values()) { 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{
} }
} }
/** class Collection {
* Set up all input methods constructor(keyer) {
* this.html =new HTML(keyer)
* @param keyer Keyer object for everyone to use this.keyboard =new Keyboard(keyer)
*/ this.midi =new MIDI(keyer)
export function SetupAll(keyer) { this.gamepad =new Gamepad(keyer)
return { this.collection = [this.html, this.keyboard, this.midi, this.gamepad]
HTML: new HTML(keyer), }
Keyboard: new Keyboard(keyer),
MIDI: new MIDI(keyer), /**
Gamepad: new Gamepad(keyer), * Set duration of all inputs
*
* @param duration Duration to set
*/
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}

View File

@ -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. * A straight keyer.
@ -67,12 +74,10 @@ class QSet extends Set {
*/ */
class StraightKeyer { class StraightKeyer {
/** /**
* @param {TxControl} beginTxFunc Callback to begin transmitting * @param {Transmitter} output Transmitter object
* @param {TxControl} endTxFunc Callback to end transmitting
*/ */
constructor(beginTxFunc, endTxFunc) { constructor(output) {
this.beginTxFunc = beginTxFunc this.output = output
this.endTxFunc = endTxFunc
this.Reset() this.Reset()
} }
@ -89,7 +94,7 @@ class StraightKeyer {
* Reset state and stop all transmissions. * Reset state and stop all transmissions.
*/ */
Reset() { Reset() {
this.endTxFunc() this.output.EndTx()
this.txRelays = [] this.txRelays = []
} }
@ -140,9 +145,9 @@ class StraightKeyer {
if (wasClosed != nowClosed) { if (wasClosed != nowClosed) {
if (nowClosed) { if (nowClosed) {
this.beginTxFunc() this.output.BeginTx()
} else { } else {
this.endTxFunc() this.output.EndTx()
} }
} }
} }
@ -471,6 +476,19 @@ const Keyers = {
robo: RoboKeyer.Keyer, robo: RoboKeyer.Keyer,
} }
export { const Numbers = {
Keyers, straight: 1,
cootie: 1,
bug: 2,
elbug: 3,
singledot: 4,
ultimatic: 5,
iambic: 6,
iambica: 7,
iambicb: 8,
keyahead: 9,
}
export {
Keyers, Numbers,
} }

View File

@ -27,7 +27,9 @@ const Second = 1000 * Millisecond
const OscillatorRampDuration = 5*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.") 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 * Compute the special "Audio Context" time
* *
@ -42,13 +44,6 @@ function BuzzerAudioContextTime(when) {
return Math.max(when - acOffset, 0) / Second 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 { class Oscillator {
/** /**
* Create a new oscillator, and encase it in a Gain for control. * 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 {boolean} tx Transmit or receive tone
* @param {number} when Time to begin, in ms (0=now) * @param {number} when Time to begin, in ms (0=now)
*/ */
Buzz(tx, when=0) { async Buzz(tx, when=0) {
console.log("Buzz", tx, when) console.log("Buzz", tx, when)
} }
@ -165,7 +160,7 @@ class Buzzer {
* @param {boolean} tx Transmit or receive tone * @param {boolean} tx Transmit or receive tone
* @param {number} when Time to end, in ms (0=now) * @param {number} when Time to end, in ms (0=now)
*/ */
Silence(tx, when=0) { async Silence(tx, when=0) {
console.log("Silence", tx, when) console.log("Silence", tx, when)
} }
@ -210,6 +205,10 @@ class ToneBuzzer extends AudioBuzzer {
this.rxOsc = new Oscillator(lowFreq, txGain) this.rxOsc = new Oscillator(lowFreq, txGain)
this.txOsc = new Oscillator(highFreq, 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 {boolean} tx Transmit or receive tone
* @param {number} when Time to begin, in ms (0=now) * @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 let osc = tx?this.txOsc:this.rxOsc
osc.SoundAt(when) osc.SoundAt(when)
} }
@ -229,7 +228,7 @@ class ToneBuzzer extends AudioBuzzer {
* @param {boolean} tx Transmit or receive tone * @param {boolean} tx Transmit or receive tone
* @param {number} when Time to end, in ms (0=now) * @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 let osc = tx?this.txOsc:this.rxOsc
osc.HushAt(when) osc.HushAt(when)
} }
@ -249,7 +248,7 @@ class TelegraphBuzzer extends AudioBuzzer{
this.openSample = new Sample("telegraph-b.mp3") this.openSample = new Sample("telegraph-b.mp3")
} }
Buzz(tx, when=0) { async Buzz(tx, when=0) {
if (tx) { if (tx) {
this.hum.SoundAt(when) this.hum.SoundAt(when)
} else { } else {
@ -257,7 +256,7 @@ class TelegraphBuzzer extends AudioBuzzer{
} }
} }
Silence(tx ,when=0) { async Silence(tx ,when=0) {
if (tx) { if (tx) {
this.hum.HushAt(when) this.hum.HushAt(when)
} else { } else {
@ -266,29 +265,174 @@ class TelegraphBuzzer extends AudioBuzzer{
} }
} }
class Lamp extends Buzzer { class LampBuzzer extends Buzzer {
constructor(element) { constructor() {
super() super()
this.element = element this.elements = document.querySelectorAll(".recv-lamp")
} }
Buzz(tx, when=0) { async Buzz(tx, when=0) {
if (tx) return if (tx) return
let ms = when?when - Date.now():0 let ms = when?when - Date.now():0
setTimeout( setTimeout(
() =>{ () =>{
this.element.classList.add("rx") for (let e of this.elements) {
e.classList.add("rx")
}
}, },
ms, ms,
) )
} }
Silence(tx, when=0) { async Silence(tx, when=0) {
if (tx) return if (tx) return
let ms = when?when - Date.now():0 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}

View File

@ -16,7 +16,7 @@
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */ -webkit-user-select: none; /* 2022-04-26 Safari still needs this */
} }
#recv.rx { .recv-lamp.rx {
background-color: orange; background-color: orange;
} }
@ -83,4 +83,4 @@ code {
#charts canvas { #charts canvas {
height: 0.5em; height: 0.5em;
width: 100%; width: 100%;
} }

View File

@ -1,5 +1,5 @@
import {Keyers} from "./keyers.mjs" import * as Keyers from "./keyers.mjs"
import * as Buzzer from "./buzzer.mjs" import * as Outputs from "./outputs.mjs"
import * as Inputs from "./inputs.mjs" import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs" import * as Repeaters from "./repeaters.mjs"
import * as Chart from "./chart.mjs" import * as Chart from "./chart.mjs"
@ -10,7 +10,7 @@ const Second = 1000 * Millisecond
const Minute = 60 * Second const Minute = 60 * Second
/** /**
* Pop up a message, using an notification.. * Pop up a message, using an notification.
* *
* @param {string} msg Message to display * @param {string} msg Message to display
*/ */
@ -37,16 +37,17 @@ class VailClient {
this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps
this.beginTxTime = null // Time when we began transmitting this.beginTxTime = null // Time when we began transmitting
// Make helpers // Outputs
this.lamp = new Buzzer.Lamp(document.querySelector("#recv")) this.outputs = new Outputs.Collection()
this.buzzer = new Buzzer.ToneBuzzer()
this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx()) // Keyers
this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx()) this.straightKeyer = new Keyers.Keyers.straight(this)
this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence()) this.keyer = new Keyers.Keyers.straight(this)
this.roboKeyer = new Keyers.Keyers.robo(() => this.Buzz(), () => this.Silence())
// Set up various input methods // Set up various input methods
// Send this as the keyer so we can intercept dit and dah events for charts // 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 // Maximize button
for (let e of document.querySelectorAll("button.maximize")) { for (let e of document.querySelectorAll("button.maximize")) {
@ -69,9 +70,7 @@ class VailClient {
} }
this.keyer.SetDitDuration(this.ditDuration) this.keyer.SetDitDuration(this.ditDuration)
this.roboKeyer.SetDitDuration(this.ditDuration) this.roboKeyer.SetDitDuration(this.ditDuration)
for (let i of Object.values(this.inputs)) { this.inputs.SetDitDuration(this.ditDuration)
i.SetDitDuration(this.ditDuration)
}
}) })
this.inputInit("#rx-delay", e => { this.inputInit("#rx-delay", e => {
this.rxDelay = e.target.value * Second this.rxDelay = e.target.value * Second
@ -89,7 +88,7 @@ class VailClient {
this.setTimingCharts(true) this.setTimingCharts(true)
// Turn off the "muted" symbol when we can start making noise // Turn off the "muted" symbol when we can start making noise
Buzzer.Ready() Outputs.AudioReady()
.then(() => { .then(() => {
console.log("Audio context ready") console.log("Audio context ready")
document.querySelector("#muted").classList.add("is-hidden") document.querySelector("#muted").classList.add("is-hidden")
@ -117,12 +116,13 @@ class VailClient {
} }
setKeyer(keyerName) { setKeyer(keyerName) {
let newKeyerClass = Keyers[keyerName] let newKeyerClass = Keyers.Keyers[keyerName]
let newKeyerNumber = Keyers.Numbers[keyerName]
if (!newKeyerClass) { if (!newKeyerClass) {
console.error("Keyer not found", keyerName) console.error("Keyer not found", keyerName)
return return
} }
let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx()) let newKeyer = new newKeyerClass(this)
let i = 0 let i = 0
for (let keyName of newKeyer.KeyNames()) { for (let keyName of newKeyer.KeyNames()) {
let e = document.querySelector(`.key[data-key="${i}"]`) let e = document.querySelector(`.key[data-key="${i}"]`)
@ -132,24 +132,23 @@ class VailClient {
this.keyer.Release() this.keyer.Release()
this.keyer = newKeyer this.keyer = newKeyer
this.inputs.SetKeyerMode(newKeyerNumber)
document.querySelector("#keyer-rate").dispatchEvent(new Event("input")) document.querySelector("#keyer-rate").dispatchEvent(new Event("input"))
} }
Buzz() { Buzz() {
this.buzzer.Buzz() this.outputs.Buzz(false)
this.lamp.Buzz()
if (this.rxChart) this.rxChart.Set(1) if (this.rxChart) this.rxChart.Set(1)
} }
Silence() { Silence() {
this.buzzer.Silence() this.outputs.Silence()
this.lamp.Silence()
if (this.rxChart) this.rxChart.Set(0) if (this.rxChart) this.rxChart.Set(0)
} }
BuzzDuration(tx, when, duration) { BuzzDuration(tx, when, duration) {
this.buzzer.BuzzDuration(tx, when, duration) this.outputs.BuzzDuration(tx, when, duration)
this.lamp.BuzzDuration(tx, when, duration)
let chart = tx?this.txChart:this.rxChart let chart = tx?this.txChart:this.rxChart
if (chart) { if (chart) {
@ -163,10 +162,11 @@ class VailClient {
* *
* Called from the keyer. * Called from the keyer.
*/ */
beginTx() { BeginTx() {
this.beginTxTime = Date.now() this.beginTxTime = Date.now()
this.buzzer.Buzz(true) this.outputs.Buzz(true)
if (this.txChart) this.txChart.Set(1) if (this.txChart) this.txChart.Set(1)
} }
/** /**
@ -174,13 +174,13 @@ class VailClient {
* *
* Called from the keyer * Called from the keyer
*/ */
endTx() { EndTx() {
if (!this.beginTxTime) { if (!this.beginTxTime) {
return return
} }
let endTxTime = Date.now() let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime let duration = endTxTime - this.beginTxTime
this.buzzer.Silence(true) this.outputs.Silence(true)
this.repeater.Transmit(this.beginTxTime, duration) this.repeater.Transmit(this.beginTxTime, duration)
this.beginTxTime = null this.beginTxTime = null
if (this.txChart) this.txChart.Set(0) if (this.txChart) this.txChart.Set(0)
@ -222,10 +222,10 @@ class VailClient {
*/ */
setTelegraphBuzzer(enable) { setTelegraphBuzzer(enable) {
if (enable) { if (enable) {
this.buzzer = new Buzzer.TelegraphBuzzer() this.outputs.SetAudioType("telegraph")
toast("Telegraphs only make sound when receiving!") toast("Telegraphs only make sound when receiving!")
} else { } else {
this.buzzer = new Buzzer.ToneBuzzer() this.outputs.SetAudioType()
} }
} }
@ -343,7 +343,7 @@ class VailClient {
*/ */
error(msg) { error(msg) {
toast(msg) toast(msg)
this.buzzer.Error() this.outputs.Error()
} }
/** /**