-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathindex.ts
159 lines (140 loc) · 4.78 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import path from "path";
import ffmpeg from "fluent-ffmpeg";
export type SplitAudioParams = {
mergedTrack: string; // source track
outputDir: string; // directory, where to put the tracks (with all the required slashes)
ffmpegPath?: string; // path to ffmpeg.exe
artist?: string; // meta info, optional
album?: string; // meta info, optional
trackNames?: string[]; // meta info, optional
maxNoiseLevel?: number; // silence is defined below this dB value
minSilenceLength?: number; // (sec) we are searching for silence intervals at least of this lenght
minSongLength?: number; // (sec) if a track is sorter than this, we merge it to the previous track
fastStart?: boolean; // optional flag for faststart
};
export async function splitAudio(params: SplitAudioParams): Promise<void> {
return new Promise((resolve, reject) => {
params.ffmpegPath = params.ffmpegPath || "ffmpeg";
params.maxNoiseLevel = params.maxNoiseLevel || -40;
params.minSilenceLength = params.minSilenceLength || 0.2;
params.minSongLength = params.minSongLength || 20;
const extensionMatch = params.mergedTrack.match(/\w+$/);
if (!extensionMatch) throw new Error(`invalid 'mergedTrack' param`);
const fileExtension = extensionMatch[0];
let ffmpegCommand = ffmpeg()
.setFfmpegPath(params.ffmpegPath)
.input(params.mergedTrack)
.audioFilters(
`silencedetect=noise=${params.maxNoiseLevel}dB:d=${params.minSilenceLength}`
)
.outputFormat("null");
ffmpegCommand
.on("start", (cmdline) => console.log(cmdline))
.on("end", (_, silenceDetectResult) => {
const tracks: Array<{
trackStart: number;
trackEnd: number;
}> = [];
const splitPattern =
/silence_start: ([\w\.]+)[\s\S]+?silence_end: ([\w\.]+)/g;
var silenceInfo: RegExpExecArray | null;
while ((silenceInfo = splitPattern.exec(silenceDetectResult))) {
const [_, silenceStart, silenceEnd] = silenceInfo;
const silenceMiddle = (parseInt(silenceEnd) + parseInt(silenceStart)) / 2;
const trackStart = tracks[tracks.length - 1]?.trackEnd || 0;
const trackEnd = silenceMiddle;
const trackLength = trackEnd - trackStart;
if (trackLength >= params.minSongLength! || tracks.length === 0) {
tracks.push({
trackStart,
trackEnd,
});
} else {
// song is too short -> merge it to the previous one
const lastTrack = tracks[tracks.length - 1];
lastTrack.trackEnd = trackEnd;
tracks[tracks.length - 1] = lastTrack;
}
}
// add last track
if (tracks.length > 0) {
tracks.push({
trackStart: tracks[tracks.length - 1]!.trackEnd,
trackEnd: 999999,
});
}
// split the tracks
const promises = tracks.map((track, index) => {
const trackName =
params.trackNames?.[index] ||
`Track ${(index + 1).toString().padStart(2, "0")}`;
const trackStart = new Date(Math.max(0, track.trackStart * 1000))
.toISOString()
.substr(11, 8);
const trackLength = track.trackEnd - track.trackStart;
return extractAudio({
ffmpegPath: params.ffmpegPath!,
inputTrack: params.mergedTrack,
start: trackStart,
length: trackLength,
artist: params.artist,
album: params.album,
outputTrack: `${params.outputDir + trackName}.${fileExtension}`,
fastStart: params.fastStart,
});
});
Promise.all(promises)
.then(() => resolve())
.catch(reject);
})
.on("error", reject)
.output("-")
.run();
});
}
export type ExtractAudioParams = {
ffmpegPath: string; // path to ffmpeg.exe
inputTrack: string; // source track
start: number | string; // start seconds in the source
length: number; // duration to extract
artist?: string; // meta info, optional
album?: string; // meta info, optional
outputTrack: string; // output track
fastStart?: boolean; // optional flag for faststart
};
export async function extractAudio(params: ExtractAudioParams): Promise<void> {
return new Promise((resolve, reject) => {
const title = path.parse(params.outputTrack).name;
let ffmpegCommand = ffmpeg()
.setFfmpegPath(params.ffmpegPath)
.input(params.inputTrack)
.setStartTime(params.start)
.setDuration(params.length)
.noVideo()
.addOutputOptions("-metadata", `title="${title}"`);
if (params.artist) {
ffmpegCommand = ffmpegCommand.addOutputOptions(
"-metadata",
`artist="${params.artist}"`
);
}
if (params.album) {
ffmpegCommand = ffmpegCommand.addOutputOptions(
"-metadata",
`album="${params.album}"`
);
}
if (params.fastStart) {
ffmpegCommand = ffmpegCommand.addOutputOptions(
"-movflags",
"faststart"
);
}
ffmpegCommand
.outputOptions("-c:a", "copy")
.on("start", (cmdline) => console.log(cmdline))
.on("end", resolve)
.on("error", reject)
.saveToFile(params.outputTrack);
});
}