Skip to content

Instantly share code, notes, and snippets.

@nattog
Last active October 22, 2022 13:31
Show Gist options
  • Save nattog/68012931581ebfddfd16339228705f70 to your computer and use it in GitHub Desktop.
Save nattog/68012931581ebfddfd16339228705f70 to your computer and use it in GitHub Desktop.
Classes to handle recording audio in browser with microphone and recording output of web audio API
import { Recorder } from "./recorder.ts";
type Nullable <T> = T | null;
export class InputRecorder {
private callback: (url: string) => void;
private _launchButton: Nullable<HTMLButtonElement>;
private _recordButton: Nullable<HTMLButtonElement>;
private _stopButton: Nullable<HTMLButtonElement>;
private _errorLabel: Nullable<HTMLDivElement>;
constructor(element: HTMLElement, callback: (url: string) => void) {
this.callback = callback;
this._launchButton = element.querySelector(".launch"); // Triggers permissions request
this._recordButton = element.querySelector(".record"); // Triggers recording start
this._stopButton = element.querySelector(".stop"); // Triggers recording end
this._errorLabel = element.querySelector(".error_label"); // Displays error message
this.checkExistingPermissions();
if (this._launchButton) {
this._launchButton.onclick = this.onLaunchClick.bind(this);
}
}
private checkExistingPermissions() {
const permissionType = 'microphone' as PermissionName; // Potential issue with Firefox
navigator.permissions.query({ name: permissionType }).then((result) => {
if (result.state === 'granted') {
this.onLaunchClick();
}
});
}
private onLaunchClick() {
navigator.mediaDevices
.getUserMedia({audio: true})
.then(this.onSuccess.bind(this), this.onError.bind(this));
}
private onSuccess(stream: MediaStream) {
const recorder = new Recorder(stream, this.callback.bind(this));
// No need for launch button anymore
this._launchButton?.parentElement?.removeChild(this._launchButton);
if (this._errorLabel) {
this._errorLabel.classList.add("hidden");
}
if (this._recordButton && this._stopButton) {
// Toggle display of recording buttons
this._recordButton.classList.remove("hidden");
this._stopButton.classList.remove("hidden");
this._recordButton.onclick = () => {
recorder.start();
if (this._recordButton && this._stopButton) {
this._recordButton.disabled = true;
this._stopButton.disabled = false;
this._recordButton.classList.add("recording");
}
}
this._stopButton.onclick = () => {
recorder.stop();
if (this._recordButton && this._stopButton) {
this._stopButton.disabled = true;
this._recordButton.disabled = false;
this._recordButton.classList.remove("recording");
}
}
}
}
private onError(err: string) {
console.error(err);
if (this._errorLabel) {
this._errorLabel.classList.remove("hidden");
}
}
}
import { Recorder } from "./recorder.ts";
export class OutputRecorder {
armed = false;
private recorder: Recorder;
private _armButton: HTMLButtonElement | null;
constructor(audioContext: AudioContext, sourceNode: AudioNode, element: HTMLElement, callback: (url: string) => void) {
const mediaStreamDestination = audioContext.createMediaStreamDestination();
sourceNode.connect(mediaStreamDestination);
this.recorder = new Recorder(mediaStreamDestination.stream, callback);
this._armButton = element.querySelector(".record");
if (this._armButton) {
this._armButton.onclick = () => {
this.armed = true;
if (this._armButton) {
this._armButton.disabled = true;
this._armButton.innerText = "Armed";
}
}
}
}
start() {
if (this.armed) {
this.recorder.start();
if (this._armButton) {
this._armButton.innerText = "Recording";
this._armButton.disabled = true;
this._armButton.classList.add("recording");
}
}
}
stop() {
if (this.armed) {
this.recorder.stop();
this.armed = false;
if (this._armButton) {
this._armButton.disabled = false;
this._armButton.classList.remove("recording");
this._armButton.innerText = "Arm output"
}
}
}
}
type OnCreateObjectURL = (value: string) => void;
export class Recorder {
private _data: Blob[] = [];
private _mediaRecorder: MediaRecorder;
constructor(stream: MediaStream, callback: OnCreateObjectURL) {
this._mediaRecorder = new MediaRecorder(stream);
this._mediaRecorder.ondataavailable = (e) => this._data.push(e.data);
this._mediaRecorder.onstop = () => {
const blob = new Blob(this._data, { "type": this.data[0].type });
this._data = []; // Ensure any future recordings don't have stale data
callback(window.URL.createObjectURL(blob)) // Return audio data in the callback
}
}
get data() {
return this._data
}
start() {
if (this._mediaRecorder.state !== "recording") {
this._mediaRecorder.start();
}
}
stop() {
if (this._mediaRecorder.state === "recording") {
this._mediaRecorder.stop();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment