@editframe/elements 0.15.0-beta.8 → 0.16.0-beta.0

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.
Files changed (52) hide show
  1. package/dist/EF_FRAMEGEN.d.ts +14 -10
  2. package/dist/EF_FRAMEGEN.js +17 -28
  3. package/dist/elements/EFCaptions.js +0 -7
  4. package/dist/elements/EFImage.js +0 -4
  5. package/dist/elements/EFMedia.d.ts +13 -7
  6. package/dist/elements/EFMedia.js +217 -111
  7. package/dist/elements/EFSourceMixin.js +2 -1
  8. package/dist/elements/EFTemporal.browsertest.d.ts +4 -3
  9. package/dist/elements/EFTemporal.d.ts +14 -11
  10. package/dist/elements/EFTemporal.js +63 -87
  11. package/dist/elements/EFTimegroup.d.ts +2 -4
  12. package/dist/elements/EFTimegroup.js +15 -103
  13. package/dist/elements/EFVideo.js +3 -1
  14. package/dist/elements/EFWaveform.d.ts +3 -2
  15. package/dist/elements/EFWaveform.js +39 -26
  16. package/dist/elements/durationConverter.d.ts +8 -8
  17. package/dist/elements/durationConverter.js +2 -2
  18. package/dist/elements/updateAnimations.d.ts +9 -0
  19. package/dist/elements/updateAnimations.js +62 -0
  20. package/dist/getRenderInfo.d.ts +51 -0
  21. package/dist/getRenderInfo.js +72 -0
  22. package/dist/gui/EFFilmstrip.js +7 -16
  23. package/dist/gui/EFFitScale.d.ts +27 -0
  24. package/dist/gui/EFFitScale.js +138 -0
  25. package/dist/gui/EFWorkbench.d.ts +2 -5
  26. package/dist/gui/EFWorkbench.js +13 -56
  27. package/dist/gui/TWMixin.css.js +1 -1
  28. package/dist/gui/TWMixin.js +14 -2
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +6 -1
  31. package/dist/style.css +6 -3
  32. package/package.json +9 -4
  33. package/src/elements/EFCaptions.browsertest.ts +2 -2
  34. package/src/elements/EFCaptions.ts +0 -7
  35. package/src/elements/EFImage.browsertest.ts +2 -2
  36. package/src/elements/EFImage.ts +0 -4
  37. package/src/elements/EFMedia.browsertest.ts +14 -14
  38. package/src/elements/EFMedia.ts +291 -136
  39. package/src/elements/EFSourceMixin.ts +4 -4
  40. package/src/elements/EFTemporal.browsertest.ts +64 -31
  41. package/src/elements/EFTemporal.ts +99 -119
  42. package/src/elements/EFTimegroup.ts +15 -133
  43. package/src/elements/EFVideo.ts +3 -1
  44. package/src/elements/EFWaveform.ts +54 -39
  45. package/src/elements/durationConverter.ts +9 -4
  46. package/src/elements/updateAnimations.ts +88 -0
  47. package/src/gui/ContextMixin.ts +0 -3
  48. package/src/gui/EFFilmstrip.ts +7 -16
  49. package/src/gui/EFFitScale.ts +152 -0
  50. package/src/gui/EFWorkbench.ts +18 -65
  51. package/src/gui/TWMixin.ts +19 -2
  52. package/types.json +1 -1
@@ -10,11 +10,11 @@ import type { TrackFragmentIndex, TrackSegment } from "@editframe/assets";
10
10
  import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
11
11
  import { MP4File } from "@editframe/assets/MP4File.js";
12
12
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
13
- import { EF_RENDERING } from "../EF_RENDERING.js";
14
13
  import { EFSourceMixin } from "./EFSourceMixin.js";
15
- import { EFTemporal, isEFTemporal } from "./EFTemporal.js";
14
+ import { EFTemporal } from "./EFTemporal.js";
16
15
  import { FetchMixin } from "./FetchMixin.js";
