stitcher function
This commit is contained in:
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/src/index.ts",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/distribution/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -2,6 +2,7 @@ import path from 'path';
|
||||
import fsp from 'fs/promises';
|
||||
import json5 from 'json5';
|
||||
import Sequencer from './sequencer.js';
|
||||
import { Stitcher } from './stitcher.js';
|
||||
import type {Programs, Segments, Sequences} from './sequencer.js';
|
||||
import type { Voices } from './voice.js';
|
||||
import type { Options } from 'openweather-api-node';
|
||||
@@ -20,6 +21,6 @@ console.log('morning-report\nCory Sanin 2025\n');
|
||||
const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' }));
|
||||
const sequence = await Sequencer(config);
|
||||
console.log(sequence.join('\n'));
|
||||
|
||||
await Stitcher(sequence);
|
||||
|
||||
export type { Config };
|
||||
|
@@ -0,0 +1,34 @@
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const ENCTOOL = process.env['ENCTOOL'] || 'ffmpeg';
|
||||
|
||||
function ffmpeg(args: string[], files: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`${ENCTOOL} ${args.join(' ')}`);
|
||||
const process = spawn(ENCTOOL, args);
|
||||
const to = setTimeout(async () => {
|
||||
process.kill();
|
||||
reject(new Error('timed out'));
|
||||
}, 5000 * files);
|
||||
process.on('exit', async (code) => {
|
||||
clearTimeout(to);
|
||||
if (code !== 0) {
|
||||
reject(new Error(`exited with ${code}`));
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function Stitcher(files: string[]) {
|
||||
const args: string[] = [];
|
||||
files.forEach(f => args.push('-i', f));
|
||||
args.push('-filter_complex', `[0:a][1:a][2:a]concat=n=${files.length}:v=0:a=1[out]`);
|
||||
args.push('-map', '[out]', '-ar', '44100', '-ac', '2', '-c:a', 'pcm_s16le', 'output.wav');
|
||||
await ffmpeg(args, files.length);
|
||||
}
|
||||
|
||||
export default Stitcher;
|
||||
export { Stitcher };
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { type CurrentWeather, type Options } from 'openweather-api-node';
|
||||
import { Sequencer } from '../src/sequencer.js';
|
||||
import type { Config } from '../src/index.js';
|
||||
|
87
test/stitcher.test.ts
Normal file
87
test/stitcher.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Stitcher } from '../src/stitcher.js';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { EventEmitter } from 'events';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const mockChildProcess = new (class MockChildProcess
|
||||
extends EventEmitter {
|
||||
kill = vi.fn(() => {
|
||||
return true;
|
||||
});
|
||||
})();
|
||||
|
||||
vi.mock('child_process', () => {
|
||||
return {
|
||||
spawn: vi.fn(() => mockChildProcess)
|
||||
}
|
||||
});
|
||||
|
||||
describe('stitcher', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('passes the correct arguments to ffmpeg', async () => {
|
||||
const p = Stitcher(['1.flac', 'dir/2.flac']);
|
||||
mockChildProcess.emit('exit', 0, null);
|
||||
await p;
|
||||
expect(spawn).toBeCalledWith('ffmpeg', [
|
||||
"-i",
|
||||
'1.flac',
|
||||
'-i',
|
||||
'dir/2.flac',
|
||||
'-filter_complex',
|
||||
'[0:a][1:a][2:a]concat=n=2:v=0:a=1[out]',
|
||||
'-map',
|
||||
'[out]',
|
||||
'-ar',
|
||||
'44100',
|
||||
'-ac',
|
||||
'2',
|
||||
'-c:a',
|
||||
'pcm_s16le',
|
||||
'output.wav'
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws an error when ffmpeg fails', async () => {
|
||||
const p = Stitcher(['sound.mp3']);
|
||||
mockChildProcess.emit('exit', 1, null);
|
||||
await expect(p).rejects.toThrow('exited with 1');
|
||||
expect(spawn).toBeCalledWith('ffmpeg', [
|
||||
"-i",
|
||||
'sound.mp3',
|
||||
'-filter_complex',
|
||||
'[0:a][1:a][2:a]concat=n=1:v=0:a=1[out]',
|
||||
'-map',
|
||||
'[out]',
|
||||
'-ar',
|
||||
'44100',
|
||||
'-ac',
|
||||
'2',
|
||||
'-c:a',
|
||||
'pcm_s16le',
|
||||
'output.wav'
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws an error when ffmpeg takes longer than expected', { timeout: 6000 }, async () => {
|
||||
await expect(Stitcher(['in.wav'])).rejects.toThrow('timed out');
|
||||
expect(spawn).toBeCalledWith('ffmpeg', [
|
||||
"-i",
|
||||
'in.wav',
|
||||
'-filter_complex',
|
||||
'[0:a][1:a][2:a]concat=n=1:v=0:a=1[out]',
|
||||
'-map',
|
||||
'[out]',
|
||||
'-ar',
|
||||
'44100',
|
||||
'-ac',
|
||||
'2',
|
||||
'-c:a',
|
||||
'pcm_s16le',
|
||||
'output.wav'
|
||||
]);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user