apphouse
Version:
Component library for React that uses observable state management and theme-able components.
375 lines (343 loc) • 10.2 kB
text/typescript
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));
};
}