mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: added simple radio effect and left/right panner
This commit is contained in:
parent
dbd87d5724
commit
2bbcbc5576
@ -6,7 +6,6 @@ import { makeID } from "../other/utils";
|
||||
import { FileSource } from "./filesource";
|
||||
import { AudioSource } from "./audiosource";
|
||||
import { Buffer } from "buffer";
|
||||
import { PlaybackPipeline } from "./playbackpipeline";
|
||||
import { AudioSink } from "./audiosink";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { UnitSink } from "./unitsink";
|
||||
@ -30,9 +29,6 @@ export class AudioManager {
|
||||
#input: MediaDeviceInfo;
|
||||
#output: MediaDeviceInfo;
|
||||
|
||||
/* The playback pipeline enables audio playback on the speakers/headphones */
|
||||
#playbackPipeline: PlaybackPipeline;
|
||||
|
||||
/* The audio sinks used to transmit the audio stream to the SRS backend */
|
||||
#sinks: AudioSink[] = [];
|
||||
|
||||
@ -82,8 +78,6 @@ export class AudioManager {
|
||||
//@ts-ignore
|
||||
if (this.#output) this.#audioContext.setSinkId(this.#output.deviceId);
|
||||
|
||||
this.#playbackPipeline = new PlaybackPipeline();
|
||||
|
||||
/* Connect the audio websocket */
|
||||
let res = location.toString().match(/(?:http|https):\/\/(.+):/);
|
||||
if (res === null) res = location.toString().match(/(?:http|https):\/\/(.+)/);
|
||||
@ -129,7 +123,7 @@ export class AudioManager {
|
||||
var dst = new ArrayBuffer(audioPacket.getAudioData().buffer.byteLength);
|
||||
new Uint8Array(dst).set(new Uint8Array(audioPacket.getAudioData().buffer));
|
||||
sink.recordArrayBuffer(audioPacket.getAudioData().buffer);
|
||||
this.#playbackPipeline.playBuffer(dst);
|
||||
sink.playBuffer(dst);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -156,16 +150,19 @@ export class AudioManager {
|
||||
let newRadio = this.addRadio();
|
||||
newRadio?.setFrequency(options.frequency);
|
||||
newRadio?.setModulation(options.modulation);
|
||||
newRadio?.setPan(options.pan)
|
||||
});
|
||||
} else {
|
||||
/* Add two default radios and connect to the microphone*/
|
||||
let newRadio = this.addRadio();
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
newRadio.setPan(-1);
|
||||
|
||||
newRadio = this.addRadio();
|
||||
this.#sources.find((source) => source instanceof MicrophoneSource)?.connect(newRadio);
|
||||
this.#sources.find((source) => source instanceof TextToSpeechSource)?.connect(newRadio);
|
||||
newRadio.setPan(1);
|
||||
}
|
||||
|
||||
let sessionFileSources = getApp().getSessionDataManager().getSessionData().fileSources;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { getApp } from "../olympusapp";
|
||||
import { Filter, Noise } from "./audiolibrary";
|
||||
|
||||
export class PlaybackPipeline {
|
||||
#decoder = new AudioDecoder({
|
||||
@ -8,10 +9,12 @@ export class PlaybackPipeline {
|
||||
#trackGenerator: any; // TODO can we have typings?
|
||||
#writer: any;
|
||||
#gainNode: GainNode;
|
||||
#pannerNode: StereoPannerNode;
|
||||
#enabled: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.#decoder.configure({
|
||||
codec: 'opus',
|
||||
codec: "opus",
|
||||
numberOfChannels: 1,
|
||||
sampleRate: 16000,
|
||||
//@ts-ignore // TODO why is this giving an error?
|
||||
@ -30,8 +33,21 @@ export class PlaybackPipeline {
|
||||
|
||||
/* Connect to the device audio output */
|
||||
this.#gainNode = getApp().getAudioManager().getAudioContext().createGain();
|
||||
this.#pannerNode = getApp().getAudioManager().getAudioContext().createStereoPanner();
|
||||
let splitter = getApp().getAudioManager().getAudioContext().createChannelSplitter();
|
||||
let bandpass = new Filter(getApp().getAudioManager().getAudioContext(), "banpass", 600, 10);
|
||||
bandpass.setup();
|
||||
|
||||
mediaStreamSource.connect(this.#gainNode);
|
||||
this.#gainNode.connect(getApp().getAudioManager().getAudioContext().destination);
|
||||
this.#gainNode.connect(bandpass.input);
|
||||
bandpass.output.connect(splitter);
|
||||
splitter.connect(this.#pannerNode);
|
||||
|
||||
this.#pannerNode.pan.setValueAtTime(0, getApp().getAudioManager().getAudioContext().currentTime);
|
||||
|
||||
let noise = new Noise(getApp().getAudioManager().getAudioContext(), 0.01);
|
||||
noise.init();
|
||||
noise.connect(this.#gainNode);
|
||||
}
|
||||
|
||||
playBuffer(arrayBuffer) {
|
||||
@ -48,9 +64,23 @@ export class PlaybackPipeline {
|
||||
this.#decoder.decode(encodedAudioChunk);
|
||||
}
|
||||
|
||||
setEnabled(enabled) {
|
||||
if (enabled && !this.#enabled) {
|
||||
this.#enabled = true;
|
||||
this.#pannerNode.connect(getApp().getAudioManager().getAudioContext().destination);
|
||||
} else if (!enabled && this.#enabled) {
|
||||
this.#enabled = false;
|
||||
this.#pannerNode.disconnect(getApp().getAudioManager().getAudioContext().destination);
|
||||
}
|
||||
}
|
||||
|
||||
setPan(pan) {
|
||||
this.#pannerNode.pan.setValueAtTime(pan, getApp().getAudioManager().getAudioContext().currentTime);
|
||||
}
|
||||
|
||||
#handleDecodedData(audioData) {
|
||||
this.#writer.ready.then(() => {
|
||||
this.#writer.write(audioData);
|
||||
})
|
||||
this.#writer.write(audioData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { AudioSinksChangedEvent } from "../events";
|
||||
import { makeID } from "../other/utils";
|
||||
import { Recorder } from "./recorder";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { PlaybackPipeline } from "./playbackpipeline";
|
||||
|
||||
/* Radio sink, basically implements a simple SRS Client in Olympus. Does not support encryption at this moment */
|
||||
export class RadioSink extends AudioSink {
|
||||
@ -22,11 +23,15 @@ export class RadioSink extends AudioSink {
|
||||
#guid = makeID(22);
|
||||
#recorder: Recorder;
|
||||
#transmittingUnit: Unit | undefined;
|
||||
#pan: number = 0;
|
||||
#playbackPipeline: PlaybackPipeline;
|
||||
speechDataAvailable: (blob: Blob) => void = (blob) => {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#playbackPipeline = new PlaybackPipeline();
|
||||
|
||||
this.#recorder = new Recorder();
|
||||
this.#recorder.onRecordingCompleted = (blob) => this.speechDataAvailable(blob);
|
||||
|
||||
@ -109,11 +114,23 @@ export class RadioSink extends AudioSink {
|
||||
return this.#volume;
|
||||
}
|
||||
|
||||
setPan(pan: number) {
|
||||
this.#pan = pan;
|
||||
this.#playbackPipeline.setPan(pan);
|
||||
AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
}
|
||||
|
||||
getPan() {
|
||||
return this.#pan;
|
||||
}
|
||||
|
||||
setReceiving(receiving) {
|
||||
// Only do it if actually changed
|
||||
if (receiving !== this.#receiving) {
|
||||
AudioSinksChangedEvent.dispatch(getApp().getAudioManager().getSinks());
|
||||
|
||||
this.#playbackPipeline.setEnabled(receiving);
|
||||
|
||||
if (getApp().getAudioManager().getSpeechRecognition()) {
|
||||
if (receiving) this.#recorder.start();
|
||||
else this.#recorder.stop();
|
||||
@ -168,4 +185,8 @@ export class RadioSink extends AudioSink {
|
||||
getTransmittingUnit() {
|
||||
return this.#transmittingUnit;
|
||||
}
|
||||
|
||||
playBuffer(arrayBuffer) {
|
||||
this.#playbackPipeline.playBuffer(arrayBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ export interface OlympusConfig {
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
radios?: { frequency: number; modulation: number }[];
|
||||
radios?: { frequency: number; modulation: number; pan: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: {ID: number}[];
|
||||
connections?: any[];
|
||||
|
||||
@ -21,6 +21,7 @@ export class SessionDataManager {
|
||||
return {
|
||||
frequency: radioSink.getFrequency(),
|
||||
modulation: radioSink.getModulation(),
|
||||
pan: radioSink.getPan()
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { OlStateButton } from "../../components/olstatebutton";
|
||||
import { faEarListen, faMicrophoneLines } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RadioSink } from "../../../audio/radiosink";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||
|
||||
export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKeys: string[]; onExpanded: () => void }, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@ -18,16 +19,16 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
<div
|
||||
data-receiving={props.radio.getReceiving()}
|
||||
className={`
|
||||
box-border flex cursor-pointer flex-col content-center justify-between
|
||||
gap-2 rounded-md border-2 border-transparent bg-olympus-200/30 px-4 py-3
|
||||
box-border flex flex-col content-center justify-between gap-2 rounded-md
|
||||
border-2 border-transparent bg-olympus-200/30 px-4 py-3
|
||||
data-[receiving='true']:border-white
|
||||
`}
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
|
||||
>
|
||||
<div className="flex content-center justify-between gap-2">
|
||||
<div className="flex cursor-pointer content-center justify-between gap-2" onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}>
|
||||
<div
|
||||
className={`h-fit w-fit cursor-pointer rounded-sm py-2`}
|
||||
onClick={() => {
|
||||
@ -76,6 +77,17 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
props.radio.setFrequency(value);
|
||||
}}
|
||||
/>
|
||||
<div className="flex content-center gap-2 p-2">
|
||||
<div>Left</div>
|
||||
<OlRangeSlider
|
||||
value={props.radio.getPan() * 50 + 50}
|
||||
onChange={(ev) => {
|
||||
props.radio.setPan((Number(ev.currentTarget.value) - 50) / 50);
|
||||
}}
|
||||
className="my-auto"
|
||||
></OlRangeSlider>
|
||||
<div>Right</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<OlLabelToggle
|
||||
leftLabel="AM"
|
||||
@ -90,7 +102,9 @@ export const RadioSinkPanel = forwardRef((props: { radio: RadioSink; shortcutKey
|
||||
className="ml-auto"
|
||||
checked={props.radio.getPtt()}
|
||||
icon={faMicrophoneLines}
|
||||
onClick={() => {props.radio.setPtt(!props.radio.getPtt())}}
|
||||
onClick={() => {
|
||||
props.radio.setPtt(!props.radio.getPtt());
|
||||
}}
|
||||
tooltip="Talk on frequency"
|
||||
></OlStateButton>
|
||||
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
const WaveFile = require('wavefile').WaveFile;
|
||||
|
||||
var fs = require('fs');
|
||||
let source = fs.readFileSync('sample3.WAV');
|
||||
let wav = new WaveFile(source);
|
||||
let wavBuffer = wav.toBuffer();
|
||||
const { OpusEncoder } = require('@discordjs/opus');
|
||||
const encoder = new OpusEncoder(16000, 1);
|
||||
|
||||
let fileIndex = 0;
|
||||
let packetID = 0;
|
||||
|
||||
var udp = require("dgram");
|
||||
var udpClient = udp.createSocket("udp4");
|
||||
|
||||
let clientData = {
|
||||
ClientGuid: "AZi9CkptY0yW_C-3YmI7oQ",
|
||||
Name: "Olympus",
|
||||
Seat: 0,
|
||||
Coalition: 0,
|
||||
AllowRecord: false,
|
||||
RadioInfo: {
|
||||
radios: [
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
{
|
||||
enc: false,
|
||||
encKey: 0,
|
||||
freq: 1.0,
|
||||
modulation: 3,
|
||||
secFreq: 1.0,
|
||||
retransmit: false,
|
||||
},
|
||||
],
|
||||
unit: "",
|
||||
unitId: 0,
|
||||
iff: {
|
||||
control: 2,
|
||||
mode1: -1,
|
||||
mode2: -1,
|
||||
mode3: -1,
|
||||
mode4: false,
|
||||
mic: -1,
|
||||
status: 0,
|
||||
},
|
||||
ambient: { vol: 1.0, abType: "" },
|
||||
},
|
||||
LatLngPosition: { lat: 0.0, lng: 0.0, alt: 0.0 },
|
||||
};
|
||||
|
||||
var net = require("net");
|
||||
|
||||
var tcpClient = new net.Socket();
|
||||
|
||||
tcpClient.on("data", function (data) {
|
||||
console.log("Received: " + data);
|
||||
|
||||
});
|
||||
|
||||
tcpClient.on("close", function () {
|
||||
console.log("Connection closed");
|
||||
});
|
||||
|
||||
tcpClient.connect(5002, "localhost", function () {
|
||||
console.log("Connected");
|
||||
|
||||
setTimeout(() => {
|
||||
let SYNC = {
|
||||
Client: clientData,
|
||||
MsgType: 2,
|
||||
Version: "2.1.0.10",
|
||||
};
|
||||
let string = JSON.stringify(SYNC);
|
||||
tcpClient.write(string + "\n");
|
||||
|
||||
setInterval(() => {
|
||||
let slice = [];
|
||||
for (let i = 0; i < 16000 * 0.04; i++) {
|
||||
slice.push(wavBuffer[Math.round(fileIndex) * 2], wavBuffer[Math.round(fileIndex) * 2 + 1]);
|
||||
fileIndex += 44100 / 16000;
|
||||
}
|
||||
const encoded = encoder.encode(new Uint8Array(slice));
|
||||
|
||||
let header = [
|
||||
0, 0,
|
||||
0, 0,
|
||||
0, 0
|
||||
]
|
||||
|
||||
let encFrequency = [...doubleToByteArray(251000000)];
|
||||
let encModulation = [2];
|
||||
let encEncryption = [0];
|
||||
|
||||
let encUnitID = getBytes(100000001, 4);
|
||||
let encPacketID = getBytes(packetID, 8);
|
||||
packetID++;
|
||||
let encHops = [0];
|
||||
|
||||
let packet = [].concat(header, [...encoded], encFrequency, encModulation, encEncryption, encUnitID, encPacketID, encHops, [...Buffer.from(clientData.ClientGuid, 'utf-8')], [...Buffer.from(clientData.ClientGuid, 'utf-8')]);
|
||||
|
||||
let encPacketLen = getBytes(packet.length, 2);
|
||||
packet[0] = encPacketLen[0];
|
||||
packet[1] = encPacketLen[1];
|
||||
|
||||
let encAudioLen = getBytes(encoded.length, 2);
|
||||
packet[2] = encAudioLen[0];
|
||||
packet[3] = encAudioLen[1];
|
||||
|
||||
let frequencyAudioLen = getBytes(10, 2);
|
||||
packet[4] = frequencyAudioLen[0];
|
||||
packet[5] = frequencyAudioLen[1];
|
||||
|
||||
let data = new Uint8Array(packet);
|
||||
udpClient.send(data, 5002, "localhost", function (error) {
|
||||
if (error) {
|
||||
tcpClient.close();
|
||||
} else {
|
||||
console.log("Data sent !!!");
|
||||
}
|
||||
});
|
||||
}, 40);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
function getBytes(value, length) {
|
||||
let res = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
res.push(value & 255);
|
||||
value = value >> 8;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function doubleToByteArray(number) {
|
||||
var buffer = new ArrayBuffer(8); // JS numbers are 8 bytes long, or 64 bits
|
||||
var longNum = new Float64Array(buffer); // so equivalent to Float64
|
||||
|
||||
longNum[0] = number;
|
||||
|
||||
return Array.from(new Uint8Array(buffer));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user