vocaroo / simple-audio-recorder

A simple web audio recording library with encoding to MP3 and optional streaming/chunked output. Now with React hook and component!
48 stars 6 forks source link
audio mp3 recorder web

Simple Audio Recorder

A simple web audio recording library with encoding to MP3 (using lamejs) and optional streaming/chunked output. Made by Vocaroo, the quick and easy online voice recorder!

Now including both a vanilla-js version and an easy to use react hook and component!

Vanilla-js

import AudioRecorder from "simple-audio-recorder";

AudioRecorder.preload("mp3worker.js");

let recorder = new AudioRecorder();

recorder.start().then(() => {
    console.log("Recording started...");
}).catch(error => {
    console.log(error);
});

recorder.stop().then(mp3Blob => {
    console.log("Recording stopped...");

    const newAudio = document.createElement("audio");
    newAudio.src = URL.createObjectURL(mp3Blob);
    newAudio.controls = true;
    document.body.append(newAudio);
}).catch(error => {
    console.log(error);
});

React hook and component

import {SimpleAudioRecorder, useSimpleAudioRecorder} from "simple-audio-recorder/react";

export default function App() {
    const recorder = useSimpleAudioRecorder({workerUrl : "mp3worker.js"});

    const viewInitial = <button onClick={recorder.start}>start recording</button>;
    const viewRecording = <button onClick={recorder.stop}>stop recording</button>;
    const viewError = (<>{viewInitial} <div>Error occurred! {recorder.errorStr}</div></>);

    return (
        <div>
            <SimpleAudioRecorder
                {...recorder.getProps()}
                viewInitial={viewInitial}
                viewRecording={viewRecording}
                viewError={viewError}/>

            {recorder.mp3Urls.map(url => 
                <audio key={url} src={url} controls/>
            )}
        </div>
    );
}

Examples

On codepen

Included in the project

To run the built in examples in the ./examples/ directory, start a dev server from the project root and then navigate to them.

Or start developing with:

yarn install
yarn start

...or whatever the npm equivalant is.

Usage

Including

yarn add simple-audio-recorder
import AudioRecorder from "simple-audio-recorder";

Alternatively, just use a script tag:

<script type="text/javascript" src="https://github.com/vocaroo/simple-audio-recorder/raw/master/audiorecorder.js"></script>

Also, you must make sure that you distribute the web worker file "mp3worker.js" along with your application.

Preload the MP3 encoder worker:

// This is a static method.
// You should preload the worker immediately on page load to enable recording to start quickly
AudioRecorder.preload("./mp3worker.js");

Create an audio recorder

let recorder = new AudioRecorder({
    recordingGain : 1, // Initial recording volume
    encoderBitRate : 96, // MP3 encoding bit rate
    streaming : false, // Data will be returned in chunks (ondataavailable callback) as it is encoded,
                        // rather than at the end as one large blob
    streamBufferSize : 50000, // Size of encoded mp3 data chunks returned by ondataavailable, if streaming is enabled
    constraints : { // Optional audio constraints, see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
        channelCount : 1, // Set to 2 to hint for stereo if it's available, or leave as 1 to force mono at all times
        autoGainControl : true,
        echoCancellation : true,
        noiseSuppression : true
    },

    // Used for debugging only. Force using the older script processor instead of AudioWorklet.
    // forceScriptProcessor : true
});

Use promises to start and stop recording

recorder.start().then(() => {
    console.log("Recording started...");
}).catch(error => {
    console.log(error);
});

recorder.stop().then(mp3Blob => {
    // Do something with the mp3 Blob!
}).catch(error => {
    console.log(error);
});

Or use events

recorder.onstart = () => {
    console.log("Recording started...");
};

recorder.onstop = (mp3Blob) => {
    // Do something with the mp3 Blob!
    // When using onstop, mp3Blob could in rare cases be null if nothing was recorded
    // (with the Promise API, that would be a stop() promise rejection)
};

recorder.onerror = (error) => {
    console.log(error);
};

// if onerror is set, start and stop won't return a promise
recorder.start();

// later...
recorder.stop();

Handle encoded data chunks

Want to receive encoded data chunks as they are produced? Useful for streaming uploads to a remote server.