17
16
  import { EFTargetable } from "./TargetController.ts";
17
+ import { updateAnimations } from "./updateAnimations.ts";
18
18
 
19
19
  const log = debug("ef:elements:EFMedia");
20
20
 
@@ -102,9 +102,6 @@ export class EFMedia extends EFTargetable(
102
102
 
103
103
  fragmentIndexPath() {
104
104
  if (this.assetId) {
105
- if (EF_RENDERING()) {
106
- return `editframe://api/v1/isobmff_files/${this.assetId}/index`;
107
- }
108
105
  return `${this.apiHost}/api/v1/isobmff_files/${this.assetId}/index`;
109
106
  }
110
107
  return `/@ef-track-fragment-index/${this.src ?? ""}`;
@@ -112,9 +109,6 @@ export class EFMedia extends EFTargetable(
112
109
 
113
110
  fragmentTrackPath(trackId: string) {
114
111
  if (this.assetId) {
115
- if (EF_RENDERING()) {
116
- return `editframe://api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
117
- }
118
112
  return `${this.apiHost}/api/v1/isobmff_tracks/${this.assetId}/${trackId}`;
119
113
  }
120
114
  // trackId is only specified as a query in the @ef-track url shape
@@ -126,12 +120,21 @@ export class EFMedia extends EFTargetable(
126
120
  public trackFragmentIndexLoader = new Task(this, {
127
121
  args: () => [this.fragmentIndexPath(), this.fetch] as const,
128
122
  task: async ([fragmentIndexPath, fetch], { signal }) => {
129
- const response = await fetch(fragmentIndexPath, { signal });
130
- return (await response.json()) as Record<number, TrackFragmentIndex>;
123
+ try {
124
+ const response = await fetch(fragmentIndexPath, { signal });
125
+
126
+ return (await response.json()) as Record<number, TrackFragmentIndex>;
127
+ } catch (error) {
128
+ log("Failed to load track fragment index", error);
129
+ return undefined;
130
+ }
131
131
  },
132
132
  onComplete: () => {
133
+ this.requestUpdate("intrinsicDurationMs");
133
134
  this.requestUpdate("ownCurrentTimeMs");
135
+ console.log("Requesting update for durationMs", this, this.rootTimegroup);
134
136
  this.rootTimegroup?.requestUpdate("ownCurrentTimeMs");
137
+ this.rootTimegroup?.requestUpdate("durationMs");
135
138
  },
136
139
  });
137
140
 
@@ -341,75 +344,7 @@ export class EFMedia extends EFTargetable(
341
344
  changedProperties.has("currentTime") ||
342
345
  changedProperties.has("ownCurrentTimeMs")
343
346
  ) {
344
- const timelineTimeMs = (this.rootTimegroup ?? this).currentTimeMs;
345
- if (
346
- this.startTimeMs > timelineTimeMs ||
347
- this.endTimeMs < timelineTimeMs
348
- ) {
349
- this.style.display = "none";
350
- return;
351
- }
352
- this.style.display = "";
353
- const animations = this.getAnimations({ subtree: true });
354
-
355
- this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
356
- this.style.setProperty(
357
- "--ef-transition-duration",
358
- `${this.parentTimegroup?.overlapMs ?? 0}ms`,
359
- );
360
- this.style.setProperty(
361
- "--ef-transition-out-start",
362
- `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
363
- );
364
-
365
- for (const animation of animations) {
366
- if (animation.playState === "running") {
367
- animation.pause();
368
- }
369
- const effect = animation.effect;
370
- if (!(effect && effect instanceof KeyframeEffect)) {
371
- return;
372
- }
373
- const target = effect.target;
374
- // TODO: better generalize work avoidance for temporal elements
375
- if (!target) {
376
- return;
377
- }
378
- if (target.closest("ef-video, ef-audio") !== this) {
379
- return;
380
- }
381
-
382
- // Important to avoid going to the end of the animation
383
- // or it will reset awkwardly.
384
- if (isEFTemporal(target)) {
385
- const timing = effect.getTiming();
386
- const duration = Number(timing.duration) ?? 0;
387
- const delay = Number(timing.delay);
388
- const newTime = Math.floor(
389
- Math.min(target.ownCurrentTimeMs, duration - 1 + delay),
390
- );
391
- if (Number.isNaN(newTime)) {
392
- return;
393
- }
394
- animation.currentTime = newTime;
395
- } else if (target) {
396
- const nearestTimegroup = target.closest("ef-timegroup");
397
- if (!nearestTimegroup) {
398
- return;
399
- }
400
- const timing = effect.getTiming();
401
- const duration = Number(timing.duration) ?? 0;
402
- const delay = Number(timing.delay);
403
- const newTime = Math.floor(
404
- Math.min(nearestTimegroup.ownCurrentTimeMs, duration - 1 + delay),
405
- );
406
-
407
- if (Number.isNaN(newTime)) {
408
- return;
409
- }
410
- animation.currentTime = newTime;
411
- }
412
- }
347
+ updateAnimations(this);
413
348
  }
414
349
  }
415
350
 
@@ -417,7 +352,7 @@ export class EFMedia extends EFTargetable(
417
352
  return true;
418
353
  }
419
354
 
420
- get durationMs() {
355
+ get intrinsicDurationMs() {
421
356
  if (!this.trackFragmentIndexLoader.value) {
422
357
  return 0;
423
358
  }
@@ -430,39 +365,7 @@ export class EFMedia extends EFTargetable(
430
365
  if (durations.length === 0) {
431
366
  return 0;
432
367
  }
433
- if (
434
- this.sourceInMs &&
435
- this.sourceOutMs &&
436
- this.sourceOutMs > this.sourceInMs
437
- ) {
438
- return Math.max(this.sourceOutMs - this.sourceInMs);
439
- }
440
- if (this.sourceInMs) {
441
- return (
442
- Math.max(...durations) -
443
- this.trimStartMs -
444
- this.trimEndMs -
445
- this.sourceInMs
446
- );
447
- }
448
- if (this.sourceOutMs) {
449
- return (
450
- Math.max(...durations) -
451
- this.trimStartMs -
452
- this.trimEndMs -
453
- this.sourceOutMs
454
- );
455
- }
456
- if (this.sourceInMs && this.sourceOutMs) {
457
- return (
458
- Math.max(...durations) -
459
- this.trimStartMs -
460
- this.trimEndMs -
461
- this.sourceOutMs -
462
- this.sourceInMs
463
- );
464
- }
465
- return Math.max(...durations) - this.trimStartMs - this.trimEndMs;
368
+ return Math.max(...durations);
466
369
  }
467
370
 
468
371
  #audioContext = new OfflineAudioContext(2, 48000 / 30, 48000);
@@ -500,13 +403,15 @@ export class EFMedia extends EFTargetable(
500
403
  async fetchAudioSpanningTime(fromMs: number, toMs: number) {
501
404
  // Adjust range for track's own time
502
405
  if (this.sourceInMs) {
503
- fromMs -= this.startTimeMs - this.trimStartMs - this.sourceInMs;
406
+ fromMs -=
407
+ this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceInMs ?? 0);
504
408
  }
505
409
  if (this.sourceOutMs) {
506
- toMs -= this.startTimeMs - this.trimStartMs - this.sourceOutMs;
410
+ toMs -=
411
+ this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceOutMs ?? 0);
507
412
  }
508
- fromMs -= this.startTimeMs - this.trimStartMs;
509
- toMs -= this.startTimeMs - this.trimStartMs;
413
+ fromMs -= this.startTimeMs - (this.trimStartMs ?? 0);
414
+ toMs -= this.startTimeMs - (this.trimStartMs ?? 0);
510
415
 
511
416
  await this.trackFragmentIndexLoader.taskComplete;
512
417
  const audioTrackId = this.defaultAudioTrackId;
@@ -576,22 +481,51 @@ export class EFMedia extends EFTargetable(
576
481
  blob: audioBlob,
577
482
  startMs:
578
483
  (firstFragment.dts / audioTrackIndex.timescale) * 1000 -
579
- this.trimStartMs,
484
+ (this.trimStartMs ?? 0),
580
485
  endMs:
581
486
  (lastFragment.dts / audioTrackIndex.timescale) * 1000 +
582
487
  (lastFragment.duration / audioTrackIndex.timescale) * 1000 -
583
- this.trimEndMs,
488
+ (this.trimEndMs ?? 0),
584
489
  };
585
490
  }
586
491
 
587
- @property({ type: Number })
588
- fftSize = 512; // Default FFT size
492
+ set fftSize(value: number) {
493
+ const oldValue = this.fftSize;
494
+ this.setAttribute("fft-size", String(value));
495
+ this.requestUpdate("fft-size", oldValue);
496
+ }
589
497
 
590
- @property({ type: Number })
591
- fftDecay = 8; // Default number of frames to analyze
498
+ set fftDecay(value: number) {
499
+ const oldValue = this.fftDecay;
500
+ this.setAttribute("fft-decay", String(value));
501
+ this.requestUpdate("fft-decay", oldValue);
502
+ }
503
+
504
+ get fftSize() {
505
+ return Number.parseInt(this.getAttribute("fft-size") ?? "128", 10);
506
+ }
507
+
508
+ get fftDecay() {
509
+ return Number.parseInt(this.getAttribute("fft-decay") ?? "8", 10);
510
+ }
511
+
512
+ set interpolateFrequencies(value: boolean) {
513
+ const oldValue = this.interpolateFrequencies;
514
+ this.setAttribute("interpolate-frequencies", String(value));
515
+ this.requestUpdate("interpolate-frequencies", oldValue);
516
+ }
517
+
518
+ get interpolateFrequencies() {
519
+ return this.getAttribute("interpolate-frequencies") !== "false";
520
+ }
521
+
522
+ get shouldInterpolateFrequencies() {
523
+ if (this.hasAttribute("interpolate-frequencies")) {
524
+ return this.getAttribute("interpolate-frequencies") !== "false";
525
+ }
526
+ return false;
527
+ }
592
528
 
593
- private static readonly MIN_DB = -90;
594
- private static readonly MAX_DB = -20;
595
529
  private static readonly DECAY_WEIGHT = 0.7;
596
530
 
597
531
  // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
@@ -616,6 +550,132 @@ export class EFMedia extends EFTargetable(
616
550
  return weights;
617
551
  }
618
552
 
553
+ #byteTimeDomainCache = new LRUCache<string, Uint8Array>(100);
554
+
555
+ byteTimeDomainTask = new Task(this, {
556
+ autoRun: EF_INTERACTIVE,
557
+ args: () =>
558
+ [
559
+ this.audioBufferTask.status,
560
+ this.currentSourceTimeMs,
561
+ this.fftSize,
562
+ this.fftDecay,
563
+ this.fftGain,
564
+ this.shouldInterpolateFrequencies,
565
+ ] as const,
566
+ task: async () => {
567
+ await this.audioBufferTask.taskComplete;
568
+ if (!this.audioBufferTask.value) return null;
569
+ if (this.currentSourceTimeMs <= 0) return null;
570
+
571
+ const currentTimeMs = this.currentSourceTimeMs;
572
+ const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
573
+ const audioBuffer = this.audioBufferTask.value.buffer;
574
+
575
+ const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
576
+ const cachedData = this.#byteTimeDomainCache.get(smoothedKey);
577
+ if (cachedData) return cachedData;
578
+
579
+ // Process multiple frames with decay, similar to the reference code
580
+ const framesData = await Promise.all(
581
+ Array.from({ length: this.fftDecay }, async (_, frameIndex) => {
582
+ const frameOffset = frameIndex * (1000 / 30);
583
+ const startTime = Math.max(
584
+ 0,
585
+ (currentTimeMs - frameOffset - startOffsetMs) / 1000,
586
+ );
587
+
588
+ const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
589
+ const cachedFrame = this.#byteTimeDomainCache.get(cacheKey);
590
+ if (cachedFrame) return cachedFrame;
591
+
592
+ const audioContext = new OfflineAudioContext(
593
+ 2,
594
+ 48000 * (1 / 30),
595
+ 48000,
596
+ );
597
+
598
+ const source = audioContext.createBufferSource();
599
+ source.buffer = audioBuffer;
600
+
601
+ // Create analyzer for PCM data
602
+ const analyser = audioContext.createAnalyser();
603
+ analyser.fftSize = this.fftSize; // Ensure enough samples
604
+ analyser.minDecibels = -90;
605
+ analyser.maxDecibels = -20;
606
+
607
+ const gainNode = audioContext.createGain();
608
+ gainNode.gain.value = this.fftGain; // Amplify the signal
609
+
610
+ source.connect(gainNode);
611
+ gainNode.connect(analyser);
612
+ analyser.connect(audioContext.destination);
613
+
614
+ source.start(0, startTime, 1 / 30);
615
+
616
+ const dataLength = analyser.fftSize / 2;
617
+ try {
618
+ await audioContext.startRendering();
619
+ const frameData = new Uint8Array(dataLength);
620
+ analyser.getByteTimeDomainData(frameData);
621
+
622
+ // const points = frameData;
623
+ // Calculate RMS and midpoint values
624
+ const points = new Uint8Array(dataLength);
625
+ for (let i = 0; i < dataLength; i++) {
626
+ const pointSamples = frameData.slice(
627
+ i * (frameData.length / dataLength),
628
+ (i + 1) * (frameData.length / dataLength),
629
+ );
630
+
631
+ // Calculate RMS while preserving sign
632
+ const rms = Math.sqrt(
633
+ pointSamples.reduce((sum, sample) => {
634
+ const normalized = (sample - 128) / 128;
635
+ return sum + normalized * normalized;
636
+ }, 0) / pointSamples.length,
637
+ );
638
+
639
+ // Get average sign of the samples to determine direction
640
+ const avgSign = Math.sign(
641
+ pointSamples.reduce((sum, sample) => sum + (sample - 128), 0),
642
+ );
643
+
644
+ // Convert RMS back to byte range, preserving direction
645
+ points[i] = Math.min(255, Math.round(128 + avgSign * rms * 128));
646
+ }
647
+
648
+ this.#byteTimeDomainCache.set(cacheKey, points);
649
+ return points;
650
+ } finally {
651
+ source.disconnect();
652
+ analyser.disconnect();
653
+ }
654
+ }),
655
+ );
656
+
657
+ // Combine frames with decay weighting
658
+ const frameLength = framesData[0]?.length ?? 0;
659
+ const smoothedData = new Uint8Array(frameLength);
660
+
661
+ for (let i = 0; i < frameLength; i++) {
662
+ let weightedSum = 0;
663
+ let weightSum = 0;
664
+
665
+ framesData.forEach((frame, frameIndex) => {
666
+ const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
667
+ weightedSum += (frame[i] ?? 0) * decayWeight;
668
+ weightSum += decayWeight;
669
+ });
670
+
671
+ smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
672
+ }
673
+
674
+ this.#byteTimeDomainCache.set(smoothedKey, smoothedData);
675
+ return smoothedData;
676
+ },
677
+ });
678
+
619
679
  #frequencyDataCache = new LRUCache<string, Uint8Array>(100);
620
680
 
621
681
  frequencyDataTask = new Task(this, {
@@ -624,8 +684,10 @@ export class EFMedia extends EFTargetable(
624
684
  [
625
685
  this.audioBufferTask.status,
626
686
  this.currentSourceTimeMs,
627
- this.fftSize, // Add fftSize to dependency array
628
- this.fftDecay, // Add fftDecay to dependency array
687
+ this.fftSize,
688
+ this.fftDecay,
689
+ this.fftGain,
690
+ this.shouldInterpolateFrequencies,
629
691
  ] as const,
630
692
  task: async () => {
631
693
  await this.audioBufferTask.taskComplete;
@@ -635,7 +697,7 @@ export class EFMedia extends EFTargetable(
635
697
  const currentTimeMs = this.currentSourceTimeMs;
636
698
  const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
637
699
  const audioBuffer = this.audioBufferTask.value.buffer;
638
- const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
700
+ const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
639
701
 
640
702
  const cachedSmoothedData = this.#frequencyDataCache.get(smoothedKey);
641
703
  if (cachedSmoothedData) {
@@ -651,7 +713,7 @@ export class EFMedia extends EFTargetable(
651
713
  );
652
714
 
653
715
  // Cache key for this specific frame
654
- const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
716
+ const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
655
717
 
656
718
  // Check cache for this specific frame
657
719
  const cachedFrame = this.#frequencyDataCache.get(cacheKey);
@@ -666,13 +728,23 @@ export class EFMedia extends EFTargetable(
666
728
  );
667
729
  const analyser = audioContext.createAnalyser();
668
730
  analyser.fftSize = this.fftSize;
669
- analyser.minDecibels = EFMedia.MIN_DB;
670
- analyser.maxDecibels = EFMedia.MAX_DB;
731
+ analyser.minDecibels = -90;
732
+ analyser.maxDecibels = -10;
733
+
734
+ const gainNode = audioContext.createGain();
735
+ gainNode.gain.value = this.fftGain;
736
+
737
+ const filter = audioContext.createBiquadFilter();
738
+ filter.type = "bandpass";
739
+ filter.frequency.value = 15000;
740
+ filter.Q.value = 0.05;
671
741
 
672
742
  const audioBufferSource = audioContext.createBufferSource();
673
743
  audioBufferSource.buffer = audioBuffer;
674
744
 
675
- audioBufferSource.connect(analyser);
745
+ audioBufferSource.connect(filter);
746
+ filter.connect(gainNode);
747
+ gainNode.connect(analyser);
676
748
  analyser.connect(audioContext.destination);
677
749
 
678
750
  audioBufferSource.start(0, startTime, 1 / 30);
@@ -702,7 +774,7 @@ export class EFMedia extends EFTargetable(
702
774
 
703
775
  framesData.forEach((frame, frameIndex) => {
704
776
  const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
705
- // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
777
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
706
778
  weightedSum += frame[i]! * decayWeight;
707
779
  weightSum += decayWeight;
708
780
  });
@@ -712,7 +784,7 @@ export class EFMedia extends EFTargetable(
712
784
 
713
785
  // Apply frequency weights using instance FREQ_WEIGHTS
714
786
  smoothedData.forEach((value, i) => {
715
- // biome-ignore lint/style/noNonNullAssertion: Will exist due to forEach
787
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
716
788
  const freqWeight = this.FREQ_WEIGHTS[i]!;
717
789
  smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
718
790
  });
@@ -723,8 +795,91 @@ export class EFMedia extends EFTargetable(
723
795
  0,
724
796
  Math.floor(smoothedData.length / 2),
725
797
  );
726
- this.#frequencyDataCache.set(smoothedKey, slicedData);
727
- return slicedData;
798
+ const processedData = this.shouldInterpolateFrequencies
799
+ ? processFFTData(slicedData)
800
+ : slicedData;
801
+ this.#frequencyDataCache.set(smoothedKey, processedData);
802
+ return processedData;
728
803
  },
729
804
  });
805
+
806
+ set fftGain(value: number) {
807
+ const oldValue = this.fftGain;
808
+ this.setAttribute("fft-gain", String(value));
809
+ this.requestUpdate("fft-gain", oldValue);
810
+ }
811
+
812
+ get fftGain() {
813
+ return Number.parseFloat(this.getAttribute("fft-gain") ?? "3.0");
814
+ }
815
+ }
816
+
817
+ function processFFTData(fftData: Uint8Array, zeroThresholdPercent = 0.1) {
818
+ // Step 1: Determine the threshold for zeros
819
+ const totalBins = fftData.length;
820
+ const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
821
+
822
+ // Step 2: Interrogate the FFT output to find the cutoff point
823
+ let zeroCount = 0;
824
+ let cutoffIndex = totalBins; // Default to the end of the array
825
+
826
+ for (let i = totalBins - 1; i >= 0; i--) {
827
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
828
+ if (fftData[i]! < 10) {
829
+ zeroCount++;
830
+ } else {
831
+ // If we encounter a non-zero value, we can stop
832
+ if (zeroCount >= zeroThresholdCount) {
833
+ cutoffIndex = i + 1; // Include this index
834
+ break;
835
+ }
836
+ }
837
+ }
838
+
839
+ if (cutoffIndex < zeroThresholdCount) {
840
+ return fftData;
841
+ }
842
+
843
+ // Step 3: Resample the "good" portion of the data
844
+ const goodData = fftData.slice(0, cutoffIndex);
845
+ const resampledData = interpolateData(goodData, fftData.length);
846
+
847
+ // Step 4: Attenuate the top 10% of interpolated samples
848
+ const attenuationStartIndex = Math.floor(totalBins * 0.9);
849
+ for (let i = attenuationStartIndex; i < totalBins; i++) {
850
+ // Calculate attenuation factor that goes from 1 to 0 over the top 10%
851
+ const attenuationProgress =
852
+ (i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + 0.2;
853
+ const attenuationFactor = Math.max(0, 1 - attenuationProgress);
854
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
855
+ resampledData[i] = Math.floor(resampledData[i]! * attenuationFactor);
856
+ }
857
+
858
+ return resampledData;
859
+ }
860
+
861
+ function interpolateData(data: Uint8Array, targetSize: number) {
862
+ const resampled = new Uint8Array(targetSize);
863
+ const dataLength = data.length;
864
+
865
+ for (let i = 0; i < targetSize; i++) {
866
+ // Calculate the corresponding index in the original data
867
+ const ratio = (i / (targetSize - 1)) * (dataLength - 1);
868
+ const index = Math.floor(ratio);
869
+ const fraction = ratio - index;
870
+
871
+ // Handle edge cases
872
+ if (index >= dataLength - 1) {
873
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
874
+ resampled[i] = data[dataLength - 1]!; // Last value
875
+ } else {
876
+ // Linear interpolation
877
+ resampled[i] = Math.round(
878
+ // biome-ignore lint/style/noNonNullAssertion: Manual bounds check
879
+ data[index]! * (1 - fraction) + data[index + 1]! * fraction,
880
+ );
881
+ }
882
+ }
883
+
884
+ return resampled;
730
885
  }
@@ -18,12 +18,12 @@ export function EFSourceMixin<T extends Constructor<LitElement>>(
18
18
  ) {
19
19
  class EFSourceElement extends superClass {
20
20
  get apiHost() {
21
- return (
21
+ const apiHost =
22
22
  this.closest("ef-configuration")?.apiHost ??
23
23
  this.closest("ef-workbench")?.apiHost ??
24
- this.closest("ef-preview")?.apiHost ??
25
- "https://editframe.dev"
26
- );
24
+ this.closest("ef-preview")?.apiHost;
25
+
26
+ return apiHost || "https://editframe.dev";
27
27
  }
28
28
 
29
29
  @property({ type: String })