├── index.js ├── .babelrc ├── .npmignore ├── .gitignore ├── spec ├── constants │ └── cmajor.js ├── support │ ├── run.js │ └── jasmine.json ├── helpers │ └── reporter.js └── index.spec.js ├── src ├── detectors │ ├── addons.cc │ ├── yin │ │ ├── index.js │ │ └── addon │ │ │ ├── yin.h │ │ │ └── yin.cc │ ├── macleod │ │ ├── index.js │ │ └── addon │ │ │ ├── macleod.h │ │ │ └── macleod.cc │ ├── amdf.js │ └── dynamic_wavelet.js └── index.js ├── binding.gyp ├── test-addon.js ├── LICENSE ├── package.json ├── .eslintrc └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib') 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | npm-debug.log 4 | *.gch 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | build 4 | npm-debug.log 5 | *.gch 6 | -------------------------------------------------------------------------------- /spec/constants/cmajor.js: -------------------------------------------------------------------------------- 1 | module.exports = [440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880] 2 | -------------------------------------------------------------------------------- /spec/support/run.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine') 2 | 3 | const jasmine = new Jasmine() 4 | jasmine.loadConfigFile('spec/support/jasmine.json') 5 | jasmine.execute() 6 | -------------------------------------------------------------------------------- /spec/helpers/reporter.js: -------------------------------------------------------------------------------- 1 | const JasmineConsoleReporter = require('jasmine-console-reporter') 2 | 3 | const reporter = new JasmineConsoleReporter({ 4 | activity: true 5 | }) 6 | jasmine.getEnv().addReporter(reporter) 7 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /src/detectors/addons.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "yin/addon/yin.h" 3 | #include "macleod/addon/macleod.h" 4 | 5 | void InitAll(v8::Local exports) { 6 | Yin::Init(exports); 7 | MacLeod::Init(exports); 8 | } 9 | 10 | NODE_MODULE(addon, InitAll) 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const AMDF = require('./detectors/amdf') 2 | const YIN = require('./detectors/yin') 3 | const MacLeod = require('./detectors/macleod') 4 | const DynamicWavelet = require('./detectors/dynamic_wavelet') 5 | 6 | module.exports = { 7 | AMDF, 8 | YIN, 9 | MacLeod, 10 | DynamicWavelet 11 | } 12 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "addon", 5 | "sources": [ 6 | "src/detectors/addons.cc", 7 | "src/detectors/yin/addon/yin.cc", 8 | "src/detectors/macleod/addon/macleod.cc" 9 | ], 10 | "include_dirs": [ 11 | " { 4 | const detector = new Yin(options.sampleRate, options.threshold, options.probabilityThreshold) 5 | 6 | function YIN (data) { 7 | let actualData = data 8 | if (!(data instanceof Float64Array)) actualData = Float64Array.from(data) 9 | return detector.getPitch(actualData) 10 | } 11 | YIN.getResult = data => { 12 | let actualData = data 13 | if (!(data instanceof Float64Array)) actualData = Float64Array.from(data) 14 | return detector.getResult(actualData) 15 | } 16 | 17 | return YIN 18 | } 19 | -------------------------------------------------------------------------------- /src/detectors/macleod/index.js: -------------------------------------------------------------------------------- 1 | const { MacLeod } = require('../../../build/Release/addon') 2 | 3 | module.exports = (options = {}) => { 4 | const detector = new MacLeod(options.bufferSize, options.sampleRate, options.cutoff, options.freqCutoff, options.probabilityThreshold) 5 | 6 | function macLeod (data) { 7 | let actualData = data 8 | if (!(data instanceof Float64Array)) actualData = Float64Array.from(data) 9 | return detector.getPitch(actualData) 10 | } 11 | macLeod.getResult = data => { 12 | let actualData = data 13 | if (!(data instanceof Float64Array)) actualData = Float64Array.from(data) 14 | return detector.getResult(actualData) 15 | } 16 | 17 | return macLeod 18 | } 19 | -------------------------------------------------------------------------------- /test-addon.js: -------------------------------------------------------------------------------- 1 | const Pitchfinder = require('./index') 2 | 3 | const sine = [] 4 | const fs = 47000 5 | const f = 440 6 | const bufferSize = 1024 7 | 8 | for (let i = 0; i < bufferSize; i++) { 9 | sine.push(100 * Math.sin(2 * Math.PI * f / fs * i)) 10 | } 11 | 12 | const yinJs = Pitchfinder.YIN({ sampleRate: fs }) 13 | console.time('JS') 14 | let pitch 15 | for (let i = 0; i < 1000; i++) { 16 | pitch = yinJs.getResult(sine) 17 | } 18 | console.timeEnd('JS') 19 | console.log(pitch, 'err: ' + (440 - pitch.pitch)) 20 | 21 | const macLeod = Pitchfinder.MacLeod({ bufferSize: 2048, sampleRate: fs }) 22 | console.time('Addon') 23 | for (let i = 0; i < 1000; i++) { 24 | pitch = macLeod.getResult(Float64Array.from(sine)) 25 | } 26 | console.timeEnd('Addon') 27 | console.log(pitch, 'err: ' + (440 - pitch.pitch)) 28 | -------------------------------------------------------------------------------- /src/detectors/yin/addon/yin.h: -------------------------------------------------------------------------------- 1 | #ifndef YIN_H 2 | #define YIN_H 3 | 4 | #include 5 | 6 | #define DEFAULT_YIN_THRESHOLD 0.10 7 | #define DEFAULT_YIN_SAMPLE_RATE 44100 8 | #define DEFAULT_YIN_PROBABILITY_THRESHOLD 0.1 9 | 10 | class Yin : public Nan::ObjectWrap { 11 | public: 12 | static void Init(v8::Local exports); 13 | static v8::Local NewInstance(v8::Local arg); 14 | 15 | private: 16 | 17 | void init(double sampleRate, double threshold, double probabilityThreshold); 18 | Yin(); 19 | Yin(double sampleRate, double threshold, double probabilityThreshold); 20 | ~Yin(); 21 | double threshold; 22 | double sampleRate; 23 | double probability; 24 | double probabilityThreshold; 25 | 26 | double calculatePitch (double* data, size_t dataSize); 27 | 28 | static Nan::Persistent constructor; 29 | static void New(const Nan::FunctionCallbackInfo& info); 30 | static void getPitch(const Nan::FunctionCallbackInfo& info); 31 | static void getResult(const Nan::FunctionCallbackInfo& info); 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cristóvão Trevisan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | const { Sinewave, Squarewave, Trianglewave } = require('wave-generator') 2 | const { AMDF, YIN, MacLeod, DynamicWavelet } = require('../src') 3 | 4 | const cmajor = require('./constants/cmajor') 5 | const fs = 44100 6 | const length = 1024 7 | const waveforms = [Sinewave, Squarewave, Trianglewave] 8 | const algorithms = [{ 9 | name: 'AMDF', 10 | detector: AMDF(), 11 | precision: 0.01 12 | }, { 13 | name: 'YIN', 14 | detector: YIN(), 15 | precision: 0.005 16 | }, { 17 | name: 'MacLeod', 18 | detector: MacLeod({ bufferSize: length }), 19 | precision: 0.005 20 | }, { 21 | name: 'DynamicWavelet', 22 | detector: DynamicWavelet(), 23 | precision: 0.01 24 | }] 25 | 26 | waveforms.forEach(waveform => { 27 | describe(waveform.name, () => { 28 | algorithms.forEach(algo => { 29 | describe(algo.name, () => { 30 | cmajor.forEach(f => { 31 | const wave = waveform(length, f, fs, 30)(length) 32 | it(`${f} hz`, () => { 33 | expect(Math.abs(1 - algo.detector(wave) / f)).toBeLessThan(algo.precision) 34 | }) 35 | }) 36 | }) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-pitchfinder", 3 | "version": "2.1.0", 4 | "description": "A pitch-detection library for node (using C++ Addon)", 5 | "scripts": { 6 | "clean": "rm -rf ./lib ./build", 7 | "build": "npm run clean && node-gyp configure build && babel src -d lib", 8 | "lint": "standard", 9 | "test": "babel-node spec/support/run.js", 10 | "ci-test": "npm install && npm run lint && npm test", 11 | "prepublish": "npm run lint && npm run build" 12 | }, 13 | "license": "GNU v3", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/cristovao-trevisan/node-pitchfinder" 17 | }, 18 | "keywords": [ 19 | "pitch", 20 | "frequency", 21 | "detector", 22 | "detect", 23 | "detection", 24 | "find", 25 | "YIN", 26 | "AMDF", 27 | "autocorrelation", 28 | "music", 29 | "audio" 30 | ], 31 | "author": "Cristóvão Trevisan", 32 | "bugs": { 33 | "url": "https://github.com/cristovao-trevisan/node-pitchfinder/issues" 34 | }, 35 | "homepage": "https://github.com/cristovao-trevisan/node-pitchfinder#readme", 36 | "devDependencies": { 37 | "babel-cli": "^6.26.0", 38 | "babel-eslint": "^10.1.0", 39 | "babel-preset-es2015": "^6.24.1", 40 | "babel-preset-stage-0": "^6.24.1", 41 | "eslint": "^7.17.0", 42 | "jasmine": "^3.6.3", 43 | "jasmine-console-reporter": "^3.1.0", 44 | "standard": "^16.0.3", 45 | "wave-generator": "^0.1.1" 46 | }, 47 | "dependencies": { 48 | "nan": "^2.14.2", 49 | "node-gyp": "^7.1.2" 50 | }, 51 | "standard": { 52 | "parser": "babel-eslint", 53 | "env": "jasmine", 54 | "ignore": [ 55 | "build", 56 | "lib" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/detectors/macleod/addon/macleod.h: -------------------------------------------------------------------------------- 1 | #ifndef MACLEOD_H 2 | #define MACLEOD_H 3 | 4 | #include 5 | #include 6 | 7 | #define DEFAULT_MACLEOD_BUFFER_SIZE 1024 8 | #define DEFAULT_MACLEOD_SAMPLE_RATE 44100 9 | #define DEFAULT_MACLEOD_CUTOFF 0.97 10 | #define DEFAULT_MACLEOD_LOWER_PITCH_CUTOFF 80 11 | #define DEFAULT_MACLEOD_PROBABILITY_THRESHOLD 0 12 | 13 | #define MACLEOD_SMALL_CUTOFF 0.5 14 | 15 | class MacLeod : public Nan::ObjectWrap { 16 | public: 17 | static void Init(v8::Local exports); 18 | static v8::Local NewInstance(v8::Local arg); 19 | 20 | private: 21 | 22 | void init(unsigned int bufferSize, double sampleRate, double cutoff, double freqCutoff, double probabilityThreshold); 23 | MacLeod(); 24 | MacLeod(unsigned int bufferSize, double sampleRate, double cutoff, double freqCutoff, double probabilityThreshold); 25 | ~MacLeod(); 26 | unsigned int bufferSize; 27 | double* nsdf; 28 | double sampleRate; 29 | double cutoff; 30 | double probability; 31 | double probabilityThreshold; 32 | double lowerPitchCutoff; 33 | double turningPointX; 34 | double turningPointY; 35 | std::vector maxPositions; 36 | std::vector periodEstimates; 37 | std::vector ampEstimates; 38 | 39 | void normalizedSquareDifference(double* data, size_t dataSize); 40 | void parabolicInterpolation(unsigned int tau); 41 | void peakPicking(size_t dataSize); 42 | double calculatePitch (double* data, size_t dataSize); 43 | 44 | static Nan::Persistent constructor; 45 | static void New(const Nan::FunctionCallbackInfo& info); 46 | static void getPitch(const Nan::FunctionCallbackInfo& info); 47 | static void getResult(const Nan::FunctionCallbackInfo& info); 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "ecmaFeatures": { 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": false, 8 | "commonjs": true, 9 | "es6": true, 10 | "node": true, 11 | "mocha": true 12 | }, 13 | "rules": { 14 | "consistent-return": 0, 15 | "strict": 0, 16 | "no-underscore-dangle": 0, 17 | "no-multi-spaces": 0, 18 | "space-infix-ops": 0, 19 | "quotes": [ 0 ], 20 | "new-cap": 0, 21 | "comma-spacing": 0, 22 | "no-use-before-define": 0, 23 | "camelcase": 0, 24 | "curly": 0, 25 | "no-trailing-spaces": 0, 26 | "key-spacing": 2, 27 | "semi-spacing": 0, 28 | "no-unused-expressions": 0, 29 | "eol-last": 0, 30 | "dot-notation": [2, {"allowPattern": "^NODE_ENV$"}], 31 | "no-extend-native": 0, 32 | "comma-dangle": 0, 33 | "no-redeclare": 2, 34 | "no-shadow": 2, 35 | "no-new-func": 2, 36 | "no-unused-vars": 2, 37 | "semi": [2, "always"], 38 | "no-extra-semi": 2, 39 | "no-debugger": 2, // disallow use of debugger 40 | "no-dupe-keys": 2, // disallow duplicate keys when creating object literals 41 | "no-empty": 2, // disallow empty statements 42 | "no-empty-character-class": 2,// disallow the use of empty character classes in regular expressions 43 | "no-ex-assign": 2, // disallow assigning to the exception in a catch block 44 | "no-func-assign": 2, // disallow overwriting functions written as function declarations 45 | "no-invalid-regexp": 2, // disallow invalid regular expression strings in the RegExp constructor 46 | "no-unreachable": 2, // disallow unreachable statements after a return, throw, continue, or break statement 47 | "no-unexpected-multiline": 2, 48 | "no-undef": 2, 49 | "no-shadow-restricted-names": 2, 50 | "no-delete-var": 2, 51 | "no-label-var": 2, 52 | "no-var": 2, 53 | "no-const-assign": 2, 54 | "prefer-const": 2 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/detectors/amdf.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_MIN_FREQUENCY = 82 2 | const DEFAULT_MAX_FREQUENCY = 1000 3 | const DEFAULT_RATIO = 5 4 | const DEFAULT_SENSITIVITY = 0.1 5 | const DEFAULT_SAMPLE_RATE = 44100 6 | 7 | module.exports = function (config = {}) { 8 | const sampleRate = config.sampleRate || DEFAULT_SAMPLE_RATE 9 | const minFrequency = config.minFrequency || DEFAULT_MIN_FREQUENCY 10 | const maxFrequency = config.maxFrequency || DEFAULT_MAX_FREQUENCY 11 | const sensitivity = config.sensitivity || DEFAULT_SENSITIVITY 12 | const ratio = config.ratio || DEFAULT_RATIO 13 | const amd = [] 14 | const maxPeriod = Math.round(sampleRate / minFrequency + 0.5) 15 | const minPeriod = Math.round(sampleRate / maxFrequency + 0.5) 16 | 17 | return function AMDFDetector (float32AudioBuffer) { 18 | 'use strict' 19 | 20 | const maxShift = float32AudioBuffer.length 21 | 22 | let t = 0 23 | let minval = Infinity 24 | let maxval = -Infinity 25 | let frames1, frames2, calcSub, i, j, u, aux1, aux2 26 | 27 | // Find the average magnitude difference for each possible period offset. 28 | for (i = 0; i < maxShift; i++) { 29 | if (minPeriod <= i && i <= maxPeriod) { 30 | for (aux1 = 0, aux2 = i, t = 0, frames1 = [], frames2 = []; aux1 < maxShift - i; t++, aux2++, aux1++) { 31 | frames1[t] = float32AudioBuffer[aux1] 32 | frames2[t] = float32AudioBuffer[aux2] 33 | } 34 | 35 | // Take the difference between these frames. 36 | const frameLength = frames1.length 37 | calcSub = [] 38 | for (u = 0; u < frameLength; u++) { 39 | calcSub[u] = frames1[u] - frames2[u] 40 | } 41 | 42 | // Sum the differences. 43 | let summation = 0 44 | for (u = 0; u < frameLength; u++) { 45 | summation += Math.abs(calcSub[u]) 46 | } 47 | amd[i] = summation 48 | } 49 | } 50 | 51 | for (j = minPeriod; j < maxPeriod; j++) { 52 | if (amd[j] < minval) minval = amd[j] 53 | if (amd[j] > maxval) maxval = amd[j] 54 | } 55 | 56 | const cutoff = Math.round((sensitivity * (maxval - minval)) + minval) 57 | for (j = minPeriod; j <= maxPeriod && amd[j] > cutoff; j++); 58 | 59 | const searchLength = minPeriod / 2 60 | minval = amd[j] 61 | let minpos = j 62 | for (i = j - 1; i < j + searchLength && i <= maxPeriod; i++) { 63 | if (amd[i] < minval) { 64 | minval = amd[i] 65 | minpos = i 66 | } 67 | } 68 | 69 | if (Math.round(amd[minpos] * ratio) < maxval) { 70 | return sampleRate / minpos 71 | } else { 72 | return null 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Standard - JavaScript Style Guide 3 | NPM version badge 4 |

