├── .gitignore
├── examples
├── react-hook-example
│ ├── public
│ │ └── mp3worker.js
│ ├── vite.config.js
│ ├── src
│ │ ├── main.jsx
│ │ └── App.jsx
│ ├── index.html
│ └── package.json
├── react-hook-countdown-example
│ ├── public
│ │ └── mp3worker.js
│ ├── vite.config.js
│ ├── src
│ │ ├── main.jsx
│ │ └── App.jsx
│ ├── index.html
│ └── package.json
├── minimal-example
│ └── index.html
├── minimal-example-promises
│ └── index.html
├── misc
│ └── test-encoding-queue-size.html
└── main-example
│ └── index.html
├── .github
└── GitHubAudioRecorderHeader.png
├── src
├── utils.js
├── Timer.js
├── mp3worker
│ ├── mp3worker.js
│ └── WorkerEncoder.js
├── react.js
└── AudioRecorder.js
├── package.json
├── rollup.config.js
├── dist
├── audiorecorder.min.js
├── index.mjs
├── index.cjs
├── audiorecorder.js
├── audiorecorder.min.js.map
├── react.mjs
└── react.cjs
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/*LICENSE.txt
3 |
--------------------------------------------------------------------------------
/examples/react-hook-example/public/mp3worker.js:
--------------------------------------------------------------------------------
1 | ../../../dist/mp3worker.js
--------------------------------------------------------------------------------
/examples/react-hook-countdown-example/public/mp3worker.js:
--------------------------------------------------------------------------------
1 | ../../../dist/mp3worker.js
--------------------------------------------------------------------------------
/.github/GitHubAudioRecorderHeader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vocaroo/simple-audio-recorder/HEAD/.github/GitHubAudioRecorderHeader.png
--------------------------------------------------------------------------------
/examples/react-hook-example/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/examples/react-hook-countdown-example/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/examples/react-hook-example/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root')).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/examples/react-hook-countdown-example/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root')).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/examples/react-hook-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Simple Audio Recorder Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/react-hook-countdown-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Simple Audio Recorder Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/react-hook-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hook-example",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0"
14 | },
15 | "devDependencies": {
16 | "@types/react": "^18.2.15",
17 | "@types/react-dom": "^18.2.7",
18 | "@vitejs/plugin-react": "^4.0.3",
19 | "vite": "^4.4.5"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/react-hook-countdown-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hook-example",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0"
14 | },
15 | "devDependencies": {
16 | "@types/react": "^18.2.15",
17 | "@types/react-dom": "^18.2.7",
18 | "@vitejs/plugin-react": "^4.0.3",
19 | "vite": "^4.4.5"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 |
2 | export function stopStream(stream) {
3 | if (stream.getTracks) {
4 | stream.getTracks().forEach(track => track.stop());
5 | } else {
6 | stream.stop(); // Deprecated
7 | }
8 | }
9 |
10 | // https://stackoverflow.com/a/9039885
11 | export function detectIOS() {
12 | return [
13 | 'iPad Simulator',
14 | 'iPhone Simulator',
15 | 'iPod Simulator',
16 | 'iPad',
17 | 'iPhone',
18 | 'iPod'
19 | ].includes(navigator.platform)
20 | // iPad on iOS 13 detection
21 | || (navigator.userAgent.includes("Mac") && "ontouchend" in document);
22 | }
23 |
24 | export function detectSafari() {
25 | return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
26 | }
27 |
--------------------------------------------------------------------------------
/src/Timer.js:
--------------------------------------------------------------------------------
1 |
2 | export default class Timer {
3 | constructor() {
4 | this.reset();
5 | }
6 |
7 | reset() {
8 | this.startTime = null; // May be modified when resuming, so not the true start time.
9 | this.stoppedTime = null;
10 | }
11 |
12 | start() {
13 | if (!this.startTime) {
14 | this.startTime = Date.now();
15 | }
16 |
17 | if (this.stoppedTime) {
18 | // Skip time forward by the time length we were stopped
19 | this.startTime += Date.now() - this.stoppedTime;
20 | this.stoppedTime = null;
21 | }
22 | }
23 |
24 | resetAndStart() {
25 | this.reset();
26 | this.start();
27 | }
28 |
29 | stop() {
30 | if (!this.stoppedTime) {
31 | this.stoppedTime = Date.now();
32 | }
33 | }
34 |
35 | getTime() {
36 | if (this.startTime) {
37 | if (this.stoppedTime) {
38 | return this.stoppedTime - this.startTime;
39 | } else {
40 | return Date.now() - this.startTime;
41 | }
42 | } else {
43 | return 0;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/minimal-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/examples/minimal-example-promises/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/examples/react-hook-example/src/App.jsx:
--------------------------------------------------------------------------------
1 | import {SimpleAudioRecorder, useSimpleAudioRecorder} from "../../../src/react.js";
2 |
3 | export default function App() {
4 | const recorder = useSimpleAudioRecorder({
5 | workerUrl : "mp3worker.js",
6 | onDataAvailable : data => console.log("DATA AVAILABLE", data.length),
7 | onComplete : mp3Blob => console.log("RECORDING COMPLETE!", mp3Blob),
8 | onError : error => console.log("RECORDING ERROR!", error)
9 | });
10 |
11 | const viewInitial = (
12 |
13 | );
14 |
15 | const viewRecording = (
16 | <>
17 |
20 |
23 | >
24 | );
25 |
26 | const viewPaused = (
27 | <>
28 |
31 |
32 | >
33 | );
34 |
35 | const viewError = (
36 | <>
37 | {viewInitial}
38 | Error occurred! {recorder.errorStr}
39 | >
40 | );
41 |
42 | return (
43 |
44 |
50 |
51 |
52 |
53 | {recorder.mp3Urls.toReversed().map(url =>
54 |
57 | )}
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/examples/misc/test-encoding-queue-size.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple-audio-recorder",
3 | "version": "1.3.0",
4 | "description": "Web audio recording library with encoding to mp3 and chunked output",
5 | "keywords": [
6 | "audio",
7 | "sound",
8 | "recorder",
9 | "mp3",
10 | "encoder",
11 | "microphone",
12 | "react",
13 | "hook"
14 | ],
15 | "homepage": "https://github.com/vocaroo/simple-audio-recorder",
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/vocaroo/simple-audio-recorder.git"
19 | },
20 | "type": "module",
21 | "main": "dist/index.cjs",
22 | "module": "dist/index.mjs",
23 | "exports": {
24 | ".": {
25 | "import": "./dist/index.mjs",
26 | "require": "./dist/index.cjs",
27 | "default": "./dist/index.mjs"
28 | },
29 | "./react": {
30 | "import": "./dist/react.mjs",
31 | "require": "./dist/react.cjs"
32 | },
33 | "./umd": "./dist/audiorecorder.min.js"
34 | },
35 | "sideEffects": false,
36 | "files": [
37 | "dist",
38 | "README.md"
39 | ],
40 | "scripts": {
41 | "build": "NODE_ENV=production rollup -c",
42 | "dev": "concurrently \"rollup -c -w\" \"live-server --open=/examples/main-example/\""
43 | },
44 | "license": "MIT",
45 | "devDependencies": {
46 | "@babel/core": "^7",
47 | "@babel/preset-env": "^7",
48 | "@rollup/plugin-babel": "^6",
49 | "@rollup/plugin-commonjs": "^25",
50 | "@rollup/plugin-node-resolve": "^15",
51 | "@rollup/plugin-replace": "^5",
52 | "@rollup/plugin-terser": "^0.4.4",
53 | "concurrently": "^7.6.0",
54 | "lamejstmp": "1.0.1",
55 | "live-server": "1.2.1",
56 | "rollup": "^4"
57 | },
58 | "peerDependencies": {
59 | "react": ">=16.8.0"
60 | },
61 | "peerDependenciesMeta": {
62 | "react": {
63 | "optional": true
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/examples/react-hook-countdown-example/src/App.jsx:
--------------------------------------------------------------------------------
1 | import {SimpleAudioRecorder, useSimpleAudioRecorder} from "../../../src/react.js";
2 |
3 | export default function App() {
4 | const recorder = useSimpleAudioRecorder({
5 | workerUrl : "mp3worker.js",
6 | onDataAvailable : data => console.log("DATA AVAILABLE", data.length),
7 | onComplete : mp3Blob => console.log("RECORDING COMPLETE!", mp3Blob),
8 | onError : error => console.log("RECORDING ERROR!", error),
9 | countdown : 3000
10 | });
11 |
12 | const viewInitial = (
13 |
14 | );
15 |
16 | const viewCountdown = (
17 |
20 | );
21 |
22 | const viewRecording = (
23 | <>
24 |
27 |
30 | >
31 | );
32 |
33 | const viewPaused = (
34 | <>
35 |
38 |
39 | >
40 | );
41 |
42 | const viewError = (
43 | <>
44 | {viewInitial}
45 | Error occurred! {recorder.errorStr}
46 | >
47 | );
48 |
49 | return (
50 |
51 |
58 |
59 |
60 |
61 | {recorder.mp3Urls.toReversed().map(url =>
62 |
65 | )}
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from "@rollup/plugin-node-resolve";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import replace from "@rollup/plugin-replace";
4 | import {babel} from "@rollup/plugin-babel";
5 | import terser from "@rollup/plugin-terser";
6 |
7 | const isProd = process.env.NODE_ENV === "production";
8 | const extensions = [".js", ".mjs"];
9 |
10 | const minify = terser({
11 | format : {comments : false},
12 | compress : {
13 | passes : 2,
14 | pure_getters : true,
15 | unsafe_math : false,
16 | },
17 | mangle : {toplevel : true},
18 | });
19 |
20 | const basePlugins = [
21 | resolve({extensions}),
22 | commonjs(),
23 | // keep builds SSR-safe and deterministic
24 | replace({
25 | preventAssignment : true,
26 | values : {
27 | "process.env.NODE_ENV" : JSON.stringify("production"),
28 | },
29 | }),
30 | babel({
31 | babelHelpers : "bundled",
32 | presets : [["@babel/preset-env", {targets : ">0.5%, not dead"}]],
33 | extensions,
34 | exclude : /node_modules/,
35 | }),
36 | ];
37 |
38 | // Core library (AudioRecorder)
39 | const coreInput = "src/AudioRecorder.js";
40 |
41 | export default [
42 | // Core: ESM + CJS for bundlers/SSR
43 | {
44 | input : coreInput,
45 | plugins : basePlugins,
46 | output : [
47 | {file : "dist/index.mjs", format : "es", sourcemap : true},
48 | {file : "dist/index.cjs", format : "cjs", exports : "default", sourcemap : true},
49 | ],
50 | },
51 |
52 | // Core: UMD for
7 |
8 |
9 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | autoGainControl:
134 | noiseSuppression:
135 | echoCancellation:
136 | force using old script processor:
137 |
138 |
139 |
140 |