@remotion/timeline-utils 4.0.522 → 4.0.524
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/audio-waveform-worker-types.d.ts +1 -0
- package/dist/audio-waveform/draw-peaks.js +10 -3
- package/dist/audio-waveform/get-waveform-cache-key.d.ts +2 -0
- package/dist/audio-waveform/get-waveform-cache-key.js +17 -0
- package/dist/audio-waveform/load-waveform-peaks.d.ts +7 -1
- package/dist/audio-waveform/load-waveform-peaks.js +24 -19
- package/dist/audio-waveform/subscribe-to-waveform-peaks.d.ts +3 -2
- package/dist/audio-waveform/subscribe-to-waveform-peaks.js +13 -12
- package/dist/audio-waveform/waveform-peak-processor.d.ts +4 -2
- package/dist/audio-waveform/waveform-peak-processor.js +13 -2
- package/dist/audio-waveform-worker.js +5 -4
- package/dist/esm/audio-waveform-worker.mjs +54 -24
- package/dist/esm/index.mjs +74 -35
- package/dist/format-average-audio-volume.d.ts +1 -0
- package/dist/format-average-audio-volume.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/package.json +5 -5
|
@@ -10,6 +10,7 @@ export type AudioWaveformWorkerPeaksMessage = {
|
|
|
10
10
|
readonly requestId: number;
|
|
11
11
|
readonly peaks: Float32Array;
|
|
12
12
|
readonly final: boolean;
|
|
13
|
+
readonly averageVolume: number | null;
|
|
13
14
|
};
|
|
14
15
|
export type AudioWaveformWorkerErrorMessage = {
|
|
15
16
|
readonly type: 'error';
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.drawBars = void 0;
|
|
4
4
|
const parse_color_1 = require("./parse-color");
|
|
5
5
|
const CLIPPING_COLOR = '#FF7F50';
|
|
6
|
+
const WAVEFORM_HEIGHT_SCALE = 0.8;
|
|
6
7
|
const getVolumeAtBar = ({ barIndex, numBars, volume, }) => {
|
|
7
8
|
var _a;
|
|
8
9
|
if (typeof volume === 'number') {
|
|
@@ -37,6 +38,7 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
|
|
|
37
38
|
const imageData = ctx.createImageData(w, height);
|
|
38
39
|
const { data } = imageData;
|
|
39
40
|
const numBars = width;
|
|
41
|
+
const fullScaleHalfBar = (height * WAVEFORM_HEIGHT_SCALE) / 2;
|
|
40
42
|
for (let barIndex = 0; barIndex < numBars; barIndex++) {
|
|
41
43
|
const x = barIndex;
|
|
42
44
|
if (x >= w)
|
|
@@ -45,15 +47,20 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
|
|
|
45
47
|
const peak = peaks[peakIndex] || 0;
|
|
46
48
|
const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
|
|
47
49
|
const scaledPeak = peak * barVolume;
|
|
48
|
-
const halfBar = Math.max(0, Math.min(height / 2,
|
|
50
|
+
const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * fullScaleHalfBar));
|
|
49
51
|
if (halfBar === 0)
|
|
50
52
|
continue;
|
|
51
53
|
const mid = height / 2;
|
|
52
54
|
const barY = Math.round(mid - halfBar);
|
|
53
55
|
const barEnd = Math.round(mid + halfBar);
|
|
54
56
|
const isClipping = scaledPeak > 1;
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
+
// Color the overflow above full scale, keeping tiny overshoots visible.
|
|
58
|
+
const clipTopEnd = isClipping
|
|
59
|
+
? Math.min(Math.max(barY + 2, Math.round(mid - fullScaleHalfBar)), barEnd)
|
|
60
|
+
: barY;
|
|
61
|
+
const clipBotStart = isClipping
|
|
62
|
+
? Math.max(Math.min(barEnd - 2, Math.round(mid + fullScaleHalfBar)), barY)
|
|
63
|
+
: barEnd;
|
|
57
64
|
for (let y = barY; y < clipTopEnd; y++) {
|
|
58
65
|
const idx = (y * w + x) * 4;
|
|
59
66
|
data[idx] = cr;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getWaveformCacheKey = void 0;
|
|
4
|
+
const trackIds = new WeakMap();
|
|
5
|
+
let nextTrackId = 0;
|
|
6
|
+
const getWaveformCacheKey = (src, waveformSampleRate) => {
|
|
7
|
+
if (typeof src === 'string') {
|
|
8
|
+
return `${waveformSampleRate}:url:${src}`;
|
|
9
|
+
}
|
|
10
|
+
let id = trackIds.get(src);
|
|
11
|
+
if (id === undefined) {
|
|
12
|
+
id = nextTrackId++;
|
|
13
|
+
trackIds.set(src, id);
|
|
14
|
+
}
|
|
15
|
+
return `${waveformSampleRate}:track:${id}`;
|
|
16
|
+
};
|
|
17
|
+
exports.getWaveformCacheKey = getWaveformCacheKey;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import type { InputAudioTrack } from 'mediabunny';
|
|
1
2
|
import { TARGET_SAMPLE_RATE } from './constants';
|
|
3
|
+
export type WaveformResult = {
|
|
4
|
+
readonly peaks: Float32Array;
|
|
5
|
+
readonly averageVolume: number | null;
|
|
6
|
+
};
|
|
2
7
|
export { TARGET_SAMPLE_RATE };
|
|
3
8
|
type Progress = {
|
|
9
|
+
readonly averageVolume: number | null;
|
|
4
10
|
readonly peaks: Float32Array;
|
|
5
11
|
readonly completedPeaks: number;
|
|
6
12
|
readonly totalPeaks: number;
|
|
@@ -11,4 +17,4 @@ type LoadWaveformPeaksOptions = {
|
|
|
11
17
|
readonly progressIntervalInMs?: number;
|
|
12
18
|
readonly waveformSampleRate?: number;
|
|
13
19
|
};
|
|
14
|
-
export declare function loadWaveformPeaks(
|
|
20
|
+
export declare function loadWaveformPeaks(src: string | InputAudioTrack, signal: AbortSignal, options?: LoadWaveformPeaksOptions): Promise<WaveformResult>;
|
|
@@ -5,44 +5,45 @@ exports.loadWaveformPeaks = loadWaveformPeaks;
|
|
|
5
5
|
const mediabunny_1 = require("mediabunny");
|
|
6
6
|
const constants_1 = require("./constants");
|
|
7
7
|
Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
|
|
8
|
+
const get_waveform_cache_key_1 = require("./get-waveform-cache-key");
|
|
8
9
|
const trim_audio_sample_before_zero_1 = require("./trim-audio-sample-before-zero");
|
|
9
10
|
const waveform_peak_processor_1 = require("./waveform-peak-processor");
|
|
10
11
|
const DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
11
12
|
const peaksCache = new Map();
|
|
12
|
-
async function loadWaveformPeaks(
|
|
13
|
+
async function loadWaveformPeaks(src, signal, options) {
|
|
13
14
|
var _a, _b, _c;
|
|
14
15
|
const waveformSampleRate = (_a = options === null || options === void 0 ? void 0 : options.waveformSampleRate) !== null && _a !== void 0 ? _a : constants_1.TARGET_SAMPLE_RATE;
|
|
15
16
|
if (!Number.isFinite(waveformSampleRate) || waveformSampleRate <= 0) {
|
|
16
17
|
throw new Error('The waveform sample rate must be a positive number.');
|
|
17
18
|
}
|
|
18
|
-
const cacheKey =
|
|
19
|
+
const cacheKey = (0, get_waveform_cache_key_1.getWaveformCacheKey)(src, waveformSampleRate);
|
|
19
20
|
const cached = peaksCache.get(cacheKey);
|
|
20
21
|
if (cached) {
|
|
21
22
|
(0, waveform_peak_processor_1.emitWaveformProgress)({
|
|
22
|
-
peaks: cached,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
peaks: cached.peaks,
|
|
24
|
+
averageVolume: cached.averageVolume,
|
|
25
|
+
completedPeaks: cached.peaks.length,
|
|
26
|
+
totalPeaks: cached.peaks.length,
|
|
25
27
|
final: true,
|
|
26
28
|
onProgress: options === null || options === void 0 ? void 0 : options.onProgress,
|
|
27
29
|
});
|
|
28
30
|
return cached;
|
|
29
31
|
}
|
|
30
|
-
const input =
|
|
31
|
-
formats: mediabunny_1.ALL_FORMATS,
|
|
32
|
-
|
|
33
|
-
});
|
|
32
|
+
const input = typeof src === 'string'
|
|
33
|
+
? new mediabunny_1.Input({ formats: mediabunny_1.ALL_FORMATS, source: new mediabunny_1.UrlSource(src) })
|
|
34
|
+
: null;
|
|
34
35
|
try {
|
|
35
|
-
const audioTrack = await input.getPrimaryAudioTrack();
|
|
36
|
+
const audioTrack = typeof src === 'string' ? await input.getPrimaryAudioTrack() : src;
|
|
36
37
|
if (!audioTrack) {
|
|
37
|
-
return new Float32Array(0);
|
|
38
|
+
return { peaks: new Float32Array(0), averageVolume: null };
|
|
38
39
|
}
|
|
39
40
|
if (await audioTrack.isLive()) {
|
|
40
41
|
throw new Error('Live streams are not currently supported by Remotion. Sorry! Source: ' +
|
|
41
|
-
|
|
42
|
+
(typeof src === 'string' ? src : `audio track ${src.id}`));
|
|
42
43
|
}
|
|
43
44
|
if (await audioTrack.isRelativeToUnixEpoch()) {
|
|
44
45
|
throw new Error('Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: ' +
|
|
45
|
-
|
|
46
|
+
(typeof src === 'string' ? src : `audio track ${src.id}`));
|
|
46
47
|
}
|
|
47
48
|
const audioSampleRate = await audioTrack.getSampleRate();
|
|
48
49
|
const durationInSeconds = (_b = (await audioTrack.getDurationFromMetadata({ skipLiveWait: true }))) !== null && _b !== void 0 ? _b : (await audioTrack.computeDuration({ skipLiveWait: true }));
|
|
@@ -60,7 +61,7 @@ async function loadWaveformPeaks(url, signal, options) {
|
|
|
60
61
|
for await (const sample of sink.samples()) {
|
|
61
62
|
if (signal.aborted) {
|
|
62
63
|
sample.close();
|
|
63
|
-
return new Float32Array(0);
|
|
64
|
+
return { peaks: new Float32Array(0), averageVolume: null };
|
|
64
65
|
}
|
|
65
66
|
const startFrame = (0, trim_audio_sample_before_zero_1.getAudioSampleStartFrameAtTimelineZero)(sample);
|
|
66
67
|
if (startFrame === null) {
|
|
@@ -86,15 +87,19 @@ async function loadWaveformPeaks(url, signal, options) {
|
|
|
86
87
|
frameCount,
|
|
87
88
|
});
|
|
88
89
|
const channels = Math.max(1, sample.numberOfChannels);
|
|
90
|
+
const { sampleRate } = sample;
|
|
89
91
|
sample.close();
|
|
90
|
-
processor.processSampleChunk(floats, channels);
|
|
92
|
+
processor.processSampleChunk(floats, channels, sampleRate);
|
|
91
93
|
}
|
|
92
94
|
processor.finalize();
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
const result = {
|
|
96
|
+
peaks: processor.peaks,
|
|
97
|
+
averageVolume: processor.averageVolume,
|
|
98
|
+
};
|
|
99
|
+
peaksCache.set(cacheKey, result);
|
|
100
|
+
return result;
|
|
96
101
|
}
|
|
97
102
|
finally {
|
|
98
|
-
input.dispose();
|
|
103
|
+
input === null || input === void 0 ? void 0 : input.dispose();
|
|
99
104
|
}
|
|
100
105
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { InputAudioTrack } from 'mediabunny';
|
|
1
2
|
export declare const subscribeToWaveformPeaks: ({ src, waveformSampleRate, onPeaks, onError, }: {
|
|
2
|
-
readonly src: string;
|
|
3
|
+
readonly src: string | InputAudioTrack;
|
|
3
4
|
readonly waveformSampleRate?: number | undefined;
|
|
4
|
-
readonly onPeaks: (peaks: Float32Array<ArrayBufferLike>, final: boolean) => void;
|
|
5
|
+
readonly onPeaks: (peaks: Float32Array<ArrayBufferLike>, final: boolean, averageVolume: number | null) => void;
|
|
5
6
|
readonly onError: (error: Error) => void;
|
|
6
7
|
}) => () => void;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.subscribeToWaveformPeaks = void 0;
|
|
4
4
|
const constants_1 = require("./constants");
|
|
5
|
+
const get_waveform_cache_key_1 = require("./get-waveform-cache-key");
|
|
5
6
|
const load_waveform_peaks_1 = require("./load-waveform-peaks");
|
|
6
7
|
const make_audio_waveform_worker_1 = require("./make-audio-waveform-worker");
|
|
7
8
|
// All AudioWaveform instances share one decode worker and one peaks cache,
|
|
@@ -13,9 +14,9 @@ const inFlightByRequestId = new Map();
|
|
|
13
14
|
let worker = null;
|
|
14
15
|
let workerFailed = false;
|
|
15
16
|
let nextRequestId = 0;
|
|
16
|
-
const emitPeaks = (load, peaks, final) => {
|
|
17
|
+
const emitPeaks = (load, peaks, final, averageVolume) => {
|
|
17
18
|
if (final) {
|
|
18
|
-
peaksCache.set(load.cacheKey, peaks);
|
|
19
|
+
peaksCache.set(load.cacheKey, { peaks, averageVolume });
|
|
19
20
|
load.latestPeaks = null;
|
|
20
21
|
inFlightByCacheKey.delete(load.cacheKey);
|
|
21
22
|
if (load.requestId !== null) {
|
|
@@ -26,7 +27,7 @@ const emitPeaks = (load, peaks, final) => {
|
|
|
26
27
|
load.latestPeaks = peaks;
|
|
27
28
|
}
|
|
28
29
|
for (const listener of load.listeners) {
|
|
29
|
-
listener.onPeaks(peaks, final);
|
|
30
|
+
listener.onPeaks(peaks, final, averageVolume);
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
33
|
const emitError = (load, error) => {
|
|
@@ -51,11 +52,11 @@ const startMainThreadLoad = (load) => {
|
|
|
51
52
|
}
|
|
52
53
|
// The processor mutates the same array in place; snapshot it so
|
|
53
54
|
// React state updates see a new reference.
|
|
54
|
-
emitPeaks(load, peaks.slice(), false);
|
|
55
|
+
emitPeaks(load, peaks.slice(), false, null);
|
|
55
56
|
},
|
|
56
57
|
})
|
|
57
|
-
.then((peaks) => {
|
|
58
|
-
emitPeaks(load, peaks, true);
|
|
58
|
+
.then(({ peaks, averageVolume }) => {
|
|
59
|
+
emitPeaks(load, peaks, true, averageVolume);
|
|
59
60
|
})
|
|
60
61
|
.catch((error) => {
|
|
61
62
|
emitError(load, error instanceof Error
|
|
@@ -97,7 +98,7 @@ const getOrCreateWorker = () => {
|
|
|
97
98
|
emitError(load, new Error(message.message));
|
|
98
99
|
return;
|
|
99
100
|
}
|
|
100
|
-
emitPeaks(load, message.peaks, message.final);
|
|
101
|
+
emitPeaks(load, message.peaks, message.final, message.averageVolume);
|
|
101
102
|
});
|
|
102
103
|
worker.addEventListener('error', (event) => {
|
|
103
104
|
event.preventDefault();
|
|
@@ -106,10 +107,10 @@ const getOrCreateWorker = () => {
|
|
|
106
107
|
return worker;
|
|
107
108
|
};
|
|
108
109
|
const subscribeToWaveformPeaks = ({ src, waveformSampleRate = constants_1.TARGET_SAMPLE_RATE, onPeaks, onError, }) => {
|
|
109
|
-
const cacheKey =
|
|
110
|
+
const cacheKey = (0, get_waveform_cache_key_1.getWaveformCacheKey)(src, waveformSampleRate);
|
|
110
111
|
const cached = peaksCache.get(cacheKey);
|
|
111
112
|
if (cached) {
|
|
112
|
-
onPeaks(cached, true);
|
|
113
|
+
onPeaks(cached.peaks, true, cached.averageVolume);
|
|
113
114
|
return () => undefined;
|
|
114
115
|
}
|
|
115
116
|
const listener = { onPeaks, onError };
|
|
@@ -117,7 +118,7 @@ const subscribeToWaveformPeaks = ({ src, waveformSampleRate = constants_1.TARGET
|
|
|
117
118
|
if (existing) {
|
|
118
119
|
existing.listeners.add(listener);
|
|
119
120
|
if (existing.latestPeaks) {
|
|
120
|
-
onPeaks(existing.latestPeaks, false);
|
|
121
|
+
onPeaks(existing.latestPeaks, false, null);
|
|
121
122
|
}
|
|
122
123
|
return () => {
|
|
123
124
|
existing.listeners.delete(listener);
|
|
@@ -132,8 +133,8 @@ const subscribeToWaveformPeaks = ({ src, waveformSampleRate = constants_1.TARGET
|
|
|
132
133
|
requestId: null,
|
|
133
134
|
};
|
|
134
135
|
inFlightByCacheKey.set(cacheKey, load);
|
|
135
|
-
const workerInstance = getOrCreateWorker();
|
|
136
|
-
if (workerInstance) {
|
|
136
|
+
const workerInstance = typeof src === 'string' ? getOrCreateWorker() : null;
|
|
137
|
+
if (workerInstance && typeof src === 'string') {
|
|
137
138
|
const requestId = nextRequestId++;
|
|
138
139
|
load.requestId = requestId;
|
|
139
140
|
inFlightByRequestId.set(requestId, load);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type Progress = {
|
|
2
2
|
readonly peaks: Float32Array;
|
|
3
|
+
readonly averageVolume: number | null;
|
|
3
4
|
readonly completedPeaks: number;
|
|
4
5
|
readonly totalPeaks: number;
|
|
5
6
|
readonly final: boolean;
|
|
@@ -13,10 +14,11 @@ type WaveformPeakProcessorOptions = {
|
|
|
13
14
|
};
|
|
14
15
|
type WaveformPeakProcessor = {
|
|
15
16
|
readonly peaks: Float32Array;
|
|
16
|
-
|
|
17
|
+
readonly averageVolume: number | null;
|
|
18
|
+
processSampleChunk: (floats: Float32Array, channels: number, sampleRate: number) => void;
|
|
17
19
|
finalize: () => void;
|
|
18
20
|
};
|
|
19
|
-
export declare const emitWaveformProgress: ({ completedPeaks, final, onProgress, peaks, totalPeaks, }: Progress & {
|
|
21
|
+
export declare const emitWaveformProgress: ({ averageVolume, completedPeaks, final, onProgress, peaks, totalPeaks, }: Progress & {
|
|
20
22
|
readonly onProgress?: ((progress: Progress) => void) | undefined;
|
|
21
23
|
}) => void;
|
|
22
24
|
export declare const createWaveformPeakProcessor: ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }: WaveformPeakProcessorOptions) => WaveformPeakProcessor;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWaveformPeakProcessor = exports.emitWaveformProgress = void 0;
|
|
4
|
-
const emitWaveformProgress = ({ completedPeaks, final, onProgress, peaks, totalPeaks, }) => {
|
|
4
|
+
const emitWaveformProgress = ({ averageVolume, completedPeaks, final, onProgress, peaks, totalPeaks, }) => {
|
|
5
5
|
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
6
|
+
averageVolume,
|
|
6
7
|
peaks,
|
|
7
8
|
completedPeaks,
|
|
8
9
|
totalPeaks,
|
|
@@ -12,6 +13,8 @@ const emitWaveformProgress = ({ completedPeaks, final, onProgress, peaks, totalP
|
|
|
12
13
|
exports.emitWaveformProgress = emitWaveformProgress;
|
|
13
14
|
const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }) => {
|
|
14
15
|
const peaks = new Float32Array(totalPeaks);
|
|
16
|
+
let energy = 0;
|
|
17
|
+
let duration = 0;
|
|
15
18
|
let peakIndex = 0;
|
|
16
19
|
let peakMax = 0;
|
|
17
20
|
let sampleInPeak = 0;
|
|
@@ -28,6 +31,7 @@ const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, p
|
|
|
28
31
|
lastProgressAt = timestamp;
|
|
29
32
|
lastProgressPeak = peakIndex;
|
|
30
33
|
(0, exports.emitWaveformProgress)({
|
|
34
|
+
averageVolume: force && duration > 0 ? 10 * Math.log10(energy / duration) : null,
|
|
31
35
|
peaks,
|
|
32
36
|
completedPeaks: peakIndex,
|
|
33
37
|
totalPeaks,
|
|
@@ -37,19 +41,26 @@ const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, p
|
|
|
37
41
|
};
|
|
38
42
|
return {
|
|
39
43
|
peaks,
|
|
40
|
-
|
|
44
|
+
get averageVolume() {
|
|
45
|
+
return duration > 0 ? 10 * Math.log10(energy / duration) : null;
|
|
46
|
+
},
|
|
47
|
+
processSampleChunk: (floats, channels, sampleRate) => {
|
|
41
48
|
var _a;
|
|
42
49
|
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
43
50
|
for (let frame = 0; frame < frameCount; frame++) {
|
|
44
51
|
// `f32` copies are interleaved, so timing advances per frame.
|
|
45
52
|
let framePeak = 0;
|
|
53
|
+
let frameEnergy = 0;
|
|
46
54
|
for (let channel = 0; channel < channels; channel++) {
|
|
47
55
|
const sampleIndex = frame * channels + channel;
|
|
48
56
|
const abs = Math.abs((_a = floats[sampleIndex]) !== null && _a !== void 0 ? _a : 0);
|
|
57
|
+
frameEnergy += abs * abs;
|
|
49
58
|
if (abs > framePeak) {
|
|
50
59
|
framePeak = abs;
|
|
51
60
|
}
|
|
52
61
|
}
|
|
62
|
+
energy += frameEnergy / channels / sampleRate;
|
|
63
|
+
duration += 1 / sampleRate;
|
|
53
64
|
if (framePeak > peakMax) {
|
|
54
65
|
peakMax = framePeak;
|
|
55
66
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
/// <reference lib="webworker" />
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
|
|
5
|
-
const postPeaks = (requestId, peaks, final) => {
|
|
5
|
+
const postPeaks = (requestId, peaks, final, averageVolume) => {
|
|
6
6
|
// Structured cloning copies the array, so the decoder can keep
|
|
7
7
|
// mutating its buffer while the main thread reads the snapshot.
|
|
8
8
|
const payload = {
|
|
9
9
|
type: 'peaks',
|
|
10
|
+
averageVolume,
|
|
10
11
|
requestId,
|
|
11
12
|
peaks,
|
|
12
13
|
final,
|
|
@@ -29,12 +30,12 @@ self.addEventListener('message', (event) => {
|
|
|
29
30
|
waveformSampleRate: message.waveformSampleRate,
|
|
30
31
|
onProgress: ({ peaks, final }) => {
|
|
31
32
|
if (!final) {
|
|
32
|
-
postPeaks(message.requestId, peaks, false);
|
|
33
|
+
postPeaks(message.requestId, peaks, false, null);
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
})
|
|
36
|
-
.then((peaks) => {
|
|
37
|
-
postPeaks(message.requestId, peaks, true);
|
|
37
|
+
.then(({ peaks, averageVolume }) => {
|
|
38
|
+
postPeaks(message.requestId, peaks, true, averageVolume);
|
|
38
39
|
})
|
|
39
40
|
.catch((error) => {
|
|
40
41
|
postError(message.requestId, error);
|
|
@@ -4,6 +4,21 @@ import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
|
|
|
4
4
|
// src/audio-waveform/constants.ts
|
|
5
5
|
var TARGET_SAMPLE_RATE = 100;
|
|
6
6
|
|
|
7
|
+
// src/audio-waveform/get-waveform-cache-key.ts
|
|
8
|
+
var trackIds = new WeakMap;
|
|
9
|
+
var nextTrackId = 0;
|
|
10
|
+
var getWaveformCacheKey = (src, waveformSampleRate) => {
|
|
11
|
+
if (typeof src === "string") {
|
|
12
|
+
return `${waveformSampleRate}:url:${src}`;
|
|
13
|
+
}
|
|
14
|
+
let id = trackIds.get(src);
|
|
15
|
+
if (id === undefined) {
|
|
16
|
+
id = nextTrackId++;
|
|
17
|
+
trackIds.set(src, id);
|
|
18
|
+
}
|
|
19
|
+
return `${waveformSampleRate}:track:${id}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
7
22
|
// src/audio-waveform/trim-audio-sample-before-zero.ts
|
|
8
23
|
var getAudioSampleStartFrameAtTimelineZero = (sample) => {
|
|
9
24
|
if (sample.timestamp + sample.duration <= 0) {
|
|
@@ -17,6 +32,7 @@ var getAudioSampleStartFrameAtTimelineZero = (sample) => {
|
|
|
17
32
|
|
|
18
33
|
// src/audio-waveform/waveform-peak-processor.ts
|
|
19
34
|
var emitWaveformProgress = ({
|
|
35
|
+
averageVolume,
|
|
20
36
|
completedPeaks,
|
|
21
37
|
final,
|
|
22
38
|
onProgress,
|
|
@@ -24,6 +40,7 @@ var emitWaveformProgress = ({
|
|
|
24
40
|
totalPeaks
|
|
25
41
|
}) => {
|
|
26
42
|
onProgress?.({
|
|
43
|
+
averageVolume,
|
|
27
44
|
peaks,
|
|
28
45
|
completedPeaks,
|
|
29
46
|
totalPeaks,
|
|
@@ -38,6 +55,8 @@ var createWaveformPeakProcessor = ({
|
|
|
38
55
|
now
|
|
39
56
|
}) => {
|
|
40
57
|
const peaks = new Float32Array(totalPeaks);
|
|
58
|
+
let energy = 0;
|
|
59
|
+
let duration = 0;
|
|
41
60
|
let peakIndex = 0;
|
|
42
61
|
let peakMax = 0;
|
|
43
62
|
let sampleInPeak = 0;
|
|
@@ -54,6 +73,7 @@ var createWaveformPeakProcessor = ({
|
|
|
54
73
|
lastProgressAt = timestamp;
|
|
55
74
|
lastProgressPeak = peakIndex;
|
|
56
75
|
emitWaveformProgress({
|
|
76
|
+
averageVolume: force && duration > 0 ? 10 * Math.log10(energy / duration) : null,
|
|
57
77
|
peaks,
|
|
58
78
|
completedPeaks: peakIndex,
|
|
59
79
|
totalPeaks,
|
|
@@ -63,17 +83,24 @@ var createWaveformPeakProcessor = ({
|
|
|
63
83
|
};
|
|
64
84
|
return {
|
|
65
85
|
peaks,
|
|
66
|
-
|
|
86
|
+
get averageVolume() {
|
|
87
|
+
return duration > 0 ? 10 * Math.log10(energy / duration) : null;
|
|
88
|
+
},
|
|
89
|
+
processSampleChunk: (floats, channels, sampleRate) => {
|
|
67
90
|
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
68
91
|
for (let frame = 0;frame < frameCount; frame++) {
|
|
69
92
|
let framePeak = 0;
|
|
93
|
+
let frameEnergy = 0;
|
|
70
94
|
for (let channel = 0;channel < channels; channel++) {
|
|
71
95
|
const sampleIndex = frame * channels + channel;
|
|
72
96
|
const abs = Math.abs(floats[sampleIndex] ?? 0);
|
|
97
|
+
frameEnergy += abs * abs;
|
|
73
98
|
if (abs > framePeak) {
|
|
74
99
|
framePeak = abs;
|
|
75
100
|
}
|
|
76
101
|
}
|
|
102
|
+
energy += frameEnergy / channels / sampleRate;
|
|
103
|
+
duration += 1 / sampleRate;
|
|
77
104
|
if (framePeak > peakMax) {
|
|
78
105
|
peakMax = framePeak;
|
|
79
106
|
}
|
|
@@ -102,37 +129,35 @@ var createWaveformPeakProcessor = ({
|
|
|
102
129
|
// src/audio-waveform/load-waveform-peaks.ts
|
|
103
130
|
var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
104
131
|
var peaksCache = new Map;
|
|
105
|
-
async function loadWaveformPeaks(
|
|
132
|
+
async function loadWaveformPeaks(src, signal, options) {
|
|
106
133
|
const waveformSampleRate = options?.waveformSampleRate ?? TARGET_SAMPLE_RATE;
|
|
107
134
|
if (!Number.isFinite(waveformSampleRate) || waveformSampleRate <= 0) {
|
|
108
135
|
throw new Error("The waveform sample rate must be a positive number.");
|
|
109
136
|
}
|
|
110
|
-
const cacheKey =
|
|
137
|
+
const cacheKey = getWaveformCacheKey(src, waveformSampleRate);
|
|
111
138
|
const cached = peaksCache.get(cacheKey);
|
|
112
139
|
if (cached) {
|
|
113
140
|
emitWaveformProgress({
|
|
114
|
-
peaks: cached,
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
peaks: cached.peaks,
|
|
142
|
+
averageVolume: cached.averageVolume,
|
|
143
|
+
completedPeaks: cached.peaks.length,
|
|
144
|
+
totalPeaks: cached.peaks.length,
|
|
117
145
|
final: true,
|
|
118
146
|
onProgress: options?.onProgress
|
|
119
147
|
});
|
|
120
148
|
return cached;
|
|
121
149
|
}
|
|
122
|
-
const input = new Input({
|
|
123
|
-
formats: ALL_FORMATS,
|
|
124
|
-
source: new UrlSource(url)
|
|
125
|
-
});
|
|
150
|
+
const input = typeof src === "string" ? new Input({ formats: ALL_FORMATS, source: new UrlSource(src) }) : null;
|
|
126
151
|
try {
|
|
127
|
-
const audioTrack = await input.getPrimaryAudioTrack();
|
|
152
|
+
const audioTrack = typeof src === "string" ? await input.getPrimaryAudioTrack() : src;
|
|
128
153
|
if (!audioTrack) {
|
|
129
|
-
return new Float32Array(0);
|
|
154
|
+
return { peaks: new Float32Array(0), averageVolume: null };
|
|
130
155
|
}
|
|
131
156
|
if (await audioTrack.isLive()) {
|
|
132
|
-
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " +
|
|
157
|
+
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
|
|
133
158
|
}
|
|
134
159
|
if (await audioTrack.isRelativeToUnixEpoch()) {
|
|
135
|
-
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " +
|
|
160
|
+
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
|
|
136
161
|
}
|
|
137
162
|
const audioSampleRate = await audioTrack.getSampleRate();
|
|
138
163
|
const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
|
|
@@ -149,7 +174,7 @@ async function loadWaveformPeaks(url, signal, options) {
|
|
|
149
174
|
for await (const sample of sink.samples()) {
|
|
150
175
|
if (signal.aborted) {
|
|
151
176
|
sample.close();
|
|
152
|
-
return new Float32Array(0);
|
|
177
|
+
return { peaks: new Float32Array(0), averageVolume: null };
|
|
153
178
|
}
|
|
154
179
|
const startFrame = getAudioSampleStartFrameAtTimelineZero(sample);
|
|
155
180
|
if (startFrame === null) {
|
|
@@ -175,22 +200,27 @@ async function loadWaveformPeaks(url, signal, options) {
|
|
|
175
200
|
frameCount
|
|
176
201
|
});
|
|
177
202
|
const channels = Math.max(1, sample.numberOfChannels);
|
|
203
|
+
const { sampleRate } = sample;
|
|
178
204
|
sample.close();
|
|
179
|
-
processor.processSampleChunk(floats, channels);
|
|
205
|
+
processor.processSampleChunk(floats, channels, sampleRate);
|
|
180
206
|
}
|
|
181
207
|
processor.finalize();
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
const result = {
|
|
209
|
+
peaks: processor.peaks,
|
|
210
|
+
averageVolume: processor.averageVolume
|
|
211
|
+
};
|
|
212
|
+
peaksCache.set(cacheKey, result);
|
|
213
|
+
return result;
|
|
185
214
|
} finally {
|
|
186
|
-
input
|
|
215
|
+
input?.dispose();
|
|
187
216
|
}
|
|
188
217
|
}
|
|
189
218
|
|
|
190
219
|
// src/audio-waveform-worker.ts
|
|
191
|
-
var postPeaks = (requestId, peaks, final) => {
|
|
220
|
+
var postPeaks = (requestId, peaks, final, averageVolume) => {
|
|
192
221
|
const payload = {
|
|
193
222
|
type: "peaks",
|
|
223
|
+
averageVolume,
|
|
194
224
|
requestId,
|
|
195
225
|
peaks,
|
|
196
226
|
final
|
|
@@ -213,11 +243,11 @@ self.addEventListener("message", (event) => {
|
|
|
213
243
|
waveformSampleRate: message.waveformSampleRate,
|
|
214
244
|
onProgress: ({ peaks, final }) => {
|
|
215
245
|
if (!final) {
|
|
216
|
-
postPeaks(message.requestId, peaks, false);
|
|
246
|
+
postPeaks(message.requestId, peaks, false, null);
|
|
217
247
|
}
|
|
218
248
|
}
|
|
219
|
-
}).then((peaks) => {
|
|
220
|
-
postPeaks(message.requestId, peaks, true);
|
|
249
|
+
}).then(({ peaks, averageVolume }) => {
|
|
250
|
+
postPeaks(message.requestId, peaks, true, averageVolume);
|
|
221
251
|
}).catch((error) => {
|
|
222
252
|
postError(message.requestId, error);
|
|
223
253
|
});
|
package/dist/esm/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var parseColor = (color) => {
|
|
|
17
17
|
|
|
18
18
|
// src/audio-waveform/draw-peaks.ts
|
|
19
19
|
var CLIPPING_COLOR = "#FF7F50";
|
|
20
|
+
var WAVEFORM_HEIGHT_SCALE = 0.8;
|
|
20
21
|
var getVolumeAtBar = ({
|
|
21
22
|
barIndex,
|
|
22
23
|
numBars,
|
|
@@ -56,6 +57,7 @@ var drawBars = ({
|
|
|
56
57
|
const imageData = ctx.createImageData(w, height);
|
|
57
58
|
const { data } = imageData;
|
|
58
59
|
const numBars = width;
|
|
60
|
+
const fullScaleHalfBar = height * WAVEFORM_HEIGHT_SCALE / 2;
|
|
59
61
|
for (let barIndex = 0;barIndex < numBars; barIndex++) {
|
|
60
62
|
const x = barIndex;
|
|
61
63
|
if (x >= w)
|
|
@@ -64,15 +66,15 @@ var drawBars = ({
|
|
|
64
66
|
const peak = peaks[peakIndex] || 0;
|
|
65
67
|
const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
|
|
66
68
|
const scaledPeak = peak * barVolume;
|
|
67
|
-
const halfBar = Math.max(0, Math.min(height / 2, scaledPeak *
|
|
69
|
+
const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * fullScaleHalfBar));
|
|
68
70
|
if (halfBar === 0)
|
|
69
71
|
continue;
|
|
70
72
|
const mid = height / 2;
|
|
71
73
|
const barY = Math.round(mid - halfBar);
|
|
72
74
|
const barEnd = Math.round(mid + halfBar);
|
|
73
75
|
const isClipping = scaledPeak > 1;
|
|
74
|
-
const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
|
|
75
|
-
const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
|
|
76
|
+
const clipTopEnd = isClipping ? Math.min(Math.max(barY + 2, Math.round(mid - fullScaleHalfBar)), barEnd) : barY;
|
|
77
|
+
const clipBotStart = isClipping ? Math.max(Math.min(barEnd - 2, Math.round(mid + fullScaleHalfBar)), barY) : barEnd;
|
|
76
78
|
for (let y = barY;y < clipTopEnd; y++) {
|
|
77
79
|
const idx = (y * w + x) * 4;
|
|
78
80
|
data[idx] = cr;
|
|
@@ -176,6 +178,21 @@ var getVisibleWaveformVolume = ({
|
|
|
176
178
|
// src/audio-waveform/load-waveform-peaks.ts
|
|
177
179
|
import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
|
|
178
180
|
|
|
181
|
+
// src/audio-waveform/get-waveform-cache-key.ts
|
|
182
|
+
var trackIds = new WeakMap;
|
|
183
|
+
var nextTrackId = 0;
|
|
184
|
+
var getWaveformCacheKey = (src, waveformSampleRate) => {
|
|
185
|
+
if (typeof src === "string") {
|
|
186
|
+
return `${waveformSampleRate}:url:${src}`;
|
|
187
|
+
}
|
|
188
|
+
let id = trackIds.get(src);
|
|
189
|
+
if (id === undefined) {
|
|
190
|
+
id = nextTrackId++;
|
|
191
|
+
trackIds.set(src, id);
|
|
192
|
+
}
|
|
193
|
+
return `${waveformSampleRate}:track:${id}`;
|
|
194
|
+
};
|
|
195
|
+
|
|
179
196
|
// src/audio-waveform/trim-audio-sample-before-zero.ts
|
|
180
197
|
var getAudioSampleStartFrameAtTimelineZero = (sample) => {
|
|
181
198
|
if (sample.timestamp + sample.duration <= 0) {
|
|
@@ -189,6 +206,7 @@ var getAudioSampleStartFrameAtTimelineZero = (sample) => {
|
|
|
189
206
|
|
|
190
207
|
// src/audio-waveform/waveform-peak-processor.ts
|
|
191
208
|
var emitWaveformProgress = ({
|
|
209
|
+
averageVolume,
|
|
192
210
|
completedPeaks,
|
|
193
211
|
final,
|
|
194
212
|
onProgress,
|
|
@@ -196,6 +214,7 @@ var emitWaveformProgress = ({
|
|
|
196
214
|
totalPeaks
|
|
197
215
|
}) => {
|
|
198
216
|
onProgress?.({
|
|
217
|
+
averageVolume,
|
|
199
218
|
peaks,
|
|
200
219
|
completedPeaks,
|
|
201
220
|
totalPeaks,
|
|
@@ -210,6 +229,8 @@ var createWaveformPeakProcessor = ({
|
|
|
210
229
|
now
|
|
211
230
|
}) => {
|
|
212
231
|
const peaks = new Float32Array(totalPeaks);
|
|
232
|
+
let energy = 0;
|
|
233
|
+
let duration = 0;
|
|
213
234
|
let peakIndex = 0;
|
|
214
235
|
let peakMax = 0;
|
|
215
236
|
let sampleInPeak = 0;
|
|
@@ -226,6 +247,7 @@ var createWaveformPeakProcessor = ({
|
|
|
226
247
|
lastProgressAt = timestamp;
|
|
227
248
|
lastProgressPeak = peakIndex;
|
|
228
249
|
emitWaveformProgress({
|
|
250
|
+
averageVolume: force && duration > 0 ? 10 * Math.log10(energy / duration) : null,
|
|
229
251
|
peaks,
|
|
230
252
|
completedPeaks: peakIndex,
|
|
231
253
|
totalPeaks,
|
|
@@ -235,17 +257,24 @@ var createWaveformPeakProcessor = ({
|
|
|
235
257
|
};
|
|
236
258
|
return {
|
|
237
259
|
peaks,
|
|
238
|
-
|
|
260
|
+
get averageVolume() {
|
|
261
|
+
return duration > 0 ? 10 * Math.log10(energy / duration) : null;
|
|
262
|
+
},
|
|
263
|
+
processSampleChunk: (floats, channels, sampleRate) => {
|
|
239
264
|
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
240
265
|
for (let frame = 0;frame < frameCount; frame++) {
|
|
241
266
|
let framePeak = 0;
|
|
267
|
+
let frameEnergy = 0;
|
|
242
268
|
for (let channel = 0;channel < channels; channel++) {
|
|
243
269
|
const sampleIndex = frame * channels + channel;
|
|
244
270
|
const abs = Math.abs(floats[sampleIndex] ?? 0);
|
|
271
|
+
frameEnergy += abs * abs;
|
|
245
272
|
if (abs > framePeak) {
|
|
246
273
|
framePeak = abs;
|
|
247
274
|
}
|
|
248
275
|
}
|
|
276
|
+
energy += frameEnergy / channels / sampleRate;
|
|
277
|
+
duration += 1 / sampleRate;
|
|
249
278
|
if (framePeak > peakMax) {
|
|
250
279
|
peakMax = framePeak;
|
|
251
280
|
}
|
|
@@ -274,37 +303,35 @@ var createWaveformPeakProcessor = ({
|
|
|
274
303
|
// src/audio-waveform/load-waveform-peaks.ts
|
|
275
304
|
var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
276
305
|
var peaksCache = new Map;
|
|
277
|
-
async function loadWaveformPeaks(
|
|
306
|
+
async function loadWaveformPeaks(src, signal, options) {
|
|
278
307
|
const waveformSampleRate = options?.waveformSampleRate ?? TARGET_SAMPLE_RATE;
|
|
279
308
|
if (!Number.isFinite(waveformSampleRate) || waveformSampleRate <= 0) {
|
|
280
309
|
throw new Error("The waveform sample rate must be a positive number.");
|
|
281
310
|
}
|
|
282
|
-
const cacheKey =
|
|
311
|
+
const cacheKey = getWaveformCacheKey(src, waveformSampleRate);
|
|
283
312
|
const cached = peaksCache.get(cacheKey);
|
|
284
313
|
if (cached) {
|
|
285
314
|
emitWaveformProgress({
|
|
286
|
-
peaks: cached,
|
|
287
|
-
|
|
288
|
-
|
|
315
|
+
peaks: cached.peaks,
|
|
316
|
+
averageVolume: cached.averageVolume,
|
|
317
|
+
completedPeaks: cached.peaks.length,
|
|
318
|
+
totalPeaks: cached.peaks.length,
|
|
289
319
|
final: true,
|
|
290
320
|
onProgress: options?.onProgress
|
|
291
321
|
});
|
|
292
322
|
return cached;
|
|
293
323
|
}
|
|
294
|
-
const input = new Input({
|
|
295
|
-
formats: ALL_FORMATS,
|
|
296
|
-
source: new UrlSource(url)
|
|
297
|
-
});
|
|
324
|
+
const input = typeof src === "string" ? new Input({ formats: ALL_FORMATS, source: new UrlSource(src) }) : null;
|
|
298
325
|
try {
|
|
299
|
-
const audioTrack = await input.getPrimaryAudioTrack();
|
|
326
|
+
const audioTrack = typeof src === "string" ? await input.getPrimaryAudioTrack() : src;
|
|
300
327
|
if (!audioTrack) {
|
|
301
|
-
return new Float32Array(0);
|
|
328
|
+
return { peaks: new Float32Array(0), averageVolume: null };
|
|
302
329
|
}
|
|
303
330
|
if (await audioTrack.isLive()) {
|
|
304
|
-
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " +
|
|
331
|
+
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
|
|
305
332
|
}
|
|
306
333
|
if (await audioTrack.isRelativeToUnixEpoch()) {
|
|
307
|
-
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " +
|
|
334
|
+
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
|
|
308
335
|
}
|
|
309
336
|
const audioSampleRate = await audioTrack.getSampleRate();
|
|
310
337
|
const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
|
|
@@ -321,7 +348,7 @@ async function loadWaveformPeaks(url, signal, options) {
|
|
|
321
348
|
for await (const sample of sink.samples()) {
|
|
322
349
|
if (signal.aborted) {
|
|
323
350
|
sample.close();
|
|
324
|
-
return new Float32Array(0);
|
|
351
|
+
return { peaks: new Float32Array(0), averageVolume: null };
|
|
325
352
|
}
|
|
326
353
|
const startFrame = getAudioSampleStartFrameAtTimelineZero(sample);
|
|
327
354
|
if (startFrame === null) {
|
|
@@ -347,15 +374,19 @@ async function loadWaveformPeaks(url, signal, options) {
|
|
|
347
374
|
frameCount
|
|
348
375
|
});
|
|
349
376
|
const channels = Math.max(1, sample.numberOfChannels);
|
|
377
|
+
const { sampleRate } = sample;
|
|
350
378
|
sample.close();
|
|
351
|
-
processor.processSampleChunk(floats, channels);
|
|
379
|
+
processor.processSampleChunk(floats, channels, sampleRate);
|
|
352
380
|
}
|
|
353
381
|
processor.finalize();
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
382
|
+
const result = {
|
|
383
|
+
peaks: processor.peaks,
|
|
384
|
+
averageVolume: processor.averageVolume
|
|
385
|
+
};
|
|
386
|
+
peaksCache.set(cacheKey, result);
|
|
387
|
+
return result;
|
|
357
388
|
} finally {
|
|
358
|
-
input
|
|
389
|
+
input?.dispose();
|
|
359
390
|
}
|
|
360
391
|
}
|
|
361
392
|
// src/audio-waveform/slice-waveform-peaks.ts
|
|
@@ -455,9 +486,9 @@ var inFlightByRequestId = new Map;
|
|
|
455
486
|
var worker = null;
|
|
456
487
|
var workerFailed = false;
|
|
457
488
|
var nextRequestId = 0;
|
|
458
|
-
var emitPeaks = (load, peaks, final) => {
|
|
489
|
+
var emitPeaks = (load, peaks, final, averageVolume) => {
|
|
459
490
|
if (final) {
|
|
460
|
-
peaksCache2.set(load.cacheKey, peaks);
|
|
491
|
+
peaksCache2.set(load.cacheKey, { peaks, averageVolume });
|
|
461
492
|
load.latestPeaks = null;
|
|
462
493
|
inFlightByCacheKey.delete(load.cacheKey);
|
|
463
494
|
if (load.requestId !== null) {
|
|
@@ -467,7 +498,7 @@ var emitPeaks = (load, peaks, final) => {
|
|
|
467
498
|
load.latestPeaks = peaks;
|
|
468
499
|
}
|
|
469
500
|
for (const listener of load.listeners) {
|
|
470
|
-
listener.onPeaks(peaks, final);
|
|
501
|
+
listener.onPeaks(peaks, final, averageVolume);
|
|
471
502
|
}
|
|
472
503
|
};
|
|
473
504
|
var emitError = (load, error) => {
|
|
@@ -488,10 +519,10 @@ var startMainThreadLoad = (load) => {
|
|
|
488
519
|
if (final) {
|
|
489
520
|
return;
|
|
490
521
|
}
|
|
491
|
-
emitPeaks(load, peaks.slice(), false);
|
|
522
|
+
emitPeaks(load, peaks.slice(), false, null);
|
|
492
523
|
}
|
|
493
|
-
}).then((peaks) => {
|
|
494
|
-
emitPeaks(load, peaks, true);
|
|
524
|
+
}).then(({ peaks, averageVolume }) => {
|
|
525
|
+
emitPeaks(load, peaks, true, averageVolume);
|
|
495
526
|
}).catch((error) => {
|
|
496
527
|
emitError(load, error instanceof Error ? error : new Error("Failed to load waveform peaks"));
|
|
497
528
|
});
|
|
@@ -529,7 +560,7 @@ var getOrCreateWorker = () => {
|
|
|
529
560
|
emitError(load, new Error(message.message));
|
|
530
561
|
return;
|
|
531
562
|
}
|
|
532
|
-
emitPeaks(load, message.peaks, message.final);
|
|
563
|
+
emitPeaks(load, message.peaks, message.final, message.averageVolume);
|
|
533
564
|
});
|
|
534
565
|
worker.addEventListener("error", (event) => {
|
|
535
566
|
event.preventDefault();
|
|
@@ -543,10 +574,10 @@ var subscribeToWaveformPeaks = ({
|
|
|
543
574
|
onPeaks,
|
|
544
575
|
onError
|
|
545
576
|
}) => {
|
|
546
|
-
const cacheKey =
|
|
577
|
+
const cacheKey = getWaveformCacheKey(src, waveformSampleRate);
|
|
547
578
|
const cached = peaksCache2.get(cacheKey);
|
|
548
579
|
if (cached) {
|
|
549
|
-
onPeaks(cached, true);
|
|
580
|
+
onPeaks(cached.peaks, true, cached.averageVolume);
|
|
550
581
|
return () => {
|
|
551
582
|
return;
|
|
552
583
|
};
|
|
@@ -556,7 +587,7 @@ var subscribeToWaveformPeaks = ({
|
|
|
556
587
|
if (existing) {
|
|
557
588
|
existing.listeners.add(listener);
|
|
558
589
|
if (existing.latestPeaks) {
|
|
559
|
-
onPeaks(existing.latestPeaks, false);
|
|
590
|
+
onPeaks(existing.latestPeaks, false, null);
|
|
560
591
|
}
|
|
561
592
|
return () => {
|
|
562
593
|
existing.listeners.delete(listener);
|
|
@@ -571,8 +602,8 @@ var subscribeToWaveformPeaks = ({
|
|
|
571
602
|
requestId: null
|
|
572
603
|
};
|
|
573
604
|
inFlightByCacheKey.set(cacheKey, load);
|
|
574
|
-
const workerInstance = getOrCreateWorker();
|
|
575
|
-
if (workerInstance) {
|
|
605
|
+
const workerInstance = typeof src === "string" ? getOrCreateWorker() : null;
|
|
606
|
+
if (workerInstance && typeof src === "string") {
|
|
576
607
|
const requestId = nextRequestId++;
|
|
577
608
|
load.requestId = requestId;
|
|
578
609
|
inFlightByRequestId.set(requestId, load);
|
|
@@ -1099,6 +1130,13 @@ var renderFrameStripToCanvas = async ({
|
|
|
1099
1130
|
frameHeight
|
|
1100
1131
|
});
|
|
1101
1132
|
};
|
|
1133
|
+
// src/format-average-audio-volume.ts
|
|
1134
|
+
var formatAverageAudioVolume = (volume) => {
|
|
1135
|
+
if (volume === null) {
|
|
1136
|
+
return "Unavailable";
|
|
1137
|
+
}
|
|
1138
|
+
return volume === -Infinity ? "−∞ dBFS (silent)" : `${volume.toFixed(1)} dBFS`;
|
|
1139
|
+
};
|
|
1102
1140
|
export {
|
|
1103
1141
|
subscribeToWaveformPeaks,
|
|
1104
1142
|
sliceWaveformPeaks,
|
|
@@ -1116,6 +1154,7 @@ export {
|
|
|
1116
1154
|
getDurationOfOneFrame,
|
|
1117
1155
|
getAspectRatioFromCache,
|
|
1118
1156
|
frameDatabase,
|
|
1157
|
+
formatAverageAudioVolume,
|
|
1119
1158
|
fillWithCachedFrames,
|
|
1120
1159
|
fillFrameWhereItFits,
|
|
1121
1160
|
extractFrames,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const formatAverageAudioVolume: (volume: number | null) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatAverageAudioVolume = void 0;
|
|
4
|
+
const formatAverageAudioVolume = (volume) => {
|
|
5
|
+
if (volume === null) {
|
|
6
|
+
return 'Unavailable';
|
|
7
|
+
}
|
|
8
|
+
return volume === -Infinity
|
|
9
|
+
? '−∞ dBFS (silent)'
|
|
10
|
+
: `${volume.toFixed(1)} dBFS`;
|
|
11
|
+
};
|
|
12
|
+
exports.formatAverageAudioVolume = formatAverageAudioVolume;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,3 +18,4 @@ export { WEBCODECS_TIMESCALE, calculateTimestampSlots, drawSlot, ensureSlots, fi
|
|
|
18
18
|
export { renderFrameStripToCanvas } from './render-frame-strip-to-canvas';
|
|
19
19
|
export type { RenderFrameStripToCanvasOptions } from './render-frame-strip-to-canvas';
|
|
20
20
|
export { resizeVideoFrame } from './resize-video-frame';
|
|
21
|
+
export { formatAverageAudioVolume } from './format-average-audio-volume';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.WEBCODECS_TIMESCALE = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.clampTimestampsToDuration = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.subscribeToWaveformPeaks = exports.sliceVisibleWaveformPeaks = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
|
|
3
|
+
exports.formatAverageAudioVolume = exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.WEBCODECS_TIMESCALE = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.clampTimestampsToDuration = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.subscribeToWaveformPeaks = exports.sliceVisibleWaveformPeaks = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
|
|
4
4
|
const constants_1 = require("./audio-waveform/constants");
|
|
5
5
|
Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
|
|
6
6
|
const draw_peaks_1 = require("./audio-waveform/draw-peaks");
|
|
@@ -46,3 +46,5 @@ const render_frame_strip_to_canvas_1 = require("./render-frame-strip-to-canvas")
|
|
|
46
46
|
Object.defineProperty(exports, "renderFrameStripToCanvas", { enumerable: true, get: function () { return render_frame_strip_to_canvas_1.renderFrameStripToCanvas; } });
|
|
47
47
|
const resize_video_frame_1 = require("./resize-video-frame");
|
|
48
48
|
Object.defineProperty(exports, "resizeVideoFrame", { enumerable: true, get: function () { return resize_video_frame_1.resizeVideoFrame; } });
|
|
49
|
+
const format_average_audio_volume_1 = require("./format-average-audio-volume");
|
|
50
|
+
Object.defineProperty(exports, "formatAverageAudioVolume", { enumerable: true, get: function () { return format_average_audio_volume_1.formatAverageAudioVolume; } });
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"url": "https://github.com/remotion-dev/remotion/tree/main/packages/timeline-utils"
|
|
4
4
|
},
|
|
5
5
|
"name": "@remotion/timeline-utils",
|
|
6
|
-
"version": "4.0.
|
|
6
|
+
"version": "4.0.524",
|
|
7
7
|
"description": "Internal utilities for rendering Remotion timelines",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"scripts": {
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"url": "https://github.com/remotion-dev/remotion/issues"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"mediabunny": "1.
|
|
25
|
-
"remotion": "4.0.
|
|
24
|
+
"mediabunny": "1.56.1",
|
|
25
|
+
"remotion": "4.0.524"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@mediabunny/server": "1.
|
|
29
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
28
|
+
"@mediabunny/server": "1.56.1",
|
|
29
|
+
"@remotion/eslint-config-internal": "4.0.524",
|
|
30
30
|
"eslint": "9.19.0",
|
|
31
31
|
"@typescript/native-preview": "7.0.0-dev.20260217.1"
|
|
32
32
|
},
|