mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
More work on external audio sources, started adding generic audio packet handling
This commit is contained in:
parent
9c2ce526d3
commit
5726d6dee2
@ -149,6 +149,12 @@ export class AudioManager {
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent("audioSinksUpdated"));
|
||||
}
|
||||
|
||||
removeSource(source) {
|
||||
source.disconnect();
|
||||
this.#sources = this.#sources.filter((v) => v != source);
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
}
|
||||
|
||||
getSources() {
|
||||
return this.#sources;
|
||||
|
||||
@ -23,7 +23,7 @@ var packetID = 0;
|
||||
export class AudioPacket {
|
||||
#packet: Uint8Array;
|
||||
|
||||
constructor(data, settings, guid) {
|
||||
constructor(data, settings, guid, lat?, lng?, alt?) {
|
||||
let header: number[] = [0, 0, 0, 0, 0, 0];
|
||||
|
||||
let encFrequency: number[] = [...doubleToByteArray(settings.frequency)];
|
||||
@ -48,6 +48,10 @@ export class AudioPacket {
|
||||
[...Buffer.from(guid, "utf-8")]
|
||||
);
|
||||
|
||||
if (lat !== undefined && lng !== undefined && alt !== undefined) {
|
||||
packet.concat([...doubleToByteArray(lat)], [...doubleToByteArray(lng)], [...doubleToByteArray(alt)]);
|
||||
}
|
||||
|
||||
let encPacketLen = getBytes(packet.length, 2);
|
||||
packet[0] = encPacketLen[0];
|
||||
packet[1] = encPacketLen[1];
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
import { getApp } from "../olympusapp";
|
||||
import { AudioSink } from "./audiosink";
|
||||
import { WebAudioPeakMeter } from "web-audio-peak-meter";
|
||||
|
||||
export abstract class AudioSource {
|
||||
#connectedTo: AudioSink[] = [];
|
||||
#name = "";
|
||||
#playing = false;
|
||||
#meter: WebAudioPeakMeter;
|
||||
#volume: number = 1.0;
|
||||
#gainNode: GainNode;
|
||||
|
||||
constructor() {
|
||||
this.#gainNode = getApp().getAudioManager().getAudioContext().createGain();
|
||||
this.#meter = new WebAudioPeakMeter(this.#gainNode, document.createElement('div'));
|
||||
}
|
||||
|
||||
connect(sink: AudioSink) {
|
||||
this.getNode().connect(sink.getNode());
|
||||
@ -18,7 +27,7 @@ export abstract class AudioSource {
|
||||
} else {
|
||||
this.getNode().disconnect();
|
||||
}
|
||||
|
||||
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
}
|
||||
|
||||
@ -34,6 +43,23 @@ export abstract class AudioSource {
|
||||
return this.#connectedTo;
|
||||
}
|
||||
|
||||
setVolume(volume) {
|
||||
this.#volume = volume;
|
||||
this.#gainNode.gain.exponentialRampToValueAtTime(volume, getApp().getAudioManager().getAudioContext().currentTime + 0.02);
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
}
|
||||
|
||||
getVolume() {
|
||||
return this.#volume;
|
||||
}
|
||||
|
||||
getMeter() {
|
||||
return this.#meter;
|
||||
}
|
||||
|
||||
getNode() {
|
||||
return this.#gainNode;
|
||||
}
|
||||
|
||||
abstract play(): void;
|
||||
abstract getNode(): AudioNode;
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ import { getApp } from "../olympusapp";
|
||||
import {WebAudioPeakMeter} from 'web-audio-peak-meter';
|
||||
|
||||
export class FileSource extends AudioSource {
|
||||
#gainNode: GainNode;
|
||||
#file: File | null = null;
|
||||
#source: AudioBufferSourceNode;
|
||||
#duration: number = 0;
|
||||
@ -14,8 +13,6 @@ export class FileSource extends AudioSource {
|
||||
#audioBuffer: AudioBuffer;
|
||||
#restartTimeout: any;
|
||||
#looping = false;
|
||||
#meter: WebAudioPeakMeter;
|
||||
#volume: number = 1.0;
|
||||
|
||||
constructor(file) {
|
||||
super();
|
||||
@ -23,11 +20,10 @@ export class FileSource extends AudioSource {
|
||||
|
||||
this.setName(this.#file?.name ?? "N/A");
|
||||
|
||||
this.#gainNode = getApp().getAudioManager().getAudioContext().createGain();
|
||||
|
||||
if (!this.#file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
var contents = e.target?.result;
|
||||
@ -44,18 +40,12 @@ export class FileSource extends AudioSource {
|
||||
reader.readAsArrayBuffer(this.#file);
|
||||
}
|
||||
|
||||
getNode() {
|
||||
return this.#gainNode;
|
||||
}
|
||||
|
||||
play() {
|
||||
this.#source = getApp().getAudioManager().getAudioContext().createBufferSource();
|
||||
this.#source.buffer = this.#audioBuffer;
|
||||
this.#source.connect(this.#gainNode);
|
||||
this.#source.connect(this.getNode());
|
||||
this.#source.loop = this.#looping;
|
||||
|
||||
this.#meter = new WebAudioPeakMeter(this.#gainNode, document.createElement('div'));
|
||||
|
||||
this.#source.start(0, this.#currentPosition);
|
||||
this.#playing = true;
|
||||
const now = Date.now() / 1000;
|
||||
@ -79,6 +69,7 @@ export class FileSource extends AudioSource {
|
||||
|
||||
stop() {
|
||||
this.#source.stop();
|
||||
this.#source.disconnect();
|
||||
this.#playing = false;
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
@ -88,10 +79,6 @@ export class FileSource extends AudioSource {
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
}
|
||||
|
||||
setGain(gain) {
|
||||
this.#gainNode.gain.setValueAtTime(gain, getApp().getAudioManager().getAudioContext().currentTime);
|
||||
}
|
||||
|
||||
getPlaying() {
|
||||
return this.#playing;
|
||||
}
|
||||
@ -123,17 +110,4 @@ export class FileSource extends AudioSource {
|
||||
getLooping() {
|
||||
return this.#looping;
|
||||
}
|
||||
|
||||
getMeter() {
|
||||
return this.#meter;
|
||||
}
|
||||
|
||||
setVolume(volume) {
|
||||
this.#volume = volume;
|
||||
this.#gainNode.gain.exponentialRampToValueAtTime(volume, getApp().getAudioManager().getAudioContext().currentTime + 0.02);
|
||||
}
|
||||
|
||||
getVolume() {
|
||||
return this.#volume;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { getApp } from "../olympusapp";
|
||||
import { AudioSource } from "./audiosource";
|
||||
|
||||
export class MicrophoneSource extends AudioSource {
|
||||
#node: AudioNode;
|
||||
#node: MediaStreamAudioSourceNode;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -14,14 +14,12 @@ export class MicrophoneSource extends AudioSource {
|
||||
const microphone = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
if (getApp().getAudioManager().getAudioContext()) {
|
||||
this.#node = getApp().getAudioManager().getAudioContext().createMediaStreamSource(microphone);
|
||||
|
||||
this.#node.connect(this.getNode());
|
||||
}
|
||||
}
|
||||
|
||||
getNode() {
|
||||
return this.#node;
|
||||
}
|
||||
|
||||
play() {
|
||||
// TODO, now is always on
|
||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,10 @@ export class UnitSink extends AudioSink {
|
||||
frequency: 243000000,
|
||||
modulation: 255, // HOPEFULLY this will never be used by SRS, indicates "loudspeaker" mode
|
||||
},
|
||||
getApp().getAudioManager().getGuid()
|
||||
getApp().getAudioManager().getGuid(),
|
||||
this.#unit.getPosition().lat,
|
||||
this.#unit.getPosition().lng,
|
||||
this.#unit.getPosition().alt
|
||||
);
|
||||
getApp().getAudioManager().send(packet.getArray());
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ import { AudioManager } from "./audio/audiomanager";
|
||||
|
||||
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
|
||||
export var IP = window.location.toString();
|
||||
export var connectedToServer = true; // Temporary
|
||||
export var connectedToServer = true; // TODO Temporary
|
||||
|
||||
export class OlympusApp {
|
||||
/* Global data */
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { AudioSourcePanel } from "./components/audiosourcepanel";
|
||||
import { AudioSourcePanel } from "./components/sourcepanel";
|
||||
import { AudioSource } from "../../audio/audiosource";
|
||||
|
||||
export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
|
||||
@ -1,119 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { AudioSource } from "../../../audio/audiosource";
|
||||
import { FaTrash, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
import { FaUnlink } from "react-icons/fa";
|
||||
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
||||
import { FileSource } from "../../../audio/filesource";
|
||||
|
||||
export function AudioSourcePanel(props: { source: AudioSource }) {
|
||||
const [meterLevel, setMeterLevel] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
// TODO apply to all sources
|
||||
if (props.source instanceof FileSource) {
|
||||
setMeterLevel(props.source.getMeter().getPeaks().current[0]);
|
||||
}
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center justify-between gap-2 rounded-md
|
||||
bg-olympus-200/30 py-3 pl-4 pr-5
|
||||
`}
|
||||
>
|
||||
<span>{props.source.getName()}</span>
|
||||
{props.source instanceof FileSource && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-4">
|
||||
<OlStateButton
|
||||
checked={false}
|
||||
icon={props.source.getPlaying() ? faPause : faPlay}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.stop() : props.source.play();
|
||||
}}
|
||||
tooltip="Play file"
|
||||
></OlStateButton>
|
||||
<OlRangeSlider
|
||||
value={(props.source.getCurrentPosition() / props.source.getDuration()) * 100}
|
||||
onChange={(ev) => {
|
||||
if (props.source instanceof FileSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||
}}
|
||||
className="my-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={props.source.getLooping()}
|
||||
icon={faRepeat}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping());
|
||||
}}
|
||||
tooltip="Loop"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="h-[40px] min-w-[40px] p-2">
|
||||
<FaVolumeHigh className="h-full w-full" />
|
||||
</div>
|
||||
<div className="relative flex w-full flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
absolute top-[18px] flex h-2 min-w-full translate-y-[-5px]
|
||||
flex-row border-gray-500
|
||||
`}
|
||||
>
|
||||
<div
|
||||
style={{ minWidth: `${meterLevel * 100}%` }}
|
||||
className={`rounded-full bg-gray-200`}
|
||||
></div>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
value={props.source.getVolume() * 100}
|
||||
onChange={(ev) => {
|
||||
if (props.source instanceof FileSource) props.source.setVolume(parseFloat(ev.currentTarget.value) / 100);
|
||||
}}
|
||||
className="absolute top-[18px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[40px] min-w-[40px] p-2">
|
||||
<span>{Math.round(props.source.getVolume() * 100)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm">Connected to:</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.source.getConnectedTo().map((sink) => {
|
||||
return (
|
||||
<div className="flex justify-between text-sm">
|
||||
{sink.getName()}
|
||||
<FaUnlink className="cursor-pointer" onClick={() => props.source.disconnect(sink)}></FaUnlink>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<OlDropdown label="Connect to:">
|
||||
{getApp()
|
||||
.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => !props.source.getConnectedTo().includes(sink))
|
||||
.map((sink) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
props.source.connect(sink);
|
||||
}}
|
||||
>
|
||||
{sink.getName()}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -17,7 +17,7 @@ export function RadioPanel(props: { radio: RadioSink }) {
|
||||
>
|
||||
<div className="flex content-center justify-between">
|
||||
<span className="my-auto">{props.radio.getName()}</span>
|
||||
<div className="rounded-md bg-red-800 p-2" onClick={() => {getApp().getAudioManager().removeSink(props.radio);}}>
|
||||
<div className="cursor-pointer rounded-md bg-red-800 p-2" onClick={() => {getApp().getAudioManager().removeSink(props.radio);}}>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
141
frontend/react/src/ui/panels/components/sourcepanel.tsx
Normal file
141
frontend/react/src/ui/panels/components/sourcepanel.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { AudioSource } from "../../../audio/audiosource";
|
||||
import { FaArrowRight, FaTrash, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
import { FaUnlink } from "react-icons/fa";
|
||||
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
||||
import { FileSource } from "../../../audio/filesource";
|
||||
import { MicrophoneSource } from "../../../audio/microphonesource";
|
||||
|
||||
export function AudioSourcePanel(props: { source: AudioSource }) {
|
||||
const [meterLevel, setMeterLevel] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
setMeterLevel(props.source.getMeter().getPeaks().current[0]);
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
let availabileSinks = getApp()
|
||||
.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => !props.source.getConnectedTo().includes(sink));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center justify-between gap-2 rounded-md
|
||||
bg-olympus-200/30 py-3 pl-4 pr-5
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-between gap-2">
|
||||
<span className="break-all">{props.source.getName()}</span>
|
||||
{!(props.source instanceof MicrophoneSource) && (
|
||||
<div
|
||||
className={`
|
||||
mb-auto aspect-square cursor-pointer rounded-md bg-red-800 p-2
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp().getAudioManager().removeSource(props.source);
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 rounded-md bg-olympus-400 p-2">
|
||||
{props.source instanceof FileSource && (
|
||||
<div className="flex gap-4">
|
||||
<OlStateButton
|
||||
checked={false}
|
||||
icon={props.source.getPlaying() ? faPause : faPlay}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.stop() : props.source.play();
|
||||
}}
|
||||
tooltip="Play file"
|
||||
></OlStateButton>
|
||||
<OlRangeSlider
|
||||
value={props.source.getDuration() > 0 ? (props.source.getCurrentPosition() / props.source.getDuration()) * 100 : 0}
|
||||
onChange={(ev) => {
|
||||
if (props.source instanceof FileSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||
}}
|
||||
className="my-auto"
|
||||
/>
|
||||
<OlStateButton
|
||||
checked={props.source.getLooping()}
|
||||
icon={faRepeat}
|
||||
onClick={() => {
|
||||
if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping());
|
||||
}}
|
||||
tooltip="Loop"
|
||||
></OlStateButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-4">
|
||||
<div className="h-[40px] min-w-[40px] p-2">
|
||||
<FaVolumeHigh className="h-full w-full" />
|
||||
</div>
|
||||
<div className="relative flex w-full flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
absolute top-[18px] flex h-2 min-w-full translate-y-[-5px]
|
||||
flex-row border-gray-500
|
||||
`}
|
||||
>
|
||||
<div style={{ minWidth: `${meterLevel * 100}%` }} className={`
|
||||
rounded-full bg-gray-200
|
||||
`}></div>
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
value={props.source.getVolume() * 100}
|
||||
onChange={(ev) => {
|
||||
props.source.setVolume(parseFloat(ev.currentTarget.value) / 100);
|
||||
}}
|
||||
className="absolute top-[18px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[40px] min-w-[40px] p-2">
|
||||
<span>{Math.round(props.source.getVolume() * 100)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="text-sm">Connected to:</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{props.source.getConnectedTo().map((sink) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex justify-start gap-2 rounded-full bg-olympus-400 px-4 py-1
|
||||
text-sm
|
||||
`}
|
||||
>
|
||||
<FaArrowRight className="my-auto"></FaArrowRight>
|
||||
{sink.getName()}
|
||||
<FaUnlink className="my-auto ml-auto cursor-pointer text-red-400" onClick={() => props.source.disconnect(sink)}></FaUnlink>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{availabileSinks.length > 0 && (
|
||||
<OlDropdown label="Connect to:">
|
||||
{availabileSinks.map((sink) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
props.source.connect(sink);
|
||||
}}
|
||||
>
|
||||
{sink.getName()}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -134,14 +134,6 @@ export function UI() {
|
||||
setLoginModalVisible(false);
|
||||
}
|
||||
|
||||
/* Temporary during devel */
|
||||
//useEffect(() => {
|
||||
// window.setTimeout(() => {
|
||||
// checkPassword("admin");
|
||||
// connect("devel");
|
||||
// }, 1000)
|
||||
//}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
|
||||
@ -850,8 +850,7 @@ export abstract class Unit extends CustomMarker {
|
||||
if (targetPosition) getApp().getUnitsManager().addDestination(targetPosition, false, 0, units);
|
||||
}
|
||||
);
|
||||
|
||||
/* Temporarily removed until more work can be done for external sounds
|
||||
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"speaker",
|
||||
@ -864,7 +863,6 @@ export abstract class Unit extends CustomMarker {
|
||||
},
|
||||
{ executeImmediately: true }
|
||||
);
|
||||
*/
|
||||
|
||||
contextActionSet.addDefaultContextAction(
|
||||
this,
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"morgan": "~1.9.1",
|
||||
"open": "^10.0.0",
|
||||
"opus-decoder": "^0.7.6",
|
||||
"regedit": "^5.1.2",
|
||||
"save": "^2.9.0",
|
||||
"sha256": "^0.2.0",
|
||||
@ -31,10 +30,12 @@
|
||||
"tcp-ping-port": "^1.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"wavefile": "^11.0.0",
|
||||
"web-audio-api": "^0.2.2",
|
||||
"ws": "^8.18.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"opus-decoder": "^0.7.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
|
||||
236
frontend/server/src/audio/audiopacket.ts
Normal file
236
frontend/server/src/audio/audiopacket.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { Buffer } from "buffer";
|
||||
import {
|
||||
byteArrayToDouble,
|
||||
byteArrayToInteger,
|
||||
doubleToByteArray,
|
||||
integerToByteArray,
|
||||
} from "../utils";
|
||||
|
||||
var packetID = 0;
|
||||
|
||||
export class AudioPacket {
|
||||
#encodedData: Uint8Array;
|
||||
|
||||
/* Mandatory data */
|
||||
#frequencies: { frequency: number; modulation: number; encryption: number }[];
|
||||
#audioData: Uint8Array;
|
||||
#transmissionGUID: string;
|
||||
#clientGUID: string;
|
||||
|
||||
/* Default data */
|
||||
#unitID: number = 0;
|
||||
#packetID: number = 0;
|
||||
#hops: number = 0;
|
||||
|
||||
/* Out of standard data (this is not compliant with SRS standard, used for external audio effects) */
|
||||
#latitude: number | null = null;
|
||||
#longitude: number | null = null;
|
||||
#altitude: number | null = null;
|
||||
|
||||
setEncodedData(encodedData: Uint8Array) {
|
||||
this.#encodedData = encodedData;
|
||||
}
|
||||
|
||||
getEncodedData() {
|
||||
return this.#encodedData;
|
||||
}
|
||||
|
||||
setFrequencies(frequencies: { frequency: number; modulation: number; encryption: number }[]) {
|
||||
this.#frequencies = frequencies;
|
||||
}
|
||||
|
||||
getFrequencies() {
|
||||
return this.#frequencies;
|
||||
}
|
||||
|
||||
setAudioData(audioData: Uint8Array) {
|
||||
this.#audioData = audioData;
|
||||
}
|
||||
|
||||
getAudioData() {
|
||||
return this.#audioData;
|
||||
}
|
||||
|
||||
setTransmissionGUID(transmissionGUID: string) {
|
||||
this.#transmissionGUID = transmissionGUID;
|
||||
}
|
||||
|
||||
getTransmissionGUID() {
|
||||
return this.#transmissionGUID;
|
||||
}
|
||||
|
||||
setClientGUID(clientGUID: string) {
|
||||
this.#clientGUID = clientGUID;
|
||||
}
|
||||
|
||||
getClientGUID() {
|
||||
return this.#clientGUID;
|
||||
}
|
||||
|
||||
setUnitID(unitID: number) {
|
||||
this.#unitID = unitID;
|
||||
}
|
||||
|
||||
getUnitID() {
|
||||
return this.#unitID;
|
||||
}
|
||||
|
||||
setPacketID(packetID: number) {
|
||||
this.#packetID = packetID;
|
||||
}
|
||||
|
||||
getPacketID() {
|
||||
return this.#packetID;
|
||||
}
|
||||
|
||||
setHops(hops: number) {
|
||||
this.#hops = hops;
|
||||
}
|
||||
|
||||
getHops() {
|
||||
return this.#hops;
|
||||
}
|
||||
|
||||
setLatitude(latitude: number) {
|
||||
this.#latitude = latitude;
|
||||
}
|
||||
|
||||
getLatitude() {
|
||||
return this.#latitude;
|
||||
}
|
||||
|
||||
setLongitude(longitude: number) {
|
||||
this.#longitude = longitude;
|
||||
}
|
||||
|
||||
getLongitude() {
|
||||
return this.#longitude;
|
||||
}
|
||||
|
||||
setAltitude(altitude: number) {
|
||||
this.#altitude = altitude;
|
||||
}
|
||||
|
||||
getAltitude() {
|
||||
return this.#altitude;
|
||||
}
|
||||
|
||||
fromByteArray(byteArray: Uint8Array) {
|
||||
let totalLength = byteArrayToInteger(byteArray.slice(0, 2));
|
||||
let audioLength = byteArrayToInteger(byteArray.slice(2, 4));
|
||||
let frequenciesLength = byteArrayToInteger(byteArray.slice(4, 6));
|
||||
|
||||
/* Perform some sanity checks */
|
||||
if (totalLength !== byteArray.length) {
|
||||
console.log(
|
||||
`Warning, audio packet expected length is ${totalLength} but received length is ${byteArray.length}, aborting...`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frequenciesLength % 10 !== 0) {
|
||||
console.log(
|
||||
`Warning, audio packet frequencies data length is ${frequenciesLength} which is not a multiple of 10, aborting...`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Extract the audio data */
|
||||
this.#audioData = byteArray.slice(6, 6 + audioLength);
|
||||
|
||||
/* Extract the frequencies */
|
||||
let offset = 6 + audioLength;
|
||||
for (let idx = 0; idx < frequenciesLength / 10; idx++) {
|
||||
this.#frequencies.push({
|
||||
frequency: byteArrayToDouble(byteArray.slice(offset, offset + 8)),
|
||||
encryption: byteArray[offset + 8],
|
||||
modulation: byteArray[offset + 9],
|
||||
});
|
||||
offset += 10;
|
||||
}
|
||||
|
||||
/* Extract the remaining data */
|
||||
this.#unitID = byteArrayToInteger(byteArray.slice(offset, offset + 4));
|
||||
offset += 4;
|
||||
this.#packetID = byteArrayToInteger(byteArray.slice(offset, offset + 8));
|
||||
offset += 8;
|
||||
this.#hops = byteArrayToInteger(byteArray.slice(offset, offset + 1));
|
||||
offset += 1;
|
||||
this.#transmissionGUID = byteArray.slice(offset, offset + 22).toString();
|
||||
offset += 22;
|
||||
this.#clientGUID = byteArray.slice(offset, offset + 22).toString();
|
||||
offset += 22;
|
||||
}
|
||||
|
||||
toByteArray() {
|
||||
/* Perform some sanity checks // TODO check correct values */
|
||||
if (this.#frequencies.length === 0) {
|
||||
console.log("Warning, could not encode audio packet, no frequencies data provided, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#audioData === undefined) {
|
||||
console.log("Warning, could not encode audio packet, no audio data provided, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#transmissionGUID === undefined) {
|
||||
console.log("Warning, could not encode audio packet, no transmission GUID provided, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#clientGUID === undefined) {
|
||||
console.log("Warning, could not encode audio packet, no client GUID provided, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the array for the header
|
||||
let header: number[] = [0, 0, 0, 0, 0, 0];
|
||||
|
||||
// Encode the frequencies data
|
||||
let frequenciesData = ([] as number[])
|
||||
this.#frequencies.forEach((data) => {
|
||||
return frequenciesData.concat([...doubleToByteArray(data.frequency)], [data.modulation], [data.encryption]);
|
||||
})
|
||||
|
||||
// Encode unitID, packetID, hops
|
||||
let encUnitID: number[] = integerToByteArray(this.#unitID, 4);
|
||||
let encPacketID: number[] = integerToByteArray(this.#packetID, 8);
|
||||
let encHops: number[] = [this.#hops];
|
||||
|
||||
// Assemble packet
|
||||
let encodedData: number[] = ([] as number[]).concat(
|
||||
header,
|
||||
[...this.#audioData],
|
||||
frequenciesData,
|
||||
encUnitID,
|
||||
encPacketID,
|
||||
encHops,
|
||||
[...Buffer.from(this.#transmissionGUID, "utf-8")],
|
||||
[...Buffer.from(this.#clientGUID, "utf-8")]
|
||||
);
|
||||
|
||||
if (this.#latitude !== undefined && this.#longitude !== undefined && this.#altitude !== undefined) {
|
||||
encodedData.concat(
|
||||
[...doubleToByteArray(this.#latitude)],
|
||||
[...doubleToByteArray(this.#longitude)],
|
||||
[...doubleToByteArray(this.#altitude)]
|
||||
);
|
||||
}
|
||||
|
||||
// Set the lengths of the parts
|
||||
let encPacketLen = integerToByteArray(encodedData.length, 2);
|
||||
encodedData[0] = encPacketLen[0];
|
||||
encodedData[1] = encPacketLen[1];
|
||||
|
||||
let encAudioLen = integerToByteArray(this.#audioData.length, 2);
|
||||
encodedData[2] = encAudioLen[0];
|
||||
encodedData[3] = encAudioLen[1];
|
||||
|
||||
let frequencyAudioLen = integerToByteArray(frequenciesData.length, 2);
|
||||
encodedData[4] = frequencyAudioLen[0];
|
||||
encodedData[5] = frequencyAudioLen[1];
|
||||
|
||||
this.#encodedData = new Uint8Array([0].concat(encodedData));
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,13 @@
|
||||
import { defaultSRSData } from "./defaultdata";
|
||||
|
||||
const { OpusEncoder } = require("@discordjs/opus");
|
||||
const encoder = new OpusEncoder(16000, 1);
|
||||
|
||||
let decoder = null;
|
||||
import('opus-decoder').then((res) => {
|
||||
decoder = new res.OpusDecoder();
|
||||
});
|
||||
|
||||
var net = require("net");
|
||||
var bufferString = "";
|
||||
|
||||
@ -117,6 +123,7 @@ export class SRSHandler {
|
||||
let offset = 6 + audioLength + frequenciesLength;
|
||||
|
||||
if (modulation == 255) {
|
||||
packetUint8Array = packetUint8Array.slice(0, -24) // Remove position data
|
||||
packetUint8Array[6 + audioLength + 8] = 2;
|
||||
this.clients.forEach((client) => {
|
||||
getBytes(client.RadioInfo.unitId, 4).forEach((value, idx) => {
|
||||
|
||||
30
frontend/server/src/utils.ts
Normal file
30
frontend/server/src/utils.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export function byteArrayToInteger(array) {
|
||||
let res = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
res = res << 8;
|
||||
res += array[array.length - i - 1];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function integerToByteArray(value, length) {
|
||||
let res: number[] = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
res.push(value & 255);
|
||||
value = value >> 8;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function doubleToByteArray(number) {
|
||||
var buffer = new ArrayBuffer(8); // JS numbers are 8 bytes long, or 64 bits
|
||||
var longNum = new Float64Array(buffer); // so equivalent to Float64
|
||||
|
||||
longNum[0] = number;
|
||||
|
||||
return Array.from(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
export function byteArrayToDouble(array) {
|
||||
return new DataView(array.reverse().buffer).getFloat64(0);
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"allowJs": true,
|
||||
"target": "es5",
|
||||
"target": "ES2023",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user