Skip to content

Commit

Permalink
created crunker
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack committed Apr 25, 2017
1 parent a3acce1 commit 7902997
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["stage-0"],
"plugins": ["transform-es2015-classes"]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<a></a>` 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 `<a></a>` 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
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
157 changes: 157 additions & 0 deletions src/crunker.js
Original file line number Diff line number Diff line change
@@ -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);
}

}

0 comments on commit 7902997

Please sign in to comment.