mirror of https://github.com/nealey/vail.git
Note adjustment
This commit is contained in:
parent
58b6f896d5
commit
75c933f943
|
@ -79,7 +79,7 @@
|
||||||
<button id="recv">rx</button>
|
<button id="recv">rx</button>
|
||||||
<output class="has-text-info" id="note"></output>
|
<output class="has-text-info" id="note"></output>
|
||||||
<br>
|
<br>
|
||||||
<i id="muted">
|
<i class="muted">
|
||||||
If you can read this, it means the browser needs you to click somewhere on this page
|
If you can read this, it means the browser needs you to click somewhere on this page
|
||||||
before it will start beeping!
|
before it will start beeping!
|
||||||
</i>
|
</i>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<!-- 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 recv-lamp">
|
<span class="tag 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 muted"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -213,8 +213,8 @@
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label">
|
<div class="field-label">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
|
<span data-i18n="label.rx-delay">rx delay:</span>
|
||||||
<output for="rx-delay"></output>s
|
<output for="rx-delay"></output>s
|
||||||
<span data-i18n="label.rx-delay">rx delay</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
|
@ -246,6 +246,89 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label">
|
||||||
|
<label class="label">
|
||||||
|
<span data-i18n="label.gain">volume:</span>
|
||||||
|
<output for="masterGain"></output>%
|
||||||
|
<i class="mdi mdi-volume-off muted"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="masterGain"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value="100"
|
||||||
|
step="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label">
|
||||||
|
<label class="label">
|
||||||
|
<span data-i18n="label.tx-tone">tx tone:</span>
|
||||||
|
<output for="tx-tone"></output>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="tx-tone"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="127"
|
||||||
|
value="69"
|
||||||
|
step="1"
|
||||||
|
list="tones">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label">
|
||||||
|
<label class="label">
|
||||||
|
<span data-i18n="label.rx-tone">rx tone:</span>
|
||||||
|
<output for="rx-tone"></output>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="rx-tone"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="127"
|
||||||
|
value="69"
|
||||||
|
step="1"
|
||||||
|
list="tones">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<datalist id="tones">
|
||||||
|
<option value="0" label="C-1"></option>
|
||||||
|
<option value="12" label="C0"></option>
|
||||||
|
<option value="24" label="C1"></option>
|
||||||
|
<option value="36" label="C2"></option>
|
||||||
|
<option value="48" label="C3"></option>
|
||||||
|
<option value="60" label="C4"></option>
|
||||||
|
<option value="72" label="C5"></option>
|
||||||
|
<option value="84" label="C6"></option>
|
||||||
|
<option value="96" label="C7"></option>
|
||||||
|
<option value="108" label="C8"></option>
|
||||||
|
<option value="120" label="C9"></option>
|
||||||
|
</datalist>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" id="telegraph-buzzer">
|
<input type="checkbox" id="telegraph-buzzer">
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* @file Musical notes and frequencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** One half step in equal temperament */
|
||||||
|
const semitone = Math.pow(2, 1/12)
|
||||||
|
const ln_semitone = Math.log(semitone)
|
||||||
|
const A4_note = 69
|
||||||
|
const A4_freq = 440
|
||||||
|
const Cn1_note = 0
|
||||||
|
const Cn1_freq = A4_freq / Math.pow(semitone, A4_note)
|
||||||
|
|
||||||
|
const note_names = [
|
||||||
|
"C",
|
||||||
|
"C♯",
|
||||||
|
"D",
|
||||||
|
"E♭",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"F♯",
|
||||||
|
"G",
|
||||||
|
"A♭",
|
||||||
|
"A",
|
||||||
|
"B♭",
|
||||||
|
"B",
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a MIDI note to a frequency
|
||||||
|
*
|
||||||
|
* @param {Number} note MIDI note number
|
||||||
|
* @returns {Number} Frequency (Hz)
|
||||||
|
*/
|
||||||
|
function MIDINoteFrequency(note) {
|
||||||
|
return Cn1_freq * Math.pow(semitone, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return closest matching MIDI note to a frequency
|
||||||
|
*
|
||||||
|
* @param {Number} frequency Frequency (Hz)
|
||||||
|
* @returns {Number} Closest MIDI note
|
||||||
|
*/
|
||||||
|
function FrequencyMIDINote(frequency) {
|
||||||
|
return Math.round(Math.log(frequency/Cn1_freq) / ln_semitone)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MIDINoteName(note) {
|
||||||
|
let octave = Math.floor(note / 12) - 1
|
||||||
|
return note_names[note % 12] + octave
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
MIDINoteFrequency,
|
||||||
|
FrequencyMIDINote,
|
||||||
|
MIDINoteName,
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {AudioSource, AudioContextTime} from "./audio.mjs"
|
||||||
|
|
||||||
const HIGH_FREQ = 555
|
const HIGH_FREQ = 555
|
||||||
const LOW_FREQ = 444
|
const LOW_FREQ = 444
|
||||||
|
|
||||||
|
@ -26,68 +28,88 @@ 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.")
|
|
||||||
const BuzzerAudioContext = new AudioContext({
|
|
||||||
latencyHint: 0,
|
|
||||||
})
|
|
||||||
/**
|
|
||||||
* Compute the special "Audio Context" time
|
|
||||||
*
|
|
||||||
* This is is a duration from now, in seconds.
|
|
||||||
*
|
|
||||||
* @param {Date} when Date to compute
|
|
||||||
* @returns audiocontext time
|
|
||||||
*/
|
|
||||||
function BuzzerAudioContextTime(when) {
|
|
||||||
if (!when) return 0
|
|
||||||
let acOffset = Date.now() - (BuzzerAudioContext.currentTime * Second)
|
|
||||||
return Math.max(when - acOffset, 0) / Second
|
|
||||||
}
|
|
||||||
|
|
||||||
class Oscillator {
|
class Oscillator extends AudioSource {
|
||||||
/**
|
/**
|
||||||
* Create a new oscillator, and encase it in a Gain for control.
|
* Create a new oscillator, and encase it in a Gain for control.
|
||||||
*
|
*
|
||||||
|
* @param {AudioContext} context Audio context
|
||||||
* @param {number} frequency Oscillator frequency (Hz)
|
* @param {number} frequency Oscillator frequency (Hz)
|
||||||
* @param {number} gain Gain (volume) of this oscillator (0.0 - 1.0)
|
* @param {number} maxGain Maximum gain (volume) of this oscillator (0.0 - 1.0)
|
||||||
* @param {string} type Oscillator type
|
* @param {string} type Oscillator type
|
||||||
* @returns {GainNode} A new GainNode object this oscillator is connected to
|
|
||||||
*/
|
*/
|
||||||
constructor(frequency, gain = 0.5, type = "sine") {
|
constructor(context, frequency, maxGain = 0.5, type = "sine") {
|
||||||
this.targetGain = gain
|
super(context)
|
||||||
|
this.maxGain = maxGain
|
||||||
|
|
||||||
this.gainNode = BuzzerAudioContext.createGain()
|
// Start quiet
|
||||||
this.gainNode.connect(BuzzerAudioContext.destination)
|
this.masterGain.gain.value = 0
|
||||||
this.gainNode.gain.value = 0
|
|
||||||
|
|
||||||
this.osc = BuzzerAudioContext.createOscillator()
|
this.osc = new OscillatorNode(this.context)
|
||||||
this.osc.type = type
|
this.osc.type = type
|
||||||
this.osc.connect(this.gainNode)
|
this.osc.connect(this.masterGain)
|
||||||
this.osc.frequency.value = frequency
|
this.setFrequency(frequency)
|
||||||
this.osc.start()
|
this.osc.start()
|
||||||
|
|
||||||
return gain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Set oscillator frequency
|
||||||
|
*
|
||||||
|
* @param {Number} frequency New frequency (Hz)
|
||||||
|
*/
|
||||||
|
setFrequency(frequency) {
|
||||||
|
this.osc.frequency.value = frequency
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set oscillator frequency to a MIDI note number
|
||||||
|
*
|
||||||
|
* This uses an equal temperament.
|
||||||
|
*
|
||||||
|
* @param {Number} note MIDI note number
|
||||||
|
*/
|
||||||
|
setMIDINote(note) {
|
||||||
|
let frequency = 8.18 // MIDI note 0
|
||||||
|
for (let i = 0; i < note; i++) {
|
||||||
|
frequency *= 1.0594630943592953 // equal temperament half step
|
||||||
|
}
|
||||||
|
this.setFrequency(frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set gain to some value at a given time.
|
||||||
*
|
*
|
||||||
* @param {number} target Target gain
|
* @param {number} target Target gain
|
||||||
* @param {Date} when Time this should start
|
* @param {Date} when Time this should start
|
||||||
* @param {Duration} timeConstant Duration of ramp to target gain
|
* @param {Duration} timeConstant Duration of ramp to target gain
|
||||||
*/
|
*/
|
||||||
async setTargetAtTime(target, when, timeConstant=OscillatorRampDuration) {
|
async setTargetAtTime(target, when, timeConstant=OscillatorRampDuration) {
|
||||||
await BuzzerAudioContext.resume()
|
await this.context.resume()
|
||||||
this.gainNode.gain.setTargetAtTime(
|
this.masterGain.gain.setTargetAtTime(
|
||||||
target,
|
target,
|
||||||
BuzzerAudioContextTime(when),
|
AudioContextTime(this.context, when),
|
||||||
timeConstant/Second,
|
timeConstant/Second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sound at a given time.
|
||||||
|
*
|
||||||
|
* @param {Number} when When to start making noise
|
||||||
|
* @param {Number} timeConstant How long to ramp up
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
SoundAt(when=0, timeConstant=OscillatorRampDuration) {
|
SoundAt(when=0, timeConstant=OscillatorRampDuration) {
|
||||||
return this.setTargetAtTime(this.targetGain, when, timeConstant)
|
return this.setTargetAtTime(this.maxGain, when, timeConstant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shut up at a given time.
|
||||||
|
*
|
||||||
|
* @param {Number} when When to stop making noise
|
||||||
|
* @param {Number} timeConstant How long to ramp down
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
HushAt(when=0, timeConstant=OscillatorRampDuration) {
|
HushAt(when=0, timeConstant=OscillatorRampDuration) {
|
||||||
return this.setTargetAtTime(0, when, timeConstant)
|
return this.setTargetAtTime(0, when, timeConstant)
|
||||||
}
|
}
|
||||||
|
@ -96,24 +118,20 @@ class Oscillator {
|
||||||
/**
|
/**
|
||||||
* A digital sample, loaded from a URL.
|
* A digital sample, loaded from a URL.
|
||||||
*/
|
*/
|
||||||
class Sample {
|
class Sample extends AudioSource {
|
||||||
/**
|
/**
|
||||||
*
|
* @param {AudioContext} context
|
||||||
* @param {string} url URL to resource
|
* @param {string} url URL to resource
|
||||||
* @param {number} gain Gain (0.0 - 1.0)
|
|
||||||
*/
|
*/
|
||||||
constructor(url, gain=0.5) {
|
constructor(context, url) {
|
||||||
|
super(context)
|
||||||
this.resume = this.load(url)
|
this.resume = this.load(url)
|
||||||
|
|
||||||
this.gainNode = BuzzerAudioContext.createGain()
|
|
||||||
this.gainNode.connect(BuzzerAudioContext.destination)
|
|
||||||
this.gainNode.gain.value = gain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(url) {
|
async load(url) {
|
||||||
let resp = await fetch(url)
|
let resp = await fetch(url)
|
||||||
let buf = await resp.arrayBuffer()
|
let buf = await resp.arrayBuffer()
|
||||||
this.data = await BuzzerAudioContext.decodeAudioData(buf)
|
this.data = await this.context.decodeAudioData(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,22 +140,24 @@ class Sample {
|
||||||
* @param {Date} when When to begin playback
|
* @param {Date} when When to begin playback
|
||||||
*/
|
*/
|
||||||
async PlayAt(when) {
|
async PlayAt(when) {
|
||||||
await BuzzerAudioContext.resume()
|
await this.context.resume()
|
||||||
await this.resume
|
await this.resume
|
||||||
let bs = BuzzerAudioContext.createBufferSource()
|
let bs = new AudioBufferSourceNode(this.context)
|
||||||
bs.buffer = this.data
|
bs.buffer = this.data
|
||||||
bs.connect(this.gainNode)
|
bs.connect(this.masterGain)
|
||||||
bs.start(BuzzerAudioContextTime(when))
|
bs.start(AudioContextTime(this.context, when))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A (mostly) virtual class defining a buzzer.
|
* A (mostly) virtual class defining a buzzer.
|
||||||
*/
|
*/
|
||||||
class Buzzer {
|
class Buzzer extends AudioSource {
|
||||||
constructor() {
|
/**
|
||||||
|
* @param {AudioContext} context
|
||||||
|
*/
|
||||||
|
constructor(context) {
|
||||||
|
super(context)
|
||||||
this.connected = true
|
this.connected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,41 +211,66 @@ class Buzzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioBuzzer extends Buzzer {
|
class AudioBuzzer extends Buzzer {
|
||||||
constructor(errorFreq=30) {
|
/**
|
||||||
super()
|
* A buzzer that make noise
|
||||||
|
*
|
||||||
|
* @param {AudioContext} context
|
||||||
|
* @param {Number} errorFreq Error tone frequency (hz)
|
||||||
|
*/
|
||||||
|
constructor(context, errorFreq=30) {
|
||||||
|
super(context)
|
||||||
|
|
||||||
this.errorGain = new Oscillator(errorFreq, 0.1, "square")
|
this.errorTone = new Oscillator(this.context, errorFreq, 0.1, "square")
|
||||||
|
this.errorTone.connect(this.masterGain)
|
||||||
}
|
}
|
||||||
|
|
||||||
Error() {
|
Error() {
|
||||||
let now = Date.now()
|
let now = Date.now()
|
||||||
this.errorGain.SoundAt(now)
|
this.errorTone.SoundAt(now)
|
||||||
this.errorGain.HushAt(now + 200*Millisecond)
|
this.errorTone.HushAt(now + 200*Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
class ToneBuzzer extends AudioBuzzer {
|
class ToneBuzzer extends AudioBuzzer {
|
||||||
// Buzzers keep two oscillators: one high and one low.
|
constructor(context, {txGain=0.5, highFreq=HIGH_FREQ, lowFreq=LOW_FREQ} = {}) {
|
||||||
// They generate a continuous waveform,
|
super(context)
|
||||||
// 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, highFreq=HIGH_FREQ, lowFreq=LOW_FREQ} = {}) {
|
this.rxOsc = new Oscillator(this.context, lowFreq, txGain)
|
||||||
super()
|
this.txOsc = new Oscillator(this.context, highFreq, txGain)
|
||||||
|
|
||||||
this.rxOsc = new Oscillator(lowFreq, txGain)
|
this.rxOsc.connect(this.masterGain)
|
||||||
this.txOsc = new Oscillator(highFreq, txGain)
|
this.txOsc.connect(this.masterGain)
|
||||||
|
|
||||||
// Keep the speaker going always. This keeps the browser from "swapping out" our audio context.
|
// Keep the speaker going always. This keeps the browser from "swapping out" our audio context.
|
||||||
if (false) {
|
if (false) {
|
||||||
this.bgOsc = new Oscillator(1, 0.001)
|
this.bgOsc = new Oscillator(this.context, 1, 0.001)
|
||||||
this.bgOsc.SoundAt()
|
this.bgOsc.SoundAt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set MIDI note for tx/rx tone
|
||||||
|
*
|
||||||
|
* @param {Boolean} tx True to set transmit note
|
||||||
|
* @param {Number} note MIDI note to send
|
||||||
|
*/
|
||||||
|
SetMIDINote(tx, note) {
|
||||||
|
if (tx) {
|
||||||
|
this.txOsc.setMIDINote(note)
|
||||||
|
} else {
|
||||||
|
this.rxOsc.setMIDINote(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begin buzzing at time
|
* Begin buzzing at time
|
||||||
*
|
*
|
||||||
|
@ -250,17 +295,20 @@ class ToneBuzzer extends AudioBuzzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class TelegraphBuzzer extends AudioBuzzer{
|
class TelegraphBuzzer extends AudioBuzzer{
|
||||||
constructor(gain=0.6) {
|
/**
|
||||||
super()
|
*
|
||||||
|
* @param {AudioContext} context
|
||||||
|
*/
|
||||||
|
constructor(context) {
|
||||||
|
super(context)
|
||||||
|
this.hum = new Oscillator(this.context, 140, 0.005, "sawtooth")
|
||||||
|
|
||||||
this.gainNode = BuzzerAudioContext.createGain()
|
this.closeSample = new Sample(this.context, "../assets/telegraph-a.mp3")
|
||||||
this.gainNode.connect(BuzzerAudioContext.destination)
|
this.openSample = new Sample(this.context, "../assets/telegraph-b.mp3")
|
||||||
this.gainNode.gain.value = gain
|
|
||||||
|
|
||||||
this.hum = new Oscillator(140, 0.005, "sawtooth")
|
this.hum.connect(this.masterGain)
|
||||||
|
this.closeSample.connect(this.masterGain)
|
||||||
this.closeSample = new Sample("../assets/telegraph-a.mp3")
|
this.openSample.connect(this.masterGain)
|
||||||
this.openSample = new Sample("../assets/telegraph-b.mp3")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Buzz(tx, when=0) {
|
async Buzz(tx, when=0) {
|
||||||
|
@ -281,8 +329,12 @@ class TelegraphBuzzer extends AudioBuzzer{
|
||||||
}
|
}
|
||||||
|
|
||||||
class LampBuzzer extends Buzzer {
|
class LampBuzzer extends Buzzer {
|
||||||
constructor() {
|
/**
|
||||||
super()
|
*
|
||||||
|
* @param {AudioContext} context
|
||||||
|
*/
|
||||||
|
constructor(context) {
|
||||||
|
super(context)
|
||||||
this.elements = document.querySelectorAll(".recv-lamp")
|
this.elements = document.querySelectorAll(".recv-lamp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,9 +377,13 @@ class LampBuzzer extends Buzzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MIDIBuzzer extends Buzzer {
|
class MIDIBuzzer extends Buzzer {
|
||||||
constructor() {
|
/**
|
||||||
super()
|
*
|
||||||
this.SetNote(69) // A4; 440Hz
|
* @param {AudioContext} context
|
||||||
|
*/
|
||||||
|
constructor(context) {
|
||||||
|
super(context)
|
||||||
|
this.SetMIDINote(69) // A4; 440Hz
|
||||||
|
|
||||||
this.midiAccess = {outputs: []} // stub while we wait for async stuff
|
this.midiAccess = {outputs: []} // stub while we wait for async stuff
|
||||||
if (navigator.requestMIDIAccess) {
|
if (navigator.requestMIDIAccess) {
|
||||||
|
@ -381,10 +437,13 @@ class MIDIBuzzer extends Buzzer {
|
||||||
this.sendAt(when, [0x80, this.note, 0x7f])
|
this.sendAt(when, [0x80, this.note, 0x7f])
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Set note to transmit
|
* Set MIDI note for tx/rx tone
|
||||||
|
*
|
||||||
|
* @param {Boolean} tx True to set transmit note
|
||||||
|
* @param {Number} note MIDI note to send
|
||||||
*/
|
*/
|
||||||
SetNote(tx, note) {
|
SetMIDINote(tx, note) {
|
||||||
if (tx) {
|
if (tx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -392,20 +451,23 @@ class MIDIBuzzer extends Buzzer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class Collection extends AudioSource {
|
||||||
* Block until the audio system is able to start making noise.
|
/**
|
||||||
|
*
|
||||||
|
* @param {AudioContext} context Audio Context
|
||||||
*/
|
*/
|
||||||
async function AudioReady() {
|
constructor(context) {
|
||||||
await BuzzerAudioContext.resume()
|
super(context)
|
||||||
}
|
this.tone = new ToneBuzzer(this.context)
|
||||||
|
this.telegraph = new TelegraphBuzzer(this.context)
|
||||||
class Collection {
|
this.lamp = new LampBuzzer(this.context)
|
||||||
constructor() {
|
this.midi = new MIDIBuzzer(this.context)
|
||||||
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])
|
this.collection = new Set([this.tone, this.lamp, this.midi])
|
||||||
|
|
||||||
|
this.tone.connect(this.masterGain)
|
||||||
|
this.telegraph.connect(this.masterGain)
|
||||||
|
this.lamp.connect(this.masterGain)
|
||||||
|
this.midi.connect(this.masterGain)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -414,6 +476,7 @@ class Collection {
|
||||||
* @param {string} audioType "telegraph" for telegraph mode, otherwise tone mode
|
* @param {string} audioType "telegraph" for telegraph mode, otherwise tone mode
|
||||||
*/
|
*/
|
||||||
SetAudioType(audioType) {
|
SetAudioType(audioType) {
|
||||||
|
this.Panic()
|
||||||
this.collection.delete(this.telegraph)
|
this.collection.delete(this.telegraph)
|
||||||
this.collection.delete(this.tone)
|
this.collection.delete(this.tone)
|
||||||
if (audioType == "telegraph") {
|
if (audioType == "telegraph") {
|
||||||
|
@ -435,7 +498,7 @@ class Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Silence all outputs.
|
* Silence all outputs in a single direction.
|
||||||
*
|
*
|
||||||
* @param tx True if transmitting
|
* @param tx True if transmitting
|
||||||
*/
|
*/
|
||||||
|
@ -445,6 +508,27 @@ class Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Silence all outputs.
|
||||||
|
*/
|
||||||
|
Panic() {
|
||||||
|
this.Silence(true)
|
||||||
|
this.Silence(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Boolean} tx True to set transmit tone
|
||||||
|
* @param {Number} note MIDI note to set
|
||||||
|
*/
|
||||||
|
SetMIDINote(tx, note) {
|
||||||
|
for (let b of this.collection) {
|
||||||
|
if (b.SetMIDINote) {
|
||||||
|
b.SetMIDINote(tx, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buzz for a certain duration at a certain time
|
* Buzz for a certain duration at a certain time
|
||||||
*
|
*
|
||||||
|
@ -472,4 +556,4 @@ class Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {AudioReady, Collection}
|
export {Collection}
|
||||||
|
|
|
@ -4,12 +4,18 @@ 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"
|
||||||
import * as I18n from "./i18n.mjs"
|
import * as I18n from "./i18n.mjs"
|
||||||
|
import * as Music from "./music.mjs"
|
||||||
|
|
||||||
const DefaultRepeater = "General"
|
const DefaultRepeater = "General"
|
||||||
const Millisecond = 1
|
const Millisecond = 1
|
||||||
const Second = 1000 * Millisecond
|
const Second = 1000 * Millisecond
|
||||||
const Minute = 60 * Second
|
const Minute = 60 * Second
|
||||||
|
|
||||||
|
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 globalAudioContext = new AudioContext({
|
||||||
|
latencyHint: "interactive",
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pop up a message, using an notification.
|
* Pop up a message, using an notification.
|
||||||
*
|
*
|
||||||
|
@ -39,7 +45,8 @@ class VailClient {
|
||||||
this.beginTxTime = null // Time when we began transmitting
|
this.beginTxTime = null // Time when we began transmitting
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
this.outputs = new Outputs.Collection()
|
this.outputs = new Outputs.Collection(globalAudioContext)
|
||||||
|
this.outputs.connect(globalAudioContext.destination)
|
||||||
|
|
||||||
// Keyers
|
// Keyers
|
||||||
this.straightKeyer = new Keyers.Keyers.straight(this)
|
this.straightKeyer = new Keyers.Keyers.straight(this)
|
||||||
|
@ -50,6 +57,13 @@ class VailClient {
|
||||||
// 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 = new Inputs.Collection(this)
|
this.inputs = new Inputs.Collection(this)
|
||||||
|
|
||||||
|
// If the user clicks anything, try immediately to resume the audio context
|
||||||
|
document.body.addEventListener(
|
||||||
|
"click",
|
||||||
|
e => globalAudioContext.resume(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
// Maximize button
|
// Maximize button
|
||||||
for (let e of document.querySelectorAll("button.maximize")) {
|
for (let e of document.querySelectorAll("button.maximize")) {
|
||||||
e.addEventListener("click", e => this.maximize(e))
|
e.addEventListener("click", e => this.maximize(e))
|
||||||
|
@ -73,6 +87,19 @@ class VailClient {
|
||||||
this.inputInit("#rx-delay", e => {
|
this.inputInit("#rx-delay", e => {
|
||||||
this.rxDelay = e.target.value * Second
|
this.rxDelay = e.target.value * Second
|
||||||
})
|
})
|
||||||
|
this.inputInit("#masterGain", e => {
|
||||||
|
this.outputs.SetGain(e.target.value / 100)
|
||||||
|
})
|
||||||
|
this.inputInit(
|
||||||
|
"#rx-tone",
|
||||||
|
e => this.outputs.SetMIDINote(false, e.target.value),
|
||||||
|
Music.MIDINoteName,
|
||||||
|
)
|
||||||
|
this.inputInit(
|
||||||
|
"#tx-tone",
|
||||||
|
e => this.outputs.SetMIDINote(true, e.target.value),
|
||||||
|
Music.MIDINoteName,
|
||||||
|
)
|
||||||
this.inputInit("#telegraph-buzzer", e => {
|
this.inputInit("#telegraph-buzzer", e => {
|
||||||
this.setTelegraphBuzzer(e.target.checked)
|
this.setTelegraphBuzzer(e.target.checked)
|
||||||
})
|
})
|
||||||
|
@ -86,10 +113,11 @@ 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
|
||||||
Outputs.AudioReady()
|
globalAudioContext.resume()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Audio context ready")
|
for (let e of document.querySelectorAll(".muted")) {
|
||||||
document.querySelector("#muted").classList.add("is-hidden")
|
e.classList.add("is-hidden")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,8 +328,9 @@ class VailClient {
|
||||||
*
|
*
|
||||||
* @param {string} selector CSS path to the element
|
* @param {string} selector CSS path to the element
|
||||||
* @param {function} callback Callback to call with any new value that is set
|
* @param {function} callback Callback to call with any new value that is set
|
||||||
|
* @param {function} transform A function to transform the value into displayed text
|
||||||
*/
|
*/
|
||||||
inputInit(selector, callback) {
|
inputInit(selector, callback, transform=null) {
|
||||||
let element = document.querySelector(selector)
|
let element = document.querySelector(selector)
|
||||||
if (!element) {
|
if (!element) {
|
||||||
console.warn("Unable to find an input to init", selector)
|
console.warn("Unable to find an input to init", selector)
|
||||||
|
@ -322,8 +351,12 @@ class VailClient {
|
||||||
}
|
}
|
||||||
localStorage[element.id] = value
|
localStorage[element.id] = value
|
||||||
|
|
||||||
|
let displayValue = value
|
||||||
|
if (transform) {
|
||||||
|
displayValue = transform(value)
|
||||||
|
}
|
||||||
if (outputElement) {
|
if (outputElement) {
|
||||||
outputElement.value = value
|
outputElement.value = displayValue
|
||||||
}
|
}
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(e)
|
callback(e)
|
||||||
|
|
Loading…
Reference in New Issue