├── .gitignore ├── README.md ├── index.js ├── package.json └── test ├── test.js └── test.mp3 /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fpcalc 2 | 3 | This module is a wrapper around the [`fpcalc` command-line tool][chromaprint] 4 | and provides a node interface to calculate [AcoustID][] audio 5 | fingerprints for audio files. 6 | 7 | [chromaprint]: http://acoustid.org/chromaprint 8 | [acoustid]: http://acoustid.org/ 9 | 10 | ## Installing Chromaprint 11 | 12 | [`fpcalc` (provided by *Chromaprint*)][chromaprint] must be installed for 13 | this module to function. 14 | 15 | **OSX using Homebrew** 16 | 17 | ``` 18 | $ brew install chromaprint 19 | ``` 20 | 21 | **Ubuntu** 22 | 23 | ``` 24 | $ sudo apt-get install libchromaprint-tools 25 | ``` 26 | 27 | ## Example 28 | 29 | ```js 30 | var fpcalc = require("fpcalc"); 31 | fpcalc("./audio.mp3", function(err, result) { 32 | if (err) throw err; 33 | console.log(result.file, result.duration, result.fingerprint); 34 | }); 35 | ``` 36 | 37 | ## API 38 | 39 | ### `fpcalc(file, [options,] callback)` 40 | 41 | Calculates the fingerprint of the given audio file. 42 | 43 | *File* must be the path to an audio file or a readable stream. 44 | 45 | If using a stream, note that you will not get `duration` out due to an [fpcalc issue](https://github.com/acoustid/chromaprint/issues/53). 46 | 47 | *Options* may be an object with any of the following keys: 48 | 49 | * `length`: Length of the audio data used for fingerprint calculation 50 | (passed as `-length` option) 51 | * `raw`: Output the raw uncompressed fingerprint (default: `false`) 52 | * `command`: Path to the fpcalc command (default: `"fpcalc"` - expects 53 | executable in `$PATH`) 54 | 55 | *Callback* must be a function that will be called with `callback(err, 56 | result)` once the fingerprint is calculated. The *result object* will 57 | contain the following keys: 58 | 59 | * `file`: Path to the audio file 60 | * `duration`: Duration of audio file in seconds 61 | * `fingerprint`: Fingerprint of audio file - *Buffer* if `options.raw`, 62 | *String* otherwise 63 | 64 | ## Installation 65 | 66 | ``` 67 | npm install --save fpcalc 68 | ``` 69 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | "use strict"; 3 | 4 | var once = require("once"); 5 | 6 | module.exports = function(file, options, callback) { 7 | // Handle `options` parameter being optional 8 | if ( ! callback) { 9 | callback = options; 10 | options = {}; 11 | } 12 | 13 | // Make sure the callback is called only once 14 | callback = once(callback); 15 | 16 | // Command-line arguments to pass to fpcalc 17 | var args = []; 18 | 19 | // `-length` command-line argument 20 | if (options.length) { 21 | args.push("-length", options.length); 22 | } 23 | 24 | // `-raw` command-line argument 25 | if (options.raw) { 26 | args.push("-raw"); 27 | } 28 | 29 | if (file && typeof file.pipe === "function") { 30 | args.push("-"); 31 | options.stdin = file; 32 | } else { 33 | args.push(file); 34 | } 35 | 36 | run(args, options) 37 | .on("error", callback) 38 | .pipe(parse()) 39 | .on("data", function(results) { 40 | if (options.raw) { 41 | var fingerprint = results.fingerprint 42 | .split(",") 43 | .map(function(value) { 44 | return parseInt(value); 45 | }); 46 | results.fingerprintRaw = results.fingerprint; 47 | results.fingerprint = new Buffer(fingerprint.length * 4); 48 | for (var i = 0; i < fingerprint.length; i ++) { 49 | results.fingerprint.writeInt32BE(fingerprint[i], i * 4, true); 50 | } 51 | } 52 | callback(null, results); 53 | }); 54 | }; 55 | 56 | // -- Run fpcalc command 57 | 58 | var spawn = require("child_process").spawn, 59 | es = require("event-stream"), 60 | concat = require("concat-stream"), 61 | filter = require("stream-filter"), 62 | reduce = require("stream-reduce"); 63 | 64 | // Runs the fpcalc tool and returns a readable stream that will emit stdout 65 | // or an error event if an error occurs 66 | function run(args, options) { 67 | var 68 | // The command to run 69 | command = options.command || "fpcalc", 70 | 71 | // Start the fpcalc child process 72 | cp = spawn(command, args), 73 | 74 | // Create the stream that we will eventually return. This stream 75 | // passes through any data (cp's stdout) but does not emit an end 76 | // event so that we can make sure the process exited without error. 77 | stream = es.through(null, function() {}); 78 | 79 | // If passed stdin stream, pipe it to the child process 80 | if (options.stdin) { 81 | options.stdin.pipe(cp.stdin); 82 | } 83 | 84 | // Pass fpcalc stdout through the stream 85 | cp.stdout.pipe(stream); 86 | 87 | // Catch fpcalc stderr errors even when exit code is 0 88 | // See https://bitbucket.org/acoustid/chromaprint/issue/2/fpcalc-return-non-zero-exit-code-if 89 | cp.stderr.pipe(concat(function(data) { 90 | if (data && 91 | (data = data.toString()) && 92 | data.slice(0, 6) === "ERROR:") { 93 | stream.emit("error", new Error(data)); 94 | } 95 | })); 96 | 97 | // Check process exit code and end the stream 98 | cp.on("close", function(code) { 99 | if (code !== 0) { 100 | stream.emit("error", new Error("fpcalc failed")); 101 | } 102 | 103 | stream.queue(null); 104 | }); 105 | 106 | return stream; 107 | } 108 | 109 | // -- fpcalc stdout stream parsing 110 | 111 | function parse() { 112 | return es.pipeline( 113 | // Parse one complete line at a time 114 | es.split(), 115 | // Only use non-empty lines 116 | filter(Boolean), 117 | // Parse each line into name/value pair 118 | es.mapSync(parseData), 119 | // Reduce data into single result object to pass to callback 120 | reduce(function(result, data) { 121 | result[data.name] = data.value; 122 | return result; 123 | }, {}) 124 | ); 125 | } 126 | 127 | // Data is given as lines like `FILE=path/to/file`, so we split the 128 | // parts out to a name/value pair 129 | function parseData(data) { 130 | var index = data.indexOf("="); 131 | return { 132 | name: data.slice(0, index).toLowerCase(), 133 | value: data.slice(index + 1), 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fpcalc", 3 | "version": "1.3.0", 4 | "description": "Calculate AcoustID audio fingerprint", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test/test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:parshap/node-fpcalc.git" 12 | }, 13 | "keywords": [ 14 | "fpcalc", 15 | "chromaprint", 16 | "acoustid", 17 | "musicbrainz", 18 | "music", 19 | "audio", 20 | "fingerprint", 21 | "mp3" 22 | ], 23 | "author": "Parsha Pourkhomami", 24 | "license": "Public Domain", 25 | "dependencies": { 26 | "concat-stream": "^1.5.0", 27 | "event-stream": "^3.3.1", 28 | "once": "^1.3.2", 29 | "stream-filter": "^1.0.0", 30 | "stream-reduce": "^1.0.3" 31 | }, 32 | "devDependencies": { 33 | "tape": "^4.2.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | "use strict"; 3 | 4 | var path = require("path"), 5 | fs = require("fs"), 6 | test = require("tape"), 7 | fpcalc = require("../"); 8 | 9 | var TEST_FILE = path.join(__dirname, "test.mp3"); 10 | 11 | test("get audio fingerprint", function(t) { 12 | fpcalc(TEST_FILE, function(err, result) { 13 | t.ifError(err); 14 | t.ok(result.file); 15 | t.ok(result.duration); 16 | t.ok(result.fingerprint); 17 | t.end(); 18 | }); 19 | }); 20 | 21 | test("bad file path", function(t) { 22 | fpcalc("bad/path", function(err) { 23 | t.ok(err); 24 | t.end(); 25 | }); 26 | }); 27 | 28 | test("no file path", function(t) { 29 | fpcalc(undefined, function(err) { 30 | t.ok(err); 31 | t.end(); 32 | }); 33 | }); 34 | 35 | test("non-audio file", function(t) { 36 | fpcalc(path.join(__dirname, "/../index.js"), function(err) { 37 | t.ok(err); 38 | t.end(); 39 | }); 40 | }); 41 | 42 | test("fingerprint output", function(t) { 43 | t.plan(5); 44 | 45 | fpcalc(TEST_FILE, {raw: true}, function(err, result) { 46 | t.ok(result.fingerprint); 47 | t.ok(Buffer.isBuffer(result.fingerprint)); 48 | }); 49 | 50 | fpcalc(TEST_FILE, function(err, result) { 51 | t.ok(result.fingerprint); 52 | t.equal(typeof result.fingerprint, "string"); 53 | t.ok(/^[-_a-zA-Z0-9]+$/.test(result.fingerprint)); 54 | }); 55 | }); 56 | 57 | test("stream input", function(t) { 58 | t.plan(5); 59 | 60 | fpcalc(fs.createReadStream(TEST_FILE), {raw: true}, function(err, result) { 61 | t.ok(result.fingerprint); 62 | t.ok(Buffer.isBuffer(result.fingerprint)); 63 | }); 64 | 65 | fpcalc(fs.createReadStream(TEST_FILE), function(err, result) { 66 | t.ok(result.fingerprint); 67 | t.equal(typeof result.fingerprint, "string"); 68 | t.ok(/^[-_a-zA-Z0-9]+$/.test(result.fingerprint)); 69 | }); 70 | }); 71 | 72 | test("stream fignerprint is the same as file fingerprint", function(t) { 73 | t.plan(1); 74 | 75 | fpcalc(fs.createReadStream(TEST_FILE), function(err, streamResult) { 76 | fpcalc(TEST_FILE, function(err, fileResult) { 77 | t.equal(fileResult.fingerprint, streamResult.fingerprint); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parshap/node-fpcalc/acfc270584da2ae3fed6ca536375719c2d57b77a/test/test.mp3 --------------------------------------------------------------------------------