-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ffmpeg and DTX support #20
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"asi": true, | ||
"esnext": true, | ||
"esversion": 6, | ||
"eqeqeq": true, | ||
"camelcase": true, | ||
"funcscope": true, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/** | ||
* Packer options | ||
* | ||
* converters: | ||
* extensions: file extensions which packer should find | ||
* | ||
* pack: list of output formats | ||
* format: output format | ||
* title: title for console output | ||
* force: force reencoding if source and output formats are same | ||
* | ||
* rules: | ||
* encoders: | ||
* dictionary with encode rules | ||
* key: output format | ||
* value: encoder rule | ||
* | ||
* encoder: | ||
* which encoder to use. | ||
* Available encoders: ffmpeg, sox, qac, afconvert | ||
* | ||
* options: | ||
* options to pass to the encoder. | ||
* | ||
* inputFormats: | ||
* supported input formats. If presented, any unsupported format should be decoded to wav. | ||
* | ||
* decoders: | ||
* dictionary with decode rules | ||
* key: input format | ||
* value: decoder rule | ||
* | ||
* decoder: | ||
* which decoder to use | ||
* Available decoders: ffmpeg, sox, xa | ||
* | ||
* force: | ||
* always decode specified format | ||
*/ | ||
|
||
module.exports = { | ||
audio: { | ||
extensions: ['mp3', 'ogg', 'wav', 'xa'], | ||
pack: [ | ||
{ | ||
format: 'ogg', | ||
title: 'better audio performance', | ||
force: true, | ||
}, | ||
{ | ||
format: 'm4a', | ||
title: 'for iOS and Safari', | ||
}, | ||
], | ||
rules: { | ||
encoders: { | ||
// encode ogg with sox | ||
|
||
ogg: { | ||
encoder: 'sox', | ||
options: [ | ||
'-t', 'ogg', | ||
'-C', '3', | ||
], | ||
}, | ||
|
||
// encode ogg with ffmpeg | ||
|
||
// ogg: { | ||
// encoder: 'ffmpeg', | ||
// options: [ | ||
// '-q:a', '6', | ||
// '-c:a', 'libvorbis', | ||
// '-f', 'ogg', | ||
// ], | ||
// }, | ||
|
||
// encode m4a with afconvert | ||
|
||
// m4a: { | ||
// inputFormats: ['wav'], | ||
// encoder: 'afconvert', | ||
// options: [ | ||
// '-f', 'm4af', | ||
// '-b', '128000', | ||
// '-q', '127', | ||
// '-s', '2', | ||
// ], | ||
// }, | ||
|
||
// encode m4a with qaac | ||
|
||
m4a: { | ||
inputFormats: ['wav'], | ||
encoder: 'qaac', | ||
options: ['-c', '128'], | ||
}, | ||
|
||
// encode m4a with ffmpeg | ||
|
||
// m4a: { | ||
// encoder: 'ffmpeg', | ||
// options: [ | ||
// '-b:a', '192k', | ||
// '-c:a', 'aac', | ||
// '-movflags', 'frag_keyframe+empty_moov', | ||
// '-f', 'ipod', | ||
// '-vn', | ||
// ], | ||
// }, | ||
}, | ||
decoders: { | ||
// decode ogg with sox | ||
|
||
ogg: { | ||
decoder: 'sox', | ||
options: [ | ||
'-t', 'wav', | ||
], | ||
}, | ||
|
||
// decode ogg with ffmpeg | ||
|
||
// ogg: { | ||
// decoder: 'ffmpeg', | ||
// options: ['-f', 'wav'], | ||
// }, | ||
|
||
// decode mp3 with sox | ||
|
||
mp3: { | ||
decoder: 'sox', | ||
options: [ | ||
'-t', 'wav', | ||
], | ||
}, | ||
|
||
// decode mp3 with ffmpeg | ||
|
||
// mp3: { | ||
// decoder: 'ffmpeg', | ||
// options: ['-f', 'wav'], | ||
// }, | ||
|
||
// decode xa with xa | ||
|
||
xa: { | ||
decoder: 'xa', | ||
force: true, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,103 +1,94 @@ | ||
|
||
import Promise from 'bluebird' | ||
import co from 'co' | ||
import fs from 'fs' | ||
import Throat from 'throat' | ||
import { cpus } from 'os' | ||
import endpoint from 'endpoint' | ||
import { spawn, execFile as _execFile } from 'child_process' | ||
import co from 'co' | ||
import { extname, basename } from 'path' | ||
|
||
import tmp from './temporary' | ||
|
||
let readFile = Promise.promisify(fs.readFile, fs) | ||
let writeFile = Promise.promisify(fs.writeFile, fs) | ||
let execFile = Promise.promisify(_execFile) | ||
import { cpus } from 'os' | ||
import createConverter from './converters' | ||
import Throat from 'throat' | ||
|
||
let throat = new Throat(cpus().length || 1) | ||
|
||
export class AudioConvertor { | ||
constructor(type, ...extra) { | ||
this._target = type | ||
this._extra = extra | ||
constructor(options, rules) { | ||
this._target = options.format | ||
this._force = options.force | ||
this._rules = rules | ||
} | ||
|
||
convert(file) { | ||
let ext = extname(file.name).toLowerCase() | ||
if (ext === '.' + this._target && !this.force) { | ||
return Promise.resolve(file) | ||
} else { | ||
return co(function*() { | ||
let ext = extname(file.name) | ||
let name = basename(file.name, ext) + '.' + this._target | ||
return this._doConvert(file.path, this._target) | ||
.then(buffer => file.derive(name, buffer)) | ||
} | ||
} | ||
_doConvert(path, type) { | ||
if (type === 'm4a') { | ||
return co(function*() { | ||
let wav = yield this._SoX(path, 'wav') | ||
let prefix = tmp() | ||
let wavPath = prefix + '.wav' | ||
let m4aPath = prefix + '.m4a' | ||
yield writeFile(wavPath, wav) | ||
if (process.platform.match(/^win/)) { | ||
yield execFile('qaac', ['-o', m4aPath, '-c', '128', wavPath]) | ||
} else { | ||
yield execFile('afconvert', [wavPath, m4aPath, '-f', 'm4af', | ||
'-b', '128000', '-q', '127', '-s', '2']) | ||
} | ||
return yield readFile(m4aPath) | ||
}.bind(this)) | ||
} else { | ||
return this._SoX(path, type) | ||
} | ||
let inputFormat = yield this.guessFormat(file) | ||
|
||
// return original file if format did not changed and encoding not forced | ||
if (inputFormat === this._target && !this._force) { | ||
return yield file.derive(name) | ||
} | ||
|
||
// get encode rule for target format | ||
let encodeRule = this._rules.encoders[this._target] | ||
if (!encodeRule) { | ||
return Promise.reject(new Error('Encode rule not found')) | ||
} | ||
|
||
// get decodeRule for source format | ||
let decodeRule = this._rules.decoders[inputFormat] || | ||
this._rules.decoders.default | ||
|
||
let buffer | ||
// decode if encoder does not support source format or source should be force decoded | ||
if (decodeRule.force || | ||
encodeRule.inputFormats && | ||
encodeRule.inputFormats.indexOf(inputFormat) === -1) { | ||
buffer = yield this.decode(file.buffer, inputFormat, decodeRule) | ||
inputFormat = 'wav' | ||
} else { | ||
buffer = file.buffer | ||
} | ||
|
||
// encode buffer to target format | ||
buffer = yield this.encode(buffer, inputFormat, encodeRule) | ||
|
||
return yield Promise.resolve(file.derive(name, buffer)) | ||
}.bind(this)) | ||
} | ||
_SoX(path, type) { | ||
return co(function*() { | ||
let typeArgs = [ ] | ||
try { | ||
let fd = yield Promise.promisify(fs.open, fs)(path, 'r') | ||
let buffer = new Buffer(4) | ||
let read = yield Promise.promisify(fs.read, fs)(fd, buffer, 0, 4, null) | ||
yield Promise.promisify(fs.close, fs)(fd) | ||
if (read === 0) { | ||
console.error('[WARN] Empty keysound file.') | ||
} else if (buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) { | ||
typeArgs = [ '-t', 'mp3' ] | ||
} else if (buffer[0] === 0xFF && buffer[1] === 0xFB) { | ||
typeArgs = [ '-t', 'mp3' ] | ||
} else if (buffer[0] === 0x4F && buffer[1] === 0x67 && buffer[2] === 0x67 && buffer[3] === 0x53) { | ||
typeArgs = [ '-t', 'ogg' ] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure that the audio converter still works in the cases where Sorry for lack of automated testing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure, but every file I have tried was succesfully detected. Btw I pass files to ffmpeg's pipe, so ffmpeg have no information about file extension at all. I believe that ffmpeg itself guesses format by head bytes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure that the audio converter still works in the cases where Sorry for lack of automated testing in |
||
} | ||
} catch (e) { | ||
console.error('[WARN] Unable to detect file type!') | ||
|
||
guessFormat(file) { | ||
return new Promise((resolve, reject) => { | ||
let buffer = file.buffer | ||
|
||
if (buffer.length < 4) { | ||
return reject(new Error('Empty keysound file')) | ||
} | ||
|
||
if (buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) { | ||
resolve('mp3') | ||
} else if (buffer[0] === 0xFF && buffer[1] === 0xFB) { | ||
resolve('mp3') | ||
} else if (buffer[0] === 0x4F && buffer[1] === 0x67 && buffer[2] === 0x67 && buffer[3] === 0x53) { | ||
resolve('ogg') | ||
} else { | ||
let ext = extname(file.name).substr(1).toLowerCase() | ||
resolve(ext) | ||
} | ||
return yield this._doSoX(path, type, typeArgs) | ||
}.bind(this)); | ||
}) | ||
} | ||
_doSoX(path, type, inputTypeArgs) { | ||
return throat(() => new Promise((resolve, reject) => { | ||
let sox = spawn('sox', [...inputTypeArgs, path, '-t', type, ...this._extra, '-']) | ||
sox.stdin.end() | ||
sox.stderr.on('data', x => process.stderr.write(x)) | ||
let data = new Promise((resolve, reject) => { | ||
sox.stdout.pipe(endpoint((err, buffer) => { | ||
if (err) { | ||
console.error('Error reading audio!') | ||
reject(err) | ||
} else { | ||
resolve(buffer) | ||
} | ||
})) | ||
}) | ||
sox.on('close', (code) => { | ||
if (code === 0) { | ||
resolve(data) | ||
} else { | ||
console.error('Unable to convert audio file -- SoX exited ' + code) | ||
reject(new Error('SoX process exited: ' + code)) | ||
} | ||
}) | ||
})) | ||
|
||
decode(buffer, inputFormat, rule) { | ||
let decoder = createConverter(rule.decoder, rule.options) | ||
if (!decoder) { | ||
return Promise.reject(new Error(`Decoder ${rule.decoder} not found`)) | ||
} | ||
|
||
return throat(() => decoder.convert(buffer, inputFormat, rule.options)) | ||
} | ||
|
||
encode(buffer, inputFormat, rule) { | ||
let encoder = createConverter(rule.encoder, rule.options) | ||
if (!encoder) { | ||
return Promise.reject(new Error(`Encoder ${rule.decoder} not found`)) | ||
} | ||
|
||
return throat(() => encoder.convert(buffer, inputFormat, rule.options)) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried to use ffmpeg’s AAC encoder before, however the result doesn’t sound good at 128kbps. Apple’s proprietary encoder (
afconvert
andqaac
) produces a much more superior sound at 128kbps. That’s why I don’t recommend using ffmpeg.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have compared qaac and ffmpeg output but did not hear any significant difference. Maybe my ears not so perfect :)