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