├── .github └── assets │ ├── out-fft.png │ └── out-thr.png ├── lib ├── index.d.ts └── index.ts ├── package.json ├── .gitignore ├── demo.js ├── README.md ├── tsconfig.json └── LICENSE /.github/assets/out-fft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adblockradio/stream-audio-fingerprint/HEAD/.github/assets/out-fft.png -------------------------------------------------------------------------------- /.github/assets/out-thr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adblockradio/stream-audio-fingerprint/HEAD/.github/assets/out-thr.png -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Transform } from 'stream'; 3 | interface Options { 4 | readableObjectMode: true; 5 | highWaterMark: number; 6 | } 7 | interface Mark { 8 | t: number; 9 | i: number[]; 10 | v: number[]; 11 | } 12 | export declare class Codegen extends Transform { 13 | buffer: Buffer; 14 | bufferDelta: number; 15 | stepIndex: number; 16 | marks: Mark[]; 17 | threshold: any[]; 18 | fftData?: any[]; 19 | thrData?: any[]; 20 | peakData?: any[]; 21 | DT: number; 22 | SAMPLING_RATE: number; 23 | BPS: number; 24 | constructor(options?: Partial); 25 | _write(chunk: Buffer, _: any, next: Function): void; 26 | plot(): void; 27 | } 28 | export {}; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-audio-fingerprint", 3 | "version": "1.0.4", 4 | "description": "Audio landmark fingerprinting as a Node Stream module", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "prebuild": "del ./lib/*.js", 9 | "build": "tsc -d", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "audio", 14 | "fingerprint", 15 | "stream" 16 | ], 17 | "files": [ 18 | "lib/*.js", 19 | "lib/*.d.ts" 20 | ], 21 | "author": "Alexandre Storelli ", 22 | "license": "MPL-2.0", 23 | "dependencies": { 24 | "@types/node": "13.9.3", 25 | "del-cli": "3.0.0", 26 | "dsp.js": "git+https://git@github.com/corbanbrook/dsp.js.git", 27 | "node-png": "^0.4.3", 28 | "typescript": "3.8.3" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/dest4/stream-audio-fingerprint/issues" 32 | }, 33 | "homepage": "https://github.com/dest4/stream-audio-fingerprint" 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | lib/*.js 61 | 62 | # macOS hidden file 63 | .DS_Store 64 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Copyright (c) 2018 Alexandre Storelli 6 | 7 | const childProcess = require('child_process'); 8 | // const { Codegen } = require('stream-audio-fingerprint'); 9 | // Swap the line above if you're running this outside of the repo 10 | const { Codegen } = require('./lib'); 11 | 12 | const decoder = childProcess.spawn('ffmpeg', [ 13 | '-i', 'pipe:0', 14 | '-acodec', 'pcm_s16le', 15 | '-ar', '22050', 16 | '-ac', '1', 17 | '-f', 'wav', 18 | '-v', 'fatal', 19 | 'pipe:1' 20 | ], { stdio: ['pipe', 'pipe', process.stderr] }); 21 | 22 | const fingerprinter = new Codegen(); 23 | 24 | // Pipe ouput of ffmpeg decoder to fingerprinter 25 | decoder.stdout.pipe(fingerprinter); 26 | 27 | // Pipe input to this file to ffmpeg decoder 28 | process.stdin.pipe(decoder.stdin); 29 | 30 | // Log all the found fingerprints as they come in 31 | fingerprinter.on('data', data => { 32 | for (let i = 0; i < data.tcodes.length; i++) { 33 | console.log(`time=${data.tcodes[i]} fingerprint=${data.hcodes[i]}`); 34 | } 35 | }); 36 | 37 | fingerprinter.on('end', () => { 38 | console.log('Fingerprints stream ended.'); 39 | }); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audio landmark fingerprinting as a Node Stream module 2 | 3 | This module is a duplex stream (instance of stream.Transform) that converts a PCM audio signal into a series of audio fingerprints. It works with audio tracks as well as with unlimited audio streams, e.g. broadcast radio. 4 | 5 | It is one of the foundations of the [Adblock Radio project](https://github.com/adblockradio/adblockradio). 6 | 7 | ## Credits 8 | 9 | The [acoustic fingerprinting](https://en.wikipedia.org/wiki/Acoustic_fingerprint) technique used here is the landmark algorithm, as described in the [Shazam 2003 paper](http://www.ee.columbia.edu/~dpwe/papers/Wang03-shazam.pdf). 10 | The implementation in ```codegen_landmark.js``` has been inspired by the MATLAB routine of D. Ellis ["Robust Landmark-Based Audio Fingerprinting" (2009)](http://labrosa.ee.columbia.edu/matlab/fingerprint/). One significant difference with Ellis' implementation is that this module can handle unlimited audio streams, e.g. radio, and not only finished audio tracks. 11 | 12 | Note the existence of another good landmark fingerprinter in Python, [dejavu](https://github.com/worldveil/dejavu). 13 | 14 | ## Description 15 | 16 | In a nutshell, 17 | - a spectrogram is computed from the audio signal 18 | - significant peaks are chosen in this time-frequency map. a latency of 250ms is used to determine if a peak is not followed by a bigger peak. 19 | - fingerprints are computed by linking peaks with ```dt```, ```f1``` and ```f2```, ready to be inserted in a database or to be compared with other fingerprints. 20 | 21 | ![Spectrogram, peaks and pairs](.github/assets/out-fft.png) 22 | 23 | In the background, about 12s of musical content is represented as a spectrogram (top frequency is about 10kHz). The blue marks are the chosen spectrogram peaks. Grey lines are peaks pairs that each lead to a fingerprint. 24 | 25 | ![Threshold and peaks](.github/assets/out-thr.png) 26 | 27 | Given the same audio, this figure shows the same peaks and the internal *forward* threshold that prevent peaks from being too close in time and frequency. The *backward* threshold selection is not represented here. 28 | 29 | ## Usage 30 | 31 | ```shell 32 | npm install stream-audio-fingerprint 33 | ``` 34 | 35 | The algorithm is in `lib/index.ts`. 36 | 37 | A demo usage is proposed in `demo.js`. It requires the executable [ffmpeg](https://ffmpeg.org/download.html) to run. 38 | 39 | ```js 40 | const childProcess = require('child_process'); 41 | const { Codegen } = require('stream-audio-fingerprint'); 42 | 43 | const decoder = childProcess.spawn('ffmpeg', [ 44 | '-i', 'pipe:0', 45 | '-acodec', 'pcm_s16le', 46 | '-ar', '22050', 47 | '-ac', '1', 48 | '-f', 'wav', 49 | '-v', 'fatal', 50 | 'pipe:1' 51 | ], { stdio: ['pipe', 'pipe', process.stderr] }); 52 | 53 | const fingerprinter = new Codegen(); 54 | 55 | // Pipe ouput of ffmpeg decoder to fingerprinter 56 | decoder.stdout.pipe(fingerprinter); 57 | 58 | // Pipe input to this file to ffmpeg decoder 59 | process.stdin.pipe(decoder.stdin); 60 | 61 | // Log all the found fingerprints as they come in 62 | fingerprinter.on('data', data => { 63 | for (let i = 0; i < data.tcodes.length; i++) { 64 | console.log(`time=${data.tcodes[i]} fingerprint=${data.hcodes[i]}`); 65 | } 66 | }); 67 | 68 | fingerprinter.on('end', () => { 69 | console.log('Fingerprints stream ended.'); 70 | }); 71 | ``` 72 | 73 | and then we pipe audio data, either a stream or a file 74 | 75 | ```shell 76 | curl http://radiofg.impek.com/fg | node demo.js 77 | cat awesome_music.mp3 | node demo.js 78 | ``` 79 | on Windows: 80 | ``` 81 | type awesome_music.mp3 | node demo.js 82 | ``` 83 | 84 | ## Integration in your project 85 | 86 | Matching fingerprints in a database is not a trivial topic, I should write a technical note about it some day. 87 | 88 | For a reference implementation you can have a look at the code of the Adblock Radio algorithm to catch ads https://github.com/adblockradio/adblockradio/blob/master/predictor-db/hotlist.js#L150. 89 | 90 | ## License 91 | 92 | See LICENSE file. 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Copyright (c) 2018 Alexandre Storelli 6 | 7 | // Online implementation of the landmark audio fingerprinting algorithm. 8 | // inspired by D. Ellis (2009), "Robust Landmark-Based Audio Fingerprinting" 9 | // http://labrosa.ee.columbia.edu/matlab/fingerprint/ 10 | // itself inspired by Wang 2003 paper 11 | 12 | // This module exports Codegen, an instance of stream.Transform 13 | // By default, the writable side must be fed with an input signal with the following properties: 14 | // - single channel 15 | // - 16bit PCM 16 | // - 22050 Hz sampling rate 17 | // 18 | // The readable side outputs objects of the form 19 | // { tcodes: [time stamps], hcodes: [fingerprints] } 20 | 21 | import { Transform } from 'stream'; 22 | import dsp from 'dsp.js'; 23 | 24 | const log = console.log; 25 | 26 | const SAMPLING_RATE = 22050; 27 | // sampling rate in Hz. If you change this, you must adapt WINDOW_DT and PRUNING_DT below to match your needs 28 | // set the Nyquist frequency, SAMPLING_RATE/2, so as to match the max frequencies you want to get landmark fingerprints. 29 | 30 | const BPS = 2; 31 | // bytes per sample, 2 for 16 bit PCM. If you change this, you must change readInt16LE methods in the code. 32 | 33 | const MNLM = 5; 34 | // maximum number of local maxima for each spectrum. useful to tune the amount of fingerprints at output 35 | 36 | const MPPP = 3; 37 | // maximum of hashes each peak can lead to. useful to tune the amount of fingerprints at output 38 | 39 | const NFFT = 512; // size of the FFT window. As we use real signals, the spectra will have nfft/2 points. 40 | // Increasing it will give more spectral precision, less temporal precision. 41 | // It may be good or bad depending on the sounds you want to match and on whether your input is deformed by EQ or noise. 42 | 43 | const STEP = NFFT/2; // 50 % overlap 44 | // if SAMPLING_RATE is 22050 Hz, this leads to a sampling frequency 45 | // fs = (SAMPLING_RATE / STEP) /s = 86/s, or dt = 1/fs = 11,61 ms. 46 | // It's not really useful to change the overlap ratio. 47 | const DT = 1 / (SAMPLING_RATE / STEP); 48 | 49 | const FFT = new dsp.FFT(NFFT, SAMPLING_RATE); 50 | 51 | const HWIN = new Array(NFFT); // prepare the hann window 52 | for (var i=0; i { 92 | let mask = [1, 1, 1]; 93 | if (color == 'r') { 94 | mask = [0, 1, 1]; 95 | } else if (color == 'b') { 96 | mask = [1, 1, 0]; 97 | } else if (color == 'grey') { 98 | mask = [0.5, 0.5, 0.5]; 99 | } 100 | const r = 255 * Math.sqrt(Math.min(Math.max(x, 0), 1)); 101 | buffer[index] = Math.round(255 - r * mask[0]); 102 | buffer[index + 1] = Math.round(255 - r * mask[1]); 103 | buffer[index + 2] = Math.round(255 - r * mask[2]); 104 | buffer[index + 3] = 255; // alpha channel 105 | }; 106 | 107 | const minmax = (a: any[], nDim: number) => { 108 | let norm = [0, 0]; 109 | for (let x = 0; x < a.length; x++) { 110 | if (nDim == 1) { 111 | norm[0] = Math.min(a[x], norm[0]); 112 | norm[1] = Math.max(a[x], norm[1]); 113 | } else if (nDim == 2) { 114 | for (let y = 0; y < a[0].length; y++) { 115 | norm[0] = Math.min(a[x][y], norm[0]); 116 | norm[1] = Math.max(a[x][y], norm[1]); 117 | } 118 | } 119 | } 120 | return norm; 121 | }; 122 | 123 | interface Image { 124 | data: any[] 125 | width: number 126 | height: number 127 | } 128 | 129 | const drawMarker = (img: Image, x: number, y: number, radius: number) => { 130 | colormap(1, img.data, ((img.width * (img.height - 1 - y) + x) << 2), 'b'); 131 | 132 | if (radius > 1) { 133 | drawMarker(img, x + 1, y, radius - 1); 134 | drawMarker(img, x, y + 1, radius - 1); 135 | drawMarker(img, x - 1, y, radius - 1); 136 | drawMarker(img, x, y - 1, radius - 1); 137 | } 138 | 139 | return; 140 | }; 141 | 142 | const drawLine = (img: Image, x1: number, x2: number, y1: number, y2: number) => { 143 | log(`draw line x1=${x1} y1=${y1} x2=${x2} y2=${y2}`); 144 | const len = Math.round(Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2))); 145 | for (let i = 0; i <= len; i++) { 146 | const x = x1 + Math.round((x2 - x1) * i / len); 147 | const y = y1 + Math.round((y2 - y1) * i / len); 148 | colormap(1, img.data, ((img.width * (img.height - 1 - y) + x) << 2), 'grey'); 149 | } 150 | }; 151 | 152 | interface Options { 153 | readableObjectMode: true 154 | highWaterMark: number 155 | } 156 | 157 | interface Mark { 158 | t: number 159 | i: number[] 160 | v: number[] 161 | } 162 | 163 | export class Codegen extends Transform { 164 | buffer: Buffer 165 | bufferDelta: number 166 | stepIndex: number 167 | marks: Mark[] 168 | threshold: any[] 169 | fftData?: any[] 170 | thrData?: any[] 171 | peakData?: any[] 172 | DT: number 173 | SAMPLING_RATE: number 174 | BPS: number 175 | 176 | constructor(options: Partial = {}) { 177 | super({ 178 | readableObjectMode: true, 179 | highWaterMark: 10, 180 | ...options 181 | }); 182 | this.buffer = Buffer.alloc(0); 183 | this.bufferDelta = 0; 184 | 185 | this.stepIndex = 0; 186 | this.marks = []; 187 | this.threshold = new Array(NFFT / 2); 188 | for (let i = 0; i < NFFT / 2; i++) { 189 | this.threshold[i] = -3; 190 | } 191 | 192 | if (DO_PLOT) { 193 | this.fftData = []; 194 | this.thrData = []; 195 | this.peakData = []; 196 | } 197 | 198 | // Copy constants to be able to reference them in parent modules 199 | this.DT = DT; 200 | this.SAMPLING_RATE = SAMPLING_RATE; 201 | this.BPS = BPS; 202 | } 203 | 204 | _write(chunk: Buffer, _: any, next: Function) { 205 | if (VERBOSE) { 206 | log(`t=${Math.round(this.stepIndex / STEP)} received ${chunk.length} bytes`); 207 | } 208 | 209 | let tcodes: number[] = []; 210 | let hcodes: number[] = []; 211 | 212 | this.buffer = Buffer.concat([this.buffer, chunk]); 213 | 214 | while ((this.stepIndex + NFFT) * BPS < this.buffer.length + this.bufferDelta) { 215 | let data = new Array(NFFT); // window data 216 | 217 | // Fill the data, windowed (HWIN) and scaled 218 | for (let i=0,limit = NFFT; i diff[i - 1] && diff[i] > diff[i + 1] && FFT.spectrum[i] > vLocMax[MNLM - 1]) { // if local maximum big enough 250 | // insert the newly found local maximum in the ordered list of maxima 251 | for (let j = MNLM - 1; j >= 0; j--) { 252 | // navigate the table of previously saved maxima 253 | if (j >= 1 && FFT.spectrum[i] > vLocMax[j - 1]) continue; 254 | for (let k = MNLM - 1; k >= j + 1; k--) { 255 | iLocMax[k] = iLocMax[k - 1]; // offset the bottom values 256 | vLocMax[k] = vLocMax[k - 1]; 257 | } 258 | iLocMax[j] = i; 259 | vLocMax[j] = FFT.spectrum[i]; 260 | break; 261 | } 262 | } 263 | } 264 | 265 | // now that we have the MNLM highest local maxima of the spectrum, 266 | // update the local maximum threshold so that only major peaks are taken into account. 267 | for (let i = 0; i < MNLM; i++) { 268 | if (vLocMax[i] > Number.NEGATIVE_INFINITY) { 269 | for (let j = IF_MIN; j < IF_MAX; j++) { 270 | this.threshold[j] = Math.max(this.threshold[j], Math.log(FFT.spectrum[iLocMax[i]]) + EWW[iLocMax[i]][j]); 271 | } 272 | } else { 273 | vLocMax.splice(i, MNLM - i); // remove the last elements. 274 | iLocMax.splice(i, MNLM - i); 275 | break; 276 | } 277 | } 278 | 279 | if (DO_PLOT) { 280 | let tmp = new Array(NFFT / 2); 281 | for (let i = 0; i < IF_MIN; i++) { 282 | tmp[i] = 0; 283 | } 284 | for (let i = IF_MIN; i < IF_MAX; i++) { 285 | tmp[i] = Math.exp(this.threshold[i]); 286 | } 287 | for (let i = IF_MAX; i < NFFT / 2; i++) { 288 | tmp[i] = 0; 289 | } 290 | this.thrData?.push(tmp); 291 | } 292 | 293 | // Array that stores local maxima for each time step 294 | this.marks.push({ 295 | t: Math.round(this.stepIndex/STEP), 296 | i: iLocMax, 297 | v: vLocMax 298 | }); 299 | 300 | // Remove previous (in time) maxima that would be too close and/or too low. 301 | let nm = this.marks.length; 302 | let t0 = nm - PRUNING_DT - 1; 303 | for (let i = nm - 1; i >= Math.max(t0 + 1, 0); i--) { 304 | for (let j = 0; j < this.marks[i].v.length; j++) { 305 | if (this.marks[i].i[j] != 0 && Math.log(this.marks[i].v[j]) < this.threshold[this.marks[i].i[j]] + MASK_DECAY_LOG * (nm - 1 - i)) { 306 | this.marks[i].v[j] = Number.NEGATIVE_INFINITY; 307 | this.marks[i].i[j] = Number.NEGATIVE_INFINITY; 308 | } 309 | } 310 | } 311 | 312 | // Generate hashes for peaks that can no longer be pruned. stepIndex:{f1:f2:deltaindex} 313 | let nFingersTotal = 0; 314 | if (t0 >= 0) { 315 | let m = this.marks[t0]; 316 | 317 | loopCurrentPeaks: 318 | for (let i = 0; i < m.i.length; i++) { 319 | let nFingers = 0; 320 | 321 | loopPastTime: 322 | for (let j = t0; j >= Math.max(0, t0 - WINDOW_DT); j--) { 323 | 324 | let m2 = this.marks[j]; 325 | 326 | loopPastPeaks: 327 | for (let k = 0; k < m2.i.length; k++) { 328 | if (m2.i[k] != m.i[i] && Math.abs(m2.i[k] - m.i[i]) < WINDOW_DF) { 329 | tcodes.push(m.t); //Math.round(this.stepIndex/STEP)); 330 | // in the hash: dt=(t0-j) has values between 0 and WINDOW_DT, so for <65 6 bits each 331 | // f1=m2.i[k] , f2=m.i[i] between 0 and NFFT/2-1, so for <255 8 bits each. 332 | hcodes.push(m2.i[k] + NFFT / 2 * (m.i[i] + NFFT / 2 * (t0 - j))); 333 | nFingers += 1; 334 | nFingersTotal += 1; 335 | if (DO_PLOT) this.peakData?.push([m.t, j, m.i[i], m2.i[k]]); // t1, t2, f1, f2 336 | if (nFingers >= MPPP) continue loopCurrentPeaks; 337 | } 338 | } 339 | } 340 | } 341 | } 342 | if (nFingersTotal > 0 && VERBOSE) { 343 | log(`t=${Math.round(this.stepIndex / STEP)} generated ${nFingersTotal} fingerprints`); 344 | } 345 | if (!DO_PLOT) { 346 | this.marks.splice(0, t0 + 1 - WINDOW_DT); 347 | } 348 | 349 | // Decrease the threshold for the next iteration 350 | for (let j = 0; j < this.threshold.length; j++) { 351 | this.threshold[j] += MASK_DECAY_LOG; 352 | } 353 | } 354 | 355 | if (this.buffer.length > 1000000) { 356 | const delta = this.buffer.length - 20000; 357 | this.bufferDelta += delta; 358 | this.buffer = this.buffer.slice(delta); 359 | } 360 | 361 | if (VERBOSE) { 362 | // log("fp processed " + (this.practicalDecodedBytes - this.decodedBytesSinceCallback) + " while threshold is " + (0.99*this.thresholdBytes)); 363 | } 364 | 365 | if (this.stepIndex/STEP > 500 && DO_PLOT) { // approx 12 s of audio data 366 | this.plot() 367 | DO_PLOT = false; 368 | setTimeout(() => { 369 | process.exit(0); 370 | }, 3000); 371 | } 372 | 373 | if (tcodes.length > 0) { 374 | this.push({ tcodes, hcodes }); 375 | // this will eventually trigger data events on the read interface 376 | } 377 | 378 | next(); 379 | } 380 | 381 | plot() { 382 | if (!this.fftData || !this.peakData || !this.thrData) { 383 | return; 384 | } 385 | 386 | // Fft plot 387 | { 388 | console.log(`fftData len=${this.fftData.length}`); 389 | var img = new png({ width: this.fftData.length, height: this.fftData[0].length }); 390 | img.data = new Buffer(img.width * img.height * 4); 391 | var norm = minmax(this.fftData, 2); 392 | if (VERBOSE) { 393 | log("fft min=" + norm[0] + " max=" + norm[1]); 394 | } 395 | for (let x = 0; x < img.width; x++) { 396 | for (let y = 0; y < img.height; y++) { 397 | colormap(Math.abs((this.fftData[x][y] - norm[0]) / (norm[1] - norm[0])), img.data, ((img.width * (img.height - 1 - y) + x) << 2), 'r'); 398 | } 399 | } 400 | for (let i = 0; i < this.peakData.length; i++) { 401 | drawLine(img, this.peakData[i][0], this.peakData[i][1], this.peakData[i][2], this.peakData[i][3]); 402 | } 403 | 404 | for (let x = 0; x < img.width; x++) { 405 | for (let i = 0; i < this.marks[x].i.length; i++) { 406 | if (this.marks[x].i[i] > Number.NEGATIVE_INFINITY) { 407 | drawMarker(img, x, this.marks[x].i[i], 2); 408 | } 409 | } 410 | } 411 | img.pack().pipe(fs.createWriteStream('out-fft.png')); 412 | } 413 | 414 | // Threshold plot 415 | { 416 | var img = new png({ width: this.thrData.length, height: this.thrData[0].length }); 417 | img.data = new Buffer(img.width * img.height * 4); 418 | var norm = minmax(this.thrData, 2); 419 | if (VERBOSE) { 420 | log("thr min=" + norm[0] + " max=" + norm[1]); 421 | } 422 | for (let x = 0; x < img.width; x++) { 423 | for (let y = 0; y < img.height; y++) { 424 | colormap(Math.abs((this.thrData[x][y] - norm[0]) / (norm[1] - norm[0])), img.data, ((img.width * (img.height - 1 - y) + x) << 2), 'r'); 425 | } 426 | 427 | for (let i = 0; i < this.marks[x].i.length; i++) { 428 | if (this.marks[x].i[i] > Number.NEGATIVE_INFINITY) { 429 | drawMarker(img, x, this.marks[x].i[i], 2); 430 | } 431 | } 432 | } 433 | img.pack().pipe(fs.createWriteStream('out-thr.png')); 434 | } 435 | } 436 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. --------------------------------------------------------------------------------