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

View File

@ -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
*
* @param keyer Keyer object for everyone to use
*/
export function SetupAll(keyer) {
return {
HTML: new HTML(keyer),
Keyboard: new Keyboard(keyer),
MIDI: new MIDI(keyer),
Gamepad: new Gamepad(keyer),
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 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.
@ -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,
}

View File

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

View File

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

View File

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