PageSourceSearch

https://voixtek.com/assets/pitchanalyzer/pitch.js

js voixtek.com collected 2026-10-08 10:27:19 UTC 10,688 bytes, 478 lines download raw bytes

1/*global PitchAnalyzer:true, Float32Array:false, FFT:false */
2/*jshint undef:true node:true browser:true */
3
4PitchAnalyzer = this.PitchAnalyzer = (function () {
5
6var	pi	= Math.PI,
7	pi2	= pi * 2,
8	cos	= Math.cos,
9	pow	= Math.pow,
10	log	= Math.log,
11	max	= Math.max,
12	min	= Math.min,
13	abs	= Math.abs,
14	LN10	= Math.LN10,
15	sqrt	= Math.sqrt,
16	atan2	= Math.atan2,
17	round	= Math.round,
18	inf	= 1/0,
19	FFT_P	= 10,
20	FFT_N	= 1 << FFT_P,
21	BUF_N	= FFT_N * 2;
22
23function remainder (val, div) {
24	return val - round(val/div) * div;
25}
26
27function extend (obj) {
28	var	args	= arguments,
29		l	= args.length,
30		i, n;
31
32
33	for (i=1; i<l; i++){
34		for (n in args[i]){
35			if (args[i].hasOwnProperty(n)){
36				obj[n] = args[i][n];
37			}
38		}
39	}
40
41	return obj;
42}
43
44/**
45 * A class for tones.
46 *
47 * @class
48 * @static PitchAnalyzer
49 * @param default:0.0 min:0.0 type:Number freq The frequency of the tone.
50 * @param default:-Infinity max:0.0 type:Number db The volume of the tone.
51 * @param default:-Infinity max:0.0 type:Number stabledb An average of the volume of the tone.
52 * @param default:0 min:0 type:Integer age How many times the tone has been detected in a row.
53*/
54function Tone () {
55	this.harmonics = new Float32Array(Tone.MAX_HARM);
56}
57
58Tone.prototype = {
59	freq: 0.0,
60	db: -inf,
61	stabledb: -inf,
62	age: 0,
63
64	toString: function () {
65		return '{freq: ' + this.freq + ', db: ' + this.db + ', stabledb: ' + this.stabledb + ', age: ' + this.age + '}';
66	},
67
68/**
69 * Return an approximation of whether the tone has the same frequency as provided.
70 *
71 * @method Tone
72 * @private
73 * @arg {Number} freq The frequency to compare to.
74 * @return {Boolean} Whether it was a match.
75*/
76	matches: function (freq) {
77		return abs(this.freq / freq - 1.0) < 0.05;
78	},
79
80	harmonics: null
81};
82
83Tone.MIN_AGE = 2;
84Tone.MAX_HARM = 48;
85
86/**
87 * An internal class to manage the peak frequencies detected.
88 *
89 * @private
90 * @class
91 * @static PitchAnalyzer
92 * @arg default:0.0 min:0.0 type:Number !freq The frequency of the peak.
93 * @arg default:-Infinity max:0.0 type:Number !db The volume of the peak.
94*/
95function Peak (freq, db) {
96	this.freq = typeof freq === 'undefined' ? this.freq : freq;
97	this.db = typeof db === 'undefined' ? this.db : db;
98
99	this.harm = new Array(Tone.MAX_HARM);
100}
101
102Peak.prototype = {
103	harm: null,
104
105	freq: 0.0,
106	db: -inf,
107
108/**
109 * Resets the peak to default values.
110 *
111 * @method Peak
112 * @private
113*/
114	clear: function () {
115		this.freq	= Peak.prototype.freq;
116		this.db		= Peak.prototype.db;
117	}
118};
119
120/**
121 * Finds the best matching peak from a certain point in the array of peaks.
122 *
123 * @name match
124 * @static Peak
125 * @private
126 * @arg {Array} peaks The peaks to search from.
127 * @arg {Integer} pos The position to find the match for.
128 * @return {Peak} The best matching peak.
129*/
130Peak.match = function (peaks, pos) {
131	var best = pos;
132
133	if (peaks[pos - 1].db > peaks[best].db) best = pos - 1;
134	if (peaks[pos + 1].db > peaks[best].db) best = pos + 1;
135
136	return peaks[best];
137};
138
139/**
140 * A class to analyze pitch from input data.
141 *
142 * @class PitchAnalyzer
143 * @arg {Object} !options Options to override default values.
144*/
145function Analyzer (options) {
146	options = extend(this, options);
147
148	this.data = new Float32Array(FFT_N);
149	this.buffer = new Float32Array(BUF_N);
150	this.fftLastPhase = new Float32Array(BUF_N);
151	this.tones = [];
152
153	if (this.wnd === null) this.wnd = Analyzer.calculateWindow();
154	this.setupFFT();
155}
156
157Analyzer.prototype = {
158	wnd: null,
159	data: null,
160	fft: null,
161	tones: null,
162	fftLastPhase: null,
163	buffer: null,
164
165	offset: 0,
166	bufRead: 0,
167	bufWrite: 0,
168
169	MIN_FREQ: 45,
170	MAX_FREQ: 5000,
171
172	sampleRate: 44100,
173	step: 200,
174	oldFreq: 0.0,
175
176	peak: 0.0,
177
178/**
179 * Gets the current peak level in dB (negative value, 0.0 = clipping).
180 *
181 * @method PitchAnalyzer
182 * @return {Number} The current peak level (db).
183*/
184	getPeak: function () {
185		return 10.0 * log(this.peak) / LN10;
186	},
187
188	findTone: function (minFreq, maxFreq) {
189		if (!this.tones.length) {
190			this.oldFreq = 0.0;
191			return null;
192		}
193
194		minFreq = typeof minFreq === 'undefined' ? 65.0 : minFreq;
195		maxFreq = typeof maxFreq === 'undefined' ? 1000.0 : maxFreq;
196
197		var db = max.apply(null, this.tones.map(Analyzer.mapdb));
198		var best = null;
199		var bestscore = 0;
200
201		for (var i=0; i<this.tones.length; i++) {
202			if (this.tones[i].db < db - 20.0 || this.tones[i].freq < minFreq || this.tones[i].age < Tone.MIN_AGE) continue;
203			if (this.tones[i].freq > maxFreq) break;
204
205			var score = this.tones[i].db - max(180.0, abs(this.tones[i].freq - 300)) / 10.0;
206
207			if (this.oldFreq !== 0.0 && abs(this.tones[i].freq / this.oldFreq - 1.0) < 0.05) score += 10.0;
208			if (best && bestscore > score) break;
209
210			best = this.tones[i];
211			bestscore = score;
212		}
213
214		this.oldFreq = (best ? best.freq : 0.0);
215		return best;
216	},
217
218/**
219 * Copies data to the internal buffers for processing and calculates peak.
220 * Note that if the buffer overflows, unprocessed data gets discarded.
221 *
222 * @method PitchAnalyzer
223 * @arg {Float32Array} data The input data.
224*/
225	input: function (data) {
226		var buf = this.buffer;
227		var r = this.bufRead;
228		var w = this.bufWrite;
229
230		var overflow = false;
231
232		for (var i=0; i<data.length; i++) {
233			var s = data[i];
234			var p = s * s;
235
236			if (p > this.peak) this.peak = p; else this.peak *= 0.999;
237
238			buf[w] = s;
239
240			w = (w + 1) % BUF_N;
241
242			if (w === r) overflow = true;
243		}
244
245		this.bufWrite = w;
246		if (overflow) this.bufRead = (w + 1) % BUF_N;
247	},
248
249/**
250 * Processes available data and calculates tones.
251 *
252 * @method PitchAnalyzer
253*/
254	process: function () {
255		while (this.calcFFT()) this.calcTones();
256	},
257
258/**
259 * Matches new tones against old ones, merging similar ones.
260 *
261 * @method PitchAnalyzer
262 * @private
263*/
264	mergeWithOld: function (tones) {
265		var i, n;
266
267		tones.sort(function (a, b) { return a.freq < b.freq ? -1 : a.freq > b.freq ? 1 : 0; });
268
269		for (i=0, n=0; i<this.tones.length; i++) {
270			while (n < tones.length && tones[n].freq < this.tones[i].freq) n++;
271
272			if (n < tones.length && tones[n].matches(this.tones[i].freq)) {
273				tones[n].age = this.tones[i].age + 1;
274				tones[n].stabledb = 0.8 * this.tones[i].stabledb + 0.2 * tones[n].db;
275				tones[n].freq = 0.5 * (this.tones[i].freq + tones[n].freq);
276			} else if (this.tones[i].db > -80.0) {
277				tones.splice(n, 0, this.tones[i]);
278				tones[n].db -= 5.0;
279				tones[n].stabledb -= 0.1;
280			}
281
282		}
283	},
284
285/**
286 * Calculates the tones from the FFT data.
287 *
288 * @method PitchAnalyzer
289 * @private
290*/
291	calcTones: function () {
292		var	freqPerBin	= this.sampleRate / FFT_N,
293			phaseStep	= pi2 * this.step / FFT_N,
294			normCoeff	= 1.0 / FFT_N,
295			minMagnitude	= pow(10, -100.0 / 20.0) / normCoeff,
296			kMin		= ~~max(1, this.MIN_FREQ / freqPerBin),
297			kMax		= ~~min(FFT_N / 2, this.MAX_FREQ / freqPerBin),
298			peaks		= [],
299			tones		= [],
300			k, k2, p, n, t, count, freq, magnitude, phase, delta, prevdb, db, bestDiv,
301			bestScore, div, score;
302
303		for (k=0; k <= kMax; k++) {
304			peaks.push(new Peak());
305		}
306
307		for (k=1, k2=2; k<=kMax; k++, k2 += 2) {
308			/* complex absolute */
309			magnitude = sqrt(this.fft[k2] * this.fft[k2] + this.fft[k2+1] * this.fft[k2+1]);
310			/* complex arguscosine */
311			phase = atan2(this.fft[k2+1], this.fft[k2]);
312
313			delta = phase - this.fftLastPhase[k];
314			this.fftLastPhase[k] = phase;
315
316			delta -= k * phaseStep;
317			delta = remainder(delta, pi2);
318			delta /= phaseStep;
319
320			freq = (k + delta) * freqPerBin;
321
322			if (freq > 1.0 && magnitude > minMagnitude) {
323				peaks[k].freq = freq;
324				peaks[k].db = 20.0 * log(normCoeff * magnitude) / LN10;
325			}
326		}
327
328		prevdb = peaks[0].db;
329
330		for (k=1; k<kMax; k++) {
331			db = peaks[k].db;
332			if (db > prevdb) peaks[k - 1].clear();
333			if (db < prevdb) peaks[k].clear();
334			prevdb = db;
335		}
336
337		for (k=kMax-1; k >= kMin; k--) {
338			if (peaks[k].db < -70.0) continue;
339
340			bestDiv = 1;
341			bestScore = 0;
342
343			for (div = 2; div <= Tone.MAX_HARM && k / div > 1; div++) {
344				freq = peaks[k].freq / div;
345				score = 0;
346
347				for (n=1; n<div && n<8; n++) {
348					p = Peak.match(peaks, ~~(k * n / div));
349					score--;
350					if (p.db < -90.0 || abs(p.freq / n / freq - 1.0) > 0.03) continue;
351					if (n === 1) score += 4;
352					score += 2;
353				}
354
355				if (score > bestScore) {
356					bestScore = score;
357					bestDiv = div;
358				}
359			}
360
361			t = new Tone();
362
363			count = 0;
364
365			freq = peaks[k].freq / bestDiv;
366
367			t.db = peaks[k].db;
368
369			for (n=1; n<=bestDiv; n++) {
370				p = Peak.match(peaks, ~~(k * n / bestDiv));
371
372				if (abs(p.freq / n / freq - 1.0) > 0.03) continue;
373
374				if (p.db > t.db - 10.0) {
375					t.db = max(t.db, p.db);
376					count++;
377					t.freq += p.freq / n;
378				}
379
380				t.harmonics[n - 1] = p.db;
381				p.clear();
382			}
383
384			t.freq /= count;
385
386			if (t.db > -50.0 - 3.0 * count) {
387				t.stabledb = t.db;
388				tones.push(t);
389			}
390		}
391
392		this.mergeWithOld(tones);
393
394		this.tones = tones;
395	},
396
397/**
398 * Calculates the FFT for the input signal, if enough is available.
399 *
400 * @method PitchAnalyzer
401 * @private
402 * @return {Boolean} Whether there was enough data to process.
403*/
404	calcFFT: function () {
405		var r = this.bufRead;
406
407		if ((BUF_N + this.bufWrite - r) % BUF_N <= FFT_N) return false;
408
409		for (var i=0; i<FFT_N; i++) {
410			this.data[i] = this.buffer[(r + i) % BUF_N];
411		}
412
413		this.bufRead = (r + this.step) % BUF_N;
414
415		this.processFFT(this.data, this.wnd);
416
417		return true;
418	},
419
420	setupFFT: function () {
421		var RFFT = typeof FFT !== 'undefined' && FFT;
422
423		if (!RFFT) {
424			try {
425				RFFT = require('fft');
426			} catch (e) {
427				throw Error("pitch.js requires fft.js");
428			}
429		}
430
431		RFFT = RFFT.complex;
432
433		this.rfft = new RFFT(FFT_N, false);
434		this.fft = new Float32Array(FFT_N * 2);
435		this.fftInput = new Float32Array(FFT_N);
436	},
437
438	processFFT: function (data, wnd) {
439		var i;
440
441		for (i=0; i<data.length; i++) {
442			this.fftInput[i] = data[i] * wnd[i];
443		}
444
445		this.rfft.simple(this.fft, this.fftInput, 'real');
446	}
447};
448
449Analyzer.mapdb = function (e) {
450	return e.db;
451};
452
453Analyzer.Tone = Tone;
454
455/**
456 * Calculates a Hamming window for the size FFT_N, scaled up with FFT_N.
457 *
458 * @static PitchAnalyzer
459 * @return {Float32Array} The hamming window.
460*/
461Analyzer.calculateWindow = function () {
462	var	i,
463		w = new Float32Array(FFT_N);
464
465	for (i=0; i<FFT_N; i++) {
466		w[i] = 0.53836 - 0.46164 * cos(pi2 * i / (FFT_N - 1));
467	}
468
469	return w;
470};
471
472return Analyzer;
473
474}());
475
476if (typeof module !== 'undefined') {
477	module.exports = PitchAnalyzer;
478}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.