vgm-conv
Version:
Chip-type and clock converter for VGM.
400 lines (399 loc) • 14.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OPNToYM2413Converter = void 0;
const vgm_converter_1 = require("./vgm-converter");
const vgm_parser_1 = require("vgm-parser");
const vgm_write_data_buffer_1 = __importDefault(require("./vgm-write-data-buffer"));
const ym_voice_1 = require("ym-voice");
const opm_1 = require("../opm");
const fallback_voice = { program: 1, volumeOffset: 15, octaveOffset: 0 };
const MAX_VOICES = 1024;
function _calcMainTotalLevel(data) {
const alg = data[28] & 7;
let min = 0;
switch (alg) {
case 0:
case 1:
case 2:
case 3:
return data[7];
case 4:
min = Math.min(data[6], data[7]);
return min;
case 5:
case 6:
min = Math.min(data[5], data[6], data[7]);
return min;
default:
min = Math.min(data[5], data[6], data[7], data[8]);
return min;
}
}
function _normalizeTotalLevel(data) {
const alg = data[28] & 7;
let min = 0;
switch (alg) {
case 0:
case 1:
case 2:
case 3:
data[7] = 0;
break;
case 4:
min = Math.min(data[6], data[7]);
data[6] -= min;
data[7] -= min;
break;
case 5:
case 6:
min = Math.min(data[5], data[6], data[7]);
data[5] -= min;
data[6] -= min;
data[7] -= min;
break;
case 7:
min = Math.min(data[4], data[5], data[6], data[7]);
data[4] -= min;
data[5] -= min;
data[6] -= min;
data[7] -= min;
break;
}
return data;
}
function _programString(program) {
if (program < MAX_VOICES) {
return program.toString();
}
const res = [];
if (program & (1 << 10)) {
res.push("HH");
}
if (program & (1 << 11)) {
res.push("CYM");
}
if (program & (1 << 12)) {
res.push("TOM");
}
if (program & (1 << 13)) {
res.push("SD");
}
if (program & (1 << 14)) {
res.push("BD");
}
return res.join("|");
}
function _voiceMapEntryString(voice) {
const res = [`i:${_programString(voice.program)}`];
if (voice.volumeOffset != 0) {
res.push(`v:${-voice.volumeOffset}`);
}
if (voice.octaveOffset != 0) {
res.push(`o:${voice.octaveOffset}`);
}
return `{${res.join(",")}}`;
}
function _voiceMapEntryToVoiceDefinition(e) {
return {
program: e.i,
volumeOffset: e.v != null ? -e.v : 0,
octaveOffset: e.o || 0,
};
}
class OPNToYM2413Converter extends vgm_converter_1.VGMConverter {
opts;
_userVoices = {
0: [0x01, 0x01, 0x1c, 0x07, 0xf0, 0xd7, 0x00, 0x11],
};
/** instrument data to voice and volue offset (attenuation) */
_voiceDefMap = {
"000000000000000000000000000000000000000000000000000000000000": { program: 1, volumeOffset: 15, octaveOffset: 0 },
};
_voiceInfoMap = {};
_autoMap = true;
_currentVoice = [
{ hash: "", ...fallback_voice },
{ hash: "", ...fallback_voice },
{ hash: "", ...fallback_voice },
{ hash: "", ...fallback_voice },
{ hash: "", ...fallback_voice },
{ hash: "", ...fallback_voice },
];
_div = 0;
_regs = [new Uint8Array(256), new Uint8Array(256)];
_buf = new vgm_write_data_buffer_1.default(256, 1);
_keyFlags = [0, 0, 0, 0, 0, 0];
_blkFnums = [0, 0, 0, 0, 0, 0];
_rkeyFlags = [0, 0, 0, 0, 0, 0];
_rVolumes = { hh: 15, cym: 15, tom: 15, sd: 15, bd: 15 };
constructor(from, to, opts) {
super(from, to);
this.opts = opts;
if (this.opts.voiceTable && this.opts.voiceTable.opn2opll) {
const { opn2opll } = this.opts.voiceTable;
this._userVoices = { ...this._userVoices, ...opn2opll.voices };
const { mapping } = opn2opll;
if (mapping != null) {
for (let key in mapping) {
this._voiceDefMap[key] = _voiceMapEntryToVoiceDefinition(mapping[key]);
}
}
this._autoMap = opn2opll.autoMap != null ? opn2opll.autoMap : this._autoMap;
}
}
_estimateOPLLVoice(opn) {
return opn.toOPL()[0].toOPLLROMVoice();
}
_y(addr, data, optimize = true) {
const target = this.from.index ? vgm_parser_1.VGMWriteDataTargetId.ym2413_2 : vgm_parser_1.VGMWriteDataTargetId.ym2413;
const index = this.from.index;
this._buf.push(new vgm_parser_1.VGMWriteDataCommand({ target, index, port: 0, addr, data }), optimize);
}
_identifyVoice(ch) {
const port = ch < 3 ? 0 : 1;
const nch = ch < 3 ? ch : (ch + 1) & 3;
const regs = this._regs[port];
const rawVoice = [
regs[0x30 + nch],
regs[0x34 + nch],
regs[0x38 + nch],
regs[0x3c + nch],
regs[0x40 + nch],
regs[0x44 + nch],
regs[0x48 + nch],
regs[0x4c + nch],
regs[0x50 + nch],
regs[0x54 + nch],
regs[0x58 + nch],
regs[0x5c + nch],
regs[0x60 + nch],
regs[0x64 + nch],
regs[0x68 + nch],
regs[0x6c + nch],
regs[0x70 + nch],
regs[0x74 + nch],
regs[0x78 + nch],
regs[0x7c + nch],
regs[0x80 + nch],
regs[0x84 + nch],
regs[0x88 + nch],
regs[0x8c + nch],
regs[0x90 + nch],
regs[0x94 + nch],
regs[0x98 + nch],
regs[0x9c + nch],
regs[0xb0 + nch] & 0x3f,
regs[0xb4 + nch] & 0x37,
];
const hash = _normalizeTotalLevel(rawVoice)
.map((e) => ("0" + e.toString(16)).slice(-2))
.join("");
const prevHash = this._currentVoice[ch].hash;
if (hash != prevHash) {
let voiceInfo = this._voiceInfoMap[hash];
if (voiceInfo == null) {
const opnVoice = ym_voice_1.OPNVoice.decode(rawVoice);
voiceInfo = this._voiceInfoMap[hash] = {
opnVoice,
channels: new Set(),
};
}
voiceInfo.channels.add(ch);
let vdef = this._voiceDefMap[hash];
if (vdef == null) {
if (this._autoMap) {
vdef = this._estimateOPLLVoice(voiceInfo.opnVoice);
}
vdef ||= fallback_voice;
this._voiceDefMap[hash] = vdef;
}
this._currentVoice[ch] = { hash, ...vdef };
}
}
_updateInstVol(port, nch) {
const regs = this._regs[port];
const ch = nch + port * 3;
const alg = regs[0xb0 + nch] & 7;
const { program, volumeOffset } = this._currentVoice[ch];
const amps = [regs[0x40 + nch] & 0x7f, regs[0x44 + nch] & 0x7f, regs[0x48 + nch] & 0x7f, regs[0x4c + nch] & 0x7f];
let vol; // 7f * 4
switch (alg) {
case 4:
vol = (amps[2] + amps[3]) / 2;
break;
case 5:
case 6:
vol = (amps[1] + amps[2] + amps[3]) / 3;
break;
case 7:
vol = (amps[0] + amps[1] + amps[2] + amps[3]) / 4;
break;
default:
vol = amps[3];
break;
}
const vv = (vol >> 3) + volumeOffset;
if (0 < program && program < 16) {
const d = (program << 4) | Math.min(15, Math.max(0, vv));
this._y(0x30 + ch, d);
}
else if (program < MAX_VOICES) {
const v = this._userVoices[program] || this._userVoices[0];
for (let i = 0; i < v.length; i++) {
this._y(i, v[i]);
}
const d = Math.min(15, Math.max(0, vv));
this._y(0x30 + ch, d);
}
else {
this.updateRhythmVolume(program >> 10, vv);
}
}
updateRhythmVolume(r, vv) {
const v = Math.min(15, Math.max(0, vv));
switch (r) {
case 1: // HH
this._rVolumes.hh = v;
this._y(0x37, (this._rVolumes.hh << 4) | this._rVolumes.sd);
break;
case 2: // CYM
this._rVolumes.cym = v;
this._y(0x38, (this._rVolumes.tom << 4) | this._rVolumes.cym);
break;
case 4: // TOM
this._rVolumes.tom = v;
this._y(0x38, (this._rVolumes.tom << 4) | this._rVolumes.cym);
break;
case 8: // SD
this._rVolumes.sd = v;
this._y(0x37, (this._rVolumes.hh << 4) | this._rVolumes.sd);
break;
case 16: // BD
this._rVolumes.bd = v;
this._y(0x36, this._rVolumes.bd);
break;
}
}
convertFM(cmd) {
const adr = cmd.addr;
const regs = this._regs[cmd.port];
regs[adr] = cmd.data;
if (cmd.port == 0) {
if (adr === 0x28) {
const nch = cmd.data & 3;
if (nch !== 3) {
const ch = nch + (cmd.data & 4 ? 3 : 0);
const key = cmd.data >> 4 != 0 ? 1 : 0;
if (key != this._keyFlags[ch]) {
if (key) {
this._identifyVoice(ch);
}
}
this._updateInstVol(cmd.port, nch);
const { hash, program, octaveOffset } = this._currentVoice[ch];
if (program >= MAX_VOICES) {
if (key) {
this._rkeyFlags[ch] = program >> 10;
}
else {
this._rkeyFlags[ch] = 0;
}
this._y(14, 32 |
this._rkeyFlags[0] |
this._rkeyFlags[1] |
this._rkeyFlags[2] |
this._rkeyFlags[3] |
this._rkeyFlags[4] |
this._rkeyFlags[5]);
}
const blk = this._blkFnums[ch] >> 9;
const fnum = this._blkFnums[ch] & 0x1ff;
this._keyFlags[ch] = key;
const fkey = program >= 1024 ? 0 : key;
const blk_o = Math.min(7, Math.max(0, octaveOffset + blk));
const dl = fnum & 0xff;
const dh = (fkey << 4) | (blk_o << 1) | (fnum >> 8);
this._y(0x20 + ch, dh);
this._y(0x10 + ch, dl);
}
}
}
if (0x40 <= adr && adr <= 0x4f) {
// Total Level
const nch = adr & 3;
if (nch !== 3) {
this._updateInstVol(cmd.port, nch);
}
}
if ((0xa0 <= adr && adr <= 0xa2) || (0xa4 <= adr && adr < 0xa6)) {
const nch = adr & 3;
const ch = nch + cmd.port * 3;
const al = 0xa0 + nch;
const ah = 0xa4 + nch;
const fnum = (((regs[ah] & 7) << 8) | regs[al]) >> 2;
const { hash, program, octaveOffset } = this._currentVoice[ch];
const blk = (regs[ah] >> 3) & 7;
const blk_o = Math.min(7, Math.max(0, octaveOffset + blk));
const key = program >= 1024 ? 0 : this._keyFlags[ch];
const dl = fnum & 0xff;
const dh = (key << 4) | (blk_o << 1) | (fnum >> 8);
if (0xa0 <= adr && adr <= 0xa2) {
this._blkFnums[ch] = (blk << 9) | fnum;
this._y(0x20 + ch, dh);
this._y(0x10 + ch, dl);
}
}
return this._buf.commit();
}
getInitialCommands() {
// select FM 6-ch mode
this._y(14, 32);
// set rhythm freq
const goalClock = this.to.relativeClock ? this.from.clock * this.to.clock : this.to.clock || 3579545;
const ratio = 3579545 / goalClock;
this._y(0x16, 0x20 * ratio);
this._y(0x17, 0x50 * ratio);
this._y(0x18, 0xc0 * ratio);
this._y(0x26, 0x05);
this._y(0x27, 0x05);
this._y(0x28, 0x01);
this._y(0x36, 0x00);
this._y(0x37, 0x20);
this._y(0x38, 0x02);
return this._buf.commit();
}
convertCommand(cmd) {
const convertSSG = this.from.subModule == null || this.from.subModule === "ssg";
const convertFM = this.from.subModule == null || this.from.subModule === "fm";
if (cmd instanceof vgm_parser_1.VGMWriteDataCommand && cmd.chip === this.from.chip && cmd.index === this.from.index) {
if (cmd.addr < 0x10 && cmd.port === 0) {
if (convertSSG) {
// Not Supported
return [];
}
}
else {
if (convertFM) {
return this.convertFM(cmd);
}
}
}
else if (cmd instanceof vgm_parser_1.VGMEndCommand) {
for (let hash in this._voiceInfoMap) {
const { opnVoice, channels } = this._voiceInfoMap[hash];
const vdef = this._voiceDefMap[hash];
console.error(`/*${opnVoice.toMML("mucom88")}*/`);
console.error(`"${hash}":${_voiceMapEntryString(vdef)}, // Used in CH:${Array.from(channels)}`);
}
if (this.opts.opmOutput) {
(0, opm_1.writeOpmVoiceData)(this.opts.opmOutput, Object.values(this._voiceInfoMap));
}
return [cmd];
}
return [cmd];
}
}
exports.OPNToYM2413Converter = OPNToYM2413Converter;