mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
First version of external sounds
This commit is contained in:
parent
5726d6dee2
commit
d774977387
14
frontend/react/src/audio/audiodopplerprocessor.js
Normal file
14
frontend/react/src/audio/audiodopplerprocessor.js
Normal file
@ -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);
|
||||
|
||||
287
frontend/react/src/audio/audiolibrary.js
Normal file
287
frontend/react/src/audio/audiolibrary.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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")]));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
195
frontend/react/src/audio/audiounitpipeline.ts
Normal file
195
frontend/react/src/audio/audiounitpipeline.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user