Last active
October 22, 2022 13:31
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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