UNPKG

@gamestdio/audiosprite

Version:

Concat small audio files into single file and export in many formats.

331 lines (330 loc) 14.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const async_1 = __importDefault(require("async")); const underscore_1 = __importDefault(require("underscore")); const glob_1 = __importDefault(require("glob")); // @ts-ignore const ffmpeg_static_1 = __importDefault(require("ffmpeg-static")); ; const defaults = { output: 'output', path: '', export: 'ogg,m4a,mp3,ac3', format: 'default', autoplay: undefined, loop: [], silence: 0, gap: 1, minlength: 0, bitrate: 128, vbr: -1, 'vbr:vorbis': -1, samplerate: 44100, channels: 1, rawparts: '', ignorerounding: 0, logger: { debug: function () { }, info: function () { }, log: function () { } } }; function default_1(files, options) { return new Promise((resolve, reject) => { if (!files || !files.length) { return reject(new Error('No files provided')); } else { files = underscore_1.default.flatten(files.map(file => glob_1.default.sync(file))); } options = underscore_1.default.extend({}, defaults, options); // make sure output directory exists const outputDir = path_1.default.dirname(options.output); if (!fs_1.default.existsSync(outputDir)) { require('mkdirp').sync(outputDir); } let offsetCursor = 0; const wavArgs = ['-ar', `${options.samplerate}`, '-ac', `${options.channels}`, '-f', 's16le']; const tempFile = mktemp('audiosprite'); options.logger.debug('Created temporary file', { file: tempFile }); const json = { resources: [], spritemap: {} }; spawn(ffmpeg_static_1.default, ['-version']).on('exit', code => { if (code) { return reject(new Error('ffmpeg was not found on your path')); } if (options.silence) { json.spritemap.silence = { start: 0, end: options.silence, loop: true }; if (!options.autoplay) { json.autoplay = 'silence'; } appendSilence(options.silence + options.gap, tempFile, processFiles); } else { processFiles(); } }); function mktemp(prefix) { var tmpdir = require('os').tmpdir() || '.'; return path_1.default.join(tmpdir, prefix + '.' + Math.random().toString().substr(2)); } function spawn(name, opt) { options.logger.debug('Spawn', { cmd: [name].concat(opt).join(' ') }); return require('child_process').spawn(name, opt); } function pad(num, size) { var str = num.toString(); while (str.length < size) { str = '0' + str; } return str; } function makeRawAudioFile(src, cb) { var dest = mktemp('audiosprite'); options.logger.debug('Start processing', { file: src }); fs_1.default.exists(src, function (exists) { if (exists) { let code = -1; let signal = undefined; const ffmpeg = spawn(ffmpeg_static_1.default, ['-i', path_1.default.resolve(src)].concat(wavArgs).concat('pipe:')); const streamFinished = underscore_1.default.after(2, function () { if (code) { return cb({ msg: 'File could not be added', file: src, retcode: code, signal: signal }); } cb(null, dest); }); const writeStream = fs_1.default.createWriteStream(dest, { flags: 'w' }); ffmpeg.stdout.pipe(writeStream); writeStream.on('close', () => streamFinished()); ffmpeg.on('close', function (_code, _signal) { code = _code; signal = _signal; streamFinished(); }); } else { cb({ msg: 'File does not exist', file: src }); } }); } function appendFile(name, src, dest, cb) { var size = 0; var reader = fs_1.default.createReadStream(src); var writer = fs_1.default.createWriteStream(dest, { flags: 'a' }); reader.on('data', function (data) { size += data.length; }); reader.on('close', function () { var originalDuration = size / options.samplerate / options.channels / 2; options.logger.info('File added OK', { file: src, duration: originalDuration }); var extraDuration = Math.max(0, options.minlength - originalDuration); var duration = originalDuration + extraDuration; json.spritemap[name] = { start: offsetCursor, end: offsetCursor + duration, loop: name === options.autoplay || options.loop.indexOf(name) !== -1 }; offsetCursor += originalDuration; var delta = Math.ceil(duration) - duration; if (options.ignorerounding) { options.logger.info('Ignoring nearest second silence gap rounding'); extraDuration = 0; delta = 0; } appendSilence(extraDuration + delta + options.gap, dest, cb); }); reader.pipe(writer); } function appendSilence(duration, dest, cb) { var buffer = Buffer.alloc(Math.round(options.samplerate * 2 * options.channels * duration), 0); var writeStream = fs_1.default.createWriteStream(dest, { flags: 'a' }); writeStream.end(buffer); writeStream.on('close', function () { options.logger.info('Silence gap added', { duration: duration }); offsetCursor += duration; cb(); }); } function exportFile(src, dest, ext, opt, store, cb) { var outfile = dest + '.' + ext; spawn(ffmpeg_static_1.default, ['-y', '-ar', options.samplerate, '-ac', options.channels, '-f', 's16le', '-i', src] .concat(opt).concat(outfile)) .on('exit', function (code, signal) { if (code) { return cb({ msg: 'Error exporting file', format: ext, retcode: code, signal: signal }); } if (ext === 'aiff') { exportFileCaf(outfile, dest + '.caf', function (err) { if (!err && store) { json.resources.push(dest + '.caf'); } fs_1.default.unlinkSync(outfile); cb(); }); } else { options.logger.info('Exported ' + ext + ' OK', { file: outfile }); if (store) { json.resources.push(outfile); } cb(); } }); } function exportFileCaf(src, dest, cb) { if (process.platform !== 'darwin') { return cb(true); } spawn('afconvert', ['-f', 'caff', '-d', 'ima4', src, dest]) .on('exit', function (code, signal) { if (code) { return cb({ msg: 'Error exporting file', format: 'caf', retcode: code, signal: signal }); } options.logger.info('Exported caf OK', { file: dest }); return cb(); }); } function processFiles() { let formats = { aiff: [], wav: [], ac3: ['-acodec', 'ac3', '-ab', options.bitrate + 'k'], mp3: ['-ar', `${options.samplerate}`, '-f', 'mp3'], mp4: ['-ab', options.bitrate + 'k'], m4a: ['-ab', options.bitrate + 'k', '-strict', '-2'], ogg: ['-acodec', 'libvorbis', '-f', 'ogg', '-ab', options.bitrate + 'k'], opus: ['-acodec', 'libopus', '-ab', options.bitrate + 'k'], webm: ['-acodec', 'libvorbis', '-f', 'webm', '-dash', '1'], }; if (options.vbr >= 0 && options.vbr <= 9) { formats.mp3 = formats.mp3.concat(['-aq', `${options.vbr}`]); } else { formats.mp3 = formats.mp3.concat(['-ab', options.bitrate + 'k']); } // change quality of webm output - https://trac.ffmpeg.org/wiki/TheoraVorbisEncodingGuide if (options['vbr:vorbis'] >= 0 && options['vbr:vorbis'] <= 10) { formats.webm = formats.webm.concat(['-qscale:a', String(options['vbr:vorbis'])]); } else { formats.webm = formats.webm.concat(['-ab', options.bitrate + 'k']); } if (typeof (options.export) === "string") { formats = options.export.split(',').reduce(function (memo, val) { if (formats[val]) { memo[val] = formats[val]; } return memo; }, {}); } var rawparts = typeof (options.rawparts) === "string" ? options.rawparts.split(',') : null; var i = 0; options.logger.info(files); async_1.default.forEachSeries(files, function (file, cb) { i++; makeRawAudioFile(file, function (err, tmp) { if (err) { options.logger.debug(err); return cb(err); } function tempProcessed() { fs_1.default.unlinkSync(tmp); cb(); } var name = path_1.default.basename(file).replace(/\.[a-zA-Z0-9]+$/, ''); appendFile(name, tmp, tempFile, function (err) { if (rawparts != null ? rawparts.length : void 0) { async_1.default.forEachSeries(rawparts, function (ext, cb) { options.logger.debug('Start export slice', { name: name, format: ext, i: i }); exportFile(tmp, options.output + '_' + pad(i, 3), ext, formats[ext], false, cb); }, tempProcessed); } else { tempProcessed(); } }); }); }, function (err) { if (err) { return reject(new Error(`Error adding file ${err.message}`)); } async_1.default.forEachSeries(Object.keys(formats), function (ext, cb) { options.logger.debug('Start export', { format: ext }); exportFile(tempFile, options.output, ext, formats[ext], true, cb); }, function (err) { if (err) { return reject(new Error('Error exporting file')); } if (options.autoplay) { json.autoplay = options.autoplay; } json.resources = json.resources.map(function (e) { return options.path ? path_1.default.join(options.path, path_1.default.basename(e)) : e; }); var finalJson = {}; switch (options.format) { case 'howler': case 'howler2': finalJson[options.format === 'howler' ? 'urls' : 'src'] = [].concat(json.resources); finalJson.sprite = {}; for (var sn in json.spritemap) { var spriteInfo = json.spritemap[sn]; finalJson.sprite[sn] = [spriteInfo.start * 1000, (spriteInfo.end - spriteInfo.start) * 1000]; if (spriteInfo.loop) { finalJson.sprite[sn].push(true); } } break; case 'createjs': finalJson.src = json.resources[0]; finalJson.data = { audioSprite: [] }; for (var sn in json.spritemap) { var spriteInfo = json.spritemap[sn]; finalJson.data.audioSprite.push({ id: sn, startTime: spriteInfo.start * 1000, duration: (spriteInfo.end - spriteInfo.start) * 1000 }); } break; case 'default': default: finalJson = json; break; } fs_1.default.unlinkSync(tempFile); return resolve(finalJson); }); }); } }); } exports.default = default_1;