Skip to content
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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .jshintrc
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,
Expand Down
154 changes: 154 additions & 0 deletions config/packer.js
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,
},
},
},
}
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
"homepage": "https://github.com/bemusic/bemuse-tools",
"dependencies": {
"babel": "^5.4.3",
"bemuse-indexer": "^3.1.0",
"bemuse-indexer": "drummaniac/bemuse-indexer#dtx-support",
"bluebird": "^2.9.9",
"bytes": "^1.0.0",
"chalk": "^1.0.0",
"co": "^4.0.2",
"cors": "^2.7.1",
"endpoint": "^0.4.2",
"express": "^4.12.4",
"fluent-ffmpeg": "^2.1.2",
"format-json": "^1.0.3",
"glob": "^4.3.5",
"gulp-util": "^3.0.1",
Expand All @@ -40,7 +41,8 @@
"mkdirp": "^0.5.0",
"rx": "^2.5.3",
"temp": "^0.8.1",
"throat": "^1.0.0"
"throat": "^1.0.0",
"xa-dtx": "^0.0.4"
},
"devDependencies": {
"chai": "^1.10.0",
Expand Down
169 changes: 80 additions & 89 deletions src/audio.js
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',
Copy link
Member

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 and qaac) produces a much more superior sound at 128kbps. That’s why I don’t recommend using ffmpeg.

Copy link
Author

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 :)

'-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' ]
Copy link
Member

Choose a reason for hiding this comment

The 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 mp3/ogg files have .wav extension and vice versa? I do this manual format detection because many BMS archives have this problem.

Sorry for lack of automated testing in audio.js...

Copy link
Author

@mugabe mugabe Apr 17, 2018

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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 mp3/ogg files have .wav extension and vice versa? I do this manual format detection because many BMS archives have this problem.

Sorry for lack of automated testing in audio.js...

}
} 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))
}
}

Expand Down
Loading