Source: operator/MeanCrossingRate.js

  1. import { BaseLfo } from 'waves-lfo/core';
  2. const parameters = {
  3. noiseThreshold: {
  4. type: 'float',
  5. default: 0.1,
  6. },
  7. frameSize: {
  8. type: 'integer',
  9. default: 512,
  10. metas: { kind: 'static' },
  11. },
  12. hopSize: { // should be nullable
  13. type: 'integer',
  14. default: null,
  15. nullable: true,
  16. metas: { kind: 'static' },
  17. },
  18. // centeredTimeTags: {
  19. // type: 'boolean',
  20. // default: false,
  21. // }
  22. }
  23. /**
  24. * Mean Crossing Rate operator : estimates the frequency and periodicity of
  25. * a (n-dimension) signal, either on an input stream of signal frames, or by
  26. * using its own sliding window on an input stream of vectors.
  27. *
  28. * The mean is estimated on each new analyzed window using the following equation :
  29. * `mean = min + (max - min) * 0.5;`
  30. *
  31. * output: an array of size `2 * inputDimension`
  32. * (`[ frequency1, periodicity1, ... frequencyN, periodicityN ]`)
  33. *
  34. * @memberof operator
  35. * @deprecated
  36. *
  37. * @warning: This operator is considered as unstable and will be modified.
  38. * particularly the module will probably be modified to handle only `signal`
  39. * inputs. Leveraging the handling of vector frames to the end-user by making
  40. * use of `lfo.operator.Select` and `lfo.operator.Slicer`
  41. *
  42. *
  43. * @param {Object} [options] - Override default options.
  44. * @param {Number} [options.noiseThreshold=0.1] - Threshold added to the mean to
  45. * avoid confusion between noise and real signal.
  46. * @param {Number} [options.frameSize=512] - Size of the internal sliding window.
  47. * Will be ignored if input is signal.
  48. * @param {Number} [options.hopSize=null] - Number of samples between two
  49. * computations on the internal sliding window. Will be ignored is input
  50. * is signal.
  51. */
  52. // We don't use centered time tags for signal input, as we don't know if it's
  53. // already been done by a previous slicer.
  54. // So we don't implement it for now.
  55. // would be :
  56. // @param {Boolean} [options.centeredTimeTags=false] - Move the time tag to the
  57. // middle of the frame.
  58. class MeanCrossingRate extends BaseLfo {
  59. constructor(options = {}) {
  60. super(parameters, options);
  61. this._mcrs = [];
  62. }
  63. /** @private */
  64. onParamUpdate(name, value, metas) {
  65. if (!this.params.hopSize)
  66. this.params.set('hopSize', frameSize);
  67. if (this.streamParams.frameType === 'signal')
  68. this.params.set('frameSize', this.prevStreamParams.frameSize);
  69. }
  70. /** @private */
  71. processStreamParams(prevStreamParams = {}) {
  72. this.prepareStreamParams(prevStreamParams);
  73. // TODO : set output samplerate according to input samplerate + hopSize (?)
  74. this._mcrs = [];
  75. const noiseThreshold = this.params.get('noiseThreshold');
  76. const frameSize = (this.streamParams.frameType === 'vector')
  77. ? this.params.get('frameSize')
  78. : prevStreamParams.frameSize;
  79. const hopSize = this.params.get('hopSize'); // if input is signal we don't care anyway
  80. const sampleRate = prevStreamParams.sourceSampleRate;
  81. const paramsDescription = [ 'frequency', 'periodicity' ];
  82. let inputDimension = 1;
  83. if (this.streamParams.frameType === 'vector') {
  84. inputDimension = prevStreamParams.frameSize;
  85. } else if (this.streamParams.frameType === 'signal') {
  86. // if input frames are of type "signal", input dimension is 1
  87. inputDimension = 1;
  88. }
  89. this.streamParams.frameSize = 2 * inputDimension;
  90. this.streamParams.description = [];
  91. for (let i = 0; i < inputDimension; i++) {
  92. this.streamParams.description.concat(paramsDescription);
  93. this._mcrs.push(new MeanCrossingRateBase({
  94. noiseThreshold: noiseThreshold,
  95. frameSize: frameSize,
  96. hopSize: hopSize,
  97. sampleRate: sampleRate,
  98. }));
  99. }
  100. this.propagateStreamParams();
  101. }
  102. /** @private */
  103. processVector(frame) {
  104. const inData = frame.data;
  105. const outData = this.frame.data;
  106. for (let i = 0; i < this._mcrs.length; i++) {
  107. const r = this._mcrs[i].process(inData[i]);
  108. outData[i * 2] = r.frequency;
  109. outData[i * 2 + 1] = r.periodicity;
  110. }
  111. }
  112. /** @private */
  113. processSignal(frame) {
  114. const inData = frame.data;
  115. const outData = this.frame.data;
  116. const r = this._mcrs[0].processFrame(inData);
  117. outData[0] = r.frequency;
  118. outData[1] = r.periodicity;
  119. }
  120. }
  121. export default MeanCrossingRate;
  122. //----------------------------------------------------------------------------//
  123. //=============== Base class for mean crossing rate computation ==============//
  124. //----------------------------------------------------------------------------//
  125. const mcrBaseDefaults = {
  126. noiseThreshold: 0.1,
  127. // only used with internal circular buffer (fed sample(s) by sample(s)),
  128. // when input type is vector :
  129. frameSize: 50,
  130. hopSize: 5,
  131. sampleRate: null,
  132. };
  133. class MeanCrossingRateBase {
  134. constructor(options = {}) {
  135. Object.assign({}, options, mcrBaseDefaults);
  136. this.mean = 0;
  137. this.magnitude = 0;
  138. this.stdDev = 0;
  139. this.crossings = [];
  140. this.periodMean = 0;
  141. this.periodStdDev = 0;
  142. this.inputFrame = [];
  143. this.setConfig(options);
  144. //this.maxFreq = this.inputRate / 0.5;
  145. }
  146. setConfig(cfg) {
  147. if (cfg.noiseThreshold) {
  148. this.noiseThreshold = cfg.noiseThreshold;
  149. }
  150. if (cfg.frameSize) {
  151. this.frameSize = cfg.frameSize;
  152. }
  153. if (cfg.hopSize) {
  154. this.hopSize = cfg.hopSize;
  155. }
  156. if (cfg.sampleRate) {
  157. this.sampleRate = cfg.sampleRate;
  158. // this.maxFreq = this.sampleRate / 2;
  159. }
  160. this.inputBuffer = new Array(this.frameSize);
  161. for (let i = 0; i < this.frameSize; i++) {
  162. this.inputBuffer[i] = 0;
  163. }
  164. this.hopCounter = 0;
  165. this.bufferIndex = 0;
  166. this.results = { amplitude: 0, frequency: 0, periodicity: 0 };
  167. }
  168. process(value) {
  169. // update internal circular buffer
  170. // then call processFrame(this.inputBuffer) if needed
  171. this.inputBuffer[this.bufferIndex] = value;
  172. this.bufferIndex = (this.bufferIndex + 1) % this.frameSize;
  173. if (this.hopCounter === this.hopSize - 1) {
  174. this.hopCounter = 0;
  175. this.processFrame(this.inputBuffer, this.bufferIndex)
  176. } else {
  177. this.hopCounter++;
  178. }
  179. return this.results;
  180. }
  181. // compute magnitude, zero crossing rate, and periodicity
  182. processFrame(frame, offset = 0) {
  183. if (frame.length < 2) {
  184. return { amplitude: 0, frequency: 0, periodicity: 0 };
  185. }
  186. this.inputFrame = frame;
  187. this._mainAlgorithm();
  188. // TODO: improve this (2.0 is empirical factor because we don't know a priori sensor range)
  189. this.amplitude = this.stdDev * 2.0;
  190. /* * * * * * * * * * * * * * * */
  191. // this one is working with one direction crossings detection version
  192. this.frequency = this.crossings.length / Math.floor(this.inputFrame.length * 0.5); // normalized by "nyquist ratio"
  193. // this one is working with two direction crossings detection version
  194. // this.frequency = this.crossings.length / (this.inputFrame.length - 1); // beware of division by zero
  195. // if sampleRate is specified, translate normalized frequency to Hertz :
  196. if (this.sampleRate) {
  197. this.frequency *= Math.floor(this.sampleRate / 2);
  198. }
  199. /* * * * * * * * * * * * * * * */
  200. if (this.crossings.length > 2) {
  201. // periodicity is normalized based on input frame size.
  202. this.periodicity = 1.0 - Math.sqrt(this.periodStdDev / this.inputFrame.length);
  203. } else {
  204. this.periodicity = 0;
  205. }
  206. this.results.amplitude = this.amplitude;
  207. this.results.frequency = this.frequency;
  208. this.results.periodicity = this.periodicity;
  209. return this.results;
  210. }
  211. _mainAlgorithm() {
  212. // compute min, max, mean and magnitude
  213. // this.mean = 0;
  214. // this.magnitude = 0;
  215. let min, max;
  216. min = max = this.inputFrame[0];
  217. for (let i = 0; i < this.inputFrame.length; i++) {
  218. let val = this.inputFrame[i];
  219. // this.mean += val;
  220. // this.magnitude += val * val;
  221. if (val > max)
  222. max = val;
  223. else if (val < min)
  224. min = val;
  225. }
  226. // TODO : more tests to determine which mean (true mean or (max-min)/2) is the best
  227. //this.mean /= this.inputFrame.length;
  228. this.mean = min + (max - min) * 0.5;
  229. // this.magnitude /= this.inputFrame.length;
  230. // this.magnitude = Math.sqrt(this.magnitude);
  231. // compute signal stdDev and number of mean-crossings
  232. // using ascending AND / OR descending mean crossing (see comments)
  233. this.crossings = [];
  234. this.upCrossings = [];
  235. this.downCrossings = [];
  236. this.stdDev = 0;
  237. let prevDelta = this.inputFrame[0] - this.mean;
  238. //for (let i in this.inputFrame) {
  239. for (let i = 1; i < this.inputFrame.length; i++) {
  240. let delta = this.inputFrame[i] - this.mean;
  241. this.stdDev += delta * delta;
  242. if (prevDelta > this.noiseThreshold && delta < this.noiseThreshold) { // falling
  243. // this.crossings.push(i);
  244. this.downCrossings.push(i);
  245. } else if (prevDelta < this.noiseThreshold && delta > this.noiseThreshold) { // rising
  246. // this.crossings.push(i);
  247. this.upCrossings.push(i);
  248. }
  249. this.crossings = (this.upCrossings.length > this.downCrossings.length)
  250. ? this.upCrossings
  251. : this.downCrossings;
  252. prevDelta = delta;
  253. }
  254. this.stdDev = Math.sqrt(this.stdDev);
  255. // compute mean of delta-T between crossings
  256. this.periodMean = 0;
  257. for (let i = 1; i < this.crossings.length; i++) {
  258. this.periodMean += this.crossings[i] - this.crossings[i - 1];
  259. }
  260. // if we have a NaN here we don't care as we won't use this.periodMean below
  261. this.periodMean /= (this.crossings.length - 1);
  262. // compute stdDev of delta-T between crossings
  263. this.periodStdDev = 0;
  264. for (let i = 1; i < this.crossings.length; i++) {
  265. let deltaP = (this.crossings[i] - this.crossings[i - 1] - this.periodMean)
  266. this.periodStdDev += deltaP * deltaP;
  267. }
  268. if (this.crossings.length > 2) {
  269. this.periodStdDev = Math.sqrt(this.periodStdDev / (this.crossings.length - 2));
  270. }
  271. }
  272. };