+22
.vscode/launch.json
+22
.vscode/launch.json
···
1
+
{
2
+
// Use IntelliSense to learn about possible attributes.
3
+
// Hover to view descriptions of existing attributes.
4
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+
"version": "0.2.0",
6
+
"configurations": [
7
+
8
+
{
9
+
"type": "node",
10
+
"request": "launch",
11
+
"name": "Launch Program",
12
+
"skipFiles": [
13
+
"<node_internals>/**"
14
+
],
15
+
"program": "${workspaceFolder}/src/index.ts",
16
+
"preLaunchTask": "tsc: build - tsconfig.json",
17
+
"outFiles": [
18
+
"${workspaceFolder}/distribution/**/*.js"
19
+
]
20
+
}
21
+
]
22
+
}
+2
-1
src/index.ts
+2
-1
src/index.ts
···
2
2
import fsp from 'fs/promises';
3
3
import json5 from 'json5';
4
4
import Sequencer from './sequencer.js';
5
+
import { Stitcher } from './stitcher.js';
5
6
import type {Programs, Segments, Sequences} from './sequencer.js';
6
7
import type { Voices } from './voice.js';
7
8
import type { Options } from 'openweather-api-node';
···
20
21
const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' }));
21
22
const sequence = await Sequencer(config);
22
23
console.log(sequence.join('\n'));
23
-
24
+
await Stitcher(sequence);
24
25
25
26
export type { Config };
+34
src/stitcher.ts
+34
src/stitcher.ts
···
1
+
import { spawn } from 'child_process';
2
+
3
+
const ENCTOOL = process.env['ENCTOOL'] || 'ffmpeg';
4
+
5
+
function ffmpeg(args: string[], files: number): Promise<void> {
6
+
return new Promise((resolve, reject) => {
7
+
console.log(`${ENCTOOL} ${args.join(' ')}`);
8
+
const process = spawn(ENCTOOL, args);
9
+
const to = setTimeout(async () => {
10
+
process.kill();
11
+
reject(new Error('timed out'));
12
+
}, 5000 * files);
13
+
process.on('exit', async (code) => {
14
+
clearTimeout(to);
15
+
if (code !== 0) {
16
+
reject(new Error(`exited with ${code}`));
17
+
}
18
+
else {
19
+
resolve();
20
+
}
21
+
});
22
+
});
23
+
}
24
+
25
+
async function Stitcher(files: string[]) {
26
+
const args: string[] = [];
27
+
files.forEach(f => args.push('-i', f));
28
+
args.push('-filter_complex', `[0:a][1:a][2:a]concat=n=${files.length}:v=0:a=1[out]`);
29
+
args.push('-map', '[out]', '-ar', '44100', '-ac', '2', '-c:a', 'pcm_s16le', 'output.wav');
30
+
await ffmpeg(args, files.length);
31
+
}
32
+
33
+
export default Stitcher;
34
+
export { Stitcher };
+1
-1
test/sequencer.test.ts
+1
-1
test/sequencer.test.ts
···
1
-
import { describe, expect, it, vi, beforeEach } from 'vitest';
1
+
import { describe, expect, it, vi } from 'vitest';
2
2
import { type CurrentWeather, type Options } from 'openweather-api-node';
3
3
import { Sequencer } from '../src/sequencer.js';
4
4
import type { Config } from '../src/index.js';
+87
test/stitcher.test.ts
+87
test/stitcher.test.ts
···
1
+
import { Stitcher } from '../src/stitcher.js';
2
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
3
+
import { EventEmitter } from 'events';
4
+
import { spawn } from 'child_process';
5
+
6
+
const mockChildProcess = new (class MockChildProcess
7
+
extends EventEmitter {
8
+
kill = vi.fn(() => {
9
+
return true;
10
+
});
11
+
})();
12
+
13
+
vi.mock('child_process', () => {
14
+
return {
15
+
spawn: vi.fn(() => mockChildProcess)
16
+
}
17
+
});
18
+
19
+
describe('stitcher', () => {
20
+
21
+
beforeEach(() => {
22
+
vi.clearAllMocks();
23
+
});
24
+
25
+
it('passes the correct arguments to ffmpeg', async () => {
26
+
const p = Stitcher(['1.flac', 'dir/2.flac']);
27
+
mockChildProcess.emit('exit', 0, null);
28
+
await p;
29
+
expect(spawn).toBeCalledWith('ffmpeg', [
30
+
"-i",
31
+
'1.flac',
32
+
'-i',
33
+
'dir/2.flac',
34
+
'-filter_complex',
35
+
'[0:a][1:a][2:a]concat=n=2:v=0:a=1[out]',
36
+
'-map',
37
+
'[out]',
38
+
'-ar',
39
+
'44100',
40
+
'-ac',
41
+
'2',
42
+
'-c:a',
43
+
'pcm_s16le',
44
+
'output.wav'
45
+
]);
46
+
});
47
+
48
+
it('throws an error when ffmpeg fails', async () => {
49
+
const p = Stitcher(['sound.mp3']);
50
+
mockChildProcess.emit('exit', 1, null);
51
+
await expect(p).rejects.toThrow('exited with 1');
52
+
expect(spawn).toBeCalledWith('ffmpeg', [
53
+
"-i",
54
+
'sound.mp3',
55
+
'-filter_complex',
56
+
'[0:a][1:a][2:a]concat=n=1:v=0:a=1[out]',
57
+
'-map',
58
+
'[out]',
59
+
'-ar',
60
+
'44100',
61
+
'-ac',
62
+
'2',
63
+
'-c:a',
64
+
'pcm_s16le',
65
+
'output.wav'
66
+
]);
67
+
});
68
+
69
+
it('throws an error when ffmpeg takes longer than expected', { timeout: 6000 }, async () => {
70
+
await expect(Stitcher(['in.wav'])).rejects.toThrow('timed out');
71
+
expect(spawn).toBeCalledWith('ffmpeg', [
72
+
"-i",
73
+
'in.wav',
74
+
'-filter_complex',
75
+
'[0:a][1:a][2:a]concat=n=1:v=0:a=1[out]',
76
+
'-map',
77
+
'[out]',
78
+
'-ar',
79
+
'44100',
80
+
'-ac',
81
+
'2',
82
+
'-c:a',
83
+
'pcm_s16le',
84
+
'output.wav'
85
+
]);
86
+
});
87
+
});