@remotion/renderer 4.0.355 → 4.0.357
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/assets/apply-tone-frequency.d.ts +11 -0
- package/dist/assets/apply-tone-frequency.js +34 -0
- package/dist/assets/change-tempo.d.ts +34 -0
- package/dist/assets/change-tempo.js +287 -0
- package/dist/assets/change-tonefrequency.d.ts +2 -0
- package/dist/assets/change-tonefrequency.js +28 -0
- package/dist/assets/inline-audio-mixing.d.ts +8 -0
- package/dist/assets/inline-audio-mixing.js +39 -9
- package/dist/assets/resample-audiodata.d.ts +6 -0
- package/dist/assets/resample-audiodata.js +54 -0
- package/dist/assets/types.d.ts +1 -1
- package/dist/browser/Browser.d.ts +7 -4
- package/dist/browser/Browser.js +6 -3
- package/dist/browser/BrowserPage.d.ts +10 -2
- package/dist/browser/BrowserPage.js +9 -16
- package/dist/browser/Target.d.ts +3 -2
- package/dist/browser/Target.js +2 -1
- package/dist/create-audio.js +6 -0
- package/dist/default-on-log.d.ts +2 -0
- package/dist/default-on-log.js +8 -0
- package/dist/esm/error-handling.mjs +22 -14
- package/dist/esm/index.mjs +207 -108
- package/dist/get-browser-instance.d.ts +3 -2
- package/dist/get-browser-instance.js +3 -1
- package/dist/get-compositions.d.ts +2 -0
- package/dist/get-compositions.js +4 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +24 -24
- package/dist/make-page.d.ts +3 -2
- package/dist/make-page.js +2 -2
- package/dist/render-frames.d.ts +2 -0
- package/dist/render-frames.js +6 -2
- package/dist/render-media.d.ts +2 -0
- package/dist/render-media.js +13 -15
- package/dist/render-still.d.ts +2 -0
- package/dist/render-still.js +4 -1
- package/dist/select-composition.js +2 -0
- package/dist/test-gpu.d.ts +3 -1
- package/dist/test-gpu.js +2 -1
- package/dist/validate-even-dimensions-with-codec.js +16 -23
- package/ensure-browser.mjs +22 -14
- package/package.json +12 -12
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LogLevel } from '../log-level';
|
|
2
|
+
import type { CancelSignal } from '../make-cancel-signal';
|
|
3
|
+
export declare const applyToneFrequencyUsingFfmpeg: ({ input, output, toneFrequency, indent, logLevel, binariesDirectory, cancelSignal, }: {
|
|
4
|
+
input: string;
|
|
5
|
+
output: string;
|
|
6
|
+
toneFrequency: number;
|
|
7
|
+
indent: boolean;
|
|
8
|
+
logLevel: LogLevel;
|
|
9
|
+
binariesDirectory: string | null;
|
|
10
|
+
cancelSignal: CancelSignal | undefined;
|
|
11
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyToneFrequencyUsingFfmpeg = void 0;
|
|
4
|
+
const call_ffmpeg_1 = require("../call-ffmpeg");
|
|
5
|
+
const logger_1 = require("../logger");
|
|
6
|
+
const sample_rate_1 = require("../sample-rate");
|
|
7
|
+
const applyToneFrequencyUsingFfmpeg = async ({ input, output, toneFrequency, indent, logLevel, binariesDirectory, cancelSignal, }) => {
|
|
8
|
+
const filter = `asetrate=${sample_rate_1.DEFAULT_SAMPLE_RATE}*${toneFrequency},aresample=${sample_rate_1.DEFAULT_SAMPLE_RATE},atempo=1/${toneFrequency}`;
|
|
9
|
+
const args = [
|
|
10
|
+
'-hide_banner',
|
|
11
|
+
'-i',
|
|
12
|
+
input,
|
|
13
|
+
['-ac', '2'],
|
|
14
|
+
'-filter:a',
|
|
15
|
+
filter,
|
|
16
|
+
['-c:a', 'pcm_s16le'],
|
|
17
|
+
['-ar', String(sample_rate_1.DEFAULT_SAMPLE_RATE)],
|
|
18
|
+
'-y',
|
|
19
|
+
output,
|
|
20
|
+
].flat(2);
|
|
21
|
+
logger_1.Log.verbose({ indent, logLevel }, 'Changing tone frequency using FFmpeg:', JSON.stringify(args.join(' ')), 'Filter:', filter);
|
|
22
|
+
const startTimestamp = Date.now();
|
|
23
|
+
const task = (0, call_ffmpeg_1.callFf)({
|
|
24
|
+
bin: 'ffmpeg',
|
|
25
|
+
args,
|
|
26
|
+
indent,
|
|
27
|
+
logLevel,
|
|
28
|
+
binariesDirectory,
|
|
29
|
+
cancelSignal,
|
|
30
|
+
});
|
|
31
|
+
await task;
|
|
32
|
+
logger_1.Log.verbose({ indent, logLevel }, 'Changed tone frequency using FFmpeg', `${Date.now() - startTimestamp}ms`);
|
|
33
|
+
};
|
|
34
|
+
exports.applyToneFrequencyUsingFfmpeg = applyToneFrequencyUsingFfmpeg;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-scale modification (tempo change) with approximate pitch preservation
|
|
3
|
+
* for interleaved Int16 PCM with multiple channels, using a SOLA/WSOLA-like method.
|
|
4
|
+
*
|
|
5
|
+
* @param input Interleaved Int16 PCM samples (e.g., LR LR LR ...)
|
|
6
|
+
* @param channels Number of channels (e.g., 2 for stereo)
|
|
7
|
+
* @param f Tempo factor: >1.0 = faster (shorter), <1.0 = slower (longer)
|
|
8
|
+
* @param opts Optional tuning parameters
|
|
9
|
+
* @returns Interleaved Int16 PCM with length ≈ round(input.length * f)
|
|
10
|
+
*/
|
|
11
|
+
export declare function atempoInt16Interleaved(input: Int16Array, channels: number, f: number, opts?: {
|
|
12
|
+
sampleRate?: number;
|
|
13
|
+
frameMs?: number;
|
|
14
|
+
overlapRatio?: number;
|
|
15
|
+
searchMs?: number;
|
|
16
|
+
window?: 'hann' | 'hamming';
|
|
17
|
+
clamp?: boolean;
|
|
18
|
+
}): Int16Array;
|
|
19
|
+
/**
|
|
20
|
+
* Reads a WAV file, applies WSOLA tempo modification, and writes it back.
|
|
21
|
+
* Ignores the first 44 bytes (WAV header) and treats the rest as interleaved Int16 PCM.
|
|
22
|
+
*
|
|
23
|
+
* @param filePath Path to the WAV file to process
|
|
24
|
+
* @param tempoFactor Tempo factor: >1 = faster/shorter, <1 = slower/longer
|
|
25
|
+
*/
|
|
26
|
+
export declare function processWavFileWithWSOLA(filePath: string, tempoFactor: number): Promise<void>;
|
|
27
|
+
export declare const NUMBER_OF_CHANNELS = 2;
|
|
28
|
+
export declare const applyToneFrequency: (numberOfFrames: number, audioData: Int16Array, toneFrequency: number) => Int16Array;
|
|
29
|
+
export declare const resampleAudioData: ({ sourceChannels, destination, targetFrames, chunkSize, }: {
|
|
30
|
+
sourceChannels: Int16Array;
|
|
31
|
+
destination: Int16Array;
|
|
32
|
+
targetFrames: number;
|
|
33
|
+
chunkSize: number;
|
|
34
|
+
}) => void;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resampleAudioData = exports.applyToneFrequency = exports.NUMBER_OF_CHANNELS = void 0;
|
|
7
|
+
exports.atempoInt16Interleaved = atempoInt16Interleaved;
|
|
8
|
+
exports.processWavFileWithWSOLA = processWavFileWithWSOLA;
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const sample_rate_1 = require("../sample-rate");
|
|
11
|
+
function clamp16(x) {
|
|
12
|
+
const y = Math.round(x);
|
|
13
|
+
return y < -32768 ? -32768 : y > 32767 ? 32767 : y;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Time-scale modification (tempo change) with approximate pitch preservation
|
|
17
|
+
* for interleaved Int16 PCM with multiple channels, using a SOLA/WSOLA-like method.
|
|
18
|
+
*
|
|
19
|
+
* @param input Interleaved Int16 PCM samples (e.g., LR LR LR ...)
|
|
20
|
+
* @param channels Number of channels (e.g., 2 for stereo)
|
|
21
|
+
* @param f Tempo factor: >1.0 = faster (shorter), <1.0 = slower (longer)
|
|
22
|
+
* @param opts Optional tuning parameters
|
|
23
|
+
* @returns Interleaved Int16 PCM with length ≈ round(input.length * f)
|
|
24
|
+
*/
|
|
25
|
+
function atempoInt16Interleaved(input, channels, f, opts) {
|
|
26
|
+
var _a, _b, _c, _d, _e;
|
|
27
|
+
if (!Number.isFinite(f) || f <= 0) {
|
|
28
|
+
throw new Error('f must be a positive finite number');
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(channels) || channels <= 0) {
|
|
31
|
+
throw new Error('channels must be a positive integer');
|
|
32
|
+
}
|
|
33
|
+
const n = input.length;
|
|
34
|
+
if (n === 0)
|
|
35
|
+
return new Int16Array(0);
|
|
36
|
+
if (n % channels !== 0) {
|
|
37
|
+
throw new Error('input length must be a multiple of channels (interleaved PCM)');
|
|
38
|
+
}
|
|
39
|
+
// Parameters
|
|
40
|
+
const sampleRate = (_a = opts === null || opts === void 0 ? void 0 : opts.sampleRate) !== null && _a !== void 0 ? _a : 48000;
|
|
41
|
+
const frameMs = (_b = opts === null || opts === void 0 ? void 0 : opts.frameMs) !== null && _b !== void 0 ? _b : 30;
|
|
42
|
+
const overlapRatio = Math.max(0.1, Math.min(0.95, (_c = opts === null || opts === void 0 ? void 0 : opts.overlapRatio) !== null && _c !== void 0 ? _c : 0.55));
|
|
43
|
+
const searchMs = (_d = opts === null || opts === void 0 ? void 0 : opts.searchMs) !== null && _d !== void 0 ? _d : 8;
|
|
44
|
+
const winKind = (_e = opts === null || opts === void 0 ? void 0 : opts.window) !== null && _e !== void 0 ? _e : 'hann';
|
|
45
|
+
// Work in samples per channel
|
|
46
|
+
const samplesPerChannel = (n / channels) | 0;
|
|
47
|
+
// Frame sizing and hops (per channel)
|
|
48
|
+
const frameSize = Math.max(128, Math.floor((sampleRate * frameMs) / 1000));
|
|
49
|
+
const overlap = Math.floor(frameSize * overlapRatio);
|
|
50
|
+
const anaHop = Math.max(1, frameSize - overlap);
|
|
51
|
+
const synHop = Math.max(1, Math.round(anaHop * f));
|
|
52
|
+
const searchRadius = Math.max(0, Math.floor((sampleRate * searchMs) / 1000));
|
|
53
|
+
// Window
|
|
54
|
+
const win = new Float32Array(frameSize);
|
|
55
|
+
for (let i = 0; i < frameSize; i++) {
|
|
56
|
+
const x = (Math.PI * 2 * i) / (frameSize - 1);
|
|
57
|
+
win[i] =
|
|
58
|
+
winKind === 'hann' ? 0.5 * (1 - Math.cos(x)) : 0.54 - 0.46 * Math.cos(x);
|
|
59
|
+
}
|
|
60
|
+
// Output buffers as float accumulators per channel
|
|
61
|
+
const estFrames = Math.ceil((samplesPerChannel - frameSize) / anaHop) + 1;
|
|
62
|
+
const estLen = Math.max(0, frameSize + synHop * (estFrames - 1));
|
|
63
|
+
const outLenAlloc = estLen + frameSize + searchRadius + 16;
|
|
64
|
+
const out = Array.from({ length: channels }, () => new Float32Array(outLenAlloc));
|
|
65
|
+
const outWeight = new Float32Array(outLenAlloc);
|
|
66
|
+
// Helper: read one channel’s frame from interleaved PCM
|
|
67
|
+
function readChannelFrame(chan, start, dst) {
|
|
68
|
+
// start is per-channel sample index
|
|
69
|
+
let srcIndex = start * channels + chan;
|
|
70
|
+
for (let i = 0; i < frameSize; i++) {
|
|
71
|
+
const pos = start + i;
|
|
72
|
+
let v = 0;
|
|
73
|
+
if (pos >= 0 && pos < samplesPerChannel) {
|
|
74
|
+
v = input[srcIndex];
|
|
75
|
+
}
|
|
76
|
+
dst[i] = v;
|
|
77
|
+
srcIndex += channels;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Build a mono guide frame (mid/mono mix) to drive alignment
|
|
81
|
+
const guideFrame = new Float32Array(frameSize);
|
|
82
|
+
function readGuideFrame(start) {
|
|
83
|
+
for (let i = 0; i < frameSize; i++) {
|
|
84
|
+
const pos = start + i;
|
|
85
|
+
if (pos >= 0 && pos < samplesPerChannel) {
|
|
86
|
+
let sum = 0;
|
|
87
|
+
const base = (pos * channels) | 0;
|
|
88
|
+
for (let c = 0; c < channels; c++) {
|
|
89
|
+
sum += input[base + c];
|
|
90
|
+
}
|
|
91
|
+
guideFrame[i] = sum / channels;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
guideFrame[i] = 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Cross-correlation on overlap region using guide to find best local alignment
|
|
99
|
+
function bestAlignment(outPosition, baseShift) {
|
|
100
|
+
let bestShift = baseShift;
|
|
101
|
+
let bestScore = -Infinity;
|
|
102
|
+
for (let shift = -searchRadius; shift <= searchRadius; shift++) {
|
|
103
|
+
const pos = outPosition + shift - overlap;
|
|
104
|
+
let score = 0;
|
|
105
|
+
let normA = 0;
|
|
106
|
+
let normB = 0;
|
|
107
|
+
for (let i = 0; i < overlap; i++) {
|
|
108
|
+
const outIdx = pos + i;
|
|
109
|
+
const outVal = outIdx >= 0 && outIdx < outLenAlloc ? out[0][outIdx] : 0; // use channel 0 accumulator as proxy
|
|
110
|
+
const frmVal = guideFrame[i];
|
|
111
|
+
score += outVal * frmVal;
|
|
112
|
+
normA += outVal * outVal;
|
|
113
|
+
normB += frmVal * frmVal;
|
|
114
|
+
}
|
|
115
|
+
const denom = Math.sqrt((normA || 1e-9) * (normB || 1e-9));
|
|
116
|
+
const corr = score / denom;
|
|
117
|
+
if (corr > bestScore) {
|
|
118
|
+
bestScore = corr;
|
|
119
|
+
bestShift = shift;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return bestShift;
|
|
123
|
+
}
|
|
124
|
+
// Temp buffers per channel
|
|
125
|
+
const chanFrames = Array.from({ length: channels }, () => new Float32Array(frameSize));
|
|
126
|
+
let inPos = 0; // per-channel sample index
|
|
127
|
+
let outPos = 0; // per-channel sample index in accumulators
|
|
128
|
+
// First frame: place directly
|
|
129
|
+
readGuideFrame(0);
|
|
130
|
+
for (let c = 0; c < channels; c++) {
|
|
131
|
+
readChannelFrame(c, 0, chanFrames[c]);
|
|
132
|
+
for (let i = 0; i < frameSize; i++) {
|
|
133
|
+
const w = win[i];
|
|
134
|
+
const idx = i; // write starting at 0
|
|
135
|
+
out[c][idx] += chanFrames[c][i] * w;
|
|
136
|
+
if (c === 0)
|
|
137
|
+
outWeight[idx] += w;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
inPos += anaHop;
|
|
141
|
+
outPos += synHop;
|
|
142
|
+
// Process remaining frames
|
|
143
|
+
while (inPos < samplesPerChannel - 1) {
|
|
144
|
+
readGuideFrame(inPos);
|
|
145
|
+
// Find best alignment using guide
|
|
146
|
+
const shift = bestAlignment(outPos, 0);
|
|
147
|
+
const writeStart = outPos + shift - overlap;
|
|
148
|
+
// Windowed overlap-add for each channel using same alignment
|
|
149
|
+
for (let c = 0; c < channels; c++) {
|
|
150
|
+
readChannelFrame(c, inPos, chanFrames[c]);
|
|
151
|
+
for (let i = 0; i < frameSize; i++) {
|
|
152
|
+
const idx = writeStart + i;
|
|
153
|
+
if (idx >= 0 && idx < outLenAlloc) {
|
|
154
|
+
const w = win[i];
|
|
155
|
+
out[c][idx] += chanFrames[c][i] * w;
|
|
156
|
+
if (c === 0)
|
|
157
|
+
outWeight[idx] += w;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
inPos += anaHop;
|
|
162
|
+
outPos += synHop;
|
|
163
|
+
if (outPos + frameSize + searchRadius + 8 >= outLenAlloc)
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
// Normalize by accumulated window weights
|
|
167
|
+
for (let i = 0; i < outLenAlloc; i++) {
|
|
168
|
+
const w = outWeight[i];
|
|
169
|
+
if (w > 1e-9) {
|
|
170
|
+
const inv = 1 / w;
|
|
171
|
+
for (let c = 0; c < channels; c++) {
|
|
172
|
+
out[c][i] *= inv;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Target per-channel length and interleave
|
|
177
|
+
const targetPerChan = Math.max(1, Math.round(samplesPerChannel * f));
|
|
178
|
+
const targetTotal = targetPerChan * channels;
|
|
179
|
+
const result = new Int16Array(targetTotal);
|
|
180
|
+
// Clamp/convert and interleave
|
|
181
|
+
for (let i = 0; i < targetPerChan; i++) {
|
|
182
|
+
for (let c = 0; c < channels; c++) {
|
|
183
|
+
const v = i < out[c].length ? out[c][i] : 0;
|
|
184
|
+
const y = clamp16(v);
|
|
185
|
+
result[i * channels + c] = y;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Reads a WAV file, applies WSOLA tempo modification, and writes it back.
|
|
192
|
+
* Ignores the first 44 bytes (WAV header) and treats the rest as interleaved Int16 PCM.
|
|
193
|
+
*
|
|
194
|
+
* @param filePath Path to the WAV file to process
|
|
195
|
+
* @param tempoFactor Tempo factor: >1 = faster/shorter, <1 = slower/longer
|
|
196
|
+
*/
|
|
197
|
+
async function processWavFileWithWSOLA(filePath, tempoFactor) {
|
|
198
|
+
// Read the file
|
|
199
|
+
const fileBuffer = await promises_1.default.readFile(filePath);
|
|
200
|
+
// Skip first 44 bytes (WAV header) and create Int16Array
|
|
201
|
+
const audioData = new Int16Array(fileBuffer.buffer, 44);
|
|
202
|
+
// Apply WSOLA with 2 channels (stereo)
|
|
203
|
+
const processedAudio = (0, exports.applyToneFrequency)(audioData.length / 2, audioData, tempoFactor);
|
|
204
|
+
// Create new buffer with original header + processed audio
|
|
205
|
+
const newBuffer = new Uint8Array(44 + processedAudio.length * 2);
|
|
206
|
+
// Copy original header (first 44 bytes)
|
|
207
|
+
newBuffer.set(fileBuffer.subarray(0, 44), 0);
|
|
208
|
+
// Copy processed audio data
|
|
209
|
+
const processedBytes = new Uint8Array(processedAudio.buffer);
|
|
210
|
+
newBuffer.set(processedBytes, 44);
|
|
211
|
+
// Write the processed file back
|
|
212
|
+
await promises_1.default.writeFile(filePath, newBuffer);
|
|
213
|
+
}
|
|
214
|
+
exports.NUMBER_OF_CHANNELS = 2;
|
|
215
|
+
const applyToneFrequency = (numberOfFrames, audioData, toneFrequency) => {
|
|
216
|
+
// In FFmpeg, we apply toneFrequency as follows:
|
|
217
|
+
// `asetrate=${DEFAULT_SAMPLE_RATE}*${toneFrequency},aresample=${DEFAULT_SAMPLE_RATE},atempo=1/${toneFrequency}`
|
|
218
|
+
// So there are 2 steps:
|
|
219
|
+
// 1. Change the assumed sample rate
|
|
220
|
+
// 2. Resample to 48Khz
|
|
221
|
+
// 3. Apply playback rate
|
|
222
|
+
const step1SampleRate = sample_rate_1.DEFAULT_SAMPLE_RATE * toneFrequency;
|
|
223
|
+
const newNumberOfFrames = Math.round(numberOfFrames * (sample_rate_1.DEFAULT_SAMPLE_RATE / step1SampleRate));
|
|
224
|
+
const step2Data = new Int16Array(newNumberOfFrames * exports.NUMBER_OF_CHANNELS);
|
|
225
|
+
const chunkSize = numberOfFrames / newNumberOfFrames;
|
|
226
|
+
(0, exports.resampleAudioData)({
|
|
227
|
+
sourceChannels: audioData,
|
|
228
|
+
destination: step2Data,
|
|
229
|
+
targetFrames: newNumberOfFrames,
|
|
230
|
+
chunkSize,
|
|
231
|
+
});
|
|
232
|
+
const step3Data = atempoInt16Interleaved(step2Data, exports.NUMBER_OF_CHANNELS, toneFrequency, {
|
|
233
|
+
sampleRate: 48000,
|
|
234
|
+
});
|
|
235
|
+
return step3Data;
|
|
236
|
+
};
|
|
237
|
+
exports.applyToneFrequency = applyToneFrequency;
|
|
238
|
+
const fixFloatingPoint = (value) => {
|
|
239
|
+
if (value % 1 < 0.0000001) {
|
|
240
|
+
return Math.floor(value);
|
|
241
|
+
}
|
|
242
|
+
if (value % 1 > 0.9999999) {
|
|
243
|
+
return Math.ceil(value);
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
};
|
|
247
|
+
const resampleAudioData = ({ sourceChannels, destination, targetFrames, chunkSize, }) => {
|
|
248
|
+
const getSourceValues = (startUnfixed, endUnfixed, channelIndex) => {
|
|
249
|
+
const start = fixFloatingPoint(startUnfixed);
|
|
250
|
+
const end = fixFloatingPoint(endUnfixed);
|
|
251
|
+
const startFloor = Math.floor(start);
|
|
252
|
+
const startCeil = Math.ceil(start);
|
|
253
|
+
const startFraction = start - startFloor;
|
|
254
|
+
const endFraction = end - Math.floor(end);
|
|
255
|
+
const endFloor = Math.floor(end);
|
|
256
|
+
let weightedSum = 0;
|
|
257
|
+
let totalWeight = 0;
|
|
258
|
+
// Handle first fractional sample
|
|
259
|
+
if (startFraction > 0) {
|
|
260
|
+
const firstSample = sourceChannels[startFloor * exports.NUMBER_OF_CHANNELS + channelIndex];
|
|
261
|
+
weightedSum += firstSample * (1 - startFraction);
|
|
262
|
+
totalWeight += 1 - startFraction;
|
|
263
|
+
}
|
|
264
|
+
// Handle full samples
|
|
265
|
+
for (let k = startCeil; k < endFloor; k++) {
|
|
266
|
+
const num = sourceChannels[k * exports.NUMBER_OF_CHANNELS + channelIndex];
|
|
267
|
+
weightedSum += num;
|
|
268
|
+
totalWeight += 1;
|
|
269
|
+
}
|
|
270
|
+
// Handle last fractional sample
|
|
271
|
+
if (endFraction > 0) {
|
|
272
|
+
const lastSample = sourceChannels[endFloor * exports.NUMBER_OF_CHANNELS + channelIndex];
|
|
273
|
+
weightedSum += lastSample * endFraction;
|
|
274
|
+
totalWeight += endFraction;
|
|
275
|
+
}
|
|
276
|
+
const average = weightedSum / totalWeight;
|
|
277
|
+
return average;
|
|
278
|
+
};
|
|
279
|
+
for (let newFrameIndex = 0; newFrameIndex < targetFrames; newFrameIndex++) {
|
|
280
|
+
const start = newFrameIndex * chunkSize;
|
|
281
|
+
const end = start + chunkSize;
|
|
282
|
+
for (let i = 0; i < exports.NUMBER_OF_CHANNELS; i++) {
|
|
283
|
+
destination[newFrameIndex * exports.NUMBER_OF_CHANNELS + i] = getSourceValues(start, end, i);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
exports.resampleAudioData = resampleAudioData;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyToneFrequency = exports.NUMBER_OF_CHANNELS = void 0;
|
|
4
|
+
const sample_rate_1 = require("../sample-rate");
|
|
5
|
+
const change_tempo_1 = require("./change-tempo");
|
|
6
|
+
const resample_audiodata_1 = require("./resample-audiodata");
|
|
7
|
+
exports.NUMBER_OF_CHANNELS = 2;
|
|
8
|
+
const applyToneFrequency = (numberOfFrames, audioData, toneFrequency) => {
|
|
9
|
+
// In FFmpeg, we apply toneFrequency as follows:
|
|
10
|
+
// `asetrate=${DEFAULT_SAMPLE_RATE}*${toneFrequency},aresample=${DEFAULT_SAMPLE_RATE},atempo=1/${toneFrequency}`
|
|
11
|
+
// So there are 2 steps:
|
|
12
|
+
// 1. Change the assumed sample rate
|
|
13
|
+
// 2. Resample to 48Khz
|
|
14
|
+
// 3. Apply playback rate
|
|
15
|
+
const step1SampleRate = sample_rate_1.DEFAULT_SAMPLE_RATE * toneFrequency;
|
|
16
|
+
const newNumberOfFrames = Math.round(numberOfFrames * (sample_rate_1.DEFAULT_SAMPLE_RATE / step1SampleRate));
|
|
17
|
+
const step2Data = new Int16Array(newNumberOfFrames * exports.NUMBER_OF_CHANNELS);
|
|
18
|
+
const chunkSize = numberOfFrames / newNumberOfFrames;
|
|
19
|
+
(0, resample_audiodata_1.resampleAudioData)({
|
|
20
|
+
sourceChannels: audioData,
|
|
21
|
+
destination: step2Data,
|
|
22
|
+
targetFrames: newNumberOfFrames,
|
|
23
|
+
chunkSize,
|
|
24
|
+
});
|
|
25
|
+
const step3Data = (0, change_tempo_1.wsolaInt16Interleaved)(step2Data, exports.NUMBER_OF_CHANNELS, toneFrequency);
|
|
26
|
+
return step3Data;
|
|
27
|
+
};
|
|
28
|
+
exports.applyToneFrequency = applyToneFrequency;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { InlineAudioAsset } from 'remotion/no-react';
|
|
2
|
+
import type { LogLevel } from '../log-level';
|
|
3
|
+
import type { CancelSignal } from '../make-cancel-signal';
|
|
2
4
|
export declare const makeInlineAudioMixing: (dir: string) => {
|
|
3
5
|
cleanup: () => void;
|
|
4
6
|
addAsset: ({ asset, fps, totalNumberOfFrames, firstFrame, trimLeftOffset, trimRightOffset, }: {
|
|
@@ -10,5 +12,11 @@ export declare const makeInlineAudioMixing: (dir: string) => {
|
|
|
10
12
|
trimRightOffset: number;
|
|
11
13
|
}) => void;
|
|
12
14
|
getListOfAssets: () => string[];
|
|
15
|
+
finish: ({ binariesDirectory, indent, logLevel, cancelSignal, }: {
|
|
16
|
+
indent: boolean;
|
|
17
|
+
logLevel: LogLevel;
|
|
18
|
+
binariesDirectory: string | null;
|
|
19
|
+
cancelSignal: CancelSignal | undefined;
|
|
20
|
+
}) => Promise<void>;
|
|
13
21
|
};
|
|
14
22
|
export type InlineAudioMixing = ReturnType<typeof makeInlineAudioMixing>;
|
|
@@ -41,6 +41,7 @@ const node_fs_1 = __importStar(require("node:fs"));
|
|
|
41
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
42
|
const delete_directory_1 = require("../delete-directory");
|
|
43
43
|
const sample_rate_1 = require("../sample-rate");
|
|
44
|
+
const apply_tone_frequency_1 = require("./apply-tone-frequency");
|
|
44
45
|
const download_map_1 = require("./download-map");
|
|
45
46
|
const numberTo32BiIntLittleEndian = (num) => {
|
|
46
47
|
return new Uint8Array([
|
|
@@ -55,11 +56,13 @@ const numberTo16BitLittleEndian = (num) => {
|
|
|
55
56
|
};
|
|
56
57
|
const BIT_DEPTH = 16;
|
|
57
58
|
const BYTES_PER_SAMPLE = BIT_DEPTH / 8;
|
|
59
|
+
const NUMBER_OF_CHANNELS = 2;
|
|
58
60
|
const makeInlineAudioMixing = (dir) => {
|
|
59
61
|
const folderToAdd = (0, download_map_1.makeAndReturn)(dir, 'remotion-inline-audio-mixing');
|
|
60
62
|
// asset id -> file descriptor
|
|
61
63
|
const openFiles = {};
|
|
62
64
|
const writtenHeaders = {};
|
|
65
|
+
const toneFrequencies = {};
|
|
63
66
|
const cleanup = () => {
|
|
64
67
|
for (const fd of Object.values(openFiles)) {
|
|
65
68
|
try {
|
|
@@ -85,11 +88,10 @@ const makeInlineAudioMixing = (dir) => {
|
|
|
85
88
|
}
|
|
86
89
|
writtenHeaders[filePath] = true;
|
|
87
90
|
const expectedDataSize = Math.round((totalNumberOfFrames / fps - trimLeftOffset + trimRightOffset) *
|
|
88
|
-
|
|
91
|
+
NUMBER_OF_CHANNELS *
|
|
89
92
|
sample_rate_1.DEFAULT_SAMPLE_RATE *
|
|
90
93
|
BYTES_PER_SAMPLE);
|
|
91
94
|
const expectedSize = 40 + expectedDataSize;
|
|
92
|
-
const { numberOfChannels } = asset;
|
|
93
95
|
const fd = openFiles[filePath];
|
|
94
96
|
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x52, 0x49, 0x46, 0x46]), 0, 4, 0); // "RIFF"
|
|
95
97
|
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(expectedSize)), 0, 4, 4); // Remaining size
|
|
@@ -97,14 +99,32 @@ const makeInlineAudioMixing = (dir) => {
|
|
|
97
99
|
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x66, 0x6d, 0x74, 0x20]), 0, 4, 12); // "fmt "
|
|
98
100
|
(0, node_fs_1.writeSync)(fd, new Uint8Array([BIT_DEPTH, 0x00, 0x00, 0x00]), 0, 4, 16); // fmt chunk size = 16
|
|
99
101
|
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x01, 0x00]), 0, 2, 20); // Audio format (PCM) = 1, set 3 if float32 would be true
|
|
100
|
-
(0, node_fs_1.writeSync)(fd, new Uint8Array([
|
|
102
|
+
(0, node_fs_1.writeSync)(fd, new Uint8Array([NUMBER_OF_CHANNELS, 0x00]), 0, 2, 22); // Number of channels
|
|
101
103
|
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(sample_rate_1.DEFAULT_SAMPLE_RATE)), 0, 4, 24); // Sample rate
|
|
102
|
-
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(sample_rate_1.DEFAULT_SAMPLE_RATE *
|
|
103
|
-
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo16BitLittleEndian(
|
|
104
|
+
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(sample_rate_1.DEFAULT_SAMPLE_RATE * NUMBER_OF_CHANNELS * BYTES_PER_SAMPLE)), 0, 4, 28); // Byte rate
|
|
105
|
+
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo16BitLittleEndian(NUMBER_OF_CHANNELS * BYTES_PER_SAMPLE)), 0, 2, 32); // Block align
|
|
104
106
|
(0, node_fs_1.writeSync)(fd, numberTo16BitLittleEndian(BIT_DEPTH), 0, 2, 34); // Bits per sample
|
|
105
107
|
(0, node_fs_1.writeSync)(fd, new Uint8Array([0x64, 0x61, 0x74, 0x61]), 0, 4, 36); // "data"
|
|
106
108
|
(0, node_fs_1.writeSync)(fd, new Uint8Array(numberTo32BiIntLittleEndian(expectedDataSize)), 0, 4, 40); // Remaining size
|
|
107
109
|
};
|
|
110
|
+
const finish = async ({ binariesDirectory, indent, logLevel, cancelSignal, }) => {
|
|
111
|
+
for (const fd of Object.keys(openFiles)) {
|
|
112
|
+
const frequency = toneFrequencies[fd];
|
|
113
|
+
if (frequency !== 1) {
|
|
114
|
+
const tmpFile = fd.replace(/.wav$/, '-tmp.wav');
|
|
115
|
+
await (0, apply_tone_frequency_1.applyToneFrequencyUsingFfmpeg)({
|
|
116
|
+
input: fd,
|
|
117
|
+
output: tmpFile,
|
|
118
|
+
toneFrequency: frequency,
|
|
119
|
+
indent,
|
|
120
|
+
logLevel,
|
|
121
|
+
binariesDirectory,
|
|
122
|
+
cancelSignal,
|
|
123
|
+
});
|
|
124
|
+
node_fs_1.default.renameSync(tmpFile, fd);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
108
128
|
const addAsset = ({ asset, fps, totalNumberOfFrames, firstFrame, trimLeftOffset, trimRightOffset, }) => {
|
|
109
129
|
ensureAsset({
|
|
110
130
|
asset,
|
|
@@ -114,7 +134,12 @@ const makeInlineAudioMixing = (dir) => {
|
|
|
114
134
|
trimRightOffset,
|
|
115
135
|
});
|
|
116
136
|
const filePath = getFilePath(asset);
|
|
137
|
+
if (toneFrequencies[filePath] !== undefined &&
|
|
138
|
+
toneFrequencies[filePath] !== asset.toneFrequency) {
|
|
139
|
+
throw new Error(`toneFrequency must be the same across the entire audio, got ${asset.toneFrequency}, but before it was ${toneFrequencies[filePath]}`);
|
|
140
|
+
}
|
|
117
141
|
const fileDescriptor = openFiles[filePath];
|
|
142
|
+
toneFrequencies[filePath] = asset.toneFrequency;
|
|
118
143
|
let arr = new Int16Array(asset.audio);
|
|
119
144
|
const isFirst = asset.frame === firstFrame;
|
|
120
145
|
const isLast = asset.frame === totalNumberOfFrames + firstFrame - 1;
|
|
@@ -129,14 +154,18 @@ const makeInlineAudioMixing = (dir) => {
|
|
|
129
154
|
throw new Error(`samplesToShaveFromStart should be approximately an integer, is ${samplesToShaveFromStart}`);
|
|
130
155
|
}
|
|
131
156
|
if (isFirst) {
|
|
132
|
-
arr = arr.slice(Math.round(samplesToShaveFromStart) *
|
|
157
|
+
arr = arr.slice(Math.round(samplesToShaveFromStart) * NUMBER_OF_CHANNELS);
|
|
133
158
|
}
|
|
134
159
|
if (isLast) {
|
|
135
|
-
arr = arr.slice(0, arr.length + Math.round(samplesToShaveFromEnd) *
|
|
160
|
+
arr = arr.slice(0, arr.length + Math.round(samplesToShaveFromEnd) * NUMBER_OF_CHANNELS);
|
|
136
161
|
}
|
|
137
162
|
const positionInSeconds = (asset.frame - firstFrame) / fps - (isFirst ? 0 : trimLeftOffset);
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
// Always rounding down to ensure there are no gaps when the samples don't align
|
|
164
|
+
// In @remotion/media, we also round down the sample start timestamp and round up the end timestamp
|
|
165
|
+
// This might lead to overlapping, hopefully aligning perfectly!
|
|
166
|
+
// Test case: https://github.com/remotion-dev/remotion/issues/5758
|
|
167
|
+
const position = Math.floor(positionInSeconds * sample_rate_1.DEFAULT_SAMPLE_RATE) *
|
|
168
|
+
NUMBER_OF_CHANNELS *
|
|
140
169
|
BYTES_PER_SAMPLE;
|
|
141
170
|
(0, node_fs_1.writeSync)(
|
|
142
171
|
// fs
|
|
@@ -154,6 +183,7 @@ const makeInlineAudioMixing = (dir) => {
|
|
|
154
183
|
cleanup,
|
|
155
184
|
addAsset,
|
|
156
185
|
getListOfAssets,
|
|
186
|
+
finish,
|
|
157
187
|
};
|
|
158
188
|
};
|
|
159
189
|
exports.makeInlineAudioMixing = makeInlineAudioMixing;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resampleAudioData = void 0;
|
|
4
|
+
const change_tempo_1 = require("./change-tempo");
|
|
5
|
+
const fixFloatingPoint = (value) => {
|
|
6
|
+
if (value % 1 < 0.0000001) {
|
|
7
|
+
return Math.floor(value);
|
|
8
|
+
}
|
|
9
|
+
if (value % 1 > 0.9999999) {
|
|
10
|
+
return Math.ceil(value);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const resampleAudioData = ({ sourceChannels, destination, targetFrames, chunkSize, }) => {
|
|
15
|
+
const getSourceValues = (startUnfixed, endUnfixed, channelIndex) => {
|
|
16
|
+
const start = fixFloatingPoint(startUnfixed);
|
|
17
|
+
const end = fixFloatingPoint(endUnfixed);
|
|
18
|
+
const startFloor = Math.floor(start);
|
|
19
|
+
const startCeil = Math.ceil(start);
|
|
20
|
+
const startFraction = start - startFloor;
|
|
21
|
+
const endFraction = end - Math.floor(end);
|
|
22
|
+
const endFloor = Math.floor(end);
|
|
23
|
+
let weightedSum = 0;
|
|
24
|
+
let totalWeight = 0;
|
|
25
|
+
// Handle first fractional sample
|
|
26
|
+
if (startFraction > 0) {
|
|
27
|
+
const firstSample = sourceChannels[startFloor * change_tempo_1.NUMBER_OF_CHANNELS + channelIndex];
|
|
28
|
+
weightedSum += firstSample * (1 - startFraction);
|
|
29
|
+
totalWeight += 1 - startFraction;
|
|
30
|
+
}
|
|
31
|
+
// Handle full samples
|
|
32
|
+
for (let k = startCeil; k < endFloor; k++) {
|
|
33
|
+
const num = sourceChannels[k * change_tempo_1.NUMBER_OF_CHANNELS + channelIndex];
|
|
34
|
+
weightedSum += num;
|
|
35
|
+
totalWeight += 1;
|
|
36
|
+
}
|
|
37
|
+
// Handle last fractional sample
|
|
38
|
+
if (endFraction > 0) {
|
|
39
|
+
const lastSample = sourceChannels[endFloor * change_tempo_1.NUMBER_OF_CHANNELS + channelIndex];
|
|
40
|
+
weightedSum += lastSample * endFraction;
|
|
41
|
+
totalWeight += endFraction;
|
|
42
|
+
}
|
|
43
|
+
const average = weightedSum / totalWeight;
|
|
44
|
+
return average;
|
|
45
|
+
};
|
|
46
|
+
for (let newFrameIndex = 0; newFrameIndex < targetFrames; newFrameIndex++) {
|
|
47
|
+
const start = newFrameIndex * chunkSize;
|
|
48
|
+
const end = start + chunkSize;
|
|
49
|
+
for (let i = 0; i < change_tempo_1.NUMBER_OF_CHANNELS; i++) {
|
|
50
|
+
destination[newFrameIndex * change_tempo_1.NUMBER_OF_CHANNELS + i] = getSourceValues(start, end, i);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
exports.resampleAudioData = resampleAudioData;
|
package/dist/assets/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type UnsafeAsset = Omit<AudioOrVideoAsset, 'frame' | 'id' | 'volume' | 'm
|
|
|
6
6
|
volume: number[];
|
|
7
7
|
id: string;
|
|
8
8
|
playbackRate: number;
|
|
9
|
-
toneFrequency: number
|
|
9
|
+
toneFrequency: number;
|
|
10
10
|
audioStreamIndex: number;
|
|
11
11
|
};
|
|
12
12
|
export type AssetVolume = number | number[];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { BrowserLog } from '../browser-log';
|
|
17
17
|
import type { LogLevel } from '../log-level';
|
|
18
|
-
import type { Page } from './BrowserPage';
|
|
18
|
+
import type { OnLog, Page } from './BrowserPage';
|
|
19
19
|
import type { BrowserRunner } from './BrowserRunner';
|
|
20
20
|
import type { Connection } from './Connection';
|
|
21
21
|
import { EventEmitter } from './EventEmitter';
|
|
@@ -52,19 +52,21 @@ export declare class HeadlessBrowser extends EventEmitter {
|
|
|
52
52
|
runner: BrowserRunner;
|
|
53
53
|
});
|
|
54
54
|
browserContexts(): BrowserContext[];
|
|
55
|
-
newPage({ context, logLevel, indent, pageIndex, onBrowserLog, }: {
|
|
55
|
+
newPage({ context, logLevel, indent, pageIndex, onBrowserLog, onLog, }: {
|
|
56
56
|
context: SourceMapGetter;
|
|
57
57
|
logLevel: LogLevel;
|
|
58
58
|
indent: boolean;
|
|
59
59
|
pageIndex: number;
|
|
60
60
|
onBrowserLog: null | ((log: BrowserLog) => void);
|
|
61
|
+
onLog: OnLog;
|
|
61
62
|
}): Promise<Page>;
|
|
62
|
-
_createPageInContext({ context, logLevel, indent, pageIndex, onBrowserLog, }: {
|
|
63
|
+
_createPageInContext({ context, logLevel, indent, pageIndex, onBrowserLog, onLog, }: {
|
|
63
64
|
context: SourceMapGetter;
|
|
64
65
|
logLevel: LogLevel;
|
|
65
66
|
indent: boolean;
|
|
66
67
|
pageIndex: number;
|
|
67
68
|
onBrowserLog: null | ((log: BrowserLog) => void);
|
|
69
|
+
onLog: OnLog;
|
|
68
70
|
}): Promise<Page>;
|
|
69
71
|
targets(): Target[];
|
|
70
72
|
waitForTarget(predicate: (x: Target) => boolean | Promise<boolean>, options?: WaitForTargetOptions): Promise<Target>;
|
|
@@ -82,12 +84,13 @@ export declare class BrowserContext extends EventEmitter {
|
|
|
82
84
|
timeout?: number;
|
|
83
85
|
}): Promise<Target>;
|
|
84
86
|
pages(): Promise<Page[]>;
|
|
85
|
-
newPage({ context, logLevel, indent, pageIndex, onBrowserLog, }: {
|
|
87
|
+
newPage({ context, logLevel, indent, pageIndex, onBrowserLog, onLog, }: {
|
|
86
88
|
context: SourceMapGetter;
|
|
87
89
|
logLevel: LogLevel;
|
|
88
90
|
indent: boolean;
|
|
89
91
|
pageIndex: number;
|
|
90
92
|
onBrowserLog: null | ((log: BrowserLog) => void);
|
|
93
|
+
onLog: OnLog;
|
|
91
94
|
}): Promise<Page>;
|
|
92
95
|
browser(): HeadlessBrowser;
|
|
93
96
|
}
|