Comparing version 0.1.2 to 0.2.0
{ | ||
"name": "vmsg", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"description": "Library for creating voice messages", | ||
@@ -32,6 +32,6 @@ "main": "vmsg.js", | ||
"devDependencies": { | ||
"autoprefixer": "^8.0.0", | ||
"babel-plugin-transform-class-properties": "^6.24.1", | ||
"babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
"babel-preset-react": "^6.24.1", | ||
"parcel-bundler": "^1.6.0", | ||
"parcel-bundler": "^1.6.2", | ||
"parcel-plugin-disable-loaders": "^1.0.1", | ||
@@ -38,0 +38,0 @@ "react": "^16.2.0", |
@@ -8,6 +8,6 @@ # vmsg [![npm](https://img.shields.io/npm/v/vmsg.svg)](https://www.npmjs.com/package/vmsg) | ||
standard program, upload to file hosting and share the link. But why | ||
bother with all of that boredom stuff if you can do the same in browser | ||
bother with all of that tedious stuff if you can do the same in browser | ||
with a few clicks. | ||
:fireworks: :tada: **[DEMO](https://kagami.github.io/vmsg/)** :tada: :fireworks: | ||
:confetti_ball: :tada: **[DEMO](https://kagami.github.io/vmsg/)** :tada: :confetti_ball: | ||
@@ -23,10 +23,7 @@ ## Features | ||
* Chrome 57+ | ||
* Firefox 53+ | ||
* Chrome 32+ | ||
* Firefox 27+ | ||
* Safari 11+ | ||
* Edge 16+ | ||
* Edge 12+ | ||
Note that this haven't been extensively tested yet. Feel free to open | ||
issue in case of any errors. | ||
## Usage | ||
@@ -42,8 +39,8 @@ | ||
someButton.onclick = function() { | ||
record(/* {wasmURL: "/path/to/vmsg.wasm"} */).then(file => { | ||
console.log("Recorded MP3", file); | ||
record(/* {wasmURL: "/static/js/vmsg.wasm"} */).then(blob => { | ||
console.log("Recorded MP3", blob); | ||
// Can be used like this: | ||
// | ||
// const form = new FormData(); | ||
// form.append("file[]", file); | ||
// form.append("file[]", blob, "record.mp3"); | ||
// fetch("/upload.php", { | ||
@@ -60,5 +57,7 @@ // credentials: "include", | ||
That's it! Don't forget to include [vmsg.css](vmsg.css) and | ||
[vmsg.wasm](vmsg.wasm) in your project. | ||
[vmsg.wasm](vmsg.wasm) in your project. For browsers without WebAssembly | ||
support you need to also include | ||
[wasm-polyfill.js](https://github.com/Kagami/wasm-polyfill.js). | ||
See also [demo](demo) directory for a more feasible example. | ||
See [demo](demo) directory for a more feasible example. | ||
@@ -109,2 +108,8 @@ ## Development | ||
## But you can use e.g. ogv.js polyfill! | ||
* It make things more complicated, now you need both encoder and decoder | ||
* Opus gives you ~2x bitrate win but for 500kb per minute files it's not that much | ||
* MP3 is much more widespread, so even while compression is not best compatibility matters | ||
## License | ||
@@ -111,0 +116,0 @@ |
190
vmsg.js
@@ -16,7 +16,17 @@ function pad2(n) { | ||
function fetchAndInstantiate(url, imports) { | ||
const req = fetch(url, {credentials: "same-origin"}); | ||
return WebAssembly.instantiateStreaming | ||
? WebAssembly.instantiateStreaming(req, imports) | ||
: req.then(res => res.arrayBuffer()) | ||
.then(buf => WebAssembly.instantiate(buf, imports)); | ||
if (WebAssembly.instantiateStreaming) { | ||
const req = fetch(url, {credentials: "same-origin"}); | ||
return WebAssembly.instantiateStreaming(req, imports); | ||
} else { | ||
return new Promise((resolve, reject) => { | ||
const req = new XMLHttpRequest(); | ||
req.open("GET", url); | ||
req.responseType = "arraybuffer"; | ||
req.onload = () => { | ||
resolve(WebAssembly.instantiate(req.response, imports)); | ||
}; | ||
req.onerror = reject; | ||
req.send(); | ||
}); | ||
} | ||
} | ||
@@ -28,6 +38,3 @@ | ||
const WASM_PAGE_SIZE = 64 * 1024; | ||
const memory = new WebAssembly.Memory({ | ||
initial: TOTAL_MEMORY / WASM_PAGE_SIZE, | ||
maximum: TOTAL_MEMORY / WASM_PAGE_SIZE, | ||
}); | ||
let memory = null; | ||
let dynamicTop = TOTAL_STACK; | ||
@@ -41,3 +48,3 @@ // TODO(Kagami): Grow memory? | ||
// TODO(Kagami): LAME calls exit(-1) on internal error. Would be nice | ||
// to provide custom DEBUGF/ERRORF for easier debugging. By the moment | ||
// to provide custom DEBUGF/ERRORF for easier debugging. Currenty | ||
// those functions do nothing. | ||
@@ -47,14 +54,2 @@ function exit(status) { | ||
} | ||
const Runtime = { | ||
memory: memory, | ||
pow: Math.pow, | ||
exit: exit, | ||
powf: Math.pow, | ||
exp: Math.exp, | ||
sqrtf: Math.sqrt, | ||
cos: Math.cos, | ||
log: Math.log, | ||
sin: Math.sin, | ||
sbrk: sbrk, | ||
}; | ||
@@ -80,7 +75,7 @@ let FFI = null; | ||
const mp3 = new Uint8Array(memory.buffer, mp3_ref, size); | ||
const file = new File([mp3], "rec.mp3", {type: "audio/mpeg"}); | ||
const blob = new Blob([mp3], {type: "audio/mpeg"}); | ||
FFI.vmsg_free(ref); | ||
ref = null; | ||
pcm_l = null; | ||
return file; | ||
return blob; | ||
} | ||
@@ -92,6 +87,29 @@ | ||
case "init": | ||
fetchAndInstantiate(msg.data, {env: Runtime}).then(wasm => { | ||
const { wasmURL, shimURL } = msg.data; | ||
Promise.resolve().then(() => { | ||
if (!self.WebAssembly) { | ||
importScripts(shimURL); | ||
} | ||
memory = new WebAssembly.Memory({ | ||
initial: TOTAL_MEMORY / WASM_PAGE_SIZE, | ||
maximum: TOTAL_MEMORY / WASM_PAGE_SIZE, | ||
}); | ||
return { | ||
memory: memory, | ||
pow: Math.pow, | ||
exit: exit, | ||
powf: Math.pow, | ||
exp: Math.exp, | ||
sqrtf: Math.sqrt, | ||
cos: Math.cos, | ||
log: Math.log, | ||
sin: Math.sin, | ||
sbrk: sbrk, | ||
}; | ||
}).then(Runtime => { | ||
return fetchAndInstantiate(wasmURL, {env: Runtime}) | ||
}).then(wasm => { | ||
FFI = wasm.instance.exports; | ||
postMessage({type: "init", data: null}); | ||
}, err => { | ||
}).catch(err => { | ||
postMessage({type: "init-error", data: err.toString()}); | ||
@@ -107,5 +125,5 @@ }); | ||
case "stop": | ||
const file = vmsg_flush(); | ||
if (!file) return postMessage({type: "error", data: "vmsg_flush"}); | ||
postMessage({type: "stop", data: file}); | ||
const blob = vmsg_flush(); | ||
if (!blob) return postMessage({type: "error", data: "vmsg_flush"}); | ||
postMessage({type: "stop", data: blob}); | ||
break; | ||
@@ -120,3 +138,4 @@ } | ||
// https://stackoverflow.com/a/22582695 | ||
this.wasmURL = new URL(opts.wasmURL || "vmsg.wasm", location).href; | ||
this.wasmURL = new URL(opts.wasmURL || "/static/js/vmsg.wasm", location).href; | ||
this.shimURL = new URL(opts.shimURL || "/static/js/wasm-polyfill.js", location).href; | ||
this.pitch = opts.pitch || 0; | ||
@@ -138,4 +157,4 @@ this.resolve = resolve; | ||
this.workerURL = null; | ||
this.file = null; | ||
this.fileURL = null; | ||
this.blob = null; | ||
this.blobURL = null; | ||
this.tid = 0; | ||
@@ -194,3 +213,3 @@ this.start = 0; | ||
stopBtn.style.display = "none"; | ||
stopBtn.textContent = "◼"; | ||
stopBtn.textContent = "■"; | ||
stopBtn.addEventListener("click", () => this.stopRecording()); | ||
@@ -206,4 +225,4 @@ recordRow.appendChild(stopBtn); | ||
if (audio.paused) { | ||
if (this.fileURL) { | ||
audio.src = this.fileURL; | ||
if (this.blobURL) { | ||
audio.src = this.blobURL; | ||
} | ||
@@ -221,3 +240,3 @@ } else { | ||
saveBtn.disabled = true; | ||
saveBtn.addEventListener("click", () => this.close(this.file)); | ||
saveBtn.addEventListener("click", () => this.close(this.blob)); | ||
recordRow.appendChild(saveBtn); | ||
@@ -278,3 +297,3 @@ | ||
} | ||
close(file) { | ||
close(blob) { | ||
if (this.audio) this.audio.pause(); | ||
@@ -285,7 +304,7 @@ if (this.encNode) this.encNode.disconnect(); | ||
if (this.workerURL) URL.revokeObjectURL(this.workerURL); | ||
if (this.fileURL) URL.revokeObjectURL(this.fileURL); | ||
if (this.blobURL) URL.revokeObjectURL(this.blobURL); | ||
if (this.tid) clearTimeout(this.tid); | ||
this.backdrop.remove(); | ||
if (file) { | ||
this.resolve(file); | ||
if (blob) { | ||
this.resolve(blob); | ||
} else { | ||
@@ -304,19 +323,28 @@ this.reject(new Error("No record made")); | ||
initAudio() { | ||
if (!navigator.mediaDevices.getUserMedia) { | ||
const err = new Error("getUserMedia is not implemented in this browser"); | ||
return Promise.reject(err); | ||
} | ||
return navigator.mediaDevices.getUserMedia({audio: true}).then(stream => { | ||
const audioCtx = this.audioCtx = new AudioContext(); | ||
const getUserMedia = navigator.mediaDevices && navigator.mediaDevices.getUserMedia | ||
? function(constraints) { | ||
return navigator.mediaDevices.getUserMedia(constraints); | ||
} | ||
: function(constraints) { | ||
const oldGetUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; | ||
if (!oldGetUserMedia) { | ||
return Promise.reject(new Error("getUserMedia is not implemented in this browser")); | ||
} | ||
return new Promise(function(resolve, reject) { | ||
oldGetUserMedia.call(navigator, constraints, resolve, reject); | ||
}); | ||
}; | ||
return getUserMedia({audio: true}).then(stream => { | ||
const audioCtx = this.audioCtx = new (window.AudioContext | ||
|| window.webkitAudioContext)(); | ||
const sourceNode = audioCtx.createMediaStreamSource(stream); | ||
const gainNode = this.gainNode = audioCtx.createGain(); | ||
const gainNode = this.gainNode = (audioCtx.createGain | ||
|| audioCtx.createGainNode).call(audioCtx); | ||
sourceNode.connect(gainNode); | ||
const pitchFX = this.pitchFX = new Jungle(audioCtx); | ||
const encNode = this.encNode = audioCtx.createScriptProcessor(0, 1, 1); | ||
encNode.onaudioprocess = (e) => { | ||
const samples = e.inputBuffer.getChannelData(0); | ||
this.worker.postMessage({type: "data", data: samples}); | ||
}; | ||
const encNode = this.encNode = (audioCtx.createScriptProcessor | ||
|| audioCtx.createJavaScriptNode).call(audioCtx, 0, 1, 1); | ||
pitchFX.output.connect(encNode); | ||
@@ -332,3 +360,4 @@ }); | ||
const worker = this.worker = new Worker(workerURL); | ||
worker.postMessage({type: "init", data: this.wasmURL}); | ||
const { wasmURL, shimURL } = this; | ||
worker.postMessage({type: "init", data: {wasmURL, shimURL}}); | ||
return new Promise((resolve, reject) => { | ||
@@ -350,4 +379,4 @@ worker.onmessage = (e) => { | ||
case "stop": | ||
this.file = msg.data; | ||
this.fileURL = URL.createObjectURL(msg.data); | ||
this.blob = msg.data; | ||
this.blobURL = URL.createObjectURL(msg.data); | ||
this.recordBtn.style.display = ""; | ||
@@ -364,5 +393,5 @@ this.stopBtn.style.display = "none"; | ||
this.audio.pause(); | ||
this.file = null; | ||
if (this.fileURL) URL.revokeObjectURL(this.fileURL); | ||
this.fileURL = null; | ||
this.blob = null; | ||
if (this.blobURL) URL.revokeObjectURL(this.blobURL); | ||
this.blobURL = null; | ||
this.start = now(); | ||
@@ -374,2 +403,6 @@ this.updateTime(); | ||
this.worker.postMessage({type: "start", data: this.audioCtx.sampleRate}); | ||
this.encNode.onaudioprocess = (e) => { | ||
const samples = e.inputBuffer.getChannelData(0); | ||
this.worker.postMessage({type: "data", data: samples}); | ||
}; | ||
this.encNode.connect(this.audioCtx.destination); | ||
@@ -382,2 +415,3 @@ } | ||
this.encNode.disconnect(); | ||
this.encNode.onaudioprocess = null; | ||
this.worker.postMessage({type: "stop", data: null}); | ||
@@ -399,5 +433,8 @@ } | ||
* @param {Object=} opts - Options | ||
* @param {string=} opts.wasmURL - URL of the module ("vmsg.wasm" by default) | ||
* @param {number=} opts.pitch - Initial pitch shift (0 by default) | ||
* @return {Promise.<File>} A promise that contains recorded file when fulfilled. | ||
* @param {string=} opts.wasmURL - URL of the module | ||
* ("/static/js/vmsg.wasm" by default) | ||
* @param {string=} opts.shimURL - URL of the WebAssembly polyfill | ||
* ("/static/js/wasm-polyfill.js" by default) | ||
* @param {number=} opts.pitch - Initial pitch shift ([-1, 1], 0 by default) | ||
* @return {Promise.<Blob>} A promise that contains recorded blob when fulfilled. | ||
*/ | ||
@@ -419,5 +456,10 @@ export function record(opts) { | ||
/** | ||
* All available public items. | ||
*/ | ||
export default { record }; | ||
// Borrowed from and slightly modified: | ||
// https://github.com/cwilso/Audio-Input-Effects/blob/master/js/jungle.js | ||
// | ||
// Copyright 2012, Google Inc. | ||
@@ -519,4 +561,4 @@ // All rights reserved. | ||
// Create nodes for the input and output of this "module". | ||
var input = context.createGain(); | ||
var output = context.createGain(); | ||
var input = (context.createGain || context.createGainNode).call(context); | ||
var output = (context.createGain || context.createGainNode).call(context); | ||
this.input = input; | ||
@@ -542,7 +584,7 @@ this.output = output; | ||
// for switching between oct-up and oct-down | ||
var mod1Gain = context.createGain(); | ||
var mod2Gain = context.createGain(); | ||
var mod3Gain = context.createGain(); | ||
var mod1Gain = (context.createGain || context.createGainNode).call(context); | ||
var mod2Gain = (context.createGain || context.createGainNode).call(context); | ||
var mod3Gain = (context.createGain || context.createGainNode).call(context); | ||
mod3Gain.gain.value = 0; | ||
var mod4Gain = context.createGain(); | ||
var mod4Gain = (context.createGain || context.createGainNode).call(context); | ||
mod4Gain.gain.value = 0; | ||
@@ -556,7 +598,7 @@ | ||
// Delay amount for changing pitch. | ||
var modGain1 = context.createGain(); | ||
var modGain2 = context.createGain(); | ||
var modGain1 = (context.createGain || context.createGainNode).call(context); | ||
var modGain2 = (context.createGain || context.createGainNode).call(context); | ||
var delay1 = context.createDelay(); | ||
var delay2 = context.createDelay(); | ||
var delay1 = (context.createDelay || context.createDelayNode).call(context); | ||
var delay2 = (context.createDelay || context.createDelayNode).call(context); | ||
mod1Gain.connect(modGain1); | ||
@@ -578,4 +620,4 @@ mod2Gain.connect(modGain2); | ||
var mix1 = context.createGain(); | ||
var mix2 = context.createGain(); | ||
var mix1 = (context.createGain || context.createGainNode).call(context); | ||
var mix2 = (context.createGain || context.createGainNode).call(context); | ||
mix1.gain.value = 0; | ||
@@ -582,0 +624,0 @@ mix2.gain.value = 0; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
189951
788
115