Skip to content

Commit

Permalink
Merge branch 'release/2.0.0'
Browse files Browse the repository at this point in the history
* release/2.0.0:
  Bumped version
  Refactored API
  • Loading branch information
tornqvist committed May 19, 2016
2 parents 50f3f5b + 88b43dd commit e6206ff
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 97 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Generated files
index.*
dist
lib
9 changes: 0 additions & 9 deletions .eslintrc

This file was deleted.

15 changes: 15 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "eslint:recommended",
"env": {
"es6": true,
"node": true,
"browser": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"ecmaFeatures": {
"modules": true
}
}
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Build files
index.*
# Build
dist
lib/*
!lib/index.js

# Libraries
node_modules
Expand Down
5 changes: 1 addition & 4 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
# Build files
index.*

# Tests
test

# Development files
.eslintrc
.eslintrc.json
.eslintignore
.gitignore
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./detect').default;
46 changes: 15 additions & 31 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
{
"name": "bpm-detective",
"version": "1.0.1",
"version": "2.0.0",
"description": "Detects the BPM of a song or audio sample",
"main": "index.js",
"main": "lib/index.js",
"scripts": {
"postinstall": "npm run build && npm run minify",
"test": "browserify test/program.js | tape-run -b chrome",
"build": "babel lib/detect.js -o index.js --source-maps --stage 0 --modules umdStrict",
"minify": "uglifyjs index.js -o index.min.js",
"prepublish": "npm run build -s && npm run minify -s",
"test": "browserify test/program.js -t [ babelify --presets es2015 ] -t brfs | tape-run -b chrome",
"posttest": "npm run lint",
"lint": "eslint ./",
"convert": "bin/convert"
"build": "babel src/ -d lib/ --presets es2015",
"minify": "browserify lib/index.js --standalone DetectBPM | uglifyjs > dist/bpm-detective.js",
"lint": "eslint ./"
},
"repository": {
"type": "git",
Expand All @@ -30,29 +29,14 @@
},
"homepage": "https://github.com/tornqvist/bpm-detective#readme",
"devDependencies": {
"babel-eslint": "^4.0.7",
"babelify": "^6.2.0",
"brfs": "^1.4.1",
"browserify": "^11.0.1",
"eslint": "^1.0.0",
"serve": "^1.4.0",
"babel-cli": "^6.4.5",
"babel-preset-es2015": "^6.3.13",
"babelify": "^7.2.0",
"brfs": "^1.4.3",
"browserify": "^13.0.0",
"eslint": "^1.10.3",
"tape": "^4.0.1",
"tape-run": "^1.1.0",
"uglify-js": "^2.4.24",
"whatwg-fetch": "^0.9.0"
},
"dependencies": {
"babel": "^5.8.23"
},
"browserify": {
"transform": [
[
"babelify",
{
"stage": 0
}
],
"brfs"
]
"tape-run": "^2.1.3",
"uglify-js": "^2.4.24"
}
}
59 changes: 37 additions & 22 deletions lib/detect.js → src/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const OfflineContext = (window.OfflineAudioContext || window.webkitOfflineAudioC
*/

export default function detect(buffer) {
let source = getLowPassSource(buffer);
const source = getLowPassSource(buffer);

/**
* Schedule the sound to start playing at time:0
Expand All @@ -19,14 +19,27 @@ export default function detect(buffer) {
* Pipe the source through the program
*/

return Promise.all(findPeaks(source.buffer.getChannelData(0)))
.then(identifyIntervals)
.then(groupByTempo(buffer.sampleRate))
.then((groups) => {
return groups
.sort((a, b) => (b.count - a.count))
.splice(0, 5)[0].tempo;
});
return [
findPeaks,
identifyIntervals,
groupByTempo(buffer.sampleRate),
getTopCandidate
].reduce(
(state, fn) => fn(state),
source.buffer.getChannelData(0)
);
}

/**
* Sort results by count and return top candidate
* @param {Object} Candidate
* @return {Number}
*/

function getTopCandidate(candidates) {
return candidates
.sort((a, b) => (b.count - a.count))
.splice(0, 5)[0].tempo;
}

/**
Expand All @@ -36,21 +49,21 @@ export default function detect(buffer) {
*/