5 | 6 | # node-pitchfinder 7 | A compilation of pitch detection algorithms for Node (Using native C++ Addon). 8 | Based on [pitchfinder](https://github.com/peterkhayes/pitchfinder), but running a lot faster (because it's native) 9 | 10 | ## Provided pitch-finding algorithms 11 | - **MacLeod** - Best results for instruments 12 | - **YIN** - The best balance of accuracy and speed, in my experience. Occasionally provides values that are wildly incorrect. 13 | - **AMDF** - Slow and only accurate to around +/- 2%, but finds a frequency more consistenly than others. *NOT AN ADDON* 14 | - **Dynamic Wavelet** - Very fast, but struggles to identify lower frequencies. *NOT AN ADDON* 15 | - **YIN w/ FFT** *TODO* 16 | - **Goertzel** *TODO* 17 | 18 | ## Installation 19 | `npm install --save node-pitchfinder` 20 | 21 | > For node < 10 use version ^1.x, version 2+ is for node >= 10 22 | 23 | ## Usage 24 | 25 | ### Finding the pitch of a wav file in node 26 | ```javascript 27 | const fs = require('fs') 28 | const WavDecoder = require('wav-decoder') 29 | const { YIN } = require('node-pitchfinder') 30 | 31 | // see below for option parameters. 32 | const detectPitch = YIN({ sampleRate: 44100 }) 33 | 34 | const buffer = fs.readFileSync(PATH_TO_FILE) 35 | const decoded = WavDecoder.decode(buffer) // get audio data from file using `wav-decoder` 36 | const float64Array = decoded.channelData[0] // get a single channel of sound 37 | const pitch = detectPitch(float64Array) // All detectors are using float64Array internally, but you can also give an ordinary array of numbers 38 | ``` 39 | 40 | ## Configuration 41 | 42 | ### All detectors 43 | - `sampleRate` - defaults to 44100 44 | 45 | ### YIN 46 | - `threshold` - used by the algorithm 47 | - `probabilityThreshold` - don't return a pitch if probability estimate is below this number. 48 | 49 | ### AMDF 50 | - `minFrequency` - Lowest frequency detectable 51 | - `maxFrequency` - Highest frequency detectable 52 | - `sensitivity` 53 | - `ratio` 54 | 55 | ### MacLeod 56 | - `bufferSize` - Maximum data size (default 1024) 57 | - `cutoff` - Defines the relative size the chosen peak (pitch) has. 0.93 means: choose 58 | the first peak that is higher than 93% of the highest peak detected. 93% is the default value used in the Tartini user interface. 59 | - `freqCutoff` - Minimum frequency to be detected (default 80Hz) 60 | - `probabilityThreshold` - don't return a pitch if probability estimate is below this number. 61 | 62 | ### Dynamic Wavelet 63 | *no special config* 64 | 65 | ## MORE API 66 | 67 | ### YIN and MacLeod 68 | - method: getResult (data) - does not use probabilityThreshold, returns an object with probability instead, like `{ pitch: number, probability: number }` 69 | 70 | #### Usage 71 | ```js 72 | const {MacLeod} = require('node-pitchfinder') 73 | const detectPitch = MacLeod().getResult 74 | 75 | detectPitch(data) 76 | // {pitch: 440, probability: 1} 77 | ``` 78 | 79 | ## Todo 80 | - MacLeod using FFT 81 | 82 | ## Thanks 83 | Several of these algorithms were ported from Jonas Six's excellent TarsosDSP library (written in Java). If you're looking for a far deeper set of tools than this, check out his work [on his website](http://tarsos.0110.be/tag/TarsosDSP) or [on Github](https://github.com/JorenSix/TarsosDSP). 84 | 85 | Thanks to Aubio for his [YIN code](https://github.com/aubio/aubio/blob/master/src/pitch/pitchyin.c) 86 | -------------------------------------------------------------------------------- /src/detectors/dynamic_wavelet.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_SAMPLE_RATE = 44100 2 | const MAX_FLWT_LEVELS = 6 3 | const MAX_F = 3000 4 | const DIFFERENCE_LEVELS_N = 3 5 | const MAXIMA_THRESHOLD_RATIO = 0.75 6 | 7 | module.exports = function (config = {}) { 8 | const sampleRate = config.sampleRate || DEFAULT_SAMPLE_RATE 9 | 10 | return function DynamicWaveletDetector (data) { 11 | 'use strict' 12 | let float32AudioBuffer = data 13 | if (!(data instanceof Float64Array)) float32AudioBuffer = Float64Array.from(data) 14 | 15 | const mins = [] 16 | const maxs = [] 17 | const bufferLength = float32AudioBuffer.length 18 | 19 | let freq = null 20 | let theDC = 0 21 | let minValue = 0 22 | let maxValue = 0 23 | 24 | // Compute max amplitude, amplitude threshold, and the DC. 25 | for (let i = 0; i < bufferLength; i++) { 26 | const sample = float32AudioBuffer[i] 27 | theDC = theDC + sample 28 | maxValue = Math.max(maxValue, sample) 29 | minValue = Math.min(minValue, sample) 30 | } 31 | 32 | theDC /= bufferLength 33 | minValue -= theDC 34 | maxValue -= theDC 35 | const amplitudeMax = maxValue > -1 * minValue ? maxValue : -1 * minValue 36 | const amplitudeThreshold = amplitudeMax * MAXIMA_THRESHOLD_RATIO 37 | 38 | // levels, start without downsampling... 39 | let curLevel = 0 40 | let curModeDistance = -1 41 | let curSamNb = float32AudioBuffer.length 42 | let delta, nbMaxs, nbMins 43 | 44 | // Search: 45 | while (true) { 46 | delta = ~~(sampleRate / (Math.pow(2, curLevel) * MAX_F)) 47 | if (curSamNb < 2) break 48 | 49 | let dv 50 | let previousDV = -1000 51 | let lastMinIndex = -1000000 52 | let lastMaxIndex = -1000000 53 | let findMax = false 54 | let findMin = false 55 | 56 | nbMins = 0 57 | nbMaxs = 0 58 | 59 | for (let i = 2; i < curSamNb; i++) { 60 | const si = float32AudioBuffer[i] - theDC 61 | const si1 = float32AudioBuffer[i - 1] - theDC 62 | 63 | if (si1 <= 0 && si > 0) findMax = true 64 | if (si1 >= 0 && si < 0) findMin = true 65 | 66 | // min or max ? 67 | dv = si - si1 68 | 69 | if (previousDV > -1000) { 70 | if (findMin && previousDV < 0 && dv >= 0) { 71 | // minimum 72 | if (Math.abs(si) >= amplitudeThreshold) { 73 | if (i > lastMinIndex + delta) { 74 | mins[nbMins++] = i 75 | lastMinIndex = i 76 | findMin = false 77 | } 78 | } 79 | } 80 | 81 | if (findMax && previousDV > 0 && dv <= 0) { 82 | // maximum 83 | if (Math.abs(si) >= amplitudeThreshold) { 84 | if (i > lastMaxIndex + delta) { 85 | maxs[nbMaxs++] = i 86 | lastMaxIndex = i 87 | findMax = false 88 | } 89 | } 90 | } 91 | } 92 | previousDV = dv 93 | } 94 | 95 | if (nbMins === 0 && nbMaxs === 0) { 96 | // No best distance found! 97 | break 98 | } 99 | 100 | let d 101 | const distances = [] 102 | 103 | for (let i = 0; i < curSamNb; i++) { 104 | distances[i] = 0 105 | } 106 | 107 | for (let i = 0; i < nbMins; i++) { 108 | for (let j = 1; j < DIFFERENCE_LEVELS_N; j++) { 109 | if (i + j < nbMins) { 110 | d = Math.abs(mins[i] - mins[i + j]) 111 | distances[d] += 1 112 | } 113 | } 114 | } 115 | 116 | let bestDistance = -1 117 | let bestValue = -1 118 | 119 | for (let i = 0; i < curSamNb; i++) { 120 | let summed = 0 121 | for (let j = -1 * delta; j <= delta; j++) { 122 | if (i + j >= 0 && i + j < curSamNb) { 123 | summed += distances[i + j] 124 | } 125 | } 126 | 127 | if (summed === bestValue) { 128 | if (i === 2 * bestDistance) { 129 | bestDistance = i 130 | } 131 | } else if (summed > bestValue) { 132 | bestValue = summed 133 | bestDistance = i 134 | } 135 | } 136 | 137 | // averaging 138 | let distAvg = 0 139 | let nbDists = 0 140 | for (let j = -delta; j <= delta; j++) { 141 | if (bestDistance + j >= 0 && bestDistance + j < bufferLength) { 142 | const nbDist = distances[bestDistance + j] 143 | if (nbDist > 0) { 144 | nbDists += nbDist 145 | distAvg += (bestDistance + j) * nbDist 146 | } 147 | } 148 | } 149 | 150 | // This is our mode distance. 151 | distAvg /= nbDists 152 | 153 | // Continue the levels? 154 | if (curModeDistance > -1) { 155 | if (Math.abs(distAvg * 2 - curModeDistance) <= 2 * delta) { 156 | // two consecutive similar mode distances : ok ! 157 | freq = sampleRate / (Math.pow(2, curLevel - 1) * curModeDistance) 158 | break 159 | } 160 | } 161 | 162 | // not similar, continue next level; 163 | curModeDistance = distAvg 164 | 165 | curLevel++ 166 | if (curLevel >= MAX_FLWT_LEVELS || curSamNb < 2) { 167 | break 168 | } 169 | 170 | // do not modify original audio buffer, make a copy buffer, if 171 | // downsampling is needed (only once). 172 | let newFloat32AudioBuffer = float32AudioBuffer.subarray(0) 173 | if (curSamNb === distances.length) { 174 | newFloat32AudioBuffer = new Float32Array(curSamNb / 2) 175 | } 176 | for (let i = 0; i < curSamNb / 2; i++) { 177 | newFloat32AudioBuffer[i] = (float32AudioBuffer[2 * i] + float32AudioBuffer[2 * i + 1]) / 2 178 | } 179 | float32AudioBuffer = newFloat32AudioBuffer 180 | curSamNb /= 2 181 | } 182 | 183 | return freq 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/detectors/yin/addon/yin.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "yin.h" 3 | 4 | Nan::Persistent Yin::constructor; 5 | 6 | void Yin::Init(v8::Local exports) { 7 | Nan::HandleScope scope; 8 | 9 | // Prepare constructor template 10 | v8::Local context = exports->CreationContext(); 11 | v8::Local tpl = Nan::New(New); 12 | 13 | tpl->SetClassName(Nan::New("Yin").ToLocalChecked()); 14 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 15 | // Prototype 16 | Nan::SetPrototypeMethod(tpl, "getPitch", getPitch); 17 | Nan::SetPrototypeMethod(tpl, "getResult", getResult); 18 | 19 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 20 | 21 | exports->Set( 22 | context, 23 | Nan::New("Yin").ToLocalChecked(), 24 | tpl->GetFunction(context).ToLocalChecked() 25 | ); 26 | } 27 | 28 | void Yin::New(const Nan::FunctionCallbackInfo& info) { 29 | v8::Local context = info.GetIsolate()->GetCurrentContext(); 30 | 31 | double sampleRate = info[0]->IsUndefined() ? DEFAULT_YIN_SAMPLE_RATE : info[0]->NumberValue(context).FromJust(); 32 | double threshold = info[1]->IsUndefined() ? DEFAULT_YIN_THRESHOLD : info[1]->NumberValue(context).FromJust(); 33 | double probabilityThreshold = info[2]->IsUndefined() ? DEFAULT_YIN_PROBABILITY_THRESHOLD : info[2]->NumberValue(context).FromJust(); 34 | Yin* obj = new Yin(sampleRate, threshold, probabilityThreshold); 35 | obj->Wrap(info.This()); 36 | info.GetReturnValue().Set(info.This()); 37 | } 38 | 39 | void Yin::init(double sampleRate, double threshold, double probabilityThreshold) { 40 | this->sampleRate = sampleRate; 41 | this->threshold = threshold; 42 | this->probabilityThreshold = probabilityThreshold; 43 | } 44 | 45 | Yin::Yin() { 46 | init(DEFAULT_YIN_SAMPLE_RATE, DEFAULT_YIN_THRESHOLD, DEFAULT_YIN_PROBABILITY_THRESHOLD); 47 | }; 48 | Yin::Yin(double sampleRate, double threshold, double probabilityThreshold) { 49 | init(sampleRate, threshold, probabilityThreshold); 50 | }; 51 | Yin::~Yin() {}; 52 | 53 | void Yin::getPitch(const Nan::FunctionCallbackInfo& info) { 54 | Yin* obj = ObjectWrap::Unwrap(info.Holder()); 55 | assert(info[0]->IsFloat64Array()); 56 | v8::Local input = info[0].As(); 57 | Nan::TypedArrayContents inputData(input); 58 | double pitch = obj->calculatePitch((*inputData), input->Length()); 59 | if (obj->probability < obj->probabilityThreshold) pitch = -1; 60 | info.GetReturnValue().Set(Nan::New(pitch)); 61 | } 62 | 63 | void Yin::getResult(const Nan::FunctionCallbackInfo& info) { 64 | v8::Local context = info.GetIsolate()->GetCurrentContext(); 65 | 66 | Yin* obj = ObjectWrap::Unwrap(info.Holder()); 67 | assert(info[0]->IsFloat64Array()); 68 | v8::Local input = info[0].As(); 69 | Nan::TypedArrayContents inputData(input); 70 | double pitch = obj->calculatePitch((*inputData), input->Length()); 71 | v8::Local result = Nan::New(); 72 | result->Set(context, Nan::New("pitch").ToLocalChecked(), Nan::New(pitch)); 73 | result->Set(context, Nan::New("probability").ToLocalChecked(), Nan::New(obj->probability)); 74 | 75 | info.GetReturnValue().Set(result); 76 | } 77 | 78 | double Yin::calculatePitch (double* data, size_t dataSize) { 79 | unsigned int bufferSize; 80 | for (bufferSize = 1; bufferSize < dataSize; bufferSize *= 2); 81 | bufferSize /= 4; 82 | 83 | // Set up the buffer as described in step one of the YIN paper. 84 | double buffer[bufferSize]; 85 | 86 | probability = 0; 87 | long tau; 88 | unsigned int i, t; 89 | // Compute the difference function as described in step 2 of the YIN paper. 90 | for (t = 0; t < bufferSize; t++) { 91 | buffer[t] = 0; 92 | } 93 | for (t = 1; t < bufferSize; t++) { 94 | for (i = 0; i < bufferSize; i++) { 95 | double delta = data[i] - data[i + t]; 96 | buffer[t] += delta * delta; 97 | } 98 | } 99 | 100 | // Compute the cumulative mean normalized difference as described in step 3 of the paper. 101 | buffer[0] = 1; 102 | buffer[1] = 1; 103 | double runningSum = 0; 104 | for (t = 1; t < bufferSize; t++) { 105 | runningSum += buffer[t]; 106 | buffer[t] *= t / runningSum; 107 | } 108 | 109 | // Compute the absolute threshold as described in step 4 of the paper. 110 | // Since the first two positions in the array are 1, 111 | // we can start at the third position. 112 | for (tau = 2; tau < bufferSize; tau++) { 113 | if (buffer[tau] < threshold) { 114 | while (tau + 1 < bufferSize && buffer[tau + 1] < buffer[tau]) { 115 | tau++; 116 | } 117 | // found tau, exit loop and return 118 | // store the probability 119 | // From the YIN paper: The threshold determines the list of 120 | // candidates admitted to the set, and can be interpreted as the 121 | // proportion of aperiodic power tolerated 122 | // within a periodic signal. 123 | // 124 | // Since we want the periodicity and and not aperiodicity: 125 | // periodicity = 1 - aperiodicity 126 | probability = 1 - buffer[tau]; 127 | break; 128 | } 129 | } 130 | 131 | // if no pitch found, return -1 132 | if (tau == bufferSize || buffer[tau] >= threshold) { 133 | return -1; 134 | } 135 | 136 | /** 137 | * Implements step 5 of the AUBIO_YIN paper. It refines the estimated tau 138 | * value using parabolic interpolation. This is needed to detect higher 139 | * frequencies more precisely. See http://fizyka.umk.pl/nrbook/c10-2.pdf and 140 | * for more background 141 | * http://fedc.wiwi.hu-berlin.de/xplore/tutorials/xegbohtmlnode62.html 142 | */ 143 | double betterTau; 144 | long x0, x2; 145 | if (tau < 1) { 146 | x0 = tau; 147 | } else { 148 | x0 = tau - 1; 149 | } 150 | if (tau + 1 < bufferSize) { 151 | x2 = tau + 1; 152 | } else { 153 | x2 = tau; 154 | } 155 | if (x0 == tau) { 156 | if (buffer[tau] <= buffer[x2]) { 157 | betterTau = tau; 158 | } else { 159 | betterTau = x2; 160 | } 161 | } else if (x2 == tau) { 162 | if (buffer[tau] <= buffer[x0]) { 163 | betterTau = tau; 164 | } else { 165 | betterTau = x0; 166 | } 167 | } else { 168 | double s0 = buffer[x0]; 169 | double s1 = buffer[tau]; 170 | double s2 = buffer[x2]; 171 | // fixed AUBIO implementation, thanks to Karl Helgason: 172 | // (2.0f * s1 - s2 - s0) was incorrectly multiplied with -1 173 | betterTau = tau + (s2 - s0) / (2 * (2 * s1 - s2 - s0)); 174 | } 175 | 176 | return sampleRate / betterTau; 177 | } 178 | -------------------------------------------------------------------------------- /src/detectors/macleod/addon/macleod.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "macleod.h" 4 | 5 | Nan::Persistent MacLeod::constructor; 6 | 7 | bool max(double a, double b) { 8 | if (b < a) return a; 9 | return b; 10 | } 11 | 12 | void MacLeod::Init(v8::Local exports) { 13 | Nan::HandleScope scope; 14 | 15 | // Prepare constructor template 16 | v8::Local context = exports->CreationContext(); 17 | v8::Local tpl = Nan::New(New); 18 | tpl->SetClassName(Nan::New("MacLeod").ToLocalChecked()); 19 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 20 | // Prototype 21 | Nan::SetPrototypeMethod(tpl, "getPitch", getPitch); 22 | Nan::SetPrototypeMethod(tpl, "getResult", getResult); 23 | 24 | constructor.Reset(tpl->GetFunction(context).ToLocalChecked()); 25 | exports->Set(context, Nan::New("MacLeod").ToLocalChecked(), tpl->GetFunction(context).ToLocalChecked()); 26 | } 27 | 28 | void MacLeod::New(const Nan::FunctionCallbackInfo& info) { 29 | v8::Local context = info.GetIsolate()->GetCurrentContext(); 30 | 31 | unsigned int bufferSize = info[0]->IsUndefined() ? DEFAULT_MACLEOD_BUFFER_SIZE : info[0]->NumberValue(context).FromJust(); 32 | double sampleRate = info[1]->IsUndefined() ? DEFAULT_MACLEOD_SAMPLE_RATE : info[1]->NumberValue(context).FromJust(); 33 | double cutoff = info[2]->IsUndefined() ? DEFAULT_MACLEOD_CUTOFF : info[2]->NumberValue(context).FromJust(); 34 | double freqCutoff = info[3]->IsUndefined() ? DEFAULT_MACLEOD_LOWER_PITCH_CUTOFF : info[3]->NumberValue(context).FromJust(); 35 | double probabilityThreshold = info[4]->IsUndefined() ? DEFAULT_MACLEOD_PROBABILITY_THRESHOLD : info[4]->NumberValue(context).FromJust(); 36 | MacLeod* obj = new MacLeod(bufferSize, sampleRate, cutoff, freqCutoff, probabilityThreshold); 37 | obj->Wrap(info.This()); 38 | info.GetReturnValue().Set(info.This()); 39 | } 40 | 41 | void MacLeod::init(unsigned int bufferSize, double sampleRate, double cutoff, double freqCutoff, double probabilityThreshold) { 42 | this->nsdf = new double[bufferSize]; 43 | this->bufferSize = bufferSize; 44 | this->sampleRate = sampleRate; 45 | this->cutoff = cutoff; 46 | this->lowerPitchCutoff = freqCutoff; 47 | this->probabilityThreshold = probabilityThreshold; 48 | } 49 | 50 | MacLeod::MacLeod() { 51 | init(DEFAULT_MACLEOD_BUFFER_SIZE, DEFAULT_MACLEOD_SAMPLE_RATE, DEFAULT_MACLEOD_CUTOFF, DEFAULT_MACLEOD_LOWER_PITCH_CUTOFF, DEFAULT_MACLEOD_PROBABILITY_THRESHOLD); 52 | }; 53 | MacLeod::MacLeod(unsigned int bufferSize, double sampleRate, double cutoff, double freqCutoff, double probabilityThreshold) { 54 | init(bufferSize, sampleRate, cutoff, freqCutoff, probabilityThreshold); 55 | }; 56 | MacLeod::~MacLeod() { 57 | delete[] nsdf; 58 | }; 59 | 60 | void MacLeod::getPitch(const Nan::FunctionCallbackInfo& info) { 61 | MacLeod* obj = ObjectWrap::Unwrap(info.Holder()); 62 | assert(info[0]->IsFloat64Array()); 63 | v8::Local input = info[0].As(); 64 | assert(input->Length() <= obj->bufferSize); 65 | Nan::TypedArrayContents inputData(input); 66 | double pitch = obj->calculatePitch((*inputData), input->Length()); 67 | if (obj->probability < obj->probabilityThreshold) pitch = -1; 68 | info.GetReturnValue().Set(Nan::New(pitch)); 69 | } 70 | 71 | void MacLeod::getResult(const Nan::FunctionCallbackInfo& info) { 72 | v8::Local context = info.GetIsolate()->GetCurrentContext(); 73 | 74 | MacLeod* obj = ObjectWrap::Unwrap(info.Holder()); 75 | assert(info[0]->IsFloat64Array()); 76 | v8::Local input = info[0].As(); 77 | assert(input->Length() <= obj->bufferSize); 78 | Nan::TypedArrayContents inputData(input); 79 | double pitch = obj->calculatePitch((*inputData), input->Length()); 80 | v8::Local result = Nan::New(); 81 | result->Set(context, Nan::New("pitch").ToLocalChecked(), Nan::New(pitch)); 82 | result->Set(context, Nan::New("probability").ToLocalChecked(), Nan::New(obj->probability)); 83 | 84 | info.GetReturnValue().Set(result); 85 | } 86 | 87 | void MacLeod::normalizedSquareDifference(double* data, size_t dataSize) { 88 | for (size_t tau = 0; tau < dataSize; tau++) { 89 | double acf = 0; 90 | double divisorM = 0; 91 | for (size_t i = 0; i < dataSize - tau; i++) { 92 | acf += data[i] * data[i+tau]; 93 | divisorM += data[i] * data[i] + data[i + tau] * data[i + tau]; 94 | } 95 | nsdf[tau] = 2 * acf / divisorM; 96 | } 97 | } 98 | 99 | void MacLeod::parabolicInterpolation(unsigned int tau) { 100 | double nsdfa = nsdf[tau - 1]; 101 | double nsdfb = nsdf[tau]; 102 | double nsdfc = nsdf[tau + 1]; 103 | double bValue = tau; 104 | double bottom = nsdfc + nsdfa - 2 * nsdfb; 105 | if (bottom == 0) { 106 | turningPointX = bValue; 107 | turningPointY = nsdfb; 108 | } else { 109 | double delta = nsdfa - nsdfc; 110 | turningPointX = bValue + delta / (2 * bottom); 111 | turningPointY = nsdfb - delta * delta / (8 * bottom); 112 | } 113 | } 114 | 115 | void MacLeod::peakPicking(size_t dataSize) { 116 | unsigned int pos = 0; 117 | unsigned int curMaxPos = 0; 118 | 119 | // find the first negative zero crossing. 120 | while (pos < (dataSize - 1) / 3 && nsdf[pos] > 0) { 121 | pos++; 122 | } 123 | 124 | // loop over all the values below zero. 125 | while (pos < dataSize - 1 && nsdf[pos] <= 0) { 126 | pos++; 127 | } 128 | 129 | // can happen if output[0] is NAN 130 | if (pos == 0) { 131 | pos = 1; 132 | } 133 | 134 | while (pos < dataSize - 1) { 135 | assert(nsdf[pos] >= 0); 136 | if (nsdf[pos] > nsdf[pos - 1] && nsdf[pos] >= nsdf[pos + 1]) { 137 | if (curMaxPos == 0) { 138 | // the first max (between zero crossings) 139 | curMaxPos = pos; 140 | } else if (nsdf[pos] > nsdf[curMaxPos]) { 141 | // a higher max (between the zero crossings) 142 | curMaxPos = pos; 143 | } 144 | } 145 | pos++; 146 | // a negative zero crossing 147 | if (pos < dataSize - 1 && nsdf[pos] <= 0) { 148 | // if there was a maximum add it to the list of maxima 149 | if (curMaxPos > 0) { 150 | maxPositions.push_back(curMaxPos); 151 | curMaxPos = 0; // clear the maximum position, so we start 152 | // looking for a new ones 153 | } 154 | while (pos < dataSize - 1 && nsdf[pos] <= 0) { 155 | pos++; // loop over all the values below zero 156 | } 157 | } 158 | } 159 | if (curMaxPos > 0) { 160 | maxPositions.push_back(curMaxPos); 161 | } 162 | } 163 | 164 | double MacLeod::calculatePitch (double* data, size_t dataSize) { 165 | double pitch; 166 | maxPositions.clear(); 167 | periodEstimates.clear(); 168 | ampEstimates.clear(); 169 | 170 | // 1. Calculute the normalized square difference for each Tau value. 171 | normalizedSquareDifference(data, dataSize); 172 | // 2. Peak picking time: time to pick some peaks. 173 | peakPicking(dataSize); 174 | 175 | double highestAmplitude = - std::numeric_limits::infinity(); 176 | 177 | unsigned int tau; 178 | for (unsigned int i = 0; i < maxPositions.size(); i++) { 179 | tau = maxPositions[i]; 180 | // make sure every annotation has a probability attached 181 | highestAmplitude = max(highestAmplitude, nsdf[tau]); 182 | 183 | if (nsdf[tau] > MACLEOD_SMALL_CUTOFF) { 184 | // calculates turningPointX and Y 185 | parabolicInterpolation(tau); 186 | // store the turning points 187 | ampEstimates.push_back(turningPointY); 188 | periodEstimates.push_back(turningPointX); 189 | // remember the highest amplitude 190 | highestAmplitude = max(highestAmplitude, turningPointY); 191 | } 192 | } 193 | 194 | if (periodEstimates.size() > 0) { 195 | // use the overall maximum to calculate a cutoff. 196 | // The cutoff value is based on the highest value and a relative 197 | // threshold. 198 | double actualCutoff = cutoff * highestAmplitude; 199 | unsigned int periodIndex = 0; 200 | 201 | for (unsigned int i = 0; i < ampEstimates.size(); i++) { 202 | if (ampEstimates[i] >= actualCutoff) { 203 | periodIndex = i; 204 | break; 205 | } 206 | } 207 | 208 | double period = periodEstimates[periodIndex]; 209 | double pitchEstimate = sampleRate / period; 210 | 211 | if (pitchEstimate > lowerPitchCutoff) { 212 | pitch = pitchEstimate; 213 | } else { 214 | pitch = -1; 215 | } 216 | 217 | } else { 218 | // no pitch detected. 219 | pitch = -1; 220 | } 221 | 222 | probability = highestAmplitude; 223 | return pitch; 224 | } 225 | --------------------------------------------------------------------------------