Repository: Kagami/vmsg
Branch: master
Commit: 623b2940a37f
Files: 20
Total size: 51.3 KB
Directory structure:
gitextract_wa9fkvv4/
├── .babelrc
├── .browserslistrc
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── .gitmodules
├── .npmignore
├── .postcssrc
├── COPYING
├── Makefile
├── README.md
├── demo/
│ ├── index.css
│ ├── index.html
│ └── index.js
├── lame-svn.patch
├── package.json
├── vmsg.c
├── vmsg.css
├── vmsg.d.ts
├── vmsg.js
└── vmsg.wasm
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
================================================
FILE: .browserslistrc
================================================
defaults
================================================
FILE: .github/FUNDING.yml
================================================
custom: https://www.blockchain.com/btc/payment_request?address=3LKKbbi34MHYRQSLV3ZiDGoKgUmCjhTumT&message=Kagami+open+source+projects+support
================================================
FILE: .gitignore
================================================
/node_modules/
/dist/
/.cache/
/_vmsg.*
/vmsg.es5.js
================================================
FILE: .gitmodules
================================================
[submodule "lame-svn"]
path = lame-svn
url = https://github.com/Kagami/lame-svn.git
ignore = dirty
================================================
FILE: .npmignore
================================================
*
!/COPYING
!/vmsg.css
!/vmsg.js
!/vmsg.es5.js
!/vmsg.d.ts
!/vmsg.wasm
================================================
FILE: .postcssrc
================================================
{
"plugins": {
"autoprefixer": true
}
}
================================================
FILE: COPYING
================================================
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
================================================
FILE: Makefile
================================================
export EMCC_WASM_BACKEND = 1
export EMCC_EXPERIMENTAL_USE_LLD = 1
all: vmsg.wasm
lame-svn/lame/dist/lib/libmp3lame.so:
cd lame-svn/lame && \
git reset --hard && \
patch -p2 < ../../lame-svn.patch && \
emconfigure ./configure \
CFLAGS="-DNDEBUG -Oz" \
--prefix="$$(pwd)/dist" \
--host=x86-none-linux \
--disable-static \
\
--disable-gtktest \
--disable-analyzer-hooks \
--disable-decoder \
--disable-frontend \
&& \
emmake make -j8 && \
emmake make install
# WASM backend doesn't support EMSCRIPTEN_KEEPALIVE, see:
# https://github.com/kripken/emscripten/issues/6233
# Output to bare .wasm doesn't work properly so need to create
# intermediate files.
vmsg.wasm: lame-svn/lame/dist/lib/libmp3lame.so vmsg.c
emcc $^ \
-DNDEBUG -Oz --llvm-lto 3 -Ilame-svn/lame/dist/include \
-s WASM=1 \
-s "EXPORTED_FUNCTIONS=['_vmsg_init','_vmsg_encode','_vmsg_flush','_vmsg_free']" \
-o _vmsg.js
cp _vmsg.wasm $@
clean: clean-lame clean-wasm
clean-lame:
cd lame-svn && git clean -dfx
clean-wasm:
rm -f vmsg.wasm _vmsg.*
================================================
FILE: README.md
================================================
# vmsg [](https://www.npmjs.com/package/vmsg)
vmsg is a small library for creating voice messages. While traditional
way of communicating on the web is via text, sometimes it's easier or
rather funnier to express your thoughts just by saying it. Of course it
doesn't require any special support: record your voice with some
standard program, upload to file hosting and share the link. But why
bother with all of that tedious stuff if you can do the same in browser
with a few clicks.
:confetti_ball: :tada: **[DEMO](https://kagami.github.io/vmsg/)** :tada: :confetti_ball:
## Features
* No dependencies, framework-agnostic, can be easily added to any site
* Small: ~73kb gzipped WASM module and ~3kb gzipped JS + CSS
* Uses MP3 format which is widely supported
* Works in all latest browsers
## Supported browsers
* Chrome 32+
* Firefox 27+
* Safari 11+
* Edge 12+
## Usage
```
npm install vmsg --save
```
```js
import { record } from "vmsg";
someButton.onclick = function() {
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[]", blob, "record.mp3");
// fetch("/upload.php", {
// credentials: "include",
// method: "POST",
// body: form,
// }).then(resp => {
// });
});
};
```
That's it! Don't forget to include [vmsg.css](vmsg.css) and
[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 [demo](demo) directory for a more feasible example.
A minimal React example for using Recorder with your own UI can be found [here](https://codesandbox.io/s/v67oz43lm7).
See also [non React demo](https://github.com/addpipe/simple-vmsg-demo) and [Recording mp3 audio in HTML5 using vmsg](https://addpipe.com/blog/recording-mp3-audio-in-html5-using-vmsg-a-webassembly-library-based-on-lame/) article.
## Development
1. Install [Emscripten SDK](https://github.com/juj/emsdk).
2. Install latest LLVM, Clang and LLD with WebAssembly backend, fix
`LLVM_ROOT` variable of Emscripten config.
3. Make sure you have a standard GNU development environment.
4. Activate emsdk environment.
5. ```bash
git clone --recurse-submodules https://github.com/Kagami/vmsg.git && cd vmsg
make clean all
npm install
npm start
```
These instructions are very basic because there're a lot of systems with
different conventions. Docker image would probably be provided to fix it.
## Technical details for nerds
vmsg uses LAME encoder underneath compiled with Emscripten to
WebAssembly module. LAME build is optimized for size, weights only
little more than 70kb gzipped and can be super-efficiently fetched and
parsed by browser. [It's like a small image.](https://twitter.com/wycats/status/942908325775077376)
Access to microphone is implemented with Web Audio API, data samples
sent to Web Worker which is responsibe for loading WebAssembly module
and calling LAME API.
Module is produced with modern LLVM WASM backend and LLD linker which
should become standard soon, also vmsg has own tiny WASM runtime instead
of Emscripten's to decrease overall size and simplify architecture.
Worker code is included in the main JS module so end-user has to care
only about 3 files: `vmsg.js`, `vmsg.css` and `vmsg.wasm`. CSS can be
inlined too but IMO that would be ugly.
In order to support browsers without WebAssembly,
[WebAssembly polyfill](https://github.com/Kagami/wasm-polyfill.js) is
being used. It translates binary module into semantically-equivalent
JavaScript on the fly (almost asm.js compatible but doesn't fully
validate yet) so we don't need separate asm.js build and can use
standard WebAssembly API. It's not as effecient but for audio encoding
should be enough.
**See also:** [Creating WebAssembly-powered library for modern web](https://hackernoon.com/creating-webassembly-powered-library-for-modern-web-846da334f8fc) article.
## Why not MediaRecorder?
[MediaStream Recording API](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API)
is great but:
* Works only in Firefox and Chrome
* Provides little to no options, e.g. VBR quality can't be specified
* Firefox/Chrome encode only to Opus which can't be natively played in Safari and Edge
## 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
vmsg is licensed under [CC0](COPYING).
LAME is licensed under [LGPL](https://github.com/Kagami/lame-svn/blob/master/lame/COPYING).
MP3 patents seems to [have expired since April 23, 2017](https://en.wikipedia.org/wiki/LAME#Patents_and_legal_issues).
================================================
FILE: demo/index.css
================================================
body {
margin: 20px;
background: #ebeaeb;
color: #0a0a0a;
font-size: 16px;
}
body, textarea {
font-family: Helvetica,sans-serif;
line-height: 1.4;
}
textarea {
font-size: 15px;
}
a {
text-decoration: none;
}
.ribbon {
position: absolute;
top: 0;
right: 0;
border: 0;
}
.app {
margin: 0 auto;
max-width: 700px;
}
.post__header {
margin: 0 0 20px 0;
text-align: center;
}
.post__body {
margin-bottom: 20px;
}
.comments {
font-size: 15px;
}
.comment {
margin-bottom: 20px;
padding: 6px 10px;
background: #e4e1e5;
border-radius: 4px;
box-shadow: 1px 1px 4px 0 rgba(59,26,84,.6);
}
.comment__header {
margin-bottom: 3px;
}
.comment__id {
display: inline-block;
margin: 0;
margin-right: 5px;
cursor: default;
user-select: none;
}
.comment__record {
font-size: 20px;
line-height: 17px;
color: #00f;
cursor: pointer;
}
.comment__record:hover {
color: #f00;
}
.comment__body {
margin: 0;
white-space: pre;
}
.reply {
padding: 6px 10px;
background: #e4e1e5;
border-radius: 4px;
box-shadow: 1px 1px 4px 0 rgba(59,26,84,.6);
}
.reply__body {
width: 100%;
height: 80px;
background: none;
border: none;
padding: 0;
resize: none;
outline: none;
}
.reply-control {
margin-right: 5px;
cursor: pointer;
}
.reply-control:disabled {
cursor: default;
}
.reply-record {
cursor: default;
user-select: none;
}
================================================
FILE: demo/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vmsg demo</title>
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="../vmsg.css">
</head>
<body>
<a href="https://github.com/Kagami/vmsg">
<img class="ribbon" src="ribbon.png" alt="Fork me on GitHub">
</a>
<main class="app"></main>
<script src="index.js"></script>
</body>
</html>
================================================
FILE: demo/index.js
================================================
import React from "react";
import ReactDOM from "react-dom";
import vmsg from "..";
// https://github.com/parcel-bundler/parcel/issues/289
if (module.hot) {
module.hot.dispose(() => {
location.reload();
});
}
class Post extends React.Component {
render() {
return (
<article className="post">
<h3 className="post__header">Example post</h3>
<section className="post__body">
So here is a simple demo of <a href="https://github.com/Kagami/vmsg">vmsg library</a>. Imagine this is a blog post or forum thread. Below you can leave text comments, as usual. But there is one more button: “Record”. If you press it vmsg library will open you a microphone recording form. Resulting record will automatically be encoded to MP3 so file won't weight too much. So you can easily share your voice messages even on mobile network and server needs to neither waste CPU time by encoding to MP3 by itself nor using a lot of disk space to store records.
</section>
<Comments />
</article>
);
}
}
class Comments extends React.Component {
constructor(props) {
super(props);
this.state = {
comments: [],
};
this.idCounter = 0;
}
handleReplySend = (comment) => {
const { comments } = this.state;
comment = { ...comment, id: this.idCounter++ };
this.setState({comments: comments.concat(comment)});
};
render() {
const { comments } = this.state;
return (
<aside className="comments">
{comments.map(props =>
<Comment key={props.id} {...props} />
)}
<Reply onSend={this.handleReplySend} />
</aside>
);
}
}
class Comment extends React.Component {
constructor(props) {
super(props);
if (props.record) {
this.audio = new Audio();
this.audio.src = URL.createObjectURL(props.record);
}
}
handleRecordOver = () => {
this.audio.currentTime = 0;
this.audio.play();
};
handleRecordOut = () => {
this.audio.pause();
};
handleRecordClick = () => {
const a = document.createElement("a");
if (!("download" in a)) {
window.open(this.audio.src);
return;
}
a.href = this.audio.src;
a.download = "record.mp3";
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
};
render() {
const { id, body } = this.props;
return (
<article className="comment">
<header className="comment__header">
<h5 className="comment__id">
Comment #{id + 1}
</h5>
{this.renderRecord()}
</header>
<blockquote className="comment__body">
{body}
</blockquote>
</article>
);
}
renderRecord() {
const { record } = this.props;
if (!record) return null;
return (
<span
className="comment__record"
title="Hover to play / click to download"
onMouseOver={this.handleRecordOver}
onMouseOut={this.handleRecordOut}
onClick={this.handleRecordClick}
>
♫
</span>
)
}
}
class Reply extends React.Component {
constructor(props) {
super(props);
this.state = {
body: "",
record: null,
};
}
handleBodyChange = (e) => {
this.setState({body: e.target.value});
};
handleRecord = () => {
vmsg.record({
wasmURL: require("../vmsg.wasm"),
shimURL: "https://unpkg.com/wasm-polyfill.js@0.2.0/wasm-polyfill.js",
}).then(record => {
this.setState({record});
});
};
handleSend = () => {
const { body, record } = this.state;
this.setState({body: "", record: null});
this.props.onSend({body, record});
};
render() {
const { body, record } = this.state;
return (
<article className="reply">
<textarea
className="reply__body"
placeholder="Enter your comment here…"
autoFocus
value={body}
onChange={this.handleBodyChange}
/>
<footer className="reply__controls">
<button className="reply-control" disabled={!body} onClick={this.handleSend}>
Send
</button>
<button className="reply-control" onClick={this.handleRecord}>
Record
</button>
{this.renderRecord()}
</footer>
</article>
);
}
renderRecord() {
const { record } = this.state;
if (!record) return null;
const size = (record.size / 1024).toFixed(1) + "KB";
return (
<span className="reply-record">
record.mp3 ({size})
</span>
);
}
}
ReactDOM.render(<Post/>, document.querySelector(".app"));
================================================
FILE: lame-svn.patch
================================================
diff --git a/lame/configure b/lame/configure
index 52dbf02f..b0883041 100755
--- a/lame/configure
+++ b/lame/configure
@@ -14968,7 +14968,7 @@ fi
done
-if test "X${ac_cv_func_strtol}" != "Xyes"; then
+if false && test "X${ac_cv_func_strtol}" != "Xyes"; then
as_fn_error $? "function strtol is mandatory" "$LINENO" 5
fi
diff --git a/lame/libmp3lame/util.c b/lame/libmp3lame/util.c
index 69ec8577..94eacbee 100644
--- a/lame/libmp3lame/util.c
+++ b/lame/libmp3lame/util.c
@@ -713,8 +713,6 @@ fill_buffer(lame_internal_flags * gfc,
void
lame_report_def(const char *format, va_list args)
{
- (void) vfprintf(stderr, format, args);
- fflush(stderr); /* an debug function should flush immediately */
}
void
================================================
FILE: package.json
================================================
{
"name": "vmsg",
"version": "0.4.0",
"description": "Library for creating voice messages",
"main": "vmsg.js",
"typings": "vmsg.d.ts",
"scripts": {
"start": "parcel demo/index.html",
"prepare": "babel vmsg.js -o vmsg.es5.js",
"demo": "D=`mktemp -d` && parcel build demo/index.html --out-dir \"$D\" --public-url ./ && git checkout gh-pages && rm `git ls-files *` && mv \"$D\"/* . && rmdir \"$D\" && git add -A && git commit -m 'Update demo'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Kagami/vmsg.git"
},
"keywords": [
"voice",
"voice message",
"emscripten",
"webassembly",
"lame",
"mp3"
],
"author": "Kagami Hiiragi",
"license": "CC0-1.0",
"bugs": {
"url": "https://github.com/Kagami/vmsg/issues"
},
"homepage": "https://github.com/Kagami/vmsg#readme",
"parcelDisableLoaders": [
"wasm"
],
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.3.0",
"@babel/preset-env": "^7.3.1",
"autoprefixer": "^9.4.7",
"parcel-bundler": "^1.11.0",
"parcel-plugin-disable-loaders": "^1.0.3",
"react": "^16.8.1",
"react-dom": "^16.8.1"
}
}
================================================
FILE: vmsg.c
================================================
#include <stdlib.h>
#include <stdint.h>
#include <lame/lame.h>
#define WASM_EXPORT __attribute__((visibility("default")))
#define MAX_SAMPLES 16384
#define BUF_SIZE (MAX_SAMPLES * 1.25 + 7200)
typedef struct {
// Public fields.
float *pcm_l;
uint8_t *mp3;
uint32_t size;
// Private fields. Should not be touched by API user.
uint32_t max_size;
lame_global_flags *gfp;
} vmsg;
void vmsg_free(vmsg *v);
WASM_EXPORT
vmsg *vmsg_init(int rate) {
vmsg *v = calloc(1, sizeof (vmsg));
if (!v)
goto err;
v->size = 0;
// NOTE(Kagami): Must be >= BUF_SIZE.
// Reserve 1MB for encoded data initially.
v->max_size = 1024 * 1024;
v->mp3 = malloc(v->max_size);
if (!v->mp3)
goto err;
v->pcm_l = malloc(MAX_SAMPLES * sizeof(float));
if (!v->pcm_l)
goto err;
v->gfp = lame_init();
if (!v->gfp)
goto err;
lame_set_mode(v->gfp, MONO);
lame_set_num_channels(v->gfp, 1);
lame_set_in_samplerate(v->gfp, rate);
lame_set_VBR(v->gfp, vbr_default);
lame_set_VBR_quality(v->gfp, 5);
if (lame_init_params(v->gfp) < 0)
goto err;
return v;
err:
vmsg_free(v);
return NULL;
}
static int fix_mp3_size(vmsg *v) {
if (v->size + BUF_SIZE > v->max_size) {
v->max_size *= 2;
v->mp3 = realloc(v->mp3, v->max_size);
if (!v->mp3)
return -1;
}
return 0;
}
WASM_EXPORT
int vmsg_encode(vmsg *v, int nsamples) {
if (nsamples > MAX_SAMPLES)
return -1;
if (fix_mp3_size(v) < 0)
return -1;
uint8_t *buf = v->mp3 + v->size;
int n = lame_encode_buffer_ieee_float(
v->gfp, v->pcm_l, NULL, nsamples, buf, BUF_SIZE);
if (n < 0)
return n;
v->size += n;
return 0;
}
WASM_EXPORT
int vmsg_flush(vmsg *v) {
if (fix_mp3_size(v) < 0)
return -1;
uint8_t *buf = v->mp3 + v->size;
int n = lame_encode_flush(v->gfp, buf, BUF_SIZE);
if (n < 0)
return -1;
v->size += n;
n = lame_get_lametag_frame(v->gfp, v->mp3, BUF_SIZE);
if (n < 0)
return -1;
return 0;
}
WASM_EXPORT
void vmsg_free(vmsg *v) {
if (v) {
lame_close(v->gfp);
free(v->pcm_l);
free(v->mp3);
free(v);
}
}
================================================
FILE: vmsg.css
================================================
.vmsg-backdrop {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
background: rgba(0,0,0,.7);
align-items: center;
justify-content: center;
}
.vmsg-popup {
box-sizing: border-box;
width: 250px;
padding: 10px;
border-radius: 4px;
background: #e4e1e5;
box-shadow: 1px 1px 4px 0 rgba(59,26,84,.6);
display: flex;
flex-direction: column;
justify-content: center;
font-family: Helvetica,sans-serif;
font-size: 14px;
line-height: 1.4;
color: #0a0a0a;
}
.vmsg-progress {
width: 40%;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.vmsg-progress-dot {
width: 15px;
height: 15px;
border-radius: 50%;
animation: vmsg-progress 1s linear infinite;
}
.vmsg-progress-dot:nth-child(2) {
animation-delay: -0.8s;
}
.vmsg-progress-dot:nth-child(3) {
animation-delay: -0.6s;
}
@keyframes vmsg-progress {
0%, 60%, 100% {
background: none;
}
30% {
background: #9e85ad;
}
}
.vmsg-error {
font-weight: bold;
text-align: center;
}
.vmsg-record-row {
display: flex;
justify-content: space-between;
}
.vmsg-button {
min-width: 40px;
line-height: 30px;
padding: 0;
background: transparent;
border: 1px solid #ccc;
font-family: Helvetica,sans-serif;
cursor: pointer;
outline: none;
user-select: none;
}
.vmsg-button:disabled {
cursor: default;
color: #999;
}
.vmsg-button:not(:disabled):hover {
border-color: #9e85ad;
}
.vmsg-button::-moz-focus-inner {
border: 0;
}
.vmsg-record-button {
font-size: 30px;
color: #f00;
}
.vmsg-stop-button {
font-size: 25px;
color: #000;
}
.vmsg-save-button {
font-size: 25px;
color: #090;
}
.vmsg-timer {
line-height: 32px;
font-weight: bold;
color: #333;
cursor: pointer;
user-select: none;
}
.vmsg-slider-wrapper {
position: relative;
margin-top: 3px;
}
.vmsg-slider-wrapper::after {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
line-height: 14px;
text-align: center;
color: #999;
pointer-events: none;
}
.vmsg-pitch-slider-wrapper::after {
content: "pitch";
}
.vmsg-gain-slider-wrapper::after {
content: "gain";
}
.vmsg-slider {
display: block;
width: 100%;
height: 16px;
margin: 0;
padding: 0;
outline: none;
background: none;
-webkit-appearance: none;
}
.vmsg-slider::-moz-focus-outer {
border: 0;
}
.vmsg-slider::-webkit-slider-runnable-track {
box-sizing: border-box;
height: 16px;
background: none;
border: 1px solid #ccc;
}
.vmsg-slider::-moz-range-track {
box-sizing: border-box;
height: 16px;
background: none;
border: 1px solid #ccc;
}
.vmsg-slider::-ms-track {
box-sizing: border-box;
height: 16px;
background: none;
border: 1px solid #ccc;
}
.vmsg-slider::-webkit-slider-thumb {
width: 39px;
height: 14px;
background: #ccc;
cursor: pointer;
-webkit-appearance: none;
}
.vmsg-slider::-moz-range-thumb {
width: 40px;
height: 14px;
background: #ccc;
border: none;
border-radius: 0;
cursor: pointer;
}
.vmsg-slider::-ms-thumb {
width: 39px;
height: 14px;
background: #ccc;
cursor: pointer;
}
.vmsg-slider::-webkit-slider-thumb:hover {
background: #999;
}
.vmsg-slider::-moz-range-thumb:hover {
background: #999;
}
.vmsg-slider::-ms-thumb:hover {
background: #999;
}
.vmsg-slider::-ms-tooltip {
display: none;
}
================================================
FILE: vmsg.d.ts
================================================
declare module "vmsg" {
interface RecordOptions {
wasmURL?: string;
shimURL?: string;
pitch?: number;
}
export class Recorder {
constructor(opts: RecordOptions);
stopRecording(): Promise<Blob>;
initAudio(): Promise<void>;
initWorker(): Promise<void>;
init(): Promise<void>;
startRecording(): void;
close(): void;
}
interface Exports {
record: (opts?: RecordOptions) => Promise<Blob>;
}
const exports: Exports;
export default exports;
}
================================================
FILE: vmsg.js
================================================
/* eslint-disable */
function pad2(n) {
n |= 0;
return n < 10 ? `0${n}` : `${Math.min(n, 99)}`;
}
function inlineWorker() {
// TODO(Kagami): Cache compiled module in IndexedDB? It works in FF
// and Edge, see: https://github.com/mdn/webassembly-examples/issues/4
// Though gzipped WASM module currently weights ~70kb so it should be
// perfectly cached by the browser itself.
function fetchAndInstantiate(url, imports) {
if (!WebAssembly.instantiateStreaming) return fetchAndInstantiateFallback(url, imports);
const req = fetch(url, {credentials: "same-origin"});
return WebAssembly.instantiateStreaming(req, imports).catch(err => {
// https://github.com/Kagami/vmsg/issues/11
if (err.message && err.message.indexOf("Argument 0 must be provided and must be a Response") > 0) {
return fetchAndInstantiateFallback(url, imports);
} else {
throw err;
}
});
}
function fetchAndInstantiateFallback(url, imports) {
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();
});
}
// Must be in sync with emcc settings!
const TOTAL_STACK = 5 * 1024 * 1024;
const TOTAL_MEMORY = 16 * 1024 * 1024;
const WASM_PAGE_SIZE = 64 * 1024;
let memory = null;
let dynamicTop = TOTAL_STACK;
// TODO(Kagami): Grow memory?
function sbrk(increment) {
const oldDynamicTop = dynamicTop;
dynamicTop += increment;
return oldDynamicTop;
}
// TODO(Kagami): LAME calls exit(-1) on internal error. Would be nice
// to provide custom DEBUGF/ERRORF for easier debugging. Currenty
// those functions do nothing.
function exit(status) {
postMessage({type: "internal-error", data: status});
}
let FFI = null;
let ref = null;
let pcm_l = null;
function vmsg_init(rate) {
ref = FFI.vmsg_init(rate);
if (!ref) return false;
const pcm_l_ref = new Uint32Array(memory.buffer, ref, 1)[0];
pcm_l = new Float32Array(memory.buffer, pcm_l_ref);
return true;
}
function vmsg_encode(data) {
pcm_l.set(data);
return FFI.vmsg_encode(ref, data.length) >= 0;
}
function vmsg_flush() {
if (FFI.vmsg_flush(ref) < 0) return null;
const mp3_ref = new Uint32Array(memory.buffer, ref + 4, 1)[0];
const size = new Uint32Array(memory.buffer, ref + 8, 1)[0];
const mp3 = new Uint8Array(memory.buffer, mp3_ref, size);
const blob = new Blob([mp3], {type: "audio/mpeg"});
FFI.vmsg_free(ref);
ref = null;
pcm_l = null;
return blob;
}
// https://github.com/brion/min-wasm-fail
function testSafariWebAssemblyBug() {
const bin = new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,1,127,1,127,3,2,1,0,5,3,1,0,1,7,8,1,4,116,101,115,116,0,0,10,16,1,14,0,32,0,65,1,54,2,0,32,0,40,2,0,11]);
const mod = new WebAssembly.Module(bin);
const inst = new WebAssembly.Instance(mod, {});
// test storing to and loading from a non-zero location via a parameter.
// Safari on iOS 11.2.5 returns 0 unexpectedly at non-zero locations
return (inst.exports.test(4) !== 0);
}
onmessage = (e) => {
const msg = e.data;
switch (msg.type) {
case "init":
const { wasmURL, shimURL } = msg.data;
Promise.resolve().then(() => {
if (self.WebAssembly && !testSafariWebAssemblyBug()) {
delete self.WebAssembly;
}
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});
}).catch(err => {
postMessage({type: "init-error", data: err.toString()});
});
break;
case "start":
if (!vmsg_init(msg.data)) return postMessage({type: "error", data: "vmsg_init"});
break;
case "data":
if (!vmsg_encode(msg.data)) return postMessage({type: "error", data: "vmsg_encode"});
break;
case "stop":
const blob = vmsg_flush();
if (!blob) return postMessage({type: "error", data: "vmsg_flush"});
postMessage({type: "stop", data: blob});
break;
}
};
}
export class Recorder {
constructor(opts = {}, onStop = null) {
// Can't use relative URL in blob worker, see:
// https://stackoverflow.com/a/22582695
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.onStop = onStop;
this.pitch = opts.pitch || 0;
this.stream = null;
this.audioCtx = null;
this.gainNode = null;
this.pitchFX = null;
this.encNode = null;
this.worker = null;
this.workerURL = null;
this.blob = null;
this.blobURL = null;
this.resolve = null;
this.reject = null;
Object.seal(this);
}
close() {
if (this.encNode) this.encNode.disconnect();
if (this.encNode) this.encNode.onaudioprocess = null;
if (this.stream) this.stopTracks();
if (this.audioCtx) this.audioCtx.close();
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
if (this.workerURL) URL.revokeObjectURL(this.workerURL);
if (this.blobURL) URL.revokeObjectURL(this.blobURL);
}
// Without pitch shift:
// [sourceNode] -> [gainNode] -> [encNode] -> [audioCtx.destination]
// |
// -> [worker]
// With pitch shift:
// [sourceNode] -> [gainNode] -> [pitchFX] -> [encNode] -> [audioCtx.destination]
// |
// -> [worker]
initAudio() {
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) => {
this.stream = stream;
const audioCtx = this.audioCtx = new (window.AudioContext
|| window.webkitAudioContext)();
const sourceNode = audioCtx.createMediaStreamSource(stream);
const gainNode = this.gainNode = (audioCtx.createGain
|| audioCtx.createGainNode).call(audioCtx);
gainNode.gain.value = 1;
sourceNode.connect(gainNode);
const pitchFX = this.pitchFX = new Jungle(audioCtx);
pitchFX.setPitchOffset(this.pitch);
const encNode = this.encNode = (audioCtx.createScriptProcessor
|| audioCtx.createJavaScriptNode).call(audioCtx, 0, 1, 1);
pitchFX.output.connect(encNode);
gainNode.connect(this.pitch === 0 ? encNode : pitchFX.input);
});
}
initWorker() {
if (this.worker) return Promise.resolve();
// https://stackoverflow.com/a/19201292
const blob = new Blob(
["(", inlineWorker.toString(), ")()"],
{type: "application/javascript"});
const workerURL = this.workerURL = URL.createObjectURL(blob);
const worker = this.worker = new Worker(workerURL);
const { wasmURL, shimURL } = this;
worker.postMessage({type: "init", data: {wasmURL, shimURL}});
return new Promise((resolve, reject) => {
worker.onmessage = (e) => {
const msg = e.data;
switch (msg.type) {
case "init":
resolve();
break;
case "init-error":
this.close();
reject(new Error(msg.data));
break;
// TODO(Kagami): Error handling.
case "error":
case "internal-error":
this.close();
console.error("Worker error:", msg.data);
if (this.reject) this.reject(msg.data);
break;
case "stop":
this.blob = msg.data;
this.blobURL = URL.createObjectURL(msg.data);
if (this.onStop) this.onStop();
if (this.resolve) this.resolve(this.blob);
break;
}
}
});
}
init() {
return this.initAudio().then(this.initWorker.bind(this));
}
startRecording() {
if (!this.stream) throw new Error("missing audio initialization");
if (!this.worker) throw new Error("missing worker initialization");
this.blob = null;
if (this.blobURL) URL.revokeObjectURL(this.blobURL);
this.blobURL = null;
this.resolve = null;
this.reject = null;
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);
}
stopRecording() {
if (!this.stream) throw new Error("missing audio initialization");
if (!this.worker) throw new Error("missing worker initialization");
this.encNode.disconnect();
this.encNode.onaudioprocess = null;
this.stopTracks();
this.audioCtx.close();
this.worker.postMessage({type: "stop", data: null});
return new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
stopTracks() {
// Might be missed in Safari and old FF/Chrome per MDN.
if (this.stream.getTracks) {
// Hide browser's recording indicator.
this.stream.getTracks().forEach((track) => track.stop());
}
}
}
export class Form {
constructor(opts = {}, resolve, reject) {
this.recorder = new Recorder(opts, this.onStop.bind(this));
this.resolve = resolve;
this.reject = reject;
this.backdrop = null;
this.popup = null;
this.recordBtn = null;
this.stopBtn = null;
this.timer = null;
this.audio = null;
this.saveBtn = null;
this.tid = 0;
this.start = 0;
Object.seal(this);
this.recorder.initAudio()
.then(() => this.drawInit())
.then(() => this.recorder.initWorker())
.then(() => this.drawAll())
.catch((err) => this.drawError(err));
}
drawInit() {
if (this.backdrop) return;
const backdrop = this.backdrop = document.createElement("div");
backdrop.className = "vmsg-backdrop";
backdrop.addEventListener("click", () => this.close(null));
const popup = this.popup = document.createElement("div");
popup.className = "vmsg-popup";
popup.addEventListener("click", (e) => e.stopPropagation());
const progress = document.createElement("div");
progress.className = "vmsg-progress";
for (let i = 0; i < 3; i++) {
const progressDot = document.createElement("div");
progressDot.className = "vmsg-progress-dot";
progress.appendChild(progressDot);
}
popup.appendChild(progress);
backdrop.appendChild(popup);
document.body.appendChild(backdrop);
}
drawTime(msecs) {
const secs = Math.round(msecs / 1000);
this.timer.textContent = pad2(secs / 60) + ":" + pad2(secs % 60);
}
drawAll() {
this.drawInit();
this.clearAll();
const recordRow = document.createElement("div");
recordRow.className = "vmsg-record-row";
this.popup.appendChild(recordRow);
const recordBtn = this.recordBtn = document.createElement("button");
recordBtn.className = "vmsg-button vmsg-record-button";
recordBtn.textContent = "●";
recordBtn.title = "Start Recording";
recordBtn.addEventListener("click", () => this.startRecording());
recordRow.appendChild(recordBtn);
const stopBtn = this.stopBtn = document.createElement("button");
stopBtn.className = "vmsg-button vmsg-stop-button";
stopBtn.style.display = "none";
stopBtn.textContent = "■";
stopBtn.title = "Stop Recording";
stopBtn.addEventListener("click", () => this.stopRecording());
recordRow.appendChild(stopBtn);
const audio = this.audio = new Audio();
audio.autoplay = true;
const timer = this.timer = document.createElement("span");
timer.className = "vmsg-timer";
timer.title = "Preview Recording";
timer.addEventListener("click", () => {
if (audio.paused) {
if (this.recorder.blobURL) {
audio.src = this.recorder.blobURL;
}
} else {
audio.pause();
}
});
this.drawTime(0);
recordRow.appendChild(timer);
const saveBtn = this.saveBtn = document.createElement("button");
saveBtn.className = "vmsg-button vmsg-save-button";
saveBtn.textContent = "✓";
saveBtn.title = "Save Recording";
saveBtn.disabled = true;
saveBtn.addEventListener("click", () => this.close(this.recorder.blob));
recordRow.appendChild(saveBtn);
const gainWrapper = document.createElement("div");
gainWrapper.className = "vmsg-slider-wrapper vmsg-gain-slider-wrapper";
const gainSlider = document.createElement("input");
gainSlider.className = "vmsg-slider vmsg-gain-slider";
gainSlider.setAttribute("type", "range");
gainSlider.min = 0;
gainSlider.max = 2;
gainSlider.step = 0.2;
gainSlider.value = 1;
gainSlider.onchange = () => {
const gain = +gainSlider.value;
this.recorder.gainNode.gain.value = gain;
};
gainWrapper.appendChild(gainSlider);
this.popup.appendChild(gainWrapper);
const pitchWrapper = document.createElement("div");
pitchWrapper.className = "vmsg-slider-wrapper vmsg-pitch-slider-wrapper";
const pitchSlider = document.createElement("input");
pitchSlider.className = "vmsg-slider vmsg-pitch-slider";
pitchSlider.setAttribute("type", "range");
pitchSlider.min = -1;
pitchSlider.max = 1;
pitchSlider.step = 0.2;
pitchSlider.value = this.recorder.pitch;
pitchSlider.onchange = () => {
const pitch = +pitchSlider.value;
this.recorder.pitchFX.setPitchOffset(pitch);
this.recorder.gainNode.disconnect();
this.recorder.gainNode.connect(
pitch === 0 ? this.recorder.encNode : this.recorder.pitchFX.input
);
};
pitchWrapper.appendChild(pitchSlider);
this.popup.appendChild(pitchWrapper);
recordBtn.focus();
}
drawError(err) {
console.error(err);
this.drawInit();
this.clearAll();
const error = document.createElement("div");
error.className = "vmsg-error";
error.textContent = err.toString();
this.popup.appendChild(error);
}
clearAll() {
if (!this.popup) return;
this.popup.innerHTML = "";
}
close(blob) {
if (this.audio) this.audio.pause();
if (this.tid) clearTimeout(this.tid);
this.recorder.close();
this.backdrop.remove();
if (blob) {
this.resolve(blob);
} else {
this.reject(new Error("No record made"));
}
}
onStop() {
this.recordBtn.style.display = "";
this.stopBtn.style.display = "none";
this.stopBtn.disabled = false;
this.saveBtn.disabled = false;
}
startRecording() {
this.audio.pause();
this.start = Date.now();
this.updateTime();
this.recordBtn.style.display = "none";
this.stopBtn.style.display = "";
this.saveBtn.disabled = true;
this.stopBtn.focus();
this.recorder.startRecording();
}
stopRecording() {
clearTimeout(this.tid);
this.tid = 0;
this.stopBtn.disabled = true;
this.recordBtn.focus();
this.recorder.stopRecording();
}
updateTime() {
// NOTE(Kagami): We can do this in `onaudioprocess` but that would
// run too often and create unnecessary DOM updates.
this.drawTime(Date.now() - this.start);
this.tid = setTimeout(() => this.updateTime(), 300);
}
}
let shown = false;
/**
* Record a new voice message.
*
* @param {Object=} opts - Options
* @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.
*/
export function record(opts) {
return new Promise((resolve, reject) => {
if (shown) throw new Error("Record form is already opened");
shown = true;
new Form(opts, resolve, reject);
// Use `.finally` once it's available in Safari and Edge.
}).then(result => {
shown = false;
return result;
}, err => {
shown = false;
throw err;
});
}
/**
* All available public items.
*/
export default { Recorder, Form, record };
// Borrowed from and slightly modified:
// https://github.com/cwilso/Audio-Input-Effects/blob/master/js/jungle.js
// Copyright 2012, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
const delayTime = 0.100;
const fadeTime = 0.050;
const bufferTime = 0.100;
function createFadeBuffer(context, activeTime, fadeTime) {
var length1 = activeTime * context.sampleRate;
var length2 = (activeTime - 2*fadeTime) * context.sampleRate;
var length = length1 + length2;
var buffer = context.createBuffer(1, length, context.sampleRate);
var p = buffer.getChannelData(0);
var fadeLength = fadeTime * context.sampleRate;
var fadeIndex1 = fadeLength;
var fadeIndex2 = length1 - fadeLength;
// 1st part of cycle
for (var i = 0; i < length1; ++i) {
var value;
if (i < fadeIndex1) {
value = Math.sqrt(i / fadeLength);
} else if (i >= fadeIndex2) {
value = Math.sqrt(1 - (i - fadeIndex2) / fadeLength);
} else {
value = 1;
}
p[i] = value;
}
// 2nd part
for (var i = length1; i < length; ++i) {
p[i] = 0;
}
return buffer;
}
function createDelayTimeBuffer(context, activeTime, fadeTime, shiftUp) {
var length1 = activeTime * context.sampleRate;
var length2 = (activeTime - 2*fadeTime) * context.sampleRate;
var length = length1 + length2;
var buffer = context.createBuffer(1, length, context.sampleRate);
var p = buffer.getChannelData(0);
// 1st part of cycle
for (var i = 0; i < length1; ++i) {
if (shiftUp)
// This line does shift-up transpose
p[i] = (length1-i)/length;
else
// This line does shift-down transpose
p[i] = i / length1;
}
// 2nd part
for (var i = length1; i < length; ++i) {
p[i] = 0;
}
return buffer;
}
function Jungle(context) {
this.context = context;
// Create nodes for the input and output of this "module".
var input = (context.createGain || context.createGainNode).call(context);
var output = (context.createGain || context.createGainNode).call(context);
this.input = input;
this.output = output;
// Delay modulation.
var mod1 = context.createBufferSource();
var mod2 = context.createBufferSource();
var mod3 = context.createBufferSource();
var mod4 = context.createBufferSource();
this.shiftDownBuffer = createDelayTimeBuffer(context, bufferTime, fadeTime, false);
this.shiftUpBuffer = createDelayTimeBuffer(context, bufferTime, fadeTime, true);
mod1.buffer = this.shiftDownBuffer;
mod2.buffer = this.shiftDownBuffer;
mod3.buffer = this.shiftUpBuffer;
mod4.buffer = this.shiftUpBuffer;
mod1.loop = true;
mod2.loop = true;
mod3.loop = true;
mod4.loop = true;
// for switching between oct-up and oct-down
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 || context.createGainNode).call(context);
mod4Gain.gain.value = 0;
mod1.connect(mod1Gain);
mod2.connect(mod2Gain);
mod3.connect(mod3Gain);
mod4.connect(mod4Gain);
// Delay amount for changing pitch.
var modGain1 = (context.createGain || context.createGainNode).call(context);
var modGain2 = (context.createGain || context.createGainNode).call(context);
var delay1 = (context.createDelay || context.createDelayNode).call(context);
var delay2 = (context.createDelay || context.createDelayNode).call(context);
mod1Gain.connect(modGain1);
mod2Gain.connect(modGain2);
mod3Gain.connect(modGain1);
mod4Gain.connect(modGain2);
modGain1.connect(delay1.delayTime);
modGain2.connect(delay2.delayTime);
// Crossfading.
var fade1 = context.createBufferSource();
var fade2 = context.createBufferSource();
var fadeBuffer = createFadeBuffer(context, bufferTime, fadeTime);
fade1.buffer = fadeBuffer
fade2.buffer = fadeBuffer;
fade1.loop = true;
fade2.loop = true;
var mix1 = (context.createGain || context.createGainNode).call(context);
var mix2 = (context.createGain || context.createGainNode).call(context);
mix1.gain.value = 0;
mix2.gain.value = 0;
fade1.connect(mix1.gain);
fade2.connect(mix2.gain);
// Connect processing graph.
input.connect(delay1);
input.connect(delay2);
delay1.connect(mix1);
delay2.connect(mix2);
mix1.connect(output);
mix2.connect(output);
// Start
var t = context.currentTime + 0.050;
var t2 = t + bufferTime - fadeTime;
mod1.start(t);
mod2.start(t2);
mod3.start(t);
mod4.start(t2);
fade1.start(t);
fade2.start(t2);
this.mod1 = mod1;
this.mod2 = mod2;
this.mod1Gain = mod1Gain;
this.mod2Gain = mod2Gain;
this.mod3Gain = mod3Gain;
this.mod4Gain = mod4Gain;
this.modGain1 = modGain1;
this.modGain2 = modGain2;
this.fade1 = fade1;
this.fade2 = fade2;
this.mix1 = mix1;
this.mix2 = mix2;
this.delay1 = delay1;
this.delay2 = delay2;
this.setDelay(delayTime);
}
Jungle.prototype.setDelay = function(delayTime) {
this.modGain1.gain.setTargetAtTime(0.5*delayTime, 0, 0.010);
this.modGain2.gain.setTargetAtTime(0.5*delayTime, 0, 0.010);
};
Jungle.prototype.setPitchOffset = function(mult) {
if (mult>0) { // pitch up
this.mod1Gain.gain.value = 0;
this.mod2Gain.gain.value = 0;
this.mod3Gain.gain.value = 1;
this.mod4Gain.gain.value = 1;
} else { // pitch down
this.mod1Gain.gain.value = 1;
this.mod2Gain.gain.value = 1;
this.mod3Gain.gain.value = 0;
this.mod4Gain.gain.value = 0;
}
this.setDelay(delayTime*Math.abs(mult));
};
gitextract_wa9fkvv4/ ├── .babelrc ├── .browserslistrc ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .postcssrc ├── COPYING ├── Makefile ├── README.md ├── demo/ │ ├── index.css │ ├── index.html │ └── index.js ├── lame-svn.patch ├── package.json ├── vmsg.c ├── vmsg.css ├── vmsg.d.ts ├── vmsg.js └── vmsg.wasm
SYMBOL INDEX (49 symbols across 4 files)
FILE: demo/index.js
class Post (line 12) | class Post extends React.Component {
method render (line 13) | render() {
class Comments (line 26) | class Comments extends React.Component {
method constructor (line 27) | constructor(props) {
method render (line 39) | render() {
class Comment (line 52) | class Comment extends React.Component {
method constructor (line 53) | constructor(props) {
method render (line 80) | render() {
method renderRecord (line 96) | renderRecord() {
class Reply (line 113) | class Reply extends React.Component {
method constructor (line 114) | constructor(props) {
method render (line 137) | render() {
method renderRecord (line 160) | renderRecord() {
FILE: vmsg.c
type vmsg (line 9) | typedef struct {
function WASM_EXPORT (line 21) | WASM_EXPORT
function fix_mp3_size (line 58) | static int fix_mp3_size(vmsg *v) {
function WASM_EXPORT (line 68) | WASM_EXPORT
function WASM_EXPORT (line 86) | WASM_EXPORT
function WASM_EXPORT (line 104) | WASM_EXPORT
FILE: vmsg.d.ts
type RecordOptions (line 2) | interface RecordOptions {
class Recorder (line 8) | class Recorder {
type Exports (line 18) | interface Exports {
FILE: vmsg.js
function pad2 (line 3) | function pad2(n) {
function inlineWorker (line 8) | function inlineWorker() {
class Recorder (line 146) | class Recorder {
method constructor (line 147) | constructor(opts = {}, onStop = null) {
method close (line 168) | close() {
method initAudio (line 189) | initAudio() {
method initWorker (line 226) | initWorker() {
method init (line 265) | init() {
method startRecording (line 269) | startRecording() {
method stopRecording (line 285) | stopRecording() {
method stopTracks (line 299) | stopTracks() {
class Form (line 308) | class Form {
method constructor (line 309) | constructor(opts = {}, resolve, reject) {
method drawInit (line 331) | drawInit() {
method drawTime (line 354) | drawTime(msecs) {
method drawAll (line 359) | drawAll() {
method drawError (line 446) | drawError(err) {
method clearAll (line 456) | clearAll() {
method close (line 461) | close(blob) {
method onStop (line 473) | onStop() {
method startRecording (line 480) | startRecording() {
method stopRecording (line 491) | stopRecording() {
method updateTime (line 499) | updateTime() {
function record (line 520) | function record(opts) {
function createFadeBuffer (line 576) | function createFadeBuffer(context, activeTime, fadeTime) {
function createDelayTimeBuffer (line 611) | function createDelayTimeBuffer(context, activeTime, fadeTime, shiftUp) {
function Jungle (line 636) | function Jungle(context) {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (56K chars).
[
{
"path": ".babelrc",
"chars": 97,
"preview": "{\n \"presets\": [\"@babel/preset-env\"],\n \"plugins\": [\"@babel/plugin-proposal-class-properties\"]\n}\n"
},
{
"path": ".browserslistrc",
"chars": 9,
"preview": "defaults\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 142,
"preview": "custom: https://www.blockchain.com/btc/payment_request?address=3LKKbbi34MHYRQSLV3ZiDGoKgUmCjhTumT&message=Kagami+open+so"
},
{
"path": ".gitignore",
"chars": 53,
"preview": "/node_modules/\n/dist/\n/.cache/\n/_vmsg.*\n/vmsg.es5.js\n"
},
{
"path": ".gitmodules",
"chars": 102,
"preview": "[submodule \"lame-svn\"]\n\tpath = lame-svn\n\turl = https://github.com/Kagami/lame-svn.git\n\tignore = dirty\n"
},
{
"path": ".npmignore",
"chars": 71,
"preview": "*\n!/COPYING\n!/vmsg.css\n!/vmsg.js\n!/vmsg.es5.js\n!/vmsg.d.ts\n!/vmsg.wasm\n"
},
{
"path": ".postcssrc",
"chars": 48,
"preview": "{\n \"plugins\": {\n \"autoprefixer\": true\n }\n}\n"
},
{
"path": "COPYING",
"chars": 7048,
"preview": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n"
},
{
"path": "Makefile",
"chars": 1046,
"preview": "export EMCC_WASM_BACKEND = 1\nexport EMCC_EXPERIMENTAL_USE_LLD = 1\n\nall: vmsg.wasm\n\nlame-svn/lame/dist/lib/libmp3lame.so:"
},
{
"path": "README.md",
"chars": 4988,
"preview": "# vmsg [](https://www.npmjs.com/package/vmsg)\n\nvmsg is a small library for "
},
{
"path": "demo/index.css",
"chars": 1408,
"preview": "body {\n margin: 20px;\n background: #ebeaeb;\n color: #0a0a0a;\n font-size: 16px;\n}\n\nbody, textarea {\n font-family: He"
},
{
"path": "demo/index.html",
"chars": 381,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>vmsg demo</title>\n <link rel=\"stylesheet\" href=\"index.c"
},
{
"path": "demo/index.js",
"chars": 4657,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport vmsg from \"..\";\n\n// https://github.com/parcel-bundle"
},
{
"path": "lame-svn.patch",
"chars": 725,
"preview": "diff --git a/lame/configure b/lame/configure\nindex 52dbf02f..b0883041 100755\n--- a/lame/configure\n+++ b/lame/configure\n@"
},
{
"path": "package.json",
"chars": 1238,
"preview": "{\n \"name\": \"vmsg\",\n \"version\": \"0.4.0\",\n \"description\": \"Library for creating voice messages\",\n \"main\": \"vmsg.js\",\n "
},
{
"path": "vmsg.c",
"chars": 2102,
"preview": "#include <stdlib.h>\n#include <stdint.h>\n#include <lame/lame.h>\n\n#define WASM_EXPORT __attribute__((visibility(\"default\")"
},
{
"path": "vmsg.css",
"chars": 3328,
"preview": ".vmsg-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n display: flex;\n background: rgba(0,"
},
{
"path": "vmsg.d.ts",
"chars": 498,
"preview": "declare module \"vmsg\" {\n interface RecordOptions {\n wasmURL?: string;\n shimURL?: string;\n pitch?: number;\n }\n"
},
{
"path": "vmsg.js",
"chars": 24578,
"preview": "/* eslint-disable */\n\nfunction pad2(n) {\n n |= 0;\n return n < 10 ? `0${n}` : `${Math.min(n, 99)}`;\n}\n\nfunction inlineW"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the Kagami/vmsg GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (51.3 KB), approximately 14.4k tokens, and a symbol index with 49 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.