@remotion/studio 4.0.452 → 4.0.453
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/audio-waveform-worker.d.ts +1 -0
- package/dist/audio-waveform-worker.js +102 -0
- package/dist/components/AudioWaveform.d.ts +2 -0
- package/dist/components/AudioWaveform.js +166 -18
- package/dist/components/Timeline/LoopedIndicator.js +5 -19
- package/dist/components/Timeline/TimelineSequence.js +18 -10
- package/dist/components/Timeline/TimelineVideoInfo.d.ts +2 -0
- package/dist/components/Timeline/TimelineVideoInfo.js +51 -12
- package/dist/components/audio-waveform-worker-types.d.ts +28 -0
- package/dist/components/audio-waveform-worker-types.js +2 -0
- package/dist/components/draw-peaks.d.ts +1 -1
- package/dist/components/load-waveform-peaks.d.ts +11 -1
- package/dist/components/load-waveform-peaks.js +22 -33
- package/dist/components/looped-media-timeline.d.ts +6 -0
- package/dist/components/looped-media-timeline.js +14 -0
- package/dist/components/slice-waveform-peaks.d.ts +7 -0
- package/dist/components/slice-waveform-peaks.js +15 -0
- package/dist/components/waveform-peak-processor.d.ts +23 -0
- package/dist/components/waveform-peak-processor.js +77 -0
- package/dist/esm/audio-waveform-worker.mjs +345 -0
- package/dist/esm/{chunk-hxr6txpe.js → chunk-hn4803e7.js} +398 -98
- package/dist/esm/internals.mjs +398 -98
- package/dist/esm/previewEntry.mjs +398 -98
- package/dist/esm/renderEntry.mjs +1 -1
- package/dist/helpers/calculate-timeline.js +16 -0
- package/dist/helpers/get-timeline-nestedness.js +2 -1
- package/dist/make-audio-waveform-worker.d.ts +1 -0
- package/dist/make-audio-waveform-worker.js +10 -0
- package/package.json +18 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const drawBars: (canvas: HTMLCanvasElement, peaks: Float32Array<ArrayBufferLike>, color: string, volume: number, width: number) => void;
|
|
1
|
+
export declare const drawBars: (canvas: HTMLCanvasElement | OffscreenCanvas, peaks: Float32Array<ArrayBufferLike>, color: string, volume: number, width: number) => void;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
declare const TARGET_SAMPLE_RATE = 100;
|
|
2
2
|
export { TARGET_SAMPLE_RATE };
|
|
3
|
-
|
|
3
|
+
type Progress = {
|
|
4
|
+
readonly peaks: Float32Array;
|
|
5
|
+
readonly completedPeaks: number;
|
|
6
|
+
readonly totalPeaks: number;
|
|
7
|
+
readonly final: boolean;
|
|
8
|
+
};
|
|
9
|
+
type LoadWaveformPeaksOptions = {
|
|
10
|
+
readonly onProgress?: (progress: Progress) => void;
|
|
11
|
+
readonly progressIntervalInMs?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function loadWaveformPeaks(url: string, signal: AbortSignal, options?: LoadWaveformPeaksOptions): Promise<Float32Array>;
|
|
@@ -3,14 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TARGET_SAMPLE_RATE = void 0;
|
|
4
4
|
exports.loadWaveformPeaks = loadWaveformPeaks;
|
|
5
5
|
const mediabunny_1 = require("mediabunny");
|
|
6
|
+
const waveform_peak_processor_1 = require("./waveform-peak-processor");
|
|
6
7
|
const TARGET_SAMPLE_RATE = 100;
|
|
7
8
|
exports.TARGET_SAMPLE_RATE = TARGET_SAMPLE_RATE;
|
|
9
|
+
const DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
8
10
|
const peaksCache = new Map();
|
|
9
|
-
async function loadWaveformPeaks(url, signal) {
|
|
11
|
+
async function loadWaveformPeaks(url, signal, options) {
|
|
10
12
|
var _a;
|
|
11
13
|
const cached = peaksCache.get(url);
|
|
12
|
-
if (cached)
|
|
14
|
+
if (cached) {
|
|
15
|
+
(0, waveform_peak_processor_1.emitWaveformProgress)({
|
|
16
|
+
peaks: cached,
|
|
17
|
+
completedPeaks: cached.length,
|
|
18
|
+
totalPeaks: cached.length,
|
|
19
|
+
final: true,
|
|
20
|
+
onProgress: options === null || options === void 0 ? void 0 : options.onProgress,
|
|
21
|
+
});
|
|
13
22
|
return cached;
|
|
23
|
+
}
|
|
14
24
|
const input = new mediabunny_1.Input({
|
|
15
25
|
formats: mediabunny_1.ALL_FORMATS,
|
|
16
26
|
source: new mediabunny_1.UrlSource(url),
|
|
@@ -24,11 +34,14 @@ async function loadWaveformPeaks(url, signal) {
|
|
|
24
34
|
const durationInSeconds = await audioTrack.computeDuration();
|
|
25
35
|
const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
|
|
26
36
|
const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
|
|
27
|
-
const peaks = new Float32Array(totalPeaks);
|
|
28
|
-
let peakIndex = 0;
|
|
29
|
-
let peakMax = 0;
|
|
30
|
-
let sampleInPeak = 0;
|
|
31
37
|
const sink = new mediabunny_1.AudioSampleSink(audioTrack);
|
|
38
|
+
const processor = (0, waveform_peak_processor_1.createWaveformPeakProcessor)({
|
|
39
|
+
totalPeaks,
|
|
40
|
+
samplesPerPeak,
|
|
41
|
+
onProgress: options === null || options === void 0 ? void 0 : options.onProgress,
|
|
42
|
+
progressIntervalInMs: (_a = options === null || options === void 0 ? void 0 : options.progressIntervalInMs) !== null && _a !== void 0 ? _a : DEFAULT_PROGRESS_INTERVAL_IN_MS,
|
|
43
|
+
now: () => Date.now(),
|
|
44
|
+
});
|
|
32
45
|
for await (const sample of sink.samples()) {
|
|
33
46
|
if (signal.aborted) {
|
|
34
47
|
sample.close();
|
|
@@ -41,35 +54,11 @@ async function loadWaveformPeaks(url, signal) {
|
|
|
41
54
|
const floats = new Float32Array(bytesNeeded / 4);
|
|
42
55
|
sample.copyTo(floats, { format: 'f32', planeIndex: 0 });
|
|
43
56
|
const channels = Math.max(1, sample.numberOfChannels);
|
|
44
|
-
const frames = sample.numberOfFrames;
|
|
45
57
|
sample.close();
|
|
46
|
-
|
|
47
|
-
// `f32` copies are interleaved, so timing must advance per frame, not per float.
|
|
48
|
-
let framePeak = 0;
|
|
49
|
-
for (let channel = 0; channel < channels; channel++) {
|
|
50
|
-
const sampleIndex = frame * channels + channel;
|
|
51
|
-
const abs = Math.abs((_a = floats[sampleIndex]) !== null && _a !== void 0 ? _a : 0);
|
|
52
|
-
if (abs > framePeak) {
|
|
53
|
-
framePeak = abs;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (framePeak > peakMax) {
|
|
57
|
-
peakMax = framePeak;
|
|
58
|
-
}
|
|
59
|
-
sampleInPeak++;
|
|
60
|
-
if (sampleInPeak >= samplesPerPeak) {
|
|
61
|
-
if (peakIndex < totalPeaks) {
|
|
62
|
-
peaks[peakIndex] = peakMax;
|
|
63
|
-
}
|
|
64
|
-
peakIndex++;
|
|
65
|
-
peakMax = 0;
|
|
66
|
-
sampleInPeak = 0;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (sampleInPeak > 0 && peakIndex < totalPeaks) {
|
|
71
|
-
peaks[peakIndex] = peakMax;
|
|
58
|
+
processor.processSampleChunk(floats, channels);
|
|
72
59
|
}
|
|
60
|
+
processor.finalize();
|
|
61
|
+
const { peaks } = processor;
|
|
73
62
|
peaksCache.set(url, peaks);
|
|
74
63
|
return peaks;
|
|
75
64
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LoopDisplay } from 'remotion';
|
|
2
|
+
export declare const shouldTileLoopDisplay: (loopDisplay: LoopDisplay | undefined) => loopDisplay is LoopDisplay;
|
|
3
|
+
export declare const getLoopDisplayWidth: ({ visualizationWidth, loopDisplay, }: {
|
|
4
|
+
visualizationWidth: number;
|
|
5
|
+
loopDisplay: LoopDisplay | undefined;
|
|
6
|
+
}) => number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLoopDisplayWidth = exports.shouldTileLoopDisplay = void 0;
|
|
4
|
+
const shouldTileLoopDisplay = (loopDisplay) => {
|
|
5
|
+
return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
|
|
6
|
+
};
|
|
7
|
+
exports.shouldTileLoopDisplay = shouldTileLoopDisplay;
|
|
8
|
+
const getLoopDisplayWidth = ({ visualizationWidth, loopDisplay, }) => {
|
|
9
|
+
if (!(0, exports.shouldTileLoopDisplay)(loopDisplay)) {
|
|
10
|
+
return visualizationWidth;
|
|
11
|
+
}
|
|
12
|
+
return visualizationWidth / loopDisplay.numberOfTimes;
|
|
13
|
+
};
|
|
14
|
+
exports.getLoopDisplayWidth = getLoopDisplayWidth;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const sliceWaveformPeaks: ({ durationInFrames, fps, peaks, playbackRate, startFrom, }: {
|
|
2
|
+
readonly peaks: Float32Array<ArrayBufferLike>;
|
|
3
|
+
readonly startFrom: number;
|
|
4
|
+
readonly durationInFrames: number;
|
|
5
|
+
readonly fps: number;
|
|
6
|
+
readonly playbackRate: number;
|
|
7
|
+
}) => Float32Array<ArrayBufferLike>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sliceWaveformPeaks = void 0;
|
|
4
|
+
const load_waveform_peaks_1 = require("./load-waveform-peaks");
|
|
5
|
+
const sliceWaveformPeaks = ({ durationInFrames, fps, peaks, playbackRate, startFrom, }) => {
|
|
6
|
+
if (peaks.length === 0) {
|
|
7
|
+
return peaks;
|
|
8
|
+
}
|
|
9
|
+
const startTimeInSeconds = startFrom / fps;
|
|
10
|
+
const durationInSeconds = (durationInFrames / fps) * playbackRate;
|
|
11
|
+
const startPeakIndex = Math.floor(startTimeInSeconds * load_waveform_peaks_1.TARGET_SAMPLE_RATE);
|
|
12
|
+
const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * load_waveform_peaks_1.TARGET_SAMPLE_RATE);
|
|
13
|
+
return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
|
|
14
|
+
};
|
|
15
|
+
exports.sliceWaveformPeaks = sliceWaveformPeaks;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Progress = {
|
|
2
|
+
readonly peaks: Float32Array;
|
|
3
|
+
readonly completedPeaks: number;
|
|
4
|
+
readonly totalPeaks: number;
|
|
5
|
+
readonly final: boolean;
|
|
6
|
+
};
|
|
7
|
+
type WaveformPeakProcessorOptions = {
|
|
8
|
+
readonly totalPeaks: number;
|
|
9
|
+
readonly samplesPerPeak: number;
|
|
10
|
+
readonly onProgress?: (progress: Progress) => void;
|
|
11
|
+
readonly progressIntervalInMs: number;
|
|
12
|
+
readonly now: () => number;
|
|
13
|
+
};
|
|
14
|
+
type WaveformPeakProcessor = {
|
|
15
|
+
readonly peaks: Float32Array;
|
|
16
|
+
processSampleChunk: (floats: Float32Array, channels: number) => void;
|
|
17
|
+
finalize: () => void;
|
|
18
|
+
};
|
|
19
|
+
export declare const emitWaveformProgress: ({ completedPeaks, final, onProgress, peaks, totalPeaks, }: Progress & {
|
|
20
|
+
readonly onProgress?: ((progress: Progress) => void) | undefined;
|
|
21
|
+
}) => void;
|
|
22
|
+
export declare const createWaveformPeakProcessor: ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }: WaveformPeakProcessorOptions) => WaveformPeakProcessor;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWaveformPeakProcessor = exports.emitWaveformProgress = void 0;
|
|
4
|
+
const emitWaveformProgress = ({ completedPeaks, final, onProgress, peaks, totalPeaks, }) => {
|
|
5
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
6
|
+
peaks,
|
|
7
|
+
completedPeaks,
|
|
8
|
+
totalPeaks,
|
|
9
|
+
final,
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
exports.emitWaveformProgress = emitWaveformProgress;
|
|
13
|
+
const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }) => {
|
|
14
|
+
const peaks = new Float32Array(totalPeaks);
|
|
15
|
+
let peakIndex = 0;
|
|
16
|
+
let peakMax = 0;
|
|
17
|
+
let sampleInPeak = 0;
|
|
18
|
+
let lastProgressAt = 0;
|
|
19
|
+
let lastProgressPeak = 0;
|
|
20
|
+
const emitProgress = (force) => {
|
|
21
|
+
const timestamp = now();
|
|
22
|
+
if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
lastProgressAt = timestamp;
|
|
29
|
+
lastProgressPeak = peakIndex;
|
|
30
|
+
(0, exports.emitWaveformProgress)({
|
|
31
|
+
peaks,
|
|
32
|
+
completedPeaks: peakIndex,
|
|
33
|
+
totalPeaks,
|
|
34
|
+
final: force,
|
|
35
|
+
onProgress,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
peaks,
|
|
40
|
+
processSampleChunk: (floats, channels) => {
|
|
41
|
+
var _a;
|
|
42
|
+
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
43
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
44
|
+
// `f32` copies are interleaved, so timing advances per frame.
|
|
45
|
+
let framePeak = 0;
|
|
46
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
47
|
+
const sampleIndex = frame * channels + channel;
|
|
48
|
+
const abs = Math.abs((_a = floats[sampleIndex]) !== null && _a !== void 0 ? _a : 0);
|
|
49
|
+
if (abs > framePeak) {
|
|
50
|
+
framePeak = abs;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (framePeak > peakMax) {
|
|
54
|
+
peakMax = framePeak;
|
|
55
|
+
}
|
|
56
|
+
sampleInPeak++;
|
|
57
|
+
if (sampleInPeak >= samplesPerPeak) {
|
|
58
|
+
if (peakIndex < totalPeaks) {
|
|
59
|
+
peaks[peakIndex] = peakMax;
|
|
60
|
+
}
|
|
61
|
+
peakIndex++;
|
|
62
|
+
peakMax = 0;
|
|
63
|
+
sampleInPeak = 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
emitProgress(false);
|
|
67
|
+
},
|
|
68
|
+
finalize: () => {
|
|
69
|
+
if (sampleInPeak > 0 && peakIndex < totalPeaks) {
|
|
70
|
+
peaks[peakIndex] = peakMax;
|
|
71
|
+
peakIndex++;
|
|
72
|
+
}
|
|
73
|
+
emitProgress(true);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
exports.createWaveformPeakProcessor = createWaveformPeakProcessor;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// src/components/parse-color.ts
|
|
2
|
+
var colorCache = new Map;
|
|
3
|
+
var parseColor = (color) => {
|
|
4
|
+
const cached = colorCache.get(color);
|
|
5
|
+
if (cached)
|
|
6
|
+
return cached;
|
|
7
|
+
const ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
|
8
|
+
ctx.fillStyle = color;
|
|
9
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
10
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
11
|
+
const result = [r, g, b, a];
|
|
12
|
+
colorCache.set(color, result);
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/components/draw-peaks.ts
|
|
17
|
+
var CLIPPING_COLOR = "#FF7F50";
|
|
18
|
+
var drawBars = (canvas, peaks, color, volume, width) => {
|
|
19
|
+
const ctx = canvas.getContext("2d");
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error("Failed to get canvas context");
|
|
22
|
+
}
|
|
23
|
+
const { height } = canvas;
|
|
24
|
+
const w = canvas.width;
|
|
25
|
+
ctx.clearRect(0, 0, w, height);
|
|
26
|
+
if (volume === 0)
|
|
27
|
+
return;
|
|
28
|
+
const [r, g, b, a] = parseColor(color);
|
|
29
|
+
const [cr, cg, cb, ca] = parseColor(CLIPPING_COLOR);
|
|
30
|
+
const imageData = ctx.createImageData(w, height);
|
|
31
|
+
const { data } = imageData;
|
|
32
|
+
const numBars = width;
|
|
33
|
+
for (let barIndex = 0;barIndex < numBars; barIndex++) {
|
|
34
|
+
const x = barIndex;
|
|
35
|
+
if (x >= w)
|
|
36
|
+
break;
|
|
37
|
+
const peakIndex = Math.floor(barIndex / numBars * peaks.length);
|
|
38
|
+
const peak = peaks[peakIndex] || 0;
|
|
39
|
+
const scaledPeak = peak * volume;
|
|
40
|
+
const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * height / 2));
|
|
41
|
+
if (halfBar === 0)
|
|
42
|
+
continue;
|
|
43
|
+
const mid = height / 2;
|
|
44
|
+
const barY = Math.round(mid - halfBar);
|
|
45
|
+
const barEnd = Math.round(mid + halfBar);
|
|
46
|
+
const isClipping = scaledPeak > 1;
|
|
47
|
+
const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
|
|
48
|
+
const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
|
|
49
|
+
for (let y = barY;y < clipTopEnd; y++) {
|
|
50
|
+
const idx = (y * w + x) * 4;
|
|
51
|
+
data[idx] = cr;
|
|
52
|
+
data[idx + 1] = cg;
|
|
53
|
+
data[idx + 2] = cb;
|
|
54
|
+
data[idx + 3] = ca;
|
|
55
|
+
}
|
|
56
|
+
for (let y = clipTopEnd;y < clipBotStart; y++) {
|
|
57
|
+
const idx = (y * w + x) * 4;
|
|
58
|
+
data[idx] = r;
|
|
59
|
+
data[idx + 1] = g;
|
|
60
|
+
data[idx + 2] = b;
|
|
61
|
+
data[idx + 3] = a;
|
|
62
|
+
}
|
|
63
|
+
for (let y = clipBotStart;y < barEnd; y++) {
|
|
64
|
+
const idx = (y * w + x) * 4;
|
|
65
|
+
data[idx] = cr;
|
|
66
|
+
data[idx + 1] = cg;
|
|
67
|
+
data[idx + 2] = cb;
|
|
68
|
+
data[idx + 3] = ca;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
ctx.putImageData(imageData, 0, 0);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/components/load-waveform-peaks.ts
|
|
75
|
+
import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
|
|
76
|
+
|
|
77
|
+
// src/components/waveform-peak-processor.ts
|
|
78
|
+
var emitWaveformProgress = ({
|
|
79
|
+
completedPeaks,
|
|
80
|
+
final,
|
|
81
|
+
onProgress,
|
|
82
|
+
peaks,
|
|
83
|
+
totalPeaks
|
|
84
|
+
}) => {
|
|
85
|
+
onProgress?.({
|
|
86
|
+
peaks,
|
|
87
|
+
completedPeaks,
|
|
88
|
+
totalPeaks,
|
|
89
|
+
final
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
var createWaveformPeakProcessor = ({
|
|
93
|
+
totalPeaks,
|
|
94
|
+
samplesPerPeak,
|
|
95
|
+
onProgress,
|
|
96
|
+
progressIntervalInMs,
|
|
97
|
+
now
|
|
98
|
+
}) => {
|
|
99
|
+
const peaks = new Float32Array(totalPeaks);
|
|
100
|
+
let peakIndex = 0;
|
|
101
|
+
let peakMax = 0;
|
|
102
|
+
let sampleInPeak = 0;
|
|
103
|
+
let lastProgressAt = 0;
|
|
104
|
+
let lastProgressPeak = 0;
|
|
105
|
+
const emitProgress = (force) => {
|
|
106
|
+
const timestamp = now();
|
|
107
|
+
if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
lastProgressAt = timestamp;
|
|
114
|
+
lastProgressPeak = peakIndex;
|
|
115
|
+
emitWaveformProgress({
|
|
116
|
+
peaks,
|
|
117
|
+
completedPeaks: peakIndex,
|
|
118
|
+
totalPeaks,
|
|
119
|
+
final: force,
|
|
120
|
+
onProgress
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
peaks,
|
|
125
|
+
processSampleChunk: (floats, channels) => {
|
|
126
|
+
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
127
|
+
for (let frame = 0;frame < frameCount; frame++) {
|
|
128
|
+
let framePeak = 0;
|
|
129
|
+
for (let channel = 0;channel < channels; channel++) {
|
|
130
|
+
const sampleIndex = frame * channels + channel;
|
|
131
|
+
const abs = Math.abs(floats[sampleIndex] ?? 0);
|
|
132
|
+
if (abs > framePeak) {
|
|
133
|
+
framePeak = abs;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (framePeak > peakMax) {
|
|
137
|
+
peakMax = framePeak;
|
|
138
|
+
}
|
|
139
|
+
sampleInPeak++;
|
|
140
|
+
if (sampleInPeak >= samplesPerPeak) {
|
|
141
|
+
if (peakIndex < totalPeaks) {
|
|
142
|
+
peaks[peakIndex] = peakMax;
|
|
143
|
+
}
|
|
144
|
+
peakIndex++;
|
|
145
|
+
peakMax = 0;
|
|
146
|
+
sampleInPeak = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
emitProgress(false);
|
|
150
|
+
},
|
|
151
|
+
finalize: () => {
|
|
152
|
+
if (sampleInPeak > 0 && peakIndex < totalPeaks) {
|
|
153
|
+
peaks[peakIndex] = peakMax;
|
|
154
|
+
peakIndex++;
|
|
155
|
+
}
|
|
156
|
+
emitProgress(true);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/components/load-waveform-peaks.ts
|
|
162
|
+
var TARGET_SAMPLE_RATE = 100;
|
|
163
|
+
var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
164
|
+
var peaksCache = new Map;
|
|
165
|
+
async function loadWaveformPeaks(url, signal, options) {
|
|
166
|
+
const cached = peaksCache.get(url);
|
|
167
|
+
if (cached) {
|
|
168
|
+
emitWaveformProgress({
|
|
169
|
+
peaks: cached,
|
|
170
|
+
completedPeaks: cached.length,
|
|
171
|
+
totalPeaks: cached.length,
|
|
172
|
+
final: true,
|
|
173
|
+
onProgress: options?.onProgress
|
|
174
|
+
});
|
|
175
|
+
return cached;
|
|
176
|
+
}
|
|
177
|
+
const input = new Input({
|
|
178
|
+
formats: ALL_FORMATS,
|
|
179
|
+
source: new UrlSource(url)
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
const audioTrack = await input.getPrimaryAudioTrack();
|
|
183
|
+
if (!audioTrack) {
|
|
184
|
+
return new Float32Array(0);
|
|
185
|
+
}
|
|
186
|
+
const { sampleRate } = audioTrack;
|
|
187
|
+
const durationInSeconds = await audioTrack.computeDuration();
|
|
188
|
+
const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
|
|
189
|
+
const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
|
|
190
|
+
const sink = new AudioSampleSink(audioTrack);
|
|
191
|
+
const processor = createWaveformPeakProcessor({
|
|
192
|
+
totalPeaks,
|
|
193
|
+
samplesPerPeak,
|
|
194
|
+
onProgress: options?.onProgress,
|
|
195
|
+
progressIntervalInMs: options?.progressIntervalInMs ?? DEFAULT_PROGRESS_INTERVAL_IN_MS,
|
|
196
|
+
now: () => Date.now()
|
|
197
|
+
});
|
|
198
|
+
for await (const sample of sink.samples()) {
|
|
199
|
+
if (signal.aborted) {
|
|
200
|
+
sample.close();
|
|
201
|
+
return new Float32Array(0);
|
|
202
|
+
}
|
|
203
|
+
const bytesNeeded = sample.allocationSize({
|
|
204
|
+
format: "f32",
|
|
205
|
+
planeIndex: 0
|
|
206
|
+
});
|
|
207
|
+
const floats = new Float32Array(bytesNeeded / 4);
|
|
208
|
+
sample.copyTo(floats, { format: "f32", planeIndex: 0 });
|
|
209
|
+
const channels = Math.max(1, sample.numberOfChannels);
|
|
210
|
+
sample.close();
|
|
211
|
+
processor.processSampleChunk(floats, channels);
|
|
212
|
+
}
|
|
213
|
+
processor.finalize();
|
|
214
|
+
const { peaks } = processor;
|
|
215
|
+
peaksCache.set(url, peaks);
|
|
216
|
+
return peaks;
|
|
217
|
+
} finally {
|
|
218
|
+
input.dispose();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/components/looped-media-timeline.ts
|
|
223
|
+
var shouldTileLoopDisplay = (loopDisplay) => {
|
|
224
|
+
return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
|
|
225
|
+
};
|
|
226
|
+
var getLoopDisplayWidth = ({
|
|
227
|
+
visualizationWidth,
|
|
228
|
+
loopDisplay
|
|
229
|
+
}) => {
|
|
230
|
+
if (!shouldTileLoopDisplay(loopDisplay)) {
|
|
231
|
+
return visualizationWidth;
|
|
232
|
+
}
|
|
233
|
+
return visualizationWidth / loopDisplay.numberOfTimes;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/components/slice-waveform-peaks.ts
|
|
237
|
+
var sliceWaveformPeaks = ({
|
|
238
|
+
durationInFrames,
|
|
239
|
+
fps,
|
|
240
|
+
peaks,
|
|
241
|
+
playbackRate,
|
|
242
|
+
startFrom
|
|
243
|
+
}) => {
|
|
244
|
+
if (peaks.length === 0) {
|
|
245
|
+
return peaks;
|
|
246
|
+
}
|
|
247
|
+
const startTimeInSeconds = startFrom / fps;
|
|
248
|
+
const durationInSeconds = durationInFrames / fps * playbackRate;
|
|
249
|
+
const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
|
|
250
|
+
const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
|
|
251
|
+
return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/audio-waveform-worker.ts
|
|
255
|
+
var canvas = null;
|
|
256
|
+
var currentController = null;
|
|
257
|
+
var latestRequestId = 0;
|
|
258
|
+
var postError = (requestId, error) => {
|
|
259
|
+
const message = error instanceof Error ? error.message : "Failed to render waveform";
|
|
260
|
+
const payload = {
|
|
261
|
+
type: "error",
|
|
262
|
+
requestId,
|
|
263
|
+
message
|
|
264
|
+
};
|
|
265
|
+
self.postMessage(payload);
|
|
266
|
+
};
|
|
267
|
+
var drawPartialWaveform = (message, peaks) => {
|
|
268
|
+
if (!canvas) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const portionPeaks = sliceWaveformPeaks({
|
|
272
|
+
durationInFrames: shouldTileLoopDisplay(message.loopDisplay) ? message.loopDisplay.durationInFrames : message.durationInFrames,
|
|
273
|
+
fps: message.fps,
|
|
274
|
+
peaks,
|
|
275
|
+
playbackRate: message.playbackRate,
|
|
276
|
+
startFrom: message.startFrom
|
|
277
|
+
});
|
|
278
|
+
if (!shouldTileLoopDisplay(message.loopDisplay)) {
|
|
279
|
+
drawBars(canvas, portionPeaks, "rgba(255, 255, 255, 0.6)", message.volume, message.width);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const loopWidth = getLoopDisplayWidth({
|
|
283
|
+
visualizationWidth: message.width,
|
|
284
|
+
loopDisplay: message.loopDisplay
|
|
285
|
+
});
|
|
286
|
+
const targetCanvas = new OffscreenCanvas(Math.max(1, Math.ceil(loopWidth)), message.height);
|
|
287
|
+
drawBars(targetCanvas, portionPeaks, "rgba(255, 255, 255, 0.6)", message.volume, targetCanvas.width);
|
|
288
|
+
const ctx = canvas.getContext("2d");
|
|
289
|
+
if (!ctx) {
|
|
290
|
+
throw new Error("Failed to get canvas context");
|
|
291
|
+
}
|
|
292
|
+
const pattern = ctx.createPattern(targetCanvas, "repeat-x");
|
|
293
|
+
if (!pattern) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
|
|
297
|
+
ctx.clearRect(0, 0, message.width, message.height);
|
|
298
|
+
ctx.fillStyle = pattern;
|
|
299
|
+
ctx.fillRect(0, 0, message.width, message.height);
|
|
300
|
+
};
|
|
301
|
+
var renderWaveform = async (message) => {
|
|
302
|
+
if (!canvas) {
|
|
303
|
+
postError(message.requestId, new Error("Waveform canvas not initialized"));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const controller = new AbortController;
|
|
307
|
+
currentController?.abort();
|
|
308
|
+
currentController = controller;
|
|
309
|
+
latestRequestId = message.requestId;
|
|
310
|
+
try {
|
|
311
|
+
canvas.width = message.width;
|
|
312
|
+
canvas.height = message.height;
|
|
313
|
+
const peaks = await loadWaveformPeaks(message.src, controller.signal, {
|
|
314
|
+
onProgress: ({ peaks: nextPeaks }) => {
|
|
315
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
drawPartialWaveform(message, nextPeaks);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
drawPartialWaveform(message, peaks);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
postError(message.requestId, error);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
self.addEventListener("message", (event) => {
|
|
333
|
+
const message = event.data;
|
|
334
|
+
if (message.type === "init") {
|
|
335
|
+
canvas = message.canvas;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (message.type === "dispose") {
|
|
339
|
+
currentController?.abort();
|
|
340
|
+
currentController = null;
|
|
341
|
+
canvas = null;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
renderWaveform(message);
|
|
345
|
+
});
|