import { BaseLfo } from 'waves-lfo/core';
const parameters = {
noiseThreshold: {
type: 'float',
default: 0.1,
},
frameSize: {
type: 'integer',
default: 512,
metas: { kind: 'static' },
},
hopSize: { // should be nullable
type: 'integer',
default: null,
nullable: true,
metas: { kind: 'static' },
},
// centeredTimeTags: {
// type: 'boolean',
// default: false,
// }
}
/**
* Mean Crossing Rate operator : estimates the frequency and periodicity of
* a (n-dimension) signal, either on an input stream of signal frames, or by
* using its own sliding window on an input stream of vectors.
*
* The mean is estimated on each new analyzed window using the following equation :
* `mean = min + (max - min) * 0.5;`
*
* output: an array of size `2 * inputDimension`
* (`[ frequency1, periodicity1, ... frequencyN, periodicityN ]`)
*
* @memberof operator
* @deprecated
*
* @warning: This operator is considered as unstable and will be modified.
* particularly the module will probably be modified to handle only `signal`
* inputs. Leveraging the handling of vector frames to the end-user by making
* use of `lfo.operator.Select` and `lfo.operator.Slicer`
*
*
* @param {Object} [options] - Override default options.
* @param {Number} [options.noiseThreshold=0.1] - Threshold added to the mean to
* avoid confusion between noise and real signal.
* @param {Number} [options.frameSize=512] - Size of the internal sliding window.
* Will be ignored if input is signal.
* @param {Number} [options.hopSize=null] - Number of samples between two
* computations on the internal sliding window. Will be ignored is input
* is signal.
*/
// We don't use centered time tags for signal input, as we don't know if it's
// already been done by a previous slicer.
// So we don't implement it for now.
// would be :
// @param {Boolean} [options.centeredTimeTags=false] - Move the time tag to the
// middle of the frame.
class MeanCrossingRate extends BaseLfo {
constructor(options = {}) {
super(parameters, options);
this._mcrs = [];
}
/** @private */
onParamUpdate(name, value, metas) {
if (!this.params.hopSize)
this.params.set('hopSize', frameSize);
if (this.streamParams.frameType === 'signal')
this.params.set('frameSize', this.prevStreamParams.frameSize);
}
/** @private */
processStreamParams(prevStreamParams = {}) {
this.prepareStreamParams(prevStreamParams);
// TODO : set output samplerate according to input samplerate + hopSize (?)
this._mcrs = [];
const noiseThreshold = this.params.get('noiseThreshold');
const frameSize = (this.streamParams.frameType === 'vector')
? this.params.get('frameSize')
: prevStreamParams.frameSize;
const hopSize = this.params.get('hopSize'); // if input is signal we don't care anyway
const sampleRate = prevStreamParams.sourceSampleRate;
const paramsDescription = [ 'frequency', 'periodicity' ];
let inputDimension = 1;
if (this.streamParams.frameType === 'vector') {
inputDimension = prevStreamParams.frameSize;
} else if (this.streamParams.frameType === 'signal') {
// if input frames are of type "signal", input dimension is 1
inputDimension = 1;
}
this.streamParams.frameSize = 2 * inputDimension;
this.streamParams.description = [];
for (let i = 0; i < inputDimension; i++) {
this.streamParams.description.concat(paramsDescription);
this._mcrs.push(new MeanCrossingRateBase({
noiseThreshold: noiseThreshold,
frameSize: frameSize,
hopSize: hopSize,
sampleRate: sampleRate,
}));
}
this.propagateStreamParams();
}
/** @private */
processVector(frame) {
const inData = frame.data;
const outData = this.frame.data;
for (let i = 0; i < this._mcrs.length; i++) {
const r = this._mcrs[i].process(inData[i]);
outData[i * 2] = r.frequency;
outData[i * 2 + 1] = r.periodicity;
}
}
/** @private */
processSignal(frame) {
const inData = frame.data;
const outData = this.frame.data;
const r = this._mcrs[0].processFrame(inData);
outData[0] = r.frequency;
outData[1] = r.periodicity;
}
}
export default MeanCrossingRate;
//----------------------------------------------------------------------------//
//=============== Base class for mean crossing rate computation ==============//
//----------------------------------------------------------------------------//
const mcrBaseDefaults = {
noiseThreshold: 0.1,
// only used with internal circular buffer (fed sample(s) by sample(s)),
// when input type is vector :
frameSize: 50,
hopSize: 5,
sampleRate: null,
};
class MeanCrossingRateBase {
constructor(options = {}) {
Object.assign({}, options, mcrBaseDefaults);
this.mean = 0;
this.magnitude = 0;
this.stdDev = 0;
this.crossings = [];
this.periodMean = 0;
this.periodStdDev = 0;
this.inputFrame = [];
this.setConfig(options);
//this.maxFreq = this.inputRate / 0.5;
}
setConfig(cfg) {
if (cfg.noiseThreshold) {
this.noiseThreshold = cfg.noiseThreshold;
}
if (cfg.frameSize) {
this.frameSize = cfg.frameSize;
}
if (cfg.hopSize) {
this.hopSize = cfg.hopSize;
}
if (cfg.sampleRate) {
this.sampleRate = cfg.sampleRate;
// this.maxFreq = this.sampleRate / 2;
}
this.inputBuffer = new Array(this.frameSize);
for (let i = 0; i < this.frameSize; i++) {
this.inputBuffer[i] = 0;
}
this.hopCounter = 0;
this.bufferIndex = 0;
this.results = { amplitude: 0, frequency: 0, periodicity: 0 };
}
process(value) {
// update internal circular buffer
// then call processFrame(this.inputBuffer) if needed
this.inputBuffer[this.bufferIndex] = value;
this.bufferIndex = (this.bufferIndex + 1) % this.frameSize;
if (this.hopCounter === this.hopSize - 1) {
this.hopCounter = 0;
this.processFrame(this.inputBuffer, this.bufferIndex)
} else {
this.hopCounter++;
}
return this.results;
}
// compute magnitude, zero crossing rate, and periodicity
processFrame(frame, offset = 0) {
if (frame.length < 2) {
return { amplitude: 0, frequency: 0, periodicity: 0 };
}
this.inputFrame = frame;
this._mainAlgorithm();
// TODO: improve this (2.0 is empirical factor because we don't know a priori sensor range)
this.amplitude = this.stdDev * 2.0;
/* * * * * * * * * * * * * * * */
// this one is working with one direction crossings detection version
this.frequency = this.crossings.length / Math.floor(this.inputFrame.length * 0.5); // normalized by "nyquist ratio"
// this one is working with two direction crossings detection version
// this.frequency = this.crossings.length / (this.inputFrame.length - 1); // beware of division by zero
// if sampleRate is specified, translate normalized frequency to Hertz :
if (this.sampleRate) {
this.frequency *= Math.floor(this.sampleRate / 2);
}
/* * * * * * * * * * * * * * * */
if (this.crossings.length > 2) {
// periodicity is normalized based on input frame size.
this.periodicity = 1.0 - Math.sqrt(this.periodStdDev / this.inputFrame.length);
} else {
this.periodicity = 0;
}
this.results.amplitude = this.amplitude;
this.results.frequency = this.frequency;
this.results.periodicity = this.periodicity;
return this.results;
}
_mainAlgorithm() {
// compute min, max, mean and magnitude
// this.mean = 0;
// this.magnitude = 0;
let min, max;
min = max = this.inputFrame[0];
for (let i = 0; i < this.inputFrame.length; i++) {
let val = this.inputFrame[i];
// this.mean += val;
// this.magnitude += val * val;
if (val > max)
max = val;
else if (val < min)
min = val;
}
// TODO : more tests to determine which mean (true mean or (max-min)/2) is the best
//this.mean /= this.inputFrame.length;
this.mean = min + (max - min) * 0.5;
// this.magnitude /= this.inputFrame.length;
// this.magnitude = Math.sqrt(this.magnitude);
// compute signal stdDev and number of mean-crossings
// using ascending AND / OR descending mean crossing (see comments)
this.crossings = [];
this.upCrossings = [];
this.downCrossings = [];
this.stdDev = 0;
let prevDelta = this.inputFrame[0] - this.mean;
//for (let i in this.inputFrame) {
for (let i = 1; i < this.inputFrame.length; i++) {
let delta = this.inputFrame[i] - this.mean;
this.stdDev += delta * delta;
if (prevDelta > this.noiseThreshold && delta < this.noiseThreshold) { // falling
// this.crossings.push(i);
this.downCrossings.push(i);
} else if (prevDelta < this.noiseThreshold && delta > this.noiseThreshold) { // rising
// this.crossings.push(i);
this.upCrossings.push(i);
}
this.crossings = (this.upCrossings.length > this.downCrossings.length)
? this.upCrossings
: this.downCrossings;
prevDelta = delta;
}
this.stdDev = Math.sqrt(this.stdDev);
// compute mean of delta-T between crossings
this.periodMean = 0;
for (let i = 1; i < this.crossings.length; i++) {
this.periodMean += this.crossings[i] - this.crossings[i - 1];
}
// if we have a NaN here we don't care as we won't use this.periodMean below
this.periodMean /= (this.crossings.length - 1);
// compute stdDev of delta-T between crossings
this.periodStdDev = 0;
for (let i = 1; i < this.crossings.length; i++) {
let deltaP = (this.crossings[i] - this.crossings[i - 1] - this.periodMean)
this.periodStdDev += deltaP * deltaP;
}
if (this.crossings.length > 2) {
this.periodStdDev = Math.sqrt(this.periodStdDev / (this.crossings.length - 2));
}
}
};