@editframe/elements 0.15.0-beta.14 → 0.15.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,6 +68,7 @@ export declare class EFMedia extends EFMedia_base {
68
68
  set fftDecay(value: number);
69
69
  get fftSize(): number;
70
70
  get fftDecay(): number;
71
+ get shouldInterpolateFrequencies(): boolean;
71
72
  private static readonly DECAY_WEIGHT;
72
73
  get FREQ_WEIGHTS(): Float32Array;
73
74
  byteTimeDomainTask: Task<readonly [import('@lit/task').TaskStatus, number, number, number], Uint8Array | null>;
@@ -288,7 +288,7 @@ const _EFMedia = class _EFMedia2 extends EFTargetable(
288
288
  analyser.minDecibels = -90;
289
289
  analyser.maxDecibels = -20;
290
290
  const gainNode = audioContext.createGain();
291
- gainNode.gain.value = 10;
291
+ gainNode.gain.value = 2;
292
292
  source.connect(gainNode);
293
293
  gainNode.connect(analyser);
294
294
  analyser.connect(audioContext.destination);
@@ -384,16 +384,16 @@ const _EFMedia = class _EFMedia2 extends EFTargetable(
384
384
  analyser.minDecibels = -90;
385
385
  analyser.maxDecibels = -10;
386
386
  const gainNode = audioContext.createGain();
387
- gainNode.gain.value = 5;
387
+ gainNode.gain.value = 3;
388
388
  const filter = audioContext.createBiquadFilter();
389
389
  filter.type = "bandpass";
390
390
  filter.frequency.value = 15e3;
391
391
  filter.Q.value = 0.05;
392
392
  const audioBufferSource = audioContext.createBufferSource();
393
393
  audioBufferSource.buffer = audioBuffer;
394
- audioBufferSource.connect(gainNode);
395
- gainNode.connect(filter);
396
- filter.connect(analyser);
394
+ audioBufferSource.connect(filter);
395
+ filter.connect(gainNode);
396
+ gainNode.connect(analyser);
397
397
  analyser.connect(audioContext.destination);
398
398
  audioBufferSource.start(0, startTime, 1 / 30);
399
399
  try {
@@ -428,8 +428,9 @@ const _EFMedia = class _EFMedia2 extends EFTargetable(
428
428
  0,
429
429
  Math.floor(smoothedData.length / 2)
430
430
  );
431
- this.#frequencyDataCache.set(smoothedKey, slicedData);
432
- return slicedData;
431
+ const processedData = this.shouldInterpolateFrequencies ? processFFTData(slicedData) : slicedData;
432
+ this.#frequencyDataCache.set(smoothedKey, processedData);
433
+ return processedData;
433
434
  }
434
435
  });
435
436
  }
@@ -645,10 +646,14 @@ const _EFMedia = class _EFMedia2 extends EFTargetable(
645
646
  };
646
647
  }
647
648
  set fftSize(value) {
649
+ const oldValue = this.fftSize;
648
650
  this.setAttribute("fft-size", String(value));
651
+ this.requestUpdate("fft-size", oldValue);
649
652
  }
650
653
  set fftDecay(value) {
654
+ const oldValue = this.fftDecay;
651
655
  this.setAttribute("fft-decay", String(value));
656
+ this.requestUpdate("fft-decay", oldValue);
652
657
  }
