From 065cdf3648e690345cb403ad84bf177d2070192e Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Tue, 15 Oct 2024 17:40:39 +0200 Subject: [PATCH] Completed the audio panel --- frontend/react/src/audio/audiomanager.ts | 4 + frontend/react/src/audio/audiosink.ts | 5 +- frontend/react/src/audio/audiosource.ts | 8 +- frontend/react/src/audio/audiounitpipeline.ts | 65 ++- frontend/react/src/audio/unitsink.ts | 28 ++ frontend/react/src/eventscontext.tsx | 2 - frontend/react/src/other/utils.ts | 16 +- .../react/src/shortcut/shortcutmanager.ts | 6 - frontend/react/src/statecontext.tsx | 1 - .../src/ui/components/olfrequencyinput.tsx | 4 +- frontend/react/src/ui/panels/audiomenu.tsx | 400 ++++++++++++++---- .../src/ui/panels/components/radiopanel.tsx | 75 ---- .../ui/panels/components/radiosinkpanel.tsx | 106 +++++ .../src/ui/panels/components/sourcepanel.tsx | 196 ++++----- .../ui/panels/components/unitsinkpanel.tsx | 91 ++++ frontend/react/src/ui/panels/radiomenu.tsx | 116 ----- frontend/react/src/ui/panels/sidebar.tsx | 6 - .../react/src/ui/panels/unitcontrolmenu.tsx | 119 +++--- frontend/react/src/ui/ui.tsx | 10 - 19 files changed, 772 insertions(+), 486 deletions(-) delete mode 100644 frontend/react/src/ui/panels/components/radiopanel.tsx create mode 100644 frontend/react/src/ui/panels/components/radiosinkpanel.tsx create mode 100644 frontend/react/src/ui/panels/components/unitsinkpanel.tsx delete mode 100644 frontend/react/src/ui/panels/radiomenu.tsx diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 1cf7c94d..213d7dce 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -209,6 +209,10 @@ export class AudioManager { if (sink instanceof RadioSink) sink.setName(`Radio ${idx++}`); }); document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + this.#sources.forEach((source) => { + if (source.getConnectedTo().includes(sink)) + source.disconnect(sink) + }) } getGuid() { diff --git a/frontend/react/src/audio/audiosink.ts b/frontend/react/src/audio/audiosink.ts index af90064a..5aea5edb 100644 --- a/frontend/react/src/audio/audiosink.ts +++ b/frontend/react/src/audio/audiosink.ts @@ -1,7 +1,7 @@ import { getApp } from "../olympusapp"; /* Base audio sink class */ -export class AudioSink { +export abstract class AudioSink { #name: string; #gainNode: GainNode; @@ -25,4 +25,7 @@ export class AudioSink { getInputNode() { return this.#gainNode; } + + abstract setPtt(ptt: boolean): void; + abstract getPtt(): boolean; } diff --git a/frontend/react/src/audio/audiosource.ts b/frontend/react/src/audio/audiosource.ts index e852f07a..7072cbe8 100644 --- a/frontend/react/src/audio/audiosource.ts +++ b/frontend/react/src/audio/audiosource.ts @@ -18,9 +18,11 @@ export abstract class AudioSource { } connect(sink: AudioSink) { - this.getOutputNode().connect(sink.getInputNode()); - this.#connectedTo.push(sink); - document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + if (!this.#connectedTo.includes(sink)) { + this.getOutputNode().connect(sink.getInputNode()); + this.#connectedTo.push(sink); + document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); + } } disconnect(sinkToDisconnect?: AudioSink) { diff --git a/frontend/react/src/audio/audiounitpipeline.ts b/frontend/react/src/audio/audiounitpipeline.ts index 88a0b8ce..cf1ca98b 100644 --- a/frontend/react/src/audio/audiounitpipeline.ts +++ b/frontend/react/src/audio/audiounitpipeline.ts @@ -3,8 +3,6 @@ import { Unit } from "../unit/unit"; import { Filter, Noise } from "./audiolibrary"; import { AudioPacket } from "./audiopacket"; -const MAX_DISTANCE = 1852; // Ignore clients that are further away than 1NM, to save performance. - export class AudioUnitPipeline { #inputNode: GainNode; #sourceUnit: Unit; @@ -14,11 +12,15 @@ export class AudioUnitPipeline { #audioTrackProcessor: any; #encoder: AudioEncoder; + #wetGainNode: GainNode; + #delayNode: DelayNode; #convolverNode: ConvolverNode; - #tailOsc: Noise; + #tailOscillator: Noise; #distance: number = 0; #packetID = 0; + #ptt: boolean = false; + #maxDistance: number = 1852; constructor(sourceUnit: Unit, unitID: number, inputNode: GainNode) { this.#sourceUnit = sourceUnit; @@ -64,6 +66,27 @@ export class AudioUnitPipeline { /* Create the pipeline */ this.#inputNode = inputNode; this.#setupEffects(); + + /* Create the interval task to update the data */ + setInterval(() => { + /* Get the destination unit and compute the distance to it */ + let destinationUnit = getApp().getUnitsManager().getUnitByID(this.#unitID); + if (destinationUnit) { + let distance = destinationUnit?.getPosition().distanceTo(this.#sourceUnit.getPosition()); + + /* The units positions are updated at a low frequency. Filter the distance to avoid sudden volume jumps */ + this.#distance = 0.9 * this.#distance + 0.1 * distance; + + /* Don't bother updating parameters if the client is too far away */ + if (this.#distance < this.#maxDistance) { + /* Compute a new gain decreasing with distance. */ + let newGain = 1.0 - Math.pow(this.#distance / this.#maxDistance, 2); // Arbitrary + + /* Set the values of the main gain node and the multitap gain node, used for reverb effect */ + this.#gainNode.gain.setValueAtTime(newGain, getApp().getAudioManager().getAudioContext().currentTime); + } + } + }, 100); } handleEncodedData(encodedAudioChunk, unitID) { @@ -92,7 +115,7 @@ export class AudioUnitPipeline { handleRawData(audioData) { /* Ignore players that are too far away */ - if (this.#distance < MAX_DISTANCE) { + if (this.#distance < this.#maxDistance && this.#ptt) { this.#encoder.encode(audioData); audioData.close(); } @@ -102,18 +125,18 @@ export class AudioUnitPipeline { /* Create the nodes necessary for the pipeline */ this.#convolverNode = getApp().getAudioManager().getAudioContext().createConvolver(); - let wetGainNode = getApp().getAudioManager().getAudioContext().createGain(); - wetGainNode.gain.setValueAtTime(2.0, getApp().getAudioManager().getAudioContext().currentTime) - let delayNode = getApp().getAudioManager().getAudioContext().createDelay(1); - delayNode.delayTime.setValueAtTime(0.09, getApp().getAudioManager().getAudioContext().currentTime) + this.#wetGainNode = getApp().getAudioManager().getAudioContext().createGain(); + this.#wetGainNode.gain.setValueAtTime(2.0, getApp().getAudioManager().getAudioContext().currentTime) + this.#delayNode = getApp().getAudioManager().getAudioContext().createDelay(1); + this.#delayNode.delayTime.setValueAtTime(0.09, getApp().getAudioManager().getAudioContext().currentTime) this.#inputNode.connect(this.#gainNode); this.#gainNode.connect(this.#destinationNode); - this.#gainNode.connect(wetGainNode); - wetGainNode.connect(delayNode); - delayNode.connect(this.#convolverNode); + this.#gainNode.connect(this.#wetGainNode); + this.#wetGainNode.connect(this.#delayNode); + this.#delayNode.connect(this.#convolverNode); this.#convolverNode.connect(this.#destinationNode); /* Render the random noise needed for the convolver node to simulate reverb */ @@ -132,17 +155,17 @@ export class AudioUnitPipeline { ); /* A noise oscillator and a two filters are added to smooth the reverb */ - this.#tailOsc = new Noise(tailContext, 1); + this.#tailOscillator = new Noise(tailContext, 1); const tailLPFilter = new Filter(tailContext, "lowpass", 5000, 1); const tailHPFilter = new Filter(tailContext, "highpass", 500, 1); /* Initialize and connect the oscillator with the filters */ - this.#tailOsc.init(); - this.#tailOsc.connect(tailHPFilter.input); + this.#tailOscillator.init(); + this.#tailOscillator.connect(tailHPFilter.input); tailHPFilter.connect(tailLPFilter.input); tailLPFilter.connect(tailContext.destination); - this.#tailOsc.attack = attack; - this.#tailOsc.decay = decay; + this.#tailOscillator.attack = attack; + this.#tailOscillator.decay = decay; setTimeout(() => { /* Set the buffer of the convolver node */ @@ -150,8 +173,16 @@ export class AudioUnitPipeline { this.#convolverNode.buffer = buffer; }); - this.#tailOsc.on({ frequency: 500, velocity: 127 }); + this.#tailOscillator.on({ frequency: 500, velocity: 127 }); //tailOsc.off(); // TODO In the original example I copied, this was turned off. No idea why but it seems to work correctly if left on. To investigate. }, 20); } + + setPtt(ptt) { + this.#ptt = ptt; + } + + setMaxDistance(maxDistance) { + this.#maxDistance = maxDistance; + } } diff --git a/frontend/react/src/audio/unitsink.ts b/frontend/react/src/audio/unitsink.ts index 7539577e..3c4f120e 100644 --- a/frontend/react/src/audio/unitsink.ts +++ b/frontend/react/src/audio/unitsink.ts @@ -8,6 +8,8 @@ scramble calls and so on. Ideally, one may want to move this code to the backend export class UnitSink extends AudioSink { #unit: Unit; #unitPipelines: { [key: string]: AudioUnitPipeline } = {}; + #ptt: boolean = false; + #maxDistance: number = 1852; constructor(unit: Unit) { super(); @@ -33,6 +35,8 @@ export class UnitSink extends AudioSink { .forEach((unitID) => { if (unitID !== 0 && !(unitID in this.#unitPipelines)) { this.#unitPipelines[unitID] = new AudioUnitPipeline(this.#unit, unitID, this.getInputNode()); + this.#unitPipelines[unitID].setPtt(false); + this.#unitPipelines[unitID].setMaxDistance(this.#maxDistance); } }); @@ -42,4 +46,28 @@ export class UnitSink extends AudioSink { } }); } + + setPtt(ptt) { + this.#ptt = ptt; + Object.values(this.#unitPipelines).forEach((pipeline) => { + pipeline.setPtt(ptt); + }) + document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + } + + getPtt() { + return this.#ptt; + } + + setMaxDistance(maxDistance) { + this.#maxDistance = maxDistance; + Object.values(this.#unitPipelines).forEach((pipeline) => { + pipeline.setMaxDistance(maxDistance); + }) + document.dispatchEvent(new CustomEvent("audioSinksUpdated")); + } + + getMaxDistance() { + return this.#maxDistance; + } } diff --git a/frontend/react/src/eventscontext.tsx b/frontend/react/src/eventscontext.tsx index d57951db..b6864f78 100644 --- a/frontend/react/src/eventscontext.tsx +++ b/frontend/react/src/eventscontext.tsx @@ -8,7 +8,6 @@ export const EventsContext = createContext({ setDrawingMenuVisible: (e: boolean) => {}, setOptionsMenuVisible: (e: boolean) => {}, setAirbaseMenuVisible: (e: boolean) => {}, - setRadioMenuVisible: (e: boolean) => {}, setAudioMenuVisible: (e: boolean) => {}, toggleMainMenuVisible: () => {}, toggleSpawnMenuVisible: () => {}, @@ -17,7 +16,6 @@ export const EventsContext = createContext({ toggleDrawingMenuVisible: () => {}, toggleOptionsMenuVisible: () => {}, toggleAirbaseMenuVisible: () => {}, - toggleRadioMenuVisible: () => {}, toggleAudioMenuVisible: () => {}, }); diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index b60ce768..87949c54 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -11,6 +11,7 @@ import { Converter } from "usng"; import { MGRS } from "../types/types"; import { getApp } from "../olympusapp"; import { featureCollection } from "turf"; +import { randomUUID } from "crypto"; export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { const φ1 = deg2rad(lat1); // φ, λ in radians @@ -110,8 +111,8 @@ export function reciprocalHeading(heading: number): number { * @param decimal whether this is a decimal number or not * * */ -export const zeroAppend = function (num: number, places: number, decimal: boolean = false) { - var string = decimal ? num.toFixed(2) : String(num); +export const zeroAppend = function (num: number, places: number, decimal: boolean = false, decimalPlaces: number = 2) { + var string = decimal ? num.toFixed(decimalPlaces) : String(num); while (string.length < places) { string = "0" + string; } @@ -576,3 +577,14 @@ export function doubleToByteArray(number) { export function byteArrayToDouble(array) { return new DataView(array.reverse().buffer).getFloat64(0); } + +export function rand(min, max) { + return min + Math.random() * (max - min); +} + +export function getRandomColor(seed) { + var h = (seed * Math.PI * 100) % 360 + 1; + var s = 50; + var l = 50; + return 'hsl(' + h + ',' + s + '%,' + l + '%)'; +} \ No newline at end of file diff --git a/frontend/react/src/shortcut/shortcutmanager.ts b/frontend/react/src/shortcut/shortcutmanager.ts index 6f790e03..b42746ec 100644 --- a/frontend/react/src/shortcut/shortcutmanager.ts +++ b/frontend/react/src/shortcut/shortcutmanager.ts @@ -128,9 +128,6 @@ export class ShortcutManager { getApp() .getAudioManager() .getSinks() - .filter((sink) => { - return sink instanceof RadioSink; - }) [idx]?.setPtt(true); }, code: key, @@ -144,9 +141,6 @@ export class ShortcutManager { getApp() .getAudioManager() .getSinks() - .filter((sink) => { - return sink instanceof RadioSink; - }) [idx]?.setPtt(false); }, code: key, diff --git a/frontend/react/src/statecontext.tsx b/frontend/react/src/statecontext.tsx index 9ba4b8ca..8e0dc182 100644 --- a/frontend/react/src/statecontext.tsx +++ b/frontend/react/src/statecontext.tsx @@ -9,7 +9,6 @@ export const StateContext = createContext({ drawingMenuVisible: false, optionsMenuVisible: false, airbaseMenuVisible: false, - radioMenuVisible: false, audioMenuVisible: false, mapHiddenTypes: MAP_HIDDEN_TYPES_DEFAULTS, mapOptions: MAP_OPTIONS_DEFAULTS, diff --git a/frontend/react/src/ui/components/olfrequencyinput.tsx b/frontend/react/src/ui/components/olfrequencyinput.tsx index eaf4e9a3..138ce87c 100644 --- a/frontend/react/src/ui/components/olfrequencyinput.tsx +++ b/frontend/react/src/ui/components/olfrequencyinput.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { ChangeEvent } from "react"; import { OlNumberInput } from "./olnumberinput"; export function OlFrequencyInput(props: { value: number; className?: string; onChange: (value: number) => void }) { @@ -28,6 +27,7 @@ export function OlFrequencyInput(props: { value: number; className?: string; onC props.onChange(frequency); }} value={Math.floor(frequency / 1000000)} + className="!min-w-28" >
.
-
MHz
); } diff --git a/frontend/react/src/ui/panels/audiomenu.tsx b/frontend/react/src/ui/panels/audiomenu.tsx index e5a6ca23..5b716e01 100644 --- a/frontend/react/src/ui/panels/audiomenu.tsx +++ b/frontend/react/src/ui/panels/audiomenu.tsx @@ -1,19 +1,51 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Menu } from "./components/menu"; import { getApp } from "../../olympusapp"; -import { FaQuestionCircle } from "react-icons/fa"; +import { FaPlus, FaPlusCircle, FaQuestionCircle } from "react-icons/fa"; import { AudioSourcePanel } from "./components/sourcepanel"; import { AudioSource } from "../../audio/audiosource"; -import { FaVolumeHigh, FaX } from "react-icons/fa6"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faClose } from "@fortawesome/free-solid-svg-icons"; +import { RadioSinkPanel } from "./components/radiosinkpanel"; +import { AudioSink } from "../../audio/audiosink"; +import { RadioSink } from "../../audio/radiosink"; +import { UnitSinkPanel } from "./components/unitsinkpanel"; +import { UnitSink } from "../../audio/unitsink"; +import { FaMinus, FaVolumeHigh } from "react-icons/fa6"; +import { getRandomColor } from "../../other/utils"; + +let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"]; export function AudioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { + const [sinks, setSinks] = useState([] as AudioSink[]); const [sources, setSources] = useState([] as AudioSource[]); const [audioManagerEnabled, setAudioManagerEnabled] = useState(false); - const [showTip, setShowTip] = useState(true); + const [activeSource, setActiveSource] = useState(null as AudioSource | null); + const [count, setCount] = useState(0); + + /* Preallocate 128 references for the source and sink panels. If the number of references changes, React will give an error */ + const sourceRefs = Array(128) + .fill(null) + .map(() => { + return useRef(null); + }); + + const sinkRefs = Array(128) + .fill(null) + .map(() => { + return useRef(null); + }); useEffect(() => { + /* Force a rerender */ + document.addEventListener("audioSinksUpdated", () => { + setSinks( + getApp() + ?.getAudioManager() + .getSinks() + .filter((sink) => sink instanceof AudioSink) + .map((radio) => radio) + ); + }); + /* Force a rerender */ document.addEventListener("audioSourcesUpdated", () => { setSources( @@ -29,97 +61,287 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children? }); }, []); + /* When the sinks or sources change, use the count state to force a rerender to update the connection lines */ + useEffect(() => { + setCount(count + 1); + }, [sinks, sources]); + + /* List all the connections between the sinks and the sources */ + const connections = [] as any[]; + const lineCounters = [] as number[]; + const lineColors = [] as string[]; + let counter = 0; + sources.forEach((source, idx) => { + counter++; + const color = getRandomColor(counter); + source.getConnectedTo().forEach((sink) => { + if (sinks.indexOf(sink as AudioSink) !== undefined) { + connections.push([sourceRefs[idx], sinkRefs[sinks.indexOf(sink as AudioSink)]]); + lineCounters.push(counter); + lineColors.push(color); + } + }); + }); + + /* Compute the line distance to fit in the available space */ + const defaultLineDistance = 8; + const paddingRight = Math.min(lineCounters[lineCounters.length - 1] * defaultLineDistance + 40, 96); + const lineDistance = (paddingRight - 40) / lineCounters[lineCounters.length - 1]; + return ( - -
The audio source panel allows you to add and manage audio sources.
+ +
+ The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies. +
+ <> - {showTip && ( + {!audioManagerEnabled && (
- {audioManagerEnabled ? ( - <> -
- -
-
-
Use the controls to apply effects and start/stop the playback of an audio source.
-
Sources can be connected to your radios, or attached to a unit to be played on loudspeakers.
-
-
- setShowTip(false)} - icon={faClose} - className={` - ml-2 flex cursor-pointer items-center justify-center - rounded-md p-2 text-lg - dark:text-gray-500 dark:hover:bg-gray-700 - dark:hover:text-white - hover:bg-gray-200 - `} - /> -
- - ) : ( - <> -
- -
-
-
- To enable the audio menu, first start the audio backend with the{" "} - - - {" "} - button on the navigation header. -
-
- - )} +
+ +
+
+
+ To enable the audio menu, first start the audio backend with the{" "} + + + {" "} + button on the navigation header. +
+
)} - -
- <> - {sources.map((source, idx) => { - return ; +
+
+ {audioManagerEnabled && Audio sources} + <> + {sources.map((source, idx) => { + return ( + { + setCount(count + 1); + }} + /> + ); + })} + + {audioManagerEnabled && ( + + )} +
+
+ {audioManagerEnabled && Radios} + {sinks.map((sink, idx) => { + if (sink instanceof RadioSink) + return ( + { + setCount(count + 1); + }} + ref={sinkRefs[idx]} + > + ); })} - - {audioManagerEnabled && ( - + )} +
+
+ {audioManagerEnabled && sinks.find((sink) => sink instanceof UnitSink) && Unit loudspeakers} + {sinks.map((sink, idx) => { + if (sink instanceof UnitSink) + return ( + { + setCount(count + 1); + }} + > + ); + })} +
+
+ {connections + .filter((connection) => connection && connection[0].current && connection[1].current) + .map((connection, idx) => { + const start = connection[0].current; + const end = connection[1].current; + if (start && end) { + const startRect = { + top: (start as HTMLDivElement).offsetTop, + bottom: (start as HTMLDivElement).offsetTop + (start as HTMLDivElement).clientHeight, + height: (start as HTMLDivElement).clientHeight, + right: (start as HTMLDivElement).offsetLeft + (start as HTMLDivElement).clientWidth, + }; + + const endRect = { + top: (end as HTMLDivElement).offsetTop, + bottom: (end as HTMLDivElement).offsetTop + (end as HTMLDivElement).clientHeight, + height: (end as HTMLDivElement).clientHeight, + right: (end as HTMLDivElement).offsetLeft + (end as HTMLDivElement).clientWidth, + }; + return ( + <> +
+ + ); + } + }) + .reverse()} +
+
+ {sourceRefs.map((sourceRef, idx) => { + const div = sourceRef.current; + if (div) { + const divRect = { + top: (div as HTMLDivElement).offsetTop, + bottom: (div as HTMLDivElement).offsetTop + (div as HTMLDivElement).clientHeight, + height: (div as HTMLDivElement).clientHeight, + right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth, }; - }} - > - Add audio source - - )} + return ( + <> +
+
{ + activeSource !== sources[idx] ? setActiveSource(sources[idx]) : setActiveSource(null); + }} + > + +
+
+ + ); + } + })} + {activeSource && + sinkRefs.map((sinkRef, idx) => { + const div = sinkRef.current; + if (div) { + const divRect = { + top: (div as HTMLDivElement).offsetTop, + bottom: (div as HTMLDivElement).offsetTop + (div as HTMLDivElement).clientHeight, + height: (div as HTMLDivElement).clientHeight, + right: (div as HTMLDivElement).offsetLeft + (div as HTMLDivElement).clientWidth, + }; + return ( + <> +
+
{ + if (activeSource.getConnectedTo().includes(sinks[idx])) activeSource.disconnect(sinks[idx]); + else activeSource.connect(sinks[idx]); + }} + > + {" "} + {activeSource.getConnectedTo().includes(sinks[idx]) ? : } +
+
+ + ); + } + })} +
); diff --git a/frontend/react/src/ui/panels/components/radiopanel.tsx b/frontend/react/src/ui/panels/components/radiopanel.tsx deleted file mode 100644 index f267c5af..00000000 --- a/frontend/react/src/ui/panels/components/radiopanel.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { OlFrequencyInput } from "../../components/olfrequencyinput"; -import { FaTrash } from "react-icons/fa6"; -import { OlLabelToggle } from "../../components/ollabeltoggle"; -import { OlStateButton } from "../../components/olstatebutton"; -import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons"; -import { RadioSink } from "../../../audio/radiosink"; -import { getApp } from "../../../olympusapp"; - -export function RadioPanel(props: { radio: RadioSink; shortcutKey: string }) { - return ( -
-
- {props.radio.getName()} -
{ - getApp().getAudioManager().removeSink(props.radio); - }} - > - -
-
- { - props.radio.setFrequency(value); - }} - /> -
- { - props.radio.setModulation(props.radio.getModulation() === 1 ? 0 : 1); - }} - > - - - {props.shortcutKey} - - - { - props.radio.setPtt(!props.radio.getPtt()); - }} - tooltip="Talk on frequency" - > - - { - props.radio.setTuned(!props.radio.getTuned()); - }} - tooltip="Tune to radio" - > -
-
- ); -} diff --git a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx new file mode 100644 index 00000000..7a332c9e --- /dev/null +++ b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx @@ -0,0 +1,106 @@ +import React, { ForwardedRef, forwardRef, useEffect, useState } from "react"; +import { OlFrequencyInput } from "../../components/olfrequencyinput"; +import { FaChevronUp, FaXmark } from "react-icons/fa6"; +import { OlLabelToggle } from "../../components/ollabeltoggle"; +import { OlStateButton } from "../../components/olstatebutton"; +import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons"; +import { RadioSink } from "../../../audio/radiosink"; +import { getApp } from "../../../olympusapp"; + +export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef) => { + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + if (props.onExpanded) props.onExpanded(); + }, [expanded]) + + return ( +
+
+
{ + setExpanded(!expanded); + }} + > + +
+ {props.shortcutKey && (<> + + {props.shortcutKey} + + + )} + {props.radio.getName()} {!expanded && `: ${props.radio.getFrequency()/1e6} MHz ${props.radio.getModulation()? "FM": "AM"}`} {} +
{ + getApp().getAudioManager().removeSink(props.radio); + }} + > + +
+
+ {expanded && ( + <> + { + props.radio.setFrequency(value); + }} + /> +
+ { + props.radio.setModulation(props.radio.getModulation() === 1 ? 0 : 1); + }} + > + + { + props.radio.setPtt(!props.radio.getPtt()); + }} + tooltip="Talk on frequency" + > + + { + props.radio.setTuned(!props.radio.getTuned()); + }} + tooltip="Tune to radio" + > +
+ + )} +
+ ); +}); diff --git a/frontend/react/src/ui/panels/components/sourcepanel.tsx b/frontend/react/src/ui/panels/components/sourcepanel.tsx index 64507fcc..6d9a98d0 100644 --- a/frontend/react/src/ui/panels/components/sourcepanel.tsx +++ b/frontend/react/src/ui/panels/components/sourcepanel.tsx @@ -1,145 +1,131 @@ -import React, { useEffect, useState } from "react"; +import React, { ForwardedRef, forwardRef, 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 { FaChevronUp, FaVolumeHigh, FaXmark } 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 }) { +export const AudioSourcePanel = forwardRef((props: { source: AudioSource; onExpanded: () => void }, ref: ForwardedRef) => { const [meterLevel, setMeterLevel] = useState(0); + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + if (props.onExpanded) props.onExpanded(); + }, [expanded]) useEffect(() => { setInterval(() => { - setMeterLevel(Math.min(100, props.source.getMeter().getPeaks().current[0])); + setMeterLevel(Math.min(1, props.source.getMeter().getPeaks().current[0])); }, 50); }, []); - let availabileSinks = getApp() - .getAudioManager() - .getSinks() - .filter((sink) => !props.source.getConnectedTo().includes(sink)); - return (
- {props.source.getName()} +
{ + setExpanded(!expanded); + }} + > + +
+
+ + {props.source.getName()} + +
{!(props.source instanceof MicrophoneSource) && (
{ getApp().getAudioManager().removeSource(props.source); }} > - +
)}
- -
- {props.source instanceof FileSource && ( -
- { - if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.pause() : props.source.play(); - }} - tooltip="Play file" - > - 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" - /> - { - if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping()); - }} - tooltip="Loop" - > -
- )} -
-
- -
-
-
-
+ {expanded && ( + <> + {props.source instanceof FileSource && ( +
+
+ { + if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.pause() : props.source.play(); + }} + tooltip="Play file" + > + 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" + /> + { + if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping()); + }} + tooltip="Loop" + > +
- { - props.source.setVolume(parseFloat(ev.currentTarget.value) / 100); - }} - className="absolute top-[18px]" - /> -
-
- {Math.round(props.source.getVolume() * 100)}% -
-
-
+ )} - Connected to: -
- {props.source.getConnectedTo().map((sink, idx) => { - return ( -
- - {sink.getName()} - props.source.disconnect(sink)}> +
+
+
- ); - })} -
- {availabileSinks.length > 0 && ( - - {availabileSinks.map((sink, idx) => { - return ( - { - props.source.connect(sink); - }} +
+
- {sink.getName()} - - ); - })} - +
+
+ { + props.source.setVolume(parseFloat(ev.currentTarget.value) / 100); + }} + className="absolute top-[18px]" + /> +
+
+ )}
); -} +}); diff --git a/frontend/react/src/ui/panels/components/unitsinkpanel.tsx b/frontend/react/src/ui/panels/components/unitsinkpanel.tsx new file mode 100644 index 00000000..a82889aa --- /dev/null +++ b/frontend/react/src/ui/panels/components/unitsinkpanel.tsx @@ -0,0 +1,91 @@ +import React, { ForwardedRef, forwardRef, useEffect, useState } from "react"; +import { FaChevronUp, FaXmark } from "react-icons/fa6"; +import { getApp } from "../../../olympusapp"; +import { UnitSink } from "../../../audio/unitsink"; +import { OlStateButton } from "../../components/olstatebutton"; +import { faMicrophoneLines } from "@fortawesome/free-solid-svg-icons"; +import { OlRangeSlider } from "../../components/olrangeslider"; + +export const UnitSinkPanel = forwardRef((props: { sink: UnitSink; shortcutKey: string; onExpanded: () => void }, ref: ForwardedRef) => { + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + if (props.onExpanded) props.onExpanded(); + }, [expanded]); + + return ( +
+
+
{ + setExpanded(!expanded); + }} + > + +
+ {props.shortcutKey && (<> + + {props.shortcutKey} + + + )} +
+ {props.sink.getName()} +
+
{ + getApp().getAudioManager().removeSink(props.sink); + }} + > + +
+
+ {expanded && ( +
+ Near + { + props.sink.setMaxDistance((parseFloat(ev.currentTarget.value) / 100) * (1852 - 100) + 100); + }} + className="my-auto h-16" + /> + Far + { + props.sink.setPtt(!props.sink.getPtt()); + }} + tooltip="Talk on frequency" + > +
+ )} +
+ ); +}); diff --git a/frontend/react/src/ui/panels/radiomenu.tsx b/frontend/react/src/ui/panels/radiomenu.tsx deleted file mode 100644 index 9bd0d856..00000000 --- a/frontend/react/src/ui/panels/radiomenu.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Menu } from "./components/menu"; -import { getApp } from "../../olympusapp"; -import { RadioPanel } from "./components/radiopanel"; -import { FaQuestionCircle } from "react-icons/fa"; -import { RadioSink } from "../../audio/radiosink"; -import { FaVolumeHigh } from "react-icons/fa6"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faClose } from "@fortawesome/free-solid-svg-icons"; - -let shortcutKeys = ["Z", "X", "C", "V", "B", "N", "M", "K", "L"]; - -export function RadioMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { - const [radios, setRadios] = useState([] as RadioSink[]); - const [audioManagerEnabled, setAudioManagerEnabled] = useState(false); - const [showTip, setShowTip] = useState(true); - - useEffect(() => { - /* Force a rerender */ - document.addEventListener("audioSinksUpdated", () => { - setRadios( - getApp() - ?.getAudioManager() - .getSinks() - .filter((sink) => sink instanceof RadioSink) - .map((radio) => radio) - ); - }); - - document.addEventListener("audioManagerStateChanged", () => { - setAudioManagerEnabled(getApp().getAudioManager().isRunning()); - }); - }, []); - - return ( - -
The radio menu allows you to talk on radio to the players online using SRS.
- <> - {showTip && ( -
- {audioManagerEnabled ? ( - <> -
- -
-
-
Use the radio controls to tune to a frequency, then click on the PTT button to talk.
-
You can add up to 10 radios. Use the audio effects menu to play audio tracks or to add background noises.
-
-
- setShowTip(false)} - icon={faClose} - className={` - ml-2 flex cursor-pointer items-center justify-center - rounded-md p-2 text-lg - dark:text-gray-500 dark:hover:bg-gray-700 - dark:hover:text-white - hover:bg-gray-200 - `} - /> -
- - ) : ( - <> -
- -
-
-
- To enable the radio menu, first start the audio backend with the{" "} - - - {" "} - button on the navigation header. -
-
- - )} -
- )} - - -
- {radios.map((radio, idx) => { - return ; - })} - {audioManagerEnabled && radios.length < 10 && ( - - )} -
-
- ); -} diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index c7512d3d..8239b759 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -52,12 +52,6 @@ export function SideBar() { icon={faPencil} tooltip="Hide/show drawing menu" > - void }) { const [filterString, setFilterString] = useState(""); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | { radio: Radio; TACAN: TACAN }); + const [audioManagerEnabled, setAudioManagerEnabled] = useState(false); var searchBarRef = useRef(null); @@ -130,6 +131,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { document.addEventListener("clearSelection", () => { setSelectedUnits([]); }); + + document.addEventListener("audioManagerStateChanged", () => { + setAudioManagerEnabled(getApp().getAudioManager().isRunning()); + }); }, []); /* Update the current values of the shown data */ @@ -545,7 +550,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -608,7 +613,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -664,7 +669,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -702,7 +707,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -734,7 +739,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -773,7 +778,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -807,7 +812,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -871,7 +876,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -896,7 +901,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -922,7 +927,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -954,7 +959,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -987,7 +992,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -1012,7 +1017,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -1036,7 +1041,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
@@ -1062,40 +1067,55 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
- Enable loudspeakers + Loudspeakers - { - selectedUnits.forEach((unit) => { - if (!selectedUnitsData.isAudioSink) { - getApp()?.getAudioManager().addUnitSink(unit); - setSelectedUnitsData({ - ...selectedUnitsData, - isAudioSink: true, - }); - } else { - let sink = getApp() - ?.getAudioManager() - .getSinks() - .find((sink) => { - return sink instanceof UnitSink && sink.getUnit() === unit; + {audioManagerEnabled ? ( + { + selectedUnits.forEach((unit) => { + if (!selectedUnitsData.isAudioSink) { + getApp()?.getAudioManager().addUnitSink(unit); + setSelectedUnitsData({ + ...selectedUnitsData, + isAudioSink: true, }); - if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink); + } else { + let sink = getApp() + ?.getAudioManager() + .getSinks() + .find((sink) => { + return sink instanceof UnitSink && sink.getUnit() === unit; + }); + if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink); - setSelectedUnitsData({ - ...selectedUnitsData, - isAudioSink: false, - }); - } - }); - }} - /> + setSelectedUnitsData({ + ...selectedUnitsData, + isAudioSink: false, + }); + } + }); + }} + /> + ) : ( +
+ Enable audio with{" "} + + + {" "}first +
+ )}
+ {/* ============== Audio sink toggle END ============== */}
)} @@ -1196,10 +1216,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1} > - + { @@ -1318,11 +1337,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { className={` flex content-center gap-2 rounded-full ${selectedUnits[0].getFuel() > 40 && `bg-green-700`} - ${ - selectedUnits[0].getFuel() > 10 && - selectedUnits[0].getFuel() <= 40 && - `bg-yellow-700` - } + ${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && ` + bg-yellow-700 + `} ${selectedUnits[0].getFuel() <= 10 && `bg-red-700`} px-2 py-1 text-sm font-bold text-white `} diff --git a/frontend/react/src/ui/ui.tsx b/frontend/react/src/ui/ui.tsx index bca7f513..51900483 100644 --- a/frontend/react/src/ui/ui.tsx +++ b/frontend/react/src/ui/ui.tsx @@ -22,7 +22,6 @@ import { ControlsPanel } from "./panels/controlspanel"; import { MapContextMenu } from "./contextmenus/mapcontextmenu"; import { AirbaseMenu } from "./panels/airbasemenu"; import { Airbase } from "../mission/airbase"; -import { RadioMenu } from "./panels/radiomenu"; import { AudioMenu } from "./panels/audiomenu"; import { FormationMenu } from "./panels/formationmenu"; import { Unit } from "../unit/unit"; @@ -48,7 +47,6 @@ export function UI() { const [unitControlMenuVisible, setUnitControlMenuVisible] = useState(false); const [measureMenuVisible, setMeasureMenuVisible] = useState(false); const [drawingMenuVisible, setDrawingMenuVisible] = useState(false); - const [radioMenuVisible, setRadioMenuVisible] = useState(false); const [audioMenuVisible, setAudioMenuVisible] = useState(false); const [optionsMenuVisible, setOptionsMenuVisible] = useState(false); const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false); @@ -134,7 +132,6 @@ export function UI() { setDrawingMenuVisible(false); setOptionsMenuVisible(false); setAirbaseMenuVisible(false); - setRadioMenuVisible(false); setAudioMenuVisible(false); setFormationMenuVisible(false); setUnitExplosionMenuVisible(false); @@ -186,7 +183,6 @@ export function UI() { drawingMenuVisible: drawingMenuVisible, optionsMenuVisible: optionsMenuVisible, airbaseMenuVisible: airbaseMenuVisible, - radioMenuVisible: radioMenuVisible, audioMenuVisible: audioMenuVisible, mapOptions: mapOptions, mapHiddenTypes: mapHiddenTypes, @@ -204,7 +200,6 @@ export function UI() { setMeasureMenuVisible: setMeasureMenuVisible, setOptionsMenuVisible: setOptionsMenuVisible, setAirbaseMenuVisible: setAirbaseMenuVisible, - setRadioMenuVisible: setRadioMenuVisible, setAudioMenuVisible: setAudioMenuVisible, toggleMainMenuVisible: () => { hideAllMenus(); @@ -234,10 +229,6 @@ export function UI() { hideAllMenus(); setAirbaseMenuVisible(!airbaseMenuVisible); }, - toggleRadioMenuVisible: () => { - hideAllMenus(); - setRadioMenuVisible(!radioMenuVisible); - }, toggleAudioMenuVisible: () => { hideAllMenus(); setAudioMenuVisible(!audioMenuVisible); @@ -295,7 +286,6 @@ export function UI() { setUnitControlMenuVisible(false)} /> setDrawingMenuVisible(false)} /> setAirbaseMenuVisible(false)} airbase={airbase} /> - setRadioMenuVisible(false)} /> setAudioMenuVisible(false)} /> setFormationMenuVisible(false)} /> setUnitExplosionMenuVisible(false)} />