mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
First tests and integration of radio panel
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/react-leaflet": "^3.0.0",
|
"@types/react-leaflet": "^3.0.0",
|
||||||
"@types/turf": "^3.5.32",
|
"@types/turf": "^3.5.32",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"js-sha256": "^0.11.0",
|
"js-sha256": "^0.11.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-control-mini-map": "^0.4.0",
|
"leaflet-control-mini-map": "^0.4.0",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.6.0",
|
"@eslint/js": "^9.6.0",
|
||||||
|
"@types/node": "^22.5.1",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@typescript-eslint/parser": "^7.14.1",
|
"@typescript-eslint/parser": "^7.14.1",
|
||||||
|
|||||||
88
frontend/react/src/audio/audiomanager.ts
Normal file
88
frontend/react/src/audio/audiomanager.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { AudioRadioSetting } from "../interfaces";
|
||||||
|
import { getApp } from "../olympusapp";
|
||||||
|
import { Buffer } from "buffer";
|
||||||
|
import { MicrophoneHandler } from "./microphonehandler";
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
audio,
|
||||||
|
settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AudioManager {
|
||||||
|
#radioSettings: AudioRadioSetting[] = [
|
||||||
|
{
|
||||||
|
frequency: 124000000,
|
||||||
|
modulation: 0,
|
||||||
|
ptt: false,
|
||||||
|
tuned: false,
|
||||||
|
volume: 0.5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#microphoneHandlers: (MicrophoneHandler | null)[] =[];
|
||||||
|
|
||||||
|
#address: string = "localhost";
|
||||||
|
#port: number = 4000;
|
||||||
|
#socket: WebSocket | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
document.addEventListener("configLoaded", () => {
|
||||||
|
let config = getApp().getConfig();
|
||||||
|
if (config["WSPort"]) {
|
||||||
|
this.setPort(config["WSPort"]);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#microphoneHandlers = this.#radioSettings.map(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
let res = this.#address.match(/(?:http|https):\/\/(.+):/);
|
||||||
|
let wsAddress = res ? res[1] : this.#address;
|
||||||
|
|
||||||
|
this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`);
|
||||||
|
|
||||||
|
this.#socket.addEventListener("open", (event) => {
|
||||||
|
console.log("Connection to audio websocket successfull");
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#socket.addEventListener("error", (event) => {
|
||||||
|
console.log(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#socket.addEventListener("message", (event) => {
|
||||||
|
console.log("Message from server ", event.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setAddress(address) {
|
||||||
|
this.#address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPort(port) {
|
||||||
|
this.#port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRadioSettings() {
|
||||||
|
return JSON.parse(JSON.stringify(this.#radioSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
setRadioSettings(radioSettings: AudioRadioSetting[]) {
|
||||||
|
this.#radioSettings = radioSettings;
|
||||||
|
|
||||||
|
let message = {
|
||||||
|
type: "Settings update",
|
||||||
|
settings: this.#radioSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#radioSettings.forEach((setting, idx) => {
|
||||||
|
if (setting.ptt && !this.#microphoneHandlers[idx]) {
|
||||||
|
this.#microphoneHandlers[idx] = new MicrophoneHandler(this.#socket, setting);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.#socket?.readyState == 1)
|
||||||
|
this.#socket?.send(new Uint8Array([MessageType.settings, ...Buffer.from(JSON.stringify(message), "utf-8")]));
|
||||||
|
}
|
||||||
|
}
|
||||||
70
frontend/react/src/audio/audiopacket.ts
Normal file
70
frontend/react/src/audio/audiopacket.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
|
function getBytes(value, length) {
|
||||||
|
let res: number[] = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
res.push(value & 255);
|
||||||
|
value = value >> 8;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
var packetID = 0;
|
||||||
|
|
||||||
|
export class AudioPacket {
|
||||||
|
#packet: Uint8Array;
|
||||||
|
|
||||||
|
constructor(data, settings) {
|
||||||
|
let header: number[] = [0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
let encFrequency: number[] = [...doubleToByteArray(settings.frequency)];
|
||||||
|
let encModulation: number[] = [settings.modulation];
|
||||||
|
let encEncryption: number[] = [0];
|
||||||
|
|
||||||
|
let encUnitID: number[] = getBytes(100000001, 4);
|
||||||
|
let encPacketID: number[] = getBytes(packetID, 8);
|
||||||
|
packetID++;
|
||||||
|
let encHops: number[] = [0];
|
||||||
|
|
||||||
|
let packet: number[] = ([] as number[]).concat(
|
||||||
|
header,
|
||||||
|
[...data],
|
||||||
|
encFrequency,
|
||||||
|
encModulation,
|
||||||
|
encEncryption,
|
||||||
|
encUnitID,
|
||||||
|
encPacketID,
|
||||||
|
encHops,
|
||||||
|
[...Buffer.from("ImF72dh9EYcIDyYRGaF9S9", "utf-8")],
|
||||||
|
[...Buffer.from("ImF72dh9EYcIDyYRGaF9S9", "utf-8")]
|
||||||
|
);
|
||||||
|
|
||||||
|
let encPacketLen = getBytes(packet.length, 2);
|
||||||
|
packet[0] = encPacketLen[0];
|
||||||
|
packet[1] = encPacketLen[1];
|
||||||
|
|
||||||
|
let encAudioLen = getBytes(data.length, 2);
|
||||||
|
packet[2] = encAudioLen[0];
|
||||||
|
packet[3] = encAudioLen[1];
|
||||||
|
|
||||||
|
let frequencyAudioLen = getBytes(10, 2);
|
||||||
|
packet[4] = frequencyAudioLen[0];
|
||||||
|
packet[5] = frequencyAudioLen[1];
|
||||||
|
|
||||||
|
|
||||||
|
this.#packet = new Uint8Array([0].concat(packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
getArray() {
|
||||||
|
return this.#packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
frontend/react/src/audio/microphonehandler.ts
Normal file
52
frontend/react/src/audio/microphonehandler.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { AudioRadioSetting } from "../interfaces";
|
||||||
|
import { AudioPacket } from "./audiopacket";
|
||||||
|
|
||||||
|
export class MicrophoneHandler {
|
||||||
|
#socket: WebSocket;
|
||||||
|
#setting: AudioRadioSetting;
|
||||||
|
|
||||||
|
constructor(socket, setting) {
|
||||||
|
this.#socket = socket;
|
||||||
|
this.#setting = setting;
|
||||||
|
|
||||||
|
console.log("Starting microphone handler");
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||||
|
|
||||||
|
if (getUserMedia) {
|
||||||
|
//@ts-ignore
|
||||||
|
navigator.getUserMedia(
|
||||||
|
{ audio: {
|
||||||
|
sampleRate: 16000,
|
||||||
|
channelCount: 1,
|
||||||
|
volume: 1.0
|
||||||
|
} },
|
||||||
|
(stream) => {
|
||||||
|
this.start_microphone(stream);
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
alert("Error capturing audio.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert("getUserMedia not supported in this browser.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start_microphone(stream) {
|
||||||
|
const recorder = new MediaRecorder(stream);
|
||||||
|
|
||||||
|
// fires every one second and passes an BlobEvent
|
||||||
|
recorder.ondataavailable = async (event) => {
|
||||||
|
// get the Blob from the event
|
||||||
|
const blob = event.data;
|
||||||
|
|
||||||
|
let rawData = await blob.arrayBuffer();
|
||||||
|
let packet = new AudioPacket(new Uint8Array(rawData), this.#setting);
|
||||||
|
this.#socket.send(packet.getArray());
|
||||||
|
};
|
||||||
|
|
||||||
|
recorder.start(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export const EventsContext = createContext({
|
|||||||
setDrawingMenuVisible: (e: boolean) => {},
|
setDrawingMenuVisible: (e: boolean) => {},
|
||||||
setOptionsMenuVisible: (e: boolean) => {},
|
setOptionsMenuVisible: (e: boolean) => {},
|
||||||
setAirbaseMenuVisible: (e: boolean) => {},
|
setAirbaseMenuVisible: (e: boolean) => {},
|
||||||
|
setRadioMenuVisible: (e: boolean) => {},
|
||||||
toggleMainMenuVisible: () => {},
|
toggleMainMenuVisible: () => {},
|
||||||
toggleSpawnMenuVisible: () => {},
|
toggleSpawnMenuVisible: () => {},
|
||||||
toggleUnitControlMenuVisible: () => {},
|
toggleUnitControlMenuVisible: () => {},
|
||||||
@@ -15,6 +16,7 @@ export const EventsContext = createContext({
|
|||||||
toggleDrawingMenuVisible: () => {},
|
toggleDrawingMenuVisible: () => {},
|
||||||
toggleOptionsMenuVisible: () => {},
|
toggleOptionsMenuVisible: () => {},
|
||||||
toggleAirbaseMenuVisible: () => {},
|
toggleAirbaseMenuVisible: () => {},
|
||||||
|
toggleRadioMenuVisible: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EventsProvider = EventsContext.Provider;
|
export const EventsProvider = EventsContext.Provider;
|
||||||
|
|||||||
@@ -291,3 +291,11 @@ export interface ServerStatus {
|
|||||||
connected: boolean;
|
connected: boolean;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AudioRadioSetting {
|
||||||
|
frequency: number;
|
||||||
|
modulation: number;
|
||||||
|
volume: number;
|
||||||
|
ptt: boolean;
|
||||||
|
tuned: boolean;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { helicopterDatabase } from "./unit/databases/helicopterdatabase";
|
|||||||
import { groundUnitDatabase } from "./unit/databases/groundunitdatabase";
|
import { groundUnitDatabase } from "./unit/databases/groundunitdatabase";
|
||||||
import { navyUnitDatabase } from "./unit/databases/navyunitdatabase";
|
import { navyUnitDatabase } from "./unit/databases/navyunitdatabase";
|
||||||
import { Coalition, Context } from "./types/types";
|
import { Coalition, Context } from "./types/types";
|
||||||
|
import { AudioManager } from "./audio/audiomanager";
|
||||||
|
|
||||||
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
|
export var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
|
||||||
export var IP = window.location.toString();
|
export var IP = window.location.toString();
|
||||||
@@ -45,6 +46,7 @@ export class OlympusApp {
|
|||||||
#shortcutManager: ShortcutManager | null = null;
|
#shortcutManager: ShortcutManager | null = null;
|
||||||
#unitsManager: UnitsManager | null = null;
|
#unitsManager: UnitsManager | null = null;
|
||||||
#weaponsManager: WeaponsManager | null = null;
|
#weaponsManager: WeaponsManager | null = null;
|
||||||
|
#audioManager: AudioManager | null = null;
|
||||||
//#pluginsManager: // TODO
|
//#pluginsManager: // TODO
|
||||||
|
|
||||||
/* Current context */
|
/* Current context */
|
||||||
@@ -79,6 +81,10 @@ export class OlympusApp {
|
|||||||
getMissionManager() {
|
getMissionManager() {
|
||||||
return this.#missionManager as MissionManager;
|
return this.#missionManager as MissionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudioManager() {
|
||||||
|
return this.#audioManager as AudioManager;
|
||||||
|
}
|
||||||
|
|
||||||
/* TODO
|
/* TODO
|
||||||
getPluginsManager() {
|
getPluginsManager() {
|
||||||
@@ -151,9 +157,11 @@ export class OlympusApp {
|
|||||||
this.#shortcutManager = new ShortcutManager();
|
this.#shortcutManager = new ShortcutManager();
|
||||||
this.#unitsManager = new UnitsManager();
|
this.#unitsManager = new UnitsManager();
|
||||||
this.#weaponsManager = new WeaponsManager();
|
this.#weaponsManager = new WeaponsManager();
|
||||||
|
this.#audioManager = new AudioManager();
|
||||||
|
|
||||||
/* Set the address of the server */
|
/* Set the address of the server */
|
||||||
this.getServerManager().setAddress(window.location.href.split("?")[0].replace("vite/", ""));
|
this.getServerManager().setAddress(window.location.href.split("?")[0].replace("vite/", ""));
|
||||||
|
this.getAudioManager().setAddress(window.location.href.split("?")[0].replace("vite/", ""));
|
||||||
|
|
||||||
/* Setup all global events */
|
/* Setup all global events */
|
||||||
this.#setupEvents();
|
this.#setupEvents();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const StateContext = createContext({
|
|||||||
drawingMenuVisible: false,
|
drawingMenuVisible: false,
|
||||||
optionsMenuVisible: false,
|
optionsMenuVisible: false,
|
||||||
airbaseMenuVisible: false,
|
airbaseMenuVisible: false,
|
||||||
|
radioMenuVisible: false,
|
||||||
mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS,
|
mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS,
|
||||||
mapOptions: MAP_OPTIONS_DEFAULTS,
|
mapOptions: MAP_OPTIONS_DEFAULTS,
|
||||||
mapSources: [] as string[],
|
mapSources: [] as string[],
|
||||||
|
|||||||
56
frontend/react/src/ui/components/olfrequencyinput.tsx
Normal file
56
frontend/react/src/ui/components/olfrequencyinput.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
|
import { OlNumberInput } from "./olnumberinput";
|
||||||
|
|
||||||
|
export function OlFrequencyInput(props: { value: number; className?: string; onChange: (value: number) => void }) {
|
||||||
|
let frequency = props.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`
|
||||||
|
${props.className}
|
||||||
|
flex gap-2
|
||||||
|
`}>
|
||||||
|
<OlNumberInput
|
||||||
|
min={1}
|
||||||
|
max={400}
|
||||||
|
onChange={(e) => {
|
||||||
|
let newValue = Math.max(Math.min(Number(e.target.value), 400), 1) * 1000000;
|
||||||
|
let decimalPart = frequency - Math.floor(frequency / 1000000) * 1000000;
|
||||||
|
frequency = newValue + decimalPart;
|
||||||
|
props.onChange(frequency);
|
||||||
|
}}
|
||||||
|
onDecrease={() => {
|
||||||
|
frequency = Math.max(Math.min(Number(frequency - 1000000), 400000000), 1000000);
|
||||||
|
props.onChange(frequency);
|
||||||
|
}}
|
||||||
|
onIncrease={() => {
|
||||||
|
frequency = Math.max(Math.min(Number(frequency + 1000000), 400000000), 1000000);
|
||||||
|
props.onChange(frequency);
|
||||||
|
}}
|
||||||
|
value={Math.floor(frequency / 1000000)}
|
||||||
|
></OlNumberInput>
|
||||||
|
<div className="my-auto">.</div>
|
||||||
|
<OlNumberInput
|
||||||
|
min={0}
|
||||||
|
max={990}
|
||||||
|
minLength={3}
|
||||||
|
onChange={(e) => {
|
||||||
|
let newValue = Math.max(Math.min(Number(e.target.value), 990), 0) * 1000;
|
||||||
|
let integerPart = Math.floor(frequency / 1000000) * 1000000;
|
||||||
|
frequency = newValue + integerPart;
|
||||||
|
props.onChange(frequency);
|
||||||
|
}}
|
||||||
|
onDecrease={() => {
|
||||||
|
frequency = Math.max(Math.min(Number(frequency - 25000), 400000000), 1000000);
|
||||||
|
props.onChange(frequency);
|
||||||
|
}}
|
||||||
|
onIncrease={() => {
|
||||||
|
frequency = Math.max(Math.min(Number(frequency + 25000), 400000000), 1000000);
|
||||||
|
props.onChange(frequency);
|
||||||
|
}}
|
||||||
|
value={(frequency - Math.floor(frequency / 1000000) * 1000000) / 1000}
|
||||||
|
></OlNumberInput>
|
||||||
|
<div className="my-auto">MHz</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export function OlRangeSlider(props: {
|
|||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
|
className?: string;
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
}) {
|
}) {
|
||||||
var elementRef = useRef(null);
|
var elementRef = useRef(null);
|
||||||
@@ -28,6 +29,7 @@ export function OlRangeSlider(props: {
|
|||||||
max={props.max ?? 100}
|
max={props.max ?? 100}
|
||||||
step={props.step ?? 1}
|
step={props.step ?? 1}
|
||||||
className={`
|
className={`
|
||||||
|
${props.className}
|
||||||
h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200
|
h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200
|
||||||
dark:bg-gray-700
|
dark:bg-gray-700
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { OlNumberInput } from "../components/olnumberinput";
|
|||||||
import { MapOptions } from "../../types/types";
|
import { MapOptions } from "../../types/types";
|
||||||
import { getApp } from "../../olympusapp";
|
import { getApp } from "../../olympusapp";
|
||||||
|
|
||||||
export function Options(props: { open: boolean; onClose: () => void; options: MapOptions; children?: JSX.Element | JSX.Element[] }) {
|
export function OptionsMenu(props: { open: boolean; onClose: () => void; options: MapOptions; children?: JSX.Element | JSX.Element[] }) {
|
||||||
return (
|
return (
|
||||||
<Menu title="User preferences" open={props.open} showBackButton={false} onClose={props.onClose}>
|
<Menu title="User preferences" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||||
<div
|
<div
|
||||||
73
frontend/react/src/ui/panels/radiomenu.tsx
Normal file
73
frontend/react/src/ui/panels/radiomenu.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Menu } from "./components/menu";
|
||||||
|
import { OlCheckbox } from "../components/olcheckbox";
|
||||||
|
import { OlRangeSlider } from "../components/olrangeslider";
|
||||||
|
import { OlNumberInput } from "../components/olnumberinput";
|
||||||
|
import { MapOptions } from "../../types/types";
|
||||||
|
import { getApp } from "../../olympusapp";
|
||||||
|
import { OlFrequencyInput } from "../components/olfrequencyinput";
|
||||||
|
import { OlStateButton } from "../components/olstatebutton";
|
||||||
|
import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||||
|
import { FaVolumeHigh } from "react-icons/fa6";
|
||||||
|
|
||||||
|
export function RadioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||||
|
const [frequency1, setFrequency1] = useState(124000000);
|
||||||
|
const [ptt1, setPTT1] = useState(false);
|
||||||
|
const [frequency2, setFrequency2] = useState(251000000);
|
||||||
|
const [frequency3, setFrequency3] = useState(243000000);
|
||||||
|
const [frequency4, setFrequency4] = useState(11200000);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getApp()) {
|
||||||
|
let settings = getApp().getAudioManager().getRadioSettings();
|
||||||
|
settings[0].frequency = frequency1;
|
||||||
|
settings[0].ptt = ptt1;
|
||||||
|
getApp().getAudioManager().setRadioSettings(settings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu title="Radio" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex flex-col gap-2 p-5 font-normal text-gray-800
|
||||||
|
dark:text-white
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex flex-col content-center justify-between gap-2 rounded-md
|
||||||
|
bg-olympus-200/30 py-3 pl-4 pr-5
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Radio 1
|
||||||
|
<OlFrequencyInput
|
||||||
|
value={frequency1}
|
||||||
|
onChange={(value) => {
|
||||||
|
setFrequency1(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-4 py-2">
|
||||||
|
<FaVolumeHigh className="h-8 w-8 p-1" />
|
||||||
|
<OlRangeSlider value={50} onChange={() => {}} className="my-auto" />
|
||||||
|
<span className="my-auto">50</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<OlLabelToggle leftLabel="AM" rightLabel="FM" toggled={false} onClick={() => {}}></OlLabelToggle>
|
||||||
|
<OlStateButton
|
||||||
|
className="ml-auto"
|
||||||
|
checked={ptt1}
|
||||||
|
icon={faMicrophoneLines}
|
||||||
|
onClick={() => {
|
||||||
|
setPTT1(!ptt1);
|
||||||
|
}}
|
||||||
|
tooltip="Talk on frequency"
|
||||||
|
></OlStateButton>
|
||||||
|
<OlStateButton checked={false} icon={faEarListen} onClick={() => {}} tooltip="Tune to radio"></OlStateButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { OlStateButton } from "../components/olstatebutton";
|
import { OlStateButton } from "../components/olstatebutton";
|
||||||
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faPlaneDeparture } from "@fortawesome/free-solid-svg-icons";
|
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faPlaneDeparture, faRadio } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { EventsConsumer } from "../../eventscontext";
|
import { EventsConsumer } from "../../eventscontext";
|
||||||
import { StateConsumer } from "../../statecontext";
|
import { StateConsumer } from "../../statecontext";
|
||||||
import { IDLE } from "../../constants/constants";
|
import { IDLE } from "../../constants/constants";
|
||||||
@@ -58,6 +58,12 @@ export function SideBar() {
|
|||||||
icon={faPlaneDeparture}
|
icon={faPlaneDeparture}
|
||||||
tooltip="Hide/show airbase menu"
|
tooltip="Hide/show airbase menu"
|
||||||
></OlStateButton>
|
></OlStateButton>
|
||||||
|
<OlStateButton
|
||||||
|
onClick={events.toggleRadioMenuVisible}
|
||||||
|
checked={appState.radioMenuVisible}
|
||||||
|
icon={faRadio}
|
||||||
|
tooltip="Hide/show radio menu"
|
||||||
|
></OlStateButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-16 flex-wrap content-end justify-center p-4">
|
<div className="flex w-16 flex-wrap content-end justify-center p-4">
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { FaRadio } from "react-icons/fa6";
|
|||||||
import { OlNumberInput } from "../components/olnumberinput";
|
import { OlNumberInput } from "../components/olnumberinput";
|
||||||
import { Radio, TACAN } from "../../interfaces";
|
import { Radio, TACAN } from "../../interfaces";
|
||||||
import { OlStringInput } from "../components/olstringinput";
|
import { OlStringInput } from "../components/olstringinput";
|
||||||
|
import { OlFrequencyInput } from "../components/olfrequencyinput";
|
||||||
|
|
||||||
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||||
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
||||||
@@ -1112,71 +1113,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
|||||||
|
|
||||||
<div className="text-sm text-gray-200">Radio frequency</div>
|
<div className="text-sm text-gray-200">Radio frequency</div>
|
||||||
<div className="flex content-center gap-2">
|
<div className="flex content-center gap-2">
|
||||||
<OlNumberInput
|
<OlFrequencyInput value={activeAdvancedSettings? activeAdvancedSettings.radio.frequency: 124000000} onChange={(value) => {
|
||||||
min={1}
|
if (activeAdvancedSettings) {
|
||||||
max={400}
|
activeAdvancedSettings.radio.frequency = value;
|
||||||
onChange={(e) => {
|
|
||||||
let newValue = Math.max(Math.min(Number(e.target.value), 400), 1) * 1000000;
|
|
||||||
if (activeAdvancedSettings) {
|
|
||||||
let decimalPart = activeAdvancedSettings.radio.frequency - Math.floor(activeAdvancedSettings.radio.frequency / 1000000) * 1000000;
|
|
||||||
activeAdvancedSettings.radio.frequency = newValue + decimalPart;
|
|
||||||
}
|
|
||||||
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
||||||
}}
|
|
||||||
onDecrease={() => {
|
|
||||||
if (activeAdvancedSettings)
|
|
||||||
activeAdvancedSettings.radio.frequency = Math.max(
|
|
||||||
Math.min(Number(activeAdvancedSettings.radio.frequency - 1000000), 400000000),
|
|
||||||
1000000
|
|
||||||
);
|
|
||||||
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
|
||||||
}}
|
|
||||||
onIncrease={() => {
|
|
||||||
if (activeAdvancedSettings)
|
|
||||||
activeAdvancedSettings.radio.frequency = Math.max(
|
|
||||||
Math.min(Number(activeAdvancedSettings.radio.frequency + 1000000), 400000000),
|
|
||||||
1000000
|
|
||||||
);
|
|
||||||
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
|
||||||
}}
|
|
||||||
value={activeAdvancedSettings ? Math.floor(activeAdvancedSettings.radio.frequency / 1000000) : 124}
|
|
||||||
></OlNumberInput>
|
|
||||||
<div className="my-auto">.</div>
|
|
||||||
<OlNumberInput
|
|
||||||
min={0}
|
|
||||||
max={990}
|
|
||||||
minLength={3}
|
|
||||||
onChange={(e) => {
|
|
||||||
let newValue = Math.max(Math.min(Number(e.target.value), 990), 0) * 1000;
|
|
||||||
if (activeAdvancedSettings) {
|
|
||||||
let integerPart = Math.floor(activeAdvancedSettings.radio.frequency / 1000000) * 1000000;
|
|
||||||
activeAdvancedSettings.radio.frequency = newValue + integerPart;
|
|
||||||
}
|
|
||||||
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
|
||||||
}}
|
|
||||||
onDecrease={() => {
|
|
||||||
if (activeAdvancedSettings)
|
|
||||||
activeAdvancedSettings.radio.frequency = Math.max(
|
|
||||||
Math.min(Number(activeAdvancedSettings.radio.frequency - 25000), 400000000),
|
|
||||||
1000000
|
|
||||||
);
|
|
||||||
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
|
||||||
}}
|
|
||||||
onIncrease={() => {
|
|
||||||
if (activeAdvancedSettings)
|
|
||||||
activeAdvancedSettings.radio.frequency = Math.max(
|
|
||||||
Math.min(Number(activeAdvancedSettings.radio.frequency + 25000), 400000000),
|
|
||||||
1000000
|
|
||||||
);
|
|
||||||
setActiveAdvancedSettings(JSON.parse(JSON.stringify(activeAdvancedSettings)));
|
|
||||||
}}
|
|
||||||
value={
|
|
||||||
activeAdvancedSettings
|
|
||||||
? (activeAdvancedSettings.radio.frequency - Math.floor(activeAdvancedSettings.radio.frequency / 1000000) * 1000000) / 1000
|
|
||||||
: 0
|
|
||||||
}
|
}
|
||||||
></OlNumberInput>
|
}}/>
|
||||||
<div className="my-auto">MHz</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex pt-8">
|
<div className="flex pt-8">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SpawnMenu } from "./panels/spawnmenu";
|
|||||||
import { UnitControlMenu } from "./panels/unitcontrolmenu";
|
import { UnitControlMenu } from "./panels/unitcontrolmenu";
|
||||||
import { MainMenu } from "./panels/mainmenu";
|
import { MainMenu } from "./panels/mainmenu";
|
||||||
import { SideBar } from "./panels/sidebar";
|
import { SideBar } from "./panels/sidebar";
|
||||||
import { Options } from "./panels/options";
|
import { OptionsMenu } from "./panels/optionsmenu";
|
||||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||||
import { BLUE_COMMANDER, CONTEXT_ACTION, GAME_MASTER, IDLE, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, RED_COMMANDER } from "../constants/constants";
|
import { BLUE_COMMANDER, CONTEXT_ACTION, GAME_MASTER, IDLE, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS, RED_COMMANDER } from "../constants/constants";
|
||||||
import { getApp, setupApp } from "../olympusapp";
|
import { getApp, setupApp } from "../olympusapp";
|
||||||
@@ -22,6 +22,7 @@ import { ControlsPanel } from "./panels/controlspanel";
|
|||||||
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
|
import { MapContextMenu } from "./contextmenus/mapcontextmenu";
|
||||||
import { AirbaseMenu } from "./panels/airbasemenu";
|
import { AirbaseMenu } from "./panels/airbasemenu";
|
||||||
import { Airbase } from "../mission/airbase";
|
import { Airbase } from "../mission/airbase";
|
||||||
|
import { RadioMenu } from "./panels/radiomenu";
|
||||||
|
|
||||||
export type OlympusUIState = {
|
export type OlympusUIState = {
|
||||||
mainMenuVisible: boolean;
|
mainMenuVisible: boolean;
|
||||||
@@ -42,6 +43,7 @@ export function UI() {
|
|||||||
const [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false);
|
const [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false);
|
||||||
const [measureMenuVisible, setMeasureMenuVisible] = useState(false);
|
const [measureMenuVisible, setMeasureMenuVisible] = useState(false);
|
||||||
const [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
|
const [drawingMenuVisible, setDrawingMenuVisible] = useState(false);
|
||||||
|
const [radioMenuVisible, setRadioMenuVisible] = useState(false);
|
||||||
const [optionsMenuVisible, setOptionsMenuVisible] = useState(false);
|
const [optionsMenuVisible, setOptionsMenuVisible] = useState(false);
|
||||||
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
|
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
|
||||||
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||||
@@ -97,6 +99,7 @@ export function UI() {
|
|||||||
setDrawingMenuVisible(false);
|
setDrawingMenuVisible(false);
|
||||||
setOptionsMenuVisible(false);
|
setOptionsMenuVisible(false);
|
||||||
setAirbaseMenuVisible(false);
|
setAirbaseMenuVisible(false);
|
||||||
|
setRadioMenuVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPassword(password: string) {
|
function checkPassword(password: string) {
|
||||||
@@ -153,6 +156,7 @@ export function UI() {
|
|||||||
drawingMenuVisible: drawingMenuVisible,
|
drawingMenuVisible: drawingMenuVisible,
|
||||||
optionsMenuVisible: optionsMenuVisible,
|
optionsMenuVisible: optionsMenuVisible,
|
||||||
airbaseMenuVisible: airbaseMenuVisible,
|
airbaseMenuVisible: airbaseMenuVisible,
|
||||||
|
radioMenuVisible: radioMenuVisible,
|
||||||
mapOptions: mapOptions,
|
mapOptions: mapOptions,
|
||||||
mapHiddenTypes: mapHiddenTypes,
|
mapHiddenTypes: mapHiddenTypes,
|
||||||
mapSources: mapSources,
|
mapSources: mapSources,
|
||||||
@@ -169,6 +173,7 @@ export function UI() {
|
|||||||
setMeasureMenuVisible: setMeasureMenuVisible,
|
setMeasureMenuVisible: setMeasureMenuVisible,
|
||||||
setOptionsMenuVisible: setOptionsMenuVisible,
|
setOptionsMenuVisible: setOptionsMenuVisible,
|
||||||
setAirbaseMenuVisible: setAirbaseMenuVisible,
|
setAirbaseMenuVisible: setAirbaseMenuVisible,
|
||||||
|
setRadioMenuVisible: setRadioMenuVisible,
|
||||||
toggleMainMenuVisible: () => {
|
toggleMainMenuVisible: () => {
|
||||||
hideAllMenus();
|
hideAllMenus();
|
||||||
setMainMenuVisible(!mainMenuVisible);
|
setMainMenuVisible(!mainMenuVisible);
|
||||||
@@ -197,6 +202,10 @@ export function UI() {
|
|||||||
hideAllMenus();
|
hideAllMenus();
|
||||||
setAirbaseMenuVisible(!airbaseMenuVisible);
|
setAirbaseMenuVisible(!airbaseMenuVisible);
|
||||||
},
|
},
|
||||||
|
toggleRadioMenuVisible: () => {
|
||||||
|
hideAllMenus();
|
||||||
|
setRadioMenuVisible(!radioMenuVisible);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
@@ -227,10 +236,11 @@ export function UI() {
|
|||||||
<div id="map-container" className="z-0 h-full w-screen" />
|
<div id="map-container" className="z-0 h-full w-screen" />
|
||||||
<MainMenu open={mainMenuVisible} onClose={() => setMainMenuVisible(false)} />
|
<MainMenu open={mainMenuVisible} onClose={() => setMainMenuVisible(false)} />
|
||||||
<SpawnMenu open={spawnMenuVisible} onClose={() => setSpawnMenuVisible(false)} />
|
<SpawnMenu open={spawnMenuVisible} onClose={() => setSpawnMenuVisible(false)} />
|
||||||
<Options open={optionsMenuVisible} onClose={() => setOptionsMenuVisible(false)} options={mapOptions} />
|
<OptionsMenu open={optionsMenuVisible} onClose={() => setOptionsMenuVisible(false)} options={mapOptions} />
|
||||||
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
|
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
|
||||||
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
|
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
|
||||||
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase}/>
|
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase}/>
|
||||||
|
<RadioMenu open={radioMenuVisible} onClose={() => setRadioMenuVisible(false)} />
|
||||||
|
|
||||||
<MiniMapPanel />
|
<MiniMapPanel />
|
||||||
<ControlsPanel />
|
<ControlsPanel />
|
||||||
|
|||||||
BIN
frontend/server/Example.ogg
Normal file
BIN
frontend/server/Example.ogg
Normal file
Binary file not shown.
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discordjs/opus": "^0.9.0",
|
||||||
"appjs": "^0.0.20",
|
"appjs": "^0.0.20",
|
||||||
"appjs-win32": "^0.0.19",
|
"appjs-win32": "^0.0.19",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
|
"node-opus": "^0.3.3",
|
||||||
"open": "^10.0.0",
|
"open": "^10.0.0",
|
||||||
"regedit": "^5.1.2",
|
"regedit": "^5.1.2",
|
||||||
"save": "^2.9.0",
|
"save": "^2.9.0",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"srtm-elevation": "^2.1.2",
|
"srtm-elevation": "^2.1.2",
|
||||||
"tcp-ping-port": "^1.0.1",
|
"tcp-ping-port": "^1.0.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
"wavefile": "^11.0.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
frontend/server/sample1.WAV
Normal file
BIN
frontend/server/sample1.WAV
Normal file
Binary file not shown.
BIN
frontend/server/sample3.WAV
Normal file
BIN
frontend/server/sample3.WAV
Normal file
Binary file not shown.
@@ -5,6 +5,7 @@ import logger = require("morgan");
|
|||||||
import fs = require("fs");
|
import fs = require("fs");
|
||||||
import bodyParser = require("body-parser");
|
import bodyParser = require("body-parser");
|
||||||
import cors = require("cors");
|
import cors = require("cors");
|
||||||
|
import { AudioBackend } from "./audio/audiobackend";
|
||||||
|
|
||||||
/* Load the proxy middleware plugin */
|
/* Load the proxy middleware plugin */
|
||||||
import httpProxyMiddleware = require("http-proxy-middleware");
|
import httpProxyMiddleware = require("http-proxy-middleware");
|
||||||
@@ -83,5 +84,10 @@ module.exports = function (configLocation, viteProxy) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config["audio"]) {
|
||||||
|
let audioBackend = new AudioBackend(config["audio"]["SRSPort"], config["audio"]["WSPort"]);
|
||||||
|
audioBackend.start();
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|||||||
27
frontend/server/src/audio/audiobackend.ts
Normal file
27
frontend/server/src/audio/audiobackend.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
import { SRSHandler } from "./srshandler";
|
||||||
|
|
||||||
|
export class AudioBackend {
|
||||||
|
SRSPort: number = 5002;
|
||||||
|
WSPort: number = 4000;
|
||||||
|
handlers: SRSHandler[] = [];
|
||||||
|
|
||||||
|
constructor(SRSPort, WSPort) {
|
||||||
|
this.SRSPort = SRSPort ?? this.SRSPort;
|
||||||
|
this.WSPort = WSPort ?? this.WSPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const wss = new WebSocketServer({ port: this.WSPort });
|
||||||
|
|
||||||
|
wss.on("connection", (ws) => {
|
||||||
|
this.handlers.push(new SRSHandler(ws, this.SRSPort));
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.on("disconnection", (ws) => {
|
||||||
|
this.handlers = this.handlers.filter((handler) => {
|
||||||
|
handler.ws != ws;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
112
frontend/server/src/audio/defaultdata.ts
Normal file
112
frontend/server/src/audio/defaultdata.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
export var defaultSRSData = {
|
||||||
|
ClientGuid: "",
|
||||||
|
Name: "",
|
||||||
|
Seat: 0,
|
||||||
|
Coalition: 0,
|
||||||
|
AllowRecord: false,
|
||||||
|
RadioInfo: {
|
||||||
|
radios: [
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
unit: "",
|
||||||
|
unitId: 0,
|
||||||
|
iff: {
|
||||||
|
control: 2,
|
||||||
|
mode1: -1,
|
||||||
|
mode2: -1,
|
||||||
|
mode3: -1,
|
||||||
|
mode4: false,
|
||||||
|
mic: -1,
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
ambient: { vol: 1.0, abType: "" },
|
||||||
|
},
|
||||||
|
LatLngPosition: { lat: 0.0, lng: 0.0, alt: 0.0 },
|
||||||
|
};
|
||||||
84
frontend/server/src/audio/srshandler.ts
Normal file
84
frontend/server/src/audio/srshandler.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { defaultSRSData } from "./defaultdata";
|
||||||
|
var net = require("net");
|
||||||
|
|
||||||
|
const SRS_VERSION = "2.1.0.10";
|
||||||
|
|
||||||
|
var globalIndex = 1;
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
audio,
|
||||||
|
settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeID(length) {
|
||||||
|
let result = "";
|
||||||
|
const characters =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
let counter = 0;
|
||||||
|
while (counter < length) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SRSHandler {
|
||||||
|
ws: any;
|
||||||
|
tcp = new net.Socket();
|
||||||
|
udp = require("dgram").createSocket("udp4");
|
||||||
|
data = JSON.parse(JSON.stringify(defaultSRSData));
|
||||||
|
syncInterval: any;
|
||||||
|
|
||||||
|
constructor(ws, SRSPort) {
|
||||||
|
this.data.ClientGuid = "ImF72dh9EYcIDyYRGaF9S9";
|
||||||
|
this.data.Name = `Olympus${globalIndex}`;
|
||||||
|
globalIndex += 1;
|
||||||
|
|
||||||
|
/* Websocket */
|
||||||
|
this.ws = ws;
|
||||||
|
this.ws.on("error", console.error);
|
||||||
|
this.ws.on("message", (data) => {
|
||||||
|
switch (data[0]) {
|
||||||
|
case MessageType.audio:
|
||||||
|
this.udp.send(data.slice(1), 5002, "localhost", function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.log("Error!!!");
|
||||||
|
} else {
|
||||||
|
console.log("Data sent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case MessageType.settings:
|
||||||
|
let message = JSON.parse(data.slice(1));
|
||||||
|
message.settings.forEach((setting, idx) => {
|
||||||
|
this.data.RadioInfo.radios[idx].freq = setting.frequency;
|
||||||
|
this.data.RadioInfo.radios[idx].modulation = setting.modulation;
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.ws.on("close", () => {
|
||||||
|
this.tcp.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* TCP */
|
||||||
|
this.tcp.connect(SRSPort, "localhost", () => {
|
||||||
|
console.log("Connected");
|
||||||
|
|
||||||
|
this.syncInterval = setInterval(() => {
|
||||||
|
let SYNC = {
|
||||||
|
Client: this.data,
|
||||||
|
MsgType: 2,
|
||||||
|
Version: SRS_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.tcp.readyState == "open")
|
||||||
|
this.tcp.write(`${JSON.stringify(SYNC)}\n`);
|
||||||
|
else clearInterval(this.syncInterval);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ module.exports = function (configLocation) {
|
|||||||
if (fs.existsSync(configLocation)) {
|
if (fs.existsSync(configLocation)) {
|
||||||
let rawdata = fs.readFileSync(configLocation, "utf-8");
|
let rawdata = fs.readFileSync(configLocation, "utf-8");
|
||||||
const config = JSON.parse(rawdata);
|
const config = JSON.parse(rawdata);
|
||||||
res.send(JSON.stringify(config.frontend));
|
res.send(JSON.stringify({...config.frontend, ...(config.audio ?? {}) }));
|
||||||
res.end()
|
res.end()
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
|
|||||||
219
frontend/server/srs.js
Normal file
219
frontend/server/srs.js
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
const WaveFile = require('wavefile').WaveFile;
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
let source = fs.readFileSync('sample3.WAV');
|
||||||
|
let wav = new WaveFile(source);
|
||||||
|
let wavBuffer = wav.toBuffer();
|
||||||
|
const { OpusEncoder } = require('@discordjs/opus');
|
||||||
|
const encoder = new OpusEncoder(16000, 1);
|
||||||
|
|
||||||
|
let fileIndex = 0;
|
||||||
|
let packetID = 0;
|
||||||
|
|
||||||
|
var udp = require("dgram");
|
||||||
|
var udpClient = udp.createSocket("udp4");
|
||||||
|
|
||||||
|
let clientData = {
|
||||||
|
ClientGuid: "AZi9CkptY0yW_C-3YmI7oQ",
|
||||||
|
Name: "Olympus",
|
||||||
|
Seat: 0,
|
||||||
|
Coalition: 0,
|
||||||
|
AllowRecord: false,
|
||||||
|
RadioInfo: {
|
||||||
|
radios: [
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enc: false,
|
||||||
|
encKey: 0,
|
||||||
|
freq: 1.0,
|
||||||
|
modulation: 3,
|
||||||
|
secFreq: 1.0,
|
||||||
|
retransmit: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
unit: "",
|
||||||
|
unitId: 0,
|
||||||
|
iff: {
|
||||||
|
control: 2,
|
||||||
|
mode1: -1,
|
||||||
|
mode2: -1,
|
||||||
|
mode3: -1,
|
||||||
|
mode4: false,
|
||||||
|
mic: -1,
|
||||||
|
status: 0,
|
||||||
|
},
|
||||||
|
ambient: { vol: 1.0, abType: "" },
|
||||||
|
},
|
||||||
|
LatLngPosition: { lat: 0.0, lng: 0.0, alt: 0.0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var net = require("net");
|
||||||
|
|
||||||
|
var tcpClient = new net.Socket();
|
||||||
|
|
||||||
|
tcpClient.on("data", function (data) {
|
||||||
|
console.log("Received: " + data);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
tcpClient.on("close", function () {
|
||||||
|
console.log("Connection closed");
|
||||||
|
});
|
||||||
|
|
||||||
|
tcpClient.connect(5002, "localhost", function () {
|
||||||
|
console.log("Connected");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
let SYNC = {
|
||||||
|
Client: clientData,
|
||||||
|
MsgType: 2,
|
||||||
|
Version: "2.1.0.10",
|
||||||
|
};
|
||||||
|
let string = JSON.stringify(SYNC);
|
||||||
|
tcpClient.write(string + "\n");
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
let slice = [];
|
||||||
|
for (let i = 0; i < 16000 * 0.04; i++) {
|
||||||
|
slice.push(wavBuffer[Math.round(fileIndex) * 2], wavBuffer[Math.round(fileIndex) * 2 + 1]);
|
||||||
|
fileIndex += 44100 / 16000;
|
||||||
|
}
|
||||||
|
const encoded = encoder.encode(new Uint8Array(slice));
|
||||||
|
|
||||||
|
let header = [
|
||||||
|
0, 0,
|
||||||
|
0, 0,
|
||||||
|
0, 0
|
||||||
|
]
|
||||||
|
|
||||||
|
let encFrequency = [...doubleToByteArray(251000000)];
|
||||||
|
let encModulation = [2];
|
||||||
|
let encEncryption = [0];
|
||||||
|
|
||||||
|
let encUnitID = getBytes(100000001, 4);
|
||||||
|
let encPacketID = getBytes(packetID, 8);
|
||||||
|
packetID++;
|
||||||
|
let encHops = [0];
|
||||||
|
|
||||||
|
let packet = [].concat(header, [...encoded], encFrequency, encModulation, encEncryption, encUnitID, encPacketID, encHops, [...Buffer.from(clientData.ClientGuid, 'utf-8')], [...Buffer.from(clientData.ClientGuid, 'utf-8')]);
|
||||||
|
|
||||||
|
let encPacketLen = getBytes(packet.length, 2);
|
||||||
|
packet[0] = encPacketLen[0];
|
||||||
|
packet[1] = encPacketLen[1];
|
||||||
|
|
||||||
|
let encAudioLen = getBytes(encoded.length, 2);
|
||||||
|
packet[2] = encAudioLen[0];
|
||||||
|
packet[3] = encAudioLen[1];
|
||||||
|
|
||||||
|
let frequencyAudioLen = getBytes(10, 2);
|
||||||
|
packet[4] = frequencyAudioLen[0];
|
||||||
|
packet[5] = frequencyAudioLen[1];
|
||||||
|
|
||||||
|
let data = new Uint8Array(packet);
|
||||||
|
udpClient.send(data, 5002, "localhost", function (error) {
|
||||||
|
if (error) {
|
||||||
|
tcpClient.close();
|
||||||
|
} else {
|
||||||
|
console.log("Data sent !!!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 40);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBytes(value, length) {
|
||||||
|
let res = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
res.push(value & 255);
|
||||||
|
value = value >> 8;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"backend": {
|
"backend": {
|
||||||
"address": "localhost",
|
"address": "localhost",
|
||||||
"port": 4512
|
"port": 3001
|
||||||
},
|
},
|
||||||
"authentication": {
|
"authentication": {
|
||||||
"gameMasterPassword": "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33",
|
"gameMasterPassword": "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33",
|
||||||
@@ -33,5 +33,9 @@
|
|||||||
"DCS Map (Official)": "https://maps.dcsolympus.com/maps",
|
"DCS Map (Official)": "https://maps.dcsolympus.com/maps",
|
||||||
"DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps"
|
"DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"SRSPort": 5002,
|
||||||
|
"WSPort": 4000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user