diff --git a/.vscode/settings.json b/.vscode/settings.json index 4fd6cbf..883e279 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,6 @@ "editor.fontSize": 16, "editor.formatOnSave": true, "workbench.iconTheme": "ayu", - "workbench.colorTheme": "Ayu Dark Bordered", + "workbench.colorTheme": "Ayu Mirage Bordered", "vetur.format.defaultFormatter.html": "prettier" } diff --git a/nuxt.config.js b/nuxt.config.js index bc171ef..0e11009 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -32,7 +32,7 @@ export default { /* ** Plugins to load before mounting the App */ - plugins: [], + plugins: [{ src: '~/plugins/WebAudioRecorder.js', mode: 'client' }], /* ** Nuxt.js dev-modules */ diff --git a/package-lock.json b/package-lock.json index f82ca30..1e72a18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8584,9 +8584,9 @@ "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "requires": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -16319,35 +16319,80 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, "ts-jest": { - "version": "23.10.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.10.5.tgz", - "integrity": "sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A==", + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.1.tgz", + "integrity": "sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==", "dev": true, "requires": { "bs-logger": "0.x", "buffer-from": "1.x", "fast-json-stable-stringify": "2.x", "json5": "2.x", + "lodash.memoize": "4.x", "make-error": "1.x", + "micromatch": "4.x", "mkdirp": "0.x", - "resolve": "1.x", - "semver": "^5.5", - "yargs-parser": "10.x" + "semver": "6.x", + "yargs-parser": "18.x" }, "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } @@ -16892,44 +16937,20 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==" }, "vue-jest": { - "version": "4.0.0-beta.2", - "resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-4.0.0-beta.2.tgz", - "integrity": "sha512-SywBIciuIfqsCb8Eb9UQ02s06+NV8Ry8KnbyhAfnvnyFFulIuh7ujtga9eJYq720nCS4Hz4TpVtS4pD1ZbUILQ==", + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-4.0.0-beta.3.tgz", + "integrity": "sha512-XYDNO2cPeK3/nqKwpBYPIGPr0QeSPQOm6R0fTfGvxN3mUVD/tK99sBPL7BzbJy5p8PI72jxzOcP2ZOlHmD7DYA==", "dev": true, "requires": { "@babel/plugin-transform-modules-commonjs": "^7.2.0", - "@vue/component-compiler-utils": "^2.4.0", + "@vue/component-compiler-utils": "^3.1.0", "chalk": "^2.1.0", + "convert-source-map": "^1.6.0", "extract-from-css": "^0.4.4", - "source-map": "^0.5.6", - "ts-jest": "^23.10.5" + "source-map": "0.5.6", + "ts-jest": "^25.2.1" }, "dependencies": { - "@vue/component-compiler-utils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz", - "integrity": "sha512-IHjxt7LsOFYc0DkTncB7OXJL7UzwOLPPQCfEUNyxL2qt+tF12THV+EO33O1G2Uk4feMSWua3iD39Itszx0f0bw==", - "dev": true, - "requires": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.14", - "postcss-selector-parser": "^5.0.0", - "prettier": "1.16.3", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -16941,49 +16962,10 @@ "supports-color": "^5.3.0" } }, - "cssesc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", - "dev": true - }, - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "postcss-selector-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", - "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", - "dev": true, - "requires": { - "cssesc": "^2.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, - "prettier": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.3.tgz", - "integrity": "sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", "dev": true } } diff --git a/package.json b/package.json index 0cfe84e..63f1e52 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,6 @@ "lint-staged": "^10.1.3", "prettier": "^1.19.1", "stylelint": "^10.1.0", - "vue-jest": "^4.0.0-0" + "vue-jest": "^4.0.0-beta.3" } } diff --git a/pages/index.vue b/pages/index.vue index 3d1a6bc..aeb68bc 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -72,7 +72,7 @@ Send recording: {{ - `Recording ${selected + 1}` + selectedLabel }} None @@ -138,7 +138,7 @@ export default { recorder: null, input: null, audioContext: null, - selected: null, + selected: 0, timerStatus: 'stopped', recordings: [], count: 0, @@ -150,6 +150,13 @@ export default { snackbarError: false } }, + computed: { + selectedLabel() { + return this.recordings && this.recordings[this.selected] + ? `Recording ${this.recordings[this.selected].number}` + : 'None' + } + }, mounted() { // get access to the mic if (navigator && navigator.mediaDevices) { @@ -160,62 +167,6 @@ export default { .then((stream) => { // and until we do, enable the recording button this.canRecord = true - - console.warn( - 'getUserMedia() success, stream created, initializing WebAudioRecorder...' - ) - - const AudioContext = window.AudioContext || window.webkitAudioContext - this.audioContext = new AudioContext() - - // assign to getUserMediaStream for later use - this.getUserMediaStream = stream - /* use the stream */ - this.input = this.audioContext.createMediaStreamSource(stream) - - const options = { - workerDir: 'js/WebAudioRecorder/', - encoding: ENCODING_TYPE, - onEncoderLoading: (recorder, encoding) => { - // show "loading encoder..." display - console.warn('Loading ' + encoding + ' encoder...') - }, - onEncoderLoaded: (recorder, encoding) => { - // hide "loading encoder..." display - console.warn(encoding + ' encoder loaded') - }, - onComplete: (recorder, blob) => { - console.warn('Encoding complete') - const url = URL.createObjectURL(blob) - - this.recordings.push({ - number: ++this.count, - id: getId(url), // the url already gives us a unique id, so we might as well use that :D - audio: url, - blob, - encoding: ENCODING_TYPE - }) - - function getId(url) { - const href = url.replace('blob:', '') - const parser = document.createElement('a') - - parser.href = href - return parser.pathname.substring(1) - } - }, - onTimeout: this.stopRecording - } - - this.recorder = new window.WebAudioRecorder(this.input, options) - - this.recorder.setOptions({ - timeLimit: this.TIME_LIMIT, - encodeAfterRecord: ENCODE_AFTER_RECORD, - mp3: { - bitRate: 160 - } - }) }) } }, @@ -235,9 +186,76 @@ export default { this.isRecording = false }, startRecording() { - this.recorder.startRecording() - console.warn('Recording started') - this.isRecording = true + if (navigator && navigator.mediaDevices) { + navigator.mediaDevices + .getUserMedia({ + audio: true + }) + .then((stream) => { + // assign to getUserMediaStream for later use + this.getUserMediaStream = stream + console.warn( + 'getUserMedia() success, stream created, initializing WebAudioRecorder...' + ) + + const AudioContext = + window.AudioContext || window.webkitAudioContext + this.audioContext = new AudioContext() + + /* use the stream */ + this.input = this.audioContext.createMediaStreamSource( + this.getUserMediaStream + ) + + const options = { + workerDir: 'js/WebAudioRecorder/', + encoding: ENCODING_TYPE, + onEncoderLoading: (recorder, encoding) => { + // show "loading encoder..." display + console.warn('Loading ' + encoding + ' encoder...') + }, + onEncoderLoaded: (recorder, encoding) => { + // hide "loading encoder..." display + console.warn(encoding + ' encoder loaded') + }, + onComplete: (recorder, blob) => { + console.warn('Encoding complete') + const url = URL.createObjectURL(blob) + + this.recordings.push({ + number: ++this.count, + id: getId(url), // the url already gives us a unique id, so we might as well use that :D + audio: url, + blob, + encoding: ENCODING_TYPE + }) + + function getId(url) { + const href = url.replace('blob:', '') + const parser = document.createElement('a') + + parser.href = href + return parser.pathname.substring(1) + } + }, + onTimeout: this.stopRecording + } + + this.recorder = new window.WebAudioRecorder(this.input, options) + + this.recorder.setOptions({ + timeLimit: this.TIME_LIMIT, + encodeAfterRecord: ENCODE_AFTER_RECORD, + mp3: { + bitRate: 160 + } + }) + + this.recorder.startRecording() + console.warn('Recording started') + this.isRecording = true + }) + } }, stopRecording() { this.isRecording = false @@ -282,11 +300,6 @@ export default { } ) } - }, - head() { - return { - script: [{ src: '/WebAudioRecorder.js', defer: true }] - } } } diff --git a/plugins/WebAudioRecorder.js b/plugins/WebAudioRecorder.js new file mode 100644 index 0000000..62bda26 --- /dev/null +++ b/plugins/WebAudioRecorder.js @@ -0,0 +1,207 @@ +;(function(window) { + // internal: same as jQuery.extend(true, args...) + const extend = function() { + const target = arguments[0] + const sources = [].slice.call(arguments, 1) + for (let i = 0; i < sources.length; ++i) { + const src = sources[i] + for (const key in src) { + const val = src[key] + target[key] = + typeof val === 'object' + ? extend(typeof target[key] === 'object' ? target[key] : {}, val) + : val + } + } + return target + } + + const WORKER_FILE = { + wav: 'WebAudioRecorderWav.js', + ogg: 'WebAudioRecorderOgg.js', + mp3: 'WebAudioRecorderMp3.js' + } + + // default configs + const CONFIGS = { + workerDir: '/', // worker scripts dir (end with /) + numChannels: 2, // number of channels + encoding: 'wav', // encoding (can be changed at runtime) + + // runtime options + options: { + timeLimit: 300, // recording time limit (sec) + encodeAfterRecord: false, // process encoding after recording + progressInterval: 1000, // encoding progress report interval (millisec) + bufferSize: undefined, // buffer size (use browser default) + + // encoding-specific options + wav: { + mimeType: 'audio/wav' + }, + ogg: { + mimeType: 'audio/ogg', + quality: 0.5 // (VBR only): quality = [-0.1 .. 1] + }, + mp3: { + mimeType: 'audio/mpeg', + bitRate: 160 // (CBR only): bit rate = [64 .. 320] + } + } + } + + // constructor + const WebAudioRecorder = function(sourceNode, configs) { + extend(this, CONFIGS, configs || {}) + this.context = sourceNode.context + if (this.context.createScriptProcessor == null) + this.context.createScriptProcessor = this.context.createJavaScriptNode + this.input = this.context.createGain() + sourceNode.connect(this.input) + this.buffer = [] + this.initWorker() + } + + // instance methods + extend(WebAudioRecorder.prototype, { + isRecording() { + return this.processor != null + }, + + setEncoding(encoding) { + if (this.isRecording()) + this.error('setEncoding: cannot set encoding during recording') + else if (this.encoding !== encoding) { + this.encoding = encoding + this.initWorker() + } + }, + + setOptions(options) { + if (this.isRecording()) + this.error('setOptions: cannot set options during recording') + else { + extend(this.options, options) + this.worker.postMessage({ command: 'options', options: this.options }) + } + }, + + startRecording() { + if (this.isRecording()) + this.error('startRecording: previous recording is running') + else { + const numChannels = this.numChannels + const buffer = this.buffer + const worker = this.worker + this.processor = this.context.createScriptProcessor( + this.options.bufferSize, + this.numChannels, + this.numChannels + ) + this.input.connect(this.processor) + this.processor.connect(this.context.destination) + this.processor.onaudioprocess = function(event) { + for (let ch = 0; ch < numChannels; ++ch) + buffer[ch] = event.inputBuffer.getChannelData(ch) + worker.postMessage({ command: 'record', buffer }) + } + this.worker.postMessage({ + command: 'start', + bufferSize: this.processor.bufferSize + }) + this.startTime = Date.now() + } + }, + + recordingTime() { + return this.isRecording() ? (Date.now() - this.startTime) * 0.001 : null + }, + + cancelRecording() { + if (this.isRecording()) { + this.input.disconnect() + this.processor.disconnect() + delete this.processor + this.worker.postMessage({ command: 'cancel' }) + } else this.error('cancelRecording: no recording is running') + }, + + finishRecording() { + if (this.isRecording()) { + this.input.disconnect() + this.processor.disconnect() + delete this.processor + this.worker.postMessage({ command: 'finish' }) + } else this.error('finishRecording: no recording is running') + }, + + cancelEncoding() { + if (this.options.encodeAfterRecord) + if (this.isRecording()) + this.error('cancelEncoding: recording is not finished') + else { + this.onEncodingCanceled(this) + this.initWorker() + } + else this.error('cancelEncoding: invalid method call') + }, + + initWorker() { + if (this.worker != null) this.worker.terminate() + this.onEncoderLoading(this, this.encoding) + this.worker = new Worker(this.workerDir + WORKER_FILE[this.encoding]) + const _this = this + this.worker.onmessage = function(event) { + const data = event.data + switch (data.command) { + case 'loaded': + _this.onEncoderLoaded(_this, _this.encoding) + break + case 'timeout': + _this.onTimeout(_this) + break + case 'progress': + _this.onEncodingProgress(_this, data.progress) + break + case 'complete': + _this.onComplete(_this, data.blob) + break + case 'error': + _this.error(data.message) + } + } + this.worker.postMessage({ + command: 'init', + config: { + sampleRate: this.context.sampleRate, + numChannels: this.numChannels + }, + options: this.options + }) + }, + + error(message) { + this.onError(this, 'WebAudioRecorder.js:' + message) + }, + + // event handlers + onEncoderLoading(recorder, encoding) {}, + onEncoderLoaded(recorder, encoding) {}, + onTimeout(recorder) { + recorder.finishRecording() + }, + onEncodingProgress(recorder, progress) {}, + onEncodingCanceled(recorder) {}, + onComplete(recorder, blob) { + recorder.onError( + recorder, + 'WebAudioRecorder.js: You must override .onComplete event' + ) + }, + onError(recorder, message) { + console.log(message) + } + }) + + window.WebAudioRecorder = WebAudioRecorder +})(window) diff --git a/static/WebAudioRecorder.js b/static/WebAudioRecorder.js deleted file mode 100755 index 052abb8..0000000 --- a/static/WebAudioRecorder.js +++ /dev/null @@ -1,199 +0,0 @@ -(function(window) { - // internal: same as jQuery.extend(true, args...) - var extend = function() { - var target = arguments[0], - sources = [].slice.call(arguments, 1); - for (var i = 0; i < sources.length; ++i) { - var src = sources[i]; - for (key in src) { - var val = src[key]; - target[key] = typeof val === "object" - ? extend(typeof target[key] === "object" ? target[key] : {}, val) - : val; - } - } - return target; - }; - - var WORKER_FILE = { - wav: "WebAudioRecorderWav.js", - ogg: "WebAudioRecorderOgg.js", - mp3: "WebAudioRecorderMp3.js" - }; - - // default configs - var CONFIGS = { - workerDir: "/", // worker scripts dir (end with /) - numChannels: 2, // number of channels - encoding: "wav", // encoding (can be changed at runtime) - - // runtime options - options: { - timeLimit: 300, // recording time limit (sec) - encodeAfterRecord: false, // process encoding after recording - progressInterval: 1000, // encoding progress report interval (millisec) - bufferSize: undefined, // buffer size (use browser default) - - // encoding-specific options - wav: { - mimeType: "audio/wav" - }, - ogg: { - mimeType: "audio/ogg", - quality: 0.5 // (VBR only): quality = [-0.1 .. 1] - }, - mp3: { - mimeType: "audio/mpeg", - bitRate: 160 // (CBR only): bit rate = [64 .. 320] - } - } - }; - - // constructor - var WebAudioRecorder = function(sourceNode, configs) { - extend(this, CONFIGS, configs || {}); - this.context = sourceNode.context; - if (this.context.createScriptProcessor == null) - this.context.createScriptProcessor = this.context.createJavaScriptNode; - this.input = this.context.createGain(); - sourceNode.connect(this.input); - this.buffer = []; - this.initWorker(); - }; - - // instance methods - extend(WebAudioRecorder.prototype, { - isRecording: function() { return this.processor != null; }, - - setEncoding: function(encoding) { - if (this.isRecording()) - this.error("setEncoding: cannot set encoding during recording"); - else if (this.encoding !== encoding) { - this.encoding = encoding; - this.initWorker(); - } - }, - - setOptions: function(options) { - if (this.isRecording()) - this.error("setOptions: cannot set options during recording"); - else { - extend(this.options, options); - this.worker.postMessage({ command: "options", options: this.options }); - } - }, - - startRecording: function() { - if (this.isRecording()) - this.error("startRecording: previous recording is running"); - else { - var numChannels = this.numChannels, - buffer = this.buffer, - worker = this.worker; - this.processor = this.context.createScriptProcessor( - this.options.bufferSize, - this.numChannels, this.numChannels); - this.input.connect(this.processor); - this.processor.connect(this.context.destination); - this.processor.onaudioprocess = function(event) { - for (var ch = 0; ch < numChannels; ++ch) - buffer[ch] = event.inputBuffer.getChannelData(ch); - worker.postMessage({ command: "record", buffer: buffer }); - }; - this.worker.postMessage({ - command: "start", - bufferSize: this.processor.bufferSize - }); - this.startTime = Date.now(); - } - }, - - recordingTime: function() { - return this.isRecording() ? (Date.now() - this.startTime) * 0.001 : null; - }, - - cancelRecording: function() { - if (this.isRecording()) { - this.input.disconnect(); - this.processor.disconnect(); - delete this.processor; - this.worker.postMessage({ command: "cancel" }); - } else - this.error("cancelRecording: no recording is running"); - }, - - finishRecording: function() { - if (this.isRecording()) { - this.input.disconnect(); - this.processor.disconnect(); - delete this.processor; - this.worker.postMessage({ command: "finish" }); - } else - this.error("finishRecording: no recording is running"); - }, - - cancelEncoding: function() { - if (this.options.encodeAfterRecord) - if (this.isRecording()) - this.error("cancelEncoding: recording is not finished"); - else { - this.onEncodingCanceled(this); - this.initWorker(); - } - else - this.error("cancelEncoding: invalid method call"); - }, - - initWorker: function() { - if (this.worker != null) - this.worker.terminate(); - this.onEncoderLoading(this, this.encoding); - this.worker = new Worker(this.workerDir + WORKER_FILE[this.encoding]); - var _this = this; - this.worker.onmessage = function(event) { - var data = event.data; - switch (data.command) { - case "loaded": - _this.onEncoderLoaded(_this, _this.encoding); - break; - case "timeout": - _this.onTimeout(_this); - break; - case "progress": - _this.onEncodingProgress(_this, data.progress); - break; - case "complete": - _this.onComplete(_this, data.blob); - break; - case "error": - _this.error(data.message); - } - }; - this.worker.postMessage({ - command: "init", - config: { - sampleRate: this.context.sampleRate, - numChannels: this.numChannels - }, - options: this.options - }); - }, - - error: function(message) { - this.onError(this, "WebAudioRecorder.js:" + message); - }, - - // event handlers - onEncoderLoading: function(recorder, encoding) {}, - onEncoderLoaded: function(recorder, encoding) {}, - onTimeout: function(recorder) { recorder.finishRecording(); }, - onEncodingProgress: function (recorder, progress) {}, - onEncodingCanceled: function(recorder) {}, - onComplete: function(recorder, blob) { - recorder.onError(recorder, "WebAudioRecorder.js: You must override .onComplete event"); - }, - onError: function(recorder, message) { console.log(message); } - }); - - window.WebAudioRecorder = WebAudioRecorder; -})(window); diff --git a/static/js/WebAudioRecorder/WebAudioRecorderMp3.js b/static/js/WebAudioRecorder/WebAudioRecorderMp3.js index b6f73c2..ca6a4cf 100755 --- a/static/js/WebAudioRecorder/WebAudioRecorderMp3.js +++ b/static/js/WebAudioRecorder/WebAudioRecorderMp3.js @@ -1,91 +1,99 @@ -importScripts("Mp3LameEncoder.min.js"); +importScripts('Mp3LameEncoder.min.js') var NUM_CH = 2, // constant - sampleRate = 44100, - options = undefined, - maxBuffers = undefined, - encoder = undefined, - recBuffers = undefined, - bufferCount = 0; + sampleRate = 44100, + options = undefined, + maxBuffers = undefined, + encoder = undefined, + recBuffers = undefined, + bufferCount = 0 function error(message) { - self.postMessage({ command: "error", message: "mp3: " + message }); + self.postMessage({ command: 'error', message: 'mp3: ' + message }) } function init(data) { if (data.config.numChannels === NUM_CH) { - sampleRate = data.config.sampleRate; - options = data.options; - } else - error("numChannels must be " + NUM_CH); -}; + sampleRate = data.config.sampleRate + options = data.options + } else error('numChannels must be ' + NUM_CH) +} function setOptions(opt) { - if (encoder || recBuffers) - error("cannot set options during recording"); - else - options = opt; + if (encoder || recBuffers) error('cannot set options during recording') + else options = opt } function start(bufferSize) { - maxBuffers = Math.ceil(options.timeLimit * sampleRate / bufferSize); - if (options.encodeAfterRecord) - recBuffers = []; - else - encoder = new Mp3LameEncoder(sampleRate, options.mp3.bitRate); + maxBuffers = Math.ceil((options.timeLimit * sampleRate) / bufferSize) + if (options.encodeAfterRecord) recBuffers = [] + else encoder = new Mp3LameEncoder(sampleRate, options.mp3.bitRate) } function record(buffer) { - if (bufferCount++ < maxBuffers) - if (encoder) - encoder.encode(buffer); - else - recBuffers.push(buffer); - else - self.postMessage({ command: "timeout" }); -}; + if (bufferCount++ < maxBuffers) { + if (encoder) { + encoder.encode(buffer) + } else { + recBuffers.push(buffer) + } + } else { + self.postMessage({ command: 'timeout' }) + } +} function postProgress(progress) { - self.postMessage({ command: "progress", progress: progress }); -}; + self.postMessage({ command: 'progress', progress: progress }) +} function finish() { if (recBuffers) { - postProgress(0); - encoder = new Mp3LameEncoder(sampleRate, options.mp3.bitRate); - var timeout = Date.now() + options.progressInterval; + postProgress(0) + encoder = new Mp3LameEncoder(sampleRate, options.mp3.bitRate) + var timeout = Date.now() + options.progressInterval while (recBuffers.length > 0) { - encoder.encode(recBuffers.shift()); - var now = Date.now(); + encoder.encode(recBuffers.shift()) + var now = Date.now() if (now > timeout) { - postProgress((bufferCount - recBuffers.length) / bufferCount); - timeout = now + options.progressInterval; + postProgress((bufferCount - recBuffers.length) / bufferCount) + timeout = now + options.progressInterval } } - postProgress(1); + postProgress(1) } self.postMessage({ - command: "complete", + command: 'complete', blob: encoder.finish(options.mp3.mimeType) - }); - cleanup(); -}; + }) + cleanup() +} function cleanup() { - encoder = recBuffers = undefined; - bufferCount = 0; + encoder = recBuffers = undefined + bufferCount = 0 } self.onmessage = function(event) { - var data = event.data; + var data = event.data switch (data.command) { - case "init": init(data); break; - case "options": setOptions(data.options); break; - case "start": start(data.bufferSize); break; - case "record": record(data.buffer); break; - case "finish": finish(); break; - case "cancel": cleanup(); + case 'init': + init(data) + break + case 'options': + setOptions(data.options) + break + case 'start': + start(data.bufferSize) + break + case 'record': + record(data.buffer) + break + case 'finish': + finish() + break + case 'cancel': + cleanup() } -}; +} -self.postMessage({ command: "loaded" }); +self.postMessage({ command: 'loaded' })