stitcher function
All checks were successful
NPM Audit Check / Check NPM audit (push) Successful in -2m17s
Unit tests / Unit tests (push) Successful in -2m6s

This commit is contained in:
2025-08-29 16:09:08 -05:00
parent 0c3b5c3be5
commit 749b3e082c
5 changed files with 146 additions and 2 deletions

22
.vscode/launch.json vendored Normal file
View 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"
]
}
]
}

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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
View 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'
]);
});
});