Skip to content

Commit

Permalink
Exposes additional configuration options and overrides. Updates READM…
Browse files Browse the repository at this point in the history
…E.md
  • Loading branch information
Kevin Dolan committed May 14, 2022
1 parent 842f8dd commit 84f1044
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ yarn-error.log

secrets.json
config/local*
config/test*
build/*
*.wav
license.txt
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ and a fresh default `config/` directory will be generated when you restart FD To
##### Detection - Required
- `minRecordingLengthSec`: Recordings will be a minimum of this many seconds. Use a value that works for you. Keep in
mind the larger this value, the longer it will take for Post Recording notifications to be sent.
- `maxRecordingLengthSec`: Recordings will be a maximum of this many seconds. Use a value that works for you. Keep in
mind the larger this value, the longer it will take for Post Recording notifications to be sent. If no value is specified
the default is 1.5x the minRecordingLengthSec.
- `defaultMatchThreshold`: This is the default number of samples needed for a frequency match to be completed. For example,
a tone of 1000Hz with a `matchThreshold` of 6 requires that 6 samples match 1000Hz within the `tolerancePercent` to be
considered a match. Adjust this lower to increase sensitivity (tones matched faster). Adjust this value higher to decrease
Expand All @@ -157,6 +160,14 @@ and a fresh default `config/` directory will be generated when you restart FD To
Default is `0.05` or 5%. For example, if a tone of 1000Hz is specified with a `tolerancePercent` of `0.05` any detected
sample with a frequency between 950Hz and 1050Hz is considered a match counting towards the `matchThreshold` for that
tone.
- `defaultResetTimeoutMs`: This is how many milliseconds after detecting a tone (or series of tones) the detector will wait
before resetting. Make sure to account for the length of tones when setting this value. For example, if each tone lasts
2 seconds a `defaultResetTimeoutMs` value of `3000` would only allow 1 second to match the next tone in the sequence.
- `defaultLockoutTimeoutMs`: Acts as an absolute refractory period for the detector after the series of tones is detected.
This prevents a single tone from triggering multiple detection events.
- `isRecordingEnabled`: Global setting for enabling recording. This option can be overridden in each detector config.
This option is only used as a fallback value if a detector config does not specify the `isRecordingEnabled`. The default
behavior is `true`.
:memo: `defaultMatchThreshold` can be overridden at the `detector` level using `matchThreshold`. `defaultTolerancePercent`
can be overridden at the `detector` level using `tolerancePercent`
Expand All @@ -178,6 +189,12 @@ as a starting point. There can be as many detectors in a single configuration fi
is used.
- `tolerancePercent` (Optional): If specified overrides the `defaultTolerancePercent`. If excluded the `defaultTolerancePercent`
is used.
- `resetTimeoutMs` (Optional): Sets reset timeout on a per detector basis. See `defaultResetTimeoutMs` above.
- `isRecordingEnabled` (Optional): Overrides the global `isRecordingEnabled` option. If specified the value ALWAYS overrides the
global value.
- `lockoutTimeoutMs` (Optional): Sets lockout timeout on a per detector basis. See `defaultLockoutTimeoutMs` above.
- `minRecordingLengthSec` (Optional): Sets min recording length on a per detector basis. See `minRecordingLengthSec` above.
- `maxRecordingLengthSec` (Optional): Sets max recording length on a per detector basis. See `maxRecordingLengthSec` above.
###### Notifications
Notification settings are specified for each detector individually.
The `notifications` property is made up of two sections that use the same format: `preRecording` and `postRecording`.
Expand Down
9 changes: 8 additions & 1 deletion bin/fdToneNotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,21 @@ async function fdToneNotify({webServer=false}={}){
silenceAmplitude: config.audio.silenceAmplitude,
sampleRate: config.audio.sampleRate,
minRecordingLengthSec: config.audio.minRecordingLengthSec,
frequencyScaleFactor: config.audio.frequencyScaleFactor
maxRecordingLengthSec: config.audio.maxRecordingLengthSec,
frequencyScaleFactor: config.audio.frequencyScaleFactor,
recording: config.detection.hasOwnProperty("isRecordingEnabled") ? !!config.detection.isRecordingEnabled : null //Defaults to null to indicate not set
});
config.detection.detectors.forEach(detectorConfig => {
const options = {
name: detectorConfig.name,
tones: detectorConfig.tones,
resetTimeoutMs: detectorConfig.resetTimeoutMs ? detectorConfig.resetTimeoutMs : config.detection.defaultResetTimeoutMs,
lockoutTimeoutMs: detectorConfig.lockoutTimeoutMs ? detectorConfig.lockoutTimeoutMs : config.detection.defaultLockoutTimeoutMs,
minRecordingLengthSec: detectorConfig.minRecordingLengthSec ? detectorConfig.minRecordingLengthSec : config.detection.minRecordingLengthSec,
maxRecordingLengthSec: detectorConfig.maxRecordingLengthSec ? detectorConfig.maxRecordingLengthSec : config.detection.maxRecordingLengthSec,
matchThreshold: detectorConfig.matchThreshold ? detectorConfig.matchThreshold : config.detection.defaultMatchThreshold,
tolerancePercent: detectorConfig.tolerancePercent ? detectorConfig.tolerancePercent : config.detection.defaultTolerancePercent,
isRecordingEnabled: detectorConfig.hasOwnProperty("isRecordingEnabled") ? !!detectorConfig.isRecordingEnabled : null, //Defaults to null to indicate not set
notifications: detectorConfig.notifications
};
log.info(`Adding Detector for ${options.name} with tones ${options.tones.map(v => `${v}Hz`).join(', ')}. `
Expand Down
11 changes: 10 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@
"detection":
{
"minRecordingLengthSec": 30,
"maxRecordingLengthSec": 45,
"defaultMatchThreshold": 6,
"defaultTolerancePercent": 0.05,
"defaultResetTimeoutMs": 5000,
"defaultLockoutTimeoutMs": 7000,
"isRecordingEnabled": true,
"detectors": [
{
"name": "Test Fire Department",
"tones": [911, 3000],
"matchThreshold": 6,
"tolerancePercent": 0.02,
"notifications": {
"resetTimeoutMs": 4000,
"isRecordingEnabled": true,
"lockoutTimeoutMs": 6000,
"minRecordingLengthSec": 45,
"maxRecordingLengthSec": 60,
"notifications": {
"preRecording": {
"pushbullet": [
{
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function setupProgram(){
'is monitored. When a multi tone is detected the result is logged to the console. Use this mode to determine the frequencies to monitor and ' +
'enter the results in the "tones" parameter for the corresponding department.')
.option('--test-notifications', 'Send test notifications')
.option('--csv-to-config', 'Send test notifications')
.option('--csv-to-config', 'Build a config file from a csv')
.option('--debug', 'Overrides FD_LOG_LEVEL environment var forcing the log level to debug')
.option('--silly', 'Overrides FD_LOG_LEVEL environment var forcing the log level to silly')
.option('--instance-name', 'Overrides NODE_APP_INSTANCE environment allowing different config files for different instances running' +
Expand Down
17 changes: 14 additions & 3 deletions obj/TonesDetector.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const {SilenceDetector} = require("./SilenceDetector");
class TonesDetector extends EventEmitter{
constructor({name, tones= [], tolerancePercent= 0.02,
matchThreshold= 8, silenceAmplitude=0.05,
notifications, lockoutTimeoutMs=5000, resetTimeoutMs=7000}) {
notifications, lockoutTimeoutMs=5000, resetTimeoutMs=7000, minRecordingLengthSec=30, maxRecordingLengthSec}) {
super();

this.name = name;
this.name = name ? name : ``;
this.tones = tones;
this.tolerancePercent = tolerancePercent;
this.matchThreshold = matchThreshold;
Expand All @@ -27,6 +27,15 @@ class TonesDetector extends EventEmitter{

this.resetTimeoutMs = resetTimeoutMs;
this._fullResetTimeout = null;

//Not used here but accessed by the RecordingService
this.minRecordingLengthSec = minRecordingLengthSec;
this.maxRecordingLengthSec = maxRecordingLengthSec ? maxRecordingLengthSec : minRecordingLengthSec * 1.5;
if(this.maxRecordingLengthSec < this.minRecordingLengthSec){
log.alert(`For tone ${name} the minRecordingLengthSec is ${this.minRecordingLengthSec} and the maxRecordingLengthSec ` +
`is ${maxRecordingLengthSec}. This is invalid and maxRecordingLengthSec will default to 1.5x minRecordingLengthSec.`);
this.maxRecordingLengthSec = this.minRecordingLengthSec * 1.5;
}
}

__buildToneDetectors(){
Expand Down Expand Up @@ -85,7 +94,9 @@ class TonesDetector extends EventEmitter{
tones: this.tones,
tolerancePercent: this.tolerancePercent,
matchThreshold: this.matchThreshold,
notifications: this.notifications
notifications: this.notifications,
minRecordingLengthSec: this.minRecordingLengthSec,
maxRecordingLengthSec: this.maxRecordingLengthSec,
}
}

Expand Down
49 changes: 38 additions & 11 deletions service/DetectionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const {NotificationParams} = require('../obj/NotificationParams');
const NO_DATA_INTERVAL_SEC = 30;

class DetectionService extends EventEmitter{
constructor({audioInterface, sampleRate, recording: isRecordingEnabled = true, areNotificationsEnabled=true,
minRecordingLengthSec=30, frequencyScaleFactor=1,
constructor({audioInterface, sampleRate, recording: isRecordingEnabled, areNotificationsEnabled=true,
minRecordingLengthSec=30, maxRecordingLengthSec, frequencyScaleFactor=1,
silenceAmplitude=0.05,
}) {
super();
Expand All @@ -32,9 +32,16 @@ class DetectionService extends EventEmitter{
this._audioProcessor.on('audio', data => this.emit('audio', data)); //Forward event

this.frequencyScaleFactor = frequencyScaleFactor;

this.minRecordingLengthSec = minRecordingLengthSec;
this.maxRecordingLengthSec = maxRecordingLengthSec ? maxRecordingLengthSec : minRecordingLengthSec * 1.5;
if(this.maxRecordingLengthSec < this.minRecordingLengthSec){
log.alert(`The global minRecordingLengthSec is ${this.minRecordingLengthSec} and the maxRecordingLengthSec ` +
`is ${maxRecordingLengthSec}. This is invalid and maxRecordingLengthSec will default to 1.5x minRecordingLengthSec.`);
this.maxRecordingLengthSec = this.minRecordingLengthSec * 1.5;
}

this.isRecordingEnabled = isRecordingEnabled;
this.isRecordingEnabled = isRecordingEnabled === undefined ? null : isRecordingEnabled ;
this.areNotificationsEnabled = areNotificationsEnabled;

this.toneDetectors = [];
Expand All @@ -51,19 +58,29 @@ class DetectionService extends EventEmitter{
});
}

addToneDetector({name, tones= [], tolerancePercent= 0.02,
matchThreshold= 6, logLevel="debug", notifications, resetTimeoutMs=7000}){
addToneDetector({name, tones= [], tolerancePercent, isRecordingEnabled,
matchThreshold, logLevel="debug", notifications, resetTimeoutMs, lockoutTimeoutMs, minRecordingLengthSec, maxRecordingLengthSec}){
const tonesDetector = new TonesDetector({
name,
tones: tones,
matchThreshold: matchThreshold,
tolerancePercent: tolerancePercent,
matchThreshold,
tolerancePercent,
notifications,
resetTimeoutMs
resetTimeoutMs,
lockoutTimeoutMs,
minRecordingLengthSec: minRecordingLengthSec ? minRecordingLengthSec : this.minRecordingLengthSec,
maxRecordingLengthSec: maxRecordingLengthSec ? maxRecordingLengthSec : this.maxRecordingLengthSec
});

const message = `Creating detector for tone(s) ${tones.map(v => `${v}Hz`).join(", ")} ` +
`with tolerance ±${tolerancePercent}`;
if(isRecordingEnabled === undefined)
isRecordingEnabled = null;
const calculatedIsRecordingEnabled = this._isRecordingEnabled(isRecordingEnabled);

const message = `Creating detector for${tonesDetector.name ? ` ${tonesDetector.name}` : ""} tone(s) ${tones.map(v => `${v}Hz`).join(", ")} ` +
`with tolerance ±${tonesDetector.tolerancePercent}, match threshold ${tonesDetector.matchThreshold}, ` +
`reset timeout ${tonesDetector.resetTimeoutMs}ms, lockout timeout ${tonesDetector.lockoutTimeoutMs}ms, ` +
`minimum recording length ${tonesDetector.minRecordingLengthSec} seconds, max recording length ${tonesDetector.maxRecordingLengthSec} seconds, ` +
`and recoding is ${calculatedIsRecordingEnabled ? "enabled" : "disabled"}.`;
if(!log[logLevel])
log.debug(message);
else
Expand Down Expand Up @@ -95,7 +112,7 @@ class DetectionService extends EventEmitter{
return results;
});

if(this.isRecordingEnabled) {
if(calculatedIsRecordingEnabled) {
//Start recording in new thread. Post recording notifications sent from new thread
log.debug(`Starting recorder & post recording notification processing. Thread Id: ${recordingThread.threadId}`);
recordingThread.sendMessage(notificationParams.toObj());
Expand All @@ -110,6 +127,16 @@ class DetectionService extends EventEmitter{
this.toneDetectors.push(tonesDetector);
return tonesDetector;
}

_isRecordingEnabled(detectorLevelIsRecordingEnabled) {
if (detectorLevelIsRecordingEnabled === null) {
if (this.isRecordingEnabled === null) //If not specified globally return true
return true;
return this.isRecordingEnabled; //Use specified global value
} else
return detectorLevelIsRecordingEnabled; //use specified detector value
}

}

module.exports = {DetectionService};
8 changes: 4 additions & 4 deletions service/RecordingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ class RecordingService{

//Failsafe Timeout
const failSafeTimeout = setTimeout(() => {
log.warning("Fail Safe End Recoding - Max Time Limit Reached");
log.warning(`Fail Safe End Recoding - Max Time Limit Reached after ${notificationParams.detector.maxRecordingLengthSec} seconds`);
cb();
}, config.detection.minRecordingLengthSec * 1.5 * 1000);
}, notificationParams.detector.maxRecordingLengthSec * 1000);

//Min Recording Length Timeout
setTimeout(function(){
log.debug("Initial Recording Complete... Waiting for silence");
log.debug(`Initial Recording Complete after ${notificationParams.detector.minRecordingLengthSec} seconds... Waiting for silence`);
silenceDetector.on('silenceDetected', cb);
}, config.detection.minRecordingLengthSec * 1000);
}, notificationParams.detector.minRecordingLengthSec * 1000);
});
}

Expand Down
25 changes: 25 additions & 0 deletions test/service/DetectionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ describe("DetectionService", function() {
});
});

it(`should correctly calculate isRecordingEnabled`, async function() {
const args = DYNAMIC_TEST_ARGS[0];
let detection = new DetectionService({micInputStream: null, sampleRate: args.sampleRate, frequencyScaleFactor: args.frequencyScaleFactor, notifications: false});
//isRecordingEnabled was not specified in args
//When not specified at global level calculated value should match detector value
expect(detection._isRecordingEnabled(true)).to.be.true;
expect(detection._isRecordingEnabled(false)).to.be.false;
//When not specified in either location should default to true
expect(detection._isRecordingEnabled(null)).to.be.true;

//Set to false at the service level
detection = new DetectionService({recording: false, micInputStream: null, sampleRate: args.sampleRate, frequencyScaleFactor: args.frequencyScaleFactor, notifications: false});
expect(detection._isRecordingEnabled(true)).to.be.true; //Should override
expect(detection._isRecordingEnabled(false)).to.be.false; //Should override
//When not specified in either location should default to true
expect(detection._isRecordingEnabled(null)).to.be.false; //Should fallback to service

//Set to true at the service level
detection = new DetectionService({recording: true, micInputStream: null, sampleRate: args.sampleRate, frequencyScaleFactor: args.frequencyScaleFactor, notifications: false});
expect(detection._isRecordingEnabled(true)).to.be.true; //Should override
expect(detection._isRecordingEnabled(false)).to.be.false; //Should override
//When not specified in either location should default to true
expect(detection._isRecordingEnabled(null)).to.be.true; //Should fallback to service
});

});

async function generateTest({filename, tones, sampleRate, frequencyScaleFactor=1, freqsString}) {
Expand Down

0 comments on commit 84f1044

Please sign in to comment.