/**
 * Furnace Tracker - multi-system chiptune tracker
 * Copyright (C) 2021-2024 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_OPM;
          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 if (sys==8) {
          ins->type=DIV_INS_OPM;
        } 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();
          // piece of crap offset by 1
          if (ins->type==DIV_INS_AY) {
            ins->std.waveMacro.val[i]++;
          }
        }
      } 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) {
        bool volIsCutoff=false;

        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
          volIsCutoff=reader.readI();
        } else {
          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 (volIsCutoff) {
          // move to alg (new cutoff)
          ins->std.algMacro.len=ins->std.volMacro.len;
          ins->std.algMacro.loop=ins->std.volMacro.loop;
          ins->std.algMacro.rel=ins->std.volMacro.rel;
          for (int j=0; j<ins->std.algMacro.len; j++) {
            ins->std.algMacro.val[j]=-(ins->std.volMacro.val[j]-18);
          }
          ins->std.volMacro.len=0;
          memset(ins->std.volMacro.val,0,256*sizeof(int));
        }
        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_OPM;
        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, bool loadAssets, bool readInsName) {
  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;
  bool isOldFurnaceIns=false;
  try {
    reader.read(magic,4);
    if (memcmp("FINS",magic,4)==0 || memcmp("FINB",magic,4)==0) {
      isFurnaceInstr=true;
      logV("found a new Furnace ins");
    } else {
      reader.read(&magic[4],12);
      if (memcmp("-Furnace instr.-",magic,16)==0) {
        logV("found an old Furnace ins");
        isFurnaceInstr=true;
        isOldFurnaceIns=true;
      }
    }
  } catch (EndOfFileException& e) {
    reader.seek(0,SEEK_SET);
  }

  if (isFurnaceInstr) {
    DivInstrument* ins=new DivInstrument;
    try {
      short version=0;
      if (isOldFurnaceIns) {
        version=reader.readS();
        reader.readS(); // reserved
      } else {
        version=reader.readS();
        reader.seek(0,SEEK_SET);
      }

      if (version>DIV_ENGINE_VERSION) {
        warnings="this instrument is made with a more recent version of Furnace!";
      }

      if (isOldFurnaceIns) {
        unsigned int dataPtr=reader.readI();
        reader.seek(dataPtr,SEEK_SET);
      }

      ins->name=stripPath;

      if (ins->readInsData(reader,version,loadAssets?(&song):NULL)!=DIV_DATA_SUCCESS) {
        lastError="invalid instrument header/data!";
        delete ins;
        delete[] buf;
        return ret;
      } else {
        if (!readInsName) {
          ins->name=stripPath;
        }
        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;
      } else {
        // unknown format
        lastError="unknown instrument format";
        delete[] buf;
        return ret;
      }
    }

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