audio-splitter
Version:
Simple package to split a merged audio track to parts by silence analysis
160 lines (140 loc) • 4.9 kB
text/typescript
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);
});
}