UNPKG

poly-peach

Version:

A targeted pitch-detection library for node in the browser

877 lines (876 loc) 413 kB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Chromagram = void 0; var FFT = require('fft.js'); var TOTAL_NOTES = 12; var DEFAULT_REFERENCE_FREQUENCY = 130.81278265; // C3 var DEFAULT_HARMONICS = 2; var DEFAULT_OCTAVES = 4; var DEFAULT_BINS = 2; var Chromagram = /** @class */ (function () { function Chromagram(frameSize, fs) { this.bufferSize = 8192; this.kiss_ready = false; this.setSamplingFrequency(fs); this.setInputAudioFrameSize(frameSize); this.setParameters(DEFAULT_REFERENCE_FREQUENCY, DEFAULT_HARMONICS, DEFAULT_OCTAVES, DEFAULT_BINS); } Chromagram.prototype.setParameters = function (_referenceFrequency, _numHarmonics, _numOctaves, _numBinsToSearch) { this.setReferenceFrequency(_referenceFrequency); this.setNumHarmonics(_numHarmonics); this.setNumOctaves(_numOctaves); this.setNumBinsToSearch(_numBinsToSearch); this.noteFrequencies = new Array(TOTAL_NOTES * this.numOctaves); for (var i = 0; i < this.noteFrequencies.length; i++) { this.noteFrequencies[i] = this.referenceFrequency * Math.pow(2, ((i) / TOTAL_NOTES)); } this.setupFFT(); this.buffer = new Array(this.bufferSize); this.buffer.fill(0); this.realInput = new Array(this.bufferSize); this.realInput.fill(0); this.chromagram = new Array(TOTAL_NOTES); this.chromagram.fill(0); this.pitches = new Array(TOTAL_NOTES * this.numOctaves); this.pitches.fill(0); for (var i = 0; i < this.chromagram.length; i++) { this.chromagram[i] = 0.0; } for (var i = 0; i < this.pitches.length; i++) { this.pitches[i] = 0.0; } this.magnitudeSpectrum = new Array((this.bufferSize / 2) + 1); this.magnitudeSpectrum.fill(0); this.makeHammingWindow(); this.numSamplesSinceLastCalculation = 0; this.chromaCalculationInterval = 4096; this.chromaReady = false; }; Chromagram.prototype.processAudioFrame = function (inputAudioFrame) { this.chromaReady = false; this.downSampleFrame(inputAudioFrame); for (var i = 0; i < this.bufferSize - this.downSampledAudioFrameSize; i++) { this.buffer[i] = this.buffer[i + this.downSampledAudioFrameSize]; } var n = 0; for (var i = (this.bufferSize - this.downSampledAudioFrameSize); i < this.bufferSize; i++) { this.buffer[i] = this.downsampledInputAudioFrame[n]; n++; } this.numSamplesSinceLastCalculation += this.inputAudioFrameSize; if (this.numSamplesSinceLastCalculation >= this.chromaCalculationInterval) { this.calculateChromagram(); this.numSamplesSinceLastCalculation = 0; } }; Chromagram.prototype.setInputAudioFrameSize = function (frameSize) { this.inputAudioFrameSize = frameSize; this.downsampledInputAudioFrame = new Array(this.inputAudioFrameSize / 4); this.downsampledInputAudioFrame.fill(0); this.downSampledAudioFrameSize = this.downsampledInputAudioFrame.length; }; Chromagram.prototype.setReferenceFrequency = function (freq) { this.referenceFrequency = freq; }; Chromagram.prototype.getReferenceFrequency = function () { return this.referenceFrequency; }; Chromagram.prototype.setNumHarmonics = function (n) { this.numHarmonics = n; }; Chromagram.prototype.setNumOctaves = function (n) { this.numOctaves = n; }; Chromagram.prototype.getNumOctaves = function () { return this.numOctaves; }; Chromagram.prototype.setNumBinsToSearch = function (n) { this.numBinsToSearch = n; }; Chromagram.prototype.setSamplingFrequency = function (fs) { this.samplingFrequency = fs; }; Chromagram.prototype.setChromaCalculationInterval = function (numSamples) { this.chromaCalculationInterval = numSamples; }; Chromagram.prototype.getChromagram = function () { return this.chromagram; }; Chromagram.prototype.getPitches = function () { return this.pitches; }; Chromagram.prototype.isReady = function () { return this.chromaReady; }; Chromagram.prototype.setupFFT = function () { this.fft = new FFT(this.bufferSize); this.complexOutput = this.fft.createComplexArray(); this.kiss_ready = true; }; Chromagram.prototype.calculateChromagram = function () { this.calculateMagnitudeSpectrum(); var divisorRatio = ((this.samplingFrequency) / 4.0) / (this.bufferSize); for (var n = 0; n < TOTAL_NOTES; n++) { var chromaSum = 0.0; for (var octave = 1; octave <= this.numOctaves; octave++) { var noteSum = 0.0; var centerBin = this.round((this.noteFrequencies[n + (octave - 1) * TOTAL_NOTES]) / divisorRatio); var minBin = centerBin - (this.numBinsToSearch); var maxBin = centerBin + (this.numBinsToSearch); var maxVal = 0.0; for (var k = minBin; k < maxBin; k++) { if (this.magnitudeSpectrum[k] > maxVal) { maxVal = this.magnitudeSpectrum[k]; } } noteSum += maxVal; this.pitches[n + (octave - 1) * TOTAL_NOTES] = noteSum; //TODO:deprecated? noteSum = 0.0; for (var harmonic = 1; harmonic <= this.numHarmonics; harmonic++) { var centerBin_1 = this.round((this.noteFrequencies[n + (octave - 1) * TOTAL_NOTES] * octave * harmonic) / divisorRatio); var minBin_1 = centerBin_1 - (this.numBinsToSearch * harmonic); var maxBin_1 = centerBin_1 + (this.numBinsToSearch * harmonic); var maxVal_1 = 0.0; for (var k = minBin_1; k < maxBin_1; k++) { if (this.magnitudeSpectrum[k] > maxVal_1) { maxVal_1 = this.magnitudeSpectrum[k]; } } noteSum += (maxVal_1 / harmonic); } chromaSum += noteSum; } this.chromagram[n] = chromaSum; } this.chromaReady = true; }; Chromagram.prototype.calculateMagnitudeSpectrum = function () { var i = 0; for (var i_1 = 0; i_1 < this.bufferSize; i_1++) { this.realInput[i_1] = this.buffer[i_1] * this.window[i_1]; } // This is where all the magic happens. this.fft.realTransform(this.complexOutput, this.realInput); this.fft.completeSpectrum(this.complexOutput); //TODO:is this unnecessary? for (var i_2 = 0; i_2 < (this.bufferSize / 2) + 1; i_2++) { this.magnitudeSpectrum[i_2] = Math.sqrt(Math.pow(this.complexOutput[i_2 * 2], 2) + Math.pow(this.complexOutput[i_2 * 2 + 1], 2)); this.magnitudeSpectrum[i_2] = Math.sqrt(this.magnitudeSpectrum[i_2]); } }; Chromagram.prototype.downSampleFrame = function (inputAudioFrame) { var filteredFrame = new Array(this.inputAudioFrameSize); filteredFrame.fill(0); var b0, b1, b2, a1, a2; var x_1, x_2, y_1, y_2; b0 = 0.2929; b1 = 0.5858; b2 = 0.2929; a1 = -0.0000; a2 = 0.1716; x_1 = 0; x_2 = 0; y_1 = 0; y_2 = 0; for (var i = 0; i < this.inputAudioFrameSize; i++) { filteredFrame[i] = inputAudioFrame[i] * b0 + x_1 * b1 + x_2 * b2 - y_1 * a1 - y_2 * a2; x_2 = x_1; x_1 = inputAudioFrame[i]; y_2 = y_1; y_1 = filteredFrame[i]; } for (var i = 0; i < this.inputAudioFrameSize / 4; i++) { this.downsampledInputAudioFrame[i] = filteredFrame[i * 4]; } }; Chromagram.prototype.makeHammingWindow = function () { this.window = new Array(this.bufferSize); this.window.fill(0); for (var n = 0; n < this.bufferSize; n++) { this.window[n] = 0.54 - 0.46 * Math.cos(2 * Math.PI * (n / (this.bufferSize))); } }; Chromagram.prototype.round = function (val) { return Math.floor(val + 0.5); }; return Chromagram; }()); exports.Chromagram = Chromagram; },{"fft.js":3}],2:[function(require,module,exports){ "use strict"; /* global Cookies openToast closeAlert ABCJS backPageWithTitle openPageWithTitle closeMenu createAudioMeter */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PolyPeachDetector = void 0; var Tone = __importStar(require("tone")); var chromagram_1 = require("./chromagram"); var DEBUG = true; var MIN_VOLUME = 5; // The number of harmonic frequencies to check for looking to see if a pitch exists. var MIN_HARMONICS = 3; //some false positives caused by small harmonic overlap? //const MAX_ERROR_THRESHOLD = 1.0; // @4 harmonics, excludes some notes and doesn't catch doublestops well, @3, too much error on doublestops //const MAX_ERROR_THRESHOLD = 1.5; // var MAX_ERROR_THRESHOLD = 2; // //const MAX_ERROR_THRESHOLD = 3; // //const MIN_HARMONICS = 4; //const MAX_ERROR_THRESHOLD = 1.25; var MAX_HARMONICS = 10; // The minimum amount of max sound amplitude present before pitches will be checked. var MIN_LEVEL_FOR_SOUND = 2; var DEFAULT_HARMONICS = 2; //const DEFAULT_OCTAVES = 4; var DEFAULT_BINS = 2; // No harmonics lower than this will be tracked. All signature data will be relative to this. var DEFAULT_REFERENCE_NOTE = 'E2'; //const DEFAULT_REFERENCE_NOTE = 'A1'; var DEFAULT_NUMBER_OF_OCTAVES = 6; var UPPER_CUTOFF_MIDI_NOTE = 91; var NOTES_PER_OCTAVE = 12; var PIXELS_PER_NOTE = 40; //10; var SEND_AUDIO = true; var MAX_SAMPLES_SENT = 0; //10; var VIOLIN = 'violin'; var CLASSICAL_GUITAR = 'classical-guitar'; var DEFAULT_SIGNATURE_TYPE = CLASSICAL_GUITAR; // To record new signatures: // 1. cd into ./examples // 2. run ./runserver // 3. enter note to track // 4. click "start/stop" to enable microphone, clicking "allow" if your browser asks for permission // 5. click "Record Signature" // 6. play the note on the instrument // 7. copy and paste the array from the browser console. var SIGNATURES = {}; // {instrument_type: {note: [normalized level of 1st harmonic, 2nd, etc.]}} SIGNATURES[CLASSICAL_GUITAR] = {}; SIGNATURES[CLASSICAL_GUITAR]['B1'] = [0.008353722475263978, 0.08353722475263978, 0.24922334320976444, 0.21343707037889467, 0.08451587405792234, 0.17224138200379366, 0.033774901708252765, 0.06911056800073963, 0.05806738126941937, 0.027738532143309357]; SIGNATURES[CLASSICAL_GUITAR]['C2'] = [0.01714748510848196, 0.1714748510848196, 0.30549932824977016, 0.1593114605402403, 0.11062781202791976, 0.06184840797192317, 0.04204884018654787, 0.042482957620565455, 0.05339368161661821, 0.036165175593113594]; SIGNATURES[CLASSICAL_GUITAR]['D2'] = [0.025229232239432863, 0.25229232239432864, 0.14094727664008924, 0.1734900507321308, 0.10845957248258795, 0.10996644391858704, 0.010030969047985007, 0.1120414001657053, 0.05370098154720274, 0.01384175083195036]; // 6st string. SIGNATURES[CLASSICAL_GUITAR]['E2'] = [0.122295850941197, 0.27716587569620493, 0.1046082802481929, 0.05761219339793058, 0.12444229293881741, 0.15180154548623329, 0.053207216628688006, 0.02185373479648957, 0.06224104688970442, 0.024771962976541564]; SIGNATURES[CLASSICAL_GUITAR]['F2'] = [0.15259686104111125, 0.28668328011189786, 0.11993617759324385, 0.08804660602158135, 0.15614909453650105, 0.08070953117753697, 0.013026241192724324, 0.0474708679595814, 0.022405788673492494, 0.03297555169232946]; SIGNATURES[CLASSICAL_GUITAR]['F#2'] = [0.19981994584211402, 0.24276112744798772, 0.13648929793835604, 0.14170221027382515, 0.1203684431613425, 0.06036032903502198, 0.023712159055240007, 0.03610047645095376, 0.02473857486053893, 0.013947435934619772]; SIGNATURES[CLASSICAL_GUITAR]['G2'] = [0.22618409995563174, 0.20155479389616904, 0.10257553556556541, 0.17439074032147298, 0.11695759614650089, 0.07192534944260795, 0.01741167753093079, 0.05359579223273711, 0.017342094669194272, 0.018062320239189782]; SIGNATURES[CLASSICAL_GUITAR]['G#2'] = [0.33466280713665697, 0.3043186039403842, 0.07984114631986007, 0.07504719822409595, 0.08339294470532709, 0.034448030796226596, 0.016805303295821954, 0.03214603322839447, 0.016106413595188846, 0.02323151875804414]; // 5th string. SIGNATURES[CLASSICAL_GUITAR]['A2'] = [0.21305179051991668, 0.3236132470349405, 0.1368776078426075, 0.08019394247329312, 0.06972486852464517, 0.0625289093161398, 0.026538690377956985, 0.03750825592137052, 0.02829046185499598, 0.021672226134133825]; SIGNATURES[CLASSICAL_GUITAR]['A#2'] = [0.2756962345230539, 0.33860423891732877, 0.066235334238123, 0.06052939171600377, 0.15440572227975335, 0.02190650792742798, 0.02349425709613552, 0.02344385846113954, 0.017135039932272396, 0.01854941490876166]; SIGNATURES[CLASSICAL_GUITAR]['B2'] = [0.4363488985495379, 0.15176725793302612, 0.2068833578050863, 0.08769104213654381, 0.02641690692140292, 0.02070560523161303, 0.013690927827612422, 0.023548212061537538, 0.01098217927159482, 0.021965612262044923]; SIGNATURES[CLASSICAL_GUITAR]['C3'] = [0.37268696402710794, 0.25097732930212985, 0.17437340898603107, 0.12202075147504517, 0.02875668005157027, 0.016617394811492894, 0.010077927794351954, 0.007663981931853062, 0.010605381862906875, 0.006220179757510845]; SIGNATURES[CLASSICAL_GUITAR]['C#3'] = [0.38355481044917034, 0.29048633651878, 0.04404286832530228, 0.14825954168398223, 0.04614102248568601, 0.02563601483376563, 0.01846847022681665, 0.020433747981030707, 0.006748730873919201, 0.016228456621546886]; // 4th string. SIGNATURES[CLASSICAL_GUITAR]['D3'] = [0.19052171971039922, 0.23573111621152357, 0.1110230492189988, 0.10667965932904586, 0.08593086337050101, 0.12243648655075469, 0.025705879617473015, 0.04881641269592747, 0.05448051604126656, 0.01867429725410989]; SIGNATURES[CLASSICAL_GUITAR]['D#3'] = [0.30326866301242844, 0.3547987449559368, 0.10964822024998035, 0.056454684519237755, 0.07005368551875059, 0.029817329381958996, 0.020358047500074385, 0.017265382025746737, 0.019627604697326377, 0.018707638138559513]; SIGNATURES[CLASSICAL_GUITAR]['E3'] = [0.31950839385316243, 0.31400405685970206, 0.12961389057380995, 0.0494357280400105, 0.08862879708918667, 0.029648356262953222, 0.01302980525786959, 0.02310527252053414, 0.018829161756370606, 0.014196537786400674]; SIGNATURES[CLASSICAL_GUITAR]['F3'] = [0.4227381755980699, 0.2504423221601583, 0.08568165991472737, 0.09200226597190139, 0.05182182288232276, 0.02415415578515949, 0.02576502249938607, 0.02386761976802234, 0.007556108329283733, 0.01597084709096853]; SIGNATURES[CLASSICAL_GUITAR]['F#3'] = [0.37328046462767045, 0.3278589163502336, 0.06379871695069021, 0.07906126070454116, 0.03660305804442305, 0.025974525817361253, 0.02483289883687511, 0.025017444009689527, 0.021320893016365933, 0.022251821642149675]; // 3rd string. SIGNATURES[CLASSICAL_GUITAR]['G3'] = [0.3478124168956828, 0.2667452323382379, 0.15271960293094702, 0.02373241402731488, 0.02790539428432555, 0.106944445686604, 0.01977932481312516, 0.018209953940704866, 0.01749100412568327, 0.01866021095737449]; SIGNATURES[CLASSICAL_GUITAR]['G#3'] = [0.4599766355388572, 0.26320702950972713, 0.04322157569460409, 0.04072061851410703, 0.07674196277631087, 0.014509616643671595, 0.03049577738129965, 0.02664493809175701, 0.025683661518831023, 0.018798184330834345]; SIGNATURES[CLASSICAL_GUITAR]['A3'] = [0.5828021638313978, 0.13567284130713392, 0.030422531853270485, 0.04765921843151788, 0.07626200067141474, 0.017910681470522043, 0.03035879868206245, 0.03149735627867146, 0.02960604433652343, 0.01780836313748575]; SIGNATURES[CLASSICAL_GUITAR]['A#3'] = [0.5609431762836757, 0.11281271296528593, 0.035094436972956264, 0.030622746291425224, 0.10462088519913475, 0.03382399838070928, 0.03610938736690961, 0.014787856554261755, 0.03485905877721128, 0.03632574120843019]; // 2nd string. //SIGNATURES[CLASSICAL_GUITAR]['B3'] = [0.45812360766117177, 0.22569654387239138, 0.08385472881295354, 0.03784660709035133, 0.067932984065355, 0.043595046248842105, 0.022832206170645086, 0.016670699901498897, 0.018432353879482378, 0.025015222297308768]; SIGNATURES[CLASSICAL_GUITAR]['B3'] = [0.20416023768959102, 0.3475973381378323, 0.1602396585574644, 0.0991694407643142, 0.051813370130704334, 0.03809460701686076, 0.023090131061721652, 0.03134701544176759, 0.024464470033069365, 0.02002373116667437]; SIGNATURES[CLASSICAL_GUITAR]['C4'] = [0.4265380262694899, 0.2014094294439763, 0.07040921204851498, 0.030215480742709747, 0.16398363158284152, 0.018920963069382898, 0.03498375462512141, 0.014281689796883143, 0.010947858865323937, 0.028309953555755996]; SIGNATURES[CLASSICAL_GUITAR]['C#4'] = [0.3947819111735955, 0.29613240846268435, 0.095024328495909, 0.029305839637546332, 0.04033909780064078, 0.04294226310553229, 0.021882364712289066, 0.04289871088835553, 0.015461724584054813, 0.021231351139392455]; SIGNATURES[CLASSICAL_GUITAR]['D4'] = [0.4613202657508963, 0.19895166585883253, 0.04533505806707816, 0.08074094582262469, 0.10304533421659158, 0.03077555756617126, 0.016365371942191967, 0.014581829048814566, 0.02434462605591637, 0.02453934567088261]; SIGNATURES[CLASSICAL_GUITAR]['D#4'] = [0.5854065741539815, 0.11778358901506492, 0.04995925536733159, 0.03518609309823251, 0.05215560594391319, 0.04590253722484267, 0.01809519281777682, 0.04347989801709912, 0.02047184510552993, 0.03155940925622779]; // 1st string. SIGNATURES[CLASSICAL_GUITAR]['E4'] = [0.4227419009962166, 0.22302453943146514, 0.10955192220930718, 0.05993144832265702, 0.038007787319864726, 0.0420601828393388, 0.02887116223754167, 0.029881439579449, 0.0233191706005812, 0.02261044646357872]; SIGNATURES[CLASSICAL_GUITAR]['F4'] = [0.41951238537990204, 0.24807954900637944, 0.04892334719573636, 0.033681106554851535, 0.10457163283171468, 0.021505430170142018, 0.033429783515443214, 0.034443999892255994, 0.033464667966526876, 0.02238809748704805]; SIGNATURES[CLASSICAL_GUITAR]['F#4'] = [0.6200452653367138, 0.12277575043636056, 0.04308149913316804, 0.03575003289383122, 0.041491526656213196, 0.025467852783021116, 0.01998044187118477, 0.03824589387748832, 0.019784202962627048, 0.03337753404939184]; SIGNATURES[CLASSICAL_GUITAR]['G4'] = [0.6221927406360364, 0.07355226869109247, 0.04012794263149465, 0.0330287884520763, 0.06856591296473696, 0.02402285855911311, 0.042143343951339486, 0.043542079001139676, 0.022644997466246434, 0.030179067646724476]; SIGNATURES[CLASSICAL_GUITAR]['G#4'] = [0.5797332544376933, 0.148789841445366, 0.025548698252446125, 0.04935218631551829, 0.046885362108485665, 0.035757134577954444, 0.033886885397673386, 0.026961940285525777, 0.025745987282593603, 0.02733870989674342]; SIGNATURES[CLASSICAL_GUITAR]['A4'] = [0.47864366810706593, 0.20622321921895564, 0.04586652719995738, 0.06038346656512431, 0.07163933275326169, 0.03569482026277012, 0.027508795242085705, 0.027114940676047052, 0.027745659571490965, 0.01917957040324128]; SIGNATURES[CLASSICAL_GUITAR]['A#4'] = [0.5092562367964059, 0.18991306214034215, 0.045518715140658146, 0.03111248671727977, 0.037026727754982974, 0.0396601928212558, 0.04584257516006542, 0.01591565624357895, 0.038774180898246, 0.04698016632718479]; SIGNATURES[CLASSICAL_GUITAR]['B4'] = [0.5487119427918494, 0.1370791045592195, 0.08594769989772379, 0.0577913634983074, 0.029608941860511873, 0.02660689794326807, 0.023067836990903763, 0.02348441440907624, 0.022595474780219834, 0.04510632326892013]; SIGNATURES[CLASSICAL_GUITAR]['C5'] = [0.6149462161077834, 0.06016983735633915, 0.039621406350026984, 0.034689398761210265, 0.044057270627706896, 0.04368457903186212, 0.03164944297041697, 0.04205920430546308, 0.02762802287841264, 0.061494621610778344]; SIGNATURES[CLASSICAL_GUITAR]['C#5'] = [0.67102231811212, 0.04267008219169093, 0.037615793101930285, 0.03167059633501829, 0.03290609354432486, 0.036667542778211756, 0.022600801036773217, 0.0372883589525666, 0.020456182136152044, 0.067102231811212]; SIGNATURES[CLASSICAL_GUITAR]['D5'] = [0.5896884622375657, 0.0843839635034977, 0.03569883130414447, 0.04733343856634157, 0.03351175343854408, 0.036329786411323325, 0.025616191768596117, 0.022947786297612037, 0.0655209402486184, 0.05896884622375657]; SIGNATURES[CLASSICAL_GUITAR]['D#5'] = [0.3786013504365225, 0.15923227751721877, 0.12347215479184781, 0.06531736952535841, 0.06258435609909092, 0.06202700442390176, 0.026718679132954774, 0.04211985631428361, 0.04206681671516917, 0.03786013504365225]; SIGNATURES[CLASSICAL_GUITAR]['E5'] = [0.5769269488739688, 0.0694908710048988, 0.036725161033295636, 0.027811632839895694, 0.0521782038004998, 0.017797559085498647, 0.025158065545969874, 0.0721158686092461, 0.06410299431932985, 0.05769269488739688]; var RECORD_SIGNATURE_BUTTON_OFF = 'Record Signature'; var RECORD_SIGNATURE_BUTTON_ON = 'Recording... '; function getHarmonicFrequencyFromNote(note, i) { // Given a note, returns the Frequency() object associated with it's given ith harmonic. // Note, i is 1-indexed. // e.g. getHarmonicNote('E2', 1) == 'E2' // e.g. getHarmonicNote('E2', 2) == 'E3' var freqObj = Tone.Frequency(note); var freq = freqObj.toFrequency(); var harmonicFreq = freq * i; var harmonicFreqObj = Tone.Frequency(harmonicFreq); return harmonicFreqObj; } function getHarmonicNoteFromNote(note, i) { // Given a note, returns the note associated with it's given ith harmonic. // Note, i is 1-indexed. // e.g. getHarmonicNote('E2', 1) == 'E2' // e.g. getHarmonicNote('E2', 2) == 'E3' return getHarmonicFrequencyFromNote(note, i).toNote(); } function sumArray(ary) { // Returns the sum of all elements in the array. return ary.reduce(function (accumulator, a) { return accumulator + a; }, 0); } function normalizeArray(ary) { // Scales all array values so that their sum is 1. var sum = sumArray(ary); var new_ary = new Array(ary.length); for (var i = 0; i < ary.length; i++) { new_ary[i] = ary[i] / sum; } return new_ary; } function getIndexOfMaxArrayValue(ary) { return ary.indexOf(Math.max.apply(Math, __spreadArray([], __read(ary)))); } function scaleArray(ary, s) { var new_ary = new Array(ary.length); for (var i = 0; i < ary.length; i++) { new_ary[i] = ary[i] * s; } return new_ary; } var PolyPeachDetector = /** @class */ (function () { function PolyPeachDetector() { this._pitchVisualizationCtx = null; this._gradientA = null; this.microphone_source = null; this.script_processor_node = null; this.media_stream = null; this.histogramProcessor = null; //private _referenceOctave = null; this._referenceFrequency = null; this._lastReferenceFrequency = null; this._tracking_notes = {}; // {note: [harmonic frequencies]} this._tracking_notes_index_to_note = {}; //{pitch_index: '<note_letter><octave_number>'} this._max_level_for_signature = null; this._max_signature_for_note = null; // {harmonic: level} this._signature_error = {}; // {note: lowest signature match error seen since last refresh} this._signature_type = DEFAULT_SIGNATURE_TYPE; this._display_count = 0; this._pitchVisualizationCtx = null; this._gradientA = null; this._pitchSum = 0; this._pitchCanvasWidth = null; //40*12*numOctavesToShow; this._draw = false; this._running = false; this._samples_sent = 0; //this._audioCtx = new AudioContext(); this._audioCtx = new (AudioContext || window.webkitAudioContext)(); this.microphone_source = null; this.script_processor_node = null; this.media_stream = null; this.script_processor_node = null; this.histogramAudioCtx = null; this.histogramSource = null; this.histogramProcessor = null; this._referenceFrequency = Tone.Frequency(DEFAULT_REFERENCE_NOTE); this._num_harmonics = DEFAULT_HARMONICS; this._num_octaves = DEFAULT_NUMBER_OF_OCTAVES; this._num_bins = DEFAULT_BINS; this._lastReferenceFrequency = null; this._tracking_notes = {}; // {note: [harmonic frequencies]} this._tracking_notes_harmonics = new Set(); // all the notes, including their harmonics, we're currently looking for this._tracking_notes_harmonics_midi = new Set(); this._tracking_notes_harmonics_index = new Set(); this._tracking_notes_index_to_note = {}; //{pitch_index: '<note_letter><octave_number>'} //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set this._tracking_notes_found = new Set(); // {set of string notes (e.g. "A3") that have been found} this._tracking_notes_found_midi = new Set(); // Like _tracking_notes_found, except stores midi numbers. //this._currentChroma = new Float32Array(NOTES_PER_OCTAVE); //this._currentPitches = new Float32Array(NOTES_PER_OCTAVE * DEFAULT_NUMBER_OF_OCTAVES); this._currentPitches = new Array(NOTES_PER_OCTAVE * DEFAULT_NUMBER_OF_OCTAVES); this._recording_signature = false; this._max_level_for_signature = null; this._max_signature_for_note = null; // {harmonic: level} this._signature_error = {}; // {note: lowest signature match error seen since last refresh} this.chromagram = new chromagram_1.Chromagram(1024, 44100); this.chromagram.setParameters(this._referenceFrequency.toFrequency(), this._num_harmonics, this._num_octaves, this._num_bins); } PolyPeachDetector.prototype.clearNotesFound = function () { this._tracking_notes_found = new Set(); this._tracking_notes_found_midi = new Set(); }; PolyPeachDetector.prototype.setDraw = function (v) { this._draw = Boolean(v); }; PolyPeachDetector.prototype.getDraw = function () { return this._draw; }; PolyPeachDetector.prototype.onDrawClick = function () { this._draw = !this._draw; }; PolyPeachDetector.prototype.bindUI = function () { document.querySelector('#alert-histogram .start-stop-button').addEventListener('click', (this.onStartStopClick).bind(this)); document.querySelector('#alert-histogram .draw-button').addEventListener('click', (this.onDrawClick).bind(this)); document.querySelector('#alert-histogram .reference-note').addEventListener('change', (this.pushReferenceNote).bind(this)); document.querySelector('#alert-histogram .track-notes').addEventListener('change', (this.on_track_note_change).bind(this)); document.querySelector('#alert-histogram .record-signature-button').addEventListener('click', (this.onRecordSignatureClick).bind(this)); }; PolyPeachDetector.prototype.clearTrackingNotesFound = function () { this._tracking_notes_found = new Set(); this._tracking_notes_found_midi = new Set(); this._signature_error = {}; this.showTrackingNotesFound(); }; PolyPeachDetector.prototype.on_track_note_change = function (event) { this.set_tracking_notes(null); }; PolyPeachDetector.prototype.onStartStopClick = function () { console.log('startstop'); this.toggleAudioStream(); }; PolyPeachDetector.prototype.onRecordSignatureClick = function () { console.log('record'); console.log('this._tracking_notes:'); console.log(this._tracking_notes); var tracking_notes_length = Object.keys(this._tracking_notes).length; console.log('tracking_notes_length:' + tracking_notes_length); if (!tracking_notes_length) { console.log('No tracking note specified.'); return; } if (tracking_notes_length > 1) { console.log('You can only record a signature for one tracking note.'); return; } this._recording_signature = true; this._max_level_for_signature = 0; this._max_signature_for_note = {}; var record_button = document.querySelector('#alert-histogram .record-signature-button'); if (record_button) { record_button.textContent = RECORD_SIGNATURE_BUTTON_ON; } setTimeout((this.onRecordSignatureComplete).bind(this), 5000); }; PolyPeachDetector.prototype.onRecordSignatureComplete = function () { var tracking_note = Object.keys(this._tracking_notes)[0].toUpperCase(); var record_button = document.querySelector('#alert-histogram .record-signature-button'); if (record_button) { record_button.textContent = RECORD_SIGNATURE_BUTTON_OFF; } console.log('record done'); this._recording_signature = false; console.log('signature:'); console.log(this._max_signature_for_note); var textEl = document.querySelector('#alert-histogram .note-signatures'); if (textEl) { var norm = new Array(MAX_HARMONICS); for (var i = 0; i < MAX_HARMONICS; i++) { var harmonicNote = getHarmonicNoteFromNote(tracking_note, i + 1); norm[i] = this._max_signature_for_note[harmonicNote]; } norm = normalizeArray(norm); console.log('signature.norm:'); console.log(norm); textEl['value'] += '[\'' + tracking_note + '\'] = ' + JSON.stringify(norm).replaceAll('"', '\'') + ';\n'; } }; //onWorkerMessage(ev) { //// When the worker initializes, it'll send us a message saying it's ready. //// When we get that message, we'll send it a message telling it what reference note to use as its lowest note. //if (!this._worker_ready) { //this._worker_ready = ev.data.ready; //if (this._worker_ready) { //this.pushReferenceNote(); //} //return; //} //this._pitchCanvasWidth = PIXELS_PER_NOTE * NOTES_PER_OCTAVE * ev.data.numOctaves; //if (ev.data.referenceFrequency != this._lastReferenceFrequency) { //this._referenceOctave = ev.data.referenceOctave; //this._referenceFrequency = Tone.Frequency(ev.data.referenceFrequency); //this.set_tracking_notes(null); //this._lastReferenceFrequency = ev.data.referenceFrequency; //} //this._currentPitches = ev.data.currentPitches; //} PolyPeachDetector.prototype.getPitchVisualizationCtx = function () { if (this._pitchCanvasWidth != null) { var pitchEl = document.querySelector('#alert-histogram .pitches-visualizer'); if (pitchEl) { pitchEl.innerHTML = '<canvas width="' + this._pitchCanvasWidth + '" height="256" style="width:100%;">'; this._pitchVisualizationCtx = document.querySelector('#alert-histogram .pitches-visualizer canvas')['getContext']('2d'); //TODO:fix? this._gradientA = this._pitchVisualizationCtx.createLinearGradient(0, 0, 0, 512); this._gradientA.addColorStop(1, '#000000'); this._gradientA.addColorStop(0.75, '#2ecc71'); this._gradientA.addColorStop(0.25, '#f1c40f'); this._gradientA.addColorStop(0, '#e74c3c'); this._pitchVisualizationCtx.font = '' + PIXELS_PER_NOTE + 'px Georgia'; } else { console.log('no pitch el found'); // The modal containing the canvas has been destroyed, so there's nothing to show. this._pitchVisualizationCtx = null; return; } } return this._pitchVisualizationCtx; }; PolyPeachDetector.prototype.getGradient = function () { return this._gradientA; }; PolyPeachDetector.prototype.pushReferenceNote = function () { console.log('pushReferenceNote'); var note = DEFAULT_REFERENCE_NOTE; var referenceNoteEl = document.querySelector('#alert-histogram .reference-note'); if (referenceNoteEl) { note = (referenceNoteEl['value'] || '').trim(); } if (!note) { return; } var freq_obj = Tone.Frequency(note); if (freq_obj.toMidi() >= UPPER_CUTOFF_MIDI_NOTE) { // TODO:Fix? The browser tends to crash when we go over g6 or a6? console.log('Note too high.'); return; } console.log('pushing reference note'); this._referenceFrequency = freq_obj; this.chromagram.setParameters(this._referenceFrequency.toFrequency(), this._num_harmonics, this._num_octaves, this._num_bins); }; PolyPeachDetector.prototype.set_tracking_notes_from_midi_pitches = function (midi_pitches) { var letter_notes = []; for (var i = 0; i < midi_pitches.length; i++) { letter_notes.push(Tone.Frequency(midi_pitches[i].pitch, 'midi').toNote()); } console.log('letter_notes:'); console.log(letter_notes); this.set_tracking_notes(letter_notes); }; PolyPeachDetector.prototype.set_tracking_notes = function (notes) { /* * Arguments: * * notes := An optional space-delimited string of letter notes, or an array of letter notes. * */ //TODO:call this after reference note changes? if (this._referenceFrequency == null) { console.log('No reference frequency set.'); return; } console.log('updateTrackingNotes'); var notes_list; if (notes == null) { // If no notes given, attempt to pull from modal. var trackNotesEl = document.querySelector('#alert-histogram .track-notes'); if (trackNotesEl) { notes = trackNotesEl['value']; } notes_list = (notes || '').replace(/\s+/g, ' ').trim().split(' '); } else if (typeof (notes) == 'string') { notes_list = notes.replace(/\s+/g, ' ').trim().split(' '); } else if (typeof (notes) == 'object') { notes_list = notes; } console.log('notes_list:' + notes_list); this._tracking_notes = {}; //{note:[pitch indexes for each harmonic]} this._tracking_notes_found = new Set(); this._tracking_notes_found_midi = new Set(); this._tracking_notes_harmonics = new Set(); this._tracking_notes_harmonics_midi = new Set(); this._tracking_notes_harmonics_index = new Set(); this._tracking_notes_index_to_note = {}; var referenceMidi = this._referenceFrequency.toMidi(); for (var i in notes_list) { var note = notes_list[i].toUpperCase().trim(); if (!note) { continue; } var freqObj = Tone.Frequency(note); var freq = freqObj.toFrequency(); this._tracking_notes[note] = []; for (var j = 1; j <= MAX_HARMONICS; j++) { var harmonic_freq = Tone.Frequency(freq * j); var harmonic_midi = harmonic_freq.toMidi(); var pitch_index = harmonic_midi - referenceMidi; if (j <= MIN_HARMONICS) { // Only actively look for the first N harmonics. this._tracking_notes[note].push(pitch_index); this._tracking_notes_harmonics.add(harmonic_freq.toNote()); this._tracking_notes_harmonics_midi.add(harmonic_midi); this._tracking_notes_harmonics_index.add(pitch_index); } // But store more information for additional harmonics to help us in recording signatures. this._tracking_notes_index_to_note[pitch_index] = harmonic_freq.toNote(); } } var ary = Array.prototype.slice.call(Array.from(this._tracking_notes_harmonics)).sort(); if (this._draw) { var noteAndHarmonicsEl = document.querySelector('#alert-histogram .track-notes-and-harmonics'); if (noteAndHarmonicsEl) { noteAndHarmonicsEl.textContent = ary.join(' '); } } }; PolyPeachDetector.prototype.showTrackingNotesFound = function () { var ary = Array.prototype.slice.call(Array.from(this._tracking_notes_found)).sort(); var notesFoundEl = document.querySelector('#alert-histogram .notes-found'); if (notesFoundEl) { notesFoundEl.textContent = ary.join(' '); } }; PolyPeachDetector.prototype.handleHistogramAudioStream = function (stream) { this.media_stream = stream; this.histogramAudioCtx = new AudioContext(); this.histogramSource = this.histogramAudioCtx.createMediaStreamSource(stream); this.histogramProcessor = this.histogramAudioCtx.createScriptProcessor(1024, 1, 1); this.histogramSource.connect(this.histogramProcessor); this.histogramProcessor.onaudioprocess = (this.onAudioProcess).bind(this); this.histogramProcessor.connect(this.histogramAudioCtx.destination); this.updateHistogramVisualization(); }; PolyPeachDetector.prototype.onAudioProcess = function (event) { if (!this._running) { return; } var audioData = event.inputBuffer.getChannelData(0); this.chromagram.processAudioFrame(audioData); this._currentPitches = this.chromagram.getPitches(); if (!this._pitchCanvasWidth) { this._pitchCanvasWidth = PIXELS_PER_NOTE * NOTES_PER_OCTAVE * this.chromagram.getNumOctaves(); this._referenceFrequency = Tone.Frequency(this.chromagram.getReferenceFrequency()); this.set_tracking_notes(null); } }; PolyPeachDetector.prototype.startMicrophone = function () { if (this._running) { console.log('Microphone already started.'); return; } console.log('Starting microphone.'); this._running = true; navigator.mediaDevices.getUserMedia({ audio: { // Minimizes 'smart' features on mobile browsers that cause audio distortions, // making pitch detection virtually impossible. echoCancellation: false, noiseSuppression: false, autoGainControl: false }, video: false }).then((this.handleHistogramAudioStream).bind(this)); console.log('Started microphone.'); }; PolyPeachDetector.prototype.stopMicrophone = function () { if (!this._running) { console.log('Microphone already stopped.'); return; } console.log('Stopping microphone...'); this._running = false; this.media_stream.getTracks().forEach(function (track) { if (track.readyState == 'live' && track.kind === 'audio') { track.stop(); } }); this.histogramSource.disconnect(); this.histogramProcessor.disconnect(); if (this.media_stream && this.media_stream.stop) { this.media_stream.stop(); } this.histogramProcessor.onaudioprocess = null; console.log('Microphone stopped.'); }; PolyPeachDetector.prototype.toggleAudioStream = function () { if (this._running) { this.stopMicrophone(); } else { this.startMicrophone(); } }; PolyPeachDetector.prototype.getNotesFound = function () { return new Set(this._tracking_notes_found); }; PolyPeachDetector.prototype.getMidiNotesFound = function () { return new Set(this._tracking_notes_found_midi); }; PolyPeachDetector.prototype.get_pitch = function () { return 123; //TODO }; PolyPeachDetector.prototype.updateHistogramVisualization = function () { if (this._running) { requestAnimationFrame((this.updateHistogramVisualization).bind(this)); } //console.log('_draw:'+this._draw); // Show the strength of each specific pitch with octave. var pitchVisualizationCtx = this.getPitchVisualizationCtx(); var gradientA = this.getGradient(); var draw = this._draw && pitchVisualizationCtx; //console.log('pitchVisualizationCtx:'); //console.log(pitchVisualizationCtx); //console.log('draw:'+draw); //console.log('this._pitchCanvasWidth:'+this._pitchCanvasWidth); if (draw) { pitchVisualizationCtx.clearRect(0, 0, this._pitchCanvasWidth, 250); } //console.log('this._referenceFrequency:'+this._referenceFrequency); if (this._currentPitches && this._referenceFrequency) { if (this._recording_signature) { //console.log('recording...') var current_max_level = Math.max.apply(Math, __spreadArray([], __read(this._currentPitches))); if (current_max_level > this._max_level_for_signature) { this._max_level_for_signature = current_max_level; //console.log('current_max_level:'+current_max_level); //console.log('this._max_level_for_signature:'+this._max_level_for_signature); var first = true; var first_harmonic = void 0; var harmonic_count = 0; for (var pi in this._tracking_notes_index_to_note) { harmonic_count += 1; var harmonic_note = this._tracking_notes_index_to_note[pi]; if (first) { first_harmonic = this._currentPitches[pi]; first = !first; } var harmonic_level = this._currentPitches[pi]; // If we aren't tracking the high octave harmonics, then estimate it based on rough formula using the first harmonic. if (!harmonic_level || isNaN(harmonic_level)) { harmonic_level = first_harmonic / harmonic_count; } this._max_signature_for_note[harmonic_note] = harmonic_level; //console.log('note:' + harmonic_note + '=' + harmonic_level); } } } var referenceMidi = this._referenceFrequency.toMidi(); var level_sum = 0; var level_max = 0; var pitches_by_index = []; // [[pitch_index, pitch_value]] //console.log('this._currentPitches.length:'+this._currentPitches.length); for (var i = 0; i < this._currentPitches.length; i++) { var value = this._currentPitches[i]; //console.log('value['+i+']'+value); pitches_by_index.push([i, value]); level_sum += value; level_max = Math.max(level_max, value); if (draw) { // Draw level gradient proportional to pitch strength. // The max value we've seen in a chroma is >50 pitchVisualizationCtx.fillStyle = gradientA; pitchVisualizationCtx.fillRect(i * PIXELS_PER_NOTE, 250, PIXELS_PER_NOTE - 1, value * -5); // x, y, width, height // Draw note name at bottom of bar. pitchVisualizationCtx.fillStyle = 'black'; var freq = Tone.Frequency(referenceMidi + i, 'midi'); pitchVisualizationCtx.font = '' + PIXELS_PER_NOTE / 2 + 'px Georgia'; pitchVisualizationCtx.fillText(freq.toNote(), (i * PIXELS_PER_NOTE) + 3, 250); // x,y } } var mean_level = level_sum / this._currentPitches.length; if (draw) { var meanLevelEl = document.querySelector('#alert-histogram .mean-level'); if (meanLevelEl) { meanLevelEl.textContent = String(mean_level); } var maxLevelEl = document.querySelector('#alert-histogram .max-level'); if (maxLevelEl) { maxLevelEl.textContent = String(level_max); } } // Compare normalized pitches to signature. var signature_error = []; for (var note in this._tracking_notes) { var signature = SIGNATURES[this._signature_type][note]; if (!signature) { break; } // Re-normalize the truncated signature. var signature_sample = void 0; signature_sample = signature.slice(0, MIN_HARMONICS); // normalized levels of harmonic notes // Extract the signature from current pitches. var harmonic_sample = new Array(MIN_HARMONICS); //Array(...this._currentPitches); var harmonic_max = 0; var overall_max = Math.max.apply(Math, __spreadArray([], __read(this._currentPitches))); for (var i = 0; i < MIN_HARMONICS; i++) { var harmonicFreqObj = getHarmonicFrequencyFromNote(note, i + 1); var harmonicIndex = harmonicFreqObj.toMidi() - this._referenceFrequency.toMidi(); harmonic_sample[i] = this._currentPitches[harmonicIndex]; harmonic_max = Math.max(harmonic_max, harmonic_sample[i]); } var too_quiet = overall_max < MIN_VOLUME; // Scale signature to match range of harmonic sample. var max_harmonic_level = Math.max.apply(Math, __spreadArray([], __read(this._currentPitches))); max_harmonic_level = Math.max(max_harmonic_level, MIN_VOLUME); var max_signature_level = Math.max.apply(Math, __spreadArray([], __read(signature_sample))); var scaling_ratio = max_harmonic_level / max_signature_level; var scaled_signature_sample = scaleArray(signature_sample, scaling_ratio); // Calculate difference. var error = 0; // mean absolute error. for (var i = 0; i < scaled_signature_sample.length; i++) { error += Math.abs(scaled_signature_sample[i] - harmonic_sample[i]); } error /= signature_sample.length; var show = false; this._signature_error[note] = Math.min(this._signature_error[note] || 999, Number(error.toFixed(2))); this._signature_error[note + 'V'] = Math.max(this._signature_error[note + 'V'] || 0, Number(overall_max.toFixed(2))); if (error < MAX_ERROR_THRESHOLD && !too_quiet) { console.log('Found note:' + note); this._tracking_notes_found.add(note); this._tracking_notes_found_midi.add(Tone.Frequency(note).toMidi()); show = true; } if (DEBUG && draw && !this._display_count) { console.log('note:' + note); console.log('harmonic_max:' + harmonic_max); console.log('signature_sample:'); console.log(signature_sample); console.log('scaled_signature_sample:'); console.log(scaled_signature_sample); console.log('harmonic_sample:'); console.log(harmonic_sample); console.log('error:' + error); } } if (draw) { if (DEBUG) { var signatureErrorEl = document.querySelector('#alert-histogram .signature-error'); if (signatureErrorEl) { v