FLAC / MP3 / WAV 批量转 aac

自动检查 ffmpeg 是否有启用 fdkacc-lib,如果没有则使用 native_aac 编码器。

关于编译带有 fdk 支援的 ffmpeg 请参见官方文档;因为 GPL 病毒的原因,就不放出编译版了。

#!/usr/bin/env node
/**
 * FLAC/MP3 to aac script
 * MIT License
 * (C) 2019 Jixun.Moe
 */

/* configuration */
const srcExt = ['flac', 'mp3', 'wav'];
const maxThreads = 4;

/*
 * override with command line options
 * node to_acc.js ./rip ./aac
 */
const srcDir  = process.argv[2] || './rip';
const destDir = process.argv[3] || './aac';

/* import modules */
const fs = require('fs');
const util = require('util');
const readdir = util.promisify(fs.readdir);
const copyFile = util.promisify(fs.copyFile);
const exec = util.promisify(require('child_process').execFile);

/* global variables */
let threads = [];
let files = [];
let aacCodec = 'aac';

function getExt(name) {
    const m = name.toLowerCase().match(/\.(\w+)$/);
    if (m) return m[1];
    return '';
}

async function scanDir(dir, outDir) {
    const scanResult = (await readdir(dir)).filter(d => d[0] != '.');
    for (let i = 0; i < scanResult.length; i++) {
        const inPath = dir + '/' + scanResult[i];
        let outPath = outDir + '/' + scanResult[i];

        const stat = fs.statSync(inPath);

        // sync directory structure.
        if (stat.isDirectory()) {
            // create subdirectory
            try {
                fs.statSync(outPath);
            } catch {
                fs.mkdirSync(outPath);
            }
            await scanDir(inPath, outPath);
            continue;
        }

        const inExt = getExt(inPath);

        // Music file to convert
        if (srcExt.indexOf(inExt) != -1) {
            outPath = outPath.slice(0, -inExt.length) + 'aac';

            try {
                fs.statSync(outPath);

                // file exists, skip...
                continue;
            } catch {}

            files.push([inPath, outPath]);
            continue;
        }
        
        // other file type
        console.info('copy %s...', inPath);
        await copyFile(inPath, outPath);
    }
}

async function doWork(thread) {
    while (true) {
        let work = files.shift();
        if (!work) break;

        const [input, output] = work;
        console.info('[T%d] converting %s...', thread, input);
        await exec('ffmpeg', ['-i', input, '-c:a', aacCodec, '-vbr', '3', output]);
    }

    console.info('thread T%d complete.', thread);
}

async function detectFdk() {
    const {stdout, stderr} = await exec('ffmpeg', ['-codecs']);

    // test for libfdk
    const useFdk = /encoders:[\w\s]* libfdk_aac/.test(stdout);
    console.info('libfdk: %s', useFdk);

    aacCodec = useFdk ? 'libfdk_aac' : 'aac';
}

async function main() {
    // let detect and scan run at the same time.
    const detect = detectFdk();
    await scanDir(srcDir, destDir);
    await detect;

    console.info('to process: %d files.', files.length);

    // create threads
    for(let i = 1; i <= maxThreads; i++) {
        threads.push(doWork(i));
    }

    // wait for thread complete
    // 感谢 orzFly
    await Promise.all(threads);
    
    console.info('done');
}

main();
Jixun的头像

Jixun