diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 5c967d07..d52d4b1b 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -6,7 +6,6 @@ import { makeID } from "../other/utils"; import { FileSource } from "./filesource"; import { AudioSource } from "./audiosource"; import { Buffer } from "buffer"; -import { PlaybackPipeline } from "./playbackpipeline"; import { AudioSink } from "./audiosink"; import { Unit } from "../unit/unit"; import { UnitSink } from "./unitsink"; @@ -30,9 +29,6 @@ export class AudioManager { #input: MediaDeviceInfo; #output: MediaDeviceInfo; - /* The playback pipeline enables audio playback on the speakers/headphones */ - #playbackPipeline: PlaybackPipeline; - /* The audio sinks used to transmit the audio stream to the SRS backend */ #sinks: AudioSink[] = []; @@ -82,8 +78,6 @@ export class AudioManager { //@ts-ignore if (this.#output) this.#audioContext.setSinkId(this.#output.deviceId); - this.#playbackPipeline = new PlaybackPipeline(); - /* Connect the audio websocket */ let res = location.toString().match(/(?:http|https):\/\/(.+):/); if (res === null) res = location.toString().match(/(?:http|https):\/\/(.+)/); @@ -129,7 +123,7 @@ export class AudioManager { var dst = new ArrayBuffer(audioPacket.getAudioData().buffer.byteLength); new Uint8Array(dst).set(new Uint8Array(audioPacket.getAudioData().buffer)); sink.recordArrayBuffer(audioPacket.getAudioData().buffer); - this.#playbackPipeline.playBuffer(dst); + sink.playBuffer(dst); } }); } else { @@ -156,16 +150,19 @@ export class AudioManager { let newRadio = this.addRadio(); newRadio?.setFrequency(options.frequency); newRadio?.setModulation(options.modulation); + newRadio?.setPan(options.pan) }); } else { /* Add two default radios and connect to the microphone*/ let newRadio = this.addRadio(); this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio); this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio); + newRadio.setPan(-1); newRadio = this.addRadio(); this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio); this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio); + newRadio.setPan(1); } let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources; diff --git a/frontend/react/src/audio/playbackpipeline.ts b/frontend/react/src/audio/playbackpipeline.ts index 2d0895ea..325cc10a 100644 --- a/frontend/react/src/audio/playbackpipeline.ts +++ b/frontend/react/src/audio/playbackpipeline.ts @@ -1,4 +1,5 @@ import { getApp } from "../olympusapp"; +import { Filter, Noise } from "./audiolibrary"; export class PlaybackPipeline { #decoder = new AudioDecoder({ @@ -8,10 +9,12 @@ export class PlaybackPipeline { #trackGenerator: any; // TODO can we have typings? #writer: any; #gainNode: GainNode; + #pannerNode: StereoPannerNode; + #enabled: boolean = false; constructor() { this.#decoder.configure({ - codec: 'opus', + codec: "opus", numberOfChannels: 1, sampleRate: 16000, //@ts-ignore // TODO why is this giving an error? @@ -30,8 +33,21 @@ export class PlaybackPipeline { /* Connect to the device audio output */ this.#gainNode = getApp().getAudioManager().getAudioContext().createGain(); + this.#pannerNode = getApp().getAudioManager().getAudioContext().createStereoPanner(); + let splitter = getApp().getAudioManager().getAudioContext().createChannelSplitter(); + let bandpass = new Filter(getApp().getAudioManager().getAudioContext(), "banpass", 600, 10); + bandpass.setup(); + mediaStreamSource.connect(this.#gainNode); - this.#gainNode.connect(getApp().getAudioManager().getAudioContext().destination); + this.#gainNode.connect(bandpass.input); + bandpass.output.connect(splitter); + splitter.connect(this.#pannerNode); + + this.#pannerNode.pan.setValueAtTime(0, getApp().getAudioManager().getAudioContext().currentTime); + + let noise = new Noise(getApp().getAudioManager().getAudioContext(), 0.01); + noise.init(); + noise.connect(this.#gainNode); } playBuffer(arrayBuffer) { @@ -48,9 +64,23 @@ export class PlaybackPipeline { this.#decoder.decode(encodedAudioChunk); } + setEnabled(enabled) { + if (enabled && !this.#enabled) { + this.#enabled = true; + this.#pannerNode.connect(getApp().getAudioManager().getAudioContext().destination); + } else if (!enabled && this.#enabled) { + this.#enabled = false; + this.#pannerNode.disconnect(getApp().getAudioManager().getAudioContext().destination); + } + } + + setPan(pan) { + this.#pannerNode.pan.setValueAtTime(pan, getApp().getAudioManager().getAudioContext().currentTime); + } + #handleDecodedData(audioData) { this.#writer.ready.then(() => { - this.#writer.write(audioData); - }) + this.#writer.write(audioData); + }); } } diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index 98777266..8cfabef1 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -5,6 +5,7 @@ import { AudioSinksChangedEvent } from "../events"; import { makeID } from "../other/utils"; import { Recorder } from "./recorder"; import { Unit } from "../unit/unit"; +import { PlaybackPipeline } from "./playbackpipeline"; /* Radio sink, basically implements a simple SRS Client in Olympus. Does not support encryption at this moment */ export class RadioSink extends AudioSink { @@ -22,11 +23,15 @@ export class RadioSink extends AudioSink { #guid = makeID(22); #recorder: Recorder; #transmittingUnit: Unit | undefined; + #pan: number = 0; + #playbackPipeline: PlaybackPipeline; speechDataAvailable: (blob: Blob) => void = (blob) => {}; constructor() { super(); + this.#playbackPipeline = new PlaybackPipeline(); + this.#recorder = new Recorder(); this.#recorder.onRecordingCompleted = (blob) => this.speechDataAvailable(blob); @@ -109,11 +114,23 @@ export class RadioSink extends AudioSink { return this.#volume; } + setPan(pan: number) { + this.#pan = pan; + this.#playbackPipeline.setPan(pan); + AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); + } + + getPan() { + return this.#pan; + } + setReceiving(receiving) { // Only do it if actually changed if (receiving !== this.#receiving) { AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks()); + this.#playbackPipeline.setEnabled(receiving); + if (getApp().getAudioManager().getSpeechRecognition()) { if (receiving) this.#recorder.start(); else this.#recorder.stop(); @@ -168,4 +185,8 @@ export class RadioSink extends AudioSink { getTransmittingUnit() { return this.#transmittingUnit; } + + playBuffer(arrayBuffer) { + this.#playbackPipeline.playBuffer(arrayBuffer); + } } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 001a9d92..baeb5719 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -45,7 +45,7 @@ export interface OlympusConfig { } export interface SessionData { - radios?: { frequency: number; modulation: number }[]; + radios?: { frequency: number; modulation: number; pan: number }[]; fileSources?: { filename: string; volume: number }[]; unitSinks?: {ID: number}[]; connections?: any[]; diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index ffee8cba..ba324ee2 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -21,6 +21,7 @@ export class SessionDataManager { return { frequency: radioSink.getFrequency(), modulation: radioSink.getModulation(), + pan: radioSink.getPan() }; }); diff --git a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx index 94a10a56..a5a41c67 100644 --- a/frontend/react/src/ui/panels/components/radiosinkpanel.tsx +++ b/frontend/react/src/ui/panels/components/radiosinkpanel.tsx @@ -6,6 +6,7 @@ import { OlStateButton } from "../../components/olstatebutton"; import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons"; import { RadioSink } from "../../../audio/radiosink"; import { getApp } from "../../../olympusapp"; +import { OlRangeSlider } from "../../components/olrangeslider"; export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef) => { const [expanded, setExpanded] = useState(false); @@ -18,16 +19,16 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
{ - setExpanded(!expanded); - }} + > -
+
{ + setExpanded(!expanded); + }}>
{ @@ -76,6 +77,17 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey props.radio.setFrequency(value); }} /> +
+
Left
+ { + props.radio.setPan((Number(ev.currentTarget.value) - 50) / 50); + }} + className="my-auto" + > +
Right
+
{props.radio.setPtt(!props.radio.getPtt())}} + onClick={() => { + props.radio.setPtt(!props.radio.getPtt()); + }} tooltip="Talk on frequency" > diff --git a/frontend/server/srs.js b/frontend/server/srs.js deleted file mode 100644 index ef9e355e..00000000 --- a/frontend/server/srs.js +++ /dev/null @@ -1,219 +0,0 @@ -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)); -} \ No newline at end of file