UNPKG

apphouse

Version:

Component library for React that uses observable state management and theme-able components.

375 lines (343 loc) 10.2 kB
import { action, computed, makeObservable, observable } from "mobx"; import { audioLog } from "./AudioLog"; // import ImpulseResponseAudio from "../samples/impulse-responses/echohall.wav"; const DEFAULT_EQ_VALUE = 1; const DEFAULT_VOLUME_VALUE = 0.5; export default class AudioSource { analyzerNode?: AnalyserNode; audioContext?: AudioContext; // AudioContext; baseEq?: BiquadFilterNode; // to control low frequency (bass) bassEqValue: number; filter?: BiquadFilterNode; gainNode?: GainNode; // to control of volume liveFeedback: boolean; midEq?: BiquadFilterNode; // to control medium frequency midEqValue: number; source: any; trebleEq?: BiquadFilterNode; // to control high frequency trebleEqValue: number; volumeValue: number; constructor() { this.analyzerNode = undefined; this.audioContext = undefined; this.baseEq = undefined; this.bassEqValue = DEFAULT_EQ_VALUE; this.filter = undefined; this.gainNode = undefined; this.liveFeedback = false; this.midEq = undefined; this.midEqValue = DEFAULT_EQ_VALUE; this.source = null; this.trebleEq = undefined; this.trebleEqValue = DEFAULT_EQ_VALUE; this.volumeValue = DEFAULT_VOLUME_VALUE; this.initContext(); makeObservable(this, { analyzerNode: observable, audioContext: observable, baseEq: observable, bassEqValue: observable, filter: observable, gainNode: observable, hasSource: computed, initContext: action, initCustomFilter: action, initWithMediaFilepath: action, initWithStream: action, liveFeedback: observable, getMediaStreamDestination: action, midEq: observable, midEqValue: observable, resetControls: action, setBassEq: action, setMidEq: action, setTrebleEq: action, setVolume: action, connectDestinationStream: action, source: observable, toggleLiveFeedback: action, trebleEq: observable, trebleEqValue: observable, volumeValue: observable, }); } get analyzer(): AnalyserNode | undefined { return this.analyzerNode; } get hasSource() { return ( this.audioContext && this.audioContext.state !== "suspended" ); } get isSuspended() { return ( this.audioContext && this.audioContext.state === "suspended" ); } initContext = () => { if (!this.audioContext && typeof window !== "undefined") { this.audioContext = new AudioContext(); audioLog.logAudioSource("Created audio context"); } if (this.audioContext) { this.analyzerNode = new AnalyserNode(this.audioContext, { fftSize: 256, }); this.gainNode = new GainNode(this.audioContext, { gain: this.volumeValue, }); this.baseEq = new BiquadFilterNode(this.audioContext, { type: "lowshelf", frequency: 500, gain: this.bassEqValue, }); this.midEq = new BiquadFilterNode(this.audioContext, { type: "peaking", frequency: 1500, Q: Math.SQRT1_2, gain: this.midEqValue, }); this.trebleEq = new BiquadFilterNode(this.audioContext, { type: "highshelf", frequency: 3000, Q: Math.SQRT1_2, gain: this.trebleEqValue, }); } }; initCustomFilter = ( type: BiquadFilterType, frequency?: number, Q?: number, gain?: number, detune?: number, ) => { if (this.audioContext) { this.filter = new BiquadFilterNode(this.audioContext, { type, frequency, Q, gain, detune, }); audioLog.logAudioSource("Initialized custom filter"); } }; initWithMediaFilepath = async ( filepath: string, ): Promise<AudioBuffer | null> => { if (this.audioContext) { const response = await fetch(filepath); const arrayBuffer = await response.arrayBuffer(); if (arrayBuffer) { const audioBuffer = this.audioContext.decodeAudioData( arrayBuffer, ); this.source = audioBuffer; audioLog.logAudioSource("Set audio buffer", audioBuffer); return this.source; } } return null; }; /** * initWithStream initializes the stream and connects all sources nodes to that * stream * @param stream Media stream * @param output A flag for live feedback, if false, no live feedback will play */ initWithStream = async ( stream: MediaStream, liveFeedback?: boolean, ): Promise<MediaStreamAudioSourceNode | undefined> => { if (this.audioContext && this.isSuspended) { await this.audioContext.resume; } if ( this.audioContext && this.baseEq && this.midEq && this.trebleEq && this.gainNode && this.analyzerNode ) { this.source = this.audioContext.createMediaStreamSource(stream); audioLog.logAudioSource( "Source set to MediaStreamAudioSourceNode", ); if (this.source && this.filter) { // connect to custom filter if there's any this.source.connect(this.filter); audioLog.logAudioSource("Connected to custom filters nodes"); } if (this.source) { this.source .connect(this.baseEq) .connect(this.midEq) .connect(this.trebleEq) .connect(this.gainNode) .connect(this.analyzerNode); audioLog.logAudioSource("Connected audio nodes"); if (liveFeedback === true) { this.toggleLiveFeedback(true); } return this.source; } } else { console.warn( "No audio context found - unable to initialize with stream", ); } return undefined; }; toggleLiveFeedback = async (value?: boolean) => { if (!this.source || !this.audioContext) { console.warn( "No source or audio context when toggling live feedback", ); return; } if (value !== undefined) { this.liveFeedback = value; } else { this.liveFeedback = !this.liveFeedback; } if (this.source) { if (this.liveFeedback === true && this.audioContext) { this.source.connect(this.audioContext.destination); audioLog.logAudioSource( "Connected audio context to output destination", ); } if (this.liveFeedback === false && this.audioContext) { this.source.disconnect(this.audioContext.destination); audioLog.logAudioSource( "Disconnect audio context from output destination", ); } } }; setVolume = (value: number) => { if (this.gainNode && this.audioContext) { this.volumeValue = value; this.gainNode.gain.setTargetAtTime( value, this.audioContext.currentTime, 0.01, ); audioLog.logAudioSource("Volume", value); } else { console.warn( "Unable to set volume: No gain node and audio context available", ); } }; setBassEq = (value: number) => { if (this.baseEq && this.audioContext) { this.bassEqValue = value; this.baseEq.gain.setTargetAtTime( value, this.audioContext.currentTime, 0.01, ); } else { console.warn( "Unable to set volume: No baseEq gain and audio context available", ); } }; setMidEq = (value: number) => { if (this.midEq && this.audioContext) { this.midEqValue = value; this.midEq.gain.setTargetAtTime( value, this.audioContext.currentTime, 0.01, ); } else { console.warn( "Unable to set volume: No midEq gain and audio context available", ); } }; setTrebleEq = (value: number) => { if (this.trebleEq && this.audioContext) { this.trebleEqValue = value; this.trebleEq.gain.setTargetAtTime( value, this.audioContext.currentTime, 0.01, ); } else { console.warn( "Unable to set volume: No trebleEq gain and audio context available", ); } }; getMediaStreamDestination = (): | MediaStreamAudioDestinationNode | undefined => { if (!this.audioContext) { console.warn( "No audio context found when trying to get media stream", ); return undefined; } if (this.audioContext) { const destination = this.audioContext.createMediaStreamDestination(); audioLog.logAudioSource( "media stream requested", destination.stream, ); return destination; } return undefined; }; connectDestinationStream = ( stream: MediaStreamAudioDestinationNode, ) => { if (this.source) { this.source.connect(stream); audioLog.logAudioSource("Connected destination stream"); } }; resetControls = ( volume?: number, midEqValue?: number, bassEqValue?: number, trebleEqValue?: number, ) => { this.setVolume(volume || DEFAULT_VOLUME_VALUE); this.setBassEq(bassEqValue || DEFAULT_EQ_VALUE); this.setMidEq(midEqValue || DEFAULT_EQ_VALUE); this.setTrebleEq(trebleEqValue || DEFAULT_EQ_VALUE); }; addReverb = async () => { // if (this.audioContext) { // const audioBuffer = this.source; // this.source = this.audioContext.createBufferSource(); // this.source.buffer = audioBuffer; // let convolver = this.audioContext.createConvolver(); // convolver.buffer = await this.audioContext.decodeAudioData( // await (await fetch(ImpulseResponseAudio)).arrayBuffer(), // ); // this.source // .connect(convolver) // .connect(this.gainNode) // .connect(this.audioContext.destination); // } }; static getAudioBuffer = async ( blob: Blob, ): Promise<AudioBuffer | undefined> => { const url = URL.createObjectURL(blob); const audioContext = new AudioContext(); const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); if (arrayBuffer) { const audioBuffer = audioContext.decodeAudioData(arrayBuffer); return audioBuffer; } return new Promise((resolve) => resolve(undefined)); }; }