diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 60746d3c3..000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,48 +0,0 @@ -# EXTREMELY IMPORTANT NOTICE - PLEASE **READ**!!!!!!! - -BY SUBMITTING AN ISSUE, YOU HEREBY AGREE TO COMPLY WITH THESE TERMS. -FAILURE TO DO SO MAY RESULT IN YOUR ISSUE BEING DECLARED VOID. - -**ADDITIONALLY, FAILURE TO COMPLY WITH POINTS 1 AND 2 WILL RESULT IN THE INABILITY TO ISSUE FURTHER ISSUE REPORTS.** - -1. this section is exclusively for ISSUES related to Furnace (bugs, major annoyances and others). ONLY THINGS THAT COUNT AS **ISSUES** (ad pedem litterae). -2. **THIS SECTION IS NOT FOR SUGGESTIONS, REQUESTS, QUESTIONS, SHOWCASE OR ANY OTHER DISCUSSIONS THAT DO NOT MEET THE CRITERIA AND DEFINITION OF AN __ISSUE__.** - - see the Discussions section if you wish to submit these. -3. check whether your issue has been reported already. - - go to the Issues section, and use the search bar that appears on top of the Issues list. -4. include the following information: - - version of Furnace (help > about) - - operating system (and version) - - whether you have downloaded Furnace, or built it from source. -5. provide these details if you believe the issue is operating system and/or computer-specific: - - CPU model - - Windows: go to Control Panel > System. - - macOS: go to the Apple menu and select About This Mac... - - Linux: use `lscpu` or `cat /proc/cpuinfo`. - - graphics card (and driver version) - - Windows: open `dxdiag` and observe the Render tab. - - macOS: go to the Apple menu and select About This Mac... - - this information is not always shown. - - this information is not necessary if you use an Apple silicon Mac. - - Linux: use `glxinfo | grep OpenGL`. -6. if your issue is an abnormal program termination (a "Crash"), you must provide additional details: - - the furnace_crash.txt file that is created by Furnace after a Crash. this file is located in the following paths: - - Windows: `C:\Users\\furnace_crash.txt` - - Linux/other: `/tmp/furnace_crash.txt` - - on macOS this file is not generated. you may retrieve information about the Crash by clicking on "Report..." or "Show Details" in the "quit unexpectedly" dialog that appears following the Crash. - - make sure to remove any personal information for privacy reasons. - - be sure to select "Don't Send" afterwards. - - the furnace.log file located in: - - Windows: `C:\Users\\AppData\Roaming\furnace\furnace.log` - - macOS: `~/Library/Application Support/furnace/furnace.log` - - Linux: `~/.config/furnace/furnace.log` - - make sure to remove any personal information for privacy reasons. - -BY SUBMITTING AN ISSUE, YOU HEREBY AGREE TO COMPLY WITH THESE TERMS. -FAILURE TO DO SO MAY RESULT IN YOUR ISSUE BEING DECLARED VOID. - -**ADDITIONALLY, FAILURE TO COMPLY WITH POINTS 1 AND 2 WILL RESULT IN THE INABILITY TO ISSUE FURTHER ISSUE REPORTS.** - -***END OF NOTICE*** -PLEASE REMOVE THIS NOTICE AFTER READING. -FAILURE TO REMOVE THIS NOTICE IS NEGLIGENCE. diff --git a/.github/issue_template/config.yml b/.github/issue_template/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/.github/issue_template/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/issue_template/issue.yml b/.github/issue_template/issue.yml new file mode 100644 index 000000000..6cfc6ba6b --- /dev/null +++ b/.github/issue_template/issue.yml @@ -0,0 +1,33 @@ +name: Issue Report +description: for issues (bugs, annoyances, crashes and similar). +body: + - type: markdown + attributes: + value: | + Suggestions, feature requests, questions, showcase or anything else? Go to the Discussions section. + - type: textarea + id: description + attributes: + label: Description + - type: textarea + id: steps + attributes: + label: Steps to Reproduce (if applicable) + - type: textarea + id: info + attributes: + label: Additional Information + - type: input + id: version + attributes: + label: Furnace version? (help > about) + validations: + required: true + - type: checkboxes + id: terms + attributes: + label: This is an issue + description: By submitting this issue, you affirm that this is an actual issue (not a suggestion, question or otherwise non-issue). + options: + - label: I hereby certify that the ticket I am submitting is an issue. + required: true diff --git a/TODO.md b/TODO.md index f8a0e67de..96f349a9b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ # to-do for 0.6.3 - 9xxx for more chips -- user presets # to-do long term diff --git a/demos/nes/infinity.fur b/demos/nes/infinity.fur new file mode 100644 index 000000000..24242d330 Binary files /dev/null and b/demos/nes/infinity.fur differ diff --git a/doc/2-interface/settings.md b/doc/2-interface/settings.md index f53d5aa2b..ec61e9371 100644 --- a/doc/2-interface/settings.md +++ b/doc/2-interface/settings.md @@ -15,6 +15,7 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o - DirectX 11: works with the majority of graphics chips/cards and is optimized specifically for Windows. - SDL Renderer: this was the only available render backend prior to the addition of dedicated OpenGL/DirectX backends in 0.6. default on macOS. - it is slower than the other backends. + - Software: this is a last resort backend which renders the interface in software. very slow! - **Render driver**: this setting appears when using the SDL Renderer backend. it allows you to select an SDL render driver. - **VSync**: synchronizes rendering to VBlank and eliminates tearing. - **Frame rate limit**: allows you to set a frame rate limit (in frames per second). diff --git a/doc/7-systems/5e01.md b/doc/7-systems/5e01.md new file mode 100644 index 000000000..a7d650c98 --- /dev/null +++ b/doc/7-systems/5e01.md @@ -0,0 +1,62 @@ +# 5E01 + +a fantasy sound chip created by Euly, based on the Ricoh 2A03, with some improvements: + +- a 37.5% duty cycle, +- 32 noise pitches instead of 16, and +- triangle channel becomes a wave channel, with four available waveforms: triangle, saw, sine and square. + +## effects + +- `11xx`: **write to delta modulation counter.** range is `00` to `7F`. + - this may be used to attenuate the triangle and noise channels; at `7F`, they will be at about 57% volume. + - will not work if a sample is playing. +- `12xx`: **set duty cycle/noise mode/waveform of channel.** + - may be `0` to `3` for the pulse channels: + - `0`: 12.5% + - `1`: 25% + - `2`: 37.5% + - `3`: 50% + - may be `0` or `1` for the noise channel: + - `0`: long (15-bit LFSR, 32767-step) + - `1`: short (9-bit LFSR, 93-step) + - may be `0` to `3` for the wave channel: + - `0`: triangle + - `1`: saw + - `2`: square + - `3`: sine +- `13xy`: **setup sweep up.** + - `x` is the time. + - `y` is the shift. + - set to `0` to disable it. +- `14xy`: **setup sweep down.** + - `x` is the time. + - `y` is the shift. + - set to `0` to disable it. +- `15xx`: **set envelope mode.** + - `0`: envelope + length counter. volume represents envelope duration. + - `1`: length counter. volume represents output volume. + - `2`: looping envelope. volume represents envelope duration. + - `3`: constant volume. default value. volume represents output volume. + - pulse and noise channels only. + - you may need to apply a phase reset (using the macro) to make the envelope effective. +- `16xx`: **set length counter.** + - see [NES](nes.md) for more information. + - this will trigger phase reset. +- `17xx`: **set frame counter mode.** + - `0`: 4-step. + - `1`: 5-step. +- `18xx`: **set PCM channel mode.** + - `00`: PCM (software). + - `01`: DPCM (hardware). + - when in DPCM mode, samples will sound muffled (due to its nature), availables pitches are limited, and loop point is ignored. +- `19xx`: **set triangle linear counter.** + - `00` to `7F` set the counter. + - `80` and higher halt it. +- `20xx`: **set DPCM frequency.** + - only works in DPCM mode. + + +## info + +this chip uses the [NES](../4-instrument/nes.md) instrument editor. diff --git a/doc/7-systems/nes.md b/doc/7-systems/nes.md index cfd1823a6..3130a9f61 100644 --- a/doc/7-systems/nes.md +++ b/doc/7-systems/nes.md @@ -2,7 +2,9 @@ the console from Nintendo that plays Super Mario Bros. and helped revive the agonizing video game market in the US during mid-80s. -also known as Famicom. it is a five-channel sound generator: first two channels play pulse wave with three different duty cycles, third is a fixed-volume triangle channel, fourth is a noise channel (can work in both pseudo-random and periodic modes) and fifth is a (D)PCM sample channel. +also known as Family Computer (Famicom), especially in Japan. + +the console is powered by the Ricoh 2A03, a CPU with sound generator built-in. it has five channels: first two channels play pulse wave with three different duty cycles, third is a fixed-volume triangle channel, fourth is a noise channel (can work in both pseudo-random and periodic modes) and fifth is a (D)PCM sample channel. ## effects diff --git a/doc/9-guides/emulation-cores.md b/doc/9-guides/emulation-cores.md index 2624f6ddc..d42dc459e 100644 --- a/doc/9-guides/emulation-cores.md +++ b/doc/9-guides/emulation-cores.md @@ -47,3 +47,10 @@ Furnace achieves the authentic sound of videogame hardware by emulating sound ch - **YMF262-LLE**: a new core written by the author of the Nuked cores. it features extremely accurate emulation. - this core uses even more CPU than YM3812-LLE. not suitable for playback or even rendering if you're impatient! +- **ESFM core**: + - **ESFMu**: the ESFM emulator. best choice but CPU intensive. + - **ESFMu (fast)**: this is a modification of ESFMu to reduce CPU usage at the cost of less accuracy. + +- **OPLL core**: + - **Nuked-OPLL**: this core is accurate and the default. + - **emu2413**: a less accurate core that uses less CPU. diff --git a/src/engine/fileOps/ftm.cpp b/src/engine/fileOps/ftm.cpp index e687e73f0..9b3b7d55b 100644 --- a/src/engine/fileOps/ftm.cpp +++ b/src/engine/fileOps/ftm.cpp @@ -23,7 +23,6 @@ // portions apparently taken from FamiTracker source, which is under GPLv2+ // TODO: -// - audit for CVEs // - format code? #include "fileOpsCommon.h" @@ -258,6 +257,7 @@ const int eff_conversion_050[][2] = { }; constexpr int ftEffectMapSize = sizeof(ftEffectMap) / sizeof(int); +constexpr int eftEffectMapSize = sizeof(eftEffectMap) / sizeof(int); int convertMacros2A03[5] = {(int)DIV_MACRO_VOL, (int)DIV_MACRO_ARP, (int)DIV_MACRO_PITCH, -1, (int)DIV_MACRO_DUTY}; int convertMacrosVRC6[5] = {(int)DIV_MACRO_VOL, (int)DIV_MACRO_ARP, (int)DIV_MACRO_PITCH, -1, (int)DIV_MACRO_DUTY}; @@ -434,7 +434,8 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si bool hasSequence[256][8]; unsigned char sequenceIndex[256][8]; unsigned char macro_types[256][8]; - std::vector> macros; + std::vector macros[256]; + std::vector encounteredBlocks; unsigned char map_channels[DIV_MAX_CHANS]; unsigned int hilightA = 4; unsigned int hilightB = 16; @@ -457,13 +458,9 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si memset(map_channels, 0xfe, DIV_MAX_CHANS * sizeof(unsigned char)); for (int i = 0; i < 256; i++) { - std::vector mac; - for (int j = 0; j < 8; j++) { - mac.push_back(DivInstrumentMacro(DIV_MACRO_VOL)); + macros[i].push_back(DivInstrumentMacro(DIV_MACRO_VOL)); } - - macros.push_back(mac); } if (!reader.seek((dnft && dnft_sig) ? 21 : 18, SEEK_SET)) { @@ -482,7 +479,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si return false; } - for (DivSubSong* i : ds.subsong) { + for (DivSubSong* i: ds.subsong) { i->clearData(); delete i; } @@ -501,13 +498,30 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } // not the end - reader.seek(-3, SEEK_CUR); + if (!reader.seek(-3, SEEK_CUR)) { + logE("couldn't seek back by 3!"); + lastError = "couldn't seek back by 3"; + delete[] file; + return false; + } blockName = reader.readString(16); unsigned int blockVersion = (unsigned int)reader.readI(); unsigned int blockSize = (unsigned int)reader.readI(); size_t blockStart = reader.tell(); logD("reading block %s (version %d, %d bytes, position %x)", blockName, blockVersion, blockSize, reader.tell()); + + for (String& i: encounteredBlocks) { + if (blockName==i) { + logE("duplicate block %s!",blockName); + lastError = "duplicate block "+blockName; + ds.unload(); + delete[] file; + return false; + } + } + encounteredBlocks.push_back(blockName); + if (blockName == "PARAMS") { // versions 7-9 don't change anything? CHECK_BLOCK_VERSION(9); @@ -529,6 +543,13 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si tchans = reader.readI(); + if (tchans<0 || tchans>=DIV_MAX_CHANS) { + logE("invalid channel count! %d",tchans); + lastError = "invalid channel count"; + delete[] file; + return false; + } + if (tchans == 5) { expansions = 0; // This is strange. Sometimes expansion chip is set to 0xFF in files } @@ -538,12 +559,15 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if (blockVersion >= 7) { // advanced Hz control int controlType = reader.readI(); - switch (controlType) { + int readHz=reader.readI(); + if (readHz<=0) { + customHz=60.0; + } else switch (controlType) { case 1: - customHz = 1000000.0 / (double)reader.readI(); + customHz = 1000000.0 / (double)readHz; break; default: - reader.readI(); + logW("unsupported tick rate control type %d",controlType); break; } } else { @@ -553,6 +577,10 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si customHz = reader.readI(); } + logV("before clamp: %f",customHz); + + if (customHz>1000.0) customHz=1000.0; + unsigned int newVibrato = 0; bool sweepReset = false; unsigned int speedSplitPoint = 0; @@ -576,6 +604,12 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if ((expansions & 16) && blockVersion >= 5) { // N163 channels n163Chans = reader.readI(); + if (n163Chans<1 || n163Chans>=9) { + logE("invalid Namco 163 channel count! %d",n163Chans); + lastError = "invalid Namco 163 channel count"; + delete[] file; + return false; + } } if (blockVersion >= 6) { speedSplitPoint = reader.readI(); @@ -779,6 +813,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } } if (calcChans != tchans) { + // TODO: would ignore trigger CVE? too bad if so! if (!eft || (eft && (expansions & 8) == 0)) // ignore since I have no idea how to tell apart E-FT versions which do or do not have PCM chan. Yes, this may lead to all the righer channels to be shifted but at least you still get note data! { logE("channel counts do not match! %d != %d", tchans, calcChans); @@ -788,17 +823,20 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } } if (tchans > DIV_MAX_CHANS) { - tchans = DIV_MAX_CHANS; - logW("too many channels!"); + logE("too many channels!"); + lastError = "too many channels"; + delete[] file; + return false; } if (blockVersion == 9 && blockSize - (reader.tell() - blockStart) == 2) // weird { - reader.seek(2, SEEK_CUR); + if (!reader.seek(2, SEEK_CUR)) { + logE("could not weird-seek by 2!"); + lastError = "could not weird-seek by 2"; + delete[] file; + return false; + } } - if (eft) { - // reader.seek(8,SEEK_CUR); - } - } else if (blockName == "INFO") { CHECK_BLOCK_VERSION(1); ds.name = reader.readString(32); @@ -831,6 +869,13 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si for (int j = 0; j <= totalSongs; j++) { unsigned char effectCols = reader.readC(); + if (effectCols>7) { + logE("too many effect columns!"); + lastError = "too many effect columns"; + delete[] file; + return false; + } + if (map_channels[i] == 0xfe) { ds.subsong[j]->pat[i].effectCols = 1; logV("- song %d has %d effect columns", j, effectCols); @@ -850,8 +895,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } else if (blockName == "INSTRUMENTS") { CHECK_BLOCK_VERSION(9); - // reader.seek(blockSize,SEEK_CUR); - ds.insLen = reader.readI(); if (ds.insLen < 0 || ds.insLen > 256) { logE("too many instruments/out of range!"); @@ -870,10 +913,10 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si for (int i = 0; i < ds.insLen; i++) { unsigned int insIndex = reader.readI(); if (insIndex >= ds.ins.size()) { - // logE("instrument index %d is out of range!",insIndex); - // lastError="instrument index out of range"; - // delete[] file; - // return false; + logE("instrument index %d is out of range!",insIndex); + lastError="instrument index out of range"; + delete[] file; + return false; } DivInstrument* ins = ds.ins[insIndex]; @@ -937,6 +980,12 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if (blockVersion >= 7) { note = reader.readC(); } + if (note<0 || note>=120) { + logE("DPCM note %d out of range!",note); + lastError = "DPCM note out of range"; + delete[] file; + return false; + } ins->amiga.noteMap[note].map = (short)((unsigned char)reader.readC()) - 1; unsigned char freq = reader.readC(); ins->amiga.noteMap[note].dpcmFreq = (freq & 15); // 0-15 = 0-15 unlooped, 128-143 = 0-15 looped @@ -981,8 +1030,9 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } case DIV_INS_OPLL: { ins->fm.opllPreset = (unsigned int)reader.readI(); + ins->fm.opllPreset&=15; - unsigned char custom_patch[8] = {0}; + unsigned char custom_patch[8]; for (int i = 0; i < 8; i++) { custom_patch[i] = reader.readC(); @@ -1020,24 +1070,35 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si for (int j = 0; j < 64; j++) { wave->data[j] = reader.readC(); } - ins->std.waveMacro.len = 1; - ins->std.waveMacro.val[0] = ds.wave.size(); for (int j = 0; j < 32; j++) { ins->fds.modTable[j] = reader.readC() - 3; } ins->fds.modSpeed = reader.readI(); ins->fds.modDepth = reader.readI(); reader.readI(); // this is delay. currently ignored. TODO. - ds.wave.push_back(wave); - ds.waveLen++; + if (ds.wave.size()>=256) { + logW("too many waves! ignoring..."); + delete wave; + } else { + ins->std.waveMacro.len = 1; + ins->std.waveMacro.val[0] = ds.wave.size(); + ds.wave.push_back(wave); + ds.waveLen++; + } unsigned int a = reader.readI(); unsigned int b = reader.readI(); - reader.seek(-8, SEEK_CUR); + if (!reader.seek(-8, SEEK_CUR)) { + logE("couldn't seek back by 8 reading FDS ins"); + lastError = "couldn't seek back by 8 reading FDS ins"; + delete[] file; + return false; + } if (a < 256 && (b & 0xFF) != 0x00) { // don't look at me like this. I don't know why this should be like this either! + logW("a is less than 256 and b is not zero!"); } else { ins->std.volMacro.len = reader.readC(); ins->std.volMacro.loop = reader.readI(); @@ -1112,12 +1173,19 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if (blockVersion >= 8) { unsigned int autopos = reader.readI(); - (void)autopos; + logV("autopos: %d",autopos); } unsigned int wave_count = reader.readI(); size_t waveOff = ds.wave.size(); + if (wave_size>256) { + logE("wave size %d out of range",wave_size); + lastError = "wave size out of range"; + delete[] file; + return false; + } + for (unsigned int ii = 0; ii < wave_count; ii++) { DivWavetable* wave = new DivWavetable(); wave->len = wave_size; @@ -1131,6 +1199,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if (ds.wave.size()<256) { ds.wave.push_back(wave); } else { + logW("too many waves..."); delete wave; } } @@ -1296,16 +1365,30 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } if (instVersion == 2) { - reader.seek(seek_amount, SEEK_CUR); // what the fuck + // I know right? + if (!reader.seek(seek_amount, SEEK_CUR)) { + logE("EFT seek fail"); + lastError = "EFT seek fail"; + delete[] file; + return false; + } } + // this commented out block left here intentionally. + // total mess of code style... for with no space, UNDEFINED CHAR, escaping the unescapable, silly var names... + // ...whatever. /*for(int tti = 0; tti < 20; tti++) { char aaaa = reader.readC(); logV("\'%c\'", aaaa); }*/ } else { - reader.seek(-4, SEEK_CUR); + if (!reader.seek(-4, SEEK_CUR)) { + logE("EFT -4 seek fail"); + lastError = "EFT -4 seek fail"; + delete[] file; + return false; + } } break; @@ -1325,7 +1408,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } ds.insLen = 128; - } else if (blockName == "SEQUENCES") { CHECK_BLOCK_VERSION(6); @@ -1342,6 +1424,14 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned int index = reader.readI(); unsigned int type = reader.readI(); unsigned char size = reader.readC(); + + if (index>=256 || type>=8) { + logE("%d: index/type out of range",i); + lastError = "sequence index/type out of range"; + delete[] file; + return false; + } + macros[index][type].len = size; for (int j = 0; j < size; j++) { @@ -1361,12 +1451,34 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned char* Indices = new unsigned char[128 * 5]; unsigned char* Types = new unsigned char[128 * 5]; + memset(Indices,0,128*5); + memset(Types,0,128*5); + for (unsigned int i = 0; i < seq_count; i++) { unsigned int index = reader.readI(); + if (index>=128*5) { + logE("%d: index out of range",i); + lastError = "sequence index out of range"; + delete[] file; + return false; + } Indices[i] = index; unsigned int type = reader.readI(); + if (type>=128*5) { + logE("%d: type out of range",i); + lastError = "sequence type out of range"; + delete[] file; + return false; + } Types[i] = type; + if (index>=256 || type>=8) { + logE("%d: index/type out of range",i); + lastError = "sequence index/type out of range"; + delete[] file; + return false; + } + unsigned char size = reader.readC(); unsigned int setting = 0; @@ -1393,7 +1505,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si DivInstrument* ins = ds.ins[k]; if (sequenceIndex[k][Types[i]] == Indices[i] && ins->type == DIV_INS_NES && hasSequence[k][Types[i]]) { copyMacro(ins, ¯os[index][type], Types[i], setting); - // memcpy(ins->std.get_macro(DIV_MACRO_VOL + (DivMacroType)Types[i], true), ¯os[sequenceIndex[index][type]][type], sizeof(DivInstrumentMacro)); } } } @@ -1412,7 +1523,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si macro_types[k][j] = setting; copyMacro(ins, ¯os[sequenceIndex[k][j]][j], j, setting); - // memcpy(ins->std.get_macro(DIV_MACRO_VOL + (DivMacroType)j, true), ¯os[sequenceIndex[k][j]][j], sizeof(DivInstrumentMacro)); } } } @@ -1425,9 +1535,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned int release = reader.readI(); unsigned int setting = reader.readI(); - // macros[index][type].rel = release; - // macro_types[index][type] = setting; - for (int k = 0; k < (int)ds.ins.size(); k++) { DivInstrument* ins = ds.ins[k]; if (sequenceIndex[k][Types[i]] == Indices[i] && ins->type == DIV_INS_NES && hasSequence[k][Types[i]]) { @@ -1435,7 +1542,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si macro_types[k][Types[i]] = setting; copyMacro(ins, ¯os[sequenceIndex[k][Types[i]]][Types[i]], Types[i], setting); - // memcpy(ins->std.get_macro(DIV_MACRO_VOL + (DivMacroType)Types[i], true), ¯os[sequenceIndex[k][Types[i]]][Types[i]], sizeof(DivInstrumentMacro)); } } } @@ -1446,12 +1552,11 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } } else if (blockName == "GROOVES") { CHECK_BLOCK_VERSION(6); - // reader.seek(blockSize,SEEK_CUR); unsigned char num_grooves = reader.readC(); int max_groove = 0; - for (int i = 0; i < 0xff; i++) { + for (int i = 0; i < 256; i++) { ds.grooves.push_back(DivGroovePattern()); } @@ -1459,15 +1564,18 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned char index = reader.readC(); unsigned char size = reader.readC(); - if (index > max_groove) + if (index > max_groove) { max_groove = index + 1; + } DivGroovePattern gp; gp.len = size; for (int sz = 0; sz < size; sz++) { unsigned char value = reader.readC(); - gp.val[sz] = value; + if (sz<16) { + gp.val[sz] = value; + } } ds.grooves[index] = gp; @@ -1488,14 +1596,21 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if ((reader.tell() - blockStart) != blockSize) { logE("block %s size does not match! block size %d curr pos %d", blockName, blockSize, reader.tell() - blockStart); } - } else if (blockName == "FRAMES") { CHECK_BLOCK_VERSION(3); for (size_t i = 0; i < ds.subsong.size(); i++) { DivSubSong* s = ds.subsong[i]; - s->ordersLen = reader.readI(); + int framesLen=reader.readI(); + if (framesLen<=1 || framesLen>256) { + logE("frames out of range"); + lastError = "frames out of range"; + delete[] file; + return false; + } + + s->ordersLen = framesLen; if (blockVersion >= 3) { s->speeds.val[0] = reader.readI(); } @@ -1510,18 +1625,31 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si s->virtualTempoN = tempo; } - s->patLen = reader.readI(); + int patLen=reader.readI(); + if (patLen<1 || patLen>256) { + logE("pattern length out of range"); + lastError = "pattern length out of range"; + delete[] file; + return false; + } + s->patLen = patLen; } int why = tchans; if (blockVersion == 1) { why = reader.readI(); + if (why<0 || why>=DIV_MAX_CHANS) { + logE("why out of range!"); + lastError = "why out of range"; + delete[] file; + return false; + } } logV("reading %d and %d orders", tchans, s->ordersLen); for (int j = 0; j < s->ordersLen; j++) { for (int k = 0; k < why; k++) { unsigned char o = reader.readC(); - // logV("%.2x",o); + if (map_channels[k]>=DIV_MAX_CHANS) continue; s->orders.ord[map_channels[k]][j] = o; } } @@ -1533,6 +1661,12 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if (blockVersion == 1) { int patLenOld = reader.readI(); + if (patLenOld<1 || patLenOld>=256) { + logE("old pattern length out of range"); + lastError = "old pattern length out of range"; + delete[] file; + return false; + } for (DivSubSong* i : ds.subsong) { i->patLen = patLenOld; } @@ -1553,6 +1687,37 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si logV("patNum: %d",patNum); logV("rows: %d",numRows); + if (subs<0 || subs>=(int)ds.subsong.size()) { + logE("subsong out of range!"); + lastError = "subsong out of range"; + delete[] file; + return false; + } + if (ch<0 || ch>=DIV_MAX_CHANS) { + logE("channel out of range!"); + lastError = "channel out of range"; + delete[] file; + return false; + } + if (map_channels[ch]>=DIV_MAX_CHANS) { + logE("mapped channel out of range!"); + lastError = "mapped channel out of range"; + delete[] file; + return false; + } + if (patNum<0 || patNum>=256) { + logE("pattern number out of range!"); + lastError = "pattern number out of range"; + delete[] file; + return false; + } + if (numRows<0) { + logE("row count is negative!"); + lastError = "row count is negative"; + delete[] file; + return false; + } + DivPattern* pat = ds.subsong[subs]->pat[map_channels[ch]].getPattern(patNum, true); for (int i = 0; i < numRows; i++) { unsigned int row = 0; @@ -1562,6 +1727,13 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si row = reader.readI(); } + if (row>=256) { + logE("row index out of range"); + lastError = "row index out of range"; + delete[] file; + return false; + } + unsigned char nextNote = reader.readC(); unsigned char nextOctave = reader.readC(); @@ -1592,6 +1764,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } unsigned char nextIns = reader.readC(); + // TODO: you sure about 0xff? if (map_channels[ch] != 0xff) { if (nextIns < 0x40 && nextNote != 0x0d && nextNote != 0x0e) { pat->data[row][2] = nextIns; @@ -1606,6 +1779,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si pat->data[row][3] = nextVol; if (map_channels[ch] == vrc6_saw_chan) // scale volume { + // TODO: shouldn't it be 32? pat->data[row][3] = (pat->data[row][3] * 42) / 15; } @@ -1625,13 +1799,9 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si effectCols = 1; } - logV("effectCols: %d",effectCols); - unsigned char nextEffectVal = 0; unsigned char nextEffect = 0; - //logV("row %d effects are read at %x",row,reader.tell()); - for (int j = 0; j < effectCols; j++) { nextEffect = reader.readC(); @@ -1727,14 +1897,12 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } - // logW("next effect %d val %d", nextEffect, nextEffectVal); - if (map_channels[ch] != 0xff) { if (nextEffect == 0 && nextEffectVal == 0) { pat->data[row][4 + (j * 2)] = -1; pat->data[row][5 + (j * 2)] = -1; } else { - if (nextEffect < ftEffectMapSize) { + if ((eft && nextEffectdata[row][4 + (j * 2)] = eftEffectMap[nextEffect]; pat->data[row][5 + (j * 2)] = eftEffectMap[nextEffect] == -1 ? -1 : nextEffectVal; @@ -1782,7 +1950,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } } else if (blockName == "DPCM SAMPLES") { CHECK_BLOCK_VERSION(1); - // reader.seek(blockSize,SEEK_CUR); unsigned char num_samples = reader.readC(); for (int i = 0; i < 256; i++) { @@ -1808,14 +1975,16 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned int sample_len = reader.readI(); + if (sample_len>=2097152) { + logE("%d: sample too large! %d",index,sample_len); + lastError = "sample too large"; + delete[] file; + return false; + } + true_size = sample_len + ((1 - (int)sample_len) & 0x0f); - sample->lengthDPCM = true_size; - sample->samples = true_size * 8; - - sample->dataDPCM = new unsigned char[true_size]; - + sample->init(true_size * 8); memset(sample->dataDPCM, 0xAA, true_size); - reader.read(sample->dataDPCM, sample_len); } @@ -1824,7 +1993,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si for (int i = 255; i > 0; i--) { DivSample* s = ds.sample[i]; - if (s->dataDPCM) { + if (s->samples>0) { last_non_empty_sample = i; break; } @@ -1835,21 +2004,42 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } ds.sampleLen = ds.sample.size(); - } else if (blockName == "SEQUENCES_VRC6") { CHECK_BLOCK_VERSION(6); unsigned char* Indices = new unsigned char[128 * 5]; unsigned char* Types = new unsigned char[128 * 5]; + memset(Indices,0,128*5); + memset(Types,0,128*5); + unsigned int seq_count = reader.readI(); for (unsigned int i = 0; i < seq_count; i++) { unsigned int index = reader.readI(); + if (index>=128*5) { + logE("%d: index out of range",i); + lastError = "sequence index out of range"; + delete[] file; + return false; + } Indices[i] = index; unsigned int type = reader.readI(); + if (type>=128*5) { + logE("%d: type out of range",i); + lastError = "sequence type out of range"; + delete[] file; + return false; + } Types[i] = type; + if (index>=256 || type>=8) { + logE("%d: index/type out of range",i); + lastError = "sequence index/type out of range"; + delete[] file; + return false; + } + unsigned char size = reader.readC(); unsigned int setting = 0; @@ -1914,9 +2104,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned int release = reader.readI(); unsigned int setting = reader.readI(); - // macros[index][type].rel = release; - // macro_types[index][type] = setting; - for (int k = 0; k < (int)ds.ins.size(); k++) { DivInstrument* ins = ds.ins[k]; if (sequenceIndex[k][Types[i]] == Indices[i] && ins->type == DIV_INS_VRC6 && hasSequence[k][Types[i]]) { @@ -1937,19 +2124,40 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si delete[] Types; } else if (blockName == "SEQUENCES_N163" || blockName == "SEQUENCES_N106") { CHECK_BLOCK_VERSION(1); - // reader.seek(blockSize,SEEK_CUR); unsigned char* Indices = new unsigned char[128 * 5]; unsigned char* Types = new unsigned char[128 * 5]; + memset(Indices,0,128*5); + memset(Types,0,128*5); + unsigned int seq_count = reader.readI(); for (unsigned int i = 0; i < seq_count; i++) { unsigned int index = reader.readI(); + if (index>=128*5) { + logE("%d: index out of range",i); + lastError = "sequence index out of range"; + delete[] file; + return false; + } Indices[i] = index; unsigned int type = reader.readI(); + if (type>=128*5) { + logE("%d: type out of range",i); + lastError = "sequence type out of range"; + delete[] file; + return false; + } Types[i] = type; + if (index>=256 || type>=8) { + logE("%d: index/type out of range",i); + lastError = "sequence index/type out of range"; + delete[] file; + return false; + } + unsigned char size = reader.readC(); unsigned int setting = 0; @@ -1974,7 +2182,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si DivInstrument* ins = ds.ins[k]; if (sequenceIndex[k][Types[i]] == Indices[i] && ins->type == DIV_INS_N163 && hasSequence[k][Types[i]]) { copyMacro(ins, ¯os[index][type], type, setting); - // memcpy(ins->std.get_macro(DIV_MACRO_VOL + (DivMacroType)Types[i], true), ¯os[sequenceIndex[index][type]][type], sizeof(DivInstrumentMacro)); } } } @@ -1984,19 +2191,40 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } else if (blockName == "SEQUENCES_S5B") { CHECK_BLOCK_VERSION(1); - // reader.seek(blockSize,SEEK_CUR); unsigned char* Indices = new unsigned char[128 * 5]; unsigned char* Types = new unsigned char[128 * 5]; + memset(Indices,0,128*5); + memset(Types,0,128*5); + unsigned int seq_count = reader.readI(); for (unsigned int i = 0; i < seq_count; i++) { unsigned int index = reader.readI(); + if (index>=128*5) { + logE("%d: index out of range",i); + lastError = "sequence index out of range"; + delete[] file; + return false; + } Indices[i] = index; unsigned int type = reader.readI(); + if (type>=128*5) { + logE("%d: type out of range",i); + lastError = "sequence type out of range"; + delete[] file; + return false; + } Types[i] = type; + if (index>=256 || type>=8) { + logE("%d: index/type out of range",i); + lastError = "sequence index/type out of range"; + delete[] file; + return false; + } + unsigned char size = reader.readC(); unsigned int setting = 0; @@ -2021,7 +2249,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si DivInstrument* ins = ds.ins[k]; if (sequenceIndex[k][type] == Indices[i] && ins->type == DIV_INS_AY && hasSequence[k][type]) { copyMacro(ins, ¯os[index][type], type, setting); - // memcpy(ins->std.get_macro(DIV_MACRO_VOL + (DivMacroType)Types[i], true), ¯os[sequenceIndex[index][type]][type], sizeof(DivInstrumentMacro)); } } } @@ -2034,14 +2261,36 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si unsigned char* Indices = new unsigned char[128 * 5]; unsigned char* Types = new unsigned char[128 * 5]; + memset(Indices,0,128*5); + memset(Types,0,128*5); + unsigned int seq_count = reader.readI(); for (unsigned int i = 0; i < seq_count; i++) { unsigned int index = reader.readI(); + if (index>=128*5) { + logE("%d: index out of range",i); + lastError = "sequence index out of range"; + delete[] file; + return false; + } Indices[i] = index; unsigned int type = reader.readI(); + if (type>=128*5) { + logE("%d: type out of range",i); + lastError = "sequence type out of range"; + delete[] file; + return false; + } Types[i] = type; + if (index>=256 || type>=8) { + logE("%d: index/type out of range",i); + lastError = "sequence index/type out of range"; + delete[] file; + return false; + } + unsigned char size = reader.readC(); unsigned int setting = 0; @@ -2066,7 +2315,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si DivInstrument* ins = ds.ins[k]; if (sequenceIndex[k][type] == Indices[i] && ins->type == DIV_INS_C64 && hasSequence[k][type]) { copyMacro(ins, ¯os[index][type], type, setting); - // memcpy(ins->std.get_macro(DIV_MACRO_VOL + (DivMacroType)Types[i], true), ¯os[sequenceIndex[index][type]][type], sizeof(DivInstrumentMacro)); } } } @@ -2075,31 +2323,33 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si delete[] Types; } else if (blockName == "JSON") { CHECK_BLOCK_VERSION(1); + logW("block JSON not supported..."); reader.seek(blockSize, SEEK_CUR); } else if (blockName == "PARAMS_EMU") { CHECK_BLOCK_VERSION(1); + logW("block PARAMS_EMU not supported..."); reader.seek(blockSize, SEEK_CUR); } else if (blockName == "DETUNETABLES") { CHECK_BLOCK_VERSION(1); + logW("block DETUNETABLES not supported..."); reader.seek(blockSize, SEEK_CUR); } else if (blockName == "COMMENTS") { CHECK_BLOCK_VERSION(1); - // reader.seek(blockSize,SEEK_CUR); unsigned int display_comment = reader.readI(); - (void)display_comment; - char ch = 1; + logV("displayComment: %d",display_comment); - do { + char ch = 0; + + // why not readString? + while (true) { ch = reader.readC(); - String sss = String() + ch; - ds.subsong[0]->notes += sss; - } while (ch != 0); + if (ch==0) break; + ds.subsong[0]->notes += ch; + } - // ds.subsong[0]->notes = reader.readS(); } else if (blockName == "PARAMS_EXTRA") { CHECK_BLOCK_VERSION(3); - // reader.seek(blockSize,SEEK_CUR); unsigned int linear_pitch = reader.readI(); ds.linearPitch = linear_pitch == 0 ? 0 : 2; @@ -2112,11 +2362,10 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } if (blockVersion >= 3) { unsigned char flats = reader.readC(); - (void)flats; + logV("flats: %d",(int)flats); } } else if (blockName == "TUNING") { CHECK_BLOCK_VERSION(1); - // reader.seek(blockSize,SEEK_CUR); if (blockVersion == 1) { int fineTuneCents = reader.readC() * 100; fineTuneCents += reader.readC(); @@ -2125,6 +2374,7 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si } } else if (blockName == "BOOKMARKS") { CHECK_BLOCK_VERSION(1); + logW("block BOOKMARKS not supported..."); reader.seek(blockSize, SEEK_CUR); } else { logE("block %s is unknown!", blockName); @@ -2152,6 +2402,8 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si if (index < 0) index = 0; + if (index>=(int)ds.ins.size()) continue; + DivInstrument* ins = ds.ins[index]; if (ins->type == DIV_INS_FM) { diff --git a/src/engine/fileOpsIns.cpp b/src/engine/fileOpsIns.cpp index 7f9559b3a..6a181f5f2 100644 --- a/src/engine/fileOpsIns.cpp +++ b/src/engine/fileOpsIns.cpp @@ -1897,7 +1897,7 @@ std::vector DivEngine::instrumentFromFile(const char* path, bool bool isOldFurnaceIns=false; try { reader.read(magic,4); - if (memcmp("FINS",magic,4)==0) { + if (memcmp("FINS",magic,4)==0 || memcmp("FINB",magic,4)==0) { isFurnaceInstr=true; logV("found a new Furnace ins"); } else { diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index 6225765aa..ba99b28f4 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -3005,6 +3005,8 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version, DivS type=1; } else if (memcmp(magic,"FINS",4)==0) { type=2; + } else if (memcmp(magic,"FINB",4)==0) { // DIV_FUR_VARIANT_B + type=2; } else { logE("invalid instrument header!"); return DIV_DATA_INVALID_HEADER; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 0b1be7a55..087124dcb 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1303,7 +1303,7 @@ void DivEngine::registerSystems() { sysDefs[DIV_SYSTEM_VRC7]=new DivSysDef( "Konami VRC7", NULL, 0x9d, 0, 6, true, false, 0x151, false, 0, 0, 0, - "like OPLL, but even more cost reductions applied. three less FM channels, and no drums mode...", + "like OPLL, but even more cost reductions applied. three FM channels went missing, and drums mode did as well...", {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6"}, {"F1", "F2", "F3", "F4", "F5", "F6"}, {DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM, DIV_CH_FM}, diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 71f88faa8..b19db9d5b 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -71,6 +71,7 @@ const char* aboutLine[]={ "Aburtos", "ActualNK358", "akumanatt", + "aloelucidity", "AmigaX", "AquaDoesStuff", "AURORA*FIELDS", diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 2f1bb9a8e..0e1a80ec5 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -4391,7 +4391,7 @@ bool FurnaceGUI::loop() { toggleMobileUI(!mobileUI); } #endif - if (ImGui::MenuItem("manage presets...",BIND_FOR(GUI_ACTION_WINDOW_USER_PRESETS))) { + if (ImGui::MenuItem("user systems...",BIND_FOR(GUI_ACTION_WINDOW_USER_PRESETS))) { userPresetsOpen=true; } if (ImGui::MenuItem("settings...",BIND_FOR(GUI_ACTION_WINDOW_SETTINGS))) { diff --git a/src/gui/gui.h b/src/gui/gui.h index 1383258f4..5571f660b 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1265,6 +1265,7 @@ struct FurnaceGUISysDef { String definition; std::vector orig; std::vector subDefs; + void bake(); FurnaceGUISysDef(const char* n, std::initializer_list def, const char* e=NULL); FurnaceGUISysDef(const char* n, const char* def, DivEngine* e); }; @@ -2089,7 +2090,7 @@ class FurnaceGUI { displayRenderTime(0), maxUndoSteps(100), vibrationStrength(0.5f), - vibrationLength(100), + vibrationLength(20), mainFontPath(""), headFontPath(""), patFontPath(""), diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 420fa9bfb..85598899d 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -7469,7 +7469,7 @@ void FurnaceGUI::drawInsEdit() { memset(oldData,0,256*sizeof(int)); memcpy(oldData,lastMacroDesc.macro->val,lastMacroDesc.macro->len*sizeof(int)); - lastMacroDesc.macro->len=MIN(128,((double)lastMacroDesc.macro->len*(macroScaleX/100.0))); + lastMacroDesc.macro->len=MIN(255,((double)lastMacroDesc.macro->len*(macroScaleX/100.0))); for (int i=0; ilen; i++) { int val=0; diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index 96912d0c5..f91a45905 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -3102,11 +3102,9 @@ void FurnaceGUI::initSystemPresets() { CATEGORY_END; } -FurnaceGUISysDef::FurnaceGUISysDef(const char* n, std::initializer_list def, const char* e): - name(n), - extra((e==NULL)?"":e) { - orig=def; +void FurnaceGUISysDef::bake() { int index=0; + definition=""; for (FurnaceGUISysDefChip& i: orig) { definition+=fmt::sprintf( "id%d=%d\nvol%d=%f\npan%d=%f\nflags%d=%s\n", @@ -3126,12 +3124,19 @@ FurnaceGUISysDef::FurnaceGUISysDef(const char* n, std::initializer_list def, const char* e): + name(n), + extra((e==NULL)?"":e) { + orig=def; + bake(); +} + FurnaceGUISysDef::FurnaceGUISysDef(const char* n, const char* def, DivEngine* e): name(n), - definition(def) { + definition(taDecodeBase64(def)) { // extract definition DivConfig conf; - conf.loadFromBase64(def); + conf.loadFromMemory(definition.c_str()); for (int i=0; i65536) sampRate=65536; altered=true; } rightClickable - DivPlatformGBAMinMod* dispatch=(DivPlatformGBAMinMod*)e->getDispatch(chan); - float maxCPU=dispatch->maxCPU*100; - ImGui::Text("Actual sample rate: %d Hz", dispatch->chipClock); - if (maxCPU>90) ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_WARNING]); - ImGui::Text("Max mixer CPU usage: %.0f%%", maxCPU); - if (maxCPU>90) ImGui::PopStyleColor(); - FurnaceGUI::popWarningColor(); + if (chan>=0) { + DivPlatformGBAMinMod* dispatch=(DivPlatformGBAMinMod*)e->getDispatch(chan); + if (dispatch!=NULL) { + float maxCPU=dispatch->maxCPU*100; + ImGui::Text("Actual sample rate: %d Hz", dispatch->chipClock); + if (maxCPU>90) ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_WARNING]); + ImGui::Text("Max mixer CPU usage: %.0f%%",maxCPU); + if (maxCPU>90) ImGui::PopStyleColor(); + FurnaceGUI::popWarningColor(); + } + } if (altered) { e->lockSave([&]() { flags.set("volScale",volScale); diff --git a/src/gui/tutorial.cpp b/src/gui/tutorial.cpp index eb46ec595..b72fbb074 100644 --- a/src/gui/tutorial.cpp +++ b/src/gui/tutorial.cpp @@ -453,7 +453,7 @@ struct FurnaceCV { newHiScore(false), playSongs(true), pleaseInitSongs(false), - lives(3), + lives(5), respawnTime(0), stage(0), shotType(0), diff --git a/src/gui/userPresets.cpp b/src/gui/userPresets.cpp index f78848300..2882fea15 100644 --- a/src/gui/userPresets.cpp +++ b/src/gui/userPresets.cpp @@ -177,7 +177,7 @@ bool FurnaceGUI::loadUserPresets(bool redundancy) { } indent>>=1; - if (!key.empty() && !value.empty()) { + if (!key.empty()) { std::vector* where=digDeep(userCategory->systems,indent); where->push_back(FurnaceGUISysDef(key.c_str(),value.c_str(),e)); } @@ -203,7 +203,7 @@ void writeSubEntries(FILE* f, std::vector& entries, int depth) for (int i=0; isystems.push_back(FurnaceGUISysDef("New Preset",{})); - selectedUserPreset.clear(); - selectedUserPreset.push_back(userCategory->systems.size()-1); + if (ImGui::BeginChild("UList",ImVec2(ImGui::GetContentRegionAvail().x,ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()))) { + ImGui::AlignTextToFramePadding(); + ImGui::Text("Systems"); + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PLUS "##AddPreset")) { + userCategory->systems.push_back(FurnaceGUISysDef("New Preset",{})); + selectedUserPreset.clear(); + selectedUserPreset.push_back(userCategory->systems.size()-1); + } + printPresets(userCategory->systems,0,depthStack); } - printPresets(userCategory->systems,0,depthStack); + ImGui::EndChild(); // editor ImGui::TableNextColumn(); - if (selectedUserPreset.empty()) { - ImGui::Text("select a preset"); - } else { - FurnaceGUISysDef* preset=selectPreset(userCategory->systems); + if (ImGui::BeginChild("UEdit",ImVec2(ImGui::GetContentRegionAvail().x,ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()))) { + if (selectedUserPreset.empty()) { + ImGui::Text("select a preset"); + } else { + FurnaceGUISysDef* preset=selectPreset(userCategory->systems); + bool doRemovePreset=false; - if (preset!=NULL) { - ImGui::AlignTextToFramePadding(); - ImGui::Text("Name"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::InputText("##PName",&preset->name); - ImGui::Separator(); - ImGui::Text("the rest..."); + if (preset!=NULL) { + ImGui::AlignTextToFramePadding(); + ImGui::Text("Name"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize("Remove").x-ImGui::GetStyle().ItemSpacing.x*2.0-ImGui::GetStyle().ItemInnerSpacing.x*2.0); + ImGui::InputText("##PName",&preset->name); + ImGui::SameLine(); + pushDestColor(); + if (ImGui::Button("Remove##UPresetRemove")) { + doRemovePreset=true; + } + popDestColor(); + + ImGui::Separator(); + + int doRemove=-1; + bool mustBake=false; + + for (size_t i=0; iorig.size(); i++) { + String tempID; + FurnaceGUISysDefChip& chip=preset->orig[i]; + + bool doInvert=(chip.vol<0); + float vol=fabs(chip.vol); + ImGui::PushID(i); + + tempID=fmt::sprintf("%s##USystem",getSystemName(chip.sys)); + ImGui::Button(tempID.c_str(),ImVec2(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize("Invert").x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0,0)); + if (ImGui::BeginPopupContextItem("SysPickerCU",ImGuiPopupFlags_MouseButtonLeft)) { + DivSystem picked=systemPicker(); + if (picked!=DIV_SYSTEM_NULL) { + chip.sys=picked; + mustBake=true; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::SameLine(); + if (ImGui::Checkbox("Invert",&doInvert)) { + chip.vol=-chip.vol; + mustBake=true; + } + ImGui::SameLine(); + pushDestColor(); + if (ImGui::Button(ICON_FA_MINUS "##USysRemove")) { + doRemove=i; + mustBake=true; + } + popDestColor(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0); + if (CWSliderFloat("Volume",&vol,0.0f,3.0f)) { + if (doInvert) { + if (vol<0.0001) vol=0.0001; + } + if (vol<0) vol=0; + if (vol>10) vol=10; + chip.vol=doInvert?-vol:vol; + mustBake=true; + } rightClickable + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0); + if (CWSliderFloat("Panning",&chip.pan,-1.0f,1.0f)) { + if (chip.pan<-1.0f) chip.pan=-1.0f; + if (chip.pan>1.0f) chip.pan=1.0f; + mustBake=true; + } rightClickable + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0); + if (CWSliderFloat("Front/Rear",&chip.panFR,-1.0f,1.0f)) { + if (chip.panFR<-1.0f) chip.panFR=-1.0f; + if (chip.panFR>1.0f) chip.panFR=1.0f; + mustBake=true; + } rightClickable + + if (ImGui::TreeNode("Configure")) { + DivConfig sysFlags; + sysFlags.loadFromBase64(chip.flags.c_str()); + if (drawSysConf(-1,i,chip.sys,sysFlags,false)) { + chip.flags=sysFlags.toBase64(); + mustBake=true; + } + ImGui::TreePop(); + } + + ImGui::PopID(); + } + + if (doRemove>=0) { + preset->orig.erase(preset->orig.begin()+doRemove); + mustBake=true; + } + + ImGui::Button(ICON_FA_PLUS "##SysAddU"); + if (ImGui::BeginPopupContextItem("SysPickerU",ImGuiPopupFlags_MouseButtonLeft)) { + DivSystem picked=systemPicker(); + if (picked!=DIV_SYSTEM_NULL) { + preset->orig.push_back(FurnaceGUISysDefChip(picked,1.0f,0.0f,"")); + mustBake=true; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::Separator(); + + ImGui::Text("Advanced"); + if (ImGui::InputTextMultiline("##UExtra",&preset->extra,ImVec2(ImGui::GetContentRegionAvail().x,120.0f*dpiScale),ImGuiInputTextFlags_UndoRedo)) { + mustBake=true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "insert additional settings in `option=value` format.\n" + "available options:\n" + "- tickRate" + ); + } + + if (mustBake) preset->bake(); + } else { + selectedUserPreset.clear(); + } + + if (doRemovePreset) { + std::vector& items=userCategory->systems; + FurnaceGUISysDef* target=NULL; + for (size_t i=0; i(int)items.size()) break; + target=&items[selectedUserPreset[i]]; + if (isubDefs; + } else { + items.erase(items.begin()+selectedUserPreset[i]); + } + } + + selectedUserPreset.clear(); + } } } + ImGui::EndChild(); ImGui::EndTable(); }