From d77497738764dc755de7bf3617a8cac3dd890250 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Mon, 9 Sep 2024 08:06:03 +0200 Subject: [PATCH] First version of external sounds --- .../react/src/audio/audiodopplerprocessor.js | 14 + frontend/react/src/audio/audiolibrary.js | 287 ++++++++++++++++++ frontend/react/src/audio/audiomanager.ts | 51 ++-- frontend/react/src/audio/audiopacket.ts | 275 ++++++++++++++--- frontend/react/src/audio/audiosink.ts | 47 +-- frontend/react/src/audio/audiosource.ts | 8 +- frontend/react/src/audio/audiounitpipeline.ts | 195 ++++++++++++ frontend/react/src/audio/filesource.ts | 2 +- frontend/react/src/audio/microphonesource.ts | 2 +- frontend/react/src/audio/radiosink.ts | 56 +++- frontend/react/src/audio/unitsink.ts | 36 +-- frontend/react/src/other/utils.ts | 47 +-- frontend/server/src/audio/audiopacket.ts | 284 +++++++++-------- frontend/server/src/audio/srshandler.ts | 92 ++---- 14 files changed, 1026 insertions(+), 370 deletions(-) create mode 100644 frontend/react/src/audio/audiodopplerprocessor.js create mode 100644 frontend/react/src/audio/audiolibrary.js create mode 100644 frontend/react/src/audio/audiounitpipeline.ts diff --git a/frontend/react/src/audio/audiodopplerprocessor.js b/frontend/react/src/audio/audiodopplerprocessor.js new file mode 100644 index 00000000..5c53086e --- /dev/null +++ b/frontend/react/src/audio/audiodopplerprocessor.js @@ -0,0 +1,14 @@ +class AudioDopplerProcessor extends AudioWorkletProcessor { + process(inputs, outputs, parameters) { + const output = outputs[0]; + output.forEach((channel) => { + for (let i = 0; i < channel.length; i++) { + channel[i] = Math.random() * 2 - 1; + } + }); + return true; + } + } + + registerProcessor("audio-doppler-processor", AudioDopplerProcessor); + \ No newline at end of file diff --git a/frontend/react/src/audio/audiolibrary.js b/frontend/react/src/audio/audiolibrary.js new file mode 100644 index 00000000..52770543 --- /dev/null +++ b/frontend/react/src/audio/audiolibrary.js @@ -0,0 +1,287 @@ +// TODO Convert to typescript +// SAFARI Polyfills +if (!window.AudioBuffer.prototype.copyToChannel) { + window.AudioBuffer.prototype.copyToChannel = function copyToChannel(buffer, channel) { + this.getChannelData(channel).set(buffer); + }; +} +if (!window.AudioBuffer.prototype.copyFromChannel) { + window.AudioBuffer.prototype.copyFromChannel = function copyFromChannel(buffer, channel) { + buffer.set(this.getChannelData(channel)); + }; +} + +export class Effect { + constructor(context) { + this.name = "effect"; + this.context = context; + this.input = this.context.createGain(); + this.effect = null; + this.bypassed = false; + this.output = this.context.createGain(); + this.setup(); + this.wireUp(); + } + + setup() { + this.effect = this.context.createGain(); + } + + wireUp() { + this.input.connect(this.effect); + this.effect.connect(this.output); + } + + connect(destination) { + this.output.connect(destination); + } +} + +export class Sample { + constructor(context) { + this.context = context; + this.buffer = this.context.createBufferSource(); + this.buffer.start(); + this.sampleBuffer = null; + this.rawBuffer = null; + this.loaded = false; + this.output = this.context.createGain(); + this.output.gain.value = 0.1; + } + + play() { + if (this.loaded) { + this.buffer = this.context.createBufferSource(); + this.buffer.buffer = this.sampleBuffer; + this.buffer.connect(this.output); + this.buffer.start(this.context.currentTime); + } + } + + connect(input) { + this.output.connect(input); + } + + load(path) { + this.loaded = false; + return fetch(path) + .then((response) => response.arrayBuffer()) + .then((myBlob) => { + return new Promise((resolve, reject) => { + this.context.decodeAudioData(myBlob, resolve, reject); + }); + }) + .then((buffer) => { + this.sampleBuffer = buffer; + this.loaded = true; + return this; + }); + } +} + +export class AmpEnvelope { + constructor(context, gain = 1) { + this.context = context; + this.output = this.context.createGain(); + this.output.gain.value = gain; + this.partials = []; + this.velocity = 0; + this.gain = gain; + this._attack = 0; + this._decay = 0.001; + this._sustain = this.output.gain.value; + this._release = 0.001; + } + + on(velocity) { + this.velocity = velocity / 127; + this.start(this.context.currentTime); + } + + off(MidiEvent) { + return this.stop(this.context.currentTime); + } + + start(time) { + this.output.gain.value = 0; + this.output.gain.setValueAtTime(0, time); + this.output.gain.setTargetAtTime(1, time, this.attack + 0.00001); + this.output.gain.setTargetAtTime(this.sustain * this.velocity, time + this.attack, this.decay); + } + + stop(time) { + this.sustain = this.output.gain.value; + this.output.gain.cancelScheduledValues(time); + this.output.gain.setValueAtTime(this.sustain, time); + this.output.gain.setTargetAtTime(0, time, this.release + 0.00001); + } + + set attack(value) { + this._attack = value; + } + + get attack() { + return this._attack; + } + + set decay(value) { + this._decay = value; + } + + get decay() { + return this._decay; + } + + set sustain(value) { + this.gain = value; + this._sustain; + } + + get sustain() { + return this.gain; + } + + set release(value) { + this._release = value; + } + + get release() { + return this._release; + } + + connect(destination) { + this.output.connect(destination); + } +} + +export class Voice { + constructor(context, type = "sawtooth", gain = 0.1) { + this.context = context; + this.type = type; + this.value = -1; + this.gain = gain; + this.output = this.context.createGain(); + this.partials = []; + this.output.gain.value = this.gain; + this.ampEnvelope = new AmpEnvelope(this.context); + this.ampEnvelope.connect(this.output); + } + + init() { + let osc = this.context.createOscillator(); + osc.type = this.type; + osc.connect(this.ampEnvelope.output); + osc.start(this.context.currentTime); + this.partials.push(osc); + } + + on(MidiEvent) { + this.value = MidiEvent.value; + this.partials.forEach((osc) => { + osc.frequency.value = MidiEvent.frequency; + }); + this.ampEnvelope.on(MidiEvent.velocity || MidiEvent); + } + + off(MidiEvent) { + this.ampEnvelope.off(MidiEvent); + this.partials.forEach((osc) => { + osc.stop(this.context.currentTime + this.ampEnvelope.release * 4); + }); + } + + connect(destination) { + this.output.connect(destination); + } + + set detune(value) { + this.partials.forEach((p) => (p.detune.value = value)); + } + + set attack(value) { + this.ampEnvelope.attack = value; + } + + get attack() { + return this.ampEnvelope.attack; + } + + set decay(value) { + this.ampEnvelope.decay = value; + } + + get decay() { + return this.ampEnvelope.decay; + } + + set sustain(value) { + this.ampEnvelope.sustain = value; + } + + get sustain() { + return this.ampEnvelope.sustain; + } + + set release(value) { + this.ampEnvelope.release = value; + } + + get release() { + return this.ampEnvelope.release; + } +} +export class Noise extends Voice { + constructor(context, gain) { + super(context, gain); + this._length = 2; + } + + get length() { + return this._length || 2; + } + set length(value) { + this._length = value; + } + + init() { + var lBuffer = new Float32Array(this.length * this.context.sampleRate); + var rBuffer = new Float32Array(this.length * this.context.sampleRate); + for (let i = 0; i < this.length * this.context.sampleRate; i++) { + lBuffer[i] = 1 - 2 * Math.random(); + rBuffer[i] = 1 - 2 * Math.random(); + } + let buffer = this.context.createBuffer(2, this.length * this.context.sampleRate, this.context.sampleRate); + buffer.copyToChannel(lBuffer, 0); + buffer.copyToChannel(rBuffer, 1); + + let osc = this.context.createBufferSource(); + osc.buffer = buffer; + osc.loop = true; + osc.loopStart = 0; + osc.loopEnd = 2; + osc.start(this.context.currentTime); + osc.connect(this.ampEnvelope.output); + this.partials.push(osc); + } + + on(MidiEvent) { + this.value = MidiEvent.value; + this.ampEnvelope.on(MidiEvent.velocity || MidiEvent); + } +} + +export class Filter extends Effect { + constructor(context, type = "lowpass", cutoff = 1000, resonance = 0.9) { + super(context); + this.name = "filter"; + this.effect.frequency.value = cutoff; + this.effect.Q.value = resonance; + this.effect.type = type; + } + + setup() { + this.effect = this.context.createBiquadFilter(); + this.effect.connect(this.output); + this.wireUp(); + } +} diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index 7b05cb30..60669849 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -2,7 +2,7 @@ import { AudioMessageType } from "../constants/constants"; import { MicrophoneSource } from "./microphonesource"; import { RadioSink } from "./radiosink"; import { getApp } from "../olympusapp"; -import { fromBytes, makeID } from "../other/utils"; +import { makeID } from "../other/utils"; import { FileSource } from "./filesource"; import { AudioSource } from "./audiosource"; import { Buffer } from "buffer"; @@ -10,6 +10,7 @@ import { PlaybackPipeline } from "./playbackpipeline"; import { AudioSink } from "./audiosink"; import { Unit } from "../unit/unit"; import { UnitSink } from "./unitsink"; +import { AudioPacket, MessageType } from "./audiopacket"; export class AudioManager { #audioContext: AudioContext; @@ -27,6 +28,7 @@ export class AudioManager { #port: number = 4000; #socket: WebSocket | null = null; #guid: string = makeID(22); + #SRSClientUnitIDs: number[] = []; constructor() { document.addEventListener("configLoaded", () => { @@ -67,14 +69,19 @@ export class AudioManager { /* Extract the audio data as array */ let packetUint8Array = new Uint8Array(await event.data.arrayBuffer()); - /* Extract the encoded audio data */ - let audioLength = fromBytes(packetUint8Array.slice(2, 4)); - let audioUint8Array = packetUint8Array.slice(6, 6 + audioLength); + if (packetUint8Array[0] === MessageType.audio) { + /* Extract the encoded audio data */ + let audioPacket = new AudioPacket(); + audioPacket.fromByteArray(packetUint8Array.slice(1)); - /* Extract the frequency value and play it on the speakers if we are listening to it*/ - let frequency = new DataView(packetUint8Array.slice(6 + audioLength, 6 + audioLength + 8).reverse().buffer).getFloat64(0); - if (sink.getFrequency() === frequency) { - this.#playbackPipeline.play(audioUint8Array.buffer); + /* Extract the frequency value and play it on the speakers if we are listening to it*/ + audioPacket.getFrequencies().forEach((frequencyInfo) => { + if (sink.getFrequency() === frequencyInfo.frequency && sink.getModulation() === frequencyInfo.modulation) { + this.#playbackPipeline.play(audioPacket.getAudioData().buffer); + } + }); + } else { + this.#SRSClientUnitIDs = JSON.parse(new TextDecoder().decode(packetUint8Array.slice(1))).unitIDs; } } }); @@ -84,8 +91,7 @@ export class AudioManager { const microphoneSource = new MicrophoneSource(); microphoneSource.initialize().then(() => { this.#sinks.forEach((sink) => { - if (sink instanceof RadioSink) - microphoneSource.connect(sink); + if (sink instanceof RadioSink) microphoneSource.connect(sink); }); this.#sources.push(microphoneSource); document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); @@ -144,12 +150,11 @@ export class AudioManager { this.#sinks = this.#sinks.filter((v) => v != sink); let idx = 1; this.#sinks.forEach((sink) => { - if (sink instanceof RadioSink) - sink.setName(`Radio ${idx++}`); + if (sink instanceof RadioSink) sink.setName(`Radio ${idx++}`); }); document.dispatchEvent(new CustomEvent("audioSinksUpdated")); } - + removeSource(source) { source.disconnect(); this.#sources = this.#sources.filter((v) => v != source); @@ -172,18 +177,24 @@ export class AudioManager { return this.#audioContext; } + getSRSClientsUnitIDs() { + return this.#SRSClientUnitIDs; + } + #syncRadioSettings() { let message = { type: "Settings update", guid: this.#guid, coalition: 2, - settings: this.#sinks.filter((sink) => sink instanceof RadioSink).map((radio) => { - return { - frequency: radio.getFrequency(), - modulation: radio.getModulation(), - ptt: radio.getPtt(), - }; - }), + settings: this.#sinks + .filter((sink) => sink instanceof RadioSink) + .map((radio) => { + return { + frequency: radio.getFrequency(), + modulation: radio.getModulation(), + ptt: radio.getPtt(), + }; + }), }; if (this.#socket?.readyState == 1) this.#socket?.send(new Uint8Array([AudioMessageType.settings, ...Buffer.from(JSON.stringify(message), "utf-8")])); diff --git a/frontend/react/src/audio/audiopacket.ts b/frontend/react/src/audio/audiopacket.ts index 398d1004..b20b134c 100644 --- a/frontend/react/src/audio/audiopacket.ts +++ b/frontend/react/src/audio/audiopacket.ts @@ -1,73 +1,250 @@ +import { byteArrayToDouble, byteArrayToInteger, doubleToByteArray, integerToByteArray } from "../other/utils"; 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; +export enum MessageType { + audio, + settings, + unitIDs +} - constructor(data, settings, guid, lat?, lng?, alt?) { +export class AudioPacket { + /* Mandatory data */ + #frequencies: { frequency: number; modulation: number; encryption: number }[] = []; + #audioData: Uint8Array; + #transmissionGUID: string; + #clientGUID: string; + + /* Default data */ + #unitID: number = 0; + #hops: number = 0; + + /* Out of standard data (this is not compliant with SRS standard, used for external audio effects) */ + #latitude: number | null = null; + #longitude: number | null = null; + #altitude: number | null = null; + + /* Usually internally set only */ + #packetID: number | null = null; + + fromByteArray(byteArray: Uint8Array) { + let totalLength = byteArrayToInteger(byteArray.slice(0, 2)); + let audioLength = byteArrayToInteger(byteArray.slice(2, 4)); + let frequenciesLength = byteArrayToInteger(byteArray.slice(4, 6)); + + /* Perform some sanity checks */ + if (totalLength !== byteArray.length) { + console.log( + `Warning, audio packet expected length is ${totalLength} but received length is ${byteArray.length}, aborting...` + ); + return; + } + + if (frequenciesLength % 10 !== 0) { + console.log( + `Warning, audio packet frequencies data length is ${frequenciesLength} which is not a multiple of 10, aborting...` + ); + return; + } + + /* Extract the audio data */ + this.#audioData = byteArray.slice(6, 6 + audioLength); + + /* Extract the frequencies */ + let offset = 6 + audioLength; + for (let idx = 0; idx < frequenciesLength / 10; idx++) { + this.#frequencies.push({ + frequency: byteArrayToDouble(byteArray.slice(offset, offset + 8)), + modulation: byteArray[offset + 8], + encryption: byteArray[offset + 9], + }); + offset += 10; + } + + /* If necessary increase the packetID */ + if (this.#packetID === null) this.#packetID = packetID++; + + /* Extract the remaining data */ + this.#unitID = byteArrayToInteger(byteArray.slice(offset, offset + 4)); + offset += 4; + this.#packetID = byteArrayToInteger(byteArray.slice(offset, offset + 8)); + offset += 8; + this.#hops = byteArrayToInteger(byteArray.slice(offset, offset + 1)); + offset += 1; + this.#transmissionGUID = new TextDecoder().decode(byteArray.slice(offset, offset + 22)); + offset += 22; + this.#clientGUID = new TextDecoder().decode(byteArray.slice(offset, offset + 22)); + offset += 22; + } + + toByteArray() { + /* Perform some sanity checks // TODO check correct values */ + if (this.#frequencies.length === 0) { + console.log( + "Warning, could not encode audio packet, no frequencies data provided, aborting..." + ); + return; + } + + if (this.#audioData === undefined) { + console.log( + "Warning, could not encode audio packet, no audio data provided, aborting..." + ); + return; + } + + if (this.#transmissionGUID === undefined) { + console.log( + "Warning, could not encode audio packet, no transmission GUID provided, aborting..." + ); + return; + } + + if (this.#clientGUID === undefined) { + console.log( + "Warning, could not encode audio packet, no client GUID provided, aborting..." + ); + return; + } + + // Prepare the array for the header let header: number[] = [0, 0, 0, 0, 0, 0]; - let encFrequency: number[] = [...doubleToByteArray(settings.frequency)]; - let encModulation: number[] = [settings.modulation]; - let encEncryption: number[] = [0]; + // Encode the frequencies data + let frequenciesData = [] as number[]; + this.#frequencies.forEach((data) => { + frequenciesData = frequenciesData.concat( + [...doubleToByteArray(data.frequency)], + [data.modulation], + [data.encryption] + ); + }); - let encUnitID: number[] = getBytes(0, 4); - let encPacketID: number[] = getBytes(packetID, 8); - packetID++; - let encHops: number[] = [0]; + // Encode unitID, packetID, hops + let encUnitID: number[] = integerToByteArray(this.#unitID, 4); + let encPacketID: number[] = integerToByteArray(this.#packetID, 8); + let encHops: number[] = [this.#hops]; - let packet: number[] = ([] as number[]).concat( + // Assemble packet + let encodedData: number[] = ([] as number[]).concat( header, - [...data], - encFrequency, - encModulation, - encEncryption, + [...this.#audioData], + frequenciesData, encUnitID, encPacketID, encHops, - [...Buffer.from(guid, "utf-8")], - [...Buffer.from(guid, "utf-8")] + [...Buffer.from(this.#transmissionGUID, "utf-8")], + [...Buffer.from(this.#clientGUID, "utf-8")] ); - if (lat !== undefined && lng !== undefined && alt !== undefined) { - packet.concat([...doubleToByteArray(lat)], [...doubleToByteArray(lng)], [...doubleToByteArray(alt)]); + if ( + this.#latitude !== undefined && + this.#longitude !== undefined && + this.#altitude !== undefined + ) { + encodedData.concat( + [...doubleToByteArray(this.#latitude)], + [...doubleToByteArray(this.#longitude)], + [...doubleToByteArray(this.#altitude)] + ); } - let encPacketLen = getBytes(packet.length, 2); - packet[0] = encPacketLen[0]; - packet[1] = encPacketLen[1]; + // Set the lengths of the parts + let encPacketLen = integerToByteArray(encodedData.length, 2); + encodedData[0] = encPacketLen[0]; + encodedData[1] = encPacketLen[1]; - let encAudioLen = getBytes(data.length, 2); - packet[2] = encAudioLen[0]; - packet[3] = encAudioLen[1]; + let encAudioLen = integerToByteArray(this.#audioData.length, 2); + encodedData[2] = encAudioLen[0]; + encodedData[3] = encAudioLen[1]; - let frequencyAudioLen = getBytes(10, 2); - packet[4] = frequencyAudioLen[0]; - packet[5] = frequencyAudioLen[1]; + let frequencyAudioLen = integerToByteArray(frequenciesData.length, 2); + encodedData[4] = frequencyAudioLen[0]; + encodedData[5] = frequencyAudioLen[1]; - this.#packet = new Uint8Array([0].concat(packet)); + return new Uint8Array([0].concat(encodedData)); } - getArray() { - return this.#packet; + setFrequencies( + frequencies: { frequency: number; modulation: number; encryption: number }[] + ) { + this.#frequencies = frequencies; + } + + getFrequencies() { + return this.#frequencies; + } + + setAudioData(audioData: Uint8Array) { + this.#audioData = audioData; + } + + getAudioData() { + return this.#audioData; + } + + setTransmissionGUID(transmissionGUID: string) { + this.#transmissionGUID = transmissionGUID; + } + + getTransmissionGUID() { + return this.#transmissionGUID; + } + + setClientGUID(clientGUID: string) { + this.#clientGUID = clientGUID; + } + + getClientGUID() { + return this.#clientGUID; + } + + setUnitID(unitID: number) { + this.#unitID = unitID; + } + + getUnitID() { + return this.#unitID; + } + + setPacketID(packetID: number) { + this.#packetID = packetID; + } + + getPacketID() { + return this.#packetID; + } + + setHops(hops: number) { + this.#hops = hops; + } + + getHops() { + return this.#hops; + } + + setLatitude(latitude: number) { + this.#latitude = latitude; + } + + getLatitude() { + return this.#latitude; + } + + setLongitude(longitude: number) { + this.#longitude = longitude; + } + + getLongitude() { + return this.#longitude; + } + + setAltitude(altitude: number) { + this.#altitude = altitude; + } + + getAltitude() { + return this.#altitude; } } diff --git a/frontend/react/src/audio/audiosink.ts b/frontend/react/src/audio/audiosink.ts index bbf46cb8..84ffea80 100644 --- a/frontend/react/src/audio/audiosink.ts +++ b/frontend/react/src/audio/audiosink.ts @@ -1,47 +1,11 @@ import { getApp } from "../olympusapp"; export abstract class AudioSink { - #encoder: AudioEncoder; #name: string; - #node: MediaStreamAudioDestinationNode; - #audioTrackProcessor: any; // TODO can we have typings? #gainNode: GainNode; constructor() { - /* A gain node is used because it allows to connect multiple inputs */ this.#gainNode = getApp().getAudioManager().getAudioContext().createGain(); - this.#node = getApp().getAudioManager().getAudioContext().createMediaStreamDestination(); - this.#node.channelCount = 1; - - this.#encoder = new AudioEncoder({ - output: (data) => this.handleEncodedData(data), - error: (e) => { - console.log(e); - }, - }); - - this.#encoder.configure({ - codec: "opus", - numberOfChannels: 1, - sampleRate: 16000, - //@ts-ignore // TODO why is it giving error? - opus: { - frameDuration: 40000, - }, - bitrateMode: "constant", - }); - - //@ts-ignore - this.#audioTrackProcessor = new MediaStreamTrackProcessor({ - track: this.#node.stream.getAudioTracks()[0], - }); - this.#audioTrackProcessor.readable.pipeTo( - new WritableStream({ - write: (arrayBuffer) => this.#handleRawData(arrayBuffer), - }) - ); - - this.#gainNode.connect(this.#node); } setName(name) { @@ -53,18 +17,11 @@ export abstract class AudioSink { } disconnect() { - this.getNode().disconnect(); + this.getInputNode().disconnect(); document.dispatchEvent(new CustomEvent("audioSinksUpdated")); } - getNode() { + getInputNode() { return this.#gainNode; } - - #handleRawData(audioData) { - this.#encoder.encode(audioData); - audioData.close(); - } - - abstract handleEncodedData(encodedAudioChunk: EncodedAudioChunk): void; } diff --git a/frontend/react/src/audio/audiosource.ts b/frontend/react/src/audio/audiosource.ts index 12256726..f281f02a 100644 --- a/frontend/react/src/audio/audiosource.ts +++ b/frontend/react/src/audio/audiosource.ts @@ -15,17 +15,17 @@ export abstract class AudioSource { } connect(sink: AudioSink) { - this.getNode().connect(sink.getNode()); + this.getOutputNode().connect(sink.getInputNode()); this.#connectedTo.push(sink); document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); } disconnect(sinkToDisconnect?: AudioSink) { if (sinkToDisconnect !== undefined) { - this.getNode().disconnect(sinkToDisconnect.getNode()); + this.getOutputNode().disconnect(sinkToDisconnect.getInputNode()); this.#connectedTo = this.#connectedTo.filter((sink) => sink != sinkToDisconnect); } else { - this.getNode().disconnect(); + this.getOutputNode().disconnect(); } document.dispatchEvent(new CustomEvent("audioSourcesUpdated")); @@ -57,7 +57,7 @@ export abstract class AudioSource { return this.#meter; } - getNode() { + getOutputNode() { return this.#gainNode; } diff --git a/frontend/react/src/audio/audiounitpipeline.ts b/frontend/react/src/audio/audiounitpipeline.ts new file mode 100644 index 00000000..649c5ab0 --- /dev/null +++ b/frontend/react/src/audio/audiounitpipeline.ts @@ -0,0 +1,195 @@ +import { getApp } from "../olympusapp"; +import { Unit } from "../unit/unit"; +import { Filter, Noise } from "./audiolibrary"; +import { AudioPacket } from "./audiopacket"; + +export class AudioUnitPipeline { + #inputNode: GainNode; + #sourceUnit: Unit; + #unitID: number; + #gainNode: GainNode; + #destinationNode: MediaStreamAudioDestinationNode; + #audioTrackProcessor: any; + #encoder: AudioEncoder; + #distance: number = 0; + + #convolver: ConvolverNode; + #delay: DelayNode; + #multitap: DelayNode[]; + #multitapGain: GainNode; + #wet: GainNode; + #tailOsc: Noise; + + #dataBuffer: number[] = []; + + constructor(sourceUnit: Unit, unitID: number, inputNode: GainNode) { + this.#sourceUnit = sourceUnit; + this.#unitID = unitID; + + /* Initialize the Opus Encoder */ + this.#encoder = new AudioEncoder({ + output: (data) => this.handleEncodedData(data, unitID), + error: (e) => { + console.log(e); + }, + }); + + this.#encoder.configure({ + codec: "opus", + numberOfChannels: 1, + sampleRate: 16000, + //@ts-ignore // TODO why is it giving error? + opus: { + frameDuration: 40000, + }, + bitrateMode: "constant", + }); + + /* Create the destination node where the stream will be written to be encoded and sent to SRS */ + this.#destinationNode = getApp().getAudioManager().getAudioContext().createMediaStreamDestination(); + this.#destinationNode.channelCount = 1; + + /* Gain node to modulate the strength of the audio */ + this.#gainNode = getApp().getAudioManager().getAudioContext().createGain(); + + /* Create the track processor to encode and write the data to SRS */ + //@ts-ignore + this.#audioTrackProcessor = new MediaStreamTrackProcessor({ + track: this.#destinationNode.stream.getAudioTracks()[0], + }); + this.#audioTrackProcessor.readable.pipeTo( + new WritableStream({ + write: (audioData) => this.handleRawData(audioData), + }) + ); + + /* Create the pipeline */ + this.#inputNode = inputNode; + this.#inputNode.connect(this.#gainNode); + this.#setupEffects(); + + /* Create the interval task to update the data */ + setInterval(() => { + let destinationUnit = getApp().getUnitsManager().getUnitByID(this.#unitID); + if (destinationUnit) { + let distance = destinationUnit?.getPosition().distanceTo(this.#sourceUnit.getPosition()); + this.#distance = 0.9 * this.#distance + 0.1 * distance; + + let newGain = 1.0 - Math.pow(this.#distance / 1000, 0.5); // Arbitrary + + this.#gainNode.gain.setValueAtTime(newGain, getApp().getAudioManager().getAudioContext().currentTime); + this.#multitapGain.gain.setValueAtTime(newGain / 10, getApp().getAudioManager().getAudioContext().currentTime); + + let reverbTime = this.#distance / 1000 / 2; //Arbitrary + let preDelay = this.#distance / 1000; // Arbitrary + this.#delay.delayTime.setValueAtTime(preDelay, getApp().getAudioManager().getAudioContext().currentTime); + this.#multitap.forEach((t, i) => { + t.delayTime.setValueAtTime(0.001 + i * (preDelay / 2), getApp().getAudioManager().getAudioContext().currentTime); + }); + this.#tailOsc.release = reverbTime / 3; + } + }, 100); + } + + handleEncodedData(encodedAudioChunk, unitID) { + let arrayBuffer = new ArrayBuffer(encodedAudioChunk.byteLength); + encodedAudioChunk.copyTo(arrayBuffer); + + let audioPacket = new AudioPacket(); + audioPacket.setAudioData(new Uint8Array(arrayBuffer)); + audioPacket.setFrequencies([ + { + frequency: 100, + modulation: 2, + encryption: 0, + }, + ]); + audioPacket.setClientGUID(getApp().getAudioManager().getGuid()); + audioPacket.setTransmissionGUID(getApp().getAudioManager().getGuid()); + + if (unitID !== 0) { + audioPacket.setUnitID(unitID); + getApp().getAudioManager().send(audioPacket.toByteArray()); + } + } + + handleRawData(audioData) { + /* Ignore players that are too far away */ + if (this.#distance < 1000) { + this.#encoder.encode(audioData); + + audioData.close(); + } + } + + #setupEffects() { + let reverbTime = 0.1; //Arbitrary + + this.#convolver = getApp().getAudioManager().getAudioContext().createConvolver(); + this.#delay = getApp().getAudioManager().getAudioContext().createDelay(1); + + this.#multitap = []; + for (let i = 2; i > 0; i--) { + this.#multitap.push(getApp().getAudioManager().getAudioContext().createDelay(1)); + } + this.#multitap.map((t, i) => { + if (this.#multitap[i + 1]) { + t.connect(this.#multitap[i + 1]); + } + }); + + this.#multitapGain = getApp().getAudioManager().getAudioContext().createGain(); + this.#multitap[this.#multitap.length - 1].connect(this.#multitapGain); + + this.#multitapGain.connect(this.#destinationNode); + this.#wet = getApp().getAudioManager().getAudioContext().createGain(); + + this.#gainNode.connect(this.#wet); + this.#wet.connect(this.#delay); + this.#wet.connect(this.#multitap[0]); + this.#delay.connect(this.#convolver); + + getApp().getAudioManager().getAudioContext().audioWorklet.addModule("audiodopplerprocessor.js").then(() => { + const randomNoiseNode = new AudioWorkletNode( + getApp().getAudioManager().getAudioContext(), + "audio-doppler-processor", + ); + this.#convolver.connect(randomNoiseNode); + randomNoiseNode.connect(this.#destinationNode); + }); + + + this.#renderTail(reverbTime); + } + + #renderTail(reverbTime) { + let attack = 0; + let decay = 0.0; + + const tailContext = new OfflineAudioContext( + 2, + getApp().getAudioManager().getAudioContext().sampleRate * reverbTime, + getApp().getAudioManager().getAudioContext().sampleRate + ); + + this.#tailOsc = new Noise(tailContext, 1); + const tailLPFilter = new Filter(tailContext, "lowpass", 5000, 1); + const tailHPFilter = new Filter(tailContext, "highpass", 500, 1); + + this.#tailOsc.init(); + this.#tailOsc.connect(tailHPFilter.input); + tailHPFilter.connect(tailLPFilter.input); + tailLPFilter.connect(tailContext.destination); + this.#tailOsc.attack = attack; + this.#tailOsc.decay = decay; + + setTimeout(() => { + tailContext.startRendering().then((buffer) => { + this.#convolver.buffer = buffer; + }); + + this.#tailOsc.on({ frequency: 500, velocity: 127 }); + //tailOsc.off(); + }, 20); + } +} diff --git a/frontend/react/src/audio/filesource.ts b/frontend/react/src/audio/filesource.ts index 7bdf006a..247ac3c7 100644 --- a/frontend/react/src/audio/filesource.ts +++ b/frontend/react/src/audio/filesource.ts @@ -43,7 +43,7 @@ export class FileSource extends AudioSource { play() { this.#source = getApp().getAudioManager().getAudioContext().createBufferSource(); this.#source.buffer = this.#audioBuffer; - this.#source.connect(this.getNode()); + this.#source.connect(this.getOutputNode()); this.#source.loop = this.#looping; this.#source.start(0, this.#currentPosition); diff --git a/frontend/react/src/audio/microphonesource.ts b/frontend/react/src/audio/microphonesource.ts index 05610380..048d6550 100644 --- a/frontend/react/src/audio/microphonesource.ts +++ b/frontend/react/src/audio/microphonesource.ts @@ -15,7 +15,7 @@ export class MicrophoneSource extends AudioSource { if (getApp().getAudioManager().getAudioContext()) { this.#node = getApp().getAudioManager().getAudioContext().createMediaStreamSource(microphone); - this.#node.connect(this.getNode()); + this.#node.connect(this.getOutputNode()); } } diff --git a/frontend/react/src/audio/radiosink.ts b/frontend/react/src/audio/radiosink.ts index aa8cb9e1..23a8d685 100644 --- a/frontend/react/src/audio/radiosink.ts +++ b/frontend/react/src/audio/radiosink.ts @@ -3,6 +3,9 @@ import { AudioPacket } from "./audiopacket"; import { getApp } from "../olympusapp"; export class RadioSink extends AudioSink { + #encoder: AudioEncoder; + #desinationNode: MediaStreamAudioDestinationNode; + #audioTrackProcessor: any; // TODO can we have typings? #frequency = 251000000; #modulation = 0; #ptt = false; @@ -11,6 +14,39 @@ export class RadioSink extends AudioSink { constructor() { super(); + + this.#encoder = new AudioEncoder({ + output: (data) => this.handleEncodedData(data), + error: (e) => { + console.log(e); + }, + }); + + this.#encoder.configure({ + codec: "opus", + numberOfChannels: 1, + sampleRate: 16000, + //@ts-ignore // TODO why is it giving error? + opus: { + frameDuration: 40000, + }, + bitrateMode: "constant", + }); + + this.#desinationNode = getApp().getAudioManager().getAudioContext().createMediaStreamDestination(); + this.#desinationNode.channelCount = 1; + + //@ts-ignore + this.#audioTrackProcessor = new MediaStreamTrackProcessor({ + track: this.#desinationNode.stream.getAudioTracks()[0], + }); + this.#audioTrackProcessor.readable.pipeTo( + new WritableStream({ + write: (arrayBuffer) => this.handleRawData(arrayBuffer), + }) + ); + + this.getInputNode().connect(this.#desinationNode); } setFrequency(frequency) { @@ -63,15 +99,21 @@ export class RadioSink extends AudioSink { encodedAudioChunk.copyTo(arrayBuffer); if (this.#ptt) { - let packet = new AudioPacket( - new Uint8Array(arrayBuffer), - { + let audioPacket = new AudioPacket(); + audioPacket.setAudioData(new Uint8Array(arrayBuffer)); + audioPacket.setFrequencies([{ frequency: this.#frequency, modulation: this.#modulation, - }, - getApp().getAudioManager().getGuid() - ); - getApp().getAudioManager().send(packet.getArray()); + encryption: 0 + }]) + audioPacket.setClientGUID(getApp().getAudioManager().getGuid()); + audioPacket.setTransmissionGUID(getApp().getAudioManager().getGuid()); + getApp().getAudioManager().send(audioPacket.toByteArray()); } } + + handleRawData(audioData) { + this.#encoder.encode(audioData); + audioData.close(); + } } diff --git a/frontend/react/src/audio/unitsink.ts b/frontend/react/src/audio/unitsink.ts index 7992aed4..76bad771 100644 --- a/frontend/react/src/audio/unitsink.ts +++ b/frontend/react/src/audio/unitsink.ts @@ -1,37 +1,29 @@ import { AudioSink } from "./audiosink"; -import { AudioPacket } from "./audiopacket"; import { getApp } from "../olympusapp"; import { Unit } from "../unit/unit"; +import { AudioUnitPipeline } from "./audiounitpipeline"; export class UnitSink extends AudioSink { #unit: Unit; + #unitPipelines: {[key: string]: AudioUnitPipeline} = {}; - constructor(unit: Unit) { + constructor(sourceUnit: Unit) { super(); - this.#unit = unit; - this.setName(`${unit.getUnitName()} - ${unit.getName()}`); + this.#unit = sourceUnit; + this.setName(`${sourceUnit.getUnitName()} - ${sourceUnit.getName()}`); + + getApp() + .getAudioManager() + .getSRSClientsUnitIDs() + .forEach((unitID) => { + if (unitID !== 0) { + this.#unitPipelines[unitID] = new AudioUnitPipeline(sourceUnit, unitID, this.getInputNode()); + } + }); } getUnit() { return this.#unit; } - - handleEncodedData(encodedAudioChunk: EncodedAudioChunk) { - let arrayBuffer = new ArrayBuffer(encodedAudioChunk.byteLength); - encodedAudioChunk.copyTo(arrayBuffer); - - let packet = new AudioPacket( - new Uint8Array(arrayBuffer), - { - frequency: 243000000, - modulation: 255, // HOPEFULLY this will never be used by SRS, indicates "loudspeaker" mode - }, - getApp().getAudioManager().getGuid(), - this.#unit.getPosition().lat, - this.#unit.getPosition().lng, - this.#unit.getPosition().alt - ); - getApp().getAudioManager().send(packet.getArray()); - } } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index 7bfed841..9359b6e2 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -533,15 +533,6 @@ export function getUnitsByLabel(filterString: string) { return [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits]; } -export function fromBytes(array) { - let res = 0; - for (let i = 0; i < array.length; i++) { - res = res << 8; - res += array[array.length - i - 1]; - } - return res; -} - export function makeID(length) { let result = ""; const characters = @@ -555,15 +546,33 @@ export function makeID(length) { return result; } -export function bufferToF32Planar(input: AudioBuffer): Float32Array { - const result = new Float32Array(input.length * 1); - - let offset = 0; - for (let i = 0; i < 1; i++) { - const data = input.getChannelData(i); - result.set(data, offset); - offset = data.length; +export function byteArrayToInteger(array) { + let res = 0; + for (let i = 0; i < array.length; i++) { + res = res << 8; + res += array[array.length - i - 1]; } + return res; +} - return result; -} \ No newline at end of file +export function integerToByteArray(value, length) { + let res: number[] = []; + for (let i = 0; i < length; i++) { + res.push(value & 255); + value = value >> 8; + } + return res; +} + +export function doubleToByteArray(number) { + var buffer = new ArrayBuffer(8); // JS numbers are 8 bytes long, or 64 bits + var longNum = new Float64Array(buffer); // so equivalent to Float64 + + longNum[0] = number; + + return Array.from(new Uint8Array(buffer)); +} + +export function byteArrayToDouble(array) { + return new DataView(array.reverse().buffer).getFloat64(0); +} diff --git a/frontend/server/src/audio/audiopacket.ts b/frontend/server/src/audio/audiopacket.ts index 6a903ef8..f8e721d2 100644 --- a/frontend/server/src/audio/audiopacket.ts +++ b/frontend/server/src/audio/audiopacket.ts @@ -1,25 +1,23 @@ +import { byteArrayToDouble, byteArrayToInteger, doubleToByteArray, integerToByteArray } from "../utils"; import { Buffer } from "buffer"; -import { - byteArrayToDouble, - byteArrayToInteger, - doubleToByteArray, - integerToByteArray, -} from "../utils"; var packetID = 0; +export enum MessageType { + audio, + settings, + unitIDs +} + export class AudioPacket { - #encodedData: Uint8Array; - /* Mandatory data */ - #frequencies: { frequency: number; modulation: number; encryption: number }[]; + #frequencies: { frequency: number; modulation: number; encryption: number }[] = []; #audioData: Uint8Array; #transmissionGUID: string; #clientGUID: string; /* Default data */ #unitID: number = 0; - #packetID: number = 0; #hops: number = 0; /* Out of standard data (this is not compliant with SRS standard, used for external audio effects) */ @@ -27,15 +25,150 @@ export class AudioPacket { #longitude: number | null = null; #altitude: number | null = null; - setEncodedData(encodedData: Uint8Array) { - this.#encodedData = encodedData; + /* Usually internally set only */ + #packetID: number | null = null; + + fromByteArray(byteArray: Uint8Array) { + let totalLength = byteArrayToInteger(byteArray.slice(0, 2)); + let audioLength = byteArrayToInteger(byteArray.slice(2, 4)); + let frequenciesLength = byteArrayToInteger(byteArray.slice(4, 6)); + + /* Perform some sanity checks */ + if (totalLength !== byteArray.length) { + console.log( + `Warning, audio packet expected length is ${totalLength} but received length is ${byteArray.length}, aborting...` + ); + return; + } + + if (frequenciesLength % 10 !== 0) { + console.log( + `Warning, audio packet frequencies data length is ${frequenciesLength} which is not a multiple of 10, aborting...` + ); + return; + } + + /* Extract the audio data */ + this.#audioData = byteArray.slice(6, 6 + audioLength); + + /* Extract the frequencies */ + let offset = 6 + audioLength; + for (let idx = 0; idx < frequenciesLength / 10; idx++) { + this.#frequencies.push({ + frequency: byteArrayToDouble(byteArray.slice(offset, offset + 8)), + modulation: byteArray[offset + 8], + encryption: byteArray[offset + 9], + }); + offset += 10; + } + + /* If necessary increase the packetID */ + if (this.#packetID === null) this.#packetID = packetID++; + + /* Extract the remaining data */ + this.#unitID = byteArrayToInteger(byteArray.slice(offset, offset + 4)); + offset += 4; + this.#packetID = byteArrayToInteger(byteArray.slice(offset, offset + 8)); + offset += 8; + this.#hops = byteArrayToInteger(byteArray.slice(offset, offset + 1)); + offset += 1; + this.#transmissionGUID = new TextDecoder().decode(byteArray.slice(offset, offset + 22)); + offset += 22; + this.#clientGUID = new TextDecoder().decode(byteArray.slice(offset, offset + 22)); + offset += 22; } - getEncodedData() { - return this.#encodedData; + toByteArray() { + /* Perform some sanity checks // TODO check correct values */ + if (this.#frequencies.length === 0) { + console.log( + "Warning, could not encode audio packet, no frequencies data provided, aborting..." + ); + return; + } + + if (this.#audioData === undefined) { + console.log( + "Warning, could not encode audio packet, no audio data provided, aborting..." + ); + return; + } + + if (this.#transmissionGUID === undefined) { + console.log( + "Warning, could not encode audio packet, no transmission GUID provided, aborting..." + ); + return; + } + + if (this.#clientGUID === undefined) { + console.log( + "Warning, could not encode audio packet, no client GUID provided, aborting..." + ); + return; + } + + // Prepare the array for the header + let header: number[] = [0, 0, 0, 0, 0, 0]; + + // Encode the frequencies data + let frequenciesData = [] as number[]; + this.#frequencies.forEach((data) => { + frequenciesData = frequenciesData.concat( + [...doubleToByteArray(data.frequency)], + [data.modulation], + [data.encryption] + ); + }); + + // Encode unitID, packetID, hops + let encUnitID: number[] = integerToByteArray(this.#unitID, 4); + let encPacketID: number[] = integerToByteArray(this.#packetID, 8); + let encHops: number[] = [this.#hops]; + + // Assemble packet + let encodedData: number[] = ([] as number[]).concat( + header, + [...this.#audioData], + frequenciesData, + encUnitID, + encPacketID, + encHops, + [...Buffer.from(this.#transmissionGUID, "utf-8")], + [...Buffer.from(this.#clientGUID, "utf-8")] + ); + + if ( + this.#latitude !== undefined && + this.#longitude !== undefined && + this.#altitude !== undefined + ) { + encodedData.concat( + [...doubleToByteArray(this.#latitude)], + [...doubleToByteArray(this.#longitude)], + [...doubleToByteArray(this.#altitude)] + ); + } + + // Set the lengths of the parts + let encPacketLen = integerToByteArray(encodedData.length, 2); + encodedData[0] = encPacketLen[0]; + encodedData[1] = encPacketLen[1]; + + let encAudioLen = integerToByteArray(this.#audioData.length, 2); + encodedData[2] = encAudioLen[0]; + encodedData[3] = encAudioLen[1]; + + let frequencyAudioLen = integerToByteArray(frequenciesData.length, 2); + encodedData[4] = frequencyAudioLen[0]; + encodedData[5] = frequencyAudioLen[1]; + + return new Uint8Array([0].concat(encodedData)); } - setFrequencies(frequencies: { frequency: number; modulation: number; encryption: number }[]) { + setFrequencies( + frequencies: { frequency: number; modulation: number; encryption: number }[] + ) { this.#frequencies = frequencies; } @@ -78,7 +211,7 @@ export class AudioPacket { setPacketID(packetID: number) { this.#packetID = packetID; } - + getPacketID() { return this.#packetID; } @@ -114,123 +247,4 @@ export class AudioPacket { getAltitude() { return this.#altitude; } - - fromByteArray(byteArray: Uint8Array) { - let totalLength = byteArrayToInteger(byteArray.slice(0, 2)); - let audioLength = byteArrayToInteger(byteArray.slice(2, 4)); - let frequenciesLength = byteArrayToInteger(byteArray.slice(4, 6)); - - /* Perform some sanity checks */ - if (totalLength !== byteArray.length) { - console.log( - `Warning, audio packet expected length is ${totalLength} but received length is ${byteArray.length}, aborting...` - ); - return; - } - - if (frequenciesLength % 10 !== 0) { - console.log( - `Warning, audio packet frequencies data length is ${frequenciesLength} which is not a multiple of 10, aborting...` - ); - return; - } - - /* Extract the audio data */ - this.#audioData = byteArray.slice(6, 6 + audioLength); - - /* Extract the frequencies */ - let offset = 6 + audioLength; - for (let idx = 0; idx < frequenciesLength / 10; idx++) { - this.#frequencies.push({ - frequency: byteArrayToDouble(byteArray.slice(offset, offset + 8)), - encryption: byteArray[offset + 8], - modulation: byteArray[offset + 9], - }); - offset += 10; - } - - /* Extract the remaining data */ - this.#unitID = byteArrayToInteger(byteArray.slice(offset, offset + 4)); - offset += 4; - this.#packetID = byteArrayToInteger(byteArray.slice(offset, offset + 8)); - offset += 8; - this.#hops = byteArrayToInteger(byteArray.slice(offset, offset + 1)); - offset += 1; - this.#transmissionGUID = byteArray.slice(offset, offset + 22).toString(); - offset += 22; - this.#clientGUID = byteArray.slice(offset, offset + 22).toString(); - offset += 22; - } - - toByteArray() { - /* Perform some sanity checks // TODO check correct values */ - if (this.#frequencies.length === 0) { - console.log("Warning, could not encode audio packet, no frequencies data provided, aborting..."); - return; - } - - if (this.#audioData === undefined) { - console.log("Warning, could not encode audio packet, no audio data provided, aborting..."); - return; - } - - if (this.#transmissionGUID === undefined) { - console.log("Warning, could not encode audio packet, no transmission GUID provided, aborting..."); - return; - } - - if (this.#clientGUID === undefined) { - console.log("Warning, could not encode audio packet, no client GUID provided, aborting..."); - return; - } - - // Prepare the array for the header - let header: number[] = [0, 0, 0, 0, 0, 0]; - - // Encode the frequencies data - let frequenciesData = ([] as number[]) - this.#frequencies.forEach((data) => { - return frequenciesData.concat([...doubleToByteArray(data.frequency)], [data.modulation], [data.encryption]); - }) - - // Encode unitID, packetID, hops - let encUnitID: number[] = integerToByteArray(this.#unitID, 4); - let encPacketID: number[] = integerToByteArray(this.#packetID, 8); - let encHops: number[] = [this.#hops]; - - // Assemble packet - let encodedData: number[] = ([] as number[]).concat( - header, - [...this.#audioData], - frequenciesData, - encUnitID, - encPacketID, - encHops, - [...Buffer.from(this.#transmissionGUID, "utf-8")], - [...Buffer.from(this.#clientGUID, "utf-8")] - ); - - if (this.#latitude !== undefined && this.#longitude !== undefined && this.#altitude !== undefined) { - encodedData.concat( - [...doubleToByteArray(this.#latitude)], - [...doubleToByteArray(this.#longitude)], - [...doubleToByteArray(this.#altitude)] - ); - } - - // Set the lengths of the parts - let encPacketLen = integerToByteArray(encodedData.length, 2); - encodedData[0] = encPacketLen[0]; - encodedData[1] = encPacketLen[1]; - - let encAudioLen = integerToByteArray(this.#audioData.length, 2); - encodedData[2] = encAudioLen[0]; - encodedData[3] = encAudioLen[1]; - - let frequencyAudioLen = integerToByteArray(frequenciesData.length, 2); - encodedData[4] = frequencyAudioLen[0]; - encodedData[5] = frequencyAudioLen[1]; - - this.#encodedData = new Uint8Array([0].concat(encodedData)); - } } diff --git a/frontend/server/src/audio/srshandler.ts b/frontend/server/src/audio/srshandler.ts index 21d81cb4..5d3eed32 100644 --- a/frontend/server/src/audio/srshandler.ts +++ b/frontend/server/src/audio/srshandler.ts @@ -1,43 +1,13 @@ +import { MessageType } from "./audiopacket"; import { defaultSRSData } from "./defaultdata"; -const { OpusEncoder } = require("@discordjs/opus"); -const encoder = new OpusEncoder(16000, 1); - -let decoder = null; -import('opus-decoder').then((res) => { - decoder = new res.OpusDecoder(); -}); - +/* TCP/IP socket */ var net = require("net"); var bufferString = ""; const SRS_VERSION = "2.1.0.10"; - var globalIndex = 1; -enum MessageType { - audio, - settings, -} - -function fromBytes(array) { - let res = 0; - for (let i = 0; i < array.length; i++) { - res = res << 8; - res += array[array.length - i - 1]; - } - return res; -} - -function getBytes(value, length) { - let res: number[] = []; - for (let i = 0; i < length; i++) { - res.push(value & 255); - value = value >> 8; - } - return res; -} - export class SRSHandler { ws: any; tcp = new net.Socket(); @@ -85,6 +55,19 @@ export class SRSHandler { if (this.tcp.readyState == "open") this.tcp.write(`${JSON.stringify(SYNC)}\n`); else clearInterval(this.syncInterval); + + let unitsBuffer = Buffer.from( + JSON.stringify({ + unitIDs: this.clients.map((client) => { + return client.RadioInfo.unitId; + }), + }), + "utf-8" + ); + + this.ws.send( + ([] as number[]).concat([MessageType.unitIDs], [...unitsBuffer]) + ); }, 1000); }); @@ -94,8 +77,7 @@ export class SRSHandler { try { let message = JSON.parse(bufferString.split("\n")[0]); bufferString = bufferString.slice(bufferString.indexOf("\n") + 1); - if (message.Clients !== undefined) - this.clients = message.Clients; + if (message.Clients !== undefined) this.clients = message.Clients; } catch (e) { console.log(e); } @@ -108,44 +90,20 @@ export class SRSHandler { }); this.udp.on("message", (message, remote) => { - if (this.ws && message.length > 22) this.ws.send(message); + if (this.ws && message.length > 22) + this.ws.send( + ([] as number[]).concat([MessageType.audio], [...message]) + ); }); } - decodeData(data){ + decodeData(data) { switch (data[0]) { case MessageType.audio: - let packetUint8Array = new Uint8Array(data.slice(1)); - - let audioLength = fromBytes(packetUint8Array.slice(2, 4)); - let frequenciesLength = fromBytes(packetUint8Array.slice(4, 6)); - let modulation = fromBytes(packetUint8Array.slice(6 + audioLength + 8, 6 + audioLength + 8 + 1)); - let offset = 6 + audioLength + frequenciesLength; - - if (modulation == 255) { - packetUint8Array = packetUint8Array.slice(0, -24) // Remove position data - packetUint8Array[6 + audioLength + 8] = 2; - this.clients.forEach((client) => { - getBytes(client.RadioInfo.unitId, 4).forEach((value, idx) => { - packetUint8Array[offset + idx] = value; - }); - - var dst = new ArrayBuffer(packetUint8Array.byteLength); - let newBuffer = new Uint8Array(dst); - newBuffer.set(new Uint8Array(packetUint8Array)); - this.udp.send(newBuffer, this.SRSPort, "localhost", (error) => { - if (error) - console.log(`Error sending data to SRS server: ${error}`); - }) - }) - } else { - this.udp.send(packetUint8Array, this.SRSPort, "localhost", (error) => { - if (error) - console.log(`Error sending data to SRS server: ${error}`); - }); - } - - + const encodedData = new Uint8Array(data.slice(1)); + this.udp.send(encodedData, this.SRSPort, "localhost", (error) => { + if (error) console.log(`Error sending data to SRS server: ${error}`); + }); break; case MessageType.settings: let message = JSON.parse(data.slice(1));