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; abstract stop(): Promise; } 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; } } }