├── .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 | 
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 | 
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.
--------------------------------------------------------------------------------