furnace/src/engine/fileOpsIns.cpp

2026 lines
61 KiB
C++

/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2022 tildearrow and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "engine.h"
#include "../ta-log.h"
#include "../fileutils.h"
#include <fmt/printf.h>
#include <limits.h>
enum DivInsFormats {
DIV_INSFORMAT_DMP,
DIV_INSFORMAT_TFI,
DIV_INSFORMAT_VGI,
DIV_INSFORMAT_FTI,
DIV_INSFORMAT_BTI,
DIV_INSFORMAT_S3I,
DIV_INSFORMAT_SBI,
DIV_INSFORMAT_Y12,
DIV_INSFORMAT_OPLI,
DIV_INSFORMAT_OPNI,
DIV_INSFORMAT_BNK,
DIV_INSFORMAT_GYB,
DIV_INSFORMAT_OPM,
DIV_INSFORMAT_WOPL,
DIV_INSFORMAT_WOPN,
DIV_INSFORMAT_FF,
};
// Reused patch data structures
// SBI and some other OPL containers
struct sbi_t {
uint8_t Mcharacteristics,
Ccharacteristics,
Mscaling_output,
Cscaling_output,
Meg_AD,
Ceg_AD,
Meg_SR,
Ceg_SR,
Mwave,
Cwave,
FeedConnect;
};
// MIDI-related
struct midibank_t {
String name;
uint8_t bankMsb,
bankLsb;
};
static void readSbiOpData(sbi_t& sbi, SafeReader& reader) {
sbi.Mcharacteristics = reader.readC();
sbi.Ccharacteristics = reader.readC();
sbi.Mscaling_output = reader.readC();
sbi.Cscaling_output = reader.readC();
sbi.Meg_AD = reader.readC();
sbi.Ceg_AD = reader.readC();
sbi.Meg_SR = reader.readC();
sbi.Ceg_SR = reader.readC();
sbi.Mwave = reader.readC();
sbi.Cwave = reader.readC();
sbi.FeedConnect = reader.readC();
}
// detune needs extra translation from register to furnace format
static inline uint8_t fmDtRegisterToFurnace(uint8_t&& dtNative) {
return (dtNative>=4) ? (7-dtNative) : (dtNative+3);
}
static bool stringNotBlank(String& str) {
return str.size() > 0 && str.find_first_not_of(' ') != String::npos;
}
void DivEngine::loadDMP(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument* ins=new DivInstrument;
// this is a ridiculous mess
unsigned char version=0;
unsigned char sys=0;
try {
reader.seek(0,SEEK_SET);
version=reader.readC();
logD(".dmp version %d",version);
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
return;
}
if (version>11) {
lastError="unknown instrument version!";
delete ins;
return;
}
ins->name=stripPath;
if (version>=11) { // 1.0
try {
sys=reader.readC();
switch (sys) {
case 1: // YMU759
ins->type=DIV_INS_FM;
logD("instrument type is YMU759");
break;
case 2: // Genesis
ins->type=DIV_INS_FM;
logD("instrument type is Genesis");
break;
case 3: // SMS
ins->type=DIV_INS_STD;
logD("instrument type is SMS");
break;
case 4: // Game Boy
ins->type=DIV_INS_GB;
logD("instrument type is Game Boy");
break;
case 5: // PC Engine
ins->type=DIV_INS_PCE;
logD("instrument type is PC Engine");
break;
case 6: // NES
ins->type=DIV_INS_STD;
logD("instrument type is NES");
break;
case 7: case 0x17: // C64
ins->type=DIV_INS_C64;
logD("instrument type is C64");
break;
case 8: // Arcade
ins->type=DIV_INS_FM;
logD("instrument type is Arcade");
break;
case 9: // Neo Geo
ins->type=DIV_INS_FM;
logD("instrument type is Neo Geo");
break;
default:
logD("instrument type is unknown");
lastError=fmt::sprintf("unknown instrument type %d!",sys);
delete ins;
return;
break;
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
return;
}
}
try {
bool mode=true;
if (version>1) {
mode=reader.readC();
logD("instrument mode is %d",mode);
if (mode==0) {
if (ins->type==DIV_INS_FM) {
if (sys==9) {
ins->type=DIV_INS_AY;
} else {
ins->type=DIV_INS_STD;
}
}
} else {
if (sys==3 || sys==6) {
ins->type=DIV_INS_OPLL;
} else if (sys==1) {
ins->type=DIV_INS_OPL;
} else {
ins->type=DIV_INS_FM;
}
}
} else {
ins->type=DIV_INS_FM;
}
if (mode) { // FM
logD("reading FM data...");
if (version<10) {
if (version>1) {
// bullcrap! no way to determine the instrument type other than a vague FM/STD!
if (reader.size()==51) {
reader.readC();
ins->fm.ops=4;
} else {
ins->fm.ops=reader.readC()?4:2;
}
} else {
// HELP
if (reader.size()==49) {
ins->fm.ops=4;
reader.readC();
} else {
ins->fm.ops=reader.readC()?2:4;
}
}
} else {
ins->fm.ops=4;
}
if (version>1) { // HELP! in which version of the format did we start storing FMS!
ins->fm.fms=reader.readC();
}
ins->fm.fb=reader.readC();
ins->fm.alg=reader.readC();
// DITTO
if (sys!=1) ins->fm.ams=reader.readC();
for (int j=0; j<ins->fm.ops; j++) {
logD("OP%d is at %d",j,reader.tell());
ins->fm.op[j].mult=reader.readC();
ins->fm.op[j].tl=reader.readC();
ins->fm.op[j].ar=reader.readC();
ins->fm.op[j].dr=reader.readC();
ins->fm.op[j].sl=reader.readC();
ins->fm.op[j].rr=reader.readC();
ins->fm.op[j].am=reader.readC();
// what the hell how do I tell!
if (sys==1) { // YMU759
ins->fm.op[j].ws=reader.readC();
ins->fm.op[j].ksl=reader.readC();
ins->fm.op[j].vib=reader.readC();
ins->fm.op[j].egt=reader.readC();
ins->fm.op[j].sus=reader.readC();
ins->fm.op[j].ksr=reader.readC();
ins->fm.op[j].dvb=reader.readC();
ins->fm.op[j].dam=reader.readC();
} else {
if (sys==3 || sys==6) { // OPLL/VRC7
ins->fm.op[j].ksr=reader.readC()?1:0;
ins->fm.op[j].vib=reader.readC();
if (j==0) {
ins->fm.opllPreset=ins->fm.op[j].vib>>4;
}
ins->fm.op[j].vib=ins->fm.op[j].vib?1:0;
ins->fm.op[j].ksl=reader.readC()?1:0;
ins->fm.op[j].ssgEnv=reader.readC();
} else {
ins->fm.op[j].rs=reader.readC();
ins->fm.op[j].dt=reader.readC();
ins->fm.op[j].dt2=ins->fm.op[j].dt>>4;
ins->fm.op[j].dt&=15;
ins->fm.op[j].d2r=reader.readC();
ins->fm.op[j].ssgEnv=reader.readC();
}
}
}
} else { // STD
logD("reading STD data...");
if (ins->type!=DIV_INS_GB) {
ins->std.volMacro.len=reader.readC();
if (version>5) {
for (int i=0; i<ins->std.volMacro.len; i++) {
ins->std.volMacro.val[i]=reader.readI();
if (ins->std.volMacro.val[i]>15 && sys==6) { // FDS
ins->type=DIV_INS_FDS;
}
}
} else {
for (int i=0; i<ins->std.volMacro.len; i++) {
ins->std.volMacro.val[i]=reader.readC();
}
}
if (version<11) for (int i=0; i<ins->std.volMacro.len; i++) {
if (ins->std.volMacro.val[i]>15 && ins->type==DIV_INS_STD) ins->type=DIV_INS_PCE;
}
if (ins->std.volMacro.len>0) {
ins->std.volMacro.open=true;
ins->std.volMacro.loop=reader.readC();
} else {
ins->std.volMacro.open=false;
}
}
ins->std.arpMacro.len=reader.readC();
if (version>5) {
for (int i=0; i<ins->std.arpMacro.len; i++) {
ins->std.arpMacro.val[i]=reader.readI();
}
} else {
for (int i=0; i<ins->std.arpMacro.len; i++) {
ins->std.arpMacro.val[i]=reader.readC();
}
}
if (ins->std.arpMacro.len>0) {
ins->std.arpMacro.open=true;
ins->std.arpMacro.loop=reader.readC();
} else {
ins->std.arpMacro.open=false;
}
if (version>8) { // TODO: when?
ins->std.arpMacro.mode=reader.readC();
}
ins->std.dutyMacro.len=reader.readC();
if (version>5) {
for (int i=0; i<ins->std.dutyMacro.len; i++) {
ins->std.dutyMacro.val[i]=reader.readI();
}
} else {
for (int i=0; i<ins->std.dutyMacro.len; i++) {
ins->std.dutyMacro.val[i]=reader.readC();
}
}
if (ins->std.dutyMacro.len>0) {
ins->std.dutyMacro.open=true;
ins->std.dutyMacro.loop=reader.readC();
} else {
ins->std.dutyMacro.open=false;
}
ins->std.waveMacro.len=reader.readC();
if (version>5) {
for (int i=0; i<ins->std.waveMacro.len; i++) {
ins->std.waveMacro.val[i]=reader.readI();
}
} else {
for (int i=0; i<ins->std.waveMacro.len; i++) {
ins->std.waveMacro.val[i]=reader.readC();
}
}
if (ins->std.waveMacro.len>0) {
ins->std.waveMacro.open=true;
ins->std.waveMacro.loop=reader.readC();
} else {
ins->std.waveMacro.open=false;
}
if (ins->type==DIV_INS_C64) {
ins->c64.triOn=reader.readC();
ins->c64.sawOn=reader.readC();
ins->c64.pulseOn=reader.readC();
ins->c64.noiseOn=reader.readC();
ins->c64.a=reader.readC();
ins->c64.d=reader.readC();
ins->c64.s=reader.readC();
ins->c64.r=reader.readC();
ins->c64.duty=(reader.readC()*4095)/100;
ins->c64.ringMod=reader.readC();
ins->c64.oscSync=reader.readC();
ins->c64.toFilter=reader.readC();
if (version<0x07) { // TODO: UNSURE
ins->c64.volIsCutoff=reader.readI();
} else {
ins->c64.volIsCutoff=reader.readC();
}
ins->c64.initFilter=reader.readC();
ins->c64.res=reader.readC();
ins->c64.cut=(reader.readC()*2047)/100;
ins->c64.hp=reader.readC();
ins->c64.bp=reader.readC();
ins->c64.lp=reader.readC();
ins->c64.ch3off=reader.readC();
// weird storage
if (ins->c64.volIsCutoff) {
for (int j=0; j<ins->std.volMacro.len; j++) {
ins->std.volMacro.val[j]-=18;
}
}
for (int j=0; j<ins->std.dutyMacro.len; j++) {
ins->std.dutyMacro.val[j]-=12;
}
}
if (ins->type==DIV_INS_GB) {
ins->gb.envVol=reader.readC();
ins->gb.envDir=reader.readC();
ins->gb.envLen=reader.readC();
ins->gb.soundLen=reader.readC();
}
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
return;
}
ret.push_back(ins);
}
void DivEngine::loadTFI(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument* ins=new DivInstrument;
try {
reader.seek(0,SEEK_SET);
ins->type=DIV_INS_FM;
ins->name=stripPath;
ins->fm.alg=reader.readC();
ins->fm.fb=reader.readC();
for (int i=0; i<4; i++) {
DivInstrumentFM::Operator& op=ins->fm.op[i];
op.mult=reader.readC();
op.dt=reader.readC();
op.tl=reader.readC();
op.rs=reader.readC();
op.ar=reader.readC();
op.dr=reader.readC();
op.d2r=reader.readC();
op.rr=reader.readC();
op.sl=reader.readC();
op.ssgEnv=reader.readC();
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
return;
}
ret.push_back(ins);
}
void DivEngine::loadVGI(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument* ins=new DivInstrument;
try {
reader.seek(0,SEEK_SET);
ins->type=DIV_INS_FM;
ins->name=stripPath;
ins->fm.alg=reader.readC();
ins->fm.fb=reader.readC();
unsigned char fmsams=reader.readC();
ins->fm.fms=fmsams&7;
ins->fm.ams=fmsams>>4;
for (int i=0; i<4; i++) {
DivInstrumentFM::Operator& op=ins->fm.op[i];
op.mult=reader.readC();
op.dt=reader.readC();
op.tl=reader.readC();
op.rs=reader.readC();
op.ar=reader.readC();
op.dr=reader.readC();
if (op.dr&0x80) {
op.am=1;
op.dr&=0x7f;
}
op.d2r=reader.readC();
op.rr=reader.readC();
op.sl=reader.readC();
op.ssgEnv=reader.readC();
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
return;
}
ret.push_back(ins);
}
void DivEngine::loadS3I(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument* ins=new DivInstrument;
try {
reader.seek(0, SEEK_SET);
uint8_t s3i_type = reader.readC();
if (s3i_type >= 2) {
ins->type = DIV_INS_OPL;
if (s3i_type > 2 && s3i_type <= 7) {
ins->fm.opllPreset = (uint8_t)(1<<4); // Flag as Drum preset.
}
// skip internal filename - we'll use the long name description
reader.seek(12, SEEK_CUR);
// skip reserved bytes
reader.seek(3, SEEK_CUR);
// 12-byte opl value - identical to SBI format
sbi_t s3i;
readSbiOpData(s3i, reader);
DivInstrumentFM::Operator& opM = ins->fm.op[0];
DivInstrumentFM::Operator& opC = ins->fm.op[1];
ins->fm.ops = 2;
opM.mult = s3i.Mcharacteristics & 0xF;
opM.ksr = ((s3i.Mcharacteristics >> 4) & 0x1);
opM.sus = ((s3i.Mcharacteristics >> 5) & 0x1);
opM.vib = ((s3i.Mcharacteristics >> 6) & 0x1);
opM.am = ((s3i.Mcharacteristics >> 7) & 0x1);
opM.tl = s3i.Mscaling_output & 0x3F;
opM.ksl = ((s3i.Mscaling_output >> 6) & 0x3);
opM.ar = ((s3i.Meg_AD >> 4) & 0xF);
opM.dr = (s3i.Meg_AD & 0xF);
opM.rr = (s3i.Meg_SR & 0xF);
opM.sl = ((s3i.Meg_SR >> 4) & 0xF);
opM.ws = s3i.Mwave;
ins->fm.alg = (s3i.FeedConnect & 0x1);
ins->fm.fb = ((s3i.FeedConnect >> 1) & 0x7);
opC.mult = s3i.Ccharacteristics & 0xF;
opC.ksr = ((s3i.Ccharacteristics >> 4) & 0x1);
opC.sus = ((s3i.Ccharacteristics >> 5) & 0x1);
opC.vib = ((s3i.Ccharacteristics >> 6) & 0x1);
opC.am = ((s3i.Ccharacteristics >> 7) & 0x1);
opC.tl = s3i.Cscaling_output & 0x3F;
opC.ksl = ((s3i.Cscaling_output >> 6) & 0x3);
opC.ar = ((s3i.Ceg_AD >> 4) & 0xF);
opC.dr = (s3i.Ceg_AD & 0xF);
opC.rr = (s3i.Ceg_SR & 0xF);
opC.sl = ((s3i.Ceg_SR >> 4) & 0xF);
opC.ws = s3i.Cwave;
// Skip more stuff we don't need
reader.seek(21, SEEK_CUR);
} else {
lastError="S3I PCM samples currently not supported.";
logE("S3I PCM samples currently not supported.");
}
String insName = reader.readString(28);
ins->name = stringNotBlank(insName) ? insName : stripPath;
int s3i_signature = reader.readI();
if (s3i_signature != 0x49524353) {
addWarning("S3I signature invalid.");
logW("S3I signature invalid.");
};
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
return;
}
ret.push_back(ins);
}
void DivEngine::loadSBI(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList; // in case 2x2op
DivInstrument* ins=new DivInstrument;
try {
reader.seek(0, SEEK_SET);
ins->type = DIV_INS_OPL;
int sbi_header = reader.readI();
// SBI header determines format
bool is_2op = (sbi_header == 0x1A494253 || sbi_header == 0x1A504F32); // SBI\x1A or 2OP\x1A
bool is_4op = (sbi_header == 0x1A504F34); // 4OP\x1A
bool is_6op = (sbi_header == 0x1A504F36); // 6OP\x1A - Freq Monster 801-specific
// 32-byte null terminated instrument name
String insName = reader.readString(32);
insName = stringNotBlank(insName) ? insName : stripPath;
auto writeOp = [](sbi_t& sbi, DivInstrumentFM::Operator& opM, DivInstrumentFM::Operator& opC) {
opM.mult = sbi.Mcharacteristics & 0xF;
opM.ksr = ((sbi.Mcharacteristics >> 4) & 0x1);
opM.sus = ((sbi.Mcharacteristics >> 5) & 0x1);
opM.vib = ((sbi.Mcharacteristics >> 6) & 0x1);
opM.am = ((sbi.Mcharacteristics >> 7) & 0x1);
opM.tl = sbi.Mscaling_output & 0x3F;
opM.ksl = ((sbi.Mscaling_output >> 6) & 0x3);
opM.ar = ((sbi.Meg_AD >> 4) & 0xF);
opM.dr = (sbi.Meg_AD & 0xF);
opM.rr = (sbi.Meg_SR & 0xF);
opM.sl = ((sbi.Meg_SR >> 4) & 0xF);
opM.ws = sbi.Mwave;
opC.mult = sbi.Ccharacteristics & 0xF;
opC.ksr = ((sbi.Ccharacteristics >> 4) & 0x1);
opC.sus = ((sbi.Ccharacteristics >> 5) & 0x1);
opC.vib = ((sbi.Ccharacteristics >> 6) & 0x1);
opC.am = ((sbi.Ccharacteristics >> 7) & 0x1);
opC.tl = sbi.Cscaling_output & 0x3F;
opC.ksl = ((sbi.Cscaling_output >> 6) & 0x3);
opC.ar = ((sbi.Ceg_AD >> 4) & 0xF);
opC.dr = (sbi.Ceg_AD & 0xF);
opC.rr = (sbi.Ceg_SR & 0xF);
opC.sl = ((sbi.Ceg_SR >> 4) & 0xF);
opC.ws = sbi.Cwave;
};
sbi_t sbi_op12; // 2op (+6op portion)
sbi_t sbi_op34; // 4op
readSbiOpData(sbi_op12, reader);
if (is_2op) {
DivInstrumentFM::Operator& opM = ins->fm.op[0];
DivInstrumentFM::Operator& opC = ins->fm.op[1];
ins->fm.ops = 2;
ins->name = insName;
writeOp(sbi_op12, opM, opC);
ins->fm.alg = (sbi_op12.FeedConnect & 0x1);
ins->fm.fb = ((sbi_op12.FeedConnect >> 1) & 0x7);
// SBTimbre extensions
uint8_t perc_voc = reader.readC();
if (perc_voc >= 6) {
ins->fm.opllPreset = (uint8_t)(1 << 4);
}
// Ignore rest of file - rest is 'reserved padding'.
reader.seek(4, SEEK_CUR);
insList.push_back(ins);
} else if (is_4op || is_6op) {
readSbiOpData(sbi_op34, reader);
// Operator placement is different so need to place in correct registers.
// Note: 6op is an unofficial extension of 4op SBIs by Darron Broad (Freq Monster 801).
// We'll only use the 4op portion here for pure OPL3.
DivInstrumentFM::Operator& opM = ins->fm.op[0];
DivInstrumentFM::Operator& opC = ins->fm.op[2];
DivInstrumentFM::Operator& opM4 = ins->fm.op[1];
DivInstrumentFM::Operator& opC4 = ins->fm.op[3];
ins->fm.ops = 4;
ins->name = insName;
ins->fm.alg = (sbi_op12.FeedConnect & 0x1) | ((sbi_op34.FeedConnect & 0x1) << 1);
ins->fm.fb = ((sbi_op34.FeedConnect >> 1) & 0x7);
writeOp(sbi_op12, opM, opC);
writeOp(sbi_op34, opM4, opC4);
if (is_6op) {
// Freq Monster 801 6op SBIs use a 4+2op layout
// Save the 4op portion before reading the 2op part
ins->name = fmt::sprintf("%s (4op)", ins->name);
insList.push_back(ins);
readSbiOpData(sbi_op12, reader);
ins = new DivInstrument;
DivInstrumentFM::Operator& opM6 = ins->fm.op[0];
DivInstrumentFM::Operator& opC6 = ins->fm.op[1];
ins->type = DIV_INS_OPL;
ins->fm.ops = 2;
ins->name = fmt::sprintf("%s (2op)", insName);
writeOp(sbi_op12, opM6, opC6);
ins->fm.alg = (sbi_op12.FeedConnect & 0x1);
ins->fm.fb = ((sbi_op12.FeedConnect >> 1) & 0x7);
}
// Ignore rest of file once we've read in all we need.
// Note: Freq Monster 801 adds a ton of other additional fields irrelevant to chip registers.
// If instrument transpose is ever supported, we can read it in maybe?
reader.seek(0, SEEK_END);
insList.push_back(ins);
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
if (ins != NULL) {
delete ins;
}
for (DivInstrument* p : insList) {
delete p;
}
return;
}
for (DivInstrument* p : insList) {
ret.push_back(p);
}
}
void DivEngine::loadOPLI(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList; // in case 2x2op
DivInstrument* ins = new DivInstrument;
auto readOpliOp = [](SafeReader& reader, DivInstrumentFM::Operator& op) {
uint8_t characteristics = reader.readC();
uint8_t keyScaleLevel = reader.readC();
uint8_t attackDecay = reader.readC();
uint8_t sustainRelease = reader.readC();
uint8_t waveSelect = reader.readC();
op.mult = characteristics & 0xF;
op.ksr = ((characteristics >> 4) & 0x1);
op.sus = ((characteristics >> 5) & 0x1);
op.vib = ((characteristics >> 6) & 0x1);
op.am = ((characteristics >> 7) & 0x1);
op.tl = keyScaleLevel & 0x3F;
op.ksl = ((keyScaleLevel >> 6) & 0x3);
op.ar = ((attackDecay >> 4) & 0xF);
op.dr = attackDecay & 0xF;
op.rr = sustainRelease & 0xF;
op.sl = ((sustainRelease >> 4) & 0xF);
op.ws = waveSelect;
};
try {
reader.seek(0, SEEK_SET);
String header = reader.readString(11);
if (header == "WOPL3-INST") {
reader.readS(); // skip version (presently no difference here)
reader.readC(); // skip isPerc field
ins->type = DIV_INS_OPL;
String insName = reader.readString(32);
insName = stringNotBlank(insName) ? insName : stripPath;
ins->name = insName;
// TODO adapt MIDI key offset to transpose?
reader.seek(7, SEEK_CUR); // skip MIDI params
uint8_t instTypeFlags = reader.readC(); // [0EEEDCBA] - see WOPL/OPLI spec
bool is_4op = ((instTypeFlags & 0x1) == 1);
bool is_2x2op = (((instTypeFlags>>1) & 0x1) == 1);
bool is_rhythm = (((instTypeFlags>>4) & 0x7) > 0);
uint8_t feedConnect = reader.readC();
uint8_t feedConnect2nd = reader.readC();
ins->fm.alg = (feedConnect & 0x1);
ins->fm.fb = ((feedConnect >> 1) & 0xF);
if (is_4op && !is_2x2op) {
ins->fm.ops = 4;
ins->fm.alg = (feedConnect & 0x1) | ((feedConnect2nd & 0x1) << 1);
for (int i : {2,0,3,1}) { // omfg >_<
readOpliOp(reader, ins->fm.op[i]);
}
} else {
ins->fm.ops = 2;
for (int i : {1,0}) {
readOpliOp(reader, ins->fm.op[i]);
}
if (is_rhythm) {
ins->fm.opllPreset = (uint8_t)(1<<4);
} else if (is_2x2op) {
// Note: Pair detuning offset not mappable. Use E5xx effect :P
ins->name = fmt::sprintf("%s (1)", insName);
insList.push_back(ins);
ins = new DivInstrument;
ins->type = DIV_INS_OPL;
ins->name = fmt::sprintf("%s (2)", insName);
ins->fm.alg = (feedConnect2nd & 0x1);
ins->fm.fb = ((feedConnect2nd >> 1) & 0xF);
for (int i : {1,0}) {
readOpliOp(reader, ins->fm.op[i]);
}
}
if (!is_2x2op) {
reader.seek(10, SEEK_CUR); // skip unused operator pair
}
}
insList.push_back(ins);
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
if (ins != NULL) {
delete ins;
}
for (DivInstrument* p : insList) {
delete p;
}
return;
}
for (DivInstrument* p : insList) {
ret.push_back(p);
}
}
void DivEngine::loadOPNI(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument* ins = new DivInstrument;
try {
reader.seek(0, SEEK_SET);
String header = reader.readString(11);
if (header == "WOPN2-INST" || header == "WOPN2-IN2T") { // omfg >_<
uint16_t version = reader.readS();
if (!(version >= 2) || version > 0xF) {
// version 1 doesn't have a version field........
reader.seek(-2, SEEK_CUR);
version = 1;
}
reader.readC(); // skip isPerc
ins->type = DIV_INS_FM;
ins->fm.ops = 4;
String insName = reader.readString(32);
ins->name = stringNotBlank(insName) ? insName : stripPath;
// TODO adapt MIDI key offset to transpose?
if (!reader.seek(3, SEEK_CUR)) { // skip MIDI params
throw EndOfFileException(&reader, reader.tell() + 3);
}
uint8_t feedAlgo = reader.readC();
ins->fm.alg = (feedAlgo & 0x7);
ins->fm.fb = ((feedAlgo>>3) & 0x7);
reader.readC(); // Skip global bank flags - see WOPN/OPNI spec
auto readOpniOp = [](SafeReader& reader, DivInstrumentFM::Operator& op) {
uint8_t dtMul = reader.readC();
uint8_t totalLevel = reader.readC();
uint8_t arRateScale = reader.readC();
uint8_t drAmpEnable = reader.readC();
uint8_t d2r = reader.readC();
uint8_t susRelease = reader.readC();
uint8_t ssgEg = reader.readC();
op.mult = dtMul & 0xF;
op.dt = ((dtMul >> 4) & 0x7);
op.tl = totalLevel & 0x7F;
op.rs = ((arRateScale >> 6) & 0x3);
op.ar = arRateScale & 0x1F;
op.dr = drAmpEnable & 0x1F;
op.am = ((drAmpEnable >> 7) & 0x1);
op.d2r = d2r & 0x1F;
op.rr = susRelease & 0xF;
op.sl = ((susRelease >> 4) & 0xF);
op.ssgEnv = ssgEg;
};
for (int i = 0; i < 4; ++i) {
readOpniOp(reader, ins->fm.op[i]);
}
ret.push_back(ins);
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
if (ins != NULL) {
delete ins;
}
}
}
void DivEngine::loadY12(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument *ins = new DivInstrument;
try {
reader.seek(0, SEEK_SET);
ins->type = DIV_INS_FM;
ins->fm.ops = 4;
ins->name = stripPath;
for (int i = 0; i < 4; ++i) {
DivInstrumentFM::Operator& insOp = ins->fm.op[i];
uint8_t tmp = reader.readC();
insOp.mult = tmp & 0xF;
// ???
insOp.dt = ((3 + (tmp >> 4)) & 0x7);
insOp.tl = (reader.readC() & 0x7F);
tmp = reader.readC();
insOp.rs = ((tmp >> 6) & 0x3);
insOp.ar = tmp & 0x1F;
tmp = reader.readC();
insOp.dr = tmp & 0x1F;
insOp.am = ((tmp >> 7) & 0x1);
insOp.d2r = (reader.readC() & 0x1F);
tmp = reader.readC();
insOp.rr = tmp & 0xF;
insOp.sl = ((tmp >> 4) & 0xF);
insOp.ssgEnv = reader.readC();
if (!reader.seek(9, SEEK_CUR)) {
throw EndOfFileException(&reader, reader.tell() + 9);
}
}
ins->fm.alg = reader.readC();
ins->fm.fb = reader.readC();
if (!reader.seek(62, SEEK_CUR)) {
throw EndOfFileException(&reader, reader.tell() + 62);
}
ret.push_back(ins);
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
if (ins != NULL) {
delete ins;
}
}
}
void DivEngine::loadBNK(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList;
std::vector<String*> instNames;
reader.seek(0, SEEK_SET);
// First distinguish between GEMS BNK and Adlib BNK
uint64_t header = reader.readL();
bool is_adlib = ((header>>8) == 0x2d42494c444100L);
bool is_failed = false;
int readCount = 0;
int insCount = 0;
if (is_adlib) {
try {
reader.seek(0x0c, SEEK_SET);
uint32_t name_offset = reader.readI();
reader.seek(0x10, SEEK_SET);
uint32_t data_offset = reader.readI();
// Seek to BNK patch names
reader.seek(name_offset, SEEK_SET);
while (reader.tell() < data_offset) {
reader.seek(3, SEEK_CUR);
instNames.push_back(new String(reader.readString(9)));
++insCount;
}
// Seek to BNK data
if (!reader.seek(data_offset, SEEK_SET)) {
throw EndOfFileException(&reader, data_offset);
};
// Read until all patches have been accounted for.
for (int i = 0; i < insCount; ++i) {
DivInstrument *ins = new DivInstrument;
ins->type = DIV_INS_OPL;
ins->fm.ops = 2;
uint8_t timbreMode = reader.readC();
reader.readC(); // skip timbre perc voice
if (timbreMode == 1) {
ins->fm.opllPreset = (uint8_t)(1<<4);
}
for (int i = 0; i < 2; ++i) {
ins->fm.op[i].ksl = reader.readC();
ins->fm.op[i].mult = reader.readC();
uint8_t fb = reader.readC();
if (i==0) {
ins->fm.fb = fb;
}
ins->fm.op[i].ar = reader.readC();
ins->fm.op[i].sl = reader.readC();
ins->fm.op[i].sus = (reader.readC() != 0) ? 1 : 0;
ins->fm.op[i].dr = reader.readC();
ins->fm.op[i].rr = reader.readC();
ins->fm.op[i].tl = reader.readC();
ins->fm.op[i].am = reader.readC();
ins->fm.op[i].vib = reader.readC();
ins->fm.op[i].ksr = reader.readC();
uint8_t alg = (reader.readC() == 0) ? 1 : 0;
if (i==0) {
ins->fm.alg = alg;
}
}
ins->fm.op[0].ws = reader.readC();
ins->fm.op[1].ws = reader.readC();
ins->name = stringNotBlank(*instNames[i]) ? (*instNames[i]) : fmt::sprintf("%s[%d]", stripPath, i);
insList.push_back(ins);
++readCount;
}
// All data read, don't care about the rest.
reader.seek(0, SEEK_END);
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
for (int i = 0; i < readCount; ++i) {
delete insList[i];
}
is_failed = true;
}
} else {
// assume GEMS BNK for now.
lastError="GEMS BNK currently not supported.";
logE("GEMS BNK currently not supported.");
}
if (!is_failed) {
for (int i = 0; i < readCount; ++i) {
ret.push_back(insList[i]);
}
}
for (String* name : instNames) {
delete name;
}
}
void DivEngine::loadFF(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
DivInstrument* insList[256];
memset(insList,0,256*sizeof(void*));
int readCount = 0;
size_t insCount = reader.size();
insCount = (insCount >> 5) + (((insCount % 0x20) > 0) ? 1 : 0);
if (insCount > 256) insCount = 256;
uint8_t buf;
try {
reader.seek(0, SEEK_SET);
for (unsigned int i = 0; i < insCount; ++i) {
insList[i] = new DivInstrument;
DivInstrument* ins = insList[i];
ins->type = DIV_INS_FM;
DivInstrumentFM::Operator op;
for (unsigned int j = 0; j < 4; j++) {
buf = reader.readC();
ins->fm.op[j].mult = buf & 0xf;
ins->fm.op[j].dt = fmDtRegisterToFurnace((buf >> 4) & 0x7);
ins->fm.op[j].ssgEnv = (buf >> 4) & 0x8;
}
for (unsigned int j = 0; j < 4; j++) {
buf = reader.readC();
ins->fm.op[j].tl = buf & 0x7f;
ins->fm.op[j].ssgEnv |= (buf >> 5) & 0x4;
}
for (unsigned int j = 0; j < 4; j++) {
buf = reader.readC();
ins->fm.op[j].ar = buf & 0x1f;
ins->fm.op[j].rs = buf >> 6;
}
for (unsigned int j = 0; j < 4; j++) {
buf = reader.readC();
ins->fm.op[j].dr = buf & 0x1f;
ins->fm.op[j].ssgEnv |= (buf >> 5) & 0x3;
ins->fm.op[j].am = buf >> 7;
}
for (unsigned int j = 0; j < 4; j++) {
buf = reader.readC();
ins->fm.op[j].d2r = buf & 0x1f;
}
for (unsigned int j = 0; j < 4; j++) {
buf = reader.readC();
ins->fm.op[j].rr = buf & 0xf;
ins->fm.op[j].sl = buf >> 4;
}
buf = reader.readC();
ins->fm.alg = buf & 0x7;
ins->fm.fb = (buf >> 3) & 0x7;
// FIXME This is encoded in Shift-JIS
ins->name = reader.readString(7);
++readCount;
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
// Include incomplete entry in deletion.
for (int i = readCount; i >= 0; --i) {
delete insList[i];
}
return;
}
for (unsigned int i = 0; i < insCount; ++i) {
ret.push_back(insList[i]);
}
}
void DivEngine::loadGYB(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList;
int readCount = 0;
bool is_failed = false;
auto readInstrument = [&](SafeReader& reader, bool readRegB4) -> DivInstrument* {
const int opOrder[] = { 0,1,2,3 };
DivInstrument* ins = new DivInstrument;
ins->type = DIV_INS_FM;
ins->fm.ops = 4;
// see https://plutiedev.com/ym2612-registers
// and https://github.com/Wohlstand/OPN2BankEditor/blob/master/Specifications/GYB-file-specification.txt
try {
uint8_t reg;
for (int i : opOrder) {
reg = reader.readC(); // MUL/DT
ins->fm.op[i].mult = reg & 0xF;
ins->fm.op[i].dt = fmDtRegisterToFurnace((reg >> 4) & 0x7);
}
for (int i : opOrder) {
reg = reader.readC(); // TL
ins->fm.op[i].tl = reg & 0x7F;
}
for (int i : opOrder) {
reg = reader.readC(); // AR/RS
ins->fm.op[i].ar = reg & 0x1F;
ins->fm.op[i].rs = ((reg >> 6) & 0x3);
}
for (int i : opOrder) {
reg = reader.readC(); // DR/AM-ENA
ins->fm.op[i].dr = reg & 0x1F;
ins->fm.op[i].am = ((reg >> 7) & 0x1);
}
for (int i : opOrder) {
reg = reader.readC(); // SR (D2R)
ins->fm.op[i].d2r = reg & 0x1F;
}
for (int i : opOrder) {
reg = reader.readC(); // RR/SL
ins->fm.op[i].rr = reg & 0xF;
ins->fm.op[i].sl = ((reg >> 4) & 0xF);
}
for (int i : opOrder) {
reg = reader.readC(); // SSG-EG
ins->fm.op[i].ssgEnv = reg & 0xF;
}
// ALG/FB
reg = reader.readC();
ins->fm.alg = reg & 0x7;
ins->fm.fb = ((reg >> 3) & 0x7);
if (readRegB4) { // PAN / PMS / AMS
reg = reader.readC();
ins->fm.fms = reg & 0x7;
ins->fm.ams = ((reg >> 4) & 0x3);
}
insList.push_back(ins);
++readCount;
return ins;
} catch (...) {
// Deallocate and rethrow to outer handler
delete ins;
throw;
}
};
auto readInstrumentName = [&](SafeReader& reader, DivInstrument* ins) {
uint8_t nameLen = reader.readC();
String insName = (nameLen>0) ? reader.readString(nameLen) : "";
ins->name = stringNotBlank(insName)
? insName
: fmt::sprintf("%s [%d]", stripPath, readCount - 1);
};
try {
reader.seek(0, SEEK_SET);
uint16_t header = reader.readS();
uint8_t insMelodyCount, insDrumCount;
if (header == 0x0C1A) { // 26 12 in decimal bytes
uint8_t version = reader.readC();
if ((version ^ 3) > 0) {
// GYBv1/2
insMelodyCount = reader.readC();
insDrumCount = reader.readC();
if (insMelodyCount > 128 || insDrumCount > 128) {
throw std::invalid_argument("GYBv1/2 patch count is out of bounds.");
}
if (!reader.seek(0x100, SEEK_CUR)) { // skip MIDI instrument mapping
throw EndOfFileException(&reader, reader.tell() + 0x100);
}
if (version == 2) {
reader.readC(); // skip LFO speed (chip-global)
}
// Instrument data
for (int i = 0; i < (insMelodyCount+insDrumCount); ++i) {
readInstrument(reader, (version == 2));
// Additional data
reader.readC(); // skip transpose
if (version == 2) {
reader.readC(); // skip padding
}
}
// Instrument name
for (int i = 0; i < (insMelodyCount+insDrumCount); ++i) {
readInstrumentName(reader, insList[i]);
}
// Map to note assignment currently not supported.
} else {
// GYBv3+
reader.readC(); // skip LFO speed (chip-global)
uint32_t fileSize = reader.readI();
uint32_t bankOffset = reader.readI();
uint32_t mapOffset = reader.readI();
if (bankOffset > fileSize || mapOffset > fileSize) {
lastError = "GYBv3 file appears to have invalid data offsets.";
logE("GYBv3 file appears to have invalid data offsets.");
}
if (!reader.seek(bankOffset, SEEK_SET)) {
throw EndOfFileException(&reader, bankOffset);
}
uint16_t insCount = reader.readS();
size_t patchPosOffset = reader.tell();
for (int i = 0; i < insCount; ++i) {
uint16_t patchSize = reader.readS();
readInstrument(reader, true);
// Additional data
reader.readC(); // skip transpose
uint8_t additionalDataFlags = reader.readC() & 0x1; // skip additional data bitfield
// if chord notes attached, skip this
if ((additionalDataFlags&1) > 0) {
uint8_t notes = reader.readC();
for (int j = 0; j < notes; ++j) {
reader.readC();
}
}
// Instrument Name
readInstrumentName(reader, insList[i]);
// Retrieve next patch
if (!reader.seek(patchPosOffset + patchSize, SEEK_SET)) {
throw EndOfFileException(&reader, patchPosOffset + patchSize);
}
patchPosOffset = reader.tell();
}
}
reader.seek(0, SEEK_END);
}
} catch (EndOfFileException& e) {
lastError = "premature end of file";
logE("premature end of file");
is_failed = true;
} catch (std::invalid_argument& e) {
lastError = fmt::sprintf("Invalid value found in patch file. %s", e.what());
logE("Invalid value found in patch file.");
logE(e.what());
is_failed = true;
}
if (!is_failed) {
for (int i = 0; i < readCount; ++i) {
if (insList[i] != NULL) {
ret.push_back(insList[i]);
}
}
} else {
for (int i = 0; i < readCount; ++i) {
delete insList[i];
}
}
}
void DivEngine::loadOPM(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList;
int readCount = 0;
bool is_failed = false;
bool patchNameRead = false,
lfoRead = false,
characteristicRead = false,
m1Read = false,
c1Read = false,
m2Read = false,
c2Read = false;
DivInstrument* newPatch = NULL;
auto completePatchRead = [&]() -> bool {
return patchNameRead && lfoRead && characteristicRead && m1Read && c1Read && m2Read && c2Read;
};
auto resetPatchRead = [&]() {
patchNameRead = lfoRead = characteristicRead = m1Read = c1Read = m2Read = c2Read = false;
newPatch = NULL;
};
auto readIntStrWithinRange = [](String&& input, int limitLow = INT_MIN, int limitHigh = INT_MAX) -> int {
int x = std::stoi(input.c_str());
if (x > limitHigh || x < limitLow) {
throw std::invalid_argument(fmt::sprintf("%s is out of bounds of range [%d..%d]", input, limitLow, limitHigh));
}
return x;
};
auto readOpmOperator = [&](SafeReader& reader, DivInstrumentFM::Operator& op) {
op.ar = readIntStrWithinRange(reader.readStringToken(), 0, 31);
op.dr = readIntStrWithinRange(reader.readStringToken(), 0, 31);
op.d2r = readIntStrWithinRange(reader.readStringToken(), 0, 31);
op.rr = readIntStrWithinRange(reader.readStringToken(), 0, 31);
op.sl = readIntStrWithinRange(reader.readStringToken(), 0, 15);
op.tl = readIntStrWithinRange(reader.readStringToken(), 0, 127);
op.rs = readIntStrWithinRange(reader.readStringToken(), 0, 3);;
op.mult = readIntStrWithinRange(reader.readStringToken(), 0, 15);
op.dt = fmDtRegisterToFurnace(readIntStrWithinRange(reader.readStringToken(), 0, 7));
op.dt2 = readIntStrWithinRange(reader.readStringToken(), 0, 3);
op.am = readIntStrWithinRange(reader.readStringToken(), 0) > 0 ? 1 : 0;
};
auto seekGroupValStart = [](SafeReader& reader, int pos) {
// Seek to position then move to next ':' character
if (!reader.seek(pos, SEEK_SET)) {
throw EndOfFileException(&reader, pos);
}
reader.readStringToken(':', false);
};
try {
reader.seek(0, SEEK_SET);
while (!reader.isEOF()) {
// Checking line prefixes since they sometimes may not have a space after the ':'
size_t linePos = reader.tell();
String token = reader.readStringToken();
if (token.size() == 0) {
continue;
}
if (token.compare(0,2,"//") == 0) {
if (!reader.isEOF()) {
reader.readStringLine();
}
continue;
}
// At this point we know any other line would be associated with patch params
if (newPatch == NULL) {
newPatch = new DivInstrument;
newPatch->type = DIV_INS_FM;
newPatch->fm.ops = 4;
}
// Read each line for their respective params. They may not be written in the same LINE order but they
// must absolutely be properly grouped per patch! See inline comments indicating line structure examples.
if (token.size() >= 2) {
if (token[0] == '@') {
// @:123 Name of patch
seekGroupValStart(reader, linePos);
// Note: Fallback to bank filename and current patch number specified by @n
String opmPatchNum = reader.readStringToken();
String insName = reader.readStringLine();
newPatch->name = stringNotBlank(insName)
? insName
: fmt::sprintf("%s @%s", stripPath, opmPatchNum);
patchNameRead = true;
} else if (token.compare(0,3,"CH:") == 0) {
// CH: PAN FL CON AMS PMS SLOT NE
seekGroupValStart(reader, linePos);
reader.readStringToken(); // skip PAN
newPatch->fm.fb = readIntStrWithinRange(reader.readStringToken(), 0, 7);
newPatch->fm.alg = readIntStrWithinRange(reader.readStringToken(), 0, 7);
newPatch->fm.ams = readIntStrWithinRange(reader.readStringToken(), 0, 4);
newPatch->fm.fms = readIntStrWithinRange(reader.readStringToken(), 0, 7);
reader.readStringToken(); // skip SLOT (no furnace equivalent...yet?)
reader.readStringToken(); // skip NE (^^^)
characteristicRead = true;
} else if (token.compare(0,3,"C1:") == 0) {
// C1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN
seekGroupValStart(reader, linePos);
readOpmOperator(reader, newPatch->fm.op[2]);
c1Read = true;
} else if (token.compare(0,3,"C2:") == 0) {
// C2: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN
seekGroupValStart(reader, linePos);
readOpmOperator(reader, newPatch->fm.op[3]);
c2Read = true;
} else if (token.compare(0,3,"M1:") == 0) {
// M1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN
seekGroupValStart(reader, linePos);
readOpmOperator(reader, newPatch->fm.op[0]);
m1Read = true;
} else if (token.compare(0,3,"M2:") == 0) {
// M2: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN
seekGroupValStart(reader, linePos);
readOpmOperator(reader, newPatch->fm.op[1]);
m2Read = true;
} else if (token.compare(0,4,"LFO:") == 0) {
// LFO:LFRQ AMD PMD WF NFRQ
seekGroupValStart(reader, linePos);
// Furnace patches do not store these as they are chip-global.
reader.readStringLine();
lfoRead = true;
} else {
// other unsupported lines ignored.
reader.readStringLine();
}
}
if (completePatchRead()) {
insList.push_back(newPatch);
resetPatchRead();
++readCount;
}
}
if (newPatch != NULL) {
addWarning("Last OPM patch read was incomplete and therefore not imported.");
logW("Last OPM patch read was incomplete and therefore not imported.");
delete newPatch;
newPatch = NULL;
}
for (int i = 0; i < readCount; ++i) {
ret.push_back(insList[i]);
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
is_failed = true;
} catch (std::invalid_argument& e) {
lastError=fmt::sprintf("Invalid value found in patch file. %s", e.what());
logE("Invalid value found in patch file.");
logE(e.what());
is_failed = true;
}
if (is_failed) {
for (int i = readCount - 1; i >= 0; --i) {
delete insList[i];
}
if (newPatch != NULL) {
delete newPatch;
}
}
}
void DivEngine::loadWOPL(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList;
bool is_failed = false;
uint16_t version;
uint16_t meloBankCount;
uint16_t percBankCount;
std::vector<midibank_t*> meloMetadata;
std::vector<midibank_t*> percMetadata;
auto readWoplOp = [](SafeReader& reader, DivInstrumentFM::Operator& op) {
uint8_t characteristics = reader.readC();
uint8_t keyScaleLevel = reader.readC();
uint8_t attackDecay = reader.readC();
uint8_t sustainRelease = reader.readC();
uint8_t waveSelect = reader.readC();
int total = 0;
total += (op.mult = characteristics & 0xF);
total += (op.ksr = ((characteristics >> 4) & 0x1));
total += (op.sus = ((characteristics >> 5) & 0x1));
total += (op.vib = ((characteristics >> 6) & 0x1));
total += (op.am = ((characteristics >> 7) & 0x1));
total += (op.tl = keyScaleLevel & 0x3F);
total += (op.ksl = ((keyScaleLevel >> 6) & 0x3));
total += (op.ar = ((attackDecay >> 4) & 0xF));
total += (op.dr = attackDecay & 0xF);
total += (op.rr = sustainRelease & 0xF);
total += (op.sl = ((sustainRelease >> 4) & 0xF));
total += (op.ws = waveSelect);
return total;
};
auto doParseWoplInstrument = [&](bool isPerc, midibank_t*& metadata, int patchNum) {
DivInstrument* ins = new DivInstrument;
try {
long patchSum = 0;
ins->type = DIV_INS_OPL;
// Establish if it is a blank instrument.
String insName = reader.readString(32);
patchSum += insName.size();
// TODO adapt MIDI key offset to transpose?
reader.seek(7, SEEK_CUR); // skip MIDI params
uint8_t instTypeFlags = reader.readC(); // [0EEEDCBA] - see WOPL/OPLI spec
bool is_4op = ((instTypeFlags & 0x1) == 1);
bool is_2x2op = (((instTypeFlags>>1) & 0x1) == 1);
bool is_rhythm = (((instTypeFlags>>4) & 0x7) > 0);
uint8_t feedConnect = reader.readC();
uint8_t feedConnect2nd = reader.readC();
ins->fm.alg = (feedConnect & 0x1);
ins->fm.fb = ((feedConnect>>1) & 0xF);
if (is_4op && !is_2x2op) {
ins->fm.ops = 4;
ins->fm.alg = (feedConnect & 0x1) | ((feedConnect2nd & 0x1) << 1);
for (int i : {2,0,3,1}) { // omfg >_<
patchSum += readWoplOp(reader, ins->fm.op[i]);
}
} else {
ins->fm.ops = 2;
for (int i : {1,0}) {
patchSum += readWoplOp(reader, ins->fm.op[i]);
}
if (is_rhythm) {
ins->fm.opllPreset = (uint8_t)(1<<4);
} else if (is_2x2op) {
// Note: Pair detuning offset not mappable. Use E5xx effect :P
ins->name = stringNotBlank(insName)
? fmt::sprintf("%s (1)", insName)
: fmt::sprintf("%s[%s] %s Patch %d (1)",
stripPath, metadata->name, (isPerc) ? "Drum" : "Melodic", patchNum);
insList.push_back(ins);
patchSum = 0;
ins = new DivInstrument;
ins->type = DIV_INS_OPL;
ins->name = fmt::sprintf("%s (2)", insName);
ins->fm.alg = (feedConnect2nd & 0x1);
ins->fm.fb = ((feedConnect2nd >> 1) & 0xF);
for (int i : {1,0}) {
patchSum += readWoplOp(reader, ins->fm.op[i]);
}
}
if (!is_2x2op) {
reader.seek(10, SEEK_CUR); // skip unused operator pair
}
}
if (version >= 3) {
reader.readS_BE(); // skip keyon delay
reader.readS_BE(); // skip keyoff delay
}
if (patchSum > 0) {
// Write instrument
// TODO: OPL3BankEditor hardcodes GM1 Melodic patch names which are not included in the bank file......
if (is_2x2op) {
ins->name = stringNotBlank(insName)
? fmt::sprintf("%s (2)", insName)
: fmt::sprintf("%s[%s] %s Patch %d (2)",
stripPath, metadata->name, (isPerc) ? "Drum" : "Melodic", patchNum);
} else {
ins->name = stringNotBlank(insName)
? insName
: fmt::sprintf("%s[%s] %s Patch %d",
stripPath, metadata->name, (isPerc) ? "Drum" : "Melodic", patchNum);
}
insList.push_back(ins);
} else {
// Empty instrument
delete ins;
}
} catch (...) {
// Deallocate and allow outer handler to do the rest.
delete ins;
throw;
}
};
try {
reader.seek(0, SEEK_SET);
String header = reader.readString(11);
if (header == "WOPL3-BANK") {
version = reader.readS();
meloBankCount = reader.readS_BE();
percBankCount = reader.readS_BE();
reader.readC(); // skip chip-global LFO
reader.readC(); // skip additional flags
if (version >= 2) {
for (int i = 0; i < meloBankCount; ++i) {
meloMetadata.push_back(new midibank_t);
String bankName = reader.readString(32);
meloMetadata[i]->bankLsb = reader.readC();
meloMetadata[i]->bankMsb = reader.readC();
meloMetadata[i]->name = stringNotBlank(bankName)
? bankName
: fmt::sprintf("%d/%d", meloMetadata[i]->bankMsb, meloMetadata[i]->bankLsb);
}
for (int i = 0; i < percBankCount; ++i) {
percMetadata.push_back(new midibank_t);
String bankName = reader.readString(32);
percMetadata[i]->bankLsb = reader.readC();
percMetadata[i]->bankMsb = reader.readC();
percMetadata[i]->name = stringNotBlank(bankName)
? bankName
: fmt::sprintf("%d/%d", percMetadata[i]->bankMsb, percMetadata[i]->bankLsb);
}
} else {
// TODO do version 1 multibank sets even exist?
meloMetadata.push_back(new midibank_t);
meloMetadata[0]->bankLsb = 0;
meloMetadata[0]->bankMsb = 0;
meloMetadata[0]->name = "0/0";
percMetadata.push_back(new midibank_t);
percMetadata[0]->bankLsb = 0;
percMetadata[0]->bankMsb = 0;
percMetadata[0]->name = "0/0";
}
for (int i = 0; i < meloBankCount; ++i) {
for (int j = 0; j < 128; ++j) {
doParseWoplInstrument(false, meloMetadata[i], j);
}
}
for (int i = 0; i < percBankCount; ++i) {
for (int j = 0; j < 128; ++j) {
doParseWoplInstrument(true, percMetadata[i], j);
}
}
}
} catch (EndOfFileException& e) {
lastError = "premature end of file";
logE("premature end of file");
is_failed = true;
}
for (midibank_t* m : meloMetadata) {
delete m;
}
for (midibank_t* m : percMetadata) {
delete m;
}
if (is_failed) {
for (DivInstrument* p : insList) {
delete p;
}
} else {
for (DivInstrument* p : insList) {
ret.push_back(p);
}
}
}
void DivEngine::loadWOPN(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath) {
std::vector<DivInstrument*> insList;
bool is_failed = false;
uint16_t version;
uint16_t meloBankCount;
uint16_t percBankCount;
std::vector<midibank_t*> meloMetadata;
std::vector<midibank_t*> percMetadata;
auto readWopnOp = [](SafeReader& reader, DivInstrumentFM::Operator& op) {
uint8_t dtMul = reader.readC();
uint8_t totalLevel = reader.readC();
uint8_t arRateScale = reader.readC();
uint8_t drAmpEnable = reader.readC();
uint8_t d2r = reader.readC();
uint8_t susRelease = reader.readC();
uint8_t ssgEg = reader.readC();
int total = 0;
total += (op.mult = dtMul & 0xF);
total += (op.dt = ((dtMul >> 4) & 0x7));
total += (op.tl = totalLevel & 0x7F);
total += (op.rs = ((arRateScale >> 6) & 0x3));
total += (op.ar = arRateScale & 0x1F);
total += (op.dr = drAmpEnable & 0x1F);
total += (op.am = ((drAmpEnable >> 7) & 0x1));
total += (op.d2r = d2r & 0x1F);
total += (op.rr = susRelease & 0xF);
total += (op.sl = ((susRelease >> 4) & 0xF));
total += (op.ssgEnv = ssgEg);
return total;
};
auto doParseWopnInstrument = [&](bool isPerc, midibank_t*& metadata, int patchNum) {
DivInstrument* ins = new DivInstrument;
try {
long patchSum = 0;
ins->type = DIV_INS_FM;
ins->fm.ops = 4;
// Establish if it is a blank instrument.
String insName = reader.readString(32);
patchSum += insName.size();
// TODO adapt MIDI key offset to transpose?
if (!reader.seek(3, SEEK_CUR)) { // skip MIDI params
throw EndOfFileException(&reader, reader.tell() + 3);
}
uint8_t feedAlgo = reader.readC();
patchSum += feedAlgo;
ins->fm.alg = (feedAlgo & 0x7);
ins->fm.fb = ((feedAlgo >> 3) & 0x7);
patchSum += reader.readC(); // Skip global bank flags - see WOPN/OPNI spec
for (int i = 0; i < 4; ++i) {
patchSum += readWopnOp(reader, ins->fm.op[i]);
}
if (version >= 2) {
reader.readS_BE(); // skip keyon delay
reader.readS_BE(); // skip keyoff delay
}
if (patchSum > 0) {
// Write instrument
// TODO: OPN2BankEditor hardcodes GM1 Melodic patch names which are not included in the bank file......
ins->name = stringNotBlank(insName)
? insName
: fmt::sprintf("%s[%s] %s Patch %d",
stripPath, metadata->name, (isPerc) ? "Drum" : "Melodic", patchNum);
insList.push_back(ins);
} else {
// Empty instrument
delete ins;
}
} catch (...) {
// Deallocate and allow outer handler to do the rest.
delete ins;
throw;
}
};
try {
reader.seek(0, SEEK_SET);
String header = reader.readString(11);
if (header == "WOPN2-BANK" || header == "WOPN2-B2NK") { // omfg >_<
version = reader.readS();
if (!(version >= 2) || version > 0xF) {
// version 1 doesn't have a version field........
reader.seek(-2, SEEK_CUR);
version = 1;
}
meloBankCount = reader.readS_BE();
percBankCount = reader.readS_BE();
reader.readC(); // skip chip-global LFO
if (version >= 2) {
for (int i = 0; i < meloBankCount; ++i) {
meloMetadata.push_back(new midibank_t);
String bankName = reader.readString(32);
meloMetadata[i]->bankLsb = reader.readC();
meloMetadata[i]->bankMsb = reader.readC();
meloMetadata[i]->name = stringNotBlank(bankName)
? bankName
: fmt::sprintf("%d/%d", meloMetadata[i]->bankMsb, meloMetadata[i]->bankLsb);
}
for (int i = 0; i < percBankCount; ++i) {
percMetadata.push_back(new midibank_t);
String bankName = reader.readString(32);
percMetadata[i]->bankLsb = reader.readC();
percMetadata[i]->bankMsb = reader.readC();
percMetadata[i]->name = stringNotBlank(bankName)
? bankName
: fmt::sprintf("%d/%d", percMetadata[i]->bankMsb, percMetadata[i]->bankLsb);
}
} else {
// TODO do version 1 multibank sets even exist?
meloMetadata.push_back(new midibank_t);
meloMetadata[0]->bankLsb = 0;
meloMetadata[0]->bankMsb = 0;
meloMetadata[0]->name = "0/0";
percMetadata.push_back(new midibank_t);
percMetadata[0]->bankLsb = 0;
percMetadata[0]->bankMsb = 0;
percMetadata[0]->name = "0/0";
}
for (int i = 0; i < meloBankCount; ++i) {
for (int j = 0; j < 128; ++j) {
doParseWopnInstrument(false, meloMetadata[i], j);
}
}
for (int i = 0; i < percBankCount; ++i) {
for (int j = 0; j < 128; ++j) {
doParseWopnInstrument(true, percMetadata[i], j);
}
}
}
} catch (EndOfFileException& e) {
lastError = "premature end of file";
logE("premature end of file");
is_failed = true;
}
for (midibank_t* m : meloMetadata) {
delete m;
}
for (midibank_t* m : percMetadata) {
delete m;
}
if (is_failed) {
for (DivInstrument* p : insList) {
delete p;
}
} else {
for (DivInstrument* p : insList) {
ret.push_back(p);
}
}
}
std::vector<DivInstrument*> DivEngine::instrumentFromFile(const char* path) {
std::vector<DivInstrument*> ret;
warnings="";
const char* pathRedux=strrchr(path,DIR_SEPARATOR);
if (pathRedux==NULL) {
pathRedux=path;
} else {
pathRedux++;
}
String stripPath;
const char* pathReduxEnd=strrchr(pathRedux,'.');
if (pathReduxEnd==NULL) {
stripPath=pathRedux;
} else {
for (const char* i=pathRedux; i!=pathReduxEnd && (*i); i++) {
stripPath+=*i;
}
}
FILE* f=ps_fopen(path,"rb");
if (f==NULL) {
lastError=strerror(errno);
return ret;
}
unsigned char* buf;
ssize_t len;
if (fseek(f,0,SEEK_END)!=0) {
lastError=strerror(errno);
fclose(f);
return ret;
}
len=ftell(f);
if (len<0) {
lastError=strerror(errno);
fclose(f);
return ret;
}
if (len==(SIZE_MAX>>1)) {
lastError=strerror(errno);
fclose(f);
return ret;
}
if (len==0) {
lastError=strerror(errno);
fclose(f);
return ret;
}
if (fseek(f,0,SEEK_SET)!=0) {
lastError=strerror(errno);
fclose(f);
return ret;
}
buf=new unsigned char[len];
if (fread(buf,1,len,f)!=(size_t)len) {
logW("did not read entire instrument file buffer!");
lastError="did not read entire instrument file!";
delete[] buf;
return ret;
}
fclose(f);
SafeReader reader=SafeReader(buf,len);
unsigned char magic[16];
bool isFurnaceInstr=false;
try {
reader.read(magic,16);
if (memcmp("-Furnace instr.-",magic,16)==0) {
isFurnaceInstr=true;
}
} catch (EndOfFileException& e) {
reader.seek(0,SEEK_SET);
}
if (isFurnaceInstr) {
DivInstrument* ins=new DivInstrument;
try {
short version=reader.readS();
reader.readS(); // reserved
if (version>DIV_ENGINE_VERSION) {
warnings="this instrument is made with a more recent version of Furnace!";
}
unsigned int dataPtr=reader.readI();
reader.seek(dataPtr,SEEK_SET);
if (ins->readInsData(reader,version)!=DIV_DATA_SUCCESS) {
lastError="invalid instrument header/data!";
delete ins;
delete[] buf;
return ret;
} else {
ret.push_back(ins);
}
} catch (EndOfFileException& e) {
lastError="premature end of file";
logE("premature end of file");
delete ins;
delete[] buf;
return ret;
}
} else { // read as a different format
const char* ext=strrchr(path,'.');
DivInsFormats format=DIV_INSFORMAT_DMP;
if (ext!=NULL) {
String extS;
for (; *ext; ext++) {
char i=*ext;
if (i>='A' && i<='Z') {
i+='a'-'A';
}
extS+=i;
}
if (extS==".dmp") {
format=DIV_INSFORMAT_DMP;
} else if (extS==".tfi") {
format=DIV_INSFORMAT_TFI;
} else if (extS==".vgi") {
format=DIV_INSFORMAT_VGI;
} else if (extS==".fti") {
format=DIV_INSFORMAT_FTI;
} else if (extS==".bti") {
format=DIV_INSFORMAT_BTI;
} else if (extS==".s3i") {
format=DIV_INSFORMAT_S3I;
} else if (extS==".sbi") {
format=DIV_INSFORMAT_SBI;
} else if (extS==".opli") {
format=DIV_INSFORMAT_OPLI;
} else if (extS==".opni") {
format=DIV_INSFORMAT_OPNI;
} else if (extS==".y12") {
format=DIV_INSFORMAT_Y12;
} else if (extS==".bnk") {
format=DIV_INSFORMAT_BNK;
} else if (extS==".gyb") {
format=DIV_INSFORMAT_GYB;
} else if (extS==".opm") {
format=DIV_INSFORMAT_OPM;
} else if (extS==".ff") {
format=DIV_INSFORMAT_FF;
} else if (extS==".wopl") {
format=DIV_INSFORMAT_WOPL;
} else if (extS==".wopn") {
format=DIV_INSFORMAT_WOPN;
}
}
switch (format) {
case DIV_INSFORMAT_DMP:
loadDMP(reader,ret,stripPath);
break;
case DIV_INSFORMAT_TFI:
loadTFI(reader,ret,stripPath);
break;
case DIV_INSFORMAT_VGI:
loadVGI(reader,ret,stripPath);
break;
case DIV_INSFORMAT_FTI: // TODO
break;
case DIV_INSFORMAT_BTI: // TODO
break;
case DIV_INSFORMAT_S3I:
loadS3I(reader,ret,stripPath);
break;
case DIV_INSFORMAT_SBI:
loadSBI(reader,ret,stripPath);
break;
case DIV_INSFORMAT_OPLI:
loadOPLI(reader,ret,stripPath);
break;
case DIV_INSFORMAT_OPNI:
loadOPNI(reader,ret,stripPath);
break;
case DIV_INSFORMAT_Y12:
loadY12(reader,ret,stripPath);
break;
case DIV_INSFORMAT_BNK:
loadBNK(reader,ret,stripPath);
break;
case DIV_INSFORMAT_FF:
loadFF(reader,ret,stripPath);
break;
case DIV_INSFORMAT_GYB:
loadGYB(reader,ret,stripPath);
break;
case DIV_INSFORMAT_OPM:
loadOPM(reader,ret,stripPath);
break;
case DIV_INSFORMAT_WOPL:
loadWOPL(reader,ret,stripPath);
break;
case DIV_INSFORMAT_WOPN:
loadWOPN(reader,ret,stripPath);
break;
}
if (reader.tell()<reader.size()) {
addWarning("https://github.com/tildearrow/furnace/issues/84");
addWarning("there is more data at the end of the file! what happened here!");
addWarning(fmt::sprintf("exactly %d bytes, if you are curious",reader.size()-reader.tell()));
}
}
delete[] buf; // since we're done with this buffer
return ret;
}