mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added track controls
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript-eslint": "^7.14.1",
|
"typescript-eslint": "^7.14.1",
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0",
|
||||||
|
"web-audio-peak-meter": "^3.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ export abstract class AudioSource {
|
|||||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect(sinkToDisconnect?: AudioSink) {
|
||||||
this.getNode().disconnect();
|
if (sinkToDisconnect !== undefined) {
|
||||||
|
this.getNode().disconnect(sinkToDisconnect.getNode());
|
||||||
|
this.#connectedTo = this.#connectedTo.filter((sink) => sink != sinkToDisconnect);
|
||||||
|
} else {
|
||||||
|
this.getNode().disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { AudioSource } from "./audiosource";
|
import { AudioSource } from "./audiosource";
|
||||||
import { bufferToF32Planar } from "../other/utils";
|
|
||||||
import { getApp } from "../olympusapp";
|
import { getApp } from "../olympusapp";
|
||||||
|
import {WebAudioPeakMeter} from 'web-audio-peak-meter';
|
||||||
|
|
||||||
export class FileSource extends AudioSource {
|
export class FileSource extends AudioSource {
|
||||||
#gainNode: GainNode;
|
#gainNode: GainNode;
|
||||||
#file: File | null = null;
|
#file: File | null = null;
|
||||||
#source: AudioBufferSourceNode;
|
#source: AudioBufferSourceNode;
|
||||||
|
#duration: number = 0;
|
||||||
|
#currentPosition: number = 0;
|
||||||
|
#updateInterval: any;
|
||||||
|
#lastUpdateTime: number = 0;
|
||||||
|
#playing = false;
|
||||||
|
#audioBuffer: AudioBuffer;
|
||||||
|
#restartTimeout: any;
|
||||||
|
#looping = false;
|
||||||
|
#meter: WebAudioPeakMeter;
|
||||||
|
#volume: number = 1.0;
|
||||||
|
|
||||||
constructor(file) {
|
constructor(file) {
|
||||||
super();
|
super();
|
||||||
@@ -14,13 +24,7 @@ export class FileSource extends AudioSource {
|
|||||||
this.setName(this.#file?.name ?? "N/A");
|
this.setName(this.#file?.name ?? "N/A");
|
||||||
|
|
||||||
this.#gainNode = getApp().getAudioManager().getAudioContext().createGain();
|
this.#gainNode = getApp().getAudioManager().getAudioContext().createGain();
|
||||||
}
|
|
||||||
|
|
||||||
getNode() {
|
|
||||||
return this.#gainNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
play() {
|
|
||||||
if (!this.#file) {
|
if (!this.#file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -28,23 +32,108 @@ export class FileSource extends AudioSource {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
var contents = e.target?.result;
|
var contents = e.target?.result;
|
||||||
if (contents) {
|
if (contents) {
|
||||||
getApp().getAudioManager().getAudioContext().decodeAudioData(contents as ArrayBuffer, (arrayBuffer) => {
|
getApp()
|
||||||
this.#source = getApp().getAudioManager().getAudioContext().createBufferSource();
|
.getAudioManager()
|
||||||
this.#source.buffer = arrayBuffer;
|
.getAudioContext()
|
||||||
this.#source.connect(this.#gainNode);
|
.decodeAudioData(contents as ArrayBuffer, (audioBuffer) => {
|
||||||
this.#source.loop = true;
|
this.#audioBuffer = audioBuffer;
|
||||||
this.#source.start();
|
this.#duration = audioBuffer.duration;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(this.#file);
|
reader.readAsArrayBuffer(this.#file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNode() {
|
||||||
|
return this.#gainNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
play() {
|
||||||
|
this.#source = getApp().getAudioManager().getAudioContext().createBufferSource();
|
||||||
|
this.#source.buffer = this.#audioBuffer;
|
||||||
|
this.#source.connect(this.#gainNode);
|
||||||
|
this.#source.loop = this.#looping;
|
||||||
|
|
||||||
|
this.#meter = new WebAudioPeakMeter(this.#gainNode, document.createElement('div'));
|
||||||
|
|
||||||
|
this.#source.start(0, this.#currentPosition);
|
||||||
|
this.#playing = true;
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
this.#lastUpdateTime = now;
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||||
|
|
||||||
|
this.#updateInterval = setInterval(() => {
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
this.#currentPosition += now - this.#lastUpdateTime;
|
||||||
|
this.#lastUpdateTime = now;
|
||||||
|
|
||||||
|
if (this.#currentPosition > this.#duration) {
|
||||||
|
this.#currentPosition = 0;
|
||||||
|
if (!this.#looping) this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.#source.stop();
|
this.#source.stop();
|
||||||
|
this.#playing = false;
|
||||||
|
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
this.#currentPosition += now - this.#lastUpdateTime;
|
||||||
|
clearInterval(this.#updateInterval);
|
||||||
|
|
||||||
|
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
setGain(gain) {
|
setGain(gain) {
|
||||||
this.#gainNode.gain.setValueAtTime(gain, getApp().getAudioManager().getAudioContext().currentTime);
|
this.#gainNode.gain.setValueAtTime(gain, getApp().getAudioManager().getAudioContext().currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlaying() {
|
||||||
|
return this.#playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPosition() {
|
||||||
|
return this.#currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDuration() {
|
||||||
|
return this.#duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPosition(percentPosition) {
|
||||||
|
if (this.#playing) {
|
||||||
|
clearTimeout(this.#restartTimeout);
|
||||||
|
this.#restartTimeout = setTimeout(() => this.play(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stop();
|
||||||
|
this.#currentPosition = (percentPosition / 100) * this.#duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLooping(looping) {
|
||||||
|
this.#looping = looping;
|
||||||
|
if (this.#source) this.#source.loop = looping;
|
||||||
|
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLooping() {
|
||||||
|
return this.#looping;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMeter() {
|
||||||
|
return this.#meter;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(volume) {
|
||||||
|
this.#volume = volume;
|
||||||
|
this.#gainNode.gain.exponentialRampToValueAtTime(volume, getApp().getAudioManager().getAudioContext().currentTime + 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVolume() {
|
||||||
|
return this.#volume;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faArrowRight, faCheckCircle, faExternalLink, faLink, faUnlink } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export function Card(props: { children?: JSX.Element | JSX.Element[]; className?: string }) {
|
export function Card(props: { children?: JSX.Element | JSX.Element[]; className?: string }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { OlStateButton } from "../../components/olstatebutton";
|
import { OlStateButton } from "../../components/olstatebutton";
|
||||||
import { faPlay, faRepeat } from "@fortawesome/free-solid-svg-icons";
|
import { faPause, faPlay, faRepeat, faStop } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { getApp } from "../../../olympusapp";
|
import { getApp } from "../../../olympusapp";
|
||||||
import { AudioSource } from "../../../audio/audiosource";
|
import { AudioSource } from "../../../audio/audiosource";
|
||||||
import { FaTrash, FaVolumeHigh } from "react-icons/fa6";
|
import { FaTrash, FaVolumeHigh } from "react-icons/fa6";
|
||||||
import { OlRangeSlider } from "../../components/olrangeslider";
|
import { OlRangeSlider } from "../../components/olrangeslider";
|
||||||
import { FaUnlink } from "react-icons/fa";
|
import { FaUnlink } from "react-icons/fa";
|
||||||
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
||||||
|
import { FileSource } from "../../../audio/filesource";
|
||||||
|
|
||||||
export function AudioSourcePanel(props: { source: AudioSource }) {
|
export function AudioSourcePanel(props: { source: AudioSource }) {
|
||||||
|
const [meterLevel, setMeterLevel] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
// TODO apply to all sources
|
||||||
|
if (props.source instanceof FileSource) {
|
||||||
|
setMeterLevel(props.source.getMeter().getPeaks().current[0]);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
@@ -17,26 +29,61 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span>{props.source.getName()}</span>
|
<span>{props.source.getName()}</span>
|
||||||
{props.source.getName() != "Microphone" && (
|
{props.source instanceof FileSource && (
|
||||||
<div className="flex gap-4 py-2">
|
<div className="flex flex-col gap-2">
|
||||||
<OlStateButton
|
<div className="flex gap-4">
|
||||||
checked={false}
|
<OlStateButton
|
||||||
icon={faPlay}
|
checked={false}
|
||||||
onClick={() => {
|
icon={props.source.getPlaying() ? faPause : faPlay}
|
||||||
props.source.play();
|
onClick={() => {
|
||||||
}}
|
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.stop() : props.source.play();
|
||||||
tooltip="Play file"
|
}}
|
||||||
></OlStateButton>
|
tooltip="Play file"
|
||||||
<OlRangeSlider
|
></OlStateButton>
|
||||||
value={50}
|
<OlRangeSlider
|
||||||
onChange={(ev) => {
|
value={(props.source.getCurrentPosition() / props.source.getDuration()) * 100}
|
||||||
//let setting = props.setting;
|
onChange={(ev) => {
|
||||||
//setting.volume = parseFloat(ev.currentTarget.value) / 100;
|
if (props.source instanceof FileSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
|
||||||
//props.updateSetting(setting);
|
}}
|
||||||
}}
|
className="my-auto"
|
||||||
className="my-auto"
|
/>
|
||||||
/>
|
<OlStateButton
|
||||||
<OlStateButton checked={false} icon={faRepeat} onClick={() => {}} tooltip="Loop"></OlStateButton>
|
checked={props.source.getLooping()}
|
||||||
|
icon={faRepeat}
|
||||||
|
onClick={() => {
|
||||||
|
if (props.source instanceof FileSource) props.source.setLooping(!props.source.getLooping());
|
||||||
|
}}
|
||||||
|
tooltip="Loop"
|
||||||
|
></OlStateButton>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="h-[40px] min-w-[40px] p-2">
|
||||||
|
<FaVolumeHigh className="h-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex w-full flex-col gap-3">
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
absolute top-[18px] flex h-2 min-w-full translate-y-[-5px]
|
||||||
|
flex-row border-gray-500
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ minWidth: `${meterLevel * 100}%` }}
|
||||||
|
className={`rounded-full bg-gray-200`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<OlRangeSlider
|
||||||
|
value={props.source.getVolume() * 100}
|
||||||
|
onChange={(ev) => {
|
||||||
|
if (props.source instanceof FileSource) props.source.setVolume(parseFloat(ev.currentTarget.value) / 100);
|
||||||
|
}}
|
||||||
|
className="absolute top-[18px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-[40px] min-w-[40px] p-2">
|
||||||
|
<span>{Math.round(props.source.getVolume() * 100)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm">Connected to:</span>
|
<span className="text-sm">Connected to:</span>
|
||||||
@@ -45,7 +92,7 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
{sink.getName()}
|
{sink.getName()}
|
||||||
<FaUnlink></FaUnlink>
|
<FaUnlink className="cursor-pointer" onClick={() => props.source.disconnect(sink)}></FaUnlink>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -55,9 +102,15 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
|
|||||||
.getSinks()
|
.getSinks()
|
||||||
.filter((sink) => !props.source.getConnectedTo().includes(sink))
|
.filter((sink) => !props.source.getConnectedTo().includes(sink))
|
||||||
.map((sink) => {
|
.map((sink) => {
|
||||||
return <OlDropdownItem onClick={() => {
|
return (
|
||||||
props.source.connect(sink);
|
<OlDropdownItem
|
||||||
}}>{sink.getName()}</OlDropdownItem>;
|
onClick={() => {
|
||||||
|
props.source.connect(sink);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sink.getName()}
|
||||||
|
</OlDropdownItem>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</OlDropdown>
|
</OlDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -851,6 +851,7 @@ export abstract class Unit extends CustomMarker {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* Temporarily removed until more work can be done for external sounds
|
||||||
contextActionSet.addContextAction(
|
contextActionSet.addContextAction(
|
||||||
this,
|
this,
|
||||||
"speaker",
|
"speaker",
|
||||||
@@ -863,6 +864,7 @@ export abstract class Unit extends CustomMarker {
|
|||||||
},
|
},
|
||||||
{ executeImmediately: true }
|
{ executeImmediately: true }
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
contextActionSet.addDefaultContextAction(
|
contextActionSet.addDefaultContextAction(
|
||||||
this,
|
this,
|
||||||
|
|||||||
Reference in New Issue
Block a user