mirror of https://github.com/nealey/vail.git
Compare commits
No commits in common. "f8b5cf22afc69062f67e6855b0f848e01664113e" and "f2302bff2a371d745c38384cbd3cade3660c96ff" have entirely different histories.
f8b5cf22af
...
f2302bff2a
|
@ -8,6 +8,6 @@ case "$1" in
|
||||||
docker -H ssh://melville.woozle.org service update --image ghcr.io/nealey/vail:main melville_vail
|
docker -H ssh://melville.woozle.org service update --image ghcr.io/nealey/vail:main melville_vail
|
||||||
;;
|
;;
|
||||||
"")
|
"")
|
||||||
rsync --delete -va static/ melville.woozle.org:/srv/vail/testing/
|
rsync -va static/ melville.woozle.org:/srv/vail/testing/
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
@ -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 class="muted">
|
<i id="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>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.9 KiB |
|
@ -1,156 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:bx="https://boxy-svg.com"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
viewBox="0 0 500 500"
|
|
||||||
version="1.1"
|
|
||||||
id="svg37"
|
|
||||||
sodipodi:docname="vail-rx.svg"
|
|
||||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
|
||||||
<metadata
|
|
||||||
id="metadata41">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1295"
|
|
||||||
inkscape:window-height="808"
|
|
||||||
id="namedview39"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="1.102"
|
|
||||||
inkscape:cx="107.57784"
|
|
||||||
inkscape:cy="249.56755"
|
|
||||||
inkscape:window-x="19"
|
|
||||||
inkscape:window-y="13"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="g11" />
|
|
||||||
<defs
|
|
||||||
id="defs5">
|
|
||||||
<mask
|
|
||||||
id="mask-0">
|
|
||||||
<rect
|
|
||||||
x="234.28"
|
|
||||||
y="129.248"
|
|
||||||
width="247.019"
|
|
||||||
height="167.227"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
id="rect2" />
|
|
||||||
</mask>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
style=""
|
|
||||||
transform="matrix(0.95245, 0, 0, 1.139872, 6.889973, -7.454849)"
|
|
||||||
id="g11">
|
|
||||||
<path
|
|
||||||
d="M 327.185 337.24 L 360.06 463.215 L 214.596 463.215 L 327.185 337.24 Z"
|
|
||||||
style="fill:#ffa500;fill-opacity:1"
|
|
||||||
transform="matrix(0.859836, -0.51057, 0.51057, 0.859836, -104.141418, 118.932152)"
|
|
||||||
bx:shape="triangle 214.596 337.24 145.464 125.975 0.774 0 1@97556a29"
|
|
||||||
id="path7" />
|
|
||||||
<rect
|
|
||||||
x="24.773"
|
|
||||||
y="44.071"
|
|
||||||
width="460.948"
|
|
||||||
height="294.539"
|
|
||||||
style="fill:#ffa500;fill-opacity:1"
|
|
||||||
rx="66.036"
|
|
||||||
ry="66.036"
|
|
||||||
id="rect9" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);"
|
|
||||||
d="M 296.4 154.545"
|
|
||||||
id="path13" />
|
|
||||||
<g
|
|
||||||
transform="matrix(1.584063, 0, 0, 1.70527, -312.019287, -153.773422)"
|
|
||||||
style="mask: url(#mask-0);"
|
|
||||||
mask="url(#mask-0)"
|
|
||||||
id="g35">
|
|
||||||
<rect
|
|
||||||
x="43.405"
|
|
||||||
y="231.448"
|
|
||||||
width="284.629"
|
|
||||||
height="38.553"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
rx="7.898"
|
|
||||||
ry="7.898"
|
|
||||||
id="rect15" />
|
|
||||||
<path
|
|
||||||
d="M 55.235 167.548 L 306.103 167.548 L 308.543 167.548 L 310.582 168.864 L 360.729 201.228 L 448.168 201.228 L 448.168 217.228 L 434.05 217.228 L 358.289 217.228 L 355.848 217.228 L 353.809 215.912 L 303.663 183.548 L 55.235 183.548 L 55.235 167.548 Z"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
id="path17" />
|
|
||||||
<rect
|
|
||||||
x="412.367"
|
|
||||||
y="179.765"
|
|
||||||
width="32.602"
|
|
||||||
height="27.855"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
id="rect19" />
|
|
||||||
<path
|
|
||||||
d="M 383.813 153.485 C 388.547 139.021 466.546 139.043 472.225 153.485 C 477.511 166.926 445.092 184.036 445.092 184.036 L 412.726 184.036 C 412.726 184.036 379.124 167.804 383.813 153.485 Z"
|
|
||||||
style="stroke-linejoin: round; fill: rgb(255, 255, 255);"
|
|
||||||
id="path21" />
|
|
||||||
<path
|
|
||||||
d="M 257.196 232.936 L 280.351 232.936 L 273.245 196.76 L 264.768 196.76 L 257.196 232.936 Z"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
id="path23" />
|
|
||||||
<rect
|
|
||||||
x="119.534"
|
|
||||||
y="151.839"
|
|
||||||
width="36.544"
|
|
||||||
height="98.388"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
rx="10.068"
|
|
||||||
ry="10.068"
|
|
||||||
id="rect25" />
|
|
||||||
<rect
|
|
||||||
x="265.233"
|
|
||||||
y="170.764"
|
|
||||||
width="7.081"
|
|
||||||
height="19.702"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
id="rect27" />
|
|
||||||
<rect
|
|
||||||
y="201.262"
|
|
||||||
width="16.756"
|
|
||||||
height="33.909"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
x="61.162"
|
|
||||||
id="rect29" />
|
|
||||||
<rect
|
|
||||||
x="66"
|
|
||||||
y="163.211"
|
|
||||||
width="7.081"
|
|
||||||
height="31.292"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
id="rect31" />
|
|
||||||
<rect
|
|
||||||
y="156.105"
|
|
||||||
width="16.756"
|
|
||||||
height="8.523"
|
|
||||||
style="fill: rgb(255, 255, 255);"
|
|
||||||
x="61.162"
|
|
||||||
id="rect33" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.5 KiB |
|
@ -19,8 +19,8 @@
|
||||||
|
|
||||||
<!-- Vail stuff -->
|
<!-- Vail stuff -->
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="icon" href="assets/vail.png" data-rx="assets/vail-rx.png" sizes="256x256" type="image/png">
|
<link rel="icon" href="assets/vail.png" sizes="256x256" type="image/png">
|
||||||
<link rel="icon" href="assets/vail.svg" data-rx="assets/vail-rx.svg" sizes="any" type="image/svg+xml">
|
<link rel="icon" href="assets/vail.svg" sizes="any" type="image/svg+xml">
|
||||||
<script type="module" src="scripts/vail.mjs"></script>
|
<script type="module" src="scripts/vail.mjs"></script>
|
||||||
<script type="module" src="scripts/ui.mjs"></script>
|
<script type="module" src="scripts/ui.mjs"></script>
|
||||||
<link rel="stylesheet" href="vail.css">
|
<link rel="stylesheet" href="vail.css">
|
||||||
|
@ -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 muted"></i>
|
<i class="mdi mdi-volume-off" id="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,91 +246,6 @@
|
||||||
</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" data-transform="note"></output>
|
|
||||||
<output for="tx-tone" data-transform="freq"></output>Hz
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field-body">
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
id="tx-tone"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="127"
|
|
||||||
value="72"
|
|
||||||
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" data-transform="note"></output>
|
|
||||||
<output for="rx-tone" data-transform="freq"></output>Hz
|
|
||||||
</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">
|
||||||
|
@ -338,29 +253,6 @@
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="field is-horizontal">
|
|
||||||
<div class="field-label">
|
|
||||||
<label class="label">
|
|
||||||
<span data-i18n="label.gain">noise</span>:
|
|
||||||
<output for="noiseGain"></output>%
|
|
||||||
<i class="mdi mdi-volume-off muted"></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field-body">
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
id="noiseGain"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value="0"
|
|
||||||
step="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
/**
|
|
||||||
* @file Provides some base audio tools.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as time from "./time.mjs"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the special "Audio Context" time
|
|
||||||
*
|
|
||||||
* This is is a duration from now, in seconds.
|
|
||||||
*
|
|
||||||
* @param {AudioContext} context
|
|
||||||
* @param {Date} when Date to compute
|
|
||||||
* @returns audiocontext time
|
|
||||||
*/
|
|
||||||
function AudioContextTime(context, when) {
|
|
||||||
if (!when) return 0
|
|
||||||
let acOffset = Date.now() - (context.currentTime * time.Second)
|
|
||||||
return Math.max(when - acOffset, 0) / time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
class AudioSource {
|
|
||||||
/**
|
|
||||||
* A generic audio source
|
|
||||||
*
|
|
||||||
* @param {AudioContext} context
|
|
||||||
*/
|
|
||||||
constructor(context) {
|
|
||||||
this.context = context
|
|
||||||
this.masterGain = new GainNode(this.context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to an audio node
|
|
||||||
*
|
|
||||||
* @param {AudioNode} destinationNode
|
|
||||||
*/
|
|
||||||
connect(destinationNode) {
|
|
||||||
this.masterGain.connect(destinationNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the master gain for this audio source.
|
|
||||||
*
|
|
||||||
* @param {Number} value New gain value
|
|
||||||
*/
|
|
||||||
SetGain(value) {
|
|
||||||
this.masterGain.gain.value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
AudioContextTime,
|
|
||||||
AudioSource,
|
|
||||||
}
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as time from "./time.mjs"
|
/** @typedef {Number} Duration */
|
||||||
|
const Millisecond = 1
|
||||||
|
const Second = 1000 * Millisecond
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chart of historical values.
|
* A chart of historical values.
|
||||||
|
@ -12,7 +14,7 @@ class HistoryChart {
|
||||||
* @param {string} style style to draw in; falls back to the `data-style` attribute
|
* @param {string} style style to draw in; falls back to the `data-style` attribute
|
||||||
* @param {Duration} duration Time to display history for
|
* @param {Duration} duration Time to display history for
|
||||||
*/
|
*/
|
||||||
constructor(canvas, style=null, duration=20*time.Second) {
|
constructor(canvas, style=null, duration=20*Second) {
|
||||||
this.canvas = canvas
|
this.canvas = canvas
|
||||||
this.ctx = canvas.getContext("2d")
|
this.ctx = canvas.getContext("2d")
|
||||||
this.duration = duration
|
this.duration = duration
|
||||||
|
@ -20,7 +22,7 @@ class HistoryChart {
|
||||||
this.data = []
|
this.data = []
|
||||||
|
|
||||||
// One canvas pixel = 20ms
|
// One canvas pixel = 20ms
|
||||||
canvas.width = duration / (20 * time.Millisecond)
|
canvas.width = duration / (20 * Millisecond)
|
||||||
|
|
||||||
// Set origin to lower-left corner
|
// Set origin to lower-left corner
|
||||||
this.ctx.scale(1, -1)
|
this.ctx.scale(1, -1)
|
||||||
|
@ -111,7 +113,7 @@ class HistoryChart {
|
||||||
* @param duration duration of chart window
|
* @param duration duration of chart window
|
||||||
* @returns new chart, or null if it couldn't find a canvas
|
* @returns new chart, or null if it couldn't find a canvas
|
||||||
*/
|
*/
|
||||||
function FromSelector(selector, style, duration=20*time.Second) {
|
function FromSelector(selector, style, duration=20*Second) {
|
||||||
let canvas = document.querySelector(selector)
|
let canvas = document.querySelector(selector)
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
return new HistoryChart(canvas, style, duration)
|
return new HistoryChart(canvas, style, duration)
|
||||||
|
|
|
@ -6,13 +6,15 @@
|
||||||
*
|
*
|
||||||
* @typedef {number} Duration
|
* @typedef {number} Duration
|
||||||
*/
|
*/
|
||||||
/** @type {Duration} */
|
|
||||||
const Millisecond = 1
|
|
||||||
/** @type {Duration} */
|
|
||||||
const Second = 1000 * Millisecond
|
|
||||||
/** @type {Duration} */
|
|
||||||
const Minute = 60 * Second
|
|
||||||
/** @type {Duration} */
|
|
||||||
const Hour = 60 * Minute
|
|
||||||
|
|
||||||
export {Millisecond, Second, Minute, Hour}
|
/** @type {Duration} */
|
||||||
|
export const Millisecond = 1
|
||||||
|
|
||||||
|
/** @type {Duration} */
|
||||||
|
export const Second = 1000 * Millisecond
|
||||||
|
|
||||||
|
/** @type {Duration} */
|
||||||
|
export const Minute = 60 * Second
|
||||||
|
|
||||||
|
/** @type {Duration} */
|
||||||
|
export const Hour = 60 * Minute
|
|
@ -1,50 +0,0 @@
|
||||||
import * as time from "./time.mjs"
|
|
||||||
|
|
||||||
const defaultIcon = "default"
|
|
||||||
|
|
||||||
class Icon {
|
|
||||||
/**
|
|
||||||
* @param {Number} timeoutDuration Duration of timeout
|
|
||||||
*/
|
|
||||||
constructor(timeoutDuration = 2*time.Second) {
|
|
||||||
this.timeoutDuration = timeoutDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the icon type
|
|
||||||
*
|
|
||||||
* @param {String} iconType Icon to set to
|
|
||||||
*/
|
|
||||||
Set(iconType=defaultIcon) {
|
|
||||||
if (iconType != defaultIcon) {
|
|
||||||
clearTimeout(this.cleanupTimer)
|
|
||||||
this.cleanupTimer = setTimeout(() => this.Set(), this.timeoutDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let e of document.querySelectorAll("link[rel=icon]")) {
|
|
||||||
if (! e.dataset[defaultIcon]) {
|
|
||||||
e.dataset[defaultIcon] = e.href
|
|
||||||
}
|
|
||||||
let url = e.dataset[iconType]
|
|
||||||
if (url) {
|
|
||||||
e.href = url
|
|
||||||
} else {
|
|
||||||
console.warn(`No data-${iconType} attribute`, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set icon at the provided time.
|
|
||||||
*
|
|
||||||
* @param {String} iconType Icon to set to
|
|
||||||
* @param {Number} when Time to set the value
|
|
||||||
*/
|
|
||||||
SetAt(iconType, when=null) {
|
|
||||||
setTimeout(() => this.Set(iconType), when - Date.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Icon,
|
|
||||||
}
|
|
|
@ -137,9 +137,6 @@ export class MIDI extends Input{
|
||||||
this.midiStateChange()
|
this.midiStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you're looking for the thing that sets the tx tone,
|
|
||||||
// that's in outputs.mjs:SetMIDINote
|
|
||||||
|
|
||||||
sendState() {
|
sendState() {
|
||||||
for (let output of this.midiAccess.outputs.values()) {
|
for (let output of this.midiAccess.outputs.values()) {
|
||||||
// Turn off keyboard mode
|
// Turn off keyboard mode
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as RoboKeyer from "./robokeyer.mjs"
|
import * as RoboKeyer from "./robokeyer.mjs"
|
||||||
import * as time from "./time.mjs"
|
|
||||||
|
|
||||||
/** Silent period between dits and dash */
|
/** Silent period between dits and dash */
|
||||||
const PAUSE = -1
|
const PAUSE = -1
|
||||||
|
@ -16,6 +15,24 @@ const DIT = 1
|
||||||
/** Length of a dah */
|
/** Length of a dah */
|
||||||
const DAH = 3
|
const DAH = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A time duration.
|
||||||
|
*
|
||||||
|
* JavaScript uses milliseconds in most (but not all) places.
|
||||||
|
* I've found it helpful to be able to multiply by a unit, so it's clear what's going on.
|
||||||
|
*
|
||||||
|
* @typedef {number} Duration
|
||||||
|
*/
|
||||||
|
/** @type {Duration} */
|
||||||
|
const Millisecond = 1
|
||||||
|
/** @type {Duration} */
|
||||||
|
const Second = 1000 * Millisecond
|
||||||
|
/** @type {Duration} */
|
||||||
|
const Minute = 60 * Second
|
||||||
|
/** @type {Duration} */
|
||||||
|
const Hour = 60 * Minute
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue Set: A Set you can shift and pop.
|
* Queue Set: A Set you can shift and pop.
|
||||||
*
|
*
|
||||||
|
@ -168,7 +185,7 @@ class BugKeyer extends StraightKeyer {
|
||||||
|
|
||||||
Reset() {
|
Reset() {
|
||||||
super.Reset()
|
super.Reset()
|
||||||
this.SetDitDuration(100 * time.Millisecond)
|
this.SetDitDuration(100 * Millisecond)
|
||||||
if (this.pulseTimer) {
|
if (this.pulseTimer) {
|
||||||
clearInterval(this.pulseTimer)
|
clearInterval(this.pulseTimer)
|
||||||
this.pulseTimer = null
|
this.pulseTimer = null
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
/**
|
|
||||||
* @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, precision=0) {
|
|
||||||
let freq = Cn1_freq * Math.pow(semitone, note)
|
|
||||||
return freq.toFixed(precision)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,126 +0,0 @@
|
||||||
import {AudioSource, AudioContextTime} from "./audio.mjs"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a white noise generator with a biquad filter
|
|
||||||
*
|
|
||||||
* @param {AudioContext} context Audio context
|
|
||||||
* @returns {BiquadFilterNode} Noise filter
|
|
||||||
*/
|
|
||||||
function WhiteNoise(context) {
|
|
||||||
let bufferSize = 17 * context.sampleRate
|
|
||||||
let noiseBuffer = new AudioBuffer({
|
|
||||||
sampleRate: context.sampleRate,
|
|
||||||
length: bufferSize,
|
|
||||||
})
|
|
||||||
let output = noiseBuffer.getChannelData(0)
|
|
||||||
for (let i = 0; i < bufferSize; i++) {
|
|
||||||
output[i] = Math.random() * 2 - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
let whiteNoise = context.createBufferSource()
|
|
||||||
whiteNoise.buffer = noiseBuffer
|
|
||||||
whiteNoise.loop = true
|
|
||||||
whiteNoise.start(0)
|
|
||||||
|
|
||||||
let noiseFilter = new BiquadFilterNode(context, {type: "bandpass"})
|
|
||||||
whiteNoise.connect(noiseFilter)
|
|
||||||
|
|
||||||
return noiseFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
class Noise extends AudioSource {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {AudioContext} context
|
|
||||||
*/
|
|
||||||
constructor(context, noises=2) {
|
|
||||||
super(context)
|
|
||||||
|
|
||||||
this.whiteNoise = []
|
|
||||||
|
|
||||||
for (let i = 0; i < noises; i++) {
|
|
||||||
let wn = {
|
|
||||||
modulator: new OscillatorNode(context),
|
|
||||||
modulatorGain: new GainNode(context),
|
|
||||||
filter: WhiteNoise(context),
|
|
||||||
filterGain: new GainNode(context),
|
|
||||||
filterGainModulator: new OscillatorNode(context),
|
|
||||||
filterGainModulatorGain: new GainNode(context),
|
|
||||||
}
|
|
||||||
|
|
||||||
wn.modulator.frequency.value = 0
|
|
||||||
wn.modulatorGain.gain.value = 0
|
|
||||||
wn.filter.frequency.value = 800
|
|
||||||
wn.filterGain.gain.value = 0.8 / noises
|
|
||||||
wn.filterGainModulator.frequency.value = 1
|
|
||||||
wn.filterGainModulatorGain.gain.value = 1
|
|
||||||
|
|
||||||
wn.modulator.connect(wn.modulatorGain)
|
|
||||||
wn.modulatorGain.connect(wn.filter.frequency)
|
|
||||||
wn.filter.connect(this.masterGain)
|
|
||||||
|
|
||||||
wn.modulator.start()
|
|
||||||
this.whiteNoise.push(wn)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.SetNoiseParams(0, 0.07, 70, 400, 0.0)
|
|
||||||
this.SetNoiseParams(1, 0.03, 200, 1600, 0.4)
|
|
||||||
this.masterGain.gain.value = 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set modulator frequency
|
|
||||||
*
|
|
||||||
* You probably want this to be under 1Hz, for a subtle sweeping effect
|
|
||||||
*
|
|
||||||
* @param {Number} n Which noise generator
|
|
||||||
* @param {Number} frequency Frequency (Hz)
|
|
||||||
*/
|
|
||||||
SetNoiseModulator(n, frequency) {
|
|
||||||
this.whiteNoise[n].modulator.frequency.value = frequency
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set modulator depth
|
|
||||||
*
|
|
||||||
* The output of the modulator [-1,1] is multiplied this and added to the
|
|
||||||
* base frequency of the filter.
|
|
||||||
*
|
|
||||||
* @param {Number} n Which noise generator
|
|
||||||
* @param {Number} depth Depth of modulation
|
|
||||||
*/
|
|
||||||
SetNoiseDepth(n, depth) {
|
|
||||||
this.whiteNoise[n].modulatorGain.gain.value = depth
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set noise filter base frequency
|
|
||||||
*
|
|
||||||
* @param {Number} n Which noise generator
|
|
||||||
* @param {Number} frequency Frequency (Hz)
|
|
||||||
*/
|
|
||||||
SetNoiseFrequency(n, frequency) {
|
|
||||||
this.whiteNoise[n].filter.frequency.value = frequency
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set gain of a noise generator
|
|
||||||
*
|
|
||||||
* @param {Number} n Which noise generator
|
|
||||||
* @param {Number} gain Gain level (typically [0-1])
|
|
||||||
*/
|
|
||||||
SetNoiseGain(n, gain) {
|
|
||||||
this.whiteNoise[n].filterGain.gain.value = gain
|
|
||||||
}
|
|
||||||
|
|
||||||
SetNoiseParams(n, modulatorFrequency, depth, baseFrequency, gain) {
|
|
||||||
this.SetNoiseModulator(n, modulatorFrequency)
|
|
||||||
this.SetNoiseDepth(n, depth)
|
|
||||||
this.SetNoiseFrequency(n, baseFrequency)
|
|
||||||
this.SetNoiseGain(n, gain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Noise,
|
|
||||||
}
|
|
|
@ -1,6 +1,3 @@
|
||||||
import {AudioSource, AudioContextTime} from "./audio.mjs"
|
|
||||||
import * as time from "./time.mjs"
|
|
||||||
|
|
||||||
const HIGH_FREQ = 555
|
const HIGH_FREQ = 555
|
||||||
const LOW_FREQ = 444
|
const LOW_FREQ = 444
|
||||||
|
|
||||||
|
@ -20,94 +17,77 @@ const LOW_FREQ = 444
|
||||||
* @typedef {number} Date
|
* @typedef {number} Date
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const Millisecond = 1
|
||||||
|
const Second = 1000 * Millisecond
|
||||||
|
|
||||||
/** The amount of time it should take an oscillator to ramp to and from zero gain
|
/** The amount of time it should take an oscillator to ramp to and from zero gain
|
||||||
*
|
*
|
||||||
* @constant {Duration}
|
* @constant {Duration}
|
||||||
*/
|
*/
|
||||||
const OscillatorRampDuration = 5*time.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 extends AudioSource {
|
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.
|
||||||
*
|
*
|
||||||
* @param {AudioContext} context Audio context
|
|
||||||
* @param {number} frequency Oscillator frequency (Hz)
|
* @param {number} frequency Oscillator frequency (Hz)
|
||||||
* @param {number} maxGain Maximum gain (volume) of this oscillator (0.0 - 1.0)
|
* @param {number} gain 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(context, frequency, maxGain = 0.5, type = "sine") {
|
constructor(frequency, gain = 0.5, type = "sine") {
|
||||||
super(context)
|
this.targetGain = gain
|
||||||
this.maxGain = maxGain
|
|
||||||
|
|
||||||
// Start quiet
|
this.gainNode = BuzzerAudioContext.createGain()
|
||||||
this.masterGain.gain.value = 0
|
this.gainNode.connect(BuzzerAudioContext.destination)
|
||||||
|
this.gainNode.gain.value = 0
|
||||||
|
|
||||||
this.osc = new OscillatorNode(this.context)
|
this.osc = BuzzerAudioContext.createOscillator()
|
||||||
this.osc.type = type
|
this.osc.type = type
|
||||||
this.osc.connect(this.masterGain)
|
this.osc.connect(this.gainNode)
|
||||||
this.setFrequency(frequency)
|
this.osc.frequency.value = 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 this.context.resume()
|
await BuzzerAudioContext.resume()
|
||||||
this.masterGain.gain.setTargetAtTime(
|
this.gainNode.gain.setTargetAtTime(
|
||||||
target,
|
target,
|
||||||
AudioContextTime(this.context, when),
|
BuzzerAudioContextTime(when),
|
||||||
timeConstant/time.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.maxGain, when, timeConstant)
|
return this.setTargetAtTime(this.targetGain, 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)
|
||||||
}
|
}
|
||||||
|
@ -116,20 +96,24 @@ class Oscillator extends AudioSource {
|
||||||
/**
|
/**
|
||||||
* A digital sample, loaded from a URL.
|
* A digital sample, loaded from a URL.
|
||||||
*/
|
*/
|
||||||
class Sample extends AudioSource {
|
class Sample {
|
||||||
/**
|
/**
|
||||||
* @param {AudioContext} context
|
*
|
||||||
* @param {string} url URL to resource
|
* @param {string} url URL to resource
|
||||||
|
* @param {number} gain Gain (0.0 - 1.0)
|
||||||
*/
|
*/
|
||||||
constructor(context, url) {
|
constructor(url, gain=0.5) {
|
||||||
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 this.context.decodeAudioData(buf)
|
this.data = await BuzzerAudioContext.decodeAudioData(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,24 +122,22 @@ class Sample extends AudioSource {
|
||||||
* @param {Date} when When to begin playback
|
* @param {Date} when When to begin playback
|
||||||
*/
|
*/
|
||||||
async PlayAt(when) {
|
async PlayAt(when) {
|
||||||
await this.context.resume()
|
await BuzzerAudioContext.resume()
|
||||||
await this.resume
|
await this.resume
|
||||||
let bs = new AudioBufferSourceNode(this.context)
|
let bs = BuzzerAudioContext.createBufferSource()
|
||||||
bs.buffer = this.data
|
bs.buffer = this.data
|
||||||
bs.connect(this.masterGain)
|
bs.connect(this.gainNode)
|
||||||
bs.start(AudioContextTime(this.context, when))
|
bs.start(BuzzerAudioContextTime(when))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A (mostly) virtual class defining a buzzer.
|
* A (mostly) virtual class defining a buzzer.
|
||||||
*/
|
*/
|
||||||
class Buzzer extends AudioSource {
|
class Buzzer {
|
||||||
/**
|
constructor() {
|
||||||
* @param {AudioContext} context
|
|
||||||
*/
|
|
||||||
constructor(context) {
|
|
||||||
super(context)
|
|
||||||
this.connected = true
|
this.connected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,66 +191,41 @@ class Buzzer extends AudioSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioBuzzer extends Buzzer {
|
class AudioBuzzer extends Buzzer {
|
||||||
/**
|
constructor(errorFreq=30) {
|
||||||
* A buzzer that make noise
|
super()
|
||||||
*
|
|
||||||
* @param {AudioContext} context
|
|
||||||
* @param {Number} errorFreq Error tone frequency (hz)
|
|
||||||
*/
|
|
||||||
constructor(context, errorFreq=30) {
|
|
||||||
super(context)
|
|
||||||
|
|
||||||
this.errorTone = new Oscillator(this.context, errorFreq, 0.1, "square")
|
this.errorGain = new Oscillator(errorFreq, 0.1, "square")
|
||||||
this.errorTone.connect(this.masterGain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Error() {
|
Error() {
|
||||||
let now = Date.now()
|
let now = Date.now()
|
||||||
this.errorTone.SoundAt(now)
|
this.errorGain.SoundAt(now)
|
||||||
this.errorTone.HushAt(now + 200*time.Millisecond)
|
this.errorGain.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 {
|
||||||
constructor(context, {txGain=0.5, highFreq=HIGH_FREQ, lowFreq=LOW_FREQ} = {}) {
|
// Buzzers keep two oscillators: one high and one low.
|
||||||
super(context)
|
// 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.
|
||||||
|
|
||||||
this.rxOsc = new Oscillator(this.context, lowFreq, txGain)
|
constructor({txGain=0.5, highFreq=HIGH_FREQ, lowFreq=LOW_FREQ} = {}) {
|
||||||
this.txOsc = new Oscillator(this.context, highFreq, txGain)
|
super()
|
||||||
|
|
||||||
this.rxOsc.connect(this.masterGain)
|
this.rxOsc = new Oscillator(lowFreq, txGain)
|
||||||
this.txOsc.connect(this.masterGain)
|
this.txOsc = new Oscillator(highFreq, txGain)
|
||||||
|
|
||||||
// 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(this.context, 1, 0.001)
|
this.bgOsc = new Oscillator(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
|
||||||
*
|
*
|
||||||
|
@ -293,20 +250,17 @@ 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.closeSample = new Sample(this.context, "../assets/telegraph-a.mp3")
|
this.gainNode = BuzzerAudioContext.createGain()
|
||||||
this.openSample = new Sample(this.context, "../assets/telegraph-b.mp3")
|
this.gainNode.connect(BuzzerAudioContext.destination)
|
||||||
|
this.gainNode.gain.value = gain
|
||||||
|
|
||||||
this.hum.connect(this.masterGain)
|
this.hum = new Oscillator(140, 0.005, "sawtooth")
|
||||||
this.closeSample.connect(this.masterGain)
|
|
||||||
this.openSample.connect(this.masterGain)
|
this.closeSample = new Sample("../assets/telegraph-a.mp3")
|
||||||
|
this.openSample = new Sample("../assets/telegraph-b.mp3")
|
||||||
}
|
}
|
||||||
|
|
||||||
async Buzz(tx, when=0) {
|
async Buzz(tx, when=0) {
|
||||||
|
@ -327,12 +281,8 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,13 +325,9 @@ class LampBuzzer extends Buzzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MIDIBuzzer extends Buzzer {
|
class MIDIBuzzer extends Buzzer {
|
||||||
/**
|
constructor() {
|
||||||
*
|
super()
|
||||||
* @param {AudioContext} context
|
this.SetNote(69) // A4; 440Hz
|
||||||
*/
|
|
||||||
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) {
|
||||||
|
@ -423,6 +369,7 @@ class MIDIBuzzer extends Buzzer {
|
||||||
if (tx) {
|
if (tx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendAt(when, [0x90, this.note, 0x7f])
|
this.sendAt(when, [0x90, this.note, 0x7f])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,38 +381,31 @@ class MIDIBuzzer extends Buzzer {
|
||||||
this.sendAt(when, [0x80, this.note, 0x7f])
|
this.sendAt(when, [0x80, this.note, 0x7f])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Set MIDI note for tx/rx tone
|
* Set note to transmit
|
||||||
*
|
*/
|
||||||
* @param {Boolean} tx True to set transmit note
|
SetNote(tx, note) {
|
||||||
* @param {Number} note MIDI note to send
|
|
||||||
*/
|
|
||||||
SetMIDINote(tx, note) {
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
this.sendAt(0, [0xB0, 0x02, note])
|
return
|
||||||
} else {
|
|
||||||
this.note = note
|
|
||||||
}
|
}
|
||||||
|
this.note = note
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Collection extends AudioSource {
|
/**
|
||||||
/**
|
* Block until the audio system is able to start making noise.
|
||||||
*
|
*/
|
||||||
* @param {AudioContext} context Audio Context
|
async function AudioReady() {
|
||||||
*/
|
await BuzzerAudioContext.resume()
|
||||||
constructor(context) {
|
}
|
||||||
super(context)
|
|
||||||
this.tone = new ToneBuzzer(this.context)
|
|
||||||
this.telegraph = new TelegraphBuzzer(this.context)
|
|
||||||
this.lamp = new LampBuzzer(this.context)
|
|
||||||
this.midi = new MIDIBuzzer(this.context)
|
|
||||||
this.collection = new Set([this.tone, this.lamp, this.midi])
|
|
||||||
|
|
||||||
this.tone.connect(this.masterGain)
|
class Collection {
|
||||||
this.telegraph.connect(this.masterGain)
|
constructor() {
|
||||||
this.lamp.connect(this.masterGain)
|
this.tone = new ToneBuzzer()
|
||||||
this.midi.connect(this.masterGain)
|
this.telegraph = new TelegraphBuzzer()
|
||||||
|
this.lamp = new LampBuzzer()
|
||||||
|
this.midi = new MIDIBuzzer()
|
||||||
|
this.collection = new Set([this.tone, this.lamp, this.midi])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -474,7 +414,6 @@ class Collection extends AudioSource {
|
||||||
* @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") {
|
||||||
|
@ -496,7 +435,7 @@ class Collection extends AudioSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Silence all outputs in a single direction.
|
* Silence all outputs.
|
||||||
*
|
*
|
||||||
* @param tx True if transmitting
|
* @param tx True if transmitting
|
||||||
*/
|
*/
|
||||||
|
@ -506,27 +445,6 @@ class Collection extends AudioSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
||||||
*
|
*
|
||||||
|
@ -554,4 +472,4 @@ class Collection extends AudioSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {Collection}
|
export {AudioReady, Collection}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import {GetFortune} from "./fortunes.mjs"
|
import {GetFortune} from "./fortunes.mjs"
|
||||||
import * as time from "./time.mjs"
|
|
||||||
|
const Millisecond = 1
|
||||||
|
const Second = 1000 * Millisecond
|
||||||
|
const Minute = 60 * Second
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two messages
|
* Compare two messages
|
||||||
|
@ -58,7 +61,7 @@ export class Vail {
|
||||||
msg => {
|
msg => {
|
||||||
this.rx(0, 0, {connected: false, notice: `Repeater disconnected: ${msg.reason}`})
|
this.rx(0, 0, {connected: false, notice: `Repeater disconnected: ${msg.reason}`})
|
||||||
console.error("Repeater connection dropped:", msg.reason)
|
console.error("Repeater connection dropped:", msg.reason)
|
||||||
setTimeout(() => this.reopen(), 2*time.Second)
|
setTimeout(() => this.reopen(), 2*Second)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -154,7 +157,7 @@ export class Vail {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Null {
|
export class Null {
|
||||||
constructor(rx, interval=3*time.Second) {
|
constructor(rx, interval=3*Second) {
|
||||||
this.rx = rx
|
this.rx = rx
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
@ -216,7 +219,7 @@ export class Fortune extends Null {
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
clearTimeout(this.timeout)
|
clearTimeout(this.timeout)
|
||||||
}
|
}
|
||||||
this.timeout = setTimeout(() => this.pulse(), 3 * time.Second)
|
this.timeout = setTimeout(() => this.pulse(), 3 * Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
Close() {
|
Close() {
|
||||||
|
|
|
@ -4,24 +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 time from "./time.mjs"
|
|
||||||
import * as Music from "./music.mjs"
|
|
||||||
import * as Icon from "./icon.mjs"
|
|
||||||
import * as Noise from "./noise.mjs"
|
|
||||||
|
|
||||||
const DefaultRepeater = "General"
|
const DefaultRepeater = "General"
|
||||||
|
const Millisecond = 1
|
||||||
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 Second = 1000 * Millisecond
|
||||||
const globalAudioContext = new AudioContext({
|
const Minute = 60 * Second
|
||||||
latencyHint: "interactive",
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
function toast(msg, timeout=4*time.Second) {
|
function toast(msg, timeout=4*Second) {
|
||||||
console.info(msg)
|
console.info(msg)
|
||||||
|
|
||||||
let errors = document.querySelector("#errors")
|
let errors = document.querySelector("#errors")
|
||||||
|
@ -41,19 +35,11 @@ class VailClient {
|
||||||
this.lagTimes = [0]
|
this.lagTimes = [0]
|
||||||
this.rxDurations = [0]
|
this.rxDurations = [0]
|
||||||
this.clockOffset = null // How badly our clock is off of the server's
|
this.clockOffset = null // How badly our clock is off of the server's
|
||||||
this.rxDelay = 0 * time.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
|
||||||
|
|
||||||
// Outputs
|
// Outputs
|
||||||
this.outputs = new Outputs.Collection(globalAudioContext)
|
this.outputs = new Outputs.Collection()
|
||||||
this.outputs.connect(globalAudioContext.destination)
|
|
||||||
|
|
||||||
// Noise
|
|
||||||
this.noise = new Noise.Noise(globalAudioContext)
|
|
||||||
this.noise.connect(globalAudioContext.destination)
|
|
||||||
|
|
||||||
// App icon
|
|
||||||
this.icon = new Icon.Icon()
|
|
||||||
|
|
||||||
// Keyers
|
// Keyers
|
||||||
this.straightKeyer = new Keyers.Keyers.straight(this)
|
this.straightKeyer = new Keyers.Keyers.straight(this)
|
||||||
|
@ -64,13 +50,6 @@ 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))
|
||||||
|
@ -83,7 +62,7 @@ class VailClient {
|
||||||
this.inputInit("#keyer-mode", e => this.setKeyer(e.target.value))
|
this.inputInit("#keyer-mode", e => this.setKeyer(e.target.value))
|
||||||
this.inputInit("#keyer-rate", e => {
|
this.inputInit("#keyer-rate", e => {
|
||||||
let rate = e.target.value
|
let rate = e.target.value
|
||||||
this.ditDuration = Math.round(time.Minute / rate / 50)
|
this.ditDuration = Math.round(Minute / rate / 50)
|
||||||
for (let e of document.querySelectorAll("[data-fill='keyer-ms']")) {
|
for (let e of document.querySelectorAll("[data-fill='keyer-ms']")) {
|
||||||
e.textContent = this.ditDuration
|
e.textContent = this.ditDuration
|
||||||
}
|
}
|
||||||
|
@ -92,31 +71,8 @@ class VailClient {
|
||||||
this.inputs.SetDitDuration(this.ditDuration)
|
this.inputs.SetDitDuration(this.ditDuration)
|
||||||
})
|
})
|
||||||
this.inputInit("#rx-delay", e => {
|
this.inputInit("#rx-delay", e => {
|
||||||
this.rxDelay = e.target.value * time.Second
|
this.rxDelay = e.target.value * Second
|
||||||
})
|
})
|
||||||
this.inputInit("#masterGain", e => {
|
|
||||||
this.outputs.SetGain(e.target.value / 100)
|
|
||||||
})
|
|
||||||
this.inputInit("#noiseGain", e => {
|
|
||||||
this.noise.SetGain(e.target.value / 100)
|
|
||||||
})
|
|
||||||
let toneTransform = {
|
|
||||||
note: Music.MIDINoteName,
|
|
||||||
freq: Music.MIDINoteFrequency,
|
|
||||||
}
|
|
||||||
this.inputInit(
|
|
||||||
"#rx-tone",
|
|
||||||
e => {
|
|
||||||
this.noise.SetNoiseFrequency(1, Music.MIDINoteFrequency(e.target.value))
|
|
||||||
this.outputs.SetMIDINote(false, e.target.value)
|
|
||||||
},
|
|
||||||
toneTransform,
|
|
||||||
)
|
|
||||||
this.inputInit(
|
|
||||||
"#tx-tone",
|
|
||||||
e => this.outputs.SetMIDINote(true, e.target.value),
|
|
||||||
toneTransform,
|
|
||||||
)
|
|
||||||
this.inputInit("#telegraph-buzzer", e => {
|
this.inputInit("#telegraph-buzzer", e => {
|
||||||
this.setTelegraphBuzzer(e.target.checked)
|
this.setTelegraphBuzzer(e.target.checked)
|
||||||
})
|
})
|
||||||
|
@ -130,11 +86,10 @@ 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
|
||||||
globalAudioContext.resume()
|
Outputs.AudioReady()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
for (let e of document.querySelectorAll(".muted")) {
|
console.log("Audio context ready")
|
||||||
e.classList.add("is-hidden")
|
document.querySelector("#muted").classList.add("is-hidden")
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,8 +137,6 @@ class VailClient {
|
||||||
|
|
||||||
Buzz() {
|
Buzz() {
|
||||||
this.outputs.Buzz(false)
|
this.outputs.Buzz(false)
|
||||||
this.icon.Set("rx")
|
|
||||||
|
|
||||||
if (this.rxChart) this.rxChart.Set(1)
|
if (this.rxChart) this.rxChart.Set(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,13 +148,7 @@ class VailClient {
|
||||||
BuzzDuration(tx, when, duration) {
|
BuzzDuration(tx, when, duration) {
|
||||||
this.outputs.BuzzDuration(tx, when, duration)
|
this.outputs.BuzzDuration(tx, when, duration)
|
||||||
|
|
||||||
let chart
|
let chart = tx?this.txChart:this.rxChart
|
||||||
if (tx) {
|
|
||||||
chart = this.txChart
|
|
||||||
} else {
|
|
||||||
chart = this.rxChart
|
|
||||||
this.icon.SetAt("rx", when)
|
|
||||||
}
|
|
||||||
if (chart) {
|
if (chart) {
|
||||||
chart.SetAt(1, when)
|
chart.SetAt(1, when)
|
||||||
chart.SetAt(0, when+duration)
|
chart.SetAt(0, when+duration)
|
||||||
|
@ -320,7 +267,6 @@ class VailClient {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Silence()
|
|
||||||
if (this.repeater) {
|
if (this.repeater) {
|
||||||
this.repeater.Close()
|
this.repeater.Close()
|
||||||
}
|
}
|
||||||
|
@ -354,9 +300,8 @@ 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 {Object.<string, function>} transform Transform functions
|
|
||||||
*/
|
*/
|
||||||
inputInit(selector, callback, transform={}) {
|
inputInit(selector, callback) {
|
||||||
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)
|
||||||
|
@ -368,7 +313,7 @@ class VailClient {
|
||||||
element.checked = (storedValue == "on")
|
element.checked = (storedValue == "on")
|
||||||
}
|
}
|
||||||
let id = element.id
|
let id = element.id
|
||||||
let outputElements = document.querySelectorAll(`[for="${id}"]`)
|
let outputElement = document.querySelector(`[for="${id}"]`)
|
||||||
|
|
||||||
element.addEventListener("input", e => {
|
element.addEventListener("input", e => {
|
||||||
let value = element.value
|
let value = element.value
|
||||||
|
@ -377,13 +322,8 @@ class VailClient {
|
||||||
}
|
}
|
||||||
localStorage[element.id] = value
|
localStorage[element.id] = value
|
||||||
|
|
||||||
for (let e of outputElements) {
|
if (outputElement) {
|
||||||
if (e.dataset.transform) {
|
outputElement.value = value
|
||||||
let tf = transform[e.dataset.transform]
|
|
||||||
e.value = tf(value)
|
|
||||||
} else {
|
|
||||||
e.value = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(e)
|
callback(e)
|
||||||
|
@ -485,7 +425,7 @@ class VailClient {
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
if (navigator.serviceWorker) {
|
if (navigator.serviceWorker) {
|
||||||
navigator.serviceWorker.register("scripts/sw.js")
|
navigator.serviceWorker.register("sw.js")
|
||||||
}
|
}
|
||||||
I18n.Setup()
|
I18n.Setup()
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -66,7 +66,7 @@ export const Xlat = {
|
||||||
"wpm": "WPM",
|
"wpm": "WPM",
|
||||||
"ms": "ms",
|
"ms": "ms",
|
||||||
"wiki": "Help",
|
"wiki": "Help",
|
||||||
"rx-delay": "rx delay",
|
"rx-delay": "receive delay",
|
||||||
"telegraph-sounds": "Telegraph sounds"
|
"telegraph-sounds": "Telegraph sounds"
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|
Loading…
Reference in New Issue