diff --git a/README.md b/README.md index e6dc7720..df486015 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,11 @@ Goertzel.Utilities.doublePeakFilter(energies1,energies2,sensitivity) doublePeakFilter does the same thing as the normal peak filter but with two arrays at the same time. +```javascript +Goertzel.Utilities.generateSineBuffer(frequencies=[], sampleRate, numberOfSamples) +``` +generateSineBuffer lets you create an artificial buffer of any number of combined sine waves. Added for testing purposes, but could have any number of uses. If you needed to create an oscillator to generate DTMF or other tones without access to a browser's audio API, that's your function. + ## conclusion I hope this project will be useful for anyone who wants to understand the Goertzel algorithm or basic signal processing with the HTML5 Audio API. diff --git a/build/dtmf.js b/build/dtmf.js index e902f5ea..8e99dfb9 100644 --- a/build/dtmf.js +++ b/build/dtmf.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.9.3 +// Generated by CoffeeScript 1.10.0 var DTMF; DTMF = (function() { diff --git a/build/goertzel.js b/build/goertzel.js index 761f8c27..be98f873 100644 --- a/build/goertzel.js +++ b/build/goertzel.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.9.3 +// Generated by CoffeeScript 1.10.0 var Goertzel; Goertzel = (function() { @@ -151,23 +151,29 @@ Goertzel = (function() { return false; } }, - generateSine: function(frequency, sampleRate, numberOfSamples) { - var buffer, i, v; - buffer = []; + generateSineBuffer: function(frequencies, sampleRate, numberOfSamples) { + var buffer, frequency, i, j, len, val, volumePerSine; + buffer = new (Uint8ClampedArray || Array)(numberOfSamples); + volumePerSine = 1 / frequencies.length; i = 0; while (i < numberOfSamples) { - v = Math.sin(Math.PI * 2 * (i / sampleRate) * frequency); - buffer.push(v); + val = 0; + for (j = 0, len = frequencies.length; j < len; j++) { + frequency = frequencies[j]; + val += Math.sin(Math.PI * 2 * (i / sampleRate) * frequency) * volumePerSine; + } + buffer[i] = val; i++; } return buffer; }, floatBufferToInt: function(floatBuffer) { - var i, intBuffer; - intBuffer = []; + var floatBufferLength, i, intBuffer; + floatBufferLength = floatBuffer.length; + intBuffer = new (Uint8ClampedArray || Array)(floatBufferLength); i = 0; - while (i < floatBuffer.length) { - intBuffer.push(Goertzel.Utilities.floatToIntSample(floatBuffer[i])); + while (i < floatBufferLength) { + intBuffer[i] = Goertzel.Utilities.floatToIntSample(floatBuffer[i]); i++; } return intBuffer; diff --git a/spec/dtmf.spec.coffee b/spec/dtmf.spec.coffee new file mode 100644 index 00000000..85a336c8 --- /dev/null +++ b/spec/dtmf.spec.coffee @@ -0,0 +1,36 @@ +`Goertzel = require('../build/goertzel');` +DTMF = require('../build/dtmf') +require('jasmine-expect') + +describe 'DTMF', -> + + pairs = [ + {low: 697, high: 1209, char: '1'} + {low: 697, high: 1336, char: '2'} + {low: 697, high: 1477, char: '3'} + {low: 697, high: 1633, char: 'A'} + {low: 770, high: 1209, char: '4'} + {low: 770, high: 1336, char: '5'} + {low: 770, high: 1477, char: '6'} + {low: 770, high: 1633, char: 'B'} + {low: 852, high: 1209, char: '7'} + {low: 852, high: 1336, char: '8'} + {low: 852, high: 1477, char: '9'} + {low: 852, high: 1633, char: 'C'} + {low: 941, high: 1209, char: '*'} + {low: 941, high: 1336, char: '0'} + {low: 941, high: 1477, char: '#'} + {low: 941, high: 1633, char: 'D'} + ] + + describe '#processSample', -> + it 'identifies all dial tones', -> + dtmf = new DTMF + sampleRate: 44100 + peakFilterSensitivity: 1.4 + repeatMin: 1 + for pair in pairs + dualTone = Goertzel.Utilities.generateSineBuffer([pair.low, pair.high], 44100, 512) + buffer = Goertzel.Utilities.floatBufferToInt(dualTone) + dtmf.processBuffer(buffer) + expect(dtmf.processBuffer(buffer)).toContain(pair.char) \ No newline at end of file diff --git a/spec/dtmf.spec.js b/spec/dtmf.spec.js new file mode 100644 index 00000000..4d253146 --- /dev/null +++ b/spec/dtmf.spec.js @@ -0,0 +1,97 @@ +// Generated by CoffeeScript 1.10.0 +Goertzel = require('../build/goertzel');; +var DTMF; + +DTMF = require('../build/dtmf'); + +require('jasmine-expect'); + +describe('DTMF', function() { + var pairs; + pairs = [ + { + low: 697, + high: 1209, + char: '1' + }, { + low: 697, + high: 1336, + char: '2' + }, { + low: 697, + high: 1477, + char: '3' + }, { + low: 697, + high: 1633, + char: 'A' + }, { + low: 770, + high: 1209, + char: '4' + }, { + low: 770, + high: 1336, + char: '5' + }, { + low: 770, + high: 1477, + char: '6' + }, { + low: 770, + high: 1633, + char: 'B' + }, { + low: 852, + high: 1209, + char: '7' + }, { + low: 852, + high: 1336, + char: '8' + }, { + low: 852, + high: 1477, + char: '9' + }, { + low: 852, + high: 1633, + char: 'C' + }, { + low: 941, + high: 1209, + char: '*' + }, { + low: 941, + high: 1336, + char: '0' + }, { + low: 941, + high: 1477, + char: '#' + }, { + low: 941, + high: 1633, + char: 'D' + } + ]; + return describe('#processSample', function() { + return it('identifies all dial tones', function() { + var buffer, dtmf, dualTone, i, len, pair, results; + dtmf = new DTMF({ + sampleRate: 44100, + peakFilterSensitivity: 1.4, + repeatMin: 1 + }); + results = []; + for (i = 0, len = pairs.length; i < len; i++) { + pair = pairs[i]; + dualTone = Goertzel.Utilities.generateSineBuffer([pair.low, pair.high], 44100, 512); + buffer = Goertzel.Utilities.floatBufferToInt(dualTone); + dtmf.processBuffer(buffer); + results.push(expect(dtmf.processBuffer(buffer)).toContain(pair.char)); + } + return results; + }); + }); +}); diff --git a/spec/goertzel.spec.coffee b/spec/goertzel.spec.coffee index de736106..35a96798 100644 --- a/spec/goertzel.spec.coffee +++ b/spec/goertzel.spec.coffee @@ -18,7 +18,7 @@ describe 'Goertzel', -> it 'biases towards the expected frequency', -> for frequency in allFrequencies - buffer = Goertzel.Utilities.generateSine(frequency, 8000, 2000) + buffer = Goertzel.Utilities.generateSineBuffer([frequency], 8000, 2000) for sample in buffer goertzel.processSample sample for f in allFrequencies diff --git a/spec/goertzel.spec.js b/spec/goertzel.spec.js index f2f67ff7..c80a9ee4 100644 --- a/spec/goertzel.spec.js +++ b/spec/goertzel.spec.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.9.3 +// Generated by CoffeeScript 1.10.0 var Goertzel; Goertzel = require('../build/goertzel'); @@ -20,7 +20,7 @@ describe('Goertzel', function() { results = []; for (i = 0, len = allFrequencies.length; i < len; i++) { frequency = allFrequencies[i]; - buffer = Goertzel.Utilities.generateSine(frequency, 8000, 2000); + buffer = Goertzel.Utilities.generateSineBuffer([frequency], 8000, 2000); for (j = 0, len1 = buffer.length; j < len1; j++) { sample = buffer[j]; goertzel.processSample(sample); diff --git a/src/goertzel.coffee b/src/goertzel.coffee index afc48726..ebf705d8 100644 --- a/src/goertzel.coffee +++ b/src/goertzel.coffee @@ -104,20 +104,25 @@ class Goertzel false ## useful for testing purposes - generateSine: (frequency, sampleRate, numberOfSamples) -> - buffer = [] + + generateSineBuffer: (frequencies, sampleRate, numberOfSamples) -> + buffer = new (Uint8ClampedArray or Array)(numberOfSamples) + volumePerSine = 1 / frequencies.length i = 0 while i < numberOfSamples - v = Math.sin(Math.PI * 2 * (i / sampleRate) * frequency) - buffer.push v + val = 0 + for frequency in frequencies + val += (Math.sin(Math.PI * 2 * (i / sampleRate) * frequency) * volumePerSine) + buffer[i] = val i++ - buffer + buffer floatBufferToInt: (floatBuffer) -> - intBuffer = [] + floatBufferLength = floatBuffer.length + intBuffer = new (Uint8ClampedArray or Array)(floatBufferLength) i = 0 - while i < floatBuffer.length - intBuffer.push Goertzel.Utilities.floatToIntSample(floatBuffer[i]) + while i < floatBufferLength + intBuffer[i] = Goertzel.Utilities.floatToIntSample(floatBuffer[i]) i++ intBuffer