Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@
},
"lint-staged": {
"*.{ts,js}": [
"biome format",
"biome lint --fix"
"mise run fix"
]
}
}
82 changes: 82 additions & 0 deletions src/analyser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
export class Analyser {
readonly node: AnalyserNode;
readonly #data: Uint8Array<ArrayBuffer>;

constructor(
readonly context: AudioContext = new AudioContext(),
readonly options: AnalyserOptions = {},
) {
this.node = this.context.createAnalyser();

if (options?.fftSize) {
this.node.fftSize = options.fftSize;
}

if (options?.minDecibels) {
this.node.minDecibels = options.minDecibels;
}

if (options?.maxDecibels) {
this.node.maxDecibels = options.maxDecibels;
}

if (options?.smoothingTimeConstant) {
this.node.smoothingTimeConstant = options.smoothingTimeConstant;
}

if (options?.channelCount) {
this.node.channelCount = options.channelCount;
}

if (options?.channelInterpretation) {
this.node.channelInterpretation = options.channelInterpretation;
}

if (options?.channelCountMode) {
this.node.channelCountMode = options.channelCountMode;
}

this.#data = new Uint8Array(this.node.frequencyBinCount);
}

/**
* Returns the frequency data provided by the default analyzer
*/
get frequencyData(): Uint8Array {
this.node.getByteFrequencyData(this.#data);
return this.#data;
}

/**
* Retrieves the current volume (average of amplitude^2)
*/
get volume(): number {
const data = this.frequencyData;

let sum = 0;

for (const amplitude of data) {
sum += amplitude * amplitude;
}

return Math.sqrt(sum / data.length);
}

connect(
destinationNode: AudioNode,
output?: number,
input?: number,
): AudioNode;
connect(destinationParam: AudioParam, output?: number): void;
connect(
destination: AudioNode | AudioParam,
output?: number,
input?: number,
): AudioNode | undefined {
if (destination instanceof AudioNode) {
return this.node.connect(destination, output, input);
} else {
this.node.connect(destination, output);
}
}
}
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./analyser.js";
export * from "./monitor.js";
export * from "./recorder.js";
export * from "./types.js";
export * from "./utils.js";
52 changes: 52 additions & 0 deletions src/monitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Analyser } from "./analyser.js";

export interface MonitorOptions {
context?: AudioContext;
defaultAnalyser?: Analyser;
}

export class Monitor {
readonly context: AudioContext;
readonly source: MediaStreamAudioSourceNode;
readonly destination: AudioDestinationNode;
readonly analyser: Analyser;
readonly #options: MonitorOptions;

constructor(
readonly stream: MediaStream,
options: MonitorOptions,
) {
this.#options = options;
this.context = this.#options.context ?? new AudioContext();
this.destination = this.context.destination;
this.source = this.context.createMediaStreamSource(this.stream);
this.analyser = this.#options.defaultAnalyser ?? new Analyser(this.context);
this.source.connect(this.analyser.node);
this.analyser.connect(this.destination);
}

/**
* Retrieves the current volume (average of amplitude^2)
*/
get volume() {
return this.analyser.volume;
}

/**
* Retrieves the current analyzer's frequency data
*/
get frequencyData() {
return this.analyser.frequencyData;
}

/**
* Adds a custom audio worklet to the current audio context
*
* @param name The registered name of the worklet
* @param path The absolute path to the worklet
*/
async installWorklet(name: string, path: string): Promise<AudioWorkletNode> {
await this.context.audioWorklet.addModule(path);
return new AudioWorkletNode(this.context, name);
}
}
Loading