@gamestdio/audiosprite
Version:
Concat small audio files into single file and export in many formats.
331 lines (330 loc) • 14.4 kB
JavaScript
;
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;