diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..cfd9c84
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["stage-0"],
+ "plugins": ["transform-es2015-classes"]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..40b878d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..689b3db
--- /dev/null
+++ b/README.md
@@ -0,0 +1,96 @@
+# Crunker
+
+Simple way to merge, concatenate, play, export and download audio files with the Web Audio API.
+
+# Example
+
+```javascript
+let audio = new Crunker();
+
+audio.fetchAudio('/song.mp3', '/another-song.mp3')
+ .then(buffers => {
+ // => [AudioBuffer, AudioBuffer]
+ audio.mergeAudio(buffers)
+ })
+ .then(merged => {
+ // => AudioBuffer
+ audio.export(merged, 'audio/mp3')
+ })
+ .then(output => {
+ // => {blob, element, url}
+ audio.download(output.blob);
+ document.append(output.element);
+ console.log(output.url);
+ });
+ .catch((error) => {
+ // => Error Message
+ });
+
+audio.notSupported(() => {
+ // Handle no browser support
+});
+```
+
+# Condensed Example
+
+```javascript
+let audio = new Crunker();
+
+audio.fetchAudio('/voice.mp3', '/shell.mp3')
+ .then(buffers => audio.mergeAudio(buffers))
+ .then(merged => audio.export(merged, 'audio/mp3'))
+ .then(output => audio.download(output.audio)})
+ .catch(error => throw new Error(error))
+```
+
+# Methods
+
+## new Crunker()
+
+Create a new Crunker, no configuration options are required.
+
+## crunker.fetchAudio(songURL, anotherSongURL)
+
+Fetch one or more audio files.
+Returns: an array of audio buffers in the order they were fetched.
+
+## crunker.mergeAudio(arrayOfBuffers);
+
+Merge two or more audio buffers.
+Returns: a single AudioBuffer object.
+
+## crunker.concatAudio(arrayOfBuffers);
+
+Concatenate two or more audio buffers in the order specified.
+Returns: a single AudioBuffer object.
+
+## crunker.export(buffer, type);
+
+Export an audio buffers with MIME type option.
+Type: `'audio/mp3', 'audio/wav', 'audio/ogg'`.
+Returns: an object containing the blob object, url, and an audio element object.
+
+## crunker.download(blob, filename);
+
+Automatically download an exported audio blob with optional filename.
+Returns: the `` element used to simulate the automatic download.
+
+## crunker.download(blob, filename);
+
+Automatically download an exported audio blob with optional filename.
+Filename: String not containing the .mp3, .wav, or .ogg file extension.
+Returns: the `` element used to simulate the automatic download.
+
+## crunker.play(blob);
+
+Starts playing the exported audio blob in the background.
+Returns: the audio source object.
+
+## audio.notSupported(callback);
+
+Execute custom code if Web Audio API is not supported by the users browser.
+Returns: The callback function.
+
+# License
+
+MIT
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f007fea
--- /dev/null
+++ b/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "crunker",
+ "version": "0.0.1",
+ "description": "Simple way to merge or concatenate audio files with the Web Audio API.",
+ "main": "src/crunker.js",
+ "directories": {
+ "test": "test",
+ "src": "src"
+ },
+ "scripts": {
+ "test": "npm run compile && mocha-phantomjs -p node_modules/phantomjs/bin/phantomjs test/test.html",
+ "compile": "babel src/crunker.js > dist/crunker.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jackedgson/crunker.git"
+ },
+ "keywords": [
+ "web-audio-api",
+ "es6",
+ "merge",
+ "concatonate",
+ "append",
+ "export",
+ "download"
+ ],
+ "author": "Jack Edgson",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/jackedgson/crunker/issues"
+ },
+ "homepage": "https://github.com/jackedgson/crunker#readme",
+ "devDependencies": {
+ "babel-cli": "^6.24.1",
+ "babel-plugin-transform-es2015-classes": "^6.24.1",
+ "babel-preset-stage-0": "^6.24.1",
+ "chai": "^3.5.0",
+ "mocha-phantomjs": "3.4.1",
+ "phantomjs": "^2.1.7"
+ }
+}
diff --git a/src/crunker.js b/src/crunker.js
new file mode 100644
index 0000000..6ba8d9e
--- /dev/null
+++ b/src/crunker.js
@@ -0,0 +1,157 @@
+'use strict';
+
+class Crunker {
+
+ constructor() {
+ this._context = this._createContext();
+ }
+
+ _createContext() {
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ return new AudioContext();
+ }
+
+ async fetchAudio(...filepaths) {
+ const files = filepaths.map(async filepath => {
+ const buffer = await fetch(filepath).then(response => response.arrayBuffer());
+ return await this._context.decodeAudioData(buffer);
+ });
+ return await Promise.all(files);
+ }
+
+ mergeAudio(buffers) {
+ let output = this._context.createBuffer(1, 44100*this._maxDuration(buffers), 44100);
+
+ buffers.map(buffer => {
+ for (let i = buffer.getChannelData(0).length - 1; i >= 0; i--) {
+ output.getChannelData(0)[i] += buffer.getChannelData(0)[i];
+ }
+ });
+ return output;
+ }
+
+ concatAudio(buffers) {
+ let output = this._context.createBuffer(1, 44100*this._totalDuration(buffers), 44100),
+ offset = 0;
+ buffers.map(buffer => {
+ output.getChannelData(0).set(buffer.getChannelData(0), offset);
+ offset += buffer.length;
+ });
+ return output;
+ }
+
+ play(buffer) {
+ const source = this._context.createBufferSource();
+ source.buffer = buffer;
+ source.connect(this._context.destination);
+ source.start();
+ return source;
+ }
+
+ export(buffer, audioType){
+ const type = audioType || 'audio/mp3';
+ const recorded = this._interleave(buffer);
+ const dataview = this._writeHeaders(recorded);
+ const audioBlob = new Blob([dataview], { type: type });
+
+ return {
+ blob: audioBlob,
+ url: this._renderURL(audioBlob),
+ element: this._renderAudioElement(audioBlob, type),
+ }
+ }
+
+ download(blob, filename) {
+ const name = filename || 'crunker';
+ const a = document.createElement("a");
+ a.style = "display: none";
+ a.href = this._renderURL(blob);
+ a.download = `${name}.${blob.type.split('/')[1]}`;
+ a.click();
+ return a;
+ }
+
+ notSupported(callback) {
+ return !this._isSupported() && callback();
+ }
+
+ close() {
+ this._context.close();
+ return this;
+ }
+
+ _maxDuration(buffers) {
+ return Math.max.apply(Math, buffers.map(buffer => buffer.duration));
+ }
+
+ _totalDuration(buffers) {
+ return buffers.map(buffer => buffer.duration).reduce((a, b) => a + b, 0);
+ }
+
+ _isSupported() {
+ return 'AudioContext' in window;
+ }
+
+ _writeHeaders(buffer) {
+ let arrayBuffer = new ArrayBuffer(44 + buffer.length * 2),
+ view = new DataView(arrayBuffer);
+
+ this._writeString(view, 0, 'RIFF');
+ view.setUint32(4, 32 + buffer.length * 2, true);
+ this._writeString(view, 8, 'WAVE');
+ this._writeString(view, 12, 'fmt ');
+ view.setUint32(16, 16, true);
+ view.setUint16(20, 1, true);
+ view.setUint16(22, 2, true);
+ view.setUint32(24, 44100, true);
+ view.setUint32(28, 44100 * 4, true);
+ view.setUint16(32, 4, true);
+ view.setUint16(34, 16, true);
+ this._writeString(view, 36, 'data');
+ view.setUint32(40, buffer.length * 2, true);
+
+ return this._floatTo16BitPCM(view, buffer, 44);
+ }
+
+ _floatTo16BitPCM(dataview, buffer, offset) {
+ for (var i = 0; i < buffer.length; i++, offset+=2){
+ let tmp = Math.max(-1, Math.min(1, buffer[i]));
+ dataview.setInt16(offset, tmp < 0 ? tmp * 0x8000 : tmp * 0x7FFF, true);
+ }
+ return dataview;
+ }
+
+ _writeString(dataview, offset, header) {
+ let output;
+ for (var i = 0; i < header.length; i++){
+ dataview.setUint8(offset + i, header.charCodeAt(i));
+ }
+ }
+
+ _interleave(input) {
+ let buffer = input.getChannelData(0),
+ length = buffer.length*2,
+ result = new Float32Array(length),
+ index = 0, inputIndex = 0;
+
+ while (index < length){
+ result[index++] = buffer[inputIndex];
+ result[index++] = buffer[inputIndex];
+ inputIndex++;
+ }
+ return result;
+ }
+
+ _renderAudioElement(blob, type) {
+ const audio = document.createElement('audio');
+ audio.controls = 'controls';
+ audio.type = type;
+ audio.src = this._renderURL(blob);
+ return audio;
+ }
+
+ _renderURL(blob) {
+ return (window.URL || window.webkitURL).createObjectURL(blob);
+ }
+
+}