@editframe/elements 0.15.0-beta.9 → 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.
- 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 +164 -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 +13 -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 +220 -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 +18 -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,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
|
-
|
|
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");
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 -=
|
|
406
|
+
fromMs -=
|
|
407
|
+
this.startTimeMs - (this.trimStartMs ?? 0) - (this.sourceInMs ?? 0);
|
|
504
408
|
}
|
|
505
409
|
if (this.sourceOutMs) {
|
|
506
|
-
toMs -=
|
|
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
|
-
|
|
588
|
-
|
|
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
|
-
|
|
591
|
-
|
|
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
|
|
@@ -626,6 +560,8 @@ export class EFMedia extends EFTargetable(
|
|
|
626
560
|
this.currentSourceTimeMs,
|
|
627
561
|
this.fftSize,
|
|
628
562
|
this.fftDecay,
|
|
563
|
+
this.fftGain,
|
|
564
|
+
this.shouldInterpolateFrequencies,
|
|
629
565
|
] as const,
|
|
630
566
|
task: async () => {
|
|
631
567
|
await this.audioBufferTask.taskComplete;
|
|
@@ -635,100 +571,107 @@ export class EFMedia extends EFTargetable(
|
|
|
635
571
|
const currentTimeMs = this.currentSourceTimeMs;
|
|
636
572
|
const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
|
|
637
573
|
const audioBuffer = this.audioBufferTask.value.buffer;
|
|
638
|
-
const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
|
|
639
574
|
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
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;
|
|
644
578
|
|
|
579
|
+
// Process multiple frames with decay, similar to the reference code
|
|
645
580
|
const framesData = await Promise.all(
|
|
646
|
-
Array.from({ length: this.fftDecay }, async (_,
|
|
647
|
-
const frameOffset =
|
|
581
|
+
Array.from({ length: this.fftDecay }, async (_, frameIndex) => {
|
|
582
|
+
const frameOffset = frameIndex * (1000 / 30);
|
|
648
583
|
const startTime = Math.max(
|
|
649
584
|
0,
|
|
650
585
|
(currentTimeMs - frameOffset - startOffsetMs) / 1000,
|
|
651
586
|
);
|
|
652
587
|
|
|
653
|
-
const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
|
|
588
|
+
const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
|
|
654
589
|
const cachedFrame = this.#byteTimeDomainCache.get(cacheKey);
|
|
655
|
-
if (cachedFrame)
|
|
656
|
-
return cachedFrame;
|
|
657
|
-
}
|
|
590
|
+
if (cachedFrame) return cachedFrame;
|
|
658
591
|
|
|
659
592
|
const audioContext = new OfflineAudioContext(
|
|
660
593
|
2,
|
|
661
594
|
48000 * (1 / 30),
|
|
662
595
|
48000,
|
|
663
596
|
);
|
|
664
|
-
const analyser = audioContext.createAnalyser();
|
|
665
|
-
analyser.fftSize = this.fftSize;
|
|
666
597
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
gainNode.gain.value = 10.0; // Try a higher gain
|
|
598
|
+
const source = audioContext.createBufferSource();
|
|
599
|
+
source.buffer = audioBuffer;
|
|
670
600
|
|
|
671
|
-
//
|
|
672
|
-
analyser
|
|
601
|
+
// Create analyzer for PCM data
|
|
602
|
+
const analyser = audioContext.createAnalyser();
|
|
603
|
+
analyser.fftSize = this.fftSize; // Ensure enough samples
|
|
673
604
|
analyser.minDecibels = -90;
|
|
674
|
-
analyser.maxDecibels = -
|
|
675
|
-
|
|
676
|
-
const audioBufferSource = audioContext.createBufferSource();
|
|
677
|
-
audioBufferSource.buffer = audioBuffer;
|
|
605
|
+
analyser.maxDecibels = -20;
|
|
678
606
|
|
|
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
|
|
607
|
+
const gainNode = audioContext.createGain();
|
|
608
|
+
gainNode.gain.value = this.fftGain; // Amplify the signal
|
|
684
609
|
|
|
685
|
-
|
|
686
|
-
gainNode.connect(
|
|
687
|
-
filter.connect(analyser);
|
|
610
|
+
source.connect(gainNode);
|
|
611
|
+
gainNode.connect(analyser);
|
|
688
612
|
analyser.connect(audioContext.destination);
|
|
689
613
|
|
|
690
|
-
|
|
614
|
+
source.start(0, startTime, 1 / 30);
|
|
691
615
|
|
|
616
|
+
const dataLength = analyser.fftSize / 2;
|
|
692
617
|
try {
|
|
693
618
|
await audioContext.startRendering();
|
|
694
|
-
|
|
695
|
-
const frameData = new Uint8Array(analyser.fftSize);
|
|
619
|
+
const frameData = new Uint8Array(dataLength);
|
|
696
620
|
analyser.getByteTimeDomainData(frameData);
|
|
697
621
|
|
|
698
|
-
|
|
699
|
-
|
|
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;
|
|
700
650
|
} finally {
|
|
701
|
-
|
|
651
|
+
source.disconnect();
|
|
702
652
|
analyser.disconnect();
|
|
703
653
|
}
|
|
704
654
|
}),
|
|
705
655
|
);
|
|
706
656
|
|
|
657
|
+
// Combine frames with decay weighting
|
|
707
658
|
const frameLength = framesData[0]?.length ?? 0;
|
|
708
659
|
const smoothedData = new Uint8Array(frameLength);
|
|
709
660
|
|
|
710
|
-
// Combine frames with decay
|
|
711
661
|
for (let i = 0; i < frameLength; i++) {
|
|
712
662
|
let weightedSum = 0;
|
|
713
663
|
let weightSum = 0;
|
|
714
664
|
|
|
715
665
|
framesData.forEach((frame, frameIndex) => {
|
|
716
666
|
const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
|
|
717
|
-
|
|
718
|
-
weightedSum += frame[i]! * decayWeight;
|
|
667
|
+
weightedSum += (frame[i] ?? 0) * decayWeight;
|
|
719
668
|
weightSum += decayWeight;
|
|
720
669
|
});
|
|
721
670
|
|
|
722
671
|
smoothedData[i] = Math.min(255, Math.round(weightedSum / weightSum));
|
|
723
672
|
}
|
|
724
673
|
|
|
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
|
-
);
|
|
674
|
+
this.#byteTimeDomainCache.set(smoothedKey, smoothedData);
|
|
732
675
|
return smoothedData;
|
|
733
676
|
},
|
|
734
677
|
});
|
|
@@ -741,8 +684,10 @@ export class EFMedia extends EFTargetable(
|
|
|
741
684
|
[
|
|
742
685
|
this.audioBufferTask.status,
|
|
743
686
|
this.currentSourceTimeMs,
|
|
744
|
-
this.fftSize,
|
|
745
|
-
this.fftDecay,
|
|
687
|
+
this.fftSize,
|
|
688
|
+
this.fftDecay,
|
|
689
|
+
this.fftGain,
|
|
690
|
+
this.shouldInterpolateFrequencies,
|
|
746
691
|
] as const,
|
|
747
692
|
task: async () => {
|
|
748
693
|
await this.audioBufferTask.taskComplete;
|
|
@@ -752,7 +697,7 @@ export class EFMedia extends EFTargetable(
|
|
|
752
697
|
const currentTimeMs = this.currentSourceTimeMs;
|
|
753
698
|
const startOffsetMs = this.audioBufferTask.value.startOffsetMs;
|
|
754
699
|
const audioBuffer = this.audioBufferTask.value.buffer;
|
|
755
|
-
const smoothedKey = `${this.fftSize}:${this.fftDecay}:${startOffsetMs}:${currentTimeMs}`;
|
|
700
|
+
const smoothedKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftDecay}:${this.fftGain}:${startOffsetMs}:${currentTimeMs}`;
|
|
756
701
|
|
|
757
702
|
const cachedSmoothedData = this.#frequencyDataCache.get(smoothedKey);
|
|
758
703
|
if (cachedSmoothedData) {
|
|
@@ -768,7 +713,7 @@ export class EFMedia extends EFTargetable(
|
|
|
768
713
|
);
|
|
769
714
|
|
|
770
715
|
// Cache key for this specific frame
|
|
771
|
-
const cacheKey = `${this.fftSize}:${startOffsetMs}:${startTime}`;
|
|
716
|
+
const cacheKey = `${this.shouldInterpolateFrequencies}:${this.fftSize}:${this.fftGain}:${startOffsetMs}:${startTime}`;
|
|
772
717
|
|
|
773
718
|
// Check cache for this specific frame
|
|
774
719
|
const cachedFrame = this.#frequencyDataCache.get(cacheKey);
|
|
@@ -783,13 +728,23 @@ export class EFMedia extends EFTargetable(
|
|
|
783
728
|
);
|
|
784
729
|
const analyser = audioContext.createAnalyser();
|
|
785
730
|
analyser.fftSize = this.fftSize;
|
|
786
|
-
analyser.minDecibels =
|
|
787
|
-
analyser.maxDecibels =
|
|
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;
|
|
788
741
|
|
|
789
742
|
const audioBufferSource = audioContext.createBufferSource();
|
|
790
743
|
audioBufferSource.buffer = audioBuffer;
|
|
791
744
|
|
|
792
|
-
audioBufferSource.connect(
|
|
745
|
+
audioBufferSource.connect(filter);
|
|
746
|
+
filter.connect(gainNode);
|
|
747
|
+
gainNode.connect(analyser);
|
|
793
748
|
analyser.connect(audioContext.destination);
|
|
794
749
|
|
|
795
750
|
audioBufferSource.start(0, startTime, 1 / 30);
|
|
@@ -819,7 +774,7 @@ export class EFMedia extends EFTargetable(
|
|
|
819
774
|
|
|
820
775
|
framesData.forEach((frame, frameIndex) => {
|
|
821
776
|
const decayWeight = EFMedia.DECAY_WEIGHT ** frameIndex;
|
|
822
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
777
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
823
778
|
weightedSum += frame[i]! * decayWeight;
|
|
824
779
|
weightSum += decayWeight;
|
|
825
780
|
});
|
|
@@ -829,7 +784,7 @@ export class EFMedia extends EFTargetable(
|
|
|
829
784
|
|
|
830
785
|
// Apply frequency weights using instance FREQ_WEIGHTS
|
|
831
786
|
smoothedData.forEach((value, i) => {
|
|
832
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
787
|
+
// biome-ignore lint/style/noNonNullAssertion: Manual bounds check
|
|
833
788
|
const freqWeight = this.FREQ_WEIGHTS[i]!;
|
|
834
789
|
smoothedData[i] = Math.min(255, Math.round(value * freqWeight));
|
|
835
790
|
});
|
|
@@ -840,8 +795,91 @@ export class EFMedia extends EFTargetable(
|
|
|
840
795
|
0,
|
|
841
796
|
Math.floor(smoothedData.length / 2),
|
|
842
797
|
);
|
|
843
|
-
this
|
|
844
|
-
|
|
798
|
+
const processedData = this.shouldInterpolateFrequencies
|
|
799
|
+
? processFFTData(slicedData)
|
|
800
|
+
: slicedData;
|
|
801
|
+
this.#frequencyDataCache.set(smoothedKey, processedData);
|
|
802
|
+
return processedData;
|
|
845
803
|
},
|
|
846
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;
|
|
847
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
|
-
|
|
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 })
|