653
658
  get fftSize() {
654
659
  return Number.parseInt(this.getAttribute("fft-size") ?? "128", 10);
@@ -656,6 +661,12 @@ const _EFMedia = class _EFMedia2 extends EFTargetable(
656
661
  get fftDecay() {
657
662
  return Number.parseInt(this.getAttribute("fft-decay") ?? "8", 10);
658
663
  }
664
+ get shouldInterpolateFrequencies() {
665
+ if (this.hasAttribute("interpolate-frequencies")) {
666
+ return this.getAttribute("interpolate-frequencies") !== "false";
667
+ }
668
+ return false;
669
+ }
659
670
  static {
660
671
  this.DECAY_WEIGHT = 0.7;
661
672
  }
@@ -690,6 +701,46 @@ __decorateClass([
690
701
  state()
691
702
  ], _EFMedia.prototype, "desiredSeekTimeMs", 2);
692
703
  let EFMedia = _EFMedia;
704
+ function processFFTData(fftData, zeroThresholdPercent = 0.1) {
705
+ const totalBins = fftData.length;
706
+ const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
707
+ let zeroCount = 0;
708
+ let cutoffIndex = totalBins;
709
+ for (let i = totalBins - 1; i >= 0; i--) {
710
+ if (fftData[i] < 10) {
711
+ zeroCount++;
712
+ } else {
713
+ if (zeroCount >= zeroThresholdCount) {
714
+ cutoffIndex = i + 1;
715
+ break;
716
+ }
717
+ }
718
+ }
719
+ if (cutoffIndex < zeroThresholdCount) {
720
+ return fftData;
721
+ }
722
+ const goodData = fftData.slice(0, cutoffIndex);
723
+ const resampledData = interpolateData(goodData, fftData.length);
724
+ return resampledData;
725
+ }
726
+ function interpolateData(data, targetSize) {
727
+ const resampled = new Uint8Array(targetSize);
728
+ const dataLength = data.length;
729
+ for (let i = 0; i < targetSize; i++) {
730
+ const ratio = i / (targetSize - 1) * (dataLength - 1);
731
+ const index = Math.floor(ratio);
732
+ const fraction = ratio - index;
733
+ if (index >= dataLength - 1) {
734
+ resampled[i] = data[dataLength - 1];
735
+ } else {
736
+ resampled[i] = Math.round(
737
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
738
+ data[index] * (1 - fraction) + data[index + 1] * fraction
739
+ );
740
+ }
741
+ }
742
+ return resampled;
743
+ }
693
744
  export {
694
745
  EFMedia,
695
746
  deepGetMediaElements
@@ -13,7 +13,7 @@ export declare class EFWaveform extends EFWaveform_base {
13
13
  private resizeObserver?;
14
14
  private mutationObserver?;
15
15
  render(): import('lit-html').TemplateResult<1>;
16
- mode: "roundBars" | "bars" | "bricks" | "line" | "curve" | "pixel" | "wave";
16
+ mode: "roundBars" | "bars" | "bricks" | "line" | "curve" | "pixel" | "wave" | "spikes";
17
17
  color: string;
18
18
  target: string;
19
19
  targetElement: EFAudio | EFVideo | null;
@@ -30,6 +30,7 @@ export declare class EFWaveform extends EFWaveform_base {
30
30
  protected drawCurve(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array): void;
31
31
  protected drawPixel(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array): void;
32
32
  protected drawWave(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array): void;
33
+ protected drawSpikes(ctx: CanvasRenderingContext2D, frequencyData: Uint8Array): void;
33
34
  frameTask: Task<readonly [EFAudio | EFVideo | null, Uint8Array | null | undefined], void>;
34
35
  get durationMs(): number;
35
36
  protected updated(changedProperties: PropertyValueMap<this>): void;
@@ -60,10 +60,10 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
60
60
  }
61
61
  switch (this.mode) {
62
62
  case "bars":
63
- this.drawBars(ctx, byteTimeData);
63
+ this.drawBars(ctx, frequencyData);
64
64
  break;
65
65
  case "bricks":
66
- this.drawBricks(ctx, byteTimeData);
66
+ this.drawBricks(ctx, frequencyData);
67
67
  break;
68
68
  case "line":
69
69
  this.drawLine(ctx, byteTimeData);
@@ -72,13 +72,16 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
72
72
  this.drawCurve(ctx, byteTimeData);
73
73
  break;
74
74
  case "pixel":
75
- this.drawPixel(ctx, byteTimeData);
75
+ this.drawPixel(ctx, frequencyData);
76
76
  break;
77
77
  case "wave":
78
- this.drawWave(ctx, byteTimeData);
78
+ this.drawWave(ctx, frequencyData);
79
+ break;
80
+ case "spikes":
81
+ this.drawSpikes(ctx, frequencyData);
79
82
  break;
80
83
  case "roundBars":
81
- this.drawRoundBars(ctx, byteTimeData);
84
+ this.drawRoundBars(ctx, frequencyData);
82
85
  break;
83
86
  }
84
87
  ctx.restore();
@@ -156,7 +159,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
156
159
  ctx.clearRect(0, 0, waveWidth, waveHeight);
157
160
  const path = new Path2D();
158
161
  frequencyData.forEach((value, i) => {
159
- const normalizedValue = Math.abs(value - 128) / 128;
162
+ const normalizedValue = value / 255;
160
163
  const barHeight = normalizedValue * waveHeight;
161
164
  const y = (waveHeight - barHeight) / 2;
162
165
  const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
@@ -175,7 +178,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
175
178
  const verticalGap = boxSize * 0.2;
176
179
  const maxBricks = Math.floor(waveHeight / (boxSize + verticalGap));
177
180
  frequencyData.forEach((value, i) => {
178
- const normalizedValue = Math.abs(value - 128) / 128;
181
+ const normalizedValue = value / 255;
179
182
  const brickCount = Math.floor(normalizedValue * maxBricks);
180
183
  for (let j = 0; j < brickCount; j++) {
181
184
  const x = columnWidth * i;
@@ -197,7 +200,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
197
200
  ctx.clearRect(0, 0, waveWidth, waveHeight);
198
201
  const path = new Path2D();
199
202
  frequencyData.forEach((value, i) => {
200
- const normalizedValue = Math.abs(value - 128) / 128;
203
+ const normalizedValue = value / 255;
201
204
  const height = normalizedValue * waveHeight;
202
205
  const x = waveWidth * paddingOuter + i * (barWidth * (1 + paddingInner));
203
206
  const y = (waveHeight - height) / 2;
@@ -258,7 +261,7 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
258
261
  ctx.clearRect(0, 0, waveWidth, waveHeight);
259
262
  const path = new Path2D();
260
263
  frequencyData.forEach((value, i) => {
261
- const normalizedValue = Math.abs(value - 128) / 128;
264
+ const normalizedValue = value / 255;
262
265
  const x = i * (waveWidth / frequencyData.length);
263
266
  const barHeight = normalizedValue * (waveHeight / 2);
264
267
  const y = baseline - barHeight;
@@ -275,40 +278,99 @@ let EFWaveform = class extends EFTemporal(TWMixin(LitElement)) {
275
278
  const startX = waveWidth * paddingOuter;
276
279
  ctx.clearRect(0, 0, waveWidth, waveHeight);
277
280
  const path = new Path2D();
278
- const firstValue = ((frequencyData[0] ?? 128) - 128) / 128;
279
- const firstY = waveHeight / 2 + firstValue * waveHeight / 2;
281
+ const firstValue = Math.min((frequencyData[0] ?? 0) / 255 * 2, 1);
282
+ const firstY = (waveHeight - firstValue * waveHeight) / 2;
280
283
  path.moveTo(startX, firstY);
281
284
  frequencyData.forEach((value, i) => {
282
- const normalizedValue = (value - 128) / 128;
285
+ const normalizedValue = Math.min(value / 255 * 2, 1);
283
286
  const x = startX + i / (frequencyData.length - 1) * availableWidth;
284
- const y = waveHeight / 2 - normalizedValue * waveHeight / 2;
287
+ const barHeight = normalizedValue * waveHeight;
288
+ const y = (waveHeight - barHeight) / 2;
285
289
  if (i === 0) {
286
290
  path.moveTo(x, y);
287
291
  } else {
288
292
  const prevX = startX + (i - 1) / (frequencyData.length - 1) * availableWidth;
289
- const prevValue = ((frequencyData[i - 1] ?? 128) - 128) / 128;
290
- const prevY = waveHeight / 2 - prevValue * waveHeight / 2;
293
+ const prevValue = Math.min((frequencyData[i - 1] ?? 0) / 255 * 2, 1);
294
+ const prevBarHeight = prevValue * waveHeight;
295
+ const prevY = (waveHeight - prevBarHeight) / 2;
291
296
  const xc = (prevX + x) / 2;
292
297
  const yc = (prevY + y) / 2;
293
298
  path.quadraticCurveTo(prevX, prevY, xc, yc);
294
299
  }
295
300
  });
296
301
  for (let i = frequencyData.length - 1; i >= 0; i--) {
297
- const normalizedValue = ((frequencyData[i] ?? 128) - 128) / 128;
302
+ const normalizedValue = Math.min((frequencyData[i] ?? 0) / 255 * 2, 1);
298
303
  const x = startX + i / (frequencyData.length - 1) * availableWidth;
299
- const y = waveHeight / 2 + normalizedValue * waveHeight / 2;
304
+ const barHeight = normalizedValue * waveHeight;
305
+ const y = (waveHeight + barHeight) / 2;
306
+ if (i === frequencyData.length - 1) {
307
+ path.lineTo(x, y);
308
+ } else {
309
+ const nextX = startX + (i + 1) / (frequencyData.length - 1) * availableWidth;
310
+ const nextValue = Math.min((frequencyData[i + 1] ?? 0) / 255 * 2, 1);
311
+ const nextBarHeight = nextValue * waveHeight;
312
+ const nextY = (waveHeight + nextBarHeight) / 2;
313
+ const xc = (nextX + x) / 2;
314
+ const yc = (nextY + y) / 2;
315
+ path.quadraticCurveTo(nextX, nextY, xc, yc);
316
+ }
317
+ }
318
+ const lastY = (waveHeight + firstValue * waveHeight) / 2;
319
+ const controlX = startX;
320
+ const controlY = (lastY + firstY) / 2;
321
+ path.quadraticCurveTo(controlX, controlY, startX, firstY);
322
+ ctx.fill(path);
323
+ }
324
+ drawSpikes(ctx, frequencyData) {
325
+ const canvas = ctx.canvas;
326
+ const waveWidth = canvas.width;
327
+ const waveHeight = canvas.height;
328
+ const paddingOuter = 0.01;
329
+ const availableWidth = waveWidth * (1 - 2 * paddingOuter);
330
+ const startX = waveWidth * paddingOuter;
331
+ ctx.clearRect(0, 0, waveWidth, waveHeight);
332
+ const path = new Path2D();
333
+ const firstValue = (frequencyData[0] ?? 0) / 255;
334
+ const firstY = (waveHeight - firstValue * waveHeight) / 2;
335
+ path.moveTo(startX, firstY);
336
+ frequencyData.forEach((value, i) => {
337
+ const normalizedValue = Math.min(value / 255 * 2, 1);
338
+ const x = startX + i / (frequencyData.length - 1) * availableWidth;
339
+ const barHeight = normalizedValue * (waveHeight / 2);
340
+ const y = (waveHeight - barHeight * 2) / 2;
341
+ if (i === 0) {
342
+ path.moveTo(x, y);
343
+ } else {
344
+ const prevX = startX + (i - 1) / (frequencyData.length - 1) * availableWidth;
345
+ const prevValue = (frequencyData[i - 1] ?? 0) / 255;
346
+ const prevBarHeight = prevValue * (waveHeight / 2);
347
+ const prevY = (waveHeight - prevBarHeight * 2) / 2;
348
+ const xc = (prevX + x) / 2;
349
+ const yc = (prevY + y) / 2;
350
+ path.quadraticCurveTo(prevX, prevY, xc, yc);
351
+ }
352
+ });
353
+ for (let i = frequencyData.length - 1; i >= 0; i--) {
354
+ const normalizedValue = Math.min((frequencyData[i] ?? 0) / 255 * 2, 1);
355
+ const x = startX + i / (frequencyData.length - 1) * availableWidth;
356
+ const barHeight = normalizedValue * (waveHeight / 2);
357
+ const y = (waveHeight + barHeight * 2) / 2;
300
358
  if (i === frequencyData.length - 1) {
301
359
  path.lineTo(x, y);
302
360
  } else {
303
361
  const nextX = startX + (i + 1) / (frequencyData.length - 1) * availableWidth;
304
- const nextValue = ((frequencyData[i + 1] ?? 128) - 128) / 128;
305
- const nextY = waveHeight / 2 + nextValue * waveHeight / 2;
362
+ const nextValue = (frequencyData[i + 1] ?? 0) / 255;
363
+ const nextBarHeight = nextValue * (waveHeight / 2);
364
+ const nextY = (waveHeight + nextBarHeight * 2) / 2;
306
365
  const xc = (nextX + x) / 2;
307
366
  const yc = (nextY + y) / 2;
308
367
  path.quadraticCurveTo(nextX, nextY, xc, yc);
309
368
  }
310
369
  }
311
- path.closePath();
370
+ const lastY = (waveHeight + firstValue * waveHeight) / 2;
371
+ const controlX = startX;
372
+ const controlY = (lastY + firstY) / 2;
373
+ path.quadraticCurveTo(controlX, controlY, startX, firstY);
312
374
  ctx.fill(path);
313
375
  }
314
376
  get durationMs() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.15.0-beta.14",
3
+ "version": "0.15.0-beta.15",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -27,7 +27,7 @@
27
27
  "license": "UNLICENSED",
28
28
  "dependencies": {
29
29
  "@bramus/style-observer": "^1.3.0",
30
- "@editframe/assets": "0.15.0-beta.14",
30
+ "@editframe/assets": "0.15.0-beta.15",
31
31
  "@lit/context": "^1.1.2",
32
32
  "@lit/task": "^1.0.1",
33
33
  "d3": "^7.9.0",
@@ -591,11 +591,15 @@ export class EFMedia extends EFTargetable(
591
591
  }
592
592
 
593
593
  set fftSize(value: number) {
594
+ const oldValue = this.fftSize;
594
595
  this.setAttribute("fft-size", String(value));
596
+ this.requestUpdate("fft-size", oldValue);
595
597
  }
596
598
 
597
599
  set fftDecay(value: number) {
600
+ const oldValue = this.fftDecay;
598
601
  this.setAttribute("fft-decay", String(value));
602
+ this.requestUpdate("fft-decay", oldValue);
599
603
  }
600
604
 
601
605
  get fftSize() {
@@ -606,6 +610,13 @@ export class EFMedia extends EFTargetable(
606
610
  return Number.parseInt(this.getAttribute("fft-decay") ?? "8", 10);
607
611
  }
608
612
 
613
+ get shouldInterpolateFrequencies() {
614
+ if (this.hasAttribute("interpolate-frequencies")) {
615
+ return this.getAttribute("interpolate-frequencies") !== "false";
616
+ }
617
+ return false;
618
+ }
619
+
609
620
  private static readonly DECAY_WEIGHT = 0.7;
610
621
 
611
622
  // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
@@ -683,7 +694,7 @@ export class EFMedia extends EFTargetable(
683
694
  analyser.maxDecibels = -20;
684
695
 
685
696
  const gainNode = audioContext.createGain();
686
- gainNode.gain.value = 10.0; // Amplify the signal
697
+ gainNode.gain.value = 2.0; // Amplify the signal
687
698
 
688
699
  source.connect(gainNode);
689
700
  gainNode.connect(analyser);
@@ -806,10 +817,9 @@ export class EFMedia extends EFTargetable(
806
817
  analyser.fftSize = this.fftSize;
807
818
  analyser.minDecibels = -90;
808
819
  analyser.maxDecibels = -10;
809
- // analyser.smoothingTimeConstant = 0.4;
810
820
 
811
821
  const gainNode = audioContext.createGain();
812
- gainNode.gain.value = 5.0;
822
+ gainNode.gain.value = 3.0;
813
823
 
814
824
  const filter = audioContext.createBiquadFilter();
815
825
  filter.type = "bandpass";
@@ -819,9 +829,9 @@ export class EFMedia extends EFTargetable(
819
829
  const audioBufferSource = audioContext.createBufferSource();
820
830
  audioBufferSource.buffer = audioBuffer;
821
831
 
822
- audioBufferSource.connect(gainNode);
823
- gainNode.connect(filter);
824
- filter.connect(analyser);
832
+ audioBufferSource.connect(filter);
833
+ filter.connect(gainNode);
834
+ gainNode.connect(analyser);
825
835
  analyser.connect(audioContext.destination);
826
836
 
827
837
  audioBufferSource.start(0, startTime, 1 / 30);
@@ -851,7 +861,7 @@ export class EFMedia extends EFTargetable(
851
861
 
852
862
  framesData.forEach((frame, frameIndex) => {
853
863
  const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
854
- // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
864
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
855
865
  weightedSum += frame[i]! * decayWeight;
856
866
  weightSum += decayWeight;
857
867
  });
@@ -861,7 +871,7 @@ export class EFMedia extends EFTargetable(
861
871
 
862
872
  // Apply frequency weights using instance FREQ_WEIGHTS
863
873
  smoothedData.forEach((value, i) => {
864
- // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
874
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
865
875
  const freqWeight = this.FREQ_WEIGHTS[i]!;
866
876
  smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
867
877
  });
@@ -872,8 +882,70 @@ export class EFMedia extends EFTargetable(
872
882
  0,
873
883
  Math.floor(smoothedData.length / 2),
874
884
  );
875
- this.#frequencyDataCache.set(smoothedKey, slicedData);
876
- return slicedData;
885
+ const processedData = this.shouldInterpolateFrequencies
886
+ ? processFFTData(slicedData)
887
+ : slicedData;
888
+ this.#frequencyDataCache.set(smoothedKey, processedData);
889
+ return processedData;
877
890
  },
878
891
  });
879
892
  }
893
+
894
+ function processFFTData(fftData: Uint8Array, zeroThresholdPercent = 0.1) {
895
+ // Step 1: Determine the threshold for zeros
896
+ const totalBins = fftData.length;
897
+ const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
898
+
899
+ // Step 2: Interrogate the FFT output to find the cutoff point
900
+ let zeroCount = 0;
901
+ let cutoffIndex = totalBins; // Default to the end of the array
902
+
903
+ for (let i = totalBins - 1; i >= 0; i--) {
904
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
905
+ if (fftData[i]! < 10) {
906
+ zeroCount++;
907
+ } else {
908
+ // If we encounter a non-zero value, we can stop
909
+ if (zeroCount >= zeroThresholdCount) {
910
+ cutoffIndex = i + 1; // Include this index
911
+ break;
912
+ }
913
+ }
914
+ }
915
+
916
+ if (cutoffIndex < zeroThresholdCount) {
917
+ return fftData;
918
+ }
919
+
920
+ // Step 3: Resample the "good" portion of the data
921
+ const goodData = fftData.slice(0, cutoffIndex);
922
+ const resampledData = interpolateData(goodData, fftData.length);
923
+
924
+ return resampledData;
925
+ }
926
+
927
+ function interpolateData(data: Uint8Array, targetSize: number) {
928
+ const resampled = new Uint8Array(targetSize);
929
+ const dataLength = data.length;
930
+
931
+ for (let i = 0; i < targetSize; i++) {
932
+ // Calculate the corresponding index in the original data
933
+ const ratio = (i / (targetSize - 1)) * (dataLength - 1);
934
+ const index = Math.floor(ratio);
935
+ const fraction = ratio - index;
936
+
937
+ // Handle edge cases
938
+ if (index >= dataLength - 1) {
939
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
940
+ resampled[i] = data[dataLength - 1]!; // Last value
941
+ } else {
942
+ // Linear interpolation
943
+ resampled[i] = Math.round(
944
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
945
+ data[index]! * (1 - fraction) + data[index + 1]! * fraction,
946
+ );
947
+ }
948
+ }
949
+
950
+ return resampled;
951
+ }