Added track controls

This commit is contained in:
Pax1601
2024-09-05 22:35:08 +02:00
parent 9bbcdac704
commit 9c2ce526d3
6 changed files with 194 additions and 43 deletions

View File

@@ -11,8 +11,14 @@ export abstract class AudioSource {
document.dispatchEvent(new CustomEvent("audioSourcesUpdated"));
}
disconnect() {
this.getNode().disconnect();
disconnect(sinkToDisconnect?: AudioSink) {
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"));
}

View File

@@ -1,11 +1,21 @@
import { AudioSource } from "./audiosource";
import { bufferToF32Planar } from "../other/utils";
import { getApp } from "../olympusapp";
import {WebAudioPeakMeter} from 'web-audio-peak-meter';
export class FileSource extends AudioSource {
#gainNode: GainNode;
#file: File | null = null;
#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) {
super();
@@ -14,13 +24,7 @@ export class FileSource extends AudioSource {
this.setName(this.#file?.name ?? "N/A");
this.#gainNode = getApp().getAudioManager().getAudioContext().createGain();
}
getNode() {
return this.#gainNode;
}
play() {
if (!this.#file) {
return;
}
@@ -28,23 +32,108 @@ export class FileSource extends AudioSource {
reader.onload = (e) => {
var contents = e.target?.result;
if (contents) {
getApp().getAudioManager().getAudioContext().decodeAudioData(contents as ArrayBuffer, (arrayBuffer) => {
this.#source = getApp().getAudioManager().getAudioContext().createBufferSource();
this.#source.buffer = arrayBuffer;
this.#source.connect(this.#gainNode);
this.#source.loop = true;
this.#source.start();
});
getApp()
.getAudioManager()
.getAudioContext()
.decodeAudioData(contents as ArrayBuffer, (audioBuffer) => {
this.#audioBuffer = audioBuffer;
this.#duration = audioBuffer.duration;
});
}
};
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() {
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) {
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;
}
}

View File

@@ -1,6 +1,6 @@
import React from "react";
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 }) {
return (

View File

@@ -1,14 +1,26 @@
import React, { useEffect, useState } from "react";
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 { AudioSource } from "../../../audio/audiosource";
import { FaTrash, FaVolumeHigh } from "react-icons/fa6";
import { OlRangeSlider } from "../../components/olrangeslider";
import { FaUnlink } from "react-icons/fa";
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
import { FileSource } from "../../../audio/filesource";
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 (
<div
className={`
@@ -17,26 +29,61 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
`}
>
<span>{props.source.getName()}</span>
{props.source.getName() != "Microphone" && (
<div className="flex gap-4 py-2">
<OlStateButton
checked={false}
icon={faPlay}
onClick={() => {
props.source.play();
}}
tooltip="Play file"
></OlStateButton>
<OlRangeSlider
value={50}
onChange={(ev) => {
//let setting = props.setting;
//setting.volume = parseFloat(ev.currentTarget.value) / 100;
//props.updateSetting(setting);
}}
className="my-auto"
/>
<OlStateButton checked={false} icon={faRepeat} onClick={() => {}} tooltip="Loop"></OlStateButton>
{props.source instanceof FileSource && (
<div className="flex flex-col gap-2">
<div className="flex gap-4">
<OlStateButton
checked={false}
icon={props.source.getPlaying() ? faPause : faPlay}
onClick={() => {
if (props.source instanceof FileSource) props.source.getPlaying() ? props.source.stop() : props.source.play();
}}
tooltip="Play file"
></OlStateButton>
<OlRangeSlider
value={(props.source.getCurrentPosition() / props.source.getDuration()) * 100}
onChange={(ev) => {
if (props.source instanceof FileSource) props.source.setCurrentPosition(parseFloat(ev.currentTarget.value));
}}
className="my-auto"
/>
<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>
)}
<span className="text-sm">Connected to:</span>
@@ -45,7 +92,7 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
return (
<div className="flex justify-between text-sm">
{sink.getName()}
<FaUnlink></FaUnlink>
<FaUnlink className="cursor-pointer" onClick={() => props.source.disconnect(sink)}></FaUnlink>
</div>
);
})}
@@ -55,9 +102,15 @@ export function AudioSourcePanel(props: { source: AudioSource }) {
.getSinks()
.filter((sink) => !props.source.getConnectedTo().includes(sink))
.map((sink) => {
return <OlDropdownItem onClick={() => {
props.source.connect(sink);
}}>{sink.getName()}</OlDropdownItem>;
return (
<OlDropdownItem
onClick={() => {
props.source.connect(sink);
}}
>
{sink.getName()}
</OlDropdownItem>
);
})}
</OlDropdown>
</div>

View File

@@ -851,6 +851,7 @@ export abstract class Unit extends CustomMarker {
}
);
/* Temporarily removed until more work can be done for external sounds
contextActionSet.addContextAction(
this,
"speaker",
@@ -863,6 +864,7 @@ export abstract class Unit extends CustomMarker {
},
{ executeImmediately: true }
);
*/
contextActionSet.addDefaultContextAction(
this,