Source: common/source/EventIn.js

  1. import BaseLfo from '../../core/BaseLfo';
  2. import SourceMixin from '../../core/SourceMixin';
  3. // http://stackoverflow.com/questions/17575790/environment-detection-node-js-or-browser
  4. const isNode = new Function('try { return this === global; } catch(e) { return false }');
  5. /**
  6. * Create a function that returns time in seconds according to the current
  7. * environnement (node or browser).
  8. * If running in node the time rely on `process.hrtime`, while if in the browser
  9. * it is provided by the `currentTime` of an `AudioContext`, this context can
  10. * optionnaly be provided to keep time consistency between several `EventIn`
  11. * nodes.
  12. *
  13. * @param {AudioContext} [audioContext=null] - Optionnal audio context.
  14. * @return {Function}
  15. * @private
  16. */
  17. function getTimeFunction(audioContext = null) {
  18. if (isNode()) {
  19. return () => {
  20. const t = process.hrtime();
  21. return t[0] + t[1] * 1e-9;
  22. }
  23. } else {
  24. return () => performance.now() / 1000;
  25. }
  26. }
  27. const definitions = {
  28. absoluteTime: {
  29. type: 'boolean',
  30. default: false,
  31. constant: true,
  32. },
  33. audioContext: {
  34. type: 'any',
  35. default: null,
  36. constant: true,
  37. nullable: true,
  38. },
  39. frameType: {
  40. type: 'enum',
  41. list: ['signal', 'vector', 'scalar'],
  42. default: 'signal',
  43. constant: true,
  44. },
  45. frameSize: {
  46. type: 'integer',
  47. default: 1,
  48. min: 1,
  49. max: +Infinity, // not recommended...
  50. metas: { kind: 'static' },
  51. },
  52. sampleRate: {
  53. type: 'float',
  54. default: null,
  55. min: 0,
  56. max: +Infinity, // same here
  57. nullable: true,
  58. metas: { kind: 'static' },
  59. },
  60. frameRate: {
  61. type: 'float',
  62. default: null,
  63. min: 0,
  64. max: +Infinity, // same here
  65. nullable: true,
  66. metas: { kind: 'static' },
  67. },
  68. description: {
  69. type: 'any',
  70. default: null,
  71. constant: true,
  72. }
  73. };
  74. /**
  75. * The `EventIn` operator allows to manually create a stream of data or to feed
  76. * a stream from another source (e.g. sensors) into a processing graph.
  77. *
  78. * @param {Object} options - Override parameters' default values.
  79. * @param {String} [options.frameType='signal'] - Type of the input - allowed
  80. * values: `signal`, `vector` or `scalar`.
  81. * @param {Number} [options.frameSize=1] - Size of the output frame.
  82. * @param {Number} [options.sampleRate=null] - Sample rate of the source stream,
  83. * if of type `signal`.
  84. * @param {Number} [options.frameRate=null] - Rate of the source stream, if of
  85. * type `vector`.
  86. * @param {Array|String} [options.description] - Optionnal description
  87. * describing the dimensions of the output frame
  88. * @param {Boolean} [options.absoluteTime=false] - Define if time should be used
  89. * as forwarded as given in the process method, or relatively to the time of
  90. * the first `process` call after start.
  91. *
  92. * @memberof module:common.source
  93. *
  94. * @todo - Add a `logicalTime` parameter to tag frame according to frame rate.
  95. *
  96. * @example
  97. * import * as lfo from 'waves-lfo/client';
  98. *
  99. * const eventIn = new lfo.source.EventIn({
  100. * frameType: 'vector',
  101. * frameSize: 3,
  102. * frameRate: 1 / 50,
  103. * description: ['alpha', 'beta', 'gamma'],
  104. * });
  105. *
  106. * // connect source to operators and sink(s)
  107. *
  108. * // initialize and start the graph
  109. * eventIn.start();
  110. *
  111. * // feed `deviceorientation` data into the graph
  112. * window.addEventListener('deviceorientation', (e) => {
  113. * const frame = {
  114. * time: window.performace.now() / 1000,
  115. * data: [e.alpha, e.beta, e.gamma],
  116. * };
  117. *
  118. * eventIn.processFrame(frame);
  119. * }, false);
  120. */
  121. class EventIn extends SourceMixin(BaseLfo) {
  122. constructor(options = {}) {
  123. super(definitions, options);
  124. const audioContext = this.params.get('audioContext');
  125. this._getTime = getTimeFunction(audioContext);
  126. this._startTime = null;
  127. this._systemTime = null;
  128. this._absoluteTime = this.params.get('absoluteTime');
  129. }
  130. /**
  131. * Propagate the `streamParams` in the graph and allow to push frames into
  132. * the graph. Any call to `process` or `processFrame` before `start` will be
  133. * ignored.
  134. *
  135. * @see {@link module:core.BaseLfo#processStreamParams}
  136. * @see {@link module:core.BaseLfo#resetStream}
  137. * @see {@link module:common.source.EventIn#stop}
  138. */
  139. start(startTime = null) {
  140. if (this.initialized === false) {
  141. if (this.initPromise === null) // init has not yet been called
  142. this.initPromise = this.init();
  143. return this.initPromise.then(() => this.start(startTime));
  144. }
  145. this._startTime = startTime;
  146. this._systemTime = null; // value set in the first `process` call
  147. this.started = true;
  148. }
  149. /**
  150. * Finalize the stream and stop the whole graph. Any call to `process` or
  151. * `processFrame` after `stop` will be ignored.
  152. *
  153. * @see {@link module:core.BaseLfo#finalizeStream}
  154. * @see {@link module:common.source.EventIn#start}
  155. */
  156. stop() {
  157. if (this.started && this._startTime !== null) {
  158. const currentTime = this._getTime();
  159. const endTime = this.frame.time + (currentTime - this._systemTime);
  160. this.finalizeStream(endTime);
  161. this.started = false;
  162. }
  163. }
  164. /** @private */
  165. processStreamParams() {
  166. const frameSize = this.params.get('frameSize');
  167. const frameType = this.params.get('frameType');
  168. const sampleRate = this.params.get('sampleRate');
  169. const frameRate = this.params.get('frameRate');
  170. const description = this.params.get('description');
  171. // init operator's stream params
  172. this.streamParams.frameSize = frameType === 'scalar' ? 1 : frameSize;
  173. this.streamParams.frameType = frameType;
  174. this.streamParams.description = description;
  175. if (frameType === 'signal') {
  176. if (sampleRate === null)
  177. throw new Error('Undefined "sampleRate" for "signal" stream');
  178. this.streamParams.sourceSampleRate = sampleRate;
  179. this.streamParams.frameRate = sampleRate / frameSize;
  180. this.streamParams.sourceSampleCount = frameSize;
  181. } else if (frameType === 'vector' || frameType === 'scalar') {
  182. if (frameRate === null)
  183. throw new Error(`Undefined "frameRate" for "${frameType}" stream`);
  184. this.streamParams.frameRate = frameRate;
  185. this.streamParams.sourceSampleRate = frameRate;
  186. this.streamParams.sourceSampleCount = 1;
  187. }
  188. this.propagateStreamParams();
  189. }
  190. /** @private */
  191. processFunction(frame) {
  192. const currentTime = this._getTime();
  193. const inData = frame.data.length ? frame.data : [frame.data];
  194. const outData = this.frame.data;
  195. // if no time provided, use system time
  196. let time = Number.isFinite(frame.time) ? frame.time : currentTime;
  197. if (this._startTime === null)
  198. this._startTime = time;
  199. if (this._absoluteTime === false)
  200. time = time - this._startTime;
  201. for (let i = 0, l = this.streamParams.frameSize; i < l; i++)
  202. outData[i] = inData[i];
  203. this.frame.time = time;
  204. this.frame.metadata = frame.metadata;
  205. // store current time to compute `endTime` on stop
  206. this._systemTime = currentTime;
  207. }
  208. /**
  209. * Alternative interface to propagate a frame in the graph. Pack `time`,
  210. * `data` and `metadata` in a frame object.
  211. *
  212. * @param {Number} time - Frame time.
  213. * @param {Float32Array|Array} data - Frame data.
  214. * @param {Object} metadata - Optionnal frame metadata.
  215. *
  216. * @example
  217. * eventIn.process(1, [0, 1, 2]);
  218. * // is equivalent to
  219. * eventIn.processFrame({ time: 1, data: [0, 1, 2] });
  220. */
  221. process(time, data, metadata = null) {
  222. this.processFrame({ time, data, metadata });
  223. }
  224. /**
  225. * Propagate a frame object in the graph.
  226. *
  227. * @param {Object} frame - Input frame.
  228. * @param {Number} frame.time - Frame time.
  229. * @param {Float32Array|Array} frame.data - Frame data.
  230. * @param {Object} [frame.metadata=undefined] - Optionnal frame metadata.
  231. *
  232. * @example
  233. * eventIn.processFrame({ time: 1, data: [0, 1, 2] });
  234. */
  235. processFrame(frame) {
  236. if (!this.started) return;
  237. this.prepareFrame();
  238. this.processFunction(frame);
  239. this.propagateFrame();
  240. }
  241. }
  242. export default EventIn;