function getLowPassSource(buffer) {
let {length, numberOfChannels, sampleRate} = buffer;
let context = new OfflineContext(numberOfChannels, length, sampleRate);
const {length, numberOfChannels, sampleRate} = buffer;
const context = new OfflineContext(numberOfChannels, length, sampleRate);

/**
* Create buffer source
*/

let source = context.createBufferSource();
const source = context.createBufferSource();
source.buffer = buffer;

/**
* Create filter
*/

let filter = context.createBiquadFilter();
const filter = context.createBiquadFilter();
filter.type = 'lowpass';

/**
Expand All @@ -72,8 +85,8 @@ function getLowPassSource(buffer) {
function findPeaks(data) {
let peaks = [];
let threshold = 0.9;
let minThresold = 0.3;
let minPeaks = 15;
const minThresold = 0.3;
const minPeaks = 15;

/**
* Keep looking for peaks lowering the threshold until
Expand All @@ -90,7 +103,9 @@ function findPeaks(data) {
*/

if (peaks.length < minPeaks) {
return [Promise.reject(new Error('Could not find enough samples for a reliable detection.'))];
throw (
new Error('Could not find enough samples for a reliable detection.')
);
}

return peaks;
Expand All @@ -104,7 +119,7 @@ function findPeaks(data) {
*/

function findPeaksAtThreshold(data, threshold) {
let peaks = [];
const peaks = [];

/**
* Identify peaks that pass the threshold, adding them to the collection
Expand Down Expand Up @@ -132,7 +147,7 @@ function findPeaksAtThreshold(data, threshold) {
*/

function identifyIntervals(peaks) {
let intervals = [];
const intervals = [];

peaks.forEach((peak, index) => {
for (let i = 0; i < 10; i+= 1) {
Expand All @@ -142,7 +157,7 @@ function identifyIntervals(peaks) {
* Try and find a matching interval and increase it's count
*/

let foundInterval = intervals.some((intervalCount) => {
let foundInterval = intervals.some(intervalCount => {
if (intervalCount.interval === interval) {
return intervalCount.count += 1;
}
Expand Down Expand Up @@ -179,9 +194,9 @@ function groupByTempo(sampleRate) {
*/

return (intervalCounts) => {
let tempoCounts = [];
const tempoCounts = [];

intervalCounts.forEach((intervalCount) => {
intervalCounts.forEach(intervalCount => {
if (intervalCount.interval !== 0) {
/**
* Convert an interval to tempo
Expand All @@ -206,7 +221,7 @@ function groupByTempo(sampleRate) {
* See if another interval resolved to the same tempo
*/

let foundTempo = tempoCounts.some((tempoCount) => {
let foundTempo = tempoCounts.some(tempoCount => {
if (tempoCount.tempo === theoreticalTempo) {
return tempoCount.count += intervalCount.count;
}
Expand Down
60 changes: 32 additions & 28 deletions test/program.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import 'babel/polyfill';
import 'whatwg-fetch';
import test from 'tape';
import detect from './../lib/detect';
import detect from './../src/detect';

const AudioContext = (window.AudioContext || window.webkitAudioContext);

Expand All @@ -12,44 +10,50 @@ const AudioContext = (window.AudioContext || window.webkitAudioContext);
// Needed to circumvent Babel's transformation
const fs = require('fs');
let fixtures = {
'90': fs.readFileSync(__dirname + '/fixtures/90bpm.wav', { encoding: 'base64' }),
'97': fs.readFileSync(__dirname + '/fixtures/97bpm.wav', { encoding: 'base64' }),
'117': fs.readFileSync(__dirname + '/fixtures/117bpm.wav', { encoding: 'base64' }),
'140': fs.readFileSync(__dirname + '/fixtures/140bpm.wav', { encoding: 'base64' }),
'170': fs.readFileSync(__dirname + '/fixtures/170bpm.wav', { encoding: 'base64' })
'90': fs.readFileSync(__dirname + '/fixtures/90bpm.wav', 'base64'),
'97': fs.readFileSync(__dirname + '/fixtures/97bpm.wav', 'base64'),
'117': fs.readFileSync(__dirname + '/fixtures/117bpm.wav', 'base64'),
'140': fs.readFileSync(__dirname + '/fixtures/140bpm.wav', 'base64'),
'170': fs.readFileSync(__dirname + '/fixtures/170bpm.wav', 'base64')
};

test('detects bpm', assert => {
test('detects bpm', t => {
const beats = [97, 117, 140, 170];

// Load beats
Promise.all(beats.map(bpm => convert(fixtures[bpm])))
// Detect bpm of all beats
.then(sources => Promise.all(sources.map(detect)))
// Match detected bpm to expected bpm
.then(results => results.map((bpm, index) => [bpm, beats[index]]))
// Map pairs through assert
.then(pairs => pairs.forEach(pair => assert.equal(...pair)))
.then(assert.end, assert.end);
Promise.all(
beats.map(bpm => convert(fixtures[bpm]))
).then(buffers => {
try {
for (let [i, buffer] of buffers.entries()) {
const bpm = detect(buffer);
t.equal(beats[i], bpm, `Detected ${ beats[i] } bpm`);
}
} catch (err) {
t.fail(err);
}
}).then(t.end, t.end);
});

test('fails with short sample', assert => {
convert(fixtures['90'])
.then(detect)
.then(bpm => assert.fail(`Detected ${ bpm }`))
.catch(err => assert.pass(err.message))
.then(assert.end);
test('fails with short sample', t => {
convert(fixtures['90']).then(buffer => {
try {
const bpm = detect(buffer);
t.fail(`Detected ${ bpm }`);
} catch (err) {
t.pass(err);
}
}).then(t.end, t.end);
});

/**
* Convert base64 string into AudioBuffer
*/

function convert(base64) {
let context = new AudioContext();
let binaryString = atob(base64);
let len = binaryString.length;
let bytes = new Uint8Array(len);
const context = new AudioContext();
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);

for (let i = 0; i < len; i += 1) {
bytes[i] = binaryString.charCodeAt(i);
Expand Down

0 comments on commit e6206ff

Please sign in to comment.