Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

151 line
4.2KB

  1. // ring-player-processor.js — AudioWorklet processor for LiveListenWS
  2. // Runs on the audio rendering thread, immune to main-thread blocking.
  3. class RingPlayerProcessor extends AudioWorkletProcessor {
  4. constructor(options) {
  5. super();
  6. const ch = options.processorOptions?.channels || 1;
  7. const ringSeconds = options.processorOptions?.ringSeconds || 1.0;
  8. const startThresholdSeconds = options.processorOptions?.startThresholdSeconds || 0.2;
  9. this._channels = ch;
  10. this._underruns = 0;
  11. this._overruns = 0;
  12. this._lastStatsFrame = 0;
  13. // Ring buffer duration at sampleRate.
  14. this._ringSize = Math.ceil(sampleRate * ch * ringSeconds);
  15. this._ring = new Float32Array(this._ringSize);
  16. this._writePos = 0;
  17. this._readPos = 0;
  18. this._started = false;
  19. this._fadeGain = 1.0;
  20. this._startThreshold = Math.ceil(sampleRate * ch * startThresholdSeconds);
  21. this.port.onmessage = (e) => {
  22. if (e.data.type === 'pcm') {
  23. this._pushSamples(new Float32Array(e.data.samples));
  24. }
  25. };
  26. }
  27. _postStats(force = false) {
  28. const frame = currentFrame;
  29. if (!force && frame - this._lastStatsFrame < sampleRate) return;
  30. this._lastStatsFrame = frame;
  31. this.port.postMessage({
  32. type: 'stats',
  33. underruns: this._underruns,
  34. overruns: this._overruns,
  35. availableFrames: Math.floor(this._available() / Math.max(1, this._channels))
  36. });
  37. }
  38. _available() {
  39. return (this._writePos - this._readPos + this._ringSize) % this._ringSize;
  40. }
  41. _pushSamples(float32arr) {
  42. const ring = this._ring;
  43. const size = this._ringSize;
  44. const n = float32arr.length;
  45. // Overrun: advance read cursor to make room
  46. const used = this._available();
  47. const free = size - used - 1;
  48. if (n > free) {
  49. this._overruns++;
  50. this._readPos = (this._readPos + (n - free)) % size;
  51. }
  52. let w = this._writePos;
  53. // Fast path: contiguous write
  54. if (w + n <= size) {
  55. ring.set(float32arr, w);
  56. w += n;
  57. if (w >= size) w = 0;
  58. } else {
  59. // Wrap around
  60. const first = size - w;
  61. ring.set(float32arr.subarray(0, first), w);
  62. ring.set(float32arr.subarray(first), 0);
  63. w = n - first;
  64. }
  65. this._writePos = w;
  66. if (!this._started && this._available() >= this._startThreshold) {
  67. this._started = true;
  68. }
  69. }
  70. process(inputs, outputs, parameters) {
  71. const output = outputs[0];
  72. const outLen = output[0]?.length || 128;
  73. const ch = this._channels;
  74. const ring = this._ring;
  75. const size = this._ringSize;
  76. if (!this._started) {
  77. for (let c = 0; c < output.length; c++) output[c].fill(0);
  78. this._postStats();
  79. return true;
  80. }
  81. const need = outLen * ch;
  82. const avail = this._available();
  83. if (avail < need) {
  84. this._underruns++;
  85. // Underrun: play what we have with fade-out, fill rest with silence
  86. const have = avail;
  87. const haveFrames = Math.floor(have / ch);
  88. const fadeLen = Math.min(64, haveFrames);
  89. const fadeStart = haveFrames - fadeLen;
  90. let r = this._readPos;
  91. for (let i = 0; i < haveFrames; i++) {
  92. let env = this._fadeGain;
  93. if (i >= fadeStart) {
  94. env *= 1.0 - (i - fadeStart) / fadeLen;
  95. }
  96. for (let c = 0; c < ch; c++) {
  97. if (c < output.length) {
  98. output[c][i] = ring[r] * env;
  99. }
  100. r = (r + 1) % size;
  101. }
  102. }
  103. this._readPos = r;
  104. // Silence the rest
  105. for (let i = haveFrames; i < outLen; i++) {
  106. for (let c = 0; c < output.length; c++) output[c][i] = 0;
  107. }
  108. this._fadeGain = 0;
  109. this._postStats(true);
  110. return true;
  111. }
  112. // Normal path
  113. let r = this._readPos;
  114. const fadeInLen = (this._fadeGain < 1.0) ? Math.min(64, outLen) : 0;
  115. for (let i = 0; i < outLen; i++) {
  116. let env = 1.0;
  117. if (i < fadeInLen) {
  118. env = this._fadeGain + (1.0 - this._fadeGain) * (i / fadeInLen);
  119. }
  120. for (let c = 0; c < ch; c++) {
  121. if (c < output.length) {
  122. output[c][i] = ring[r] * env;
  123. }
  124. r = (r + 1) % size;
  125. }
  126. }
  127. this._readPos = r;
  128. this._fadeGain = 1.0;
  129. this._postStats();
  130. return true;
  131. }
  132. }
  133. registerProcessor('ring-player-processor', RingPlayerProcessor);