vgm-conv
Version:
Chip-type and clock converter for VGM.
463 lines (462 loc) • 17 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const command_line_args_1 = __importDefault(require("command-line-args"));
const command_line_usage_1 = __importDefault(require("command-line-usage"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const index_1 = __importDefault(require("./index"));
const vgm_parser_1 = require("vgm-parser");
const optionDefinitions = [
{
name: "input",
alias: "i",
typeLabel: "{underline file}",
defaultOption: true,
description: "Input VGM file. Standard input will be used if not specified.",
},
{
name: "from",
alias: "f",
typeLabel: "{underline chip}",
description: "Specify source chip type. The first detected chip in the input file is used if not specified.",
type: String,
},
{
name: "to",
alias: "t",
typeLabel: "{underline chip}",
description: "Specify destination chip type.",
type: String,
},
{
name: "clock",
alias: "c",
typeLabel: "{underline clock}",
description: "Specify clock in Hz of destination chip. The typical clock value will be applied if not specified.",
type: Number,
},
{
name: "define",
alias: "D",
typeLabel: "{underline name=value}",
description: "Define converter option variable. See below.",
lazyMultiple: true,
type: String,
},
{
name: "output",
alias: "o",
typeLabel: "{underline file}",
description: "Output VGM file. The standard output is used if not speicified. If the given file is *.vgz, the output will be compressed.",
type: String,
},
{
name: "no-gd3",
description: "Remove GD3 tag from output.",
type: Boolean,
},
{
name: "voice-table",
typeLabel: "{underline file}",
description: "Specify the voice table file in JavaScript.",
type: String,
},
{
name: "voiceTable",
typeLabel: "{underline file}",
description: "(deprecated) Specify the voice table file in JavaScript.",
type: String,
},
{
name: "version",
alias: "v",
description: "Show version.",
type: Boolean,
},
{
name: "help",
alias: "h",
description: "Show this help.",
type: Boolean,
},
];
const sections = [
{
header: "vgm-conv",
content: "Chip-type and clock converter for VGM.",
},
{
header: "SYNOPSIS",
content: ["{underline vgm-conv} [<option>] <file>"],
},
{
header: "OPTIONS",
optionList: optionDefinitions,
},
{
header: "CLOCK CONVERSION",
content: [
{ chip: "{bold AVAILABLE CHIPS}" },
{ chip: "sn76489, ym2612" },
{ chip: "ay8910, ym2151, ym2203, ym2608" },
{ chip: "ym3812, ym3526, y8950, ymf262" },
{ chip: "ym2413" },
],
},
{
header: "CHIP CONVERSION",
content: [
{ from: "{bold FROM}", to: "{bold TO}" },
{ from: "ay8910", to: "ym2203, ym2608, ym3812, y8950, ym3526, ymf262" },
{ from: "sn76489", to: "ay8910, ym2203, ym2608, ym2612" },
{ from: "ym2413", to: "ym2608, ym3812, y8950, ym3526, ymf262" },
{ from: "ym2203, ym2203.fm", to: "ym2413" },
{ from: "ym2608, ym2608.fm, ym2608.r", to: "ym2413" },
{ from: "ym2203.ssg", to: "ay8910" },
{ from: "ym2608.ssg", to: "ay8910" },
{ from: "ym2612, ym2612.fm, ym2612.dac", to: "ym2413" },
{ from: "ym2203, ym2612.fm, ym2608.fm", to: "ym2151" },
{ from: "ym3812", to: "y8950, ym3526, ym2413" },
{ from: "y8950, ym3526", to: "ym3812, ym2413" },
],
},
{
header: "YM2203 to OPL (YM3812/Y8950/YM3526/YMF262) OPTIONS",
content: [
{
def: "{bold -D} ssgAttenuation={underline n}",
desc: "Set SSG volume attenuation level to 0.75*{underline n}(dB). The effective range is -63<={underline n}<=63.",
},
],
},
{
header: "OPN (YM2203/YM2608/YM2612) to YM2413 OPTIONS",
content: [
{
def: "{bold -D} opmOutput={underline filename}",
desc: "Output VOPM voice file. VOPM voices are only logged if this is included.",
},
],
},
{
header: "AY8910 to YM2151 OPTIONS",
content: [
{
def: "{bold -D} squareWaveAttenuation={underline n}",
desc: `Volume attenuation for the SSG square tone. 0<=n<=127. The default value is 8.`,
},
{
def: "{bold -D} whiteNoiseAttenuation={underline n}",
desc: `Volume attenuation for the SSG noice. 0<=n<=127. The default value is 0.`,
},
],
},
{
header: "YM2413 to OPL (YM3812/Y8950/YM3526/YMF262) OPTIONS",
content: [
{
def: "{bold -D} opllVariant=ym2413|vrc7|ymf281b",
desc: `Specify the YM2413 variant name. If not specified, it is detected from the VGM header.`,
},
],
},
{
header: "YM2413 to YM2608 OPTIONS",
content: [
{
def: "{bold -D} opllVariant=ym2413|vrc7|ymf281b",
desc: `Specify the YM2413 variant name. If not specified, it is detected from the VGM header.`,
},
],
},
{
header: "YM2203 to YM2151 OPTIONS",
content: [
{
def: "{bold -D} ssgAttenuation={underline n}",
desc: `Volume attenuation for the SSG. 0<=n<=127. The default value is 8.`,
},
{
def: "{bold -D} squareWaveAttenuation={underline n}",
desc: `Additional volume attenuation for the SSG square tone. 0<=n<=127. The default value is 0.`,
},
{
def: "{bold -D} whiteNoiseAttenuation={underline n}",
desc: `Additional volume attenuation for the SSG noice. 0<=n<=127. The default value is 68.`,
},
],
},
{
header: "YM2612 to YM2413 OPTIONS",
content: [
{
def: "{bold -D} decimation={underline n}",
desc: "Decimate 1 of {underline n} PCM data. 2 to 4 is recommended if USB serial device (like SPFM) is used to play VGM. {underline n}=0 disables the feature and results the best playback quality. The default value is 4.",
},
{
def: "{bold -D} dacEmulation=fmpcm|test|none",
desc: `fmpcm: use the pseudo 6-bit DAC emulation on FM channels is used (default).
test: use YM2413 test mode to realize 7.5bit DAC but this disables all FM channels.
none: disable DAC emulation (default).`,
},
],
},
{
header: "SN76489 to AY8910 OPTIONS",
content: [
{
def: "{bold -D} mixChannel={underline value}",
desc: `Specify the AY8910 channel used for noise output. The value must be one of 0, 1, 2 or none. The default value is 2.
If 'none' is specified, all noise part will be silent.
Since AY8910 has no independent noise channel, SN76489's noise channel will be mixed with a tone channel into the single AY8910's channel specified by this option.`,
},
{
def: "{bold -D} mixResolver=tone|noise|mix",
desc: `This option determines the behavior when tone and noise are requested to be key-on simultaneously on the same AY8910 channel.
- tone: tone will be output. noise will be silent.
- noise: noise will be output. tone will be silent.
- mix: both tone and noise will be output (default).`,
},
{
def: "{bold -D} periodicNoiseAssignment={underline value}",
desc: `Specify the target to which SN76489's periodic noise will be converted. The {underline value} must be one of the following:
- tone: square wave (default).
- noise: white noise.
- mix: square wave + white nosie.
- env.tri: hardware triangle envelope. This will always make volume maximum.
- env.saw: hardware saw envelope. The will always make volume maximum.
- none: no output.`,
},
{
def: "{bold -D} periodicNoisePitchShift={underline n}",
desc: `The pitch shift amount of the periodic noise conversion. pow(2, -{underline n}) will be multiplied to the noise frequency. The default value is 4.`,
},
{
def: "{bold -D} channelAttenuationMap={underline n1},{underline n2},{underline n3},{underline n4}",
desc: `Volume attenuation mapping for SN76489 channels. {underline n1}, {underline n2}, ... {underline n4} correspond to SN76489's ch1, ch2, ... ch4 respectively. The default value is 0,0,0,0.`,
},
{
def: "{bold -D} whiteNoiseAttenuation={underline n}",
desc: `Additional volume attenuation for the white noise. This value will be added to the {underline n4} specified on volumeAttenuationMap. The default value is 0.`,
},
{
def: "{bold -D} periodicNoiseAttenuation={underline n}",
desc: `Additional volume attenuation for periodic noise. This value will be added to the {underline n4} specified on volumeAttenuationMap. The default value is 0.`,
},
{
def: "{bold -D} noisePitchMap={underline n1},{underline n2},{underline n3}",
desc: `The noise frequency of AY8910. {underline n1}, {underline n2} and {underline n3} correspond to SN76489's noise frequency 0, 1 and 2 respectively. The default value is 7,15,31.`,
},
],
},
{
header: "EXAMPLES",
content: [
{
desc: "YM2612 to YM2413",
example: "$ vgm-conv -f ym2612 -t ym2413 input.vgm",
},
{
desc: "Both YM2413 and AY8910 to YM2608",
example: "$ vgm-conv -f ay8910 -t ym2608 input.vgm | vgm-conv -f ym2413 -t ym2608",
},
{
desc: "YM2203's FM part to YM2413 and SSG part to AY8910",
example: "$ vgm-conv -f ym2203.fm -t ym2413 input.vgm | vgm-conv -f ym2203 -t ay8910",
},
{
desc: "Only DAC part of YM2612 to YM2413",
example: "$ vgm-conv -f ym2612.dac -t ym2413 input.vgm",
},
{
desc: "YM2612 to YM2413@4.00MHz",
example: "$ vgm-conv -f ym2612 -t ym2413 -c 4000000 input.vgm",
},
],
},
];
const defineKeyTypeMap = {
decimation: Number,
dacEmulation: ["fmpcm", "test", "none"],
mixResolver: ["tone", "noise", "mix"],
mixChannel: Number,
opmOutput: String,
periodicNoiseAssignment: ["tone", "noise", "mix", "env.tri", "env.saw", "none"],
periodicNoisePitchShift: Number,
periodicNoiseAttenuation: Number,
whiteNoiseAttenuation: Number,
squareWaveAttenuation: Number,
noisePitchMap: Uint8Array,
channelAttenuationMap: Uint8Array,
ssgAttenuation: Number,
opllVariant: ["ym2413", "vrc7", "ymf281b"],
};
function toArrayBuffer(b) {
return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
}
function parseValue(text) {
if (text.indexOf(",") >= 0) {
return text.split(",");
}
if (text === "true") {
return true;
}
if (text === "false") {
return false;
}
if (/^[0-9]+$/.test(text)) {
return parseInt(text);
}
if (/^[0-9]+\.[0-9]+$/.test(text)) {
return parseFloat(text);
}
return text;
}
function parseDefines(defs) {
const res = {};
for (const def of defs || []) {
const nv = def.split("=");
const key = nv[0];
const keyType = defineKeyTypeMap[nv[0]];
if (keyType == null) {
throw Error(`Unknown variable '${key}'`);
}
else if (nv.length < 2) {
throw Error(`Missing value for '${key}'`);
}
else {
const value = parseValue(nv[1]);
if (keyType instanceof Array) {
if (keyType.indexOf(value) < 0) {
throw Error(`Invalid value for ${nv[0]}: ${nv[1]}`);
}
res[nv[0]] = value;
}
else if (keyType === Uint8Array) {
if (value instanceof Array) {
res[nv[0]] = new Uint8Array(value);
}
else {
throw Error(`Value for ${nv[0]} must be Array or integers.`);
}
}
else {
res[nv[0]] = value;
}
}
}
return res;
}
const defaultClocks = {
ym2413: 3579545,
ym2608: 7987200,
ym2612: 7670454,
ym2151: 4000000,
ym2203: 4000000,
ym3812: 3579545,
ym8950: 3579545,
ym3526: 3579545,
ymf262: 14318180,
};
function main(argv) {
const options = (0, command_line_args_1.default)(optionDefinitions, { argv });
options.voiceTable = options["voice-table"] || options.voiceTable;
if (options.version) {
const json = require("../package.json");
console.info(json.version);
return;
}
if (options.help || (options.from == null && options.to == null)) {
console.error((0, command_line_usage_1.default)(sections));
return;
}
if (process.platform === "win32") {
if (options.input == null) {
console.error("Please specify '--input' option. Standard input can not be used on Windows.");
return;
}
if (options.output == null) {
console.error("Please specify '--output' option. Standard output can not be used on Windows.");
return;
}
}
const input = options.input || "/dev/stdin";
const output = options.output;
try {
const buf = fs_1.default.readFileSync(input);
const vgm = vgm_parser_1.VGM.parse(toArrayBuffer(buf));
let fromChipName = null;
let fromSubModule = null;
let chips = Object.keys(vgm.chips);
if (options.from == null) {
if (chips.length > 0) {
fromChipName = chips[0];
fromSubModule = null;
}
}
if (fromChipName == null) {
const fromCM = (options.from ?? options.to).split(".");
fromChipName = fromCM[0];
fromSubModule = fromCM[1];
}
const fromCommonInfo = vgm.chips[fromChipName];
const from = {
index: 0,
chip: fromChipName,
subModule: fromSubModule,
clock: fromCommonInfo?.clock ?? 0,
variantName: fromCommonInfo?.chipType?.name,
};
const toCM = (options.to || options.from).split(".");
const toChipName = toCM[0];
const to = {
index: 0,
chip: toChipName,
clock: options.clock || defaultClocks[toChipName] || 0,
};
const opts = parseDefines(options.define);
if (options.voiceTable) {
try {
const { voiceTable } = require(path_1.default.resolve(options.voiceTable));
opts["voiceTable"] = voiceTable;
}
catch (e) {
console.error("Error in loading voice table.");
throw e;
}
}
const converted = (0, index_1.default)(vgm, from, to, opts);
if (options["no-gd3"]) {
converted.gd3tag = undefined;
}
converted.volumeModifier = 0x00;
// converted.extraHeader = {
// volumes: [
// {
// chip: "ay8910",
// chipId: 18,
// paired: false,
// flags: 0,
// volume: 128,
// absolute: false
// }
// ]
// };
const compress = /\.vgz/i.test(output);
const res = Buffer.from(converted.build({ compress }));
if (output) {
fs_1.default.writeFileSync(output, res);
}
else {
process.stdout.write(res);
}
}
catch (e) {
console.error(e.message);
process.exit(1);
}
}
main(process.argv);