-
Notifications
You must be signed in to change notification settings - Fork 2
ffmpeg and DTX support #20
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
base: master
Are you sure you want to change the base?
Changes from 3 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 |
|---|---|---|
| @@ -1,103 +1,61 @@ | ||
|
|
||
| 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 { 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) | ||
|
|
||
| let throat = new Throat(cpus().length || 1) | ||
| import xaConvert from 'xa-dtx' | ||
| import ffmpeg from 'fluent-ffmpeg' | ||
| import {ReadableBufferStream, WritableBufferStream} from './bufferstream' | ||
|
|
||
| export class AudioConvertor { | ||
| constructor(type, ...extra) { | ||
| this._target = type | ||
| this._extra = extra | ||
| } | ||
|
|
||
| convert(file) { | ||
| let ext = extname(file.name).toLowerCase() | ||
| if (ext === '.' + this._target && !this.force) { | ||
| let ext = extname(file.name) | ||
| let name = basename(file.name, ext) + '.' + this._target | ||
| ext = ext.toLowerCase().substr(1) | ||
|
|
||
| if (ext === this._target && !this.force) { | ||
| return Promise.resolve(file) | ||
| } else if (ext === 'xa') { | ||
| return xaConvert(file.path).then(wav => { | ||
| return this._doFfmpeg(wav.buffer, 'wav', this._target, name) | ||
| }).then(buffer => file.derive(name, buffer)) | ||
| } else { | ||
| let name = basename(file.name, ext) + '.' + this._target | ||
| return this._doConvert(file.path, this._target) | ||
| return this | ||
| ._doFfmpeg(file.path, ext, this._target, name) | ||
| .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) | ||
| } | ||
| } | ||
| _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' ] | ||
|
Member
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
Author
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.
Member
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!') | ||
|
|
||
| _doFfmpeg(input) { | ||
| return new Promise((resolve, reject) => { | ||
| let readStream | ||
|
|
||
| // init input stream | ||
| if (typeof input === 'string') { | ||
| readStream = fs.createReadStream(input) | ||
| } else { | ||
| readStream = new ReadableBufferStream(input) | ||
| } | ||
| 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)) | ||
| } | ||
| }) | ||
| })) | ||
|
|
||
| let writeStream = new WritableBufferStream() | ||
|
|
||
| // do ffmpeg | ||
| ffmpeg(readStream) | ||
| .output(writeStream) | ||
| .outputOptions(this._extra) | ||
| .on('end', () => { | ||
| // let outBuffer = writeStream._buffer.slice(0, writeStream._bufferPos) | ||
| resolve(writeStream.buffer) | ||
| }) | ||
| .on('error', (err) => { | ||
| console.error('Unable to convert audio file -- ffmpeg exited ' + err) | ||
| reject(new Error('ffmpeg process exited: ' + err)) | ||
| }) | ||
| .run() | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import stream from 'stream' | ||
|
|
||
| export class ReadableBufferStream extends stream.PassThrough { | ||
| constructor(buffer) { | ||
| super() | ||
|
|
||
| this.pause() | ||
| this.end(buffer) | ||
| } | ||
| } | ||
|
|
||
| export class WritableBufferStream extends stream.Writable { | ||
| constructor() { | ||
| super() | ||
|
|
||
| this._buffer = Buffer.alloc(1024 * 64) | ||
| this._bufferPos = 0 | ||
| } | ||
|
|
||
| _write(chunk, encoding, callback) { | ||
| let size = chunk.length | ||
|
|
||
| let newBufferSize = this._buffer.length | ||
| while (size + this._bufferPos > newBufferSize) { | ||
| newBufferSize *= 2 | ||
| } | ||
| if (newBufferSize > this._buffer.length) { | ||
| let newBuffer = Buffer.alloc(newBufferSize) | ||
| this._buffer.copy(newBuffer, 0, 0, this._buffer.length) | ||
| this._buffer = newBuffer | ||
| } | ||
|
|
||
| chunk.copy(this._buffer, this._bufferPos, 0) | ||
| this._bufferPos += size | ||
|
|
||
| callback() | ||
| } | ||
|
|
||
| get buffer() { | ||
| return this._buffer.slice(0, this._bufferPos) | ||
| } | ||
|
Member
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. Have you tried using e.g. https://github.com/samcday/node-stream-buffer?
Author
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. Yes. WritableStreamBuffer works and very similar to mine. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ export class Directory { | |
| this._path = path | ||
| } | ||
| files(pattern) { | ||
| return glob(pattern, { cwd: this._path }) | ||
| return glob(pattern, { cwd: this._path, nocase: true }) | ||
|
Member
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. Thanks for this fix! |
||
| .map(name => readFile(path.join(this._path, name)).then(buffer => | ||
| new FileEntry(this, name, buffer))) | ||
| } | ||
|
|
||
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 (
afconvertandqaac) 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 :)