diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 293c4cd0f..ac86f8199 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,10 @@ jobs: if [ '${{ runner.os }}' == 'macOS' ]; then amount=3 fi + # the Actions runner does not seem to be happy with two jobs at once on MinGW + if [ '${{ matrix.config.compiler }}' == 'mingw' ]; then + amount=1 + fi echo "Amount of cores we can build with: ${amount}" diff --git a/CMakeLists.txt b/CMakeLists.txt index c353d2c01..8e9c38810 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -565,6 +565,11 @@ if (NOT ANDROID) install(DIRECTORY papers DESTINATION ${CMAKE_INSTALL_DOCDIR}) install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/furnace) install(DIRECTORY demos DESTINATION ${CMAKE_INSTALL_DATADIR}/furnace) + foreach(num 16 32 64 128 256 512) + set(res ${num}x${num}) + install(FILES res/icon.iconset/icon_${res}.png RENAME furnace.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${res}/apps) + install(FILES res/icon.iconset/icon_${res}@2x.png RENAME furnace.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${res}@2/apps) + endforeach() install(FILES res/logo.png RENAME furnace.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/1024x1024/apps) endif() diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index c435ef11a..bb4b27eb2 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -814,6 +814,16 @@ bool DivEngine::removeSubSong(int index) { return true; } +void DivEngine::clearSubSongs() { + BUSY_BEGIN; + saveLock.lock(); + song.clearSongData(); + changeSong(0); + curOrder=0; + saveLock.unlock(); + BUSY_END; +} + void DivEngine::changeSystem(int index, DivSystem which, bool preserveOrder) { int chanCount=chans; quitDispatch(); @@ -1945,13 +1955,13 @@ int DivEngine::addSampleFromFile(const char* path) { } extS+=i; } - if (extS==String(".dmc")) { // read as .dmc + if (extS==".dmc") { // read as .dmc size_t len=0; DivSample* sample=new DivSample; int sampleCount=(int)song.sample.size(); sample->name=stripPath; - FILE* f=fopen(path,"rb"); + FILE* f=ps_fopen(path,"rb"); if (f==NULL) { BUSY_END; lastError=fmt::sprintf("could not open file! (%s)",strerror(errno)); diff --git a/src/engine/engine.h b/src/engine/engine.h index dc8ed044c..3dc81a418 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -399,8 +399,11 @@ class DivEngine { void loadOPNI(SafeReader& reader, std::vector& ret, String& stripPath); void loadY12(SafeReader& reader, std::vector& ret, String& stripPath); void loadBNK(SafeReader& reader, std::vector& ret, String& stripPath); + void loadGYB(SafeReader& reader, std::vector& ret, String& stripPath); void loadOPM(SafeReader& reader, std::vector& ret, String& stripPath); void loadFF(SafeReader& reader, std::vector& ret, String& stripPath); + void loadWOPL(SafeReader& reader, std::vector& ret, String& stripPath); + void loadWOPN(SafeReader& reader, std::vector& ret, String& stripPath); int loadSampleROM(String path, ssize_t expectedSize, unsigned char*& ret); @@ -820,6 +823,9 @@ class DivEngine { // remove subsong bool removeSubSong(int index); + // clear all subsong data + void clearSubSongs(); + // change system void changeSystem(int index, DivSystem which, bool preserveOrder=true); diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 362bf3c90..d5b9e8256 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -2682,16 +2682,19 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { if (song.ins.size()>256) { logE("maximum number of instruments is 256!"); lastError="maximum number of instruments is 256"; + saveLock.unlock(); return NULL; } if (song.wave.size()>256) { logE("maximum number of wavetables is 256!"); lastError="maximum number of wavetables is 256"; + saveLock.unlock(); return NULL; } if (song.sample.size()>256) { logE("maximum number of samples is 256!"); lastError="maximum number of samples is 256"; + saveLock.unlock(); return NULL; } diff --git a/src/engine/fileOpsIns.cpp b/src/engine/fileOpsIns.cpp index 521ab4249..bf83a2dcf 100644 --- a/src/engine/fileOpsIns.cpp +++ b/src/engine/fileOpsIns.cpp @@ -34,11 +34,14 @@ enum DivInsFormats { DIV_INSFORMAT_OPLI, DIV_INSFORMAT_OPNI, DIV_INSFORMAT_BNK, + DIV_INSFORMAT_GYB, DIV_INSFORMAT_OPM, + DIV_INSFORMAT_WOPL, + DIV_INSFORMAT_WOPN, DIV_INSFORMAT_FF, }; -// Patch data structures +// Reused patch data structures // SBI and some other OPL containers struct sbi_t { @@ -55,31 +58,14 @@ struct sbi_t { FeedConnect; }; -// Adlib Visual Composer BNK -struct bnkop_t { - uint8_t ksl, - multiple, - feedback, // op1 only - attack, - sustain, - eg, - decay, - releaseRate, - totalLevel, - am, - vib, - ksr, - con; // op1 only -}; -struct bnktimbre_t { - uint8_t mode, - percVoice; - bnkop_t op[2]; - uint8_t wave0, - wave1; +// MIDI-related +struct midibank_t { + String name; + uint8_t bankMsb, + bankLsb; }; -auto readSbiOpData = [](sbi_t& sbi, SafeReader& reader) { +static void readSbiOpData(sbi_t& sbi, SafeReader& reader) { sbi.Mcharacteristics = reader.readC(); sbi.Ccharacteristics = reader.readC(); sbi.Mscaling_output = reader.readC(); @@ -91,7 +77,16 @@ auto readSbiOpData = [](sbi_t& sbi, SafeReader& reader) { 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& ret, String& stripPath) { DivInstrument* ins=new DivInstrument; @@ -470,7 +465,6 @@ void DivEngine::loadS3I(SafeReader& reader, std::vector& ret, St // 12-byte opl value - identical to SBI format sbi_t s3i; - readSbiOpData(s3i, reader); DivInstrumentFM::Operator& opM = ins->fm.op[0]; @@ -511,8 +505,8 @@ void DivEngine::loadS3I(SafeReader& reader, std::vector& ret, St lastError="S3I PCM samples currently not supported."; logE("S3I PCM samples currently not supported."); } - ins->name = reader.readString(28); - ins->name = (ins->name.size() == 0) ? stripPath : ins->name; + String insName = reader.readString(28); + ins->name = stringNotBlank(insName) ? insName : stripPath; int s3i_signature = reader.readI(); @@ -531,6 +525,7 @@ void DivEngine::loadS3I(SafeReader& reader, std::vector& ret, St } void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, String& stripPath) { + std::vector insList; // in case 2x2op DivInstrument* ins=new DivInstrument; try { reader.seek(0, SEEK_SET); @@ -543,8 +538,8 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St bool is_6op = (sbi_header == 0x1A504F36); // 6OP\x1A - Freq Monster 801-specific // 32-byte null terminated instrument name - String patchName = reader.readString(32); - patchName = (patchName.size() == 0) ? stripPath : patchName; + 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; @@ -583,7 +578,7 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St DivInstrumentFM::Operator& opM = ins->fm.op[0]; DivInstrumentFM::Operator& opC = ins->fm.op[1]; ins->fm.ops = 2; - ins->name = patchName; + ins->name = insName; writeOp(sbi_op12, opM, opC); ins->fm.alg = (sbi_op12.FeedConnect & 0x1); ins->fm.fb = ((sbi_op12.FeedConnect >> 1) & 0x7); @@ -596,7 +591,7 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St // Ignore rest of file - rest is 'reserved padding'. reader.seek(4, SEEK_CUR); - ret.push_back(ins); + insList.push_back(ins); } else if (is_4op || is_6op) { readSbiOpData(sbi_op34, reader); @@ -609,7 +604,7 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St DivInstrumentFM::Operator& opM4 = ins->fm.op[1]; DivInstrumentFM::Operator& opC4 = ins->fm.op[3]; ins->fm.ops = 4; - ins->name = patchName; + 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); @@ -619,7 +614,7 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St // 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); - ret.push_back(ins); + insList.push_back(ins); readSbiOpData(sbi_op12, reader); @@ -628,7 +623,7 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St DivInstrumentFM::Operator& opC6 = ins->fm.op[1]; ins->type = DIV_INS_OPL; ins->fm.ops = 2; - ins->name = fmt::sprintf("%s (2op)", patchName); + 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); @@ -638,34 +633,64 @@ void DivEngine::loadSBI(SafeReader& reader, std::vector& ret, St // 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); - ret.push_back(ins); + insList.push_back(ins); } } catch (EndOfFileException& e) { lastError="premature end of file"; logE("premature end of file"); - delete ins; + 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& ret, String& stripPath) { + std::vector 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") { - uint16_t version = reader.readS(); - if (version > 3) { - logW("Unknown OPLI version."); - } + 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 = (insName.size() > 0) ? insName : stripPath; + 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 @@ -673,27 +698,6 @@ void DivEngine::loadOPLI(SafeReader& reader, std::vector& ret, S bool is_2x2op = (((instTypeFlags>>1) & 0x1) == 1); bool is_rhythm = (((instTypeFlags>>4) & 0x7) > 0); - 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; - }; - uint8_t feedConnect = reader.readC(); uint8_t feedConnect2nd = reader.readC(); @@ -717,7 +721,7 @@ void DivEngine::loadOPLI(SafeReader& reader, std::vector& ret, S } else if (is_2x2op) { // Note: Pair detuning offset not mappable. Use E5xx effect :P ins->name = fmt::sprintf("%s (1)", insName); - ret.push_back(ins); + insList.push_back(ins); ins = new DivInstrument; ins->type = DIV_INS_OPL; @@ -726,16 +730,28 @@ void DivEngine::loadOPLI(SafeReader& reader, std::vector& ret, S readOpliOp(reader, ins->fm.op[i]); } } + + if (!is_2x2op) { + reader.seek(10, SEEK_CUR); // skip unused operator pair + } } - // Skip rest of file - reader.seek(0, SEEK_END); - ret.push_back(ins); + insList.push_back(ins); } } catch (EndOfFileException& e) { lastError="premature end of file"; logE("premature end of file"); - delete ins; + if (ins != NULL) { + delete ins; + } + for (DivInstrument* p : insList) { + delete p; + } + return; + } + + for (DivInstrument* p : insList) { + ret.push_back(p); } } @@ -751,6 +767,7 @@ void DivEngine::loadOPNI(SafeReader& reader, std::vector& ret, S if (!(version >= 2) || version > 0xF) { // version 1 doesn't have a version field........ reader.seek(-2, SEEK_CUR); + version = 1; } reader.readC(); // skip isPerc @@ -758,8 +775,11 @@ void DivEngine::loadOPNI(SafeReader& reader, std::vector& ret, S ins->fm.ops = 4; String insName = reader.readString(32); - ins->name = (insName.size() > 0) ? insName : stripPath; - reader.seek(3, SEEK_CUR); // skip MIDI params + 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); @@ -791,14 +811,14 @@ void DivEngine::loadOPNI(SafeReader& reader, std::vector& ret, S readOpniOp(reader, ins->fm.op[i]); } - // Skip rest of file - reader.seek(0, SEEK_END); ret.push_back(ins); } } catch (EndOfFileException& e) { lastError="premature end of file"; logE("premature end of file"); - delete ins; + if (ins != NULL) { + delete ins; + } } } @@ -841,7 +861,9 @@ void DivEngine::loadY12(SafeReader& reader, std::vector& ret, St } catch (EndOfFileException& e) { lastError="premature end of file"; logE("premature end of file"); - delete ins; + if (ins != NULL) { + delete ins; + } } } @@ -855,6 +877,7 @@ void DivEngine::loadBNK(SafeReader& reader, std::vector& ret, St bool is_adlib = ((header>>8) == 0x2d42494c444100L); bool is_failed = false; int readCount = 0; + int insCount = 0; if (is_adlib) { try { @@ -868,7 +891,7 @@ void DivEngine::loadBNK(SafeReader& reader, std::vector& ret, St while (reader.tell() < data_offset) { reader.seek(3, SEEK_CUR); instNames.push_back(new String(reader.readString(9))); - ++readCount; + ++insCount; } // Seek to BNK data @@ -876,58 +899,54 @@ void DivEngine::loadBNK(SafeReader& reader, std::vector& ret, St throw EndOfFileException(&reader, data_offset); }; - // Read until EOF - for (int i = 0; i < readCount; ++i) { - bnktimbre_t timbre; - insList.push_back(new DivInstrument); - auto& ins = insList[i]; + // 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; - timbre.mode = reader.readC(); - timbre.percVoice = reader.readC(); - if (timbre.mode == 1) { + uint8_t timbreMode = reader.readC(); + reader.readC(); // skip timbre perc voice + if (timbreMode == 1) { ins->fm.opllPreset = (uint8_t)(1<<4); } - ins->fm.op[0].ksl = reader.readC(); - ins->fm.op[0].mult = reader.readC(); - ins->fm.fb = reader.readC(); - ins->fm.op[0].ar = reader.readC(); - ins->fm.op[0].sl = reader.readC(); - ins->fm.op[0].sus = (reader.readC() != 0) ? 1 : 0; - ins->fm.op[0].dr = reader.readC(); - ins->fm.op[0].rr = reader.readC(); - ins->fm.op[0].tl = reader.readC(); - ins->fm.op[0].am = reader.readC(); - ins->fm.op[0].vib = reader.readC(); - ins->fm.op[0].ksr = reader.readC(); - ins->fm.alg = (reader.readC() == 0) ? 1 : 0; - - ins->fm.op[1].ksl = reader.readC(); - ins->fm.op[1].mult = reader.readC(); - reader.readC(); // skip - ins->fm.op[1].ar = reader.readC(); - ins->fm.op[1].sl = reader.readC(); - ins->fm.op[1].sus = (reader.readC() != 0) ? 1 : 0; - ins->fm.op[1].dr = reader.readC(); - ins->fm.op[1].rr = reader.readC(); - ins->fm.op[1].tl = reader.readC(); - ins->fm.op[1].am = reader.readC(); - ins->fm.op[1].vib = reader.readC(); - ins->fm.op[1].ksr = reader.readC(); - reader.readC(); // skip + 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 = instNames[i]->length() > 0 ? (*instNames[i]) : fmt::sprintf("%s[%d]", stripPath, i); + 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 = readCount - 1; i >= 0; --i) { + for (int i = 0; i < readCount; ++i) { delete insList[i]; } is_failed = true; @@ -945,7 +964,7 @@ void DivEngine::loadBNK(SafeReader& reader, std::vector& ret, St } } - for (auto& name : instNames) { + for (String* name : instNames) { delete name; } } @@ -970,9 +989,7 @@ void DivEngine::loadFF(SafeReader& reader, std::vector& ret, Str for (unsigned int j = 0; j < 4; j++) { buf = reader.readC(); ins->fm.op[j].mult = buf & 0xf; - // detune needs extra translation from register to furnace format - const int dtNative = (buf >> 4) & 0x7; - ins->fm.op[j].dt = (dtNative >= 4) ? (7 - dtNative) : (dtNative + 3); + 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++) { @@ -1012,7 +1029,8 @@ void DivEngine::loadFF(SafeReader& reader, std::vector& ret, Str } catch (EndOfFileException& e) { lastError="premature end of file"; logE("premature end of file"); - for (int i = readCount - 1; i >= 0; --i) { + // Include incomplete entry in deletion. + for (int i = readCount; i >= 0; --i) { delete insList[i]; } return; @@ -1023,6 +1041,197 @@ void DivEngine::loadFF(SafeReader& reader, std::vector& ret, Str } } +void DivEngine::loadGYB(SafeReader& reader, std::vector& ret, String& stripPath) { + std::vector 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& ret, String& stripPath) { std::vector insList; @@ -1039,20 +1248,19 @@ void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, St DivInstrument* newPatch = NULL; - auto completePatchRead = [&]() { + 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 limitHigh) { + auto readIntStrWithinRange = [](String&& input, int limitLow, int limitHigh) -> 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>limitHigh) ? limitHigh : - (x& ret, St op.tl = readIntStrWithinRange(reader.readStringToken(), 0, 127); op.rs = readIntStrWithinRange(reader.readStringToken(), 0, 3);; op.mult = readIntStrWithinRange(reader.readStringToken(), 0, 15); - op.dt = readIntStrWithinRange(reader.readStringToken(), 0, 7); - op.dt = (op.dt >= 4) ? (7 - op.dt) : (op.dt + 3); + op.dt = fmDtRegisterToFurnace(readIntStrWithinRange(reader.readStringToken(), 0, 7)); op.dt2 = readIntStrWithinRange(reader.readStringToken(), 0, 3); op.am = readIntStrWithinRange(reader.readStringToken(), 0, 1); }; + 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; @@ -1092,53 +1308,62 @@ void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, St } // 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! Line prefixes must be separated by a space! (see inline comments) + // 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 - // Note: Fallback to bank filename and current patch number in _file_ order (not @n order) - newPatch->name = reader.readStringLine(); - newPatch->name = newPatch->name.size() > 0 ? newPatch->name : fmt::sprintf("%s[%d]", stripPath, readCount); + 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 - reader.readStringToken(); // skip NE + 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 + // 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(); @@ -1156,6 +1381,7 @@ void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, St 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) { @@ -1182,6 +1408,372 @@ void DivEngine::loadOPM(SafeReader& reader, std::vector& ret, St } } + +void DivEngine::loadWOPL(SafeReader& reader, std::vector& ret, String& stripPath) { + std::vector insList; + bool is_failed = false; + + uint16_t version; + uint16_t meloBankCount; + uint16_t percBankCount; + std::vector meloMetadata; + std::vector 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); + 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& ret, String& stripPath) { + std::vector insList; + bool is_failed = false; + + uint16_t version; + uint16_t meloBankCount; + uint16_t percBankCount; + std::vector meloMetadata; + std::vector 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 & 0x3F); + 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 DivEngine::instrumentFromFile(const char* path) { std::vector ret; warnings=""; @@ -1297,40 +1889,45 @@ std::vector DivEngine::instrumentFromFile(const char* path) { } extS+=i; } - if (extS==String(".dmp")) { + if (extS==".dmp") { format=DIV_INSFORMAT_DMP; - } else if (extS==String(".tfi")) { + } else if (extS==".tfi") { format=DIV_INSFORMAT_TFI; - } else if (extS==String(".vgi")) { + } else if (extS==".vgi") { format=DIV_INSFORMAT_VGI; - } else if (extS==String(".fti")) { + } else if (extS==".fti") { format=DIV_INSFORMAT_FTI; - } else if (extS==String(".bti")) { + } else if (extS==".bti") { format=DIV_INSFORMAT_BTI; - } else if (extS==String(".s3i")) { + } else if (extS==".s3i") { format=DIV_INSFORMAT_S3I; - } else if (extS==String(".sbi")) { + } else if (extS==".sbi") { format=DIV_INSFORMAT_SBI; - } else if (extS==String(".opli")) { + } else if (extS==".opli") { format=DIV_INSFORMAT_OPLI; - } else if (extS==String(".opni")) { - format=DIV_INSFORMAT_OPNI;; - } else if (extS==String(".y12")) { + } else if (extS==".opni") { + format=DIV_INSFORMAT_OPNI; + } else if (extS==".y12") { format=DIV_INSFORMAT_Y12; - } else if (extS==String(".bnk")) { + } else if (extS==".bnk") { format=DIV_INSFORMAT_BNK; - } else if (extS==String(".opm")) { + } else if (extS==".gyb") { + format=DIV_INSFORMAT_GYB; + } else if (extS==".opm") { format=DIV_INSFORMAT_OPM; - } else if (extS==String(".ff")) { + } 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: { + case DIV_INSFORMAT_DMP: loadDMP(reader,ret,stripPath); break; - } case DIV_INSFORMAT_TFI: loadTFI(reader,ret,stripPath); break; @@ -1351,19 +1948,28 @@ std::vector DivEngine::instrumentFromFile(const char* path) { loadOPLI(reader,ret,stripPath); break; case DIV_INSFORMAT_OPNI: - loadOPNI(reader, ret, stripPath); + loadOPNI(reader,ret,stripPath); break; case DIV_INSFORMAT_Y12: loadY12(reader,ret,stripPath); break; case DIV_INSFORMAT_BNK: - loadBNK(reader, ret, stripPath); + 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); + loadOPM(reader,ret,stripPath); + break; + case DIV_INSFORMAT_WOPL: + loadWOPL(reader,ret,stripPath); + break; + case DIV_INSFORMAT_WOPN: + loadWOPN(reader,ret,stripPath); break; } @@ -1374,5 +1980,6 @@ std::vector DivEngine::instrumentFromFile(const char* path) { } } + delete[] buf; // since we're done with this buffer return ret; } diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index 94546ed1d..329af9585 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -660,6 +660,7 @@ void DivPlatformAY8910::setFlags(unsigned int flags) { break; } ay->device_start(); + ay->device_reset(); stereo=(flags>>6)&1; } diff --git a/src/engine/platform/es5506.cpp b/src/engine/platform/es5506.cpp index b131215d2..cb33005a8 100644 --- a/src/engine/platform/es5506.cpp +++ b/src/engine/platform/es5506.cpp @@ -47,6 +47,15 @@ rWrite((a),__VA_ARGS__); \ } +#define pageReadMask(p,pm,a,...) \ + if (!skipRegisterWrites) { \ + if ((curPage&(pm))!=((p)&(pm))) { \ + curPage=(curPage&~(pm))|((p)&(pm)); \ + rWrite(0xf,curPage,(pm)); \ + } \ + rRead((a),__VA_ARGS__); \ + } + const char* regCheatSheetES5506[]={ "CR", "00|00", @@ -167,6 +176,8 @@ const char* DivPlatformES5506::getEffectName(unsigned char effect) { return "3xxx: Set filter coefficient K1"; } else if ((effect&0xf0)==0x40) { return "4xxx: Set filter coefficient K2"; + } else if ((effect&0xf0)==0x50) { + return "5xxx: Set transwave slice point"; } break; } @@ -268,6 +279,101 @@ void DivPlatformES5506::e_pin(bool state) pageWriteMask(0x00|ch,0x5f,0x00,(chan[ch].pcm.reversed?0x0000:0x0040)|0x08,0x78); chan[ch].isReverseLoop=false; } + if (chan[ch].transwaveIRQ) { + if ((chan[ch].cr&0x37)==0x34) { // IRQE = 1, BLE = 1, LPE = 0, LEI = 1 + DivInstrument* ins=parent->getIns(chan[i].ins); + if (!ins->amiga.useNoteMap && ins->amiga.transWave.enable) { + const int next=chan[ch].pcm.next; + bool sampleVaild=false; + if (next>=0 && nextamiga.transWaveMap.size()) { + DivInstrumentAmiga::TransWaveMap& transWaveInd=ins->amiga.transWaveMap[next]; + int sample=transWaveInd.ind; + if (sample>=0 && samplesong.sampleLen) { + sampleVaild=true; + chan[ch].pcm.index=sample; + chan[ch].transWave.ind=next; + DivSample* s=parent->getSample(sample); + // get frequency offset + double off=1.0; + double center=s->centerRate; + if (center<1) { + off=1.0; + } else { + off=(double)center/8363.0; + } + // get loop mode, transwave loop + double loopStart=(double)s->loopStart; + double loopEnd=(double)s->loopEnd; + DivSampleLoopMode loopMode=s->isLoopable()?s->loopMode:DIV_SAMPLE_LOOPMODE_ONESHOT; + if (transWaveInd.loopMode!=DIV_SAMPLE_LOOPMODE_ONESHOT) { + loopMode=transWaveInd.loopMode; + } else if ((chan[ch].pcm.loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT) || (!s->isLoopable())) { // default + loopMode=DIV_SAMPLE_LOOPMODE_PINGPONG; + } + // get loop position + loopStart=(double)transWaveInd.loopStart; + loopEnd=(double)transWaveInd.loopEnd; + if (ins->amiga.transWave.sliceEnable) { // sliced loop position? + chan[ch].transWave.updateSize(s->samples,loopStart,loopEnd); + chan[ch].transWave.slice=transWaveInd.slice; + chan[ch].transWave.slicePos(chan[ch].transWave.slice); + loopStart=transWaveInd.sliceStart; + loopEnd=transWaveInd.sliceEnd; + } + // get reversed + bool reversed=ins->amiga.reversed; + if (transWaveInd.reversed!=2) { + reversed=transWaveInd.reversed; + } + const unsigned int start=s->offES5506<<10; + const unsigned int length=s->samples-1; + const unsigned int end=start+(length<<11); + chan[ch].pcm.loopMode=loopMode; + chan[ch].pcm.nextFreqOffs=PITCH_OFFSET*off; + chan[ch].pcm.reversed=reversed; + chan[ch].pcm.bank=(s->offES5506>>22)&3; + chan[ch].pcm.start=start; + chan[ch].pcm.loopStart=(start+(unsigned int)(loopStart*2048.0))&0xfffff800; + chan[ch].pcm.loopEnd=(start+(unsigned int)((loopEnd-1.0)*2048.0))&0xffffff80; + chan[ch].pcm.end=end; + chan[ch].pcm.length=length; + pageWrite(0x20|ch,0x01,chan[ch].pcm.loopStart); + pageWrite(0x20|ch,0x02,chan[ch].pcm.loopEnd); + pageWrite(0x20|ch,0x03,(chan[ch].pcm.reversed)?chan[ch].pcm.loopEnd:chan[ch].pcm.loopStart); + unsigned int loopFlag=(chan[ch].pcm.bank<<14)|(chan[ch].pcm.reversed?0x0040:0x0000); + chan[ch].isReverseLoop=false; + switch (chan[ch].pcm.loopMode) { + case DIV_SAMPLE_LOOPMODE_ONESHOT: // One shot (no loop) + default: + break; + case DIV_SAMPLE_LOOPMODE_FORWARD: // Foward loop + loopFlag|=0x0008; + break; + case DIV_SAMPLE_LOOPMODE_BACKWARD: // Backward loop: IRQ enable + loopFlag|=0x0038; + chan[ch].isReverseLoop=true; + break; + case DIV_SAMPLE_LOOPMODE_PINGPONG: // Pingpong loop: Hardware support + loopFlag|=0x0018; + break; + } + // Set loop mode & Bank + pageWriteMask(0x00|ch,0x5f,0x00,loopFlag,0xfcfc); + } + } + if (sampleVaild) { + chan[ch].noteChanged.offs=1; + } + chan[ch].pcmChanged.changed=0; + } + } + chan[ch].transwaveIRQ=false; + } + if (chan[ch].isTransWave) { + pageReadMask(0x00|ch,0x5f,0x00,&chan[ch].cr); + chan[ch].transwaveIRQ=true; + chan[ch].isTransWave=false; + } } } } @@ -460,14 +566,56 @@ void DivPlatformES5506::tick(bool sysTick) { } } // control macros - if (!chan[i].keyOn) { - if (chan[i].active && chan[i].std.alg.had) { - if (chan[i].pcm.pause!=(bool)(chan[i].std.alg.val&1)) { - chan[i].pcm.pause=chan[i].std.alg.val&1; + if (chan[i].active && chan[i].std.alg.had) { + if (chan[i].pcm.pause!=(bool)(chan[i].std.alg.val&1)) { + chan[i].pcm.pause=chan[i].std.alg.val&1; + if (!chan[i].keyOn) { pageWriteMask(0x00|i,0x5f,0x00,chan[i].pcm.pause?0x0002:0x0000,0x0002); } } } + // transwave macros + if (chan[i].transWave.enable) { + if (chan[i].std.wave.had) { + if (chan[i].std.wave.val>=0 && chan[i].std.wave.valamiga.transWaveMap.size()) { + if (chan[i].pcm.next!=chan[i].std.wave.val) { + chan[i].pcm.next=chan[i].std.wave.val; + chan[i].pcmChanged.transwaveInd=1; + } + } + } + if (chan[i].std.fb.had) { + if (chan[i].transWave.sliceEnable!=(bool)(chan[i].std.fb.val&1)) { + chan[i].transWave.sliceEnable=chan[i].std.fb.val&1; + chan[i].pcmChanged.slice=1; + } + } + if (chan[i].std.fms.had) { + if (chan[i].transWave.slice!=(unsigned short)(chan[i].std.fms.val&0xfff)) { + chan[i].transWave.slice=chan[i].std.fms.val&0xfff; + chan[i].pcmChanged.slice=1; + } + } + } else if (chan[i].pcm.isNoteMap) { + // note map macros + if (chan[i].std.wave.had) { + if (chan[i].std.wave.val>=0 && chan[i].std.wave.val<120) { + if (chan[i].pcm.next!=chan[i].std.wave.val) { + chan[i].pcm.next=chan[i].std.wave.val; + chan[i].pcmChanged.index=1; + } + } + } + } else if (!chan[i].transWave.enable && !chan[i].pcm.isNoteMap) { + if (chan[i].std.wave.had) { + if (chan[i].std.wave.val>=0 && chan[i].std.wave.valsong.sampleLen) { + if (chan[i].pcm.next!=chan[i].std.wave.val) { + chan[i].pcm.next=chan[i].std.wave.val; + chan[i].pcmChanged.index=1; + } + } + } + } // update registers if (chan[i].volChanged.changed) { if (!isMuted[i]) { // calculate volume (16 bit) @@ -489,34 +637,193 @@ void DivPlatformES5506::tick(bool sysTick) { } chan[i].volChanged.changed=0; } - if (chan[i].pcmChanged) { - DivInstrument* ins=parent->getIns(chan[i].ins); - if (!ins->amiga.useNoteMap) { - double off=1.0; - if (chan[i].pcm.next>=0 && chan[i].pcm.nextsong.sampleLen) { - chan[i].pcm.index=chan[i].pcm.next; - DivSample* s=parent->getSample(chan[i].pcm.next); - if (s->centerRate<1) { - off=1.0; - } else { - off=(double)s->centerRate/8363.0; + if (chan[i].pcmChanged.changed) { + if (!chan[i].isTransWave) { + if (chan[i].pcmChanged.transwaveInd && (!ins->amiga.useNoteMap && ins->amiga.transWave.enable)) { + const int next=chan[i].pcm.next; + bool sampleVaild=false; + if (next>=0 && nextamiga.transWaveMap.size()) { + DivInstrumentAmiga::TransWaveMap& transWaveInd=ins->amiga.transWaveMap[next]; + int sample=transWaveInd.ind; + if (sample>=0 && samplesong.sampleLen) { + if (chan[i].pcm.index!=sample) { + pageWriteMask(0x00|i,0x5f,0x00,0x0034,0x00ff); // Set IRQ + chan[i].isTranswave=true; + } else { + chan[i].transWave.ind=next; + DivSample* s=parent->getSample(sample); + // get loop mode, transwave loop + double loopStart=(double)s->loopStart; + double loopEnd=(double)s->loopEnd; + DivSampleLoopMode loopMode=s->isLoopable()?s->loopMode:DIV_SAMPLE_LOOPMODE_ONESHOT; + if (transWaveInd.loopMode!=DIV_SAMPLE_LOOPMODE_ONESHOT) { + loopMode=transWaveInd.loopMode; + } else if ((chan[i].pcm.loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT) || (!s->isLoopable())) { // default + loopMode=DIV_SAMPLE_LOOPMODE_PINGPONG; + } + // get loop position + loopStart=(double)transWaveInd.loopStart; + loopEnd=(double)transWaveInd.loopEnd; + if (ins->amiga.transWave.sliceEnable) { // sliced loop position? + chan[i].transWave.updateSize(s->samples,loopStart,loopEnd); + chan[i].transWave.slice=transWaveInd.slice; + chan[i].transWave.slicePos(transWaveInd.slice); + loopStart=transWaveInd.sliceStart; + loopEnd=transWaveInd.sliceEnd; + } + // get reversed + bool reversed=ins->amiga.reversed; + if (transWaveInd.reversed!=2) { + reversed=transWaveInd.reversed; + } + chan[i].pcm.loopMode=loopMode; + chan[i].pcm.reversed=reversed; + if (sampleVaild) { + chan[i].pcmChanged.slice=1; + chan[i].pcmChanged.loopBank=1; + } + } + } } - const unsigned int start=s->offES5506<<10; - const unsigned int length=s->samples-1; - const unsigned int end=start+(length<<11); - chan[i].pcm.loopMode=s->isLoopable()?s->loopMode:DIV_SAMPLE_LOOPMODE_ONESHOT; - chan[i].pcm.freqOffs=PITCH_OFFSET*off; - chan[i].pcm.reversed=ins->amiga.reversed; - chan[i].pcm.bank=(s->offES5506>>22)&3; - chan[i].pcm.start=start; - chan[i].pcm.end=end; - chan[i].pcm.length=length; - chan[i].pcm.loopStart=(start+(s->loopStart<<11))&0xfffff800; - chan[i].pcm.loopEnd=(start+((s->loopEnd-1)<<11))&0xffffff80; - chan[i].keyOn=true; + chan[i].pcmChanged.transwaveInd=0; + } + if ((!chan[i].pcmChanged.transwaveInd) && (!chan[i].isTransWave)) { + if (chan[i].pcmChanged.index) { + const int next=chan[i].pcm.next; + bool sampleVaild=false; + if (((ins->amiga.useNoteMap && !ins->amiga.transWave.enable) && (next>=0 && next<120)) || + ((!ins->amiga.useNoteMap && ins->amiga.transWave.enable) && (next>=0 && nextamiga.transWaveMap.size())) || + ((!ins->amiga.useNoteMap && !ins->amiga.transWave.enable) && (next>=0 && nextsong.sampleLen))) { + DivInstrumentAmiga::NoteMap& noteMapind=ins->amiga.noteMap[next]; + DivInstrumentAmiga::TransWaveMap& transWaveInd=ins->amiga.transWaveMap[next]; + int sample=next; + if (ins->amiga.transWave.enable) { + sample=transWaveInd.ind; + } else if (ins->amiga.useNoteMap) { + sample=noteMapind.ind; + } + if (sample>=0 && samplesong.sampleLen) { + sampleVaild=true; + chan[i].pcm.index=sample; + chan[i].pcm.isNoteMap=ins->amiga.useNoteMap && !ins->amiga.transWave.enable; + chan[i].transWave.enable=!ins->amiga.useNoteMap && ins->amiga.transWave.enable; + chan[i].transWave.ind=next; + DivSample* s=parent->getSample(sample); + // get frequency offset + double off=1.0; + double center=s->centerRate; + if (center<1) { + off=1.0; + } else { + off=(double)center/8363.0; + } + if (ins->amiga.useNoteMap) { + off*=(double)noteMapind.freq/((double)MAX(1,center)*pow(2.0,((double)next-48.0)/12.0)); + chan[i].pcm.note=next; + } + // get loop mode, transwave loop + double loopStart=(double)s->loopStart; + double loopEnd=(double)s->loopEnd; + DivSampleLoopMode loopMode=s->isLoopable()?s->loopMode:DIV_SAMPLE_LOOPMODE_ONESHOT; + if (ins->amiga.transWave.enable) { + if (transWaveInd.loopMode!=DIV_SAMPLE_LOOPMODE_ONESHOT) { + loopMode=transWaveInd.loopMode; + } else if ((chan[i].pcm.loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT) || (!s->isLoopable())) { // default + loopMode=DIV_SAMPLE_LOOPMODE_PINGPONG; + } + // get loop position + loopStart=(double)transWaveInd.loopStart; + loopEnd=(double)transWaveInd.loopEnd; + if (ins->amiga.transWave.sliceEnable) { // sliced loop position? + chan[i].transWave.updateSize(s->samples,loopStart,loopEnd); + chan[i].transWave.slice=transWaveInd.slice; + chan[i].transWave.slicePos(transWaveInd.slice); + loopStart=transWaveInd.sliceStart; + loopEnd=transWaveInd.sliceEnd; + } + } + // get reversed + bool reversed=ins->amiga.reversed; + if (ins->amiga.transWave.enable&&transWaveInd.reversed!=2) { + reversed=transWaveInd.reversed; + } else if (ins->amiga.useNoteMap&¬eMapind.reversed!=2) { + reversed=noteMapind.reversed; + } + const unsigned int start=s->offES5506<<10; + const unsigned int length=s->samples-1; + const unsigned int end=start+(length<<11); + chan[i].pcm.loopMode=loopMode; + chan[i].pcm.nextFreqOffs=PITCH_OFFSET*off; + chan[i].pcm.reversed=reversed; + chan[i].pcm.bank=(s->offES5506>>22)&3; + chan[i].pcm.start=start; + chan[i].pcm.end=end; + chan[i].pcm.length=length; + chan[i].keyOn=true; + } + } + if (sampleVaild) { + chan[i].pcmChanged.slice=1; + chan[i].pcmChanged.loopBank=1; + chan[i].noteChanged.offs=1; + } + chan[i].pcmChanged.index=0; + } + if (chan[i].pcmChanged.slice) { + if (!chan[i].keyOn) { + if (chan[i].pcm.index>=0 && chan[i].pcm.indexsong.sampleLen) { + // get loop mode, transwave loop + DivSample* s=parent->getSample(chan[i].pcm.index); + double loopStart=(double)s->loopStart; + double loopEnd=(double)s->loopEnd; + if (ins->amiga.transWave.sliceEnable) { // sliced loop position? + chan[i].transWave.updateSize(s->samples,loopStart,loopEnd); + chan[i].transWave.slicePos(chan[i].transWave.slice); + loopStart=chan[i].transWave.sliceStart; + loopEnd=chan[i].transWave.sliceEnd; + } + const unsigned int start=s->offES5506<<10; + chan[i].pcm.loopStart=(start+(unsigned int)(loopStart*2048.0))&0xfffff800; + chan[i].pcm.loopEnd=(start+(unsigned int)((loopEnd-1.0)*2048.0))&0xffffff80; + chan[i].pcmChanged.position=1; + } + } + chan[i].pcmChanged.slice=0; + } + if (chan[i].pcmChanged.position) { + if (!chan[i].keyOn) { + pageWrite(0x20|i,0x01,(chan[i].pcm.loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT)?chan[i].pcm.start:chan[i].pcm.loopStart); + pageWrite(0x20|i,0x02,(chan[i].pcm.loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT)?chan[i].pcm.end:chan[i].pcm.loopEnd); + } + chan[i].pcmChanged.position=0; + } + if (chan[i].pcmChanged.loopBank) { + if (!chan[i].keyOn) { + unsigned int loopFlag=(chan[i].pcm.bank<<14)|(chan[i].pcm.reversed?0x0040:0x0000); + chan[i].isReverseLoop=false; + switch (chan[i].pcm.loopMode) { + case DIV_SAMPLE_LOOPMODE_ONESHOT: // One shot (no loop) + default: + break; + case DIV_SAMPLE_LOOPMODE_FORWARD: // Foward loop + loopFlag|=0x0008; + break; + case DIV_SAMPLE_LOOPMODE_BACKWARD: // Backward loop: IRQ enable + loopFlag|=0x0038; + chan[i].isReverseLoop=true; + break; + case DIV_SAMPLE_LOOPMODE_PINGPONG: // Pingpong loop: Hardware support + loopFlag|=0x0018; + break; + } + // Set loop mode & Bank + pageWriteMask(0x00|i,0x5f,0x00,loopFlag,0xfcfd); + } + chan[i].pcmChanged.loopBank=0; + } + chan[i].pcmChanged.dummy=0; } } - chan[i].pcmChanged=false; } if (chan[i].filterChanged.changed) { if (!chan[i].keyOn) { @@ -564,12 +871,13 @@ void DivPlatformES5506::tick(bool sysTick) { if (chan[i].noteChanged.offs) { if (chan[i].pcm.freqOffs!=chan[i].pcm.nextFreqOffs) { chan[i].pcm.freqOffs=chan[i].pcm.nextFreqOffs; - const int nextFreq=NOTE_ES5506(i,chan[i].nextNote); + const int nextFreq=NOTE_ES5506(i,chan[i].prevNote); if (chan[i].nextFreq!=nextFreq) { chan[i].nextFreq=nextFreq; chan[i].noteChanged.freq=1; } } + chan[i].noteChanged.offs=0; } if (chan[i].noteChanged.note) { // note value changed or frequency offset is changed if (chan[i].prevNote!=chan[i].nextNote) { @@ -580,17 +888,19 @@ void DivPlatformES5506::tick(bool sysTick) { chan[i].noteChanged.freq=1; } } + chan[i].noteChanged.note=0; } if (chan[i].noteChanged.freq) { if (chan[i].baseFreq!=chan[i].nextFreq) { chan[i].baseFreq=chan[i].nextFreq; chan[i].freqChanged=true; } + chan[i].noteChanged.freq=0; } - chan[i].noteChanged.changed=0; + chan[i].noteChanged.dummy=0; } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { - chan[i].freq=CLAMP_VAL(parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,chan[c].pcm.freqOffs),0,0x1ffff); + chan[i].freq=CLAMP_VAL(parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,chan[i].pcm.freqOffs),0,0x1ffff); if (chan[i].keyOn) { if (chan[i].pcm.index>=0 && chan[i].pcm.indexsong.sampleLen) { chan[i].k1Prev=0xffff; @@ -693,7 +1003,8 @@ int DivPlatformES5506::dispatch(DivCommand c) { sampleVaild=true; chan[c.chan].pcm.index=sample; chan[c.chan].pcm.pause=(chan[c.chan].std.alg.will)?(chan[c.chan].std.alg.val&1):false; - chan[c.chan].transWave.enable=ins->amiga.transWave.enable; + chan[c.chan].pcm.isNoteMap=ins->amiga.useNoteMap && !ins->amiga.transWave.enable; + chan[c.chan].transWave.enable=!ins->amiga.useNoteMap && ins->amiga.transWave.enable; chan[c.chan].transWave.sliceEnable=ins->amiga.transWave.sliceEnable; chan[c.chan].transWave.ind=ins->amiga.transWave.ind; DivSample* s=parent->getSample(sample); @@ -716,7 +1027,7 @@ int DivPlatformES5506::dispatch(DivCommand c) { if (ins->amiga.transWave.enable) { if (transWaveInd.loopMode!=DIV_SAMPLE_LOOPMODE_ONESHOT) { loopMode=transWaveInd.loopMode; - } else if (!s->isLoopable()) { // default + } else if ((chan[i].pcm.loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT) || (!s->isLoopable())) { // default loopMode=DIV_SAMPLE_LOOPMODE_PINGPONG; } // get loop position @@ -936,10 +1247,13 @@ int DivPlatformES5506::dispatch(DivCommand c) { case DIV_CMD_SAMPLE_POS: { if (chan[c.chan].useWave) break; if (chan[c.chan].active) { - const unsigned int pos=chan[c.chan].pcm.reversed?(chan[c.chan].pcm.length-c.value):c.value; - if ((chan[c.chan].pcm.reversed && pos>0) || ((!chan[c.chan].pcm.reversed) && pos0) || ((!chan[c.chan].pcm.reversed) && poscalcFreq((((chan[i].baseFreq*chan[i].waveLen)*(chanMax+1))/16),chan[i].pitch,false,0,chan[i].pitch2,chipClock,CHIP_FREQBASE); + // TODO: what is this mess? + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,0,chan[i].pitch2,chipClock,CHIP_FREQBASE); + chan[i].freq=(((chan[i].freq*chan[i].waveLen)*(chanMax+1))/16); if (chan[i].freq<0) chan[i].freq=0; if (chan[i].freq>0x3ffff) chan[i].freq=0x3ffff; if (chan[i].keyOn) { diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp index 949d104b8..e76d033f4 100644 --- a/src/engine/platform/opl.cpp +++ b/src/engine/platform/opl.cpp @@ -595,6 +595,7 @@ void DivPlatformOPL::tick(bool sysTick) { for (int i=0; icalcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq),chan[i].pitch2,chipClock,CHIP_FREQBASE); + if (chan[i].fixedFreq>0) chan[i].freq=chan[i].fixedFreq; if (chan[i].freq>131071) chan[i].freq=131071; int freqt=toFreq(chan[i].freq)+chan[i].pitch2; chan[i].freqH=freqt>>8; @@ -844,16 +845,18 @@ int DivPlatformOPL::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { if (c.chan>=melodicChans && chan[c.chan].state.opllPreset==16 && chan[c.chan].state.fixedDrums) { // drums if (c.chan==melodicChans) { - chan[c.chan].baseFreq=(chan[c.chan].state.kickFreq&1023)<<(chan[c.chan].state.kickFreq>>10); + chan[c.chan].fixedFreq=(chan[c.chan].state.kickFreq&1023)<<(chan[c.chan].state.kickFreq>>10); } else if (c.chan==melodicChans+1 || c.chan==melodicChans+4) { - chan[c.chan].baseFreq=(chan[c.chan].state.snareHatFreq&1023)<<(chan[c.chan].state.snareHatFreq>>10); + chan[c.chan].fixedFreq=(chan[c.chan].state.snareHatFreq&1023)<<(chan[c.chan].state.snareHatFreq>>10); } else if (c.chan==melodicChans+2 || c.chan==melodicChans+3) { - chan[c.chan].baseFreq=(chan[c.chan].state.tomTopFreq&1023)<<(chan[c.chan].state.tomTopFreq>>10); + chan[c.chan].fixedFreq=(chan[c.chan].state.tomTopFreq&1023)<<(chan[c.chan].state.tomTopFreq>>10); } else { chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].fixedFreq=0; } } else { chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].fixedFreq=0; } chan[c.chan].note=c.value; chan[c.chan].freqChanged=true; diff --git a/src/engine/platform/opl.h b/src/engine/platform/opl.h index a67d50380..61ea46481 100644 --- a/src/engine/platform/opl.h +++ b/src/engine/platform/opl.h @@ -40,7 +40,7 @@ class DivPlatformOPL: public DivDispatch { DivInstrumentFM state; DivMacroInt std; unsigned char freqH, freqL; - int freq, baseFreq, pitch, pitch2, note, ins, sample; + int freq, baseFreq, pitch, pitch2, note, ins, sample, fixedFreq; bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnacePCM, inPorta, fourOp, hardReset; int vol, outVol; unsigned char pan; @@ -58,6 +58,7 @@ class DivPlatformOPL: public DivDispatch { note(0), ins(-1), sample(-1), + fixedFreq(0), active(false), insChanged(true), freqChanged(false), diff --git a/src/engine/platform/opll.cpp b/src/engine/platform/opll.cpp index 50eab344f..027acb798 100644 --- a/src/engine/platform/opll.cpp +++ b/src/engine/platform/opll.cpp @@ -301,6 +301,7 @@ void DivPlatformOPLL::tick(bool sysTick) { for (int i=0; i<11; i++) { if (chan[i].freqChanged) { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq),chan[i].pitch2,chipClock,CHIP_FREQBASE); + if (chan[i].fixedFreq>0) chan[i].freq=chan[i].fixedFreq; if (chan[i].freq>262143) chan[i].freq=262143; int freqt=toFreq(chan[i].freq)+chan[i].pitch2; chan[i].freqL=freqt&0xff; @@ -420,15 +421,16 @@ int DivPlatformOPLL::dispatch(DivCommand c) { if (chan[c.chan].state.opllPreset==16 && chan[c.chan].state.fixedDrums) { switch (c.chan) { case 6: - chan[c.chan].baseFreq=(chan[c.chan].state.kickFreq&511)<<(chan[c.chan].state.kickFreq>>9); + chan[c.chan].fixedFreq=(chan[c.chan].state.kickFreq&511)<<(chan[c.chan].state.kickFreq>>9); break; case 7: case 10: - chan[c.chan].baseFreq=(chan[c.chan].state.snareHatFreq&511)<<(chan[c.chan].state.snareHatFreq>>9); + chan[c.chan].fixedFreq=(chan[c.chan].state.snareHatFreq&511)<<(chan[c.chan].state.snareHatFreq>>9); break; case 8: case 9: - chan[c.chan].baseFreq=(chan[c.chan].state.tomTopFreq&511)<<(chan[c.chan].state.tomTopFreq>>9); + chan[c.chan].fixedFreq=(chan[c.chan].state.tomTopFreq&511)<<(chan[c.chan].state.tomTopFreq>>9); break; default: + chan[c.chan].fixedFreq=0; chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); break; } diff --git a/src/engine/platform/opll.h b/src/engine/platform/opll.h index 7a06bbb77..d2d208269 100644 --- a/src/engine/platform/opll.h +++ b/src/engine/platform/opll.h @@ -33,7 +33,7 @@ class DivPlatformOPLL: public DivDispatch { DivInstrumentFM state; DivMacroInt std; unsigned char freqH, freqL; - int freq, baseFreq, pitch, pitch2, note, ins; + int freq, baseFreq, pitch, pitch2, note, ins, fixedFreq; bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta; int vol, outVol; unsigned char pan; @@ -50,6 +50,7 @@ class DivPlatformOPLL: public DivDispatch { pitch2(0), note(0), ins(-1), + fixedFreq(0), active(false), insChanged(true), freqChanged(false), diff --git a/src/engine/platform/pce.cpp b/src/engine/platform/pce.cpp index ba6136830..5013c2fe0 100644 --- a/src/engine/platform/pce.cpp +++ b/src/engine/platform/pce.cpp @@ -155,7 +155,7 @@ void DivPlatformPCE::tick(bool sysTick) { chan[i].std.next(); if (chan[i].std.vol.had) { chan[i].outVol=((chan[i].vol&31)*MIN(31,chan[i].std.vol.val))>>5; - if (chan[i].furnaceDac) { + if (chan[i].furnaceDac && chan[i].pcm) { // ignore for now } else { chWrite(i,0x04,0x80|chan[i].outVol); @@ -228,7 +228,7 @@ void DivPlatformPCE::tick(bool sysTick) { if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { //DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_PCE); chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true,0,chan[i].pitch2,chipClock,CHIP_DIVIDER); - if (chan[i].furnaceDac) { + if (chan[i].furnaceDac && chan[i].pcm) { double off=1.0; if (chan[i].dacSample>=0 && chan[i].dacSamplesong.sampleLen) { DivSample* s=parent->getSample(chan[i].dacSample); @@ -268,8 +268,9 @@ int DivPlatformPCE::dispatch(DivCommand c) { chan[c.chan].pcm=false; } if (chan[c.chan].pcm) { - if (skipRegisterWrites) break; if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].furnaceDac=true; + if (skipRegisterWrites) break; chan[c.chan].dacSample=ins->amiga.getSample(c.value); if (chan[c.chan].dacSample<0 || chan[c.chan].dacSample>=parent->song.sampleLen) { chan[c.chan].dacSample=-1; @@ -291,8 +292,9 @@ int DivPlatformPCE::dispatch(DivCommand c) { chan[c.chan].active=true; chan[c.chan].macroInit(ins); //chan[c.chan].keyOn=true; - chan[c.chan].furnaceDac=true; } else { + chan[c.chan].furnaceDac=false; + if (skipRegisterWrites) break; if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; } @@ -311,7 +313,6 @@ int DivPlatformPCE::dispatch(DivCommand c) { chWrite(c.chan,0x04,0xdf); addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dacRate); } - chan[c.chan].furnaceDac=false; } break; } diff --git a/src/engine/platform/segapcm.cpp b/src/engine/platform/segapcm.cpp index e811175f5..75329f172 100644 --- a/src/engine/platform/segapcm.cpp +++ b/src/engine/platform/segapcm.cpp @@ -181,11 +181,13 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { chan[c.chan].macroInit(ins); if (dumpWrites) { // Sega PCM writes DivSample* s=parent->getSample(chan[c.chan].pcm.sample); + int actualLength=(int)(s->isLoopable()?s->loopEnd:s->length8); + if (actualLength>0xfeff) actualLength=0xfeff; addWrite(0x10086+(c.chan<<3),3+((s->offSegaPCM>>16)<<3)); addWrite(0x10084+(c.chan<<3),(s->offSegaPCM)&0xff); addWrite(0x10085+(c.chan<<3),(s->offSegaPCM>>8)&0xff); - addWrite(0x10006+(c.chan<<3),MIN(255,((s->offSegaPCM&0xffff)+s->length8-1)>>8)); - if (!s->isLoopable()) { + addWrite(0x10006+(c.chan<<3),MIN(255,((s->offSegaPCM&0xffff)+actualLength-1)>>8)); + if ((s->loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT) || (s->loopStart<0 || s->loopStart>=actualLength || s->loopEnd<=s->loopStart || s->loopEnd>=actualLength)) { addWrite(0x10086+(c.chan<<3),2+((s->offSegaPCM>>16)<<3)); } else { int loopPos=(s->offSegaPCM&0xffff)+s->loopStart+s->loopOffP; @@ -212,11 +214,13 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) { chan[c.chan].furnacePCM=false; if (dumpWrites) { // Sega PCM writes DivSample* s=parent->getSample(chan[c.chan].pcm.sample); + int actualLength=(int)(s->isLoopable()?s->loopEnd:s->length8); + if (actualLength>65536) actualLength=65536; addWrite(0x10086+(c.chan<<3),3+((s->offSegaPCM>>16)<<3)); addWrite(0x10084+(c.chan<<3),(s->offSegaPCM)&0xff); addWrite(0x10085+(c.chan<<3),(s->offSegaPCM>>8)&0xff); - addWrite(0x10006+(c.chan<<3),MIN(255,((s->offSegaPCM&0xffff)+s->length8-1)>>8)); - if (!s->isLoopable()) { + addWrite(0x10006+(c.chan<<3),MIN(255,((s->offSegaPCM&0xffff)+actualLength-1)>>8)); + if ((s->loopMode==DIV_SAMPLE_LOOPMODE_ONESHOT) || (s->loopStart<0 || s->loopStart>=actualLength || s->loopEnd<=s->loopStart || s->loopEnd>=actualLength)) { addWrite(0x10086+(c.chan<<3),2+((s->offSegaPCM>>16)<<3)); } else { int loopPos=(s->offSegaPCM&0xffff)+s->loopStart+s->loopOffP; diff --git a/src/engine/platform/sound/ay8910.cpp b/src/engine/platform/sound/ay8910.cpp index 3dddf82c5..867b5d214 100644 --- a/src/engine/platform/sound/ay8910.cpp +++ b/src/engine/platform/sound/ay8910.cpp @@ -1061,7 +1061,7 @@ void ay8910_device::sound_stream_update(short** outputs, int outLen) for (int chan = 0; chan < NUM_CHANNELS; chan++) { tone = &m_tone[chan]; - const int period = tone->period * (m_step_mul << 1); + const int period = std::max(1, tone->period) * (m_step_mul << 1); tone->count += is_expanded_mode() ? 32 : ((m_feature & PSG_HAS_EXPANDED_MODE) ? 1 : 2); while (tone->count >= period) { @@ -1115,7 +1115,7 @@ void ay8910_device::sound_stream_update(short** outputs, int outLen) envelope = &m_envelope[chan]; if (envelope->holding == 0) { - const int period = envelope->period * m_env_step_mul; + const int period = std::max(1, envelope->period) * m_env_step_mul; if ((++envelope->count) >= period) { envelope->count = 0; diff --git a/src/engine/platform/su.cpp b/src/engine/platform/su.cpp index 724fb5901..dc9a185a6 100644 --- a/src/engine/platform/su.cpp +++ b/src/engine/platform/su.cpp @@ -207,13 +207,13 @@ void DivPlatformSoundUnit::tick(bool sysTick) { DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_SU); DivSample* sample=parent->getSample(ins->amiga.getSample(chan[i].note)); if (sample!=NULL) { - unsigned int sampleEnd=sample->offSU+sample->samples; + unsigned int sampleEnd=sample->offSU+(s->isLoopable()?sample->loopEnd:sample->samples); if (sampleEnd>=getSampleMemCapacity(0)) sampleEnd=getSampleMemCapacity(0)-1; chWrite(i,0x0a,sample->offSU&0xff); chWrite(i,0x0b,sample->offSU>>8); chWrite(i,0x0c,sampleEnd&0xff); chWrite(i,0x0d,sampleEnd>>8); - if (sample->loopStart>=0 && sample->loopStart<(int)sample->samples) { + if (s->isLoopable()) { unsigned int sampleLoop=sample->offSU+sample->loopStart; if (sampleLoop>=getSampleMemCapacity(0)) sampleLoop=getSampleMemCapacity(0)-1; chWrite(i,0x0e,sampleLoop&0xff); diff --git a/src/engine/platform/ym2203.cpp b/src/engine/platform/ym2203.cpp index 10fde1f29..057c98b76 100644 --- a/src/engine/platform/ym2203.cpp +++ b/src/engine/platform/ym2203.cpp @@ -1057,19 +1057,19 @@ void DivPlatformYM2203::setSkipRegisterWrites(bool value) { } void DivPlatformYM2203::setFlags(unsigned int flags) { - unsigned char ayFlags=32; + unsigned char ayFlags=16; if (flags==3) { chipClock=3000000.0; - ayFlags=36; + ayFlags=20; } else if (flags==2) { chipClock=4000000.0; - ayFlags=35; + ayFlags=19; } else if (flags==1) { chipClock=COLOR_PAL*4.0/5.0; - ayFlags=33; + ayFlags=17; } else { chipClock=COLOR_NTSC; - ayFlags=32; + ayFlags=16; } ay->setFlags(ayFlags); rate=fm->sample_rate(chipClock); @@ -1090,7 +1090,7 @@ int DivPlatformYM2203::init(DivEngine* p, int channels, int sugRate, unsigned in fm->set_fidelity(ymfm::OPN_FIDELITY_MIN); // YM2149, 2MHz ay=new DivPlatformAY8910; - ay->init(p,3,sugRate,35); + ay->init(p,3,sugRate,19); ay->toggleRegisterDump(true); setFlags(flags); diff --git a/src/engine/platform/ym2203ext.cpp b/src/engine/platform/ym2203ext.cpp index 9ca77e9fc..74bf9bac1 100644 --- a/src/engine/platform/ym2203ext.cpp +++ b/src/engine/platform/ym2203ext.cpp @@ -21,7 +21,7 @@ #include "../engine.h" #include -#include "ym2610shared.h" +#include "ym2203shared.h" #include "fmshared_OPN.h" int DivPlatformYM2203Ext::dispatch(DivCommand c) { @@ -489,9 +489,6 @@ void DivPlatformYM2203Ext::forceIns() { chan[i].freqChanged=true; } } - for (int i=3; i<6; i++) { - chan[i].insChanged=true; - } ay->forceIns(); ay->flushWrites(); diff --git a/src/engine/platform/ym2608.cpp b/src/engine/platform/ym2608.cpp index 65d36a6e6..2f7853492 100644 --- a/src/engine/platform/ym2608.cpp +++ b/src/engine/platform/ym2608.cpp @@ -1459,7 +1459,7 @@ int DivPlatformYM2608::init(DivEngine* p, int channels, int sugRate, unsigned in } // YM2149, 2MHz ay=new DivPlatformAY8910; - ay->init(p,3,sugRate,35); + ay->init(p,3,sugRate,19); ay->toggleRegisterDump(true); reset(); return 16; diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index 850a84232..43857820d 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -1454,7 +1454,7 @@ int DivPlatformYM2610::init(DivEngine* p, int channels, int sugRate, unsigned in fm=new ymfm::ym2610(iface); // YM2149, 2MHz ay=new DivPlatformAY8910; - ay->init(p,3,sugRate,35); + ay->init(p,3,sugRate,19); ay->toggleRegisterDump(true); reset(); return 14; diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp index 04c012c39..79af43394 100644 --- a/src/engine/platform/ym2610b.cpp +++ b/src/engine/platform/ym2610b.cpp @@ -1432,7 +1432,7 @@ int DivPlatformYM2610B::init(DivEngine* p, int channels, int sugRate, unsigned i fm=new ymfm::ym2610b(iface); // YM2149, 2MHz ay=new DivPlatformAY8910; - ay->init(p,3,sugRate,35); + ay->init(p,3,sugRate,19); ay->toggleRegisterDump(true); reset(); return 16; diff --git a/src/engine/platform/zxbeeper.cpp b/src/engine/platform/zxbeeper.cpp index 145cfe5b5..5fc3942fc 100644 --- a/src/engine/platform/zxbeeper.cpp +++ b/src/engine/platform/zxbeeper.cpp @@ -29,20 +29,11 @@ const char** DivPlatformZXBeeper::getRegisterSheet() { const char* DivPlatformZXBeeper::getEffectName(unsigned char effect) { switch (effect) { - case 0x10: - return "10xx: Change waveform"; - break; - case 0x11: - return "11xx: Toggle noise mode"; - break; case 0x12: - return "12xx: Setup LFO (0: disabled; 1: 1x depth; 2: 16x depth; 3: 256x depth)"; - break; - case 0x13: - return "13xx: Set LFO speed"; + return "12xx: Set pulse width"; break; case 0x17: - return "17xx: Toggle PCM mode"; + return "17xx: Trigger overlay drum"; break; } return NULL; diff --git a/src/engine/safeReader.cpp b/src/engine/safeReader.cpp index 36d6097d6..173ffd235 100644 --- a/src/engine/safeReader.cpp +++ b/src/engine/safeReader.cpp @@ -170,8 +170,8 @@ String SafeReader::readStringLine() { unsigned char c; if (isEOF()) throw EndOfFileException(this, len); - while (!isEOF() && (c = readC()) != 0) { - if (c=='\r'||c=='\n') { + while (!isEOF() && (c=readC())!=0) { + if (c=='\r' || c=='\n') { break; } ret.push_back(c); @@ -179,17 +179,17 @@ String SafeReader::readStringLine() { return ret; } -String SafeReader::readStringToken(unsigned char delim) { +String SafeReader::readStringToken(unsigned char delim, bool stripContiguous) { String ret; unsigned char c; if (isEOF()) throw EndOfFileException(this, len); while (!isEOF() && (c=readC())!=0) { - if (c == '\r' || c == '\n') { + if (c=='\r' || c=='\n') { break; } - if (c == delim) { - if (ret.length() == 0) { + if (c==delim) { + if (ret.length()==0 && stripContiguous) { continue; } break; @@ -200,5 +200,6 @@ String SafeReader::readStringToken(unsigned char delim) { } String SafeReader::readStringToken() { - return readStringToken(' '); + // This will strip LHS whitespace and only return contents after it. + return readStringToken(' ', true); } diff --git a/src/engine/safeReader.h b/src/engine/safeReader.h index e21311f98..1f7149834 100644 --- a/src/engine/safeReader.h +++ b/src/engine/safeReader.h @@ -67,7 +67,7 @@ class SafeReader { String readString(); String readString(size_t len); String readStringLine(); - String readStringToken(unsigned char delim); + String readStringToken(unsigned char delim, bool stripContiguous); String readStringToken(); inline bool isEOF() { return curSeek >= len; }; diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 373cf1ed5..fa9f0da2c 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -443,6 +443,13 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC(0x04); w->writeC(0x00); break; + case DIV_SYSTEM_SCC: + case DIV_SYSTEM_SCC_PLUS: + w->writeC(0xd2); + w->writeC(baseAddr2|3); + w->writeC(0); + w->writeC(0); + break; default: break; } @@ -1395,16 +1402,17 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { size_t memPos=0; for (int i=0; ilength8)&0xff0000)) { + unsigned int alignedSize=(sample->length8+0xff)&(~0xff); + if (alignedSize>65536) alignedSize=65536; + if ((memPos&0xff0000)!=((memPos+alignedSize)&0xff0000)) { memPos=(memPos+0xffff)&0xff0000; } + logV("- sample %d will be at %x with length %x",i,memPos,alignedSize); if (memPos>=16777216) break; sample->offSegaPCM=memPos; - unsigned int alignedSize=(sample->length8+0xff)&(~0xff); unsigned int readPos=0; - if (alignedSize>65536) alignedSize=65536; for (unsigned int j=0; jloopMode && readPos>=sample->loopEnd) || readPos>=sample->length8) { + if (((sample->loopMode != DIV_SAMPLE_LOOPMODE_ONESHOT) && readPos>=sample->loopEnd) || readPos>=sample->length8) { if (sample->isLoopable()) { readPos=sample->loopStart; pcmMem[memPos++]=((unsigned char)sample->data8[readPos]+0x80); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index d2d8a9e10..c33ced04c 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1221,9 +1221,9 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); hasOpened=fileDialog->openLoad( "Open File", - {"compatible files", "*.fur *.dmf *.mod *.ftm", + {"compatible files", "*.fur *.dmf *.mod", "all files", ".*"}, - "compatible files{.fur,.dmf,.mod,.ftm},.*", + "compatible files{.fur,.dmf,.mod},.*", workingDirSong, dpiScale ); @@ -1261,9 +1261,25 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); hasOpened=fileDialog->openLoad( "Load Instrument", - {"compatible files", "*.fui *.dmp *.tfi *.vgi *.s3i *.sbi *.opli *.opni *.y12 *.bnk *.ff *.opm", + // TODO supply loadable formats in a dynamic, scalable, "DRY" way. + {"all compatible files", "*.fui *.dmp *.tfi *.vgi *.s3i *.sbi *.opli *.opni *.y12 *.bnk *.ff *.gyb *.opm *.wopl *.wopn", + "Furnace instrument", "*.fui", + "DefleMask preset", "*.dmp", + "TFM Music Maker instrument", "*.tfi", + "VGM Music Maker instrument", "*.vgi", + "Scream Tracker 3 instrument", "*.s3i", + "SoundBlaster instrument", "*.sbi", + "Wohlstand OPL instrument", "*.opli", + "Wohlstand OPN instrument", "*.opni", + "Gens KMod patch dump", "*.y12", + "BNK file (AdLib)", "*.bnk", + "FF preset bank", "*.ff", + "2612edit GYB preset bank", "*.gyb", + "VOPM preset bank", "*.opm", + "Wohlstand WOPL bank", "*.wopl", + "Wohlstand WOPN bank", "*.wopn", "all files", ".*"}, - "compatible files{.fui,.dmp,.tfi,.vgi,.s3i,.sbi,.opli,.opni,.y12,.bnk,.ff,.opm},.*", + "all compatible files{.fui,.dmp,.tfi,.vgi,.s3i,.sbi,.opli,.opni,.y12,.bnk,.ff,.gyb,.opm,.wopl,.wopn},.*", workingDirIns, dpiScale, [this](const char* path) { @@ -3476,10 +3492,34 @@ bool FurnaceGUI::loop() { } break; case GUI_WARN_CLEAR: - if (ImGui::Button("Song (orders and patterns)")) { + if (ImGui::Button("All subsongs")) { + stop(); + e->clearSubSongs(); + curOrder=0; + oldOrder=0; + oldOrder1=0; + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Current subsong")) { stop(); e->lockEngine([this]() { - e->song.clearSongData(); + e->curSubSong->clearData(); + }); + e->setOrder(0); + curOrder=0; + oldOrder=0; + oldOrder1=0; + MARK_MODIFIED; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Orders")) { + stop(); + e->lockEngine([this]() { + memset(e->curOrders->ord,0,DIV_MAX_CHANS*256); + e->curSubSong->ordersLen=1; }); e->setOrder(0); curOrder=0; @@ -3550,7 +3590,7 @@ bool FurnaceGUI::loop() { // backup trigger if (modified) { if (backupTimer>0) { - backupTimer-=ImGui::GetIO().DeltaTime; + backupTimer=(backupTimer-ImGui::GetIO().DeltaTime); if (backupTimer<=0) { backupTask=std::async(std::launch::async,[this]() -> bool { if (backupPath==curFileName) { diff --git a/src/gui/gui.h b/src/gui/gui.h index 2abe53654..7b1d7cef4 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -801,7 +801,7 @@ class FurnaceGUI { double aboutScroll, aboutSin; float aboutHue; - double backupTimer; + std::atomic backupTimer; std::future backupTask; std::mutex backupLock; String backupPath; diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 7ef745ddb..ed7ad19ab 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -2790,6 +2790,11 @@ void FurnaceGUI::drawInsEdit() { } ImGui::EndTable(); } + if (ImGui::BeginTabItem("Transwave Macros")) { + macroList.push_back(FurnaceGUIMacroDesc("Transwave control",&ins->std.fbMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,false,NULL,NULL,true,transwaveControlModes)); + macroList.push_back(FurnaceGUIMacroDesc("Transwave slice",&ins->std.fmsMacro,0,4095,160,uiColors[GUI_COLOR_MACRO_OTHER])); + ImGui::EndTabItem(); + } } ImGui::EndDisabled(); ImGui::EndTabItem(); @@ -3438,8 +3443,6 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Envelope K2 ramp",&ins->std.ex7Macro,-128,127,160,uiColors[GUI_COLOR_MACRO_OTHER])); macroList.push_back(FurnaceGUIMacroDesc("Envelope mode",&ins->std.ex8Macro,0,2,64,uiColors[GUI_COLOR_MACRO_OTHER],false,false,NULL,NULL,true,es5506EnvelopeModes)); macroList.push_back(FurnaceGUIMacroDesc("Control",&ins->std.algMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,false,NULL,NULL,true,es5506ControlModes)); - macroList.push_back(FurnaceGUIMacroDesc("Transwave control",&ins->std.fbMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,false,NULL,NULL,true,transwaveControlModes)); - macroList.push_back(FurnaceGUIMacroDesc("Transwave slice",&ins->std.fmsMacro,0,4095,160,uiColors[GUI_COLOR_MACRO_OTHER])); } if (ins->type==DIV_INS_SU) { macroList.push_back(FurnaceGUIMacroDesc("Control",&ins->std.ex3Macro,0,4,64,uiColors[GUI_COLOR_MACRO_OTHER],false,false,NULL,NULL,true,suControlBits));