mirror of
https://github.com/parabirb/freeremote-client.git
synced 2025-08-01 13:22:37 -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": {
|
||||
"buffer": "^6.0.3",
|
||||
"extend": "^3.0.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"opus-decoder": "^0.7.7",
|
||||
"readable-stream": "^4.7.0",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tcadif": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
@ -1290,6 +1292,18 @@
|
||||
"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": {
|
||||
"version": "5.0.17",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.17.tgz",
|
||||
@ -1480,6 +1494,12 @@
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"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": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -1819,6 +1839,15 @@
|
||||
"@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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@ -2167,6 +2196,20 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
|
@ -22,8 +22,10 @@
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"extend": "^3.0.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"opus-decoder": "^0.7.7",
|
||||
"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 {
|
||||
--font-sans: "Inter", system-ui, "Roboto", sans-serif;
|
||||
--font-mono: "IBM Plex Mono", SFMono-Regular, monospace;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>remoteham</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<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>
|
||||
// deps
|
||||
import BufferQueueNode from "$lib/queue";
|
||||
import saveAs from "file-saver";
|
||||
import { untrack } from "svelte";
|
||||
import io from "socket.io-client";
|
||||
import ADIF from "tcadif/lib/ADIF";
|
||||
import { fade } from "svelte/transition";
|
||||
import BufferQueueNode from "$lib/queue";
|
||||
import { OpusDecoder } from "opus-decoder";
|
||||
import { Waterfall } from "$lib/spectrogram";
|
||||
|
||||
function clamp(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;
|
||||
}
|
||||
|
||||
const modes = {
|
||||
voice: "Voice",
|
||||
ft8: "FT8",
|
||||
psk31: "PSK31",
|
||||
};
|
||||
|
||||
// state vars
|
||||
let status = $state("disconnected");
|
||||
let remoteState = $state();
|
||||
@ -22,8 +31,13 @@
|
||||
let dbm = $state(0);
|
||||
let dbmList = $state([0]);
|
||||
let pwr = $state(0);
|
||||
let swr = $state(0);
|
||||
let swrList = $state([0]);
|
||||
let swr = $derived(avg(swrList));
|
||||
let context = $state();
|
||||
let micGain = $state(1);
|
||||
let outputGain = $state(1);
|
||||
let micLevel = $state(0);
|
||||
let clipping = $state(false);
|
||||
let recorder;
|
||||
let sunits = $derived.by(() => {
|
||||
let average = avg(dbmList);
|
||||
@ -34,6 +48,33 @@
|
||||
}
|
||||
});
|
||||
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() {
|
||||
recorder = new Recorder({
|
||||
@ -49,9 +90,24 @@
|
||||
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
|
||||
$effect(async () => {
|
||||
if (files && files.length > 0) {
|
||||
console.log("connecting :3");
|
||||
status = "connecting";
|
||||
let rawKey = await files[0].text();
|
||||
try {
|
||||
@ -65,11 +121,20 @@
|
||||
let queueNode = new BufferQueueNode({
|
||||
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);
|
||||
window.socket = socket;
|
||||
socket.on("connect", () => {
|
||||
status = "connected";
|
||||
socket.emit("auth", rawKey);
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
@ -79,7 +144,7 @@
|
||||
remoteState = state;
|
||||
});
|
||||
socket.on("dbm", (d) => {
|
||||
dbm = Math.min(d, -33);
|
||||
dbm = clamp(d, -121, -33);
|
||||
dbmList.push(dbm);
|
||||
if (dbmList.length > 25) dbmList.shift();
|
||||
});
|
||||
@ -87,7 +152,8 @@
|
||||
pwr = p;
|
||||
});
|
||||
socket.on("swr", (s) => {
|
||||
swr = s;
|
||||
swrList.push(swr);
|
||||
if (swrList.length > 10) swrList.shift();
|
||||
});
|
||||
socket.on("audio", (chunk) => {
|
||||
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) {
|
||||
console.log(e);
|
||||
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>
|
||||
|
||||
{#if status === "disconnected"}
|
||||
@ -113,7 +236,7 @@
|
||||
>
|
||||
<h1 class="text-3xl font-semibold">freeremote</h1>
|
||||
{#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
|
||||
accept="text/plain"
|
||||
bind:files
|
||||
@ -134,9 +257,419 @@
|
||||
<span class="loading loading-spinner loading-xl"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<p>{remoteState?.frequency}</p>
|
||||
<p>{dbm}</p>
|
||||
<p>{sunits}</p>
|
||||
<div class="w-screen min-h-screen flex flex-col">
|
||||
<div
|
||||
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}
|
||||
|
||||
<div class="toast toast-top toast-end">
|
||||
|
@ -3,10 +3,10 @@
|
||||
|
||||
var OggOpusEncoder, OpusEncoderLib;
|
||||
if(typeof require === 'function'){
|
||||
OpusEncoderLib = require('./libopus-encoder.js');
|
||||
OpusEncoderLib = require('./libopus-encoder.wasm.min.js');
|
||||
OggOpusEncoder = require('./oggOpusEncoder.js').OggOpusEncoder;
|
||||
} else if (typeof importScripts === "function") {
|
||||
importScripts('./libopus-encoder.js');
|
||||
importScripts('./libopus-encoder.wasm.min.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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user