@editframe/elements 0.15.0-beta.9 → 0.16.0-beta.1
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.
- package/dist/EF_FRAMEGEN.d.ts +14 -10
- package/dist/EF_FRAMEGEN.js +17 -28
- package/dist/elements/EFCaptions.js +0 -7
- package/dist/elements/EFImage.js +0 -4
- package/dist/elements/EFMedia.d.ts +13 -8
- package/dist/elements/EFMedia.js +163 -146
- package/dist/elements/EFSourceMixin.js +2 -1
- package/dist/elements/EFTemporal.browsertest.d.ts +4 -3
- package/dist/elements/EFTemporal.d.ts +14 -11
- package/dist/elements/EFTemporal.js +63 -87
- package/dist/elements/EFTimegroup.d.ts +2 -4
- package/dist/elements/EFTimegroup.js +15 -103
- package/dist/elements/EFVideo.js +3 -1
- package/dist/elements/EFWaveform.d.ts +1 -1
- package/dist/elements/EFWaveform.js +11 -28
- package/dist/elements/durationConverter.d.ts +8 -8
- package/dist/elements/durationConverter.js +2 -2
- package/dist/elements/updateAnimations.d.ts +9 -0
- package/dist/elements/updateAnimations.js +62 -0
- package/dist/getRenderInfo.d.ts +51 -0
- package/dist/getRenderInfo.js +72 -0
- package/dist/gui/EFFilmstrip.js +7 -16
- package/dist/gui/EFFitScale.d.ts +27 -0
- package/dist/gui/EFFitScale.js +138 -0
- package/dist/gui/EFWorkbench.d.ts +2 -5
- package/dist/gui/EFWorkbench.js +11 -56
- package/dist/gui/TWMixin.css.js +1 -1
- package/dist/gui/TWMixin.js +14 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -1
- package/dist/style.css +3 -3
- package/package.json +4 -3
- package/src/elements/EFCaptions.browsertest.ts +2 -2
- package/src/elements/EFCaptions.ts +0 -7
- package/src/elements/EFImage.browsertest.ts +2 -2
- package/src/elements/EFImage.ts +0 -4
- package/src/elements/EFMedia.browsertest.ts +14 -14
- package/src/elements/EFMedia.ts +219 -182
- package/src/elements/EFSourceMixin.ts +4 -4
- package/src/elements/EFTemporal.browsertest.ts +64 -31
- package/src/elements/EFTemporal.ts +99 -119
- package/src/elements/EFTimegroup.ts +15 -133
- package/src/elements/EFVideo.ts +3 -1
- package/src/elements/EFWaveform.ts +10 -44
- package/src/elements/durationConverter.ts +9 -4
- package/src/elements/updateAnimations.ts +88 -0
- package/src/gui/ContextMixin.ts +0 -3
- package/src/gui/EFFilmstrip.ts +7 -16
- package/src/gui/EFFitScale.ts +152 -0
- package/src/gui/EFWorkbench.ts +16 -65
- package/src/gui/TWMixin.ts +19 -2
- package/types.json +1 -1
package/src/elements/EFMedia.ts
CHANGED
|
@@ -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
|
|
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,20 @@ 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
|
-
|
|
130
|
-
|
|
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");
|
|
134
135
|
this.rootTimegroup?.requestUpdate("ownCurrentTimeMs");
|
|
136
|
+
this.rootTimegroup?.requestUpdate("durationMs");
|
|
135
137
|
},
|
|
136
138
|
});
|
|
137
139
|
|
|
@@ -341,75 +343,7 @@ export class EFMedia extends EFTargetable(
|
|
|
341
343
|
changedProperties.has("currentTime") ||
|
|
342
344
|
changedProperties.has("ownCurrentTimeMs")
|
|
343
345
|
) {
|
|
344
|
-
|
|
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
|
-
}
|
|
346
|
+
updateAnimations(this);
|
|
413
347
|
}
|
|
414
348
|
}
|
|
415
349
|
|
|
@@ -417,7 +351,7 @@ export class EFMedia extends EFTargetable(
|
|
|
417
351
|
return true;
|
|
418
352
|
}
|
|
419
353
|
|
|
420
|
-
get
|
|
354
|
+
get intrinsicDurationMs() {
|
|
421
355
|
if (!this.trackFragmentIndexLoader.value) {
|
|
422
356
|
return 0;
|
|
423
357
|
}
|
|
@@ -430,39 +364,7 @@ export class EFMedia extends EFTargetable(
|
|
|
430
364
|
if (durations.length === 0) {
|
|
431
365
|
return 0;
|
|
432
366
|
}
|
|
433
|
-
|
|
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;
|
|
367
|
+
return Math.max(...durations);
|
|
466
368
|
}
|
|
467
369
|
|
|
468
370
|
#audioContext = new OfflineAudioContext(2, 48000 / 30, 48000);
|
|
@@ -500,13 +402,15 @@ export class EFMedia extends EFTargetable(
|
|
|
500
402
|
async fetchAudioSpanningTime(fromMs: number, toMs: number) {
|
|
501
403
|
// Adjust range for track's own time
|
|
502
404
|
if (this.sourceInMs) {
|
|
503
|
-
fromMs -=
|
|
405
|
+
fromMs -=
|
|
406
|
+
this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceInMs ?? 0);
|
|
504
407
|
}
|
|
505
408
|
if (this.sourceOutMs) {
|
|
506
|
-
toMs -=
|
|
409
|
+
toMs -=
|
|
410
|
+
this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceOutMs ?? 0);
|
|
507
411
|
}
|
|
508
|
-
fromMs -= this.startTimeMs - this.trimStartMs;
|
|
509
|
-
toMs -= this.startTimeMs - this.trimStartMs;
|
|
412
|
+
fromMs -= this.startTimeMs - (this.trimStartMs ?? 0);
|
|
413
|
+
toMs -= this.startTimeMs - (this.trimStartMs ?? 0);
|
|
510
414
|
|
|
511
415
|
await this.trackFragmentIndexLoader.taskComplete;
|
|
512
416
|
const audioTrackId = this.defaultAudioTrackId;
|
|
@@ -576,22 +480,51 @@ export class EFMedia extends EFTargetable(
|
|
|
576
480
|
blob: audioBlob,
|
|
577
481
|
startMs:
|
|
578
482
|
(firstFragment.dts / audioTrackIndex.timescale) * 1000 -
|
|
579
|
-
this.trimStartMs,
|
|
483
|
+
(this.trimStartMs ?? 0),
|
|
580
484
|
endMs:
|
|
581
485
|
(lastFragment.dts / audioTrackIndex.timescale) * 1000 +
|
|
582
486
|
(lastFragment.duration / audioTrackIndex.timescale) * 1000 -
|
|
583
|
-
this.trimEndMs,
|
|
487
|
+
(this.trimEndMs ?? 0),
|
|
584
488
|
};
|
|
585
489
|
}
|
|
586
490
|
|
|
587
|
-
|
|
588
|
-
|
|
491
|
+
set fftSize(value: number) {
|
|
492
|
+
const oldValue = this.fftSize;
|
|
493
|
+
this.setAttribute("fft-size", String(value));
|
|
494
|
+
this.requestUpdate("fft-size", oldValue);
|
|
495
|
+
}
|
|
589
496
|
|
|
590
|
-
|
|
591
|
-
|
|
497
|
+
set fftDecay(value: number) {
|
|
498
|
+
const oldValue = this.fftDecay;
|
|
499
|
+
this.setAttribute("fft-decay", String(value));
|
|
500
|
+
this.requestUpdate("fft-decay", oldValue);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
get fftSize() {
|
|
504
|
+
return Number.parseInt(this.getAttribute("fft-size") ?? "128", 10);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
get fftDecay() {
|
|
508
|
+
return Number.parseInt(this.getAttribute("fft-decay") ?? "8", 10);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
set interpolateFrequencies(value: boolean) {
|
|
512
|
+
const oldValue = this.interpolateFrequencies;
|
|
513
|
+
this.setAttribute("interpolate-frequencies", String(value));
|
|
514
|
+
this.requestUpdate("interpolate-frequencies", oldValue);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
get interpolateFrequencies() {
|
|
518
|
+
return this.getAttribute("interpolate-frequencies") !== "false";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
get shouldInterpolateFrequencies() {
|
|
522
|
+
if (this.hasAttribute("interpolate-frequencies")) {
|
|
523
|
+
return this.getAttribute("interpolate-frequencies") !== "false";
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
592
527
|
|
|
593
|
-
private static readonly MIN_DB = -90;
|
|
594
|
-
private static readonly MAX_DB = -20;
|
|
595
528
|
private static readonly DECAY_WEIGHT = 0.7;
|
|
596
529
|
|
|
597
530
|
// Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
|
|
@@ -626,6 +559,8 @@ export class EFMedia extends EFTargetable(
|
|
|
626
559
|
this.currentSourceTimeMs,
|
|
627
560
|
this.fftSize,
|
|
628
561
|
this.fftDecay,
|
|
562
|
+
this.fftGain,
|
|
563
|
+
this.shouldInterpolateFrequencies,
|
|
629
564
|
] as const,
|
|
630
565
|
task: async () => {
|
|
631
566
|
await this.audioBufferTask.taskComplete;
|
|
@@ -635,100 +570,107 @@ export class EFMedia extends EFTargetable(
|
|
|
635
570
|
const currentTimeMs = this.currentSourceTimeMs;
|
|
636
571
|
const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
|
|
637
572
|
const audioBuffer = this.audioBufferTask.value.buffer;
|
|
638
|
-
const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
|
|
639
573
|
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
574
|
+
const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
|
|
575
|
+
const cachedData = this.#byteTimeDomainCache.get(smoothedKey);
|
|
576
|
+
if (cachedData) return cachedData;
|
|
644
577
|
|
|
578
|
+
// Process multiple frames with decay, similar to the reference code
|
|
645
579
|
const framesData = await Promise.all(
|
|
646
|
-
Array.from({ length: this.fftDecay }, async (_,
|
|
647
|
-
const frameOffset =
|
|
580
|
+
Array.from({ length: this.fftDecay }, async (_, frameIndex) => {
|
|
581
|
+
const frameOffset = frameIndex * (1000 / 30);
|
|
648
582
|
const startTime = Math.max(
|
|
649
583
|
0,
|
|
650
584
|
(currentTimeMs - frameOffset - startOffsetMs) / 1000,
|
|
651
585
|
);
|
|
652
586
|
|
|
653
|
-
const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
|
|
587
|
+
const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
|
|
654
588
|
const cachedFrame = this.#byteTimeDomainCache.get(cacheKey);
|
|
655
|
-
if (cachedFrame)
|
|
656
|
-
return cachedFrame;
|
|
657
|
-
}
|
|
589
|
+
if (cachedFrame) return cachedFrame;
|
|
658
590
|
|
|
659
591
|
const audioContext = new OfflineAudioContext(
|
|
660
592
|
2,
|
|
661
593
|
48000 * (1 / 30),
|
|
662
594
|
48000,
|
|
663
595
|
);
|
|
664
|
-
const analyser = audioContext.createAnalyser();
|
|
665
|
-
analyser.fftSize = this.fftSize;
|
|
666
596
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
gainNode.gain.value = 10.0; // Try a higher gain
|
|
597
|
+
const source = audioContext.createBufferSource();
|
|
598
|
+
source.buffer = audioBuffer;
|
|
670
599
|
|
|
671
|
-
//
|
|
672
|
-
analyser
|
|
600
|
+
// Create analyzer for PCM data
|
|
601
|
+
const analyser = audioContext.createAnalyser();
|
|
602
|
+
analyser.fftSize = this.fftSize; // Ensure enough samples
|
|
673
603
|
analyser.minDecibels = -90;
|
|
674
|
-
analyser.maxDecibels = -
|
|
675
|
-
|
|
676
|
-
const audioBufferSource = audioContext.createBufferSource();
|
|
677
|
-
audioBufferSource.buffer = audioBuffer;
|
|
604
|
+
analyser.maxDecibels = -20;
|
|
678
605
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
filter.type = "bandpass";
|
|
682
|
-
filter.frequency.value = 1000; // Center frequency in Hz
|
|
683
|
-
filter.Q.value = 0.5; // Width of the band
|
|
606
|
+
const gainNode = audioContext.createGain();
|
|
607
|
+
gainNode.gain.value = this.fftGain; // Amplify the signal
|
|
684
608
|
|
|
685
|
-
|
|
686
|
-
gainNode.connect(
|
|
687
|
-
filter.connect(analyser);
|
|
609
|
+
source.connect(gainNode);
|
|
610
|
+
gainNode.connect(analyser);
|
|
688
611
|
analyser.connect(audioContext.destination);
|
|
689
612
|
|
|
690
|
-
|
|
613
|
+
source.start(0, startTime, 1 / 30);
|
|
691
614
|
|
|
615
|
+
const dataLength = analyser.fftSize / 2;
|
|
692
616
|
try {
|
|
693
617
|
await audioContext.startRendering();
|
|
694
|
-
|
|
695
|
-
const frameData = new Uint8Array(analyser.fftSize);
|
|
618
|
+
const frameData = new Uint8Array(dataLength);
|
|
696
619
|
analyser.getByteTimeDomainData(frameData);
|
|
697
620
|
|
|
698
|
-
|
|
699
|
-
|
|
621
|
+
// const points = frameData;
|
|
622
|
+
// Calculate RMS and midpoint values
|
|
623
|
+
const points = new Uint8Array(dataLength);
|
|
624
|
+
for (let i = 0; i < dataLength; i++) {
|
|
625
|
+
const pointSamples = frameData.slice(
|
|
626
|
+
i * (frameData.length / dataLength),
|
|
627
|
+
(i + 1) * (frameData.length / dataLength),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Calculate RMS while preserving sign
|
|
631
|
+
const rms = Math.sqrt(
|
|
632
|
+
pointSamples.reduce((sum, sample) => {
|
|
633
|
+
const normalized = (sample - 128) / 128;
|
|
634
|
+
return sum + normalized * normalized;
|
|
635
|
+
}, 0) / pointSamples.length,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
// Get average sign of the samples to determine direction
|
|
639
|
+
const avgSign = Math.sign(
|
|
640
|
+
pointSamples.reduce((sum, sample) => sum + (sample - 128), 0),
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// Convert RMS back to byte range, preserving direction
|
|
644
|
+
points[i] = Math.min(255, Math.round(128 + avgSign * rms * 128));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.#byteTimeDomainCache.set(cacheKey, points);
|
|
648
|
+
return points;
|
|
700
649
|
} finally {
|
|
701
|
-
|
|
650
|
+
source.disconnect();
|
|
702
651
|
analyser.disconnect();
|
|
703
652
|
}
|
|
704
653
|
}),
|
|
705
654
|
);
|
|
706
655
|
|
|
656
|
+
// Combine frames with decay weighting
|
|
707
657
|
const frameLength = framesData[0]?.length ?? 0;
|
|
708
658
|
const smoothedData = new Uint8Array(frameLength);
|
|
709
659
|
|
|
710
|
-
// Combine frames with decay
|
|
711
660
|
for (let i = 0; i < frameLength; i++) {
|
|
712
661
|
let weightedSum = 0;
|
|
713
662
|
let weightSum = 0;
|
|
714
663
|
|
|
715
664
|
framesData.forEach((frame, frameIndex) => {
|
|
716
665
|
const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
|
|
717
|
-
|
|
718
|
-
weightedSum += frame[i]! * decayWeight;
|
|
666
|
+
weightedSum += (frame[i] ?? 0) * decayWeight;
|
|
719
667
|
weightSum += decayWeight;
|
|
720
668
|
});
|
|
721
669
|
|
|
722
670
|
smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
|
|
723
671
|
}
|
|
724
672
|
|
|
725
|
-
|
|
726
|
-
// No need to slice the data either since we want the full waveform
|
|
727
|
-
|
|
728
|
-
this.#byteTimeDomainCache.set(
|
|
729
|
-
smoothedKey,
|
|
730
|
-
smoothedData.slice(0, Math.floor(smoothedData.length * 0.8)),
|
|
731
|
-
);
|
|
673
|
+
this.#byteTimeDomainCache.set(smoothedKey, smoothedData);
|
|
732
674
|
return smoothedData;
|
|
733
675
|
},
|
|
734
676
|
});
|
|
@@ -741,8 +683,10 @@ export class EFMedia extends EFTargetable(
|
|
|
741
683
|
[
|
|
742
684
|
this.audioBufferTask.status,
|
|
743
685
|
this.currentSourceTimeMs,
|
|
744
|
-
this.fftSize,
|
|
745
|
-
this.fftDecay,
|
|
686
|
+
this.fftSize,
|
|
687
|
+
this.fftDecay,
|
|
688
|
+
this.fftGain,
|
|
689
|
+
this.shouldInterpolateFrequencies,
|
|
746
690
|
] as const,
|
|
747
691
|
task: async () => {
|
|
748
692
|
await this.audioBufferTask.taskComplete;
|
|
@@ -752,7 +696,7 @@ export class EFMedia extends EFTargetable(
|
|
|
752
696
|
const currentTimeMs = this.currentSourceTimeMs;
|
|
753
697
|
const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
|
|
754
698
|
const audioBuffer = this.audioBufferTask.value.buffer;
|
|
755
|
-
const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
|
|
699
|
+
const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
|
|
756
700
|
|
|
757
701
|
const cachedSmoothedData = this.#frequencyDataCache.get(smoothedKey);
|
|
758
702
|
if (cachedSmoothedData) {
|
|
@@ -768,7 +712,7 @@ export class EFMedia extends EFTargetable(
|
|
|
768
712
|
);
|
|
769
713
|
|
|
770
714
|
// Cache key for this specific frame
|
|
771
|
-
const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
|
|
715
|
+
const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
|
|
772
716
|
|
|
773
717
|
// Check cache for this specific frame
|
|
774
718
|
const cachedFrame = this.#frequencyDataCache.get(cacheKey);
|
|
@@ -783,13 +727,23 @@ export class EFMedia extends EFTargetable(
|
|
|
783
727
|
);
|
|
784
728
|
const analyser = audioContext.createAnalyser();
|
|
785
729
|
analyser.fftSize = this.fftSize;
|
|
786
|
-
analyser.minDecibels =
|
|
787
|
-
analyser.maxDecibels =
|
|
730
|
+
analyser.minDecibels = -90;
|
|
731
|
+
analyser.maxDecibels = -10;
|
|
732
|
+
|
|
733
|
+
const gainNode = audioContext.createGain();
|
|
734
|
+
gainNode.gain.value = this.fftGain;
|
|
735
|
+
|
|
736
|
+
const filter = audioContext.createBiquadFilter();
|
|
737
|
+
filter.type = "bandpass";
|
|
738
|
+
filter.frequency.value = 15000;
|
|
739
|
+
filter.Q.value = 0.05;
|
|
788
740
|
|
|
789
741
|
const audioBufferSource = audioContext.createBufferSource();
|
|
790
742
|
audioBufferSource.buffer = audioBuffer;
|
|
791
743
|
|
|
792
|
-
audioBufferSource.connect(
|
|
744
|
+
audioBufferSource.connect(filter);
|
|
745
|
+
filter.connect(gainNode);
|
|
746
|
+
gainNode.connect(analyser);
|
|
793
747
|
analyser.connect(audioContext.destination);
|
|
794
748
|
|
|
795
749
|
audioBufferSource.start(0, startTime, 1 / 30);
|
|
@@ -819,7 +773,7 @@ export class EFMedia extends EFTargetable(
|
|
|
819
773
|
|
|
820
774
|
framesData.forEach((frame, frameIndex) => {
|
|
821
775
|
const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
|
|
822
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
776
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
823
777
|
weightedSum += frame[i]! * decayWeight;
|
|
824
778
|
weightSum += decayWeight;
|
|
825
779
|
});
|
|
@@ -829,7 +783,7 @@ export class EFMedia extends EFTargetable(
|
|
|
829
783
|
|
|
830
784
|
// Apply frequency weights using instance FREQ_WEIGHTS
|
|
831
785
|
smoothedData.forEach((value, i) => {
|
|
832
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
786
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
833
787
|
const freqWeight = this.FREQ_WEIGHTS[i]!;
|
|
834
788
|
smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
|
|
835
789
|
});
|
|
@@ -840,8 +794,91 @@ export class EFMedia extends EFTargetable(
|
|
|
840
794
|
0,
|
|
841
795
|
Math.floor(smoothedData.length / 2),
|
|
842
796
|
);
|
|
843
|
-
this
|
|
844
|
-
|
|
797
|
+
const processedData = this.shouldInterpolateFrequencies
|
|
798
|
+
? processFFTData(slicedData)
|
|
799
|
+
: slicedData;
|
|
800
|
+
this.#frequencyDataCache.set(smoothedKey, processedData);
|
|
801
|
+
return processedData;
|
|
845
802
|
},
|
|
846
803
|
});
|
|
804
|
+
|
|
805
|
+
set fftGain(value: number) {
|
|
806
|
+
const oldValue = this.fftGain;
|
|
807
|
+
this.setAttribute("fft-gain", String(value));
|
|
808
|
+
this.requestUpdate("fft-gain", oldValue);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
get fftGain() {
|
|
812
|
+
return Number.parseFloat(this.getAttribute("fft-gain") ?? "3.0");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function processFFTData(fftData: Uint8Array, zeroThresholdPercent = 0.1) {
|
|
817
|
+
// Step 1: Determine the threshold for zeros
|
|
818
|
+
const totalBins = fftData.length;
|
|
819
|
+
const zeroThresholdCount = Math.floor(totalBins * zeroThresholdPercent);
|
|
820
|
+
|
|
821
|
+
// Step 2: Interrogate the FFT output to find the cutoff point
|
|
822
|
+
let zeroCount = 0;
|
|
823
|
+
let cutoffIndex = totalBins; // Default to the end of the array
|
|
824
|
+
|
|
825
|
+
for (let i = totalBins - 1; i >= 0; i--) {
|
|
826
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
827
|
+
if (fftData[i]! < 10) {
|
|
828
|
+
zeroCount++;
|
|
829
|
+
} else {
|
|
830
|
+
// If we encounter a non-zero value, we can stop
|
|
831
|
+
if (zeroCount >= zeroThresholdCount) {
|
|
832
|
+
cutoffIndex = i + 1; // Include this index
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (cutoffIndex < zeroThresholdCount) {
|
|
839
|
+
return fftData;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Step 3: Resample the "good" portion of the data
|
|
843
|
+
const goodData = fftData.slice(0, cutoffIndex);
|
|
844
|
+
const resampledData = interpolateData(goodData, fftData.length);
|
|
845
|
+
|
|
846
|
+
// Step 4: Attenuate the top 10% of interpolated samples
|
|
847
|
+
const attenuationStartIndex = Math.floor(totalBins * 0.9);
|
|
848
|
+
for (let i = attenuationStartIndex; i < totalBins; i++) {
|
|
849
|
+
// Calculate attenuation factor that goes from 1 to 0 over the top 10%
|
|
850
|
+
const attenuationProgress =
|
|
851
|
+
(i - attenuationStartIndex) / (totalBins - attenuationStartIndex) + 0.2;
|
|
852
|
+
const attenuationFactor = Math.max(0, 1 - attenuationProgress);
|
|
853
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
854
|
+
resampledData[i] = Math.floor(resampledData[i]! * attenuationFactor);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return resampledData;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function interpolateData(data: Uint8Array, targetSize: number) {
|
|
861
|
+
const resampled = new Uint8Array(targetSize);
|
|
862
|
+
const dataLength = data.length;
|
|
863
|
+
|
|
864
|
+
for (let i = 0; i < targetSize; i++) {
|
|
865
|
+
// Calculate the corresponding index in the original data
|
|
866
|
+
const ratio = (i / (targetSize - 1)) * (dataLength - 1);
|
|
867
|
+
const index = Math.floor(ratio);
|
|
868
|
+
const fraction = ratio - index;
|
|
869
|
+
|
|
870
|
+
// Handle edge cases
|
|
871
|
+
if (index >= dataLength - 1) {
|
|
872
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
873
|
+
resampled[i] = data[dataLength - 1]!; // Last value
|
|
874
|
+
} else {
|
|
875
|
+
// Linear interpolation
|
|
876
|
+
resampled[i] = Math.round(
|
|
877
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
878
|
+
data[index]! * (1 - fraction) + data[index + 1]! * fraction,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return resampled;
|
|
847
884
|
}
|
|
@@ -18,12 +18,12 @@ export function EFSourceMixin<T extends Constructor<LitElement>>(
|
|
|
18
18
|
) {
|
|
19
19
|
class EFSourceElement extends superClass {
|
|
20
20
|
get apiHost() {
|
|
21
|
-
|
|
21
|
+
const apiHost =
|
|
22
22
|
this.closest("ef-configuration")?.apiHost ??
|
|
23
23
|
this.closest("ef-workbench")?.apiHost ??
|
|
24
|
-
this.closest("ef-preview")?.apiHost
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
this.closest("ef-preview")?.apiHost;
|
|
25
|
+
|
|
26
|
+
return apiHost || "https://editframe.dev";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
@property({ type: String })
|