mirror of
https://github.com/parabirb/freeremote-client.git
synced 2025-08-02 05:42:27 -04:00
working SSB!
This commit is contained in:
parent
fe12d2e6a3
commit
d28399b51a
45
package-lock.json
generated
45
package-lock.json
generated
@ -10,9 +10,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"opus-decoder": "^0.7.7",
|
"opus-decoder": "^0.7.7",
|
||||||
"readable-stream": "^4.7.0",
|
"readable-stream": "^4.7.0",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"tcadif": "^2.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
@ -1290,6 +1292,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csv-parse": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/csv-stringify": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
"version": "5.0.17",
|
"version": "5.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.17.tgz",
|
||||||
@ -1480,6 +1494,12 @@
|
|||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@ -1819,6 +1839,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@ -2167,6 +2196,20 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tcadif": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tcadif/-/tcadif-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-20UPYtzjBH7NagwNzBtBwJ7PIHnZwjJSPx9UhxGfkzkgUC+iAWRZzH3pJW/7nut37r3ofIMDQtw+UV4BQigylA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"csv-parse": "^5.5.3",
|
||||||
|
"csv-stringify": "^6.4.5",
|
||||||
|
"moment": "^2.29.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tcadif": "bin/tcadif.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/totalist": {
|
"node_modules/totalist": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||||
|
@ -22,8 +22,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"opus-decoder": "^0.7.7",
|
"opus-decoder": "^0.7.7",
|
||||||
"readable-stream": "^4.7.0",
|
"readable-stream": "^4.7.0",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"tcadif": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,4 @@
|
|||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", system-ui, "Roboto", sans-serif;
|
--font-sans: "Inter", system-ui, "Roboto", sans-serif;
|
||||||
--font-mono: "IBM Plex Mono", SFMono-Regular, monospace;
|
--font-mono: "IBM Plex Mono", SFMono-Regular, monospace;
|
||||||
}
|
}
|
@ -17,6 +17,7 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>remoteham</title>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
530
src/lib/spectrogram.js
Normal file
530
src/lib/spectrogram.js
Normal file
@ -0,0 +1,530 @@
|
|||||||
|
// acquired from https://www.arc.id.au/Spectrogram.html
|
||||||
|
|
||||||
|
/*=================================================================
|
||||||
|
Filename: Spectrogram-2v01.js
|
||||||
|
Rev: 2
|
||||||
|
By: Dr A.R.Collins
|
||||||
|
Description: JavaScript graphics functions to draw Spectrograms.
|
||||||
|
|
||||||
|
Date Description By
|
||||||
|
-------|----------------------------------------------------|---
|
||||||
|
12Nov18 First beta ARC
|
||||||
|
17Nov18 Added offset into data buffer ARC
|
||||||
|
08May19 this.imageURL URL added
|
||||||
|
bugfix: fixed isNaN test
|
||||||
|
Changed sgStart, sgStop to start, stop
|
||||||
|
Added options object to constructors ARC
|
||||||
|
10May19 Enabled Left to Right as well as Top to Bottom ARC
|
||||||
|
11May19 Added RasterscanSVG ARC
|
||||||
|
12May19 Added blankline for horizontal raster scans ARC
|
||||||
|
13May19 Eliminated unnecessary putImageData ARC
|
||||||
|
14May19 Removed toDataURL, not used drawImage is better
|
||||||
|
bugfix: SVG RHC names swapped ARC
|
||||||
|
02Jun19 bugfix: startOfs not honored in horizontalNewLine ARC
|
||||||
|
03Jun19 Flipped the SVG and RHC names for waterfalls ARC
|
||||||
|
04Jun19 Unflip SVG and RHC for horizontal mode ARC
|
||||||
|
Swap "SVG" & "RHC" strings to match fn names ARC
|
||||||
|
05Jun19 bugfix: WaterfallSVG scrolling wrong way ARC
|
||||||
|
10Jun19 bugfix: support lineRate=0 for static display
|
||||||
|
bugfix: ipBufPtr must be a ptr to a ptr ARC
|
||||||
|
11Jun19 Make ipBuffers an Array of Arrays, if lineRate=0
|
||||||
|
use all buffers else use only ipBuffer[0] ARC
|
||||||
|
13Jun19 Use Waterfall and Rasterscan plus direction
|
||||||
|
Use Boolean rather than string compare ARC
|
||||||
|
16Jun19 Use const and let ARC
|
||||||
|
20Jun19 Change order of parameters ARC
|
||||||
|
21Jun19 Add setLineRate method ARC
|
||||||
|
06Jul19 Released as Rev 1v00 ARC
|
||||||
|
25Jul21 Refactor using class, arrow functions etc
|
||||||
|
Added RasterImage object
|
||||||
|
Use object.buffer as input not array of arrays ARC
|
||||||
|
25Jul21 Released as Rev 2v00 ARC
|
||||||
|
=================================================================*/
|
||||||
|
|
||||||
|
export var Waterfall, Rasterscan, RasterImage;
|
||||||
|
|
||||||
|
(function(){
|
||||||
|
class Spectrogram {
|
||||||
|
constructor(ipObj, w, h, sgMode, rhc, vert, options) {
|
||||||
|
const opt = (typeof options === 'object')? options: {}; // avoid undeclared object errors
|
||||||
|
const pxPerLine = w || 200;
|
||||||
|
const lines = h || 200;
|
||||||
|
const lineBuf = new ArrayBuffer(pxPerLine * 4); // 1 line
|
||||||
|
const lineBuf8 = new Uint8ClampedArray(lineBuf);
|
||||||
|
const lineImgData = new ImageData(lineBuf8, pxPerLine, 1); // 1 line of canvas pixels
|
||||||
|
const blankBuf = new ArrayBuffer(pxPerLine * 4); // 1 line
|
||||||
|
const blankBuf8 = new Uint8ClampedArray(blankBuf);
|
||||||
|
const blankImgData = new ImageData(blankBuf8, pxPerLine, 1); // 1 line of canvas pixels
|
||||||
|
const clearBuf = new ArrayBuffer(pxPerLine * lines * 4); // fills with 0s ie. rgba 0,0,0,0 = transparent
|
||||||
|
const clearBuf8 = new Uint8ClampedArray(clearBuf);
|
||||||
|
let offScreenCtx; // offscreen canvas drawing context
|
||||||
|
let clearImgData;
|
||||||
|
let lineRate = 30; // requested line rate for dynamic waterfalls
|
||||||
|
let interval = 0; // msec
|
||||||
|
let startOfs = 0;
|
||||||
|
let nextLine = 0;
|
||||||
|
let timerID = null;
|
||||||
|
let running = false;
|
||||||
|
let sgTime = 0;
|
||||||
|
let sgStartTime = 0;
|
||||||
|
|
||||||
|
// Matlab Jet ref: stackoverflow.com grayscale-to-red-green-blue-matlab-jet-color-scale
|
||||||
|
let colMap = [[ 0, 0, 128, 255], [ 0, 0, 131, 255], [ 0, 0, 135, 255], [ 0, 0, 139, 255],
|
||||||
|
[ 0, 0, 143, 255], [ 0, 0, 147, 255], [ 0, 0, 151, 255], [ 0, 0, 155, 255],
|
||||||
|
[ 0, 0, 159, 255], [ 0, 0, 163, 255], [ 0, 0, 167, 255], [ 0, 0, 171, 255],
|
||||||
|
[ 0, 0, 175, 255], [ 0, 0, 179, 255], [ 0, 0, 183, 255], [ 0, 0, 187, 255],
|
||||||
|
[ 0, 0, 191, 255], [ 0, 0, 195, 255], [ 0, 0, 199, 255], [ 0, 0, 203, 255],
|
||||||
|
[ 0, 0, 207, 255], [ 0, 0, 211, 255], [ 0, 0, 215, 255], [ 0, 0, 219, 255],
|
||||||
|
[ 0, 0, 223, 255], [ 0, 0, 227, 255], [ 0, 0, 231, 255], [ 0, 0, 235, 255],
|
||||||
|
[ 0, 0, 239, 255], [ 0, 0, 243, 255], [ 0, 0, 247, 255], [ 0, 0, 251, 255],
|
||||||
|
[ 0, 0, 255, 255], [ 0, 4, 255, 255], [ 0, 8, 255, 255], [ 0, 12, 255, 255],
|
||||||
|
[ 0, 16, 255, 255], [ 0, 20, 255, 255], [ 0, 24, 255, 255], [ 0, 28, 255, 255],
|
||||||
|
[ 0, 32, 255, 255], [ 0, 36, 255, 255], [ 0, 40, 255, 255], [ 0, 44, 255, 255],
|
||||||
|
[ 0, 48, 255, 255], [ 0, 52, 255, 255], [ 0, 56, 255, 255], [ 0, 60, 255, 255],
|
||||||
|
[ 0, 64, 255, 255], [ 0, 68, 255, 255], [ 0, 72, 255, 255], [ 0, 76, 255, 255],
|
||||||
|
[ 0, 80, 255, 255], [ 0, 84, 255, 255], [ 0, 88, 255, 255], [ 0, 92, 255, 255],
|
||||||
|
[ 0, 96, 255, 255], [ 0, 100, 255, 255], [ 0, 104, 255, 255], [ 0, 108, 255, 255],
|
||||||
|
[ 0, 112, 255, 255], [ 0, 116, 255, 255], [ 0, 120, 255, 255], [ 0, 124, 255, 255],
|
||||||
|
[ 0, 128, 255, 255], [ 0, 131, 255, 255], [ 0, 135, 255, 255], [ 0, 139, 255, 255],
|
||||||
|
[ 0, 143, 255, 255], [ 0, 147, 255, 255], [ 0, 151, 255, 255], [ 0, 155, 255, 255],
|
||||||
|
[ 0, 159, 255, 255], [ 0, 163, 255, 255], [ 0, 167, 255, 255], [ 0, 171, 255, 255],
|
||||||
|
[ 0, 175, 255, 255], [ 0, 179, 255, 255], [ 0, 183, 255, 255], [ 0, 187, 255, 255],
|
||||||
|
[ 0, 191, 255, 255], [ 0, 195, 255, 255], [ 0, 199, 255, 255], [ 0, 203, 255, 255],
|
||||||
|
[ 0, 207, 255, 255], [ 0, 211, 255, 255], [ 0, 215, 255, 255], [ 0, 219, 255, 255],
|
||||||
|
[ 0, 223, 255, 255], [ 0, 227, 255, 255], [ 0, 231, 255, 255], [ 0, 235, 255, 255],
|
||||||
|
[ 0, 239, 255, 255], [ 0, 243, 255, 255], [ 0, 247, 255, 255], [ 0, 251, 255, 255],
|
||||||
|
[ 0, 255, 255, 255], [ 4, 255, 251, 255], [ 8, 255, 247, 255], [ 12, 255, 243, 255],
|
||||||
|
[ 16, 255, 239, 255], [ 20, 255, 235, 255], [ 24, 255, 231, 255], [ 28, 255, 227, 255],
|
||||||
|
[ 32, 255, 223, 255], [ 36, 255, 219, 255], [ 40, 255, 215, 255], [ 44, 255, 211, 255],
|
||||||
|
[ 48, 255, 207, 255], [ 52, 255, 203, 255], [ 56, 255, 199, 255], [ 60, 255, 195, 255],
|
||||||
|
[ 64, 255, 191, 255], [ 68, 255, 187, 255], [ 72, 255, 183, 255], [ 76, 255, 179, 255],
|
||||||
|
[ 80, 255, 175, 255], [ 84, 255, 171, 255], [ 88, 255, 167, 255], [ 92, 255, 163, 255],
|
||||||
|
[ 96, 255, 159, 255], [100, 255, 155, 255], [104, 255, 151, 255], [108, 255, 147, 255],
|
||||||
|
[112, 255, 143, 255], [116, 255, 139, 255], [120, 255, 135, 255], [124, 255, 131, 255],
|
||||||
|
[128, 255, 128, 255], [131, 255, 124, 255], [135, 255, 120, 255], [139, 255, 116, 255],
|
||||||
|
[143, 255, 112, 255], [147, 255, 108, 255], [151, 255, 104, 255], [155, 255, 100, 255],
|
||||||
|
[159, 255, 96, 255], [163, 255, 92, 255], [167, 255, 88, 255], [171, 255, 84, 255],
|
||||||
|
[175, 255, 80, 255], [179, 255, 76, 255], [183, 255, 72, 255], [187, 255, 68, 255],
|
||||||
|
[191, 255, 64, 255], [195, 255, 60, 255], [199, 255, 56, 255], [203, 255, 52, 255],
|
||||||
|
[207, 255, 48, 255], [211, 255, 44, 255], [215, 255, 40, 255], [219, 255, 36, 255],
|
||||||
|
[223, 255, 32, 255], [227, 255, 28, 255], [231, 255, 24, 255], [235, 255, 20, 255],
|
||||||
|
[239, 255, 16, 255], [243, 255, 12, 255], [247, 255, 8, 255], [251, 255, 4, 255],
|
||||||
|
[255, 255, 0, 255], [255, 251, 0, 255], [255, 247, 0, 255], [255, 243, 0, 255],
|
||||||
|
[255, 239, 0, 255], [255, 235, 0, 255], [255, 231, 0, 255], [255, 227, 0, 255],
|
||||||
|
[255, 223, 0, 255], [255, 219, 0, 255], [255, 215, 0, 255], [255, 211, 0, 255],
|
||||||
|
[255, 207, 0, 255], [255, 203, 0, 255], [255, 199, 0, 255], [255, 195, 0, 255],
|
||||||
|
[255, 191, 0, 255], [255, 187, 0, 255], [255, 183, 0, 255], [255, 179, 0, 255],
|
||||||
|
[255, 175, 0, 255], [255, 171, 0, 255], [255, 167, 0, 255], [255, 163, 0, 255],
|
||||||
|
[255, 159, 0, 255], [255, 155, 0, 255], [255, 151, 0, 255], [255, 147, 0, 255],
|
||||||
|
[255, 143, 0, 255], [255, 139, 0, 255], [255, 135, 0, 255], [255, 131, 0, 255],
|
||||||
|
[255, 128, 0, 255], [255, 124, 0, 255], [255, 120, 0, 255], [255, 116, 0, 255],
|
||||||
|
[255, 112, 0, 255], [255, 108, 0, 255], [255, 104, 0, 255], [255, 100, 0, 255],
|
||||||
|
[255, 96, 0, 255], [255, 92, 0, 255], [255, 88, 0, 255], [255, 84, 0, 255],
|
||||||
|
[255, 80, 0, 255], [255, 76, 0, 255], [255, 72, 0, 255], [255, 68, 0, 255],
|
||||||
|
[255, 64, 0, 255], [255, 60, 0, 255], [255, 56, 0, 255], [255, 52, 0, 255],
|
||||||
|
[255, 48, 0, 255], [255, 44, 0, 255], [255, 40, 0, 255], [255, 36, 0, 255],
|
||||||
|
[255, 32, 0, 255], [255, 28, 0, 255], [255, 24, 0, 255], [255, 20, 0, 255],
|
||||||
|
[255, 16, 0, 255], [255, 12, 0, 255], [255, 8, 0, 255], [255, 4, 0, 255],
|
||||||
|
[255, 0, 0, 255], [251, 0, 0, 255], [247, 0, 0, 255], [243, 0, 0, 255],
|
||||||
|
[239, 0, 0, 255], [235, 0, 0, 255], [231, 0, 0, 255], [227, 0, 0, 255],
|
||||||
|
[223, 0, 0, 255], [219, 0, 0, 255], [215, 0, 0, 255], [211, 0, 0, 255],
|
||||||
|
[207, 0, 0, 255], [203, 0, 0, 255], [199, 0, 0, 255], [195, 0, 0, 255],
|
||||||
|
[191, 0, 0, 255], [187, 0, 0, 255], [183, 0, 0, 255], [179, 0, 0, 255],
|
||||||
|
[175, 0, 0, 255], [171, 0, 0, 255], [167, 0, 0, 255], [163, 0, 0, 255],
|
||||||
|
[159, 0, 0, 255], [155, 0, 0, 255], [151, 0, 0, 255], [147, 0, 0, 255],
|
||||||
|
[143, 0, 0, 255], [139, 0, 0, 255], [135, 0, 0, 255], [131, 0, 0, 255],
|
||||||
|
[ 0, 0, 0, 0]];
|
||||||
|
|
||||||
|
const incrLine = ()=>
|
||||||
|
{
|
||||||
|
if ((vert && !rhc) || (!vert && rhc))
|
||||||
|
{
|
||||||
|
nextLine++;
|
||||||
|
if (nextLine >= lines)
|
||||||
|
{
|
||||||
|
nextLine = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextLine--;
|
||||||
|
if (nextLine < 0)
|
||||||
|
{
|
||||||
|
nextLine = lines-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWaterfall = ()=> // update dynamic waterfalls at a fixed rate
|
||||||
|
{
|
||||||
|
let sgDiff;
|
||||||
|
|
||||||
|
// grab latest line of data, write it to off screen buffer, inc 'nextLine'
|
||||||
|
this.newLine();
|
||||||
|
// loop to write data data at the desired rate, data is being updated asynchronously
|
||||||
|
// ref for accurate timeout: http://www.sitepoint.com/creating-accurate-timers-in-javascript
|
||||||
|
sgTime += interval;
|
||||||
|
sgDiff = (Date.now() - sgStartTime) - sgTime;
|
||||||
|
if (running)
|
||||||
|
{
|
||||||
|
timerID = setTimeout(updateWaterfall, interval - sgDiff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProperty = (propertyName, value)=>
|
||||||
|
{
|
||||||
|
if ((typeof propertyName !== "string")||(value === undefined)) // null is OK, forces default
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (propertyName.toLowerCase())
|
||||||
|
{
|
||||||
|
case "linerate":
|
||||||
|
this.setLineRate(value); // setLine does checks for number etc
|
||||||
|
break;
|
||||||
|
case "startbin":
|
||||||
|
if (!isNaN(value) && value > 0)
|
||||||
|
{
|
||||||
|
startOfs = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "onscreenparentid":
|
||||||
|
if (typeof value === "string" && document.getElementById(value))
|
||||||
|
{
|
||||||
|
demoCvsId = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "colormap":
|
||||||
|
if (Array.isArray(value) && Array.isArray(value[0]) && value[0].length == 4)
|
||||||
|
{
|
||||||
|
colMap = value; // value must be an array of 4 element arrays to get here
|
||||||
|
if (colMap.length<256) // fill out the remaining colors with last color
|
||||||
|
{
|
||||||
|
for (let i=colMap.length; i<256; i++)
|
||||||
|
{
|
||||||
|
colMap[i] = colMap[colMap.length-1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verticalNewLine = ()=>
|
||||||
|
{
|
||||||
|
let tmpImgData, ipBuf8;
|
||||||
|
|
||||||
|
if (sgMode == "WF")
|
||||||
|
{
|
||||||
|
if (rhc)
|
||||||
|
{
|
||||||
|
// shift the current display down 1 line, oldest line drops off
|
||||||
|
tmpImgData = offScreenCtx.getImageData(0, 0, pxPerLine, lines-1);
|
||||||
|
offScreenCtx.putImageData(tmpImgData, 0, 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// shift the current display up 1 line, oldest line drops off
|
||||||
|
tmpImgData = offScreenCtx.getImageData(0, 1, pxPerLine, lines-1);
|
||||||
|
offScreenCtx.putImageData(tmpImgData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipBuf8 = Uint8ClampedArray.from(ipObj.buffer);
|
||||||
|
for (let sigVal, rgba, opIdx = 0, ipIdx = startOfs; ipIdx < pxPerLine+startOfs; opIdx += 4, ipIdx++)
|
||||||
|
{
|
||||||
|
sigVal = ipBuf8[ipIdx] || 0; // if input line too short add zeros
|
||||||
|
rgba = colMap[sigVal]; // array of rgba values
|
||||||
|
// byte reverse so number aa bb gg rr
|
||||||
|
lineBuf8[opIdx] = rgba[0]; // red
|
||||||
|
lineBuf8[opIdx+1] = rgba[1]; // green
|
||||||
|
lineBuf8[opIdx+2] = rgba[2]; // blue
|
||||||
|
lineBuf8[opIdx+3] = rgba[3]; // alpha
|
||||||
|
}
|
||||||
|
offScreenCtx.putImageData(lineImgData, 0, nextLine);
|
||||||
|
if (sgMode === "RS")
|
||||||
|
{
|
||||||
|
incrLine();
|
||||||
|
// if not static draw a white line in front of the current line to indicate new data point
|
||||||
|
if (lineRate)
|
||||||
|
{
|
||||||
|
offScreenCtx.putImageData(blankImgData, 0, nextLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const horizontalNewLine = ()=>
|
||||||
|
{
|
||||||
|
let tmpImgData, ipBuf8;
|
||||||
|
|
||||||
|
if (sgMode == "WF")
|
||||||
|
{
|
||||||
|
if (rhc)
|
||||||
|
{
|
||||||
|
// shift the current display right 1 line, oldest line drops off
|
||||||
|
tmpImgData = offScreenCtx.getImageData(0, 0, lines-1, pxPerLine);
|
||||||
|
offScreenCtx.putImageData(tmpImgData, 1, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// shift the current display left 1 line, oldest line drops off
|
||||||
|
tmpImgData = offScreenCtx.getImageData(1, 0, lines-1, pxPerLine);
|
||||||
|
offScreenCtx.putImageData(tmpImgData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// refresh the page image (it was just shifted)
|
||||||
|
const pageImgData = offScreenCtx.getImageData(0, 0, lines, pxPerLine);
|
||||||
|
if (ipObj.buffer.constructor !== Uint8Array)
|
||||||
|
{
|
||||||
|
ipBuf8 = Uint8ClampedArray.from(ipObj.buffer); // clamp input values to 0..255 range
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ipBuf8 = ipObj.buffer; // conversion already done
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let sigVal, rgba, opIdx, ipIdx=0; ipIdx < pxPerLine; ipIdx++)
|
||||||
|
{
|
||||||
|
sigVal = ipBuf8[ipIdx+startOfs] || 0; // if input line too short add zeros
|
||||||
|
rgba = colMap[sigVal]; // array of rgba values
|
||||||
|
opIdx = 4*((pxPerLine-ipIdx-1)*lines+nextLine);
|
||||||
|
// byte reverse so number aa bb gg rr
|
||||||
|
pageImgData.data[opIdx] = rgba[0]; // red
|
||||||
|
pageImgData.data[opIdx+1] = rgba[1]; // green
|
||||||
|
pageImgData.data[opIdx+2] = rgba[2]; // blue
|
||||||
|
pageImgData.data[opIdx+3] = rgba[3]; // alpha
|
||||||
|
}
|
||||||
|
if (sgMode === "RS")
|
||||||
|
{
|
||||||
|
incrLine();
|
||||||
|
// if not draw a white line in front of the current line to indicate new data point
|
||||||
|
if (lineRate)
|
||||||
|
{
|
||||||
|
for (let j=0; j < pxPerLine; j++)
|
||||||
|
{
|
||||||
|
let opIdx;
|
||||||
|
if (rhc)
|
||||||
|
{
|
||||||
|
opIdx = 4*(j*lines+nextLine);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
opIdx = 4*((pxPerLine-j-1)*lines+nextLine);
|
||||||
|
}
|
||||||
|
// byte reverse so number aa bb gg rr
|
||||||
|
pageImgData.data[opIdx] = 255; // red
|
||||||
|
pageImgData.data[opIdx+1] = 255; // green
|
||||||
|
pageImgData.data[opIdx+2] = 255; // blue
|
||||||
|
pageImgData.data[opIdx+3] = 255; // alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offScreenCtx.putImageData(pageImgData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOffScreenCanvas = ()=>
|
||||||
|
{
|
||||||
|
const cvs = document.createElement("canvas");
|
||||||
|
if (vert)
|
||||||
|
{
|
||||||
|
cvs.setAttribute('width', pxPerLine); // reset canvas pixels width
|
||||||
|
cvs.setAttribute('height', lines); // don't use style for this
|
||||||
|
clearImgData = new ImageData(clearBuf8, pxPerLine, lines);
|
||||||
|
}
|
||||||
|
else // data written in columns
|
||||||
|
{
|
||||||
|
cvs.setAttribute('width', lines); // reset canvas pixels width
|
||||||
|
cvs.setAttribute('height', pxPerLine); // don't use style for this
|
||||||
|
clearImgData = new ImageData(clearBuf8, lines, pxPerLine);
|
||||||
|
}
|
||||||
|
offScreenCtx = cvs.getContext("2d");
|
||||||
|
|
||||||
|
return cvs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== now make the exposed properties and methods ===============
|
||||||
|
|
||||||
|
this.newLine = (vert)? verticalNewLine: horizontalNewLine; // function pointers
|
||||||
|
|
||||||
|
this.offScreenCvs = createOffScreenCanvas();
|
||||||
|
|
||||||
|
this.setLineRate = function sgSetLineRate(newRate)
|
||||||
|
{
|
||||||
|
if (isNaN(newRate) || newRate > 50 || newRate < 0)
|
||||||
|
{
|
||||||
|
console.error("invalid line rate [0 <= lineRate < 50 lines/sec]");
|
||||||
|
// don't change the lineRate;
|
||||||
|
}
|
||||||
|
else if (newRate === 0) // static (one pass) raster
|
||||||
|
{
|
||||||
|
lineRate = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lineRate = newRate;
|
||||||
|
interval = 1000/lineRate; // msec
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clear = function()
|
||||||
|
{
|
||||||
|
offScreenCtx.putImageData(clearImgData, 0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.start = function()
|
||||||
|
{
|
||||||
|
sgStartTime = Date.now();
|
||||||
|
sgTime = 0;
|
||||||
|
running = true;
|
||||||
|
updateWaterfall(); // start the update loop
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stop = function()
|
||||||
|
{
|
||||||
|
running = false;
|
||||||
|
if (timerID)
|
||||||
|
{
|
||||||
|
clearTimeout(timerID);
|
||||||
|
}
|
||||||
|
// reset where the next line is to be written
|
||||||
|
if (sgMode === "RS")
|
||||||
|
{
|
||||||
|
if (vert)
|
||||||
|
{
|
||||||
|
nextLine = (rhc)? lines-1 : 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextLine = (rhc)? 0 : lines-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // WF
|
||||||
|
{
|
||||||
|
nextLine = (rhc)? 0 : lines-1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//===== set all the options ================
|
||||||
|
for (let prop in opt)
|
||||||
|
{
|
||||||
|
// check that this is opt's own property, not inherited from prototype
|
||||||
|
if (opt.hasOwnProperty(prop))
|
||||||
|
{
|
||||||
|
setProperty(prop, opt[prop]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a white line, it will show the input line for RS displays
|
||||||
|
blankBuf8.fill(255);
|
||||||
|
// make a full canvas of the color map 0 values
|
||||||
|
for (let i=0; i<pxPerLine*lines*4; i+=4)
|
||||||
|
{
|
||||||
|
// byte reverse so number aa bb gg rr
|
||||||
|
clearBuf8[i] = colMap[0][0]; // red
|
||||||
|
clearBuf8[i+1] = colMap[0][1]; // green
|
||||||
|
clearBuf8[i+2] = colMap[0][2]; // blue
|
||||||
|
clearBuf8[i+3] = colMap[0][3]; // alpha
|
||||||
|
}
|
||||||
|
// for diagnostics only
|
||||||
|
if (typeof(demoCvsId) == "string")
|
||||||
|
{
|
||||||
|
document.getElementById(demoCvsId).appendChild(this.offScreenCvs);
|
||||||
|
}
|
||||||
|
// initialize the direction and first line position
|
||||||
|
this.stop();
|
||||||
|
|
||||||
|
// everything is set
|
||||||
|
// if dynamic, wait for the start or newLine methods to be called
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Waterfall = class extends Spectrogram
|
||||||
|
{
|
||||||
|
constructor(ipObj, w, h, dir, options) // ipObj = {buffer: [..]}
|
||||||
|
{
|
||||||
|
var direction = (typeof(dir) === "string")? dir.toLowerCase() : "down";
|
||||||
|
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case "up":
|
||||||
|
super(ipObj, w, h, "WF", false, true, options);
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
default:
|
||||||
|
super(ipObj, w, h, "WF", true, true, options);
|
||||||
|
break;
|
||||||
|
case "left":
|
||||||
|
super(ipObj, w, h, "WF", false, false, options);
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
super(ipObj, w, h, "WF", true, false, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rasterscan = class extends Spectrogram
|
||||||
|
{
|
||||||
|
constructor(ipObj, w, h, dir, options) // ipObj = {buffer: [..]}
|
||||||
|
{
|
||||||
|
const direction = (typeof(dir) === "string")? dir.toLowerCase() : "down";
|
||||||
|
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case "up":
|
||||||
|
super(ipObj, w, h, "RS", true, true, options);
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
default:
|
||||||
|
super(ipObj, w, h, "RS", false, true, options);
|
||||||
|
break;
|
||||||
|
case "left":
|
||||||
|
super(ipObj, w, h, "RS", false, false, options);
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
super(ipObj, w, h, "RS", true, false, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RasterImage = class
|
||||||
|
{
|
||||||
|
constructor(dataBuf, cols, rows, options={}) // dataBuf = Array[rows][cols]
|
||||||
|
{
|
||||||
|
const ipObj = {buffer:null};
|
||||||
|
const dirs = ["up", "down", "left"];
|
||||||
|
let direction = "down";
|
||||||
|
let dirLC;
|
||||||
|
|
||||||
|
if (options.hasOwnProperty("dir") && typeof(options.dir)==="string")
|
||||||
|
{
|
||||||
|
dirLC = options.dir.toLowerCase();
|
||||||
|
}
|
||||||
|
else if (options.hasOwnProperty("direction") && typeof(options.direction)==="string")
|
||||||
|
{
|
||||||
|
dirLC = options.direction.toLowerCase();
|
||||||
|
}
|
||||||
|
if (dirLC && dirs.includes(dirLC))
|
||||||
|
direction = dirLC;
|
||||||
|
|
||||||
|
// dataBuf values are each an index (0..255) into a colorMap
|
||||||
|
// Each of 256 colorMap entries holds the 4 values RGBA each (0..255) of a color
|
||||||
|
|
||||||
|
// force static image
|
||||||
|
options.lineRate = 0;
|
||||||
|
const raster = new Rasterscan(ipObj, cols, rows, direction, options);
|
||||||
|
|
||||||
|
// now build a raster display line by line
|
||||||
|
for (let r=0; r<rows; r++)
|
||||||
|
{
|
||||||
|
ipObj.buffer = dataBuf[r];
|
||||||
|
raster.newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return raster.offScreenCvs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}())
|
@ -1,10 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
// deps
|
// deps
|
||||||
import BufferQueueNode from "$lib/queue";
|
import saveAs from "file-saver";
|
||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import io from "socket.io-client";
|
import io from "socket.io-client";
|
||||||
|
import ADIF from "tcadif/lib/ADIF";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
import BufferQueueNode from "$lib/queue";
|
||||||
import { OpusDecoder } from "opus-decoder";
|
import { OpusDecoder } from "opus-decoder";
|
||||||
|
import { Waterfall } from "$lib/spectrogram";
|
||||||
|
|
||||||
function clamp(val, min, max) {
|
function clamp(val, min, max) {
|
||||||
return Math.min(Math.max(val, min), max);
|
return Math.min(Math.max(val, min), max);
|
||||||
@ -14,6 +17,12 @@
|
|||||||
return arr.reduce((prev, cur) => prev + cur, 0) / arr.length;
|
return arr.reduce((prev, cur) => prev + cur, 0) / arr.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modes = {
|
||||||
|
voice: "Voice",
|
||||||
|
ft8: "FT8",
|
||||||
|
psk31: "PSK31",
|
||||||
|
};
|
||||||
|
|
||||||
// state vars
|
// state vars
|
||||||
let status = $state("disconnected");
|
let status = $state("disconnected");
|
||||||
let remoteState = $state();
|
let remoteState = $state();
|
||||||
@ -22,8 +31,13 @@
|
|||||||
let dbm = $state(0);
|
let dbm = $state(0);
|
||||||
let dbmList = $state([0]);
|
let dbmList = $state([0]);
|
||||||
let pwr = $state(0);
|
let pwr = $state(0);
|
||||||
let swr = $state(0);
|
let swrList = $state([0]);
|
||||||
|
let swr = $derived(avg(swrList));
|
||||||
let context = $state();
|
let context = $state();
|
||||||
|
let micGain = $state(1);
|
||||||
|
let outputGain = $state(1);
|
||||||
|
let micLevel = $state(0);
|
||||||
|
let clipping = $state(false);
|
||||||
let recorder;
|
let recorder;
|
||||||
let sunits = $derived.by(() => {
|
let sunits = $derived.by(() => {
|
||||||
let average = avg(dbmList);
|
let average = avg(dbmList);
|
||||||
@ -34,6 +48,33 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let socket;
|
let socket;
|
||||||
|
let canvas;
|
||||||
|
let bands = $state([]);
|
||||||
|
let currentBand = $derived.by(() => {
|
||||||
|
try {
|
||||||
|
let bandKeys = Object.keys(bands);
|
||||||
|
let bandValues = Object.values(bands);
|
||||||
|
return bandKeys[
|
||||||
|
bandValues.indexOf(
|
||||||
|
bandValues.find(
|
||||||
|
(band) =>
|
||||||
|
band.edges[0] <= remoteState.frequency &&
|
||||||
|
band.edges[1] > remoteState.frequency
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let clubName = $state("");
|
||||||
|
let clubEmail = $state("");
|
||||||
|
let logbook = $state([]);
|
||||||
|
let outputGainNode;
|
||||||
|
let logCallsign = $state("");
|
||||||
|
let logTime = $state("");
|
||||||
|
let logRstTx = $state("");
|
||||||
|
let logRstRx = $state("");
|
||||||
|
|
||||||
async function enableMic() {
|
async function enableMic() {
|
||||||
recorder = new Recorder({
|
recorder = new Recorder({
|
||||||
@ -49,9 +90,24 @@
|
|||||||
context = new AudioContext({ sampleRate: 48000 });
|
context = new AudioContext({ sampleRate: 48000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when output gain is changed
|
||||||
|
$effect(() => {
|
||||||
|
let gainNode = untrack(() => outputGainNode);
|
||||||
|
let gain = outputGain;
|
||||||
|
if (gainNode) gainNode.gain.value = gain;
|
||||||
|
});
|
||||||
|
|
||||||
|
// when mic gain is changed
|
||||||
|
$effect(() => {
|
||||||
|
let leRecorder = untrack(() => recorder);
|
||||||
|
let gain = micGain;
|
||||||
|
if (leRecorder) leRecorder.setRecordingGain(gain);
|
||||||
|
});
|
||||||
|
|
||||||
// when the file is uploaded we do the thing
|
// when the file is uploaded we do the thing
|
||||||
$effect(async () => {
|
$effect(async () => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
|
console.log("connecting :3");
|
||||||
status = "connecting";
|
status = "connecting";
|
||||||
let rawKey = await files[0].text();
|
let rawKey = await files[0].text();
|
||||||
try {
|
try {
|
||||||
@ -65,11 +121,20 @@
|
|||||||
let queueNode = new BufferQueueNode({
|
let queueNode = new BufferQueueNode({
|
||||||
audioContext: context,
|
audioContext: context,
|
||||||
});
|
});
|
||||||
queueNode.connect(context.destination);
|
outputGainNode = new GainNode(context);
|
||||||
|
queueNode.connect(outputGainNode);
|
||||||
|
outputGainNode.connect(context.destination);
|
||||||
|
// temporary workaround to keep latency from getting too bad, need more empirical testing though
|
||||||
|
setInterval(() => {
|
||||||
|
queueNode.disconnect(outputGainNode);
|
||||||
|
queueNode = new BufferQueueNode({
|
||||||
|
audioContext: context,
|
||||||
|
});
|
||||||
|
queueNode.connect(outputGainNode);
|
||||||
|
}, 120 * 1000);
|
||||||
socket = io(json.url);
|
socket = io(json.url);
|
||||||
window.socket = socket;
|
window.socket = socket;
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
status = "connected";
|
|
||||||
socket.emit("auth", rawKey);
|
socket.emit("auth", rawKey);
|
||||||
});
|
});
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
@ -79,7 +144,7 @@
|
|||||||
remoteState = state;
|
remoteState = state;
|
||||||
});
|
});
|
||||||
socket.on("dbm", (d) => {
|
socket.on("dbm", (d) => {
|
||||||
dbm = Math.min(d, -33);
|
dbm = clamp(d, -121, -33);
|
||||||
dbmList.push(dbm);
|
dbmList.push(dbm);
|
||||||
if (dbmList.length > 25) dbmList.shift();
|
if (dbmList.length > 25) dbmList.shift();
|
||||||
});
|
});
|
||||||
@ -87,7 +152,8 @@
|
|||||||
pwr = p;
|
pwr = p;
|
||||||
});
|
});
|
||||||
socket.on("swr", (s) => {
|
socket.on("swr", (s) => {
|
||||||
swr = s;
|
swrList.push(swr);
|
||||||
|
if (swrList.length > 10) swrList.shift();
|
||||||
});
|
});
|
||||||
socket.on("audio", (chunk) => {
|
socket.on("audio", (chunk) => {
|
||||||
queueNode._write(
|
queueNode._write(
|
||||||
@ -97,6 +163,37 @@
|
|||||||
() => {}
|
() => {}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
socket.on("login", (info) => {
|
||||||
|
bands = info.bands;
|
||||||
|
clubName = info.clubName;
|
||||||
|
clubEmail = info.clubEmail;
|
||||||
|
socket.emit("getLogbook");
|
||||||
|
status = "connected";
|
||||||
|
});
|
||||||
|
socket.on("logbook", (logs) => {
|
||||||
|
logbook = logs;
|
||||||
|
});
|
||||||
|
socket.on("error", (err) => errors.unshift(err));
|
||||||
|
// i'm actually not sure we need to do this but meh
|
||||||
|
let untrackedRecorder = untrack(() => recorder);
|
||||||
|
let scriptNode =
|
||||||
|
untrackedRecorder.audioContext.createScriptProcessor(
|
||||||
|
4096,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
scriptNode.addEventListener("audioprocess", (e) => {
|
||||||
|
let data = e.inputBuffer.getChannelData(0);
|
||||||
|
clipping = false;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
if (Math.abs(data[i]) >= 1) {
|
||||||
|
clipping = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
micLevel = Math.min(avg(data.map((x) => Math.abs(x))), 1);
|
||||||
|
});
|
||||||
|
untrackedRecorder.recordingGainNode.connect(scriptNode);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
errors.unshift("Invalid key!");
|
errors.unshift("Invalid key!");
|
||||||
@ -105,6 +202,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// submit entry function
|
||||||
|
function submitEntry() {
|
||||||
|
if (
|
||||||
|
logCallsign.length === 0 ||
|
||||||
|
logRstRx.length === 0 ||
|
||||||
|
logRstTx.length === 0 ||
|
||||||
|
logTime.length === 0 ||
|
||||||
|
!/^[0-9]{1,2}:[0-9]{2}$/.test(logTime)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
let date = new Date();
|
||||||
|
socket.emit("newEntry", {
|
||||||
|
TIME_ON: logTime.replace(":", "").padStart(4, "0").padEnd(6, "0"),
|
||||||
|
CALL: logCallsign.toUpperCase(),
|
||||||
|
MODE: "SSB",
|
||||||
|
QSO_DATE: `${date.getUTCFullYear()}${date.getUTCMonth().toString().padStart(2, "0")}${date.getUTCDate().toString().padStart(2, "0")}`,
|
||||||
|
RST_RCVD: logRstRx,
|
||||||
|
RST_SENT: logRstTx,
|
||||||
|
FREQ: (remoteState.frequency / 100000).toString(),
|
||||||
|
});
|
||||||
|
logTime = "";
|
||||||
|
logCallsign = "";
|
||||||
|
logRstRx = "";
|
||||||
|
logRstTx = "";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if status === "disconnected"}
|
{#if status === "disconnected"}
|
||||||
@ -113,7 +236,7 @@
|
|||||||
>
|
>
|
||||||
<h1 class="text-3xl font-semibold">freeremote</h1>
|
<h1 class="text-3xl font-semibold">freeremote</h1>
|
||||||
{#if context}
|
{#if context}
|
||||||
<p>Please upload your key below to access your remote station.</p>
|
<p class="text-center">Please upload your key below to access your remote station.</p>
|
||||||
<input
|
<input
|
||||||
accept="text/plain"
|
accept="text/plain"
|
||||||
bind:files
|
bind:files
|
||||||
@ -134,9 +257,419 @@
|
|||||||
<span class="loading loading-spinner loading-xl"></span>
|
<span class="loading loading-spinner loading-xl"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p>{remoteState?.frequency}</p>
|
<div class="w-screen min-h-screen flex flex-col">
|
||||||
<p>{dbm}</p>
|
<div
|
||||||
<p>{sunits}</p>
|
class="w-screen bg-base-200 shadow-sm flex items-center px-2 py-2 gap-2"
|
||||||
|
>
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-soft">
|
||||||
|
{currentBand}
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu bg-base-200 rounded-box z-1 w-52 p-2 shadow-sm"
|
||||||
|
>
|
||||||
|
{#each Object.keys(bands) as band}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onclick={() => {
|
||||||
|
if (remoteState.mode === "voice") {
|
||||||
|
socket.emit(
|
||||||
|
"frequency",
|
||||||
|
bands[band].voice[0]
|
||||||
|
);
|
||||||
|
} else if (remoteState.mode === "psk31") {
|
||||||
|
socket.emit(
|
||||||
|
"frequency",
|
||||||
|
bands[band].psk31
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
socket.emit(
|
||||||
|
"frequency",
|
||||||
|
bands[band].ft8
|
||||||
|
);
|
||||||
|
}
|
||||||
|
document.activeElement.blur();
|
||||||
|
}}>{band}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-soft">
|
||||||
|
{modes[remoteState?.mode]}
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu bg-base-200 rounded-box z-1 w-52 p-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<li><a>Voice</a></li>
|
||||||
|
<li><a>FT8</a></li>
|
||||||
|
<li><a>PSK31</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-soft btn-success" onclick={() => socket.emit("tune")}>Tune</button>
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<p>
|
||||||
|
<a class="link" href="mailto:{clubEmail}">{clubName}</a> - {remoteState
|
||||||
|
?.currentUser.callsign}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if remoteState?.mode === "voice"}
|
||||||
|
<div class="flex flex-col flex-grow items-center p-4">
|
||||||
|
<div class="flex lg:flex-row md:flex-row flex-col w-full flex-grow gap-4">
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4">
|
||||||
|
<div
|
||||||
|
class="card card-border bg-base-200 lg:w-[30vw] md:w-[50vw] w-[85vw]"
|
||||||
|
>
|
||||||
|
<div class="card-body font-mono">
|
||||||
|
<div class="lg:hidden md:hidden flex flex-row">
|
||||||
|
<button class="btn btn-soft" onclick={() => socket.emit("frequency", remoteState.frequency - 100)}>
|
||||||
|
-1 kHz
|
||||||
|
</button>
|
||||||
|
<div class="flex-grow">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-soft" onclick={() => socket.emit("frequency", remoteState.frequency + 100)}>
|
||||||
|
+1 kHz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-center"
|
||||||
|
>
|
||||||
|
{#each [7, 6, 5, 4, 3, 2, 1] as i}
|
||||||
|
<h1
|
||||||
|
class="text-6xl"
|
||||||
|
onwheel={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
socket.emit(
|
||||||
|
"frequency",
|
||||||
|
remoteState.frequency +
|
||||||
|
10 ** (i - 1)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
socket.emit(
|
||||||
|
"frequency",
|
||||||
|
remoteState.frequency -
|
||||||
|
10 ** (i - 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
let digit = Math.floor(
|
||||||
|
(remoteState?.frequency %
|
||||||
|
10 ** i) /
|
||||||
|
10 ** (i - 1)
|
||||||
|
);
|
||||||
|
if (isNaN(digit)) return 0;
|
||||||
|
else return digit;
|
||||||
|
})()}
|
||||||
|
</h1>
|
||||||
|
{#if i === 3}
|
||||||
|
<h1 class="text-6xl">.</h1>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rounded-box border border-base-content/5 bg-base-200 lg:w-[30vw] md:w-[50vw] w-[85vw]"
|
||||||
|
>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<progress
|
||||||
|
class="progress progress-primary"
|
||||||
|
value={dbm + 121}
|
||||||
|
max="88"
|
||||||
|
></progress>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
class="tooltip"
|
||||||
|
data-tip={`${dbm} dBm`}
|
||||||
|
>
|
||||||
|
<p>{sunits}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<progress
|
||||||
|
class="progress progress-warning"
|
||||||
|
value={pwr}
|
||||||
|
max={remoteState?.maxpwr}
|
||||||
|
></progress>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
class="tooltip"
|
||||||
|
data-tip="May not work on some transceivers."
|
||||||
|
>
|
||||||
|
<p>{pwr}W</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<progress
|
||||||
|
class="progress progress-error"
|
||||||
|
value={swr}
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
class="tooltip"
|
||||||
|
data-tip="SWR is approximate. Accuracy not guaranteed."
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{(() => {
|
||||||
|
if (swr > 50)
|
||||||
|
return "∞";
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
1 +
|
||||||
|
Math.floor(
|
||||||
|
(swr / 11) *
|
||||||
|
0.5 *
|
||||||
|
10
|
||||||
|
) /
|
||||||
|
10
|
||||||
|
);
|
||||||
|
})()}:1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<progress
|
||||||
|
class="progress {clipping
|
||||||
|
? 'progress-error'
|
||||||
|
: 'progress-success'}"
|
||||||
|
value={micLevel}
|
||||||
|
max="1"
|
||||||
|
></progress>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
class="tooltip"
|
||||||
|
data-tip="Set the mic gain so that this meter is as high as possible when you speak. If the meter is red while you speak, your voice may be clipping."
|
||||||
|
>
|
||||||
|
<p>Mic</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="card card-border bg-base-200 lg:w-[30vw] md:w-[50vw] w-[85vw]"
|
||||||
|
>
|
||||||
|
<div class="card-body w-full">
|
||||||
|
<p class="text-center">Output gain</p>
|
||||||
|
<div class="w-full">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="3"
|
||||||
|
bind:value={outputGain}
|
||||||
|
class="w-full range"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex justify-between px-2.5 mt-2 text-xs"
|
||||||
|
>
|
||||||
|
<span>|</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>|</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex justify-between px-2.5 mt-2 text-xs"
|
||||||
|
>
|
||||||
|
<span>Muted</span>
|
||||||
|
<span>100%</span>
|
||||||
|
<span>200%</span>
|
||||||
|
<span>300%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center">Mic gain</p>
|
||||||
|
<div class="w-full">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="3"
|
||||||
|
bind:value={micGain}
|
||||||
|
class="w-full range"
|
||||||
|
step="0.1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex justify-between px-2.5 mt-2 text-xs"
|
||||||
|
>
|
||||||
|
<span>|</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span>|</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex justify-between px-2.5 mt-2 text-xs"
|
||||||
|
>
|
||||||
|
<span>Muted</span>
|
||||||
|
<span>100%</span>
|
||||||
|
<span>200%</span>
|
||||||
|
<span>300%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-grow gap-4">
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<h1 class="text-3xl font-semibold">Logbook</h1>
|
||||||
|
<button
|
||||||
|
class="btn btn-square btn-soft material-symbols-outlined"
|
||||||
|
onclick={() => {
|
||||||
|
let blob = new Blob(
|
||||||
|
[
|
||||||
|
new ADIF({
|
||||||
|
qsos: logbook,
|
||||||
|
}).stringify({
|
||||||
|
fieldDelim: "\n",
|
||||||
|
recordDelim: "\n",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
{ type: "text/plain;charset=utf-8;" }
|
||||||
|
);
|
||||||
|
saveAs(
|
||||||
|
blob,
|
||||||
|
`${remoteState.currentUser.callsign}.adi`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex lg:flex-row flex-col gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Callsign"
|
||||||
|
class="input"
|
||||||
|
bind:value={logCallsign}
|
||||||
|
/>
|
||||||
|
<div class="join">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Time (UTC)"
|
||||||
|
class="input join-item"
|
||||||
|
bind:value={logTime}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-soft join-item"
|
||||||
|
onclick={() => {
|
||||||
|
let date = new Date();
|
||||||
|
logTime = `${date.getUTCHours().toString().padStart(2, "0")}:${date.getUTCMinutes().toString().padStart(2, "0")}`;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="RST (sent)"
|
||||||
|
class="input"
|
||||||
|
bind:value={logRstTx}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="RST (rcvd)"
|
||||||
|
class="input"
|
||||||
|
bind:value={logRstRx}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={submitEntry}
|
||||||
|
>
|
||||||
|
Add entry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if logbook.length === 0}
|
||||||
|
<div
|
||||||
|
class="flex flex-col flex-grow w-full items-center justify-center rounded-box border border-base-content/5 bg-base-200 max-h-[60vh]"
|
||||||
|
>
|
||||||
|
<p>No entries found.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="overflow-y-scroll overflow-x-auto rounded-box border border-base-content/5 bg-base-200 w-full max-h-[60vh] flex-grow"
|
||||||
|
>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Callsign</th>
|
||||||
|
<th>Mode</th>
|
||||||
|
<th>Freq (MHz)</th>
|
||||||
|
<th>RST (sent)</th>
|
||||||
|
<th>RST (rcvd)</th>
|
||||||
|
<th>Date (UTC)</th>
|
||||||
|
<th>Time (UTC)</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each logbook as entry, i}
|
||||||
|
<tr>
|
||||||
|
<td>{entry.CALL}</td>
|
||||||
|
<td>{entry.MODE}</td>
|
||||||
|
<td>{entry.FREQ}</td>
|
||||||
|
<td>{entry.RST_SENT}</td>
|
||||||
|
<td>{entry.RST_RCVD}</td>
|
||||||
|
<td>{entry.QSO_DATE}</td>
|
||||||
|
<td>{entry.TIME_ON}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn-soft btn-square btn-error material-symbols-outlined"
|
||||||
|
onclick={() => {
|
||||||
|
logbook.splice(i, 1);
|
||||||
|
socket.emit("updateLogbook", logbook);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="toast toast-bottom toast-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-xl {remoteState?.transmitting
|
||||||
|
? 'btn-error'
|
||||||
|
: 'btn-primary'} btn-circle"
|
||||||
|
onclick={() => {
|
||||||
|
if (!remoteState.transmitting) socket.emit("ptt");
|
||||||
|
else socket.emit("unptt");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="material-symbols-outlined">mic</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!--<canvas bind:this={canvas} class="w-full h-[25vh]"></canvas>-->
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="toast toast-top toast-end">
|
<div class="toast toast-top toast-end">
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
var OggOpusEncoder, OpusEncoderLib;
|
var OggOpusEncoder, OpusEncoderLib;
|
||||||
if(typeof require === 'function'){
|
if(typeof require === 'function'){
|
||||||
OpusEncoderLib = require('./libopus-encoder.js');
|
OpusEncoderLib = require('./libopus-encoder.wasm.min.js');
|
||||||
OggOpusEncoder = require('./oggOpusEncoder.js').OggOpusEncoder;
|
OggOpusEncoder = require('./oggOpusEncoder.js').OggOpusEncoder;
|
||||||
} else if (typeof importScripts === "function") {
|
} else if (typeof importScripts === "function") {
|
||||||
importScripts('./libopus-encoder.js');
|
importScripts('./libopus-encoder.wasm.min.js');
|
||||||
importScripts('./oggOpusEncoder.js');
|
importScripts('./oggOpusEncoder.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
27
static/libopus-encoder.min.js
vendored
27
static/libopus-encoder.min.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -1,5 +1,12 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
import adapter from "@sveltejs/adapter-static";
|
||||||
|
|
||||||
const config = { kit: { adapter: adapter() } };
|
const config = {
|
||||||
|
kit: { adapter: adapter() },
|
||||||
|
compilerOptions: {
|
||||||
|
warningFilter: (warning) =>
|
||||||
|
!warning.filename?.includes("node_modules") &&
|
||||||
|
!warning.code.startsWith("a11y"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user