174 lines
4.1 KiB
TypeScript
174 lines
4.1 KiB
TypeScript
import { MountedFs } from "../FileSystem/MountedFs";
|
|
import { VirtualFile } from "../FileSystem/types";
|
|
import { SoundboardContext } from "./types";
|
|
|
|
|
|
export interface PartiaSoundNodeOptions {
|
|
vFile: VirtualFile;
|
|
volume?: number;
|
|
type: 'buffered' | 'mediaElement';
|
|
}
|
|
|
|
export interface SoundNodeOptions extends PartiaSoundNodeOptions {
|
|
volume: number;
|
|
}
|
|
|
|
export abstract class SoundNode {
|
|
|
|
protected gainNode: GainNode;
|
|
protected file?: File;
|
|
public options: SoundNodeOptions;
|
|
public readonly numberOfInputs = 0;
|
|
public readonly numberOfOutputs = 1;
|
|
protected activeSources: AudioBufferSourceNode[] = [];
|
|
protected audioContext: AudioContext;
|
|
public currentTime: number = 0;
|
|
protected readonly fs: MountedFs;
|
|
|
|
constructor(
|
|
options: PartiaSoundNodeOptions,
|
|
ctx: SoundboardContext,
|
|
) {
|
|
this.options = {
|
|
...options,
|
|
volume: options.volume ?? 100
|
|
}
|
|
this.audioContext = ctx.audioContext;
|
|
this.fs = ctx.fs;
|
|
this.gainNode = this.audioContext.createGain();
|
|
this.gainNode.gain.value = (this.options.volume / 100);
|
|
}
|
|
|
|
get isPlaying() {
|
|
return this.activeSources.length > 0;
|
|
}
|
|
|
|
get name() {
|
|
return this.options.vFile.name.replace(/\.[^/.]+$/, "");
|
|
}
|
|
|
|
get key() {
|
|
return this.options.vFile.path.join('/');
|
|
}
|
|
|
|
protected async preload() {
|
|
if (!this.file) {
|
|
this.file = await this.fs.download(this.options.vFile);
|
|
}
|
|
}
|
|
|
|
connect(destination: AudioNode) {
|
|
console.log('connecting', this, destination);
|
|
this.gainNode.connect(destination);
|
|
}
|
|
|
|
disconnect(destinationNode?: AudioNode) {
|
|
if (destinationNode) {
|
|
this.gainNode.disconnect(destinationNode);
|
|
} else {
|
|
this.gainNode.disconnect();
|
|
}
|
|
}
|
|
|
|
setVolume(volume: number) {
|
|
this.gainNode.gain.value = volume / 100;
|
|
}
|
|
|
|
getVolume() {
|
|
return this.gainNode.gain.value * 100;
|
|
}
|
|
|
|
abstract play(): Promise<void>;
|
|
abstract stop(): Promise<void>;
|
|
}
|
|
|
|
export class BufferedSoundNode extends SoundNode {
|
|
private buffer?: AudioBuffer;
|
|
|
|
async preload() {
|
|
await super.preload();
|
|
if (!this.buffer) {
|
|
const arrayBuffer = await this.file!.arrayBuffer();
|
|
this.buffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
}
|
|
}
|
|
|
|
async play() {
|
|
await this.preload();
|
|
const source = this.audioContext.createBufferSource();
|
|
source.buffer = this.buffer!;
|
|
source.connect(this.gainNode);
|
|
source.start();
|
|
this.audioContext.resume();
|
|
this.activeSources.push(source);
|
|
source.addEventListener('ended', () => {
|
|
const index = this.activeSources.indexOf(source);
|
|
if (index !== -1) {
|
|
this.activeSources.splice(index, 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
async stop() {
|
|
this.activeSources.forEach((source) => source.stop());
|
|
this.activeSources = [];
|
|
}
|
|
}
|
|
|
|
export class MediaElementSoundNode extends SoundNode {
|
|
private mediaElement?: HTMLMediaElement;
|
|
private source?: MediaElementAudioSourceNode;
|
|
private _isPlaying = false;
|
|
|
|
async preload() {
|
|
await super.preload();
|
|
if (!this.mediaElement) {
|
|
this.mediaElement = document.createElement('audio');
|
|
this.mediaElement.src = URL.createObjectURL(this.file!);
|
|
this.source = this.audioContext.createMediaElementSource(this.mediaElement);
|
|
this.source.connect(this.gainNode);
|
|
this.audioContext.resume();
|
|
this.mediaElement.addEventListener('ended', ()=> {
|
|
this._isPlaying = false;
|
|
});
|
|
this.mediaElement.addEventListener('playing', (event) => {
|
|
console.log(event);
|
|
this._isPlaying = true;
|
|
});
|
|
this.mediaElement.addEventListener('pause', () => {
|
|
this._isPlaying = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
get isPlaying() {
|
|
return this._isPlaying;
|
|
}
|
|
|
|
get duration() {
|
|
return this.mediaElement!.duration;
|
|
}
|
|
|
|
async play() {
|
|
await this.preload();
|
|
await this.mediaElement?.play();
|
|
}
|
|
|
|
async stop() {
|
|
await this.mediaElement?.pause();
|
|
}
|
|
|
|
pause() {
|
|
this.mediaElement?.pause();
|
|
}
|
|
|
|
resume() {
|
|
this.mediaElement?.play();
|
|
}
|
|
|
|
seek(time: number) {
|
|
if (this.mediaElement) {
|
|
this.mediaElement.currentTime = time;
|
|
}
|
|
}
|
|
} |