First version of external sounds

This commit is contained in:
Pax1601 2024-09-09 08:06:03 +02:00
parent 5726d6dee2
commit d774977387
14 changed files with 1026 additions and 370 deletions

View 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);

View 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();
}
}

View File

@ -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")]));

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View 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);
}
}

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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));