let recorder = new AudioRecorder({
    streaming : true,
    streamBufferSize : 50000
});

let audioChunks = [];

recorder.ondataavailable = (data) => {
    // 50 KB of MP3 data received!
    audioChunks.push(data);
};

recorder.start();

// No mp3Blob will be received either with the promise API or via recorder.onstop if streaming is enabled.
recorder.stop().then(() => {
    // ...do something with all the chunks that were received by ondataavailable
    let mp3Blob = new Blob(audioChunks, {type : "audio/mpeg"});
});

Other functions/attributes

recorder.start(paused = false); // Supports starting in paused mode
recorder.pause();
recorder.resume();

recorder.setRecordingGain(gain); // Change the volume while recording is in progress (0.0 to 1.0)

recorder.time; // Access the current recorded duration in milliseconds. Time pauses when recording is paused.

// Get the amount of data remaining to be encoded
// Will only be much above zero on very slow systems as mp3 encoding is quite fast.
// A large value indicates there might be a delay between calling stop() and getting the mp3Blob
recorder.getEncodingQueueSize();

AudioRecorder.isRecordingSupported(); // Static method. Does this browser support getUserMedia?

Error handling

Error handling can be done either via promises and catching errors, or via the onerror event handler if it is set.

Errors

These are named via the error.name property

React hook and component

Please see the react hook and component example for a working example of usage.

Importing

import {
    useSimpleAudioRecorder,
    SimpleAudioRecorder,
    preloadWorker,
    RecorderStates
} from "simple-audio-recorder/react"

useSimpleAudioRecorder hook

const {
    error, // Any current error object, or null
    errorStr, // Error object as string, or null
    time, // Current recorded time in milliseconds
    countdownTimeLeft, // Time left of the countdown before recording will start, if one was set
    mp3Blobs, // List of all recordings as a blob
    mp3Urls, // List of all recordings as URLs (created with URL.createObjectURL)
    mp3Blob, // Single most recent recording blob
    mp3Url, // Single most recent recording URL
    start, stop, pause, resume, // Recording functions
    recorderState, // Current state of recorder (see RecorderStates)
    getProps // Function to get the props that can be passed to the SimpleAudioRecorder react component
} = useSimpleAudioRecorder({
    workerUrl, onDataAvailable, onComplete, onError, options, cleanup = false, timeUpdateStep = 111, countdown = 0
})

SimpleAudioRecorder component

This is a very simple state machine component that shows a different view component depending on the current recorder state.

SimpleAudioRecorder({
    // As returned by useSimpleAudioRecorder
    recorderState,
    // The components to display in each of the states.
    // Only viewInitial and viewRecording are absolutely required.
    viewInitial, viewStarting, viewCountdown, viewRecording, viewPaused, viewEncoding, viewComplete, viewError
})

preloadWorker(workerUrl)

Instead of passing a workerUrl to useSimpleAudioRecorder, it's better to call this function somewhere at the start of your app to preload the worker as soon as possible.

RecorderStates

An enumeration of possible recorder states. Used by the SimpleAudioRecorder component.

RecorderStates = {
    INITIAL,
    STARTING,
    RECORDING,
    PAUSED,
    ENCODING,
    COMPLETE,
    ERROR,
    COUNTDOWN
}

Known issues

iOS/Safari

Simple Audio Recorder uses an AudioWorkletNode to extract the audio data, where supported, and falls back to using the deprecated ScriptProcessorNode on older browsers. However, there seem to be some occasional issues using AudioWorkletNode on iOS/Safari. After about 45 seconds, audio packets from the microphone start to get dropped, creating a recording that is shorter than expected with stuttering and glitches. So currently, the deprecated ScriptProcessorNode will always be used on iOS/Safari.

AFAIK this is an unsolved issue, perhaps related to Safari's implementation of AudioWorklets and them not being given enough CPU priority. These issues only appear on some devices. Curiously, similar glitches have also been experienced when using the old ScriptProcessorNode on Chrome on other platforms.

Chrome isn't any better on iOS either as they are forced to use Safari under the hood (somehow, this feels rather familiar).

Licenses

SimpleAudioRecorder is mostly MIT licenced, but the worker is probably LGPL as it uses lamejs.