sox-audio
Version:
A NodeJS interface to SoX audio utilities
323 lines (262 loc) • 7.29 kB
JavaScript
;
var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');
var utils = require('./utils');
var through = require('through');
/* Processor methods */
module.exports = function(proto) {
proto._spawnSox = function(args, options, processCB, endCB) {
utils.which('sox', function(err, soxPath) {
if (err) {
return endCB(err);
} else if (!soxPath || soxPath.length === 0) {
return endCB(new Error('Cannot find sox'));
}
var stdout = null;
var stdoutClosed = false;
var stderr = null;
var stderrClosed = false;
var soxProc = spawn(soxPath, args, options);
if (soxProc.stderr && options.captureStderr) {
soxProc.stderr.setEncoding('utf8');
}
soxProc.on('error', function(err) {
endCB(err);
});
var exitError = null;
function handleExit(err) {
if (err) {
exitError = err;
}
if (processExited &&
(stdoutClosed || !options.captureStdout) &&
(stderrClosed || !options.captureStderr)) {
endCB(exitError, stdout, stderr);
}
}
// Handle process exit
var processExited = false;
soxProc.on('exit', function(code, signal) {
processExited = true;
console.log('Sox process exited with code ' + code +
' and signal ' + signal);
if (signal) {
handleExit(new Error('Sox process was killed with signal ' + signal));
} else if (code) {
handleExit(new Error('Sox process exited with code ' + code));
} else {
handleExit();
}
});
if (options.captureStdout) {
stdout = '';
soxProc.stdout.on('data', function(data) {
stdout += data;
});
soxProc.stdout.on('close', function() {
stdoutClosed = true;
handleExit();
});
}
if (options.captureStderr) {
stderr = '';
soxProc.stderr.on('data', function(data) {
stderr += data;
});
soxProc.stderr.on('close', function() {
stderrClosed = true;
handleExit();
});
}
// Call process callback
processCB(soxProc);
});
};
proto._getArguments = function() {
var fileOutput = this._outputs.some(function(output) {
return output.isFile;
});
// Global options
var globalOptions = this._global.get();
var inputArguments = this._inputs.reduce(function(args, input) {
var source;
var options = input.options.get();
if (typeof input.source === 'string') {
source = input.source;
} else {
// The input source is a SoxCommand, so call _getArguments on it
if (input.isSubCommand) {
var subCommandArgs = input.source._getArguments();
source = '|sox ' + subCommandArgs.join(' ');
// The input is a stream, so read from stdin
} else if (input.isStream) {
source = '-';
}
}
// For each input, add input options, then '<source>'
return args.concat(
options,
[source]
);
}, []);
// Outputs and output options
var outputArguments = this._outputs.reduce(function(args, output) {
var outputArg;
if (!output.target) {
outputArg = [];
} else if (typeof output.target === 'string') {
outputArg = [output.target];
} else {
outputArg = ['-'];
}
return args.concat(
output.options.get(),
outputArg
);
}, []);
// Effects and effect options
var effectArguments = this._effects.reduce(function(args, effect) {
var effectName = [effect.name];
return args.concat(
effectName,
effect.options.get()
);
}, []);
return [].concat(
globalOptions,
inputArguments,
outputArguments,
effectArguments
);
};
proto._prepare = function(callback, readMetadata) {
// TODO: Check codecs and formats and other pre-requisites for execution of the command
var args;
try {
args = this._getArguments();
} catch(e) {
return callback(e)
}
callback(null, args);
};
proto.run = function() {
var self = this;
var outputPresent = this._outputs.some(function(output) {
return 'target' in output;
});
if (!outputPresent) {
throw new Error('No output specified');
}
var outputStream = this._outputs.filter(function(output) {
return output.isStream;
})[0];
var inputStream = this._inputs.filter(function(input) {
return input.isStream;
})[0];
var ended = false;
function emitEnd(err, stdout, stderr) {
if (!ended) {
ended = true;
if (err) {
self.emit('error', err, stdout, stderr);
} else {
self.emit('end', stdout, stderr);
}
}
}
self._prepare(function(err, args) {
if (err) {
return emitEnd(err);
}
// Run sox command
var stdout = null;
var stderr = '';
self.emit('prepare', args);
self._spawnSox(
args,
{},
function processCB(soxProc) {
self.soxProc = soxProc;
self.emit('start', 'sox ' + args.join(' '));
// Pipe input stream if any
if (inputStream) {
console.log('Resume input stream');
inputStream.source.on('error', function(err) {
emitEnd(new Error('Input stream error: ' + err.message));
soxProc.kill();
});
var dataCounter = 0;
inputStream.source.on('end', function() {
console.log('\nInput stream ended. Bytes read: ', dataCounter);
});
// Hacky fix for piping raw input, the 2ms delay helps sox
var WAIT_TIME = 2;
var passThroughStream = through(
function write(data){
var queueTheData = function(self, data) {
return function() {
dataCounter+= data.length;
self.queue(data);
};
};
setTimeout(queueTheData(this, data), WAIT_TIME);
},
function end(){
var self = this;
setTimeout(function() {
self.queue(null);
}, WAIT_TIME);
console.log('End of pass through');
});
inputStream.source
.pipe(passThroughStream)
.pipe(soxProc.stdin);
// inputStream.source.resume();
// Set stdin error handler on sox
soxProc.stdin.on('error', function() {});
}
// TODO: Setup timeout if requested
if (outputStream) {
console.log('Streaming output required');
// Pipe sox stdout to output stream
soxProc.stdout.pipe(outputStream.target, outputStream.pipeopts);
// Handle output stream events
outputStream.target.on('close', function() {
console.log('Cmd Output Stream closed');
setTimeout(function() {
emitEnd(new Error('Output stream closed'));
soxProc.kill();
}, 20);
});
outputStream.target.on('error', function(err) {
console.log('Cmd Output stream error!', err.message);
emitEnd(new Error('Output stream error: ' + err.message));
soxProc.kill();
});
} else {
// Gather sox stdout
stdout = '';
soxProc.stdout.on('data', function(data) {
stdout += data;
});
}
soxProc.stderr.on('data', function(data) {
stderr += data;
});
},
function endCB(err) {
delete self.soxProc;
if (err) {
if (err.message.match(/sox exited with code/)) {
// Add sox error message
// err.message += ': ' + utils.
}
emitEnd(err, stdout, stderr);
} else {
emitEnd(null, stdout, stderr);
}
});
})
};
};