#define _USE_MATH_DEFINES
// OK, sorry for inserting the define here but I'm so tired of this extension
/**
 * Furnace Tracker - multi-system chiptune tracker
 * Copyright (C) 2021-2023 tildearrow and contributors
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

// I hate you clangd extension!
// how about you DON'T insert random headers before this freaking important
// define!!!!!!

#include "gui.h"
#include "util.h"
#include "../ta-log.h"
#include "../fileutils.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "ImGuiFileDialog.h"
#include "IconsFontAwesome4.h"
#include "misc/cpp/imgui_stdlib.h"
#include "plot_nolerp.h"
#include "guiConst.h"
#include "intConst.h"
#include "scaling.h"
#include "introTune.h"
#include <stdint.h>
#include <zlib.h>
#include <fmt/printf.h>
#include <stdexcept>

#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>
#include "../utfutils.h"
#define LAYOUT_INI "\\layout.ini"
#define BACKUPS_DIR "\\backups"
#else
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <pwd.h>
#include <sys/stat.h>
#define LAYOUT_INI "/layout.ini"
#define BACKUPS_DIR "/backups"
#endif

#ifdef IS_MOBILE
#define MOBILE_UI_DEFAULT true
#else
#define MOBILE_UI_DEFAULT false
#endif

#include "actionUtil.h"

bool Particle::update(float frameTime) {
  pos.x+=speed.x*frameTime;
  pos.y+=speed.y*frameTime;
  speed.x*=1.0-((1.0-friction)*frameTime);
  speed.y*=1.0-((1.0-friction)*frameTime);
  speed.y+=gravity*frameTime;
  life-=lifeSpeed*frameTime;
  return (life>0);
}

void FurnaceGUI::centerNextWindow(const char* name, float w, float h) {
  if (ImGui::IsPopupOpen(name)) {
    if (settings.centerPopup) {
      ImGui::SetNextWindowPos(ImVec2(w*0.5,h*0.5),ImGuiCond_Always,ImVec2(0.5,0.5));
    }
  }
}

void FurnaceGUI::bindEngine(DivEngine* eng) {
  e=eng;
  wavePreview.setEngine(e);
}

const char* FurnaceGUI::noteName(short note, short octave) {
  if (note==100) {
    return noteOffLabel;
  } else if (note==101) { // note off and envelope release
    return noteRelLabel;
  } else if (note==102) { // envelope release only
    return macroRelLabel;
  } else if (octave==0 && note==0) {
    return emptyLabel;
  } else if (note==0 && octave!=0) {
    return "BUG";
  }
  int seek=(note+(signed char)octave*12)+60;
  if (seek<0 || seek>=180) {
    return "???";
  }
  if (settings.flatNotes) {
    if (settings.germanNotation) return noteNamesGF[seek];
    return noteNamesF[seek];
  }
  if (settings.germanNotation) return noteNamesG[seek];
  return noteNames[seek];
}

bool FurnaceGUI::decodeNote(const char* what, short& note, short& octave) {
  if (strlen(what)!=3) return false;
  if (strcmp(what,"...")==0) {
    note=0;
    octave=0;
    return true;
  }
  if (strcmp(what,"???")==0) {
    note=0;
    octave=0;
    return true;
  }
  if (strcmp(what,"OFF")==0) {
    note=100;
    octave=0;
    return true;
  }
  if (strcmp(what,"===")==0) {
    note=101;
    octave=0;
    return true;
  }
  if (strcmp(what,"REL")==0) {
    note=102;
    octave=0;
    return true;
  }
  for (int i=0; i<180; i++) {
    if (strcmp(what,noteNames[i])==0) {
      if ((i%12)==0) {
        note=12;
        octave=(unsigned char)((i/12)-6);
      } else {
        note=i%12;
        octave=(unsigned char)((i/12)-5);
      }
      return true;
    }
  }
  return false;
}

String FurnaceGUI::encodeKeyMap(std::map<int,int>& map) {
  String ret;
  for (std::map<int,int>::value_type& i: map) {
    ret+=fmt::sprintf("%d:%d;",i.first,i.second);
  }
  return ret;
}

void FurnaceGUI::decodeKeyMap(std::map<int,int>& map, String source) {
  map.clear();
  bool inValue=false;
  bool negateKey=false;
  bool negateValue=false;
  int key=0;
  int val=0;
  for (char& i: source) {
    switch (i) {
      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
        if (inValue) {
          val*=10;
          val+=i-'0';
        } else {
          key*=10;
          key+=i-'0';
        }
        break;
      case '-':
        if (inValue) {
          negateValue=true;
        } else {
          negateKey=true;
        }
        break;
      case ':':
        inValue=true;
        break;
      case ';':
        if (inValue) {
          map[negateKey?-key:key]=negateValue?-val:val;
        }
        key=0;
        val=0;
        inValue=false;
        negateKey=false;
        negateValue=false;
        break;
    }
  }
}

void FurnaceGUI::encodeMMLStr(String& target, int* macro, int macroLen, int macroLoop, int macroRel, bool hex, bool bit30) {
  target="";
  char buf[32];
  for (int i=0; i<macroLen; i++) {
    if (i==macroLoop) target+="| ";
    if (i==macroRel) target+="/ ";
    if (bit30 && ((macro[i]&0xc0000000)==0x40000000 || (macro[i]&0xc0000000)==0x80000000)) target+="@";
    int macroVal=macro[i];
    if (macro[i]<0) {
      if (!(macroVal&0x40000000)) macroVal|=0x40000000;
    } else {
      if (macroVal&0x40000000) macroVal&=~0x40000000;
    }
    if (hex) {
      if (i==macroLen-1) {
        snprintf(buf,31,"%.2X",macroVal);
      } else {
        snprintf(buf,31,"%.2X ",macroVal);
      }
    } else {
      if (i==macroLen-1) {
        snprintf(buf,31,"%d",macroVal);
      } else {
        snprintf(buf,31,"%d ",macroVal);
      }
    }
    target+=buf;
  }
}

void FurnaceGUI::decodeMMLStrW(String& source, int* macro, int& macroLen, int macroMin, int macroMax, bool hex) {
  int buf=0;
  bool negaBuf=false;
  bool hasVal=false;
  macroLen=0;
  for (char& i: source) {
    switch (i) {
      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
        hasVal=true;
        buf*=hex?16:10;
        buf+=i-'0';
        break;
      case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
        if (hex) {
          hasVal=true;
          buf*=16;
          buf+=10+i-'A';
        }
        break;
      case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
        if (hex) {
          hasVal=true;
          buf*=16;
          buf+=10+i-'a';
        }
        break;
      case '-':
        if (!hasVal) {
          hasVal=true;
          negaBuf=true;
        }
        break;
      case ' ':
        if (hasVal) {
          hasVal=false;
          macro[macroLen]=negaBuf?-buf:buf;
          negaBuf=false;
          if (macro[macroLen]<macroMin) macro[macroLen]=macroMin;
          if (macro[macroLen]>macroMax) macro[macroLen]=macroMax;
          macroLen++;
          buf=0;
        }
        break;
    }
    if (macroLen>=256) break;
  }
  if (hasVal && macroLen<256) {
    hasVal=false;
    macro[macroLen]=negaBuf?-buf:buf;
    negaBuf=false;
    if (macro[macroLen]<macroMin) macro[macroLen]=macroMin;
    if (macro[macroLen]>macroMax) macro[macroLen]=macroMax;
    macroLen++;
    buf=0;
  }
}

void FurnaceGUI::decodeMMLStr(String& source, int* macro, unsigned char& macroLen, unsigned char& macroLoop, int macroMin, int macroMax, unsigned char& macroRel, bool bit30) {
  int buf=0;
  bool negaBuf=false;
  bool setBit30=false;
  bool hasVal=false;
  macroLen=0;
  macroLoop=255;
  macroRel=255;
  for (char& i: source) {
    switch (i) {
      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
        hasVal=true;
        buf*=10;
        buf+=i-'0';
        break;
      case '-':
        if (!hasVal) {
          hasVal=true;
          negaBuf=true;
        }
        break;
      case '@':
        if (bit30) {
          setBit30=true;
        }
        break;
      case ' ':
        if (hasVal) {
          hasVal=false;
          macro[macroLen]=negaBuf?-buf:buf;
          negaBuf=false;
          if (macro[macroLen]<macroMin) macro[macroLen]=macroMin;
          if (macro[macroLen]>macroMax) macro[macroLen]=macroMax;
          if (setBit30) macro[macroLen]^=0x40000000;
          setBit30=false;
          macroLen++;
          buf=0;
        }
        break;
      case '|':
        if (hasVal) {
          hasVal=false;
          macro[macroLen]=negaBuf?-buf:buf;
          negaBuf=false;
          if (macro[macroLen]<macroMin) macro[macroLen]=macroMin;
          if (macro[macroLen]>macroMax) macro[macroLen]=macroMax;
          if (setBit30) macro[macroLen]^=0x40000000;
          setBit30=false;
          macroLen++;
          buf=0;
        }
        if (macroLoop==255) {
          macroLoop=macroLen;
        }
        break;
      case '/':
        if (hasVal) {
          hasVal=false;
          macro[macroLen]=negaBuf?-buf:buf;
          negaBuf=false;
          if (macro[macroLen]<macroMin) macro[macroLen]=macroMin;
          if (macro[macroLen]>macroMax) macro[macroLen]=macroMax;
          if (setBit30) macro[macroLen]^=0x40000000;
          setBit30=false;
          macroLen++;
          buf=0;
        }
        if (macroRel==255) {
          macroRel=macroLen;
        }
        break;
    }
    if (macroLen>=255) break;
  }
  if (hasVal && macroLen<255) {
    hasVal=false;
    macro[macroLen]=negaBuf?-buf:buf;
    negaBuf=false;
    if (macro[macroLen]<macroMin) macro[macroLen]=macroMin;
    if (macro[macroLen]>macroMax) macro[macroLen]=macroMax;
    if (setBit30) macro[macroLen]^=0x40000000;
    setBit30=false;
    macroLen++;
    buf=0;
  }
}

#define CW_ADDITION(T) \
  if (p_min!=NULL && p_max!=NULL) { \
    if (*((T*)p_min)>*((T*)p_max)) { \
      if (wheelY<0) { \
        if ((*((T*)p_data)-wheelY)>*((T*)p_min)) { \
          *((T*)p_data)=*((T*)p_min); \
        } else { \
          *((T*)p_data)-=wheelY; \
        } \
      } else { \
        if ((*((T*)p_data)-wheelY)<*((T*)p_max)) { \
          *((T*)p_data)=*((T*)p_max); \
        } else { \
          *((T*)p_data)-=wheelY; \
        } \
      } \
    } else { \
      if (wheelY>0) { \
        if ((*((T*)p_data)+wheelY)>*((T*)p_max)) { \
          *((T*)p_data)=*((T*)p_max); \
        } else { \
          *((T*)p_data)+=wheelY; \
        } \
      } else { \
        if ((*((T*)p_data)+wheelY)<*((T*)p_min)) { \
          *((T*)p_data)=*((T*)p_min); \
        } else { \
          *((T*)p_data)+=wheelY; \
        } \
      } \
    } \
  }

bool FurnaceGUI::CWSliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) {
  if (ImGui::SliderScalar(label,data_type,p_data,p_min,p_max,format,flags)) {
    return true;
  }
  if (ImGui::IsItemHovered() && ctrlWheeling) {
    switch (data_type) {
      case ImGuiDataType_U8:
        CW_ADDITION(unsigned char);
        break;
      case ImGuiDataType_S8:
        CW_ADDITION(signed char);
        break;
      case ImGuiDataType_U16:
        CW_ADDITION(unsigned short);
        break;
      case ImGuiDataType_S16:
        CW_ADDITION(short);
        break;
      case ImGuiDataType_U32:
        CW_ADDITION(unsigned int);
        break;
      case ImGuiDataType_S32:
        CW_ADDITION(int);
        break;
      case ImGuiDataType_Float:
        CW_ADDITION(float);
        break;
      case ImGuiDataType_Double:
        CW_ADDITION(double);
        break;
    }
    return true;
  }
  return false;
}

bool FurnaceGUI::CWVSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) {
  if (ImGui::VSliderScalar(label,size,data_type,p_data,p_min,p_max,format,flags)) {
    return true;
  }
  if (ImGui::IsItemHovered() && ctrlWheeling) {
    switch (data_type) {
      case ImGuiDataType_U8:
        CW_ADDITION(unsigned char);
        break;
      case ImGuiDataType_S8:
        CW_ADDITION(signed char);
        break;
      case ImGuiDataType_U16:
        CW_ADDITION(unsigned short);
        break;
      case ImGuiDataType_S16:
        CW_ADDITION(short);
        break;
      case ImGuiDataType_U32:
        CW_ADDITION(unsigned int);
        break;
      case ImGuiDataType_S32:
        CW_ADDITION(int);
        break;
      case ImGuiDataType_Float:
        CW_ADDITION(float);
        break;
      case ImGuiDataType_Double:
        CW_ADDITION(double);
        break;
    }
    return true;
  }
  return false;
}

bool FurnaceGUI::CWSliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) {
  return CWSliderScalar(label,ImGuiDataType_S32,v,&v_min,&v_max,format,flags);
}

bool FurnaceGUI::CWSliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) {
  return CWSliderScalar(label,ImGuiDataType_Float,v,&v_min,&v_max,format,flags);
}

bool FurnaceGUI::CWVSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) {
  return CWVSliderScalar(label,size,ImGuiDataType_S32,v,&v_min,&v_max,format,flags);
}

bool FurnaceGUI::InvCheckbox(const char* label, bool* value) {
  bool t=!(*value);
  if (ImGui::Checkbox(label,&t)) {
    *value=!t;
    return true;
  }
  return false;
}

void FurnaceGUI::sameLineMaybe(float width) {
  if (width<0.0f) width=ImGui::GetFrameHeight();

  ImGui::SameLine();
  if (ImGui::GetContentRegionAvail().x<width) ImGui::NewLine();
}

const char* FurnaceGUI::getSystemName(DivSystem which) {
  /*
  if (settings.chipNames) {
    return e->getSystemChips(which);
  }
  */
  return e->getSystemName(which);
}

void FurnaceGUI::updateScroll(int amount) {
  float lineHeight=(patFont->FontSize+2*dpiScale);
  nextScroll=lineHeight*amount;
  haveHitBounds=false;
}

void FurnaceGUI::addScroll(int amount) {
  float lineHeight=(patFont->FontSize+2*dpiScale);
  nextAddScroll=lineHeight*amount;
  haveHitBounds=false;
}

void FurnaceGUI::setFileName(String name) {
#ifdef _WIN32
  wchar_t ret[4096];
  WString ws=utf8To16(name.c_str());
  int index=0;
  for (wchar_t& i: ws) {
    ret[index++]=i;
    if (index>=4095) break;
  }
  ret[index]=0;
  backupLock.lock();
  if (GetFullPathNameW(ws.c_str(),4095,ret,NULL)==0) {
    curFileName=name;
  } else {
    curFileName=utf16To8(ret);
  }
  backupLock.unlock();
#else
  char ret[4096];
  backupLock.lock();
  if (realpath(name.c_str(),ret)==NULL) {
    curFileName=name;
  } else {
    curFileName=ret;
  }
  backupLock.unlock();
#endif
  updateWindowTitle();
  pushRecentFile(curFileName);
  if (settings.alwaysPlayIntro==2) shortIntro=true;
}

void FurnaceGUI::updateWindowTitle() {
  String title;
  switch (settings.titleBarInfo) {
    case 0:
      title="Furnace";
      break;
    case 1:
      if (e->song.name.empty()) {
        title="Furnace";
      } else {
        title=fmt::sprintf("%s - Furnace",e->song.name);
      }
      break;
    case 2:
      if (curFileName.empty()) {
        title="Furnace";
      } else {
        String shortName;
        size_t pos=curFileName.rfind(DIR_SEPARATOR);
        if (pos==String::npos) {
          shortName=curFileName;
        } else {
          shortName=curFileName.substr(pos+1);
        }
        title=fmt::sprintf("%s - Furnace",shortName);
      }
      break;
    case 3:
      if (curFileName.empty()) {
        title="Furnace";
      } else {
        title=fmt::sprintf("%s - Furnace",curFileName);
      }
      break;
  }

  if (settings.titleBarSys) {
    if (e->song.systemName!="") {
      title+=fmt::sprintf(" (%s)",e->song.systemName);
    }
  }

  if (sdlWin!=NULL) SDL_SetWindowTitle(sdlWin,title.c_str());
}

void FurnaceGUI::autoDetectSystem() {
  std::map<DivSystem,int> sysCountMap;
  std::map<DivSystem,DivConfig> sysConfMap;
  for (int i=0; i<e->song.systemLen; i++) {
    auto it=sysCountMap.find(e->song.system[i]);
    if (it==sysCountMap.cend()) {
      sysCountMap[e->song.system[i]]=1;
    } else {
      it->second++;
    }
    sysConfMap[e->song.system[i]]=e->song.systemFlags[i];
  }

  logV("sysCountMap:");
  for (std::pair<DivSystem,int> k: sysCountMap) {
    logV("%s: %d",e->getSystemName(k.first),k.second);
  }

  bool isMatch=false;
  std::map<DivSystem,int> defCountMap;
  std::map<DivSystem,DivConfig> defConfMap;
  for (FurnaceGUISysCategory& i: sysCategories) {
    for (FurnaceGUISysDef& j: i.systems) {
      defCountMap.clear();
      defConfMap.clear();
      for (FurnaceGUISysDefChip& k: j.orig) {
        auto it=defCountMap.find(k.sys);
        if (it==defCountMap.cend()) {
          defCountMap[k.sys]=1;
        } else {
          it->second++;
        }
        DivConfig dc;
        dc.loadFromMemory(k.flags);
        defConfMap[k.sys]=dc;
      }
      if (defCountMap.size()!=sysCountMap.size()) continue;
      isMatch=true;
      /*logV("trying on defCountMap: %s",j.name);
      for (std::pair<DivSystem,int> k: defCountMap) {
        logV("- %s: %d",e->getSystemName(k.first),k.second);
      }*/
      for (std::pair<DivSystem,int> k: defCountMap) {
        auto countI=sysCountMap.find(k.first);
        if (countI==sysCountMap.cend()) {
          isMatch=false;
          break;
        } else if (countI->second!=k.second) {
          isMatch=false;
          break;
        }

        auto confI=sysConfMap.find(k.first);
        if (confI==sysConfMap.cend()) {
          isMatch=false;
          break;
        }
        DivConfig& sysDC=confI->second;
        auto defConfI=defConfMap.find(k.first);
        if (defConfI==defConfMap.cend()) {
          isMatch=false;
          break;
        }
        for (std::pair<String,String> l: defConfI->second.configMap()) {
          if (!sysDC.has(l.first)) {
            isMatch=false;
            break;
          }
          if (sysDC.getString(l.first,"")!=l.second) {
            isMatch=false;
            break;
          }
        }
        if (!isMatch) break;
      }
      if (isMatch) {
        logV("match found!");
        e->song.systemName=j.name;
        break;
      }
    }
    if (isMatch) break;
  }

  if (!isMatch) {
    bool isFirst=true;
    e->song.systemName="";
    for (std::pair<DivSystem,int> k: sysCountMap) {
      if (!isFirst) e->song.systemName+=" + ";
      if (k.second>1) {
        e->song.systemName+=fmt::sprintf("%d×",k.second);
      }
      e->song.systemName+=e->getSystemName(k.first);
      isFirst=false;
    }
  }
}

ImVec4 FurnaceGUI::channelColor(int ch) {
  switch (settings.channelColors) {
    case 0:
      return uiColors[GUI_COLOR_CHANNEL_BG];
      break;
    case 1:
      return uiColors[GUI_COLOR_CHANNEL_FM+e->getChannelType(ch)];
      break;
    case 2:
      return uiColors[GUI_COLOR_INSTR_STD+e->getPreferInsType(ch)];
      break;
  }
  // invalid
  return uiColors[GUI_COLOR_TEXT];
}

ImVec4 FurnaceGUI::channelTextColor(int ch) {
  switch (settings.channelTextColors) {
    case 0:
      return uiColors[GUI_COLOR_CHANNEL_FG];
      break;
    case 1:
      return uiColors[GUI_COLOR_CHANNEL_FM+e->getChannelType(ch)];
      break;
    case 2:
      return uiColors[GUI_COLOR_INSTR_STD+e->getPreferInsType(ch)];
      break;
  }
  // invalid
  return uiColors[GUI_COLOR_TEXT];
}

const char* defaultLayout="[Window][DockSpaceViewport_11111111]\n\
Pos=0,24\n\
Size=1280,776\n\
Collapsed=0\n\
\n\
[Window][Debug##Default]\n\
Pos=54,43\n\
Size=400,400\n\
Collapsed=0\n\
\n\
[Window][Play/Edit Controls]\n\
Pos=181,208\n\
Size=45,409\n\
Collapsed=0\n\
\n\
[Window][Song Information]\n\
Pos=978,24\n\
Size=302,179\n\
Collapsed=0\n\
DockId=0x0000000F,0\n\
\n\
[Window][Orders]\n\
Pos=0,24\n\
Size=345,217\n\
Collapsed=0\n\
DockId=0x00000007,0\n\
\n\
[Window][Instruments]\n\
Pos=653,24\n\
Size=323,217\n\
Collapsed=0\n\
DockId=0x00000006,0\n\
\n\
[Window][Wavetables]\n\
Pos=653,24\n\
Size=323,217\n\
Collapsed=0\n\
DockId=0x00000006,1\n\
\n\
[Window][Samples]\n\
Pos=653,24\n\
Size=323,217\n\
Collapsed=0\n\
DockId=0x00000006,2\n\
\n\
[Window][Pattern]\n\
Pos=0,243\n\
Size=939,557\n\
Collapsed=0\n\
DockId=0x00000017,0\n\
\n\
[Window][Instrument Editor]\n\
Pos=229,126\n\
Size=856,652\n\
Collapsed=0\n\
\n\
[Window][Warning]\n\
Pos=481,338\n\
Size=264,86\n\
Collapsed=0\n\
\n\
[Window][Sample Editor]\n\
Pos=47,216\n\
Size=1075,525\n\
Collapsed=0\n\
\n\
[Window][About Furnace]\n\
Size=1280,755\n\
Collapsed=0\n\
\n\
[Window][Save File##FileDialog]\n\
Pos=340,177\n\
Size=600,400\n\
Collapsed=0\n\
\n\
[Window][Wavetable Editor]\n\
Pos=253,295\n\
Size=748,378\n\
Collapsed=0\n\
\n\
[Window][Settings]\n\
Pos=655,224\n\
Size=1280,941\n\
Collapsed=0\n\
\n\
[Window][Error]\n\
Pos=491,342\n\
Size=514,71\n\
Collapsed=0\n\
\n\
[Window][Mixer]\n\
Pos=429,198\n\
Size=453,355\n\
Collapsed=0\n\
\n\
[Window][Oscilloscope]\n\
Pos=347,94\n\
Size=304,105\n\
Collapsed=0\n\
DockId=0x0000000E,0\n\
\n\
[Window][Volume Meter]\n\
Pos=1248,243\n\
Size=32,557\n\
Collapsed=0\n\
DockId=0x0000000C,0\n\
\n\
[Window][Debug]\n\
Pos=113,148\n\
Size=945,473\n\
Collapsed=0\n\
\n\
[Window][Load Sample##FileDialog]\n\
Pos=40,0\n\
Size=1200,755\n\
Collapsed=0\n\
\n\
[Window][Open File##FileDialog]\n\
Pos=250,143\n\
Size=779,469\n\
Collapsed=0\n\
\n\
[Window][Export Audio##FileDialog]\n\
Pos=339,177\n\
Size=601,400\n\
Collapsed=0\n\
\n\
[Window][Rendering...]\n\
Pos=585,342\n\
Size=114,71\n\
Collapsed=0\n\
\n\
[Window][Export VGM##FileDialog]\n\
Pos=340,177\n\
Size=600,400\n\
Collapsed=0\n\
\n\
[Window][Warning##Save FileFileDialogOverWriteDialog]\n\
Pos=390,351\n\
Size=500,71\n\
Collapsed=0\n\
\n\
[Window][Statistics]\n\
Pos=0,581\n\
Size=1246,219\n\
Collapsed=0\n\
DockId=0x00000016,0\n\
\n\
[Window][Warning##Export VGMFileDialogOverWriteDialog]\n\
Pos=390,351\n\
Size=500,71\n\
Collapsed=0\n\
\n\
[Window][Compatibility Flags]\n\
Pos=388,132\n\
Size=580,641\n\
Collapsed=0\n\
\n\
[Window][Song Comments]\n\
Pos=60,60\n\
Size=395,171\n\
Collapsed=0\n\
\n\
[Window][Warning##Export AudioFileDialogOverWriteDialog]\n\
Pos=381,351\n\
Size=500,71\n\
Collapsed=0\n\
\n\
[Window][Select Font##FileDialog]\n\
Pos=340,177\n\
Size=600,400\n\
Collapsed=0\n\
\n\
[Window][Channels]\n\
Pos=60,60\n\
Size=368,449\n\
Collapsed=0\n\
\n\
[Window][Register View]\n\
Pos=829,243\n\
Size=417,557\n\
Collapsed=0\n\
DockId=0x00000014,0\n\
\n\
[Window][New Song]\n\
Pos=267,110\n\
Size=746,534\n\
Collapsed=0\n\
\n\
[Window][Edit Controls]\n\
Pos=347,24\n\
Size=304,68\n\
Collapsed=0\n\
DockId=0x0000000D,0\n\
\n\
[Window][Play Controls]\n\
Pos=347,201\n\
Size=304,40\n\
Collapsed=0\n\
DockId=0x0000000A,0\n\
\n\
[Window][Subsongs]\n\
Pos=978,24\n\
Size=302,217\n\
Collapsed=0\n\
DockId=0x00000010,1\n\
\n\
[Window][Oscilloscope (per-channel)]\n\
Pos=1095,243\n\
Size=151,557\n\
Collapsed=0\n\
DockId=0x00000012,0\n\
\n\
[Window][Piano]\n\
Pos=177,669\n\
Size=922,118\n\
Collapsed=0\n\
\n\
[Window][Log Viewer]\n\
Pos=60,60\n\
Size=541,637\n\
Collapsed=0\n\
\n\
[Window][Pattern Manager]\n\
Pos=60,60\n\
Size=1099,366\n\
Collapsed=0\n\
\n\
[Window][Chip Manager]\n\
Pos=60,60\n\
Size=490,407\n\
Collapsed=0\n\
\n\
[Window][Speed]\n\
Pos=978,24\n\
Size=302,217\n\
Collapsed=0\n\
DockId=0x00000010,2\n\
\n\
[Window][Song Info##Song Information]\n\
Pos=978,24\n\
Size=302,217\n\
Collapsed=0\n\
DockId=0x00000010,0\n\
\n\
[Window][Effect List]\n\
Pos=941,243\n\
Size=305,557\n\
Collapsed=0\n\
DockId=0x00000018,0\n\
\n\
[Window][Intro]\n\
Pos=0,0\n\
Size=2560,1600\n\
Collapsed=0\n\
\n\
[Window][Welcome]\n\
Pos=944,666\n\
Size=672,268\n\
Collapsed=0\n\
\n\
[Window][Grooves]\n\
Pos=416,314\n\
Size=463,250\n\
Collapsed=0\n\
\n\
[Window][Clock]\n\
Pos=60,60\n\
Size=145,184\n\
Collapsed=0\n\
\n\
[Docking][Data]\n\
DockSpace             ID=0x8B93E3BD Window=0xA787BDB4 Pos=0,24 Size=1280,776 Split=Y Selected=0x6C01C512\n\
  DockNode            ID=0x00000001 Parent=0x8B93E3BD SizeRef=1280,217 Split=X Selected=0xF3094A52\n\
    DockNode          ID=0x00000003 Parent=0x00000001 SizeRef=976,231 Split=X Selected=0x65CC51DC\n\
      DockNode        ID=0x00000007 Parent=0x00000003 SizeRef=345,231 HiddenTabBar=1 Selected=0x8F5BFC9A\n\
      DockNode        ID=0x00000008 Parent=0x00000003 SizeRef=629,231 Split=X Selected=0xD2AD486B\n\
        DockNode      ID=0x00000005 Parent=0x00000008 SizeRef=304,406 Split=Y Selected=0x6D682373\n\
          DockNode    ID=0x00000009 Parent=0x00000005 SizeRef=292,175 Split=Y Selected=0x6D682373\n\
            DockNode  ID=0x0000000D Parent=0x00000009 SizeRef=292,68 HiddenTabBar=1 Selected=0xE57B1A9D\n\
            DockNode  ID=0x0000000E Parent=0x00000009 SizeRef=292,105 HiddenTabBar=1 Selected=0x6D682373\n\
          DockNode    ID=0x0000000A Parent=0x00000005 SizeRef=292,40 HiddenTabBar=1 Selected=0x0DE44CFF\n\
        DockNode      ID=0x00000006 Parent=0x00000008 SizeRef=323,406 Selected=0xB75D68C7\n\
    DockNode          ID=0x00000004 Parent=0x00000001 SizeRef=302,231 Split=Y Selected=0x60B9D088\n\
      DockNode        ID=0x0000000F Parent=0x00000004 SizeRef=302,179 Selected=0x60B9D088\n\
      DockNode        ID=0x00000010 Parent=0x00000004 SizeRef=302,36 Selected=0x82BEE2E5\n\
  DockNode            ID=0x00000002 Parent=0x8B93E3BD SizeRef=1280,512 Split=X Selected=0x6C01C512\n\
    DockNode          ID=0x0000000B Parent=0x00000002 SizeRef=1246,503 Split=X Selected=0xB9ADD0D5\n\
      DockNode        ID=0x00000011 Parent=0x0000000B SizeRef=1093,557 Split=X Selected=0xB9ADD0D5\n\
        DockNode      ID=0x00000013 Parent=0x00000011 SizeRef=827,557 Split=Y Selected=0xB9ADD0D5\n\
          DockNode    ID=0x00000015 Parent=0x00000013 SizeRef=1246,336 Split=X Selected=0xB9ADD0D5\n\
            DockNode  ID=0x00000017 Parent=0x00000015 SizeRef=939,557 CentralNode=1 HiddenTabBar=1 Selected=0xB9ADD0D5\n\
            DockNode  ID=0x00000018 Parent=0x00000015 SizeRef=305,557 Selected=0xB94874DD\n\
          DockNode    ID=0x00000016 Parent=0x00000013 SizeRef=1246,219 Selected=0xAD8E88F2\n\
        DockNode      ID=0x00000014 Parent=0x00000011 SizeRef=417,557 Selected=0x425428FB\n\
      DockNode        ID=0x00000012 Parent=0x0000000B SizeRef=151,557 HiddenTabBar=1 Selected=0x4C07BC58\n\
    DockNode          ID=0x0000000C Parent=0x00000002 SizeRef=32,503 HiddenTabBar=1 Selected=0x644DA2C1\n";


void FurnaceGUI::prepareLayout() {
  FILE* check;
  check=ps_fopen(finalLayoutPath,"r");
  if (check!=NULL) {
    fclose(check);
    return;
  }

  // copy initial layout
  logI("loading default layout.");
  check=ps_fopen(finalLayoutPath,"w");
  if (check==NULL) {
    logW("could not write default layout!");
    return;
  }

  fwrite(defaultLayout,1,strlen(defaultLayout),check);
  fclose(check);
}

float FurnaceGUI::calcBPM(const DivGroovePattern& speeds, float hz, int vN, int vD) {
  float hl=e->curSubSong->hilightA;
  if (hl<=0.0f) hl=4.0f;
  float timeBase=e->curSubSong->timeBase+1;
  float speedSum=0;
  for (int i=0; i<MIN(16,speeds.len); i++) {
    speedSum+=speeds.val[i];
  }
  speedSum/=MAX(1,speeds.len);
  if (timeBase<1.0f) timeBase=1.0f;
  if (speedSum<1.0f) speedSum=1.0f;
  if (vD<1) vD=1;
  return (60.0f*hz/(timeBase*hl*speedSum))*(float)vN/(float)vD;
}

void FurnaceGUI::play(int row) {
  memset(chanOscVol,0,DIV_MAX_CHANS*sizeof(float));
  memset(chanOscPitch,0,DIV_MAX_CHANS*sizeof(float));
  memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float));
  e->walkSong(loopOrder,loopRow,loopEnd);
  memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS);
  if (!followPattern) e->setOrder(curOrder);
  if (row>0) {
    e->playToRow(row);
  } else {
    e->play();
  }
  curNibble=false;
  orderNibble=false;
  activeNotes.clear();
}

void FurnaceGUI::setOrder(unsigned char order, bool forced) {
  curOrder=order;
  if (followPattern || forced) {
    e->setOrder(order);
  }
}

void FurnaceGUI::stop() {
  e->walkSong(loopOrder,loopRow,loopEnd);
  e->stop();
  curNibble=false;
  orderNibble=false;
}

void FurnaceGUI::previewNote(int refChan, int note, bool autoNote) {
  e->setMidiBaseChan(refChan);
  e->synchronized([this,note]() {
    e->autoNoteOn(-1,curIns,note);
  });
}

void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode, bool autoNote) {
  auto it=noteKeys.find(scancode);
  if (it!=noteKeys.cend()) {
    int key=it->second;
    int num=12*curOctave+key;
    if (num<-60) num=-60; // C-(-5)
    if (num>119) num=119; // B-9

    if (key==100) return;
    if (key==101) return;
    if (key==102) return;

    e->synchronized([this,num]() {
      e->autoNoteOff(-1,num);
    });
  }
}

void FurnaceGUI::noteInput(int num, int key, int vol) {
  DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],true);
  bool removeIns=false;

  prepareUndo(GUI_UNDO_PATTERN_EDIT);

  if (key==GUI_NOTE_OFF) { // note off
    pat->data[cursor.y][0]=100;
    pat->data[cursor.y][1]=0;
    removeIns=true;
  } else if (key==GUI_NOTE_OFF_RELEASE) { // note off + env release
    pat->data[cursor.y][0]=101;
    pat->data[cursor.y][1]=0;
    removeIns=true;
  } else if (key==GUI_NOTE_RELEASE) { // env release only
    pat->data[cursor.y][0]=102;
    pat->data[cursor.y][1]=0;
    removeIns=true;
  } else {
    pat->data[cursor.y][0]=num%12;
    pat->data[cursor.y][1]=num/12;
    if (pat->data[cursor.y][0]==0) {
      pat->data[cursor.y][0]=12;
      pat->data[cursor.y][1]--;
    }
    pat->data[cursor.y][1]=(unsigned char)pat->data[cursor.y][1];
    if (latchIns==-2) {
      if (curIns>=(int)e->song.ins.size()) curIns=-1;
      if (curIns>=0) {
        pat->data[cursor.y][2]=curIns;
      }
    } else if (latchIns!=-1 && !e->song.ins.empty()) {
      pat->data[cursor.y][2]=MIN(((int)e->song.ins.size())-1,latchIns);
    }
    int maxVol=e->getMaxVolumeChan(cursor.xCoarse);
    if (latchVol!=-1) {
      pat->data[cursor.y][3]=MIN(maxVol,latchVol);
    } else if (vol!=-1) {
      pat->data[cursor.y][3]=(vol*maxVol)/127;
    }
    if (latchEffect!=-1) pat->data[cursor.y][4]=latchEffect;
    if (latchEffectVal!=-1) pat->data[cursor.y][5]=latchEffectVal;
  }
  if (removeIns) {
    if (settings.removeInsOff) {
      pat->data[cursor.y][2]=-1;
    }
    if (settings.removeVolOff) {
      pat->data[cursor.y][3]=-1;
    }
  }
  makeUndo(GUI_UNDO_PATTERN_EDIT);
  editAdvance();
  curNibble=false;
}

void FurnaceGUI::valueInput(int num, bool direct, int target) {
  DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],true);
  prepareUndo(GUI_UNDO_PATTERN_EDIT);
  if (target==-1) target=cursor.xFine+1;
  if (direct) {
    pat->data[cursor.y][target]=num&0xff;
  } else {
    if (pat->data[cursor.y][target]==-1) pat->data[cursor.y][target]=0;
    if (!settings.pushNibble && !curNibble) {
      pat->data[cursor.y][target]=num;
    } else {
      pat->data[cursor.y][target]=((pat->data[cursor.y][target]<<4)|num)&0xff;
    }
  }
  if (cursor.xFine==1) { // instrument
    if (pat->data[cursor.y][target]>=(int)e->song.ins.size()) {
      pat->data[cursor.y][target]&=0x0f;
      if (pat->data[cursor.y][target]>=(int)e->song.ins.size()) {
        pat->data[cursor.y][target]=(int)e->song.ins.size()-1;
      }
    }
    if (settings.absorbInsInput) {
      curIns=pat->data[cursor.y][target];
      wavePreviewInit=true;
      updateFMPreview=true;
    }
    makeUndo(GUI_UNDO_PATTERN_EDIT);
    if (direct) {
      curNibble=false;
    } else {
      if (e->song.ins.size()<16) {
        curNibble=false;
        editAdvance();
      } else {
        curNibble=!curNibble;
        if (!curNibble) editAdvance();
      }
    }
  } else if (cursor.xFine==2) {
    if (curNibble) {
      if (pat->data[cursor.y][target]>e->getMaxVolumeChan(cursor.xCoarse)) pat->data[cursor.y][target]=e->getMaxVolumeChan(cursor.xCoarse);
    } else {
      pat->data[cursor.y][target]&=15;
    }
    makeUndo(GUI_UNDO_PATTERN_EDIT);
    if (direct) {
      curNibble=false;
    } else {
      if (e->getMaxVolumeChan(cursor.xCoarse)<16) {
        curNibble=false;
        editAdvance();
      } else {
        curNibble=!curNibble;
        if (!curNibble) editAdvance();
      }
    }
  } else {
    makeUndo(GUI_UNDO_PATTERN_EDIT);
    if (direct) {
      curNibble=false;
    } else {
      curNibble=!curNibble;
      if (!curNibble) {
        if (!settings.effectCursorDir) {
          editAdvance();
        } else {
          if (settings.effectCursorDir==2) {
            if (++cursor.xFine>=(3+(e->curPat[cursor.xCoarse].effectCols*2))) {
              cursor.xFine=3;
            }
          } else {
            if (cursor.xFine&1) {
              cursor.xFine++;
            } else {
              editAdvance();
              cursor.xFine--;
            }
          }
        }
      }
    }
  }
}

#define changeLatch(x) \
  if (x<0) x=0; \
  if (!latchNibble && !settings.pushNibble) x=0; \
  x=(x<<4)|num; \
  latchNibble=!latchNibble; \
  if (!latchNibble) { \
    if (++latchTarget>4) latchTarget=0; \
  }

void FurnaceGUI::keyDown(SDL_Event& ev) {
  if (introPos<11.0 && !shortIntro) return;
  if (ImGuiFileDialog::Instance()->IsOpened()) return;
  if (aboutOpen) return;

  int mapped=ev.key.keysym.sym;
  if (ev.key.keysym.mod&KMOD_CTRL) {
    mapped|=FURKMOD_CTRL;
  }
  if (ev.key.keysym.mod&KMOD_ALT) {
    mapped|=FURKMOD_ALT;
  }
  if (ev.key.keysym.mod&KMOD_GUI) {
    mapped|=FURKMOD_META;
  }
  if (ev.key.keysym.mod&KMOD_SHIFT) {
    mapped|=FURKMOD_SHIFT;
  }

  if (bindSetActive) {
    if (!ev.key.repeat) {
      switch (ev.key.keysym.sym) {
        case SDLK_LCTRL: case SDLK_RCTRL:
        case SDLK_LALT: case SDLK_RALT:
        case SDLK_LGUI: case SDLK_RGUI:
        case SDLK_LSHIFT: case SDLK_RSHIFT:
          bindSetPending=false;
          actionKeys[bindSetTarget]=(mapped&(~FURK_MASK))|0xffffff;
          break;
        default:
          actionKeys[bindSetTarget]=mapped;
          bindSetActive=false;
          bindSetPending=false;
          bindSetTarget=0;
          bindSetPrevValue=0;
          parseKeybinds();
          break;
      }
    }
    return;
  }

  if (latchTarget) {
    if (mapped==SDLK_DELETE || mapped==SDLK_BACKSPACE) {
      switch (latchTarget) {
        case 1:
          latchIns=-1;
          break;
        case 2:
          latchVol=-1;
          break;
        case 3:
          latchEffect=-1;
          break;
        case 4:
          latchEffectVal=-1;
          break;
      }
    } else {
      auto it=valueKeys.find(ev.key.keysym.sym);
      if (it!=valueKeys.cend()) {
        int num=it->second;
        switch (latchTarget) {
          case 1: // instrument
            changeLatch(latchIns);
            break;
          case 2: // volume
            changeLatch(latchVol);
            break;
          case 3: // effect
            changeLatch(latchEffect);
            break;
          case 4: // effect value
            changeLatch(latchEffectVal);
            break;
        }
      }
    }
    return;
  }

  if (sampleMapWaitingInput) {
    if (sampleMapColumn==1) {
      // TODO: map?
      if (ev.key.keysym.scancode==SDL_SCANCODE_DELETE) {
        alterSampleMap(true,-1);
        return;
      }
      auto it=noteKeys.find(ev.key.keysym.scancode);
      if (it!=noteKeys.cend()) {
        int key=it->second;
        int num=12*curOctave+key;

        if (num<-60) num=-60; // C-(-5)
        if (num>119) num=119; // B-9

        alterSampleMap(true,num);
        return;
      }
    } else {
      // TODO: map?
      if (ev.key.keysym.scancode==SDL_SCANCODE_DELETE) {
        alterSampleMap(false,-1);
        return;
      }
      auto it=valueKeys.find(ev.key.keysym.sym);
      if (it!=valueKeys.cend()) {
        int num=it->second;
        if (num<10) {
          alterSampleMap(false,num);
          return;
        }
      }
    }
  }

  // PER-WINDOW KEYS
  switch (curWindow) {
    case GUI_WINDOW_PATTERN: {
      auto actionI=actionMapPat.find(mapped);
      if (actionI!=actionMapPat.cend()) {
        int action=actionI->second;
        if (action>0) {
          doAction(action);
          return;
        }
      }
      // pattern input otherwise
      if (mapped&(FURKMOD_ALT|FURKMOD_CTRL|FURKMOD_META|FURKMOD_SHIFT)) break;
      if (!ev.key.repeat) {
        if (cursor.xFine==0) { // note
          auto it=noteKeys.find(ev.key.keysym.scancode);
          if (it!=noteKeys.cend()) {
            int key=it->second;
            int num=12*curOctave+key;

            if (num<-60) num=-60; // C-(-5)
            if (num>119) num=119; // B-9

            if (edit) {
              noteInput(num,key);
            }
          }
        } else if (edit) { // value
          auto it=valueKeys.find(ev.key.keysym.sym);
          if (it!=valueKeys.cend()) {
            int num=it->second;
            valueInput(num);
          }
        }
      }
      break;
    }
    case GUI_WINDOW_ORDERS: {
      auto actionI=actionMapOrders.find(mapped);
      if (actionI!=actionMapOrders.cend()) {
        int action=actionI->second;
        if (action>0) {
          doAction(action);
          return;
        }
      }
      // order input otherwise
      if (mapped&(FURKMOD_ALT|FURKMOD_CTRL|FURKMOD_META|FURKMOD_SHIFT)) break;
      if (orderEditMode!=0) {
        auto it=valueKeys.find(ev.key.keysym.sym);
        if (it!=valueKeys.cend()) {
          int num=it->second;
          if (orderCursor>=0 && orderCursor<e->getTotalChannelCount()) {
            prepareUndo(GUI_UNDO_CHANGE_ORDER);
            e->lockSave([this,num]() {
              if (!curNibble && !settings.pushNibble) e->curOrders->ord[orderCursor][curOrder]=0;
              e->curOrders->ord[orderCursor][curOrder]=((e->curOrders->ord[orderCursor][curOrder]<<4)|num);
            });
            MARK_MODIFIED;
            curNibble=!curNibble;
            if (orderEditMode==2 || orderEditMode==3) {
              if (!curNibble) {
                if (orderEditMode==2) {
                  orderCursor++;
                  if (orderCursor>=e->getTotalChannelCount()) orderCursor=0;
                } else if (orderEditMode==3) {
                  if (curOrder<e->curSubSong->ordersLen-1) {
                    setOrder(curOrder+1);
                  }
                }
              }
            }
            e->walkSong(loopOrder,loopRow,loopEnd);
            makeUndo(GUI_UNDO_CHANGE_ORDER);
          }
        }
      }
      break;
    }
    case GUI_WINDOW_SAMPLE_EDIT: {
      auto actionI=actionMapSample.find(mapped);
      if (actionI!=actionMapSample.cend()) {
        int action=actionI->second;
        if (action>0) {
          doAction(action);
          return;
        }
      }
      break;
    }
    case GUI_WINDOW_INS_LIST: {
      auto actionI=actionMapInsList.find(mapped);
      if (actionI!=actionMapInsList.cend()) {
        int action=actionI->second;
        if (action>0) {
          doAction(action);
          return;
        }
      }
      break;
    }
    case GUI_WINDOW_WAVE_LIST: {
      auto actionI=actionMapWaveList.find(mapped);
      if (actionI!=actionMapWaveList.cend()) {
        int action=actionI->second;
        if (action>0) {
          doAction(action);
          return;
        }
      }
      break;
    }
    case GUI_WINDOW_SAMPLE_LIST: {
      auto actionI=actionMapSampleList.find(mapped);
      if (actionI!=actionMapSampleList.cend()) {
        int action=actionI->second;
        if (action>0) {
          doAction(action);
          return;
        }
      }
      break;
    }
    default:
      break;
  }

  // GLOBAL KEYS
  auto actionI=actionMapGlobal.find(mapped);
  if (actionI!=actionMapGlobal.cend()) {
    int action=actionI->second;
    if (action>0) {
      doAction(action);
      return;
    }
  }
}

void FurnaceGUI::keyUp(SDL_Event& ev) {
  // nothing for now
}

bool dirExists(String s) {
  return dirExists(s.c_str());
}

void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
  bool hasOpened=false;
  switch (type) {
    case GUI_FILE_OPEN:
      if (!dirExists(workingDirSong)) workingDirSong=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Open File",
        {"compatible files", "*.fur *.dmf *.mod *.fc13 *.fc14 *.smod *.fc",
         "all files", "*"},
        "compatible files{.fur,.dmf,.mod,.fc13,.fc14,.smod,.fc},.*",
        workingDirSong,
        dpiScale
      );
      break;
    case GUI_FILE_OPEN_BACKUP:
      if (!dirExists(backupPath)) {
        showError("no backups made yet!");
        break;
      }
      hasOpened=fileDialog->openLoad(
        "Restore Backup",
        {"Furnace song", "*.fur"},
        "Furnace song{.fur}",
        backupPath+String(DIR_SEPARATOR_STR),
        dpiScale
      );
      break;
    case GUI_FILE_SAVE:
      if (!dirExists(workingDirSong)) workingDirSong=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save File",
        {"Furnace song", "*.fur"},
        "Furnace song{.fur}",
        workingDirSong,
        dpiScale
      );
      break;
    case GUI_FILE_SAVE_DMF:
      if (!dirExists(workingDirSong)) workingDirSong=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save File",
        {"DefleMask 1.1.3 module", "*.dmf"},
        "DefleMask 1.1.3 module{.dmf}",
        workingDirSong,
        dpiScale
      );
      break;
    case GUI_FILE_SAVE_DMF_LEGACY:
      if (!dirExists(workingDirSong)) workingDirSong=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save File",
        {"DefleMask 1.0/legacy module", "*.dmf"},
        "DefleMask 1.0/legacy module{.dmf}",
        workingDirSong,
        dpiScale
      );
      break;
    case GUI_FILE_INS_OPEN:
    case GUI_FILE_INS_OPEN_REPLACE:
      prevIns=-3;
      if (prevInsData!=NULL) {
        delete prevInsData;
        prevInsData=NULL;
      }
      prevInsData=new DivInstrument;
      *prevInsData=*e->getIns(curIns);
      if (!dirExists(workingDirIns)) workingDirIns=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Load Instrument",
        // TODO supply loadable formats in a dynamic, scalable, "DRY" way.
        // thank the author of IGFD for making things impossible
        {"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", "*"},
        "all compatible files{.fui,.dmp,.tfi,.vgi,.s3i,.sbi,.opli,.opni,.y12,.bnk,.ff,.gyb,.opm,.wopl,.wopn},.*",
        workingDirIns,
        dpiScale,
        [this](const char* path) {
          int sampleCountBefore=e->song.sampleLen;
          std::vector<DivInstrument*> instruments=e->instrumentFromFile(path,false);
          if (!instruments.empty()) {
            if (e->song.sampleLen!=sampleCountBefore) {
              e->renderSamplesP();
            }
            if (curFileDialog==GUI_FILE_INS_OPEN_REPLACE) {
              if (prevIns==-3) {
                prevIns=curIns;
              }
              if (prevIns>=0 && prevIns<=(int)e->song.ins.size()) {
                *e->song.ins[prevIns]=*instruments[0];
              }
            } else {
              e->loadTempIns(instruments[0]);
              if (curIns!=-2) {
                prevIns=curIns;
              }
              curIns=-2;
            }
          }
          for (DivInstrument* i: instruments) delete i;
        },
        (type==GUI_FILE_INS_OPEN)
      );
      break;
    case GUI_FILE_INS_SAVE:
      if (!dirExists(workingDirIns)) workingDirIns=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Instrument",
        {"Furnace instrument", "*.fui"},
        "Furnace instrument{.fui}",
        workingDirIns,
        dpiScale
      );
      break;
    case GUI_FILE_INS_SAVE_DMP:
      if (!dirExists(workingDirIns)) workingDirIns=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Instrument",
        {"DefleMask preset", "*.dmp"},
        "DefleMask preset{.dmp}",
        workingDirIns,
        dpiScale
      );
      break;
    case GUI_FILE_WAVE_OPEN:
    case GUI_FILE_WAVE_OPEN_REPLACE:
      if (!dirExists(workingDirWave)) workingDirWave=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Load Wavetable",
        {"compatible files", "*.fuw *.dmw",
         "all files", "*"},
        "compatible files{.fuw,.dmw},.*",
        workingDirWave,
        dpiScale,
        NULL, // TODO
        (type==GUI_FILE_WAVE_OPEN)
      );
      break;
    case GUI_FILE_WAVE_SAVE:
      if (!dirExists(workingDirWave)) workingDirWave=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Wavetable",
        {"Furnace wavetable", ".fuw"},
        "Furnace wavetable{.fuw}",
        workingDirWave,
        dpiScale
      );
      break;
    case GUI_FILE_WAVE_SAVE_DMW:
      if (!dirExists(workingDirWave)) workingDirWave=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Wavetable",
        {"DefleMask wavetable", ".dmw"},
        "DefleMask wavetable{.dmw}",
        workingDirWave,
        dpiScale
      );
      break;
    case GUI_FILE_WAVE_SAVE_RAW:
      if (!dirExists(workingDirWave)) workingDirWave=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Wavetable",
        {"raw data", ".raw"},
        "raw data{.raw}",
        workingDirWave,
        dpiScale
      );
      break;
    case GUI_FILE_SAMPLE_OPEN:
    case GUI_FILE_SAMPLE_OPEN_REPLACE:
      if (!dirExists(workingDirSample)) workingDirSample=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Load Sample",
        {"compatible files", "*.wav *.dmc *.brr",
         "all files", "*"},
        "compatible files{.wav,.dmc,.brr},.*",
        workingDirSample,
        dpiScale,
        NULL, // TODO
        (type==GUI_FILE_SAMPLE_OPEN)
      );
      break;
    case GUI_FILE_SAMPLE_OPEN_RAW:
    case GUI_FILE_SAMPLE_OPEN_REPLACE_RAW:
      if (!dirExists(workingDirSample)) workingDirSample=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Load Raw Sample",
        {"all files", "*"},
        ".*",
        workingDirSample,
        dpiScale
      );
      break;
    case GUI_FILE_SAMPLE_SAVE:
      if (!dirExists(workingDirSample)) workingDirSample=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Sample",
        {"Wave file", "*.wav"},
        "Wave file{.wav}",
        workingDirSample,
        dpiScale
      );
      break;
    case GUI_FILE_SAMPLE_SAVE_RAW:
      if (!dirExists(workingDirSample)) workingDirSample=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Load Raw Sample",
        {"all files", "*"},
        ".*",
        workingDirSample,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_AUDIO_ONE:
      if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Audio",
        {"Wave file", "*.wav"},
        "Wave file{.wav}",
        workingDirAudioExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_AUDIO_PER_SYS:
      if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Audio",
        {"Wave file", "*.wav"},
        "Wave file{.wav}",
        workingDirAudioExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL:
      if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Audio",
        {"Wave file", "*.wav"},
        "Wave file{.wav}",
        workingDirAudioExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_VGM:
      if (!dirExists(workingDirVGMExport)) workingDirVGMExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export VGM",
        {"VGM file", "*.vgm"},
        "VGM file{.vgm}",
        workingDirVGMExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_ZSM:
      if (!dirExists(workingDirZSMExport)) workingDirZSMExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export ZSM",
        {"ZSM file", "*.zsm"},
        "ZSM file{.zsm}",
        workingDirZSMExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_CMDSTREAM:
      if (!dirExists(workingDirROMExport)) workingDirROMExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Command Stream",
        {"text file", "*.txt"},
        "text file{.txt}",
        workingDirROMExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_CMDSTREAM_BINARY:
      if (!dirExists(workingDirROMExport)) workingDirROMExport=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Command Stream",
        {"binary file", "*.bin"},
        "binary file{.bin}",
        workingDirROMExport,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_ROM:
      showError("Coming soon!");
      break;
    case GUI_FILE_LOAD_MAIN_FONT:
      if (!dirExists(workingDirFont)) workingDirFont=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Select Font",
        {"compatible files", "*.ttf *.otf *.ttc"},
        "compatible files{.ttf,.otf,.ttc}",
        workingDirFont,
        dpiScale
      );
      break;
    case GUI_FILE_LOAD_HEAD_FONT:
      if (!dirExists(workingDirFont)) workingDirFont=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Select Font",
        {"compatible files", "*.ttf *.otf *.ttc"},
        "compatible files{.ttf,.otf,.ttc}",
        workingDirFont,
        dpiScale
      );
      break;
    case GUI_FILE_LOAD_PAT_FONT:
      if (!dirExists(workingDirFont)) workingDirFont=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Select Font",
        {"compatible files", "*.ttf *.otf *.ttc"},
        "compatible files{.ttf,.otf,.ttc}",
        workingDirFont,
        dpiScale
      );
      break;
    case GUI_FILE_IMPORT_COLORS:
      if (!dirExists(workingDirColors)) workingDirColors=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Select Color File",
        {"configuration files", "*.cfgc"},
        "configuration files{.cfgc}",
        workingDirColors,
        dpiScale
      );
      break;
    case GUI_FILE_IMPORT_KEYBINDS:
      if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Select Keybind File",
        {"configuration files", "*.cfgk"},
        "configuration files{.cfgk}",
        workingDirKeybinds,
        dpiScale
      );
      break;
    case GUI_FILE_IMPORT_LAYOUT:
      if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Select Layout File",
        {".ini files", "*.ini"},
        ".ini files{.ini}",
        workingDirKeybinds,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_COLORS:
      if (!dirExists(workingDirColors)) workingDirColors=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Colors",
        {"configuration files", "*.cfgc"},
        "configuration files{.cfgc}",
        workingDirColors,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_KEYBINDS:
      if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Keybinds",
        {"configuration files", "*.cfgk"},
        "configuration files{.cfgk}",
        workingDirKeybinds,
        dpiScale
      );
      break;
    case GUI_FILE_EXPORT_LAYOUT:
      if (!dirExists(workingDirKeybinds)) workingDirKeybinds=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Export Layout",
        {".ini files", "*.ini"},
        ".ini files{.ini}",
        workingDirKeybinds,
        dpiScale
      );
      break;
    case GUI_FILE_YRW801_ROM_OPEN:
    case GUI_FILE_TG100_ROM_OPEN:
    case GUI_FILE_MU5_ROM_OPEN:
      if (!dirExists(workingDirSample)) workingDirSample=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Load ROM",
        {"compatible files", "*.rom *.bin",
         "all files", "*"},
        "compatible files{.rom,.bin},.*",
        workingDirROM,
        dpiScale
      );
      break;
    case GUI_FILE_CMDSTREAM_OPEN:
      if (!dirExists(workingDirROM)) workingDirROM=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Play Command Stream",
        {"command stream", "*.bin",
         "all files", "*"},
        "command stream{.bin},.*",
        workingDirROM,
        dpiScale
      );
      break;
    case GUI_FILE_TEST_OPEN:
      if (!dirExists(workingDirTest)) workingDirTest=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Open Test",
        {"compatible files", "*.fur *.dmf *.mod",
         "another option", "*.wav *.ttf",
         "all files", "*"},
        "compatible files{.fur,.dmf,.mod},another option{.wav,.ttf},.*",
        workingDirTest,
        dpiScale,
        [](const char* path) {
          if (path!=NULL) {
            logI("Callback Result: %s",path);
          } else {
            logI("Callback Result: NULL");
          }
        }
      );
      break;
    case GUI_FILE_TEST_OPEN_MULTI:
      if (!dirExists(workingDirTest)) workingDirTest=getHomeDir();
      hasOpened=fileDialog->openLoad(
        "Open Test (Multi)",
        {"compatible files", "*.fur *.dmf *.mod",
         "another option", "*.wav *.ttf",
         "all files", "*"},
        "compatible files{.fur,.dmf,.mod},another option{.wav,.ttf},.*",
        workingDirTest,
        dpiScale,
        [](const char* path) {
          if (path!=NULL) {
            logI("Callback Result: %s",path);
          } else {
            logI("Callback Result: NULL");
          }
        },
        true
      );
      break;
    case GUI_FILE_TEST_SAVE:
      if (!dirExists(workingDirTest)) workingDirTest=getHomeDir();
      hasOpened=fileDialog->openSave(
        "Save Test",
        {"Furnace song", "*.fur",
         "DefleMask module", "*.dmf"},
        "Furnace song{.fur},DefleMask module{.dmf}",
        workingDirTest,
        dpiScale
      );
      break;
  }
  if (hasOpened) curFileDialog=type;
  //ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_NavEnableKeyboard;
}

int FurnaceGUI::save(String path, int dmfVersion) {
  SafeWriter* w;
  logD("saving file...");
  if (dmfVersion) {
    if (dmfVersion<24) dmfVersion=24;
    w=e->saveDMF(dmfVersion);
  } else {
    w=e->saveFur(false,settings.newPatternFormat);
  }
  if (w==NULL) {
    lastError=e->getLastError();
    logE("couldn't save! %s",lastError);
    return 3;
  }
  logV("opening file for writing...");
  FILE* outFile=ps_fopen(path.c_str(),"wb");
  if (outFile==NULL) {
    lastError=strerror(errno);
    logE("couldn't save! %s",lastError);
    w->finish();
    return 1;
  }
  if (settings.compress) {
    unsigned char zbuf[131072];
    int ret;
    z_stream zl;
    memset(&zl,0,sizeof(z_stream));
    ret=deflateInit(&zl,Z_DEFAULT_COMPRESSION);
    if (ret!=Z_OK) {
      logE("zlib error!");
      lastError="compression error";
      fclose(outFile);
      w->finish();
      return 2;
    }
    zl.avail_in=w->size();
    zl.next_in=w->getFinalBuf();
    while (zl.avail_in>0) {
      zl.avail_out=131072;
      zl.next_out=zbuf;
      if ((ret=deflate(&zl,Z_NO_FLUSH))==Z_STREAM_ERROR) {
        logE("zlib stream error!");
        lastError="zlib stream error";
        deflateEnd(&zl);
        fclose(outFile);
        w->finish();
        return 2;
      }
      size_t amount=131072-zl.avail_out;
      if (amount>0) {
        if (fwrite(zbuf,1,amount,outFile)!=amount) {
          logE("did not write entirely: %s!",strerror(errno));
          lastError=strerror(errno);
          deflateEnd(&zl);
          fclose(outFile);
          w->finish();
          return 1;
        }
      }
    }
    zl.avail_out=131072;
    zl.next_out=zbuf;
    if ((ret=deflate(&zl,Z_FINISH))==Z_STREAM_ERROR) {
      logE("zlib finish stream error!");
      lastError="zlib finish stream error";
      deflateEnd(&zl);
      fclose(outFile);
      w->finish();
      return 2;
    }
    if (131072-zl.avail_out>0) {
      if (fwrite(zbuf,1,131072-zl.avail_out,outFile)!=(131072-zl.avail_out)) {
        logE("did not write entirely: %s!",strerror(errno));
        lastError=strerror(errno);
        deflateEnd(&zl);
        fclose(outFile);
        w->finish();
        return 1;
      }
    }
    deflateEnd(&zl);
  } else {
    if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) {
      logE("did not write entirely: %s!",strerror(errno));
      lastError=strerror(errno);
      fclose(outFile);
      w->finish();
      return 1;
    }
  }
  fclose(outFile);
  w->finish();
  backupLock.lock();
  curFileName=path;
  backupLock.unlock();
  modified=false;
  updateWindowTitle();
  if (!e->getWarnings().empty()) {
    showWarning(e->getWarnings(),GUI_WARN_GENERIC);
  }
  pushRecentFile(path);
  logD("save complete.");
  return 0;
}

int FurnaceGUI::load(String path) {
  bool wasPlaying=e->isPlaying();
  if (!path.empty()) {
    logI("loading module...");
    FILE* f=ps_fopen(path.c_str(),"rb");
    if (f==NULL) {
      perror("error");
      lastError=strerror(errno);
      return 1;
    }
    if (fseek(f,0,SEEK_END)<0) {
      perror("size error");
      lastError=fmt::sprintf("on seek: %s",strerror(errno));
      fclose(f);
      return 1;
    }
    ssize_t len=ftell(f);
    if (len==(SIZE_MAX>>1)) {
      perror("could not get file length");
      lastError=fmt::sprintf("on pre tell: %s",strerror(errno));
      fclose(f);
      return 1;
    }
    if (len<1) {
      if (len==0) {
        logE("that file is empty!");
        lastError="file is empty";
      } else {
        perror("tell error");
        lastError=fmt::sprintf("on tell: %s",strerror(errno));
      }
      fclose(f);
      return 1;
    }
    if (fseek(f,0,SEEK_SET)<0) {
      perror("size error");
      lastError=fmt::sprintf("on get size: %s",strerror(errno));
      fclose(f);
      return 1;
    }
    unsigned char* file=new unsigned char[len];
    if (fread(file,1,(size_t)len,f)!=(size_t)len) {
      perror("read error");
      lastError=fmt::sprintf("on read: %s",strerror(errno));
      fclose(f);
      delete[] file;
      return 1;
    }
    fclose(f);
    if (!e->load(file,(size_t)len)) {
      lastError=e->getLastError();
      logE("could not open file!");
      return 1;
    }
  }
  backupLock.lock();
  curFileName=path;
  backupLock.unlock();
  modified=false;
  curNibble=false;
  orderNibble=false;
  orderCursor=-1;
  curOrder=0;
  oldRow=0;
  samplePos=0;
  updateSampleTex=true;
  selStart=SelectionPoint();
  selEnd=SelectionPoint();
  cursor=SelectionPoint();
  lastError="everything OK";
  undoHist.clear();
  redoHist.clear();
  updateWindowTitle();
  updateScroll(0);
  if (!e->getWarnings().empty()) {
    showWarning(e->getWarnings(),GUI_WARN_GENERIC);
  }
  pushRecentFile(path);
  // do not auto-play a backup
  if (path.find(backupPath)!=0) {
    if (settings.playOnLoad==2 || (settings.playOnLoad==1 && wasPlaying)) {
      play();
    }
  }
  return 0;
}

void FurnaceGUI::pushRecentFile(String path) {
  if (path.empty()) return;
  if (path.find(backupPath)==0) return;
  for (int i=0; i<(int)recentFile.size(); i++) {
    if (recentFile[i]==path) {
      recentFile.erase(recentFile.begin()+i);
      i--;
    }
  }
  recentFile.push_front(path);

  while (!recentFile.empty() && (int)recentFile.size()>settings.maxRecentFile) {
    recentFile.pop_back();
  }
}

void FurnaceGUI::delFirstBackup(String name) {
  std::vector<String> listOfFiles;
#ifdef _WIN32
  String findPath=backupPath+String(DIR_SEPARATOR_STR)+name+String("*.fur");
  WIN32_FIND_DATAW next;
  HANDLE backDir=FindFirstFileW(utf8To16(findPath.c_str()).c_str(),&next);
  if (backDir!=INVALID_HANDLE_VALUE) {
    do {
      listOfFiles.push_back(utf16To8(next.cFileName));
    } while (FindNextFileW(backDir,&next)!=0);
    FindClose(backDir);
  }
#else
  DIR* backDir=opendir(backupPath.c_str());
  if (backDir==NULL) {
    logW("could not open backups dir!");
    return;
  }
  while (true) {
    struct dirent* next=readdir(backDir);
    if (next==NULL) break;
    if (strstr(next->d_name,name.c_str())!=next->d_name) continue;
    listOfFiles.push_back(String(next->d_name));
  }
  closedir(backDir);
#endif

  std::sort(listOfFiles.begin(),listOfFiles.end(),[](const String& a, const String& b) -> bool {
    return strcmp(a.c_str(),b.c_str())<0;
  });

  int totalDelete=((int)listOfFiles.size())-5;
  for (int i=0; i<totalDelete; i++) {
    String toDelete=backupPath+String(DIR_SEPARATOR_STR)+listOfFiles[i];
    deleteFile(toDelete.c_str());
  }
}

int FurnaceGUI::loadStream(String path) {
  if (!path.empty()) {
    logI("loading stream...");
    FILE* f=ps_fopen(path.c_str(),"rb");
    if (f==NULL) {
      perror("error");
      lastError=strerror(errno);
      return 1;
    }
    if (fseek(f,0,SEEK_END)<0) {
      perror("size error");
      lastError=fmt::sprintf("on seek: %s",strerror(errno));
      fclose(f);
      return 1;
    }
    ssize_t len=ftell(f);
    if (len==(SIZE_MAX>>1)) {
      perror("could not get file length");
      lastError=fmt::sprintf("on pre tell: %s",strerror(errno));
      fclose(f);
      return 1;
    }
    if (len<1) {
      if (len==0) {
        logE("that file is empty!");
        lastError="file is empty";
      } else {
        perror("tell error");
        lastError=fmt::sprintf("on tell: %s",strerror(errno));
      }
      fclose(f);
      return 1;
    }
    if (fseek(f,0,SEEK_SET)<0) {
      perror("size error");
      lastError=fmt::sprintf("on get size: %s",strerror(errno));
      fclose(f);
      return 1;
    }
    unsigned char* file=new unsigned char[len];
    if (fread(file,1,(size_t)len,f)!=(size_t)len) {
      perror("read error");
      lastError=fmt::sprintf("on read: %s",strerror(errno));
      fclose(f);
      delete[] file;
      return 1;
    }
    fclose(f);
    if (!e->playStream(file,(size_t)len)) {
      lastError=e->getLastError();
      logE("could not open file!");
      return 1;
    }
  }
  return 0;
}


void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
  e->saveAudio(path.c_str(),exportLoops+1,mode,exportFadeOut);
  displayExporting=true;
}

void FurnaceGUI::editStr(String* which) {
  editString=which;
  displayEditString=true;
}

void FurnaceGUI::showWarning(String what, FurnaceGUIWarnings type) {
  warnString=what;
  warnAction=type;
  warnQuit=true;
}

void FurnaceGUI::showError(String what) {
  errorString=what;
  displayError=true;
}

String FurnaceGUI::getLastError() {
  return lastError;
}

// what monster did I just create here?
#define B30(tt) (macroDragBit30?((((tt)&0xc0000000)==0x40000000 || ((tt)&0xc0000000)==0x80000000)?0x40000000:0):0)

#define MACRO_DRAG(t) \
  if (macroDragSettingBit30) { \
    if (macroDragLastX!=x || macroDragLastY!=y) { \
      macroDragLastX=x; \
      macroDragLastY=y; \
      if (macroDragInitialValueSet) { \
        if (!macroDragInitialValue) { \
          if (t[x]&0x80000000) { \
            t[x]&=~0x40000000; \
          } else { \
            t[x]|=0x40000000; \
          } \
        } else { \
          if (t[x]&0x80000000) { \
            t[x]|=0x40000000; \
          } else { \
            t[x]&=~0x40000000; \
          } \
        } \
      } else { \
        macroDragInitialValue=(((t[x])&0xc0000000)==0x40000000 || ((t[x])&0xc0000000)==0x80000000); \
        macroDragInitialValueSet=true; \
        t[x]^=0x40000000; \
      } \
    } \
  } else if (macroDragBitMode) { \
    if (macroDragLastX!=x || macroDragLastY!=y) { \
      macroDragLastX=x; \
      macroDragLastY=y; \
      if (macroDragInitialValueSet) { \
        if (macroDragInitialValue) { \
          t[x]=(((t[x]+macroDragBitOff)&((1<<macroDragMax)-1))&(~(1<<y)))-macroDragBitOff; \
        } else { \
          t[x]=(((t[x]+macroDragBitOff)&((1<<macroDragMax)-1))|(1<<y))-macroDragBitOff; \
        } \
      } else { \
        macroDragInitialValue=(((t[x]+macroDragBitOff)&((1<<macroDragMax)-1))&(1<<y)); \
        macroDragInitialValueSet=true; \
        t[x]=(((t[x]+macroDragBitOff)&((1<<macroDragMax)-1))^(1<<y))-macroDragBitOff; \
      } \
      t[x]&=(1<<macroDragMax)-1; \
    } \
  } else { \
    if (macroDragLineMode) { \
      if (!macroDragInitialValueSet) { \
        macroDragLineInitial=ImVec2(x,y); \
        macroDragLineInitialV=ImVec2(dragX,dragY); \
        macroDragInitialValueSet=true; \
        macroDragMouseMoved=false; \
      } else if (!macroDragMouseMoved) { \
        if ((pow(dragX-macroDragLineInitialV.x,2.0)+pow(dragY-macroDragLineInitialV.y,2.0))>=16.0f) { \
          macroDragMouseMoved=true; \
        } \
      } \
      if (macroDragMouseMoved) { \
        if ((int)round(x-macroDragLineInitial.x)==0) { \
          t[x]=B30(t[x])^(int)(macroDragLineInitial.y); \
        } else { \
          if ((int)round(x-macroDragLineInitial.x)<0) { \
            for (int i=0; i<=(int)round(macroDragLineInitial.x-x); i++) { \
              int index=(int)round(x+i); \
              if (index<0) continue; \
              t[index]=B30(t[index])^(int)(y+(macroDragLineInitial.y-y)*((float)i/(float)(macroDragLineInitial.x-x))); \
            } \
          } else { \
            for (int i=0; i<=(int)round(x-macroDragLineInitial.x); i++) { \
              int index=(int)round(i+macroDragLineInitial.x); \
              if (index<0) continue; \
              t[index]=B30(t[index])^(int)(macroDragLineInitial.y+(y-macroDragLineInitial.y)*((float)i/(x-macroDragLineInitial.x))); \
            } \
          } \
        } \
      } \
    } else { \
      t[x]=B30(t[x])^(y); \
    } \
  }

void FurnaceGUI::processDrags(int dragX, int dragY) {
  if (macroDragActive) {
    if (macroDragLen>0) {
      int x=((dragX-macroDragStart.x)*macroDragLen/MAX(1,macroDragAreaSize.x));
      if (x<0) x=0;
      if (x>=macroDragLen) x=macroDragLen-1;
      x+=macroDragScroll;
      int y;
      if (macroDragBitMode) {
        y=(int)(macroDragMax-((dragY-macroDragStart.y)*(double(macroDragMax-macroDragMin)/(double)MAX(1,macroDragAreaSize.y))));
      } else {
        y=round(macroDragMax-((dragY-macroDragStart.y)*(double(macroDragMax-macroDragMin)/(double)MAX(1,macroDragAreaSize.y))));
      }
      if (y>macroDragMax) y=macroDragMax;
      if (y<macroDragMin) y=macroDragMin;
      if (macroDragChar) {
        MACRO_DRAG(macroDragCTarget);
      } else {
        MACRO_DRAG(macroDragTarget);
      }
    }
  }
  if (macroLoopDragActive) {
    if (macroLoopDragLen>0) {
      int x=(dragX-macroLoopDragStart.x)*macroLoopDragLen/MAX(1,macroLoopDragAreaSize.x);
      if (x<0) x=0;
      if (x>=macroLoopDragLen) {
        x=-1;
      } else {
        x+=macroDragScroll;
      }
      *macroLoopDragTarget=x;
    }
  }
  if (waveDragActive) {
    if (waveDragLen>0) {
      int x=(dragX-waveDragStart.x)*waveDragLen/MAX(1,waveDragAreaSize.x);
      if (x<0) x=0;
      if (x>=waveDragLen) x=waveDragLen-1;
      int y=round(waveDragMax-((dragY-waveDragStart.y)*(double(waveDragMax-waveDragMin)/(double)MAX(1,waveDragAreaSize.y))));
      if (y>waveDragMax) y=waveDragMax;
      if (y<waveDragMin) y=waveDragMin;
      waveDragTarget[x]=y;
      notifyWaveChange=true;
      MARK_MODIFIED;
    }
  }
  if (sampleDragActive) {
    int x=samplePos+round(double(dragX-sampleDragStart.x)*sampleZoom);
    int x1=samplePos+round(double(dragX-sampleDragStart.x+1)*sampleZoom);
    if (x<0) x=0;
    if (sampleDragMode) {
      if (x>=(int)sampleDragLen) x=sampleDragLen-1;
    } else {
      if (x>(int)sampleDragLen) x=sampleDragLen;
    }
    if (x1<0) x1=0;
    if (x1>=(int)sampleDragLen) x1=sampleDragLen-1;
    double y=0.5-double(dragY-sampleDragStart.y)/sampleDragAreaSize.y;
    if (sampleDragMode) { // draw
      if (sampleDragTarget) {
        if (sampleDrag16) {
          int val=y*65536;
          if (val<-32768) val=-32768;
          if (val>32767) val=32767;
          for (int i=x; i<=x1; i++) ((short*)sampleDragTarget)[i]=val;
        } else {
          int val=y*256;
          if (val<-128) val=-128;
          if (val>127) val=127;
          for (int i=x; i<=x1; i++) ((signed char*)sampleDragTarget)[i]=val;
        }
        updateSampleTex=true;
      }
    } else { // select
      if (sampleSelStart<0) {
        sampleSelStart=x;
      }
      sampleSelEnd=x;
    }
  }
  if (orderScrollLocked) {
    if (fabs(orderScrollRealOrigin.x-dragX)>2.0f*dpiScale || fabs(orderScrollRealOrigin.y-dragY)>2.0f*dpiScale) orderScrollTolerance=false;
    orderScroll=(orderScrollSlideOrigin-dragX)/(40.0*dpiScale);
    if (orderScroll<0.0f) orderScroll=0.0f;
    if (orderScroll>(float)e->curSubSong->ordersLen-1) orderScroll=e->curSubSong->ordersLen-1;
  }
}

#define checkExtension(x) \
  String lowerCase=fileName; \
  for (char& i: lowerCase) { \
    if (i>='A' && i<='Z') i+='a'-'A'; \
  } \
  if (lowerCase.size()<strlen(x) || lowerCase.rfind(x)!=lowerCase.size()-strlen(x)) { \
    fileName+=x; \
  }

#define checkExtensionDual(x,y,fallback) \
  String lowerCase=fileName; \
  for (char& i: lowerCase) { \
    if (i>='A' && i<='Z') i+='a'-'A'; \
  } \
  if (lowerCase.size()<4 || (lowerCase.rfind(x)!=lowerCase.size()-4 && lowerCase.rfind(y)!=lowerCase.size()-4)) { \
    fileName+=fallback; \
  }

#define checkExtensionTriple(x,y,z,fallback) \
  String lowerCase=fileName; \
  for (char& i: lowerCase) { \
    if (i>='A' && i<='Z') i+='a'-'A'; \
  } \
  if (lowerCase.size()<4 || (lowerCase.rfind(x)!=lowerCase.size()-4 && lowerCase.rfind(y)!=lowerCase.size()-4 && lowerCase.rfind(z)!=lowerCase.size()-4)) { \
    fileName+=fallback; \
  }

#define drawOpMask(m) \
  ImGui::PushFont(patFont); \
  ImGui::PushID("om_" #m); \
  if (ImGui::BeginTable("opMaskTable",5,ImGuiTableFlags_Borders|ImGuiTableFlags_SizingFixedFit|ImGuiTableFlags_NoHostExtendX)) { \
    ImGui::TableNextRow(); \
    ImGui::TableNextColumn(); \
    ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ACTIVE]); \
    if (ImGui::Selectable(m.note?"C-4##opMaskNote":"---##opMaskNote",m.note,ImGuiSelectableFlags_DontClosePopups)) { \
      m.note=!m.note; \
    } \
    ImGui::PopStyleColor(); \
    ImGui::TableNextColumn(); \
    ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]); \
    if (ImGui::Selectable(m.ins?"01##opMaskIns":"--##opMaskIns",m.ins,ImGuiSelectableFlags_DontClosePopups)) { \
      m.ins=!m.ins; \
    } \
    ImGui::PopStyleColor(); \
    ImGui::TableNextColumn(); \
    ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_VOLUME_MAX]); \
    if (ImGui::Selectable(m.vol?"7F##opMaskVol":"--##opMaskVol",m.vol,ImGuiSelectableFlags_DontClosePopups)) { \
      m.vol=!m.vol; \
    } \
    ImGui::PopStyleColor(); \
    ImGui::TableNextColumn(); \
    ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_EFFECT_PITCH]); \
    if (ImGui::Selectable(m.effect?"04##opMaskEffect":"--##opMaskEffect",m.effect,ImGuiSelectableFlags_DontClosePopups)) { \
      m.effect=!m.effect; \
    } \
    ImGui::TableNextColumn(); \
    if (ImGui::Selectable(m.effectVal?"72##opMaskEffectVal":"--##opMaskEffectVal",m.effectVal,ImGuiSelectableFlags_DontClosePopups)) { \
      m.effectVal=!m.effectVal; \
    } \
    ImGui::PopStyleColor(); \
    ImGui::EndTable(); \
  } \
  ImGui::PopID(); \
  ImGui::PopFont();

void FurnaceGUI::editOptions(bool topMenu) {
  char id[4096];
  editOptsVisible=true;

  if (ImGui::MenuItem("cut",BIND_FOR(GUI_ACTION_PAT_CUT))) doCopy(true,true,selStart,selEnd);
  if (ImGui::MenuItem("copy",BIND_FOR(GUI_ACTION_PAT_COPY))) doCopy(false,true,selStart,selEnd);
  if (ImGui::MenuItem("paste",BIND_FOR(GUI_ACTION_PAT_PASTE))) doPaste();
  if (!basicMode) if (ImGui::BeginMenu("paste special...")) {
    if (ImGui::MenuItem("paste mix",BIND_FOR(GUI_ACTION_PAT_PASTE_MIX))) doPaste(GUI_PASTE_MODE_MIX_FG);
    if (ImGui::MenuItem("paste mix (background)",BIND_FOR(GUI_ACTION_PAT_PASTE_MIX_BG))) doPaste(GUI_PASTE_MODE_MIX_BG);
    if (ImGui::BeginMenu("paste with ins (foreground)")) {
      if (e->song.ins.empty()) {
        ImGui::Text("no instruments available");
      }
      for (size_t i=0; i<e->song.ins.size(); i++) {
        snprintf(id,4095,"%.2X: %s",(int)i,e->song.ins[i]->name.c_str());
        if (ImGui::MenuItem(id)) {
          doPaste(GUI_PASTE_MODE_INS_FG,i);
        }
      }
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("paste with ins (background)")) {
      if (e->song.ins.empty()) {
        ImGui::Text("no instruments available");
      }
      for (size_t i=0; i<e->song.ins.size(); i++) {
        snprintf(id,4095,"%.2X: %s",(int)i,e->song.ins[i]->name.c_str());
        if (ImGui::MenuItem(id)) {
          doPaste(GUI_PASTE_MODE_INS_BG,i);
        }
      }
      ImGui::EndMenu();
    }
    if (ImGui::MenuItem("paste flood",BIND_FOR(GUI_ACTION_PAT_PASTE_FLOOD))) doPaste(GUI_PASTE_MODE_FLOOD);
    if (ImGui::MenuItem("paste overflow",BIND_FOR(GUI_ACTION_PAT_PASTE_OVERFLOW))) doPaste(GUI_PASTE_MODE_OVERFLOW);
    ImGui::EndMenu();
  }
  if (ImGui::MenuItem("delete",BIND_FOR(GUI_ACTION_PAT_DELETE))) doDelete();
  if (topMenu) {
    if (ImGui::MenuItem("select all",BIND_FOR(GUI_ACTION_PAT_SELECT_ALL))) doSelectAll();
  }
  ImGui::Separator();

  if (!basicMode) {
    if (ImGui::BeginMenu("operation mask...")) {
      drawOpMask(opMaskDelete);
      ImGui::SameLine();
      ImGui::Text("delete");

      drawOpMask(opMaskPullDelete);
      ImGui::SameLine();
      ImGui::Text("pull delete");

      drawOpMask(opMaskInsert);
      ImGui::SameLine();
      ImGui::Text("insert");

      drawOpMask(opMaskPaste);
      ImGui::SameLine();
      ImGui::Text("paste");

      drawOpMask(opMaskTransposeNote);
      ImGui::SameLine();
      ImGui::Text("transpose (note)");

      drawOpMask(opMaskTransposeValue);
      ImGui::SameLine();
      ImGui::Text("transpose (value)");

      drawOpMask(opMaskInterpolate);
      ImGui::SameLine();
      ImGui::Text("interpolate");

      drawOpMask(opMaskFade);
      ImGui::SameLine();
      ImGui::Text("fade");

      drawOpMask(opMaskInvertVal);
      ImGui::SameLine();
      ImGui::Text("invert values");

      drawOpMask(opMaskScale);
      ImGui::SameLine();
      ImGui::Text("scale");

      drawOpMask(opMaskRandomize);
      ImGui::SameLine();
      ImGui::Text("randomize");

      drawOpMask(opMaskFlip);
      ImGui::SameLine();
      ImGui::Text("flip");

      drawOpMask(opMaskCollapseExpand);
      ImGui::SameLine();
      ImGui::Text("collapse/expand");

      ImGui::EndMenu();
    }

    ImGui::Text("input latch");
    ImGui::PushFont(patFont);
    if (ImGui::BeginTable("inputLatchTable",5,ImGuiTableFlags_Borders|ImGuiTableFlags_SizingFixedFit|ImGuiTableFlags_NoHostExtendX)) {
      static char id[64];
      ImGui::TableNextRow();
      ImGui::TableNextColumn();
      ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_ACTIVE]);
      ImGui::Text("C-4");
      ImGui::PopStyleColor();
      ImGui::TableNextColumn();
      ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INS]);
      if (latchIns==-2) {
        strcpy(id,"&&##LatchIns");
      } else if (latchIns==-1) {
        strcpy(id,"..##LatchIns");
      } else {
        snprintf(id,63,"%.2x##LatchIns",latchIns&0xff);
      }
      if (ImGui::Selectable(id,latchTarget==1,ImGuiSelectableFlags_DontClosePopups)) {
        latchTarget=1;
        latchNibble=false;
      }
      if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
        latchIns=-2;
      }
      if (ImGui::IsItemHovered()) {
        ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]);
        ImGui::SetTooltip("&&: selected instrument\n..: no instrument");
        ImGui::PopStyleColor();
      }
      ImGui::PopStyleColor();
      ImGui::TableNextColumn();
      ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_VOLUME_MAX]);
      if (latchVol==-1) {
        strcpy(id,"..##LatchVol");
      } else {
        snprintf(id,63,"%.2x##LatchVol",latchVol&0xff);
      }
      if (ImGui::Selectable(id,latchTarget==2,ImGuiSelectableFlags_DontClosePopups)) {
        latchTarget=2;
        latchNibble=false;
      }
      if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
        latchVol=-1;
      }
      ImGui::PopStyleColor();
      ImGui::TableNextColumn();
      if (latchEffect==-1) {
        strcpy(id,"..##LatchFX");
        ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PATTERN_INACTIVE]);
      } else {
        const unsigned char data=latchEffect;
        snprintf(id,63,"%.2x##LatchFX",data);
        ImGui::PushStyleColor(ImGuiCol_Text,uiColors[fxColors[data]]);
      }

      if (ImGui::Selectable(id,latchTarget==3,ImGuiSelectableFlags_DontClosePopups)) {
        latchTarget=3;
        latchNibble=false;
      }
      if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
        latchEffect=-1;
      }
      ImGui::TableNextColumn();
      if (latchEffectVal==-1) {
        strcpy(id,"..##LatchFXV");
      } else {
        snprintf(id,63,"%.2x##LatchFXV",latchEffectVal&0xff);
      }
      if (ImGui::Selectable(id,latchTarget==4,ImGuiSelectableFlags_DontClosePopups)) {
        latchTarget=4;
        latchNibble=false;
      }
      if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
        latchEffectVal=-1;
      }
      ImGui::PopStyleColor();
      ImGui::EndTable();
    }
    ImGui::PopFont();
    ImGui::SameLine();
    if (ImGui::Button("Set")) {
      DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],true);
      latchIns=pat->data[cursor.y][2];
      latchVol=pat->data[cursor.y][3];
      latchEffect=pat->data[cursor.y][4];
      latchEffectVal=pat->data[cursor.y][5];
      latchTarget=0;
      latchNibble=false;
    }
    ImGui::SameLine();
    if (ImGui::Button("Reset")) {
      latchIns=-2;
      latchVol=-1;
      latchEffect=-1;
      latchEffectVal=-1;
      latchTarget=0;
      latchNibble=false;
    }
    ImGui::Separator();
  }

  if (ImGui::MenuItem("note up",BIND_FOR(GUI_ACTION_PAT_NOTE_UP))) doTranspose(1,opMaskTransposeNote);
  if (ImGui::MenuItem("note down",BIND_FOR(GUI_ACTION_PAT_NOTE_DOWN))) doTranspose(-1,opMaskTransposeNote);
  if (ImGui::MenuItem("octave up",BIND_FOR(GUI_ACTION_PAT_OCTAVE_UP))) doTranspose(12,opMaskTransposeNote);
  if (ImGui::MenuItem("octave down",BIND_FOR(GUI_ACTION_PAT_OCTAVE_DOWN)))  doTranspose(-12,opMaskTransposeNote);
  ImGui::Separator();
  if (ImGui::MenuItem("values up",BIND_FOR(GUI_ACTION_PAT_VALUE_UP))) doTranspose(1,opMaskTransposeValue);
  if (ImGui::MenuItem("values down",BIND_FOR(GUI_ACTION_PAT_VALUE_DOWN))) doTranspose(-1,opMaskTransposeValue);
  if (ImGui::MenuItem("values up (+16)",BIND_FOR(GUI_ACTION_PAT_VALUE_UP_COARSE))) doTranspose(16,opMaskTransposeValue);
  if (ImGui::MenuItem("values down (-16)",BIND_FOR(GUI_ACTION_PAT_VALUE_DOWN_COARSE)))  doTranspose(-16,opMaskTransposeValue);
  ImGui::Separator();
  ImGui::AlignTextToFramePadding();
  ImGui::Text("transpose");
  ImGui::SameLine();
  ImGui::SetNextItemWidth(120.0f*dpiScale);
  if (ImGui::InputInt("##TransposeAmount",&transposeAmount,1,1)) {
    if (transposeAmount<-96) transposeAmount=-96;
    if (transposeAmount>96) transposeAmount=96;
  }
  ImGui::SameLine();
  if (ImGui::Button("Notes")) {
    doTranspose(transposeAmount,opMaskTransposeNote);
    ImGui::CloseCurrentPopup();
  }
  ImGui::SameLine();
  if (ImGui::Button("Values")) {
    doTranspose(transposeAmount,opMaskTransposeValue);
    ImGui::CloseCurrentPopup();
  }

  ImGui::Separator();
  if (ImGui::MenuItem("interpolate",BIND_FOR(GUI_ACTION_PAT_INTERPOLATE))) doInterpolate();
  if (ImGui::BeginMenu("change instrument...")) {
    if (e->song.ins.empty()) {
      ImGui::Text("no instruments available");
    }
    for (size_t i=0; i<e->song.ins.size(); i++) {
      snprintf(id,4095,"%.2X: %s",(int)i,e->song.ins[i]->name.c_str());
      if (ImGui::MenuItem(id)) {
        doChangeIns(i);
      }
    }
    ImGui::EndMenu();
  }

  if (!basicMode) {
    if (ImGui::BeginMenu("gradient/fade...")) {
      if (ImGui::InputInt("Start",&fadeMin,1,1)) {
        if (fadeMin<0) fadeMin=0;
        if (fadeMode) {
          if (fadeMin>15) fadeMin=15;
        } else {
          if (fadeMin>255) fadeMin=255;
        }
      }
      if (ImGui::InputInt("End",&fadeMax,1,1)) {
        if (fadeMax<0) fadeMax=0;
        if (fadeMode) {
          if (fadeMax>15) fadeMax=15;
        } else {
          if (fadeMax>255) fadeMax=255;
        }
      }
      if (ImGui::Checkbox("Nibble mode",&fadeMode)) {
        if (fadeMode) {
          if (fadeMin>15) fadeMin=15;
          if (fadeMax>15) fadeMax=15;
        } else {
          if (fadeMin>255) fadeMin=255;
          if (fadeMax>255) fadeMax=255;
        }
      }
      if (ImGui::Button("Go ahead")) {
        doFade(fadeMin,fadeMax,fadeMode);
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("scale...")) {
      if (ImGui::InputFloat("##ScaleMax",&scaleMax,1,1,"%.1f%%")) {
        if (scaleMax<0.0f) scaleMax=0.0f;
        if (scaleMax>25600.0f) scaleMax=25600.0f;
      }
      if (ImGui::Button("Scale")) {
        doScale(scaleMax);
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("randomize...")) {
      if (ImGui::InputInt("Minimum",&randomizeMin,1,1)) {
        if (randomizeMin<0) randomizeMin=0;
        if (randomMode) {
          if (randomizeMin>15) randomizeMin=15;
        } else {
          if (randomizeMin>255) randomizeMin=255;
        }
        if (randomizeMin>randomizeMax) randomizeMin=randomizeMax;
      }
      if (ImGui::InputInt("Maximum",&randomizeMax,1,1)) {
        if (randomizeMax<0) randomizeMax=0;
        if (randomizeMax<randomizeMin) randomizeMax=randomizeMin;
        if (randomMode) {
          if (randomizeMax>15) randomizeMax=15;
        } else {
          if (randomizeMax>255) randomizeMax=255;
        }
      }
      if (ImGui::Checkbox("Nibble mode",&randomMode)) {
        if (randomMode) {
          if (randomizeMin>15) randomizeMin=15;
          if (randomizeMax>15) randomizeMax=15;
        } else {
          if (randomizeMin>255) randomizeMin=255;
          if (randomizeMax>255) randomizeMax=255;
        }
      }
      // TODO: add an option to set effect to specific value?
      if (ImGui::Button("Randomize")) {
        doRandomize(randomizeMin,randomizeMax,randomMode);
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndMenu();
    }
    if (ImGui::MenuItem("invert values",BIND_FOR(GUI_ACTION_PAT_INVERT_VALUES))) doInvertValues();

    ImGui::Separator();

    if (ImGui::MenuItem("flip selection",BIND_FOR(GUI_ACTION_PAT_FLIP_SELECTION))) doFlip();

    ImGui::SetNextItemWidth(120.0f*dpiScale);
    if (ImGui::InputInt("collapse/expand amount##CollapseAmount",&collapseAmount,1,1)) {
      if (collapseAmount<2) collapseAmount=2;
      if (collapseAmount>256) collapseAmount=256;
    }
    if (ImGui::MenuItem("collapse",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_ROWS))) doCollapse(collapseAmount,selStart,selEnd);
    if (ImGui::MenuItem("expand",BIND_FOR(GUI_ACTION_PAT_EXPAND_ROWS))) doExpand(collapseAmount,selStart,selEnd);

    if (topMenu) {
      ImGui::Separator();
      if (ImGui::MenuItem("collapse pattern",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_PAT))) doAction(GUI_ACTION_PAT_COLLAPSE_PAT);
      if (ImGui::MenuItem("expand pattern",BIND_FOR(GUI_ACTION_PAT_EXPAND_PAT))) doAction(GUI_ACTION_PAT_EXPAND_PAT);
    }
  }

  if (topMenu) {
    ImGui::Separator();
    if (ImGui::MenuItem("collapse song",BIND_FOR(GUI_ACTION_PAT_COLLAPSE_SONG))) doAction(GUI_ACTION_PAT_COLLAPSE_SONG);
    if (ImGui::MenuItem("expand song",BIND_FOR(GUI_ACTION_PAT_EXPAND_SONG))) doAction(GUI_ACTION_PAT_EXPAND_SONG);
  }

  if (!basicMode) {
    if (topMenu) {
      ImGui::Separator();
      if (ImGui::MenuItem("find/replace",BIND_FOR(GUI_ACTION_WINDOW_FIND),findOpen)) {
        if (findOpen) {
          findOpen=false;
        } else {
          nextWindow=GUI_WINDOW_FIND;
        }
      }
    }
  }
}

void FurnaceGUI::toggleMobileUI(bool enable, bool force) {
  if (mobileUI!=enable || force) {
    if (!mobileUI && enable) {
      if (!ImGui::SaveIniSettingsToDisk(finalLayoutPath,true)) {
        reportError(fmt::sprintf("could NOT save layout! %s",strerror(errno)));
      }
    }
    mobileUI=enable;
    if (mobileUI) {
      ImGui::GetIO().IniFilename=NULL;
      ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_InertialScrollEnable;
      ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_NoHoverColors;
      fileDialog->mobileUI=true;
    } else {
      ImGui::GetIO().IniFilename=NULL;
      if (!ImGui::LoadIniSettingsFromDisk(finalLayoutPath,true)) {
        reportError(fmt::sprintf("could NOT load layout! %s",strerror(errno)));
        ImGui::LoadIniSettingsFromMemory(defaultLayout);
      }
      ImGui::GetIO().ConfigFlags&=~ImGuiConfigFlags_InertialScrollEnable;
      ImGui::GetIO().ConfigFlags&=~ImGuiConfigFlags_NoHoverColors;
      fileDialog->mobileUI=false;
    }
  }
}

void FurnaceGUI::pushToggleColors(bool status) {
  ImVec4 toggleColor=status?uiColors[GUI_COLOR_TOGGLE_ON]:uiColors[GUI_COLOR_TOGGLE_OFF];
  ImGui::PushStyleColor(ImGuiCol_Button,toggleColor);
  if (!mobileUI) {
    if (settings.guiColorsBase) {
      toggleColor.x*=0.8f;
      toggleColor.y*=0.8f;
      toggleColor.z*=0.8f;
    } else {
      toggleColor.x=CLAMP(toggleColor.x*1.3f,0.0f,1.0f);
      toggleColor.y=CLAMP(toggleColor.y*1.3f,0.0f,1.0f);
      toggleColor.z=CLAMP(toggleColor.z*1.3f,0.0f,1.0f);
    }
  }
  ImGui::PushStyleColor(ImGuiCol_ButtonHovered,toggleColor);
  if (settings.guiColorsBase) {
    toggleColor.x*=0.8f;
    toggleColor.y*=0.8f;
    toggleColor.z*=0.8f;
  } else {
    toggleColor.x=CLAMP(toggleColor.x*1.5f,0.0f,1.0f);
    toggleColor.y=CLAMP(toggleColor.y*1.5f,0.0f,1.0f);
    toggleColor.z=CLAMP(toggleColor.z*1.5f,0.0f,1.0f);
  }
  ImGui::PushStyleColor(ImGuiCol_ButtonActive,toggleColor);
}

void FurnaceGUI::popToggleColors() {
  ImGui::PopStyleColor(3);
}

int _processEvent(void* instance, SDL_Event* event) {
  return ((FurnaceGUI*)instance)->processEvent(event);
}

#if SDL_VERSION_ATLEAST(2,0,17)
#define VALID_MODS KMOD_NUM|KMOD_CAPS|KMOD_SCROLL
#else
#define VALID_MODS KMOD_NUM|KMOD_CAPS
#endif

int FurnaceGUI::processEvent(SDL_Event* ev) {
  if (introPos<11.0 && !shortIntro) return 1;
#ifdef IS_MOBILE
  if (ev->type==SDL_APP_TERMINATING) {
    // TODO: save last song state here
  } else if (ev->type==SDL_APP_WILLENTERBACKGROUND) {
    commitState();
    e->saveConf();
  }
#endif
  if (ev->type==SDL_KEYDOWN) {
    if (!ev->key.repeat && latchTarget==0 && !wantCaptureKeyboard && !sampleMapWaitingInput && (ev->key.keysym.mod&(~(VALID_MODS)))==0) {
      if (settings.notePreviewBehavior==0) return 1;
      switch (curWindow) {
        case GUI_WINDOW_SAMPLE_EDIT:
        case GUI_WINDOW_SAMPLE_LIST: {
          auto it=noteKeys.find(ev->key.keysym.scancode);
          if (it!=noteKeys.cend()) {
            int key=it->second;
            int num=12*curOctave+key;
            if (key!=100 && key!=101 && key!=102) {
              int pStart=-1;
              int pEnd=-1;
              if (curWindow==GUI_WINDOW_SAMPLE_EDIT) {
                if (sampleSelStart!=sampleSelEnd) {
                  pStart=sampleSelStart;
                  pEnd=sampleSelEnd;
                  if (pStart>pEnd) {
                    pStart^=pEnd;
                    pEnd^=pStart;
                    pStart^=pEnd;
                  }
                }
              }
              e->previewSample(curSample,num,pStart,pEnd);
              samplePreviewOn=true;
              samplePreviewKey=ev->key.keysym.scancode;
              samplePreviewNote=num;
            }
          }
          break;
        }
        case GUI_WINDOW_WAVE_LIST:
        case GUI_WINDOW_WAVE_EDIT: {
          auto it=noteKeys.find(ev->key.keysym.scancode);
          if (it!=noteKeys.cend()) {
            int key=it->second;
            int num=12*curOctave+key;
            if (key!=100 && key!=101 && key!=102) {
              e->previewWave(curWave,num);
              wavePreviewOn=true;
              wavePreviewKey=ev->key.keysym.scancode;
              wavePreviewNote=num;
            }
          }
          break;
        }
        case GUI_WINDOW_ORDERS: // ignore here
          break;
        case GUI_WINDOW_PATTERN:
          if (settings.notePreviewBehavior==1) {
            if (cursor.xFine!=0) break;
          } else if (settings.notePreviewBehavior==2) {
            if (edit && cursor.xFine!=0) break;
          }
          // fall-through
        default: {
          auto it=noteKeys.find(ev->key.keysym.scancode);
          if (it!=noteKeys.cend()) {
            int key=it->second;
            int num=12*curOctave+key;

            if (num<-60) num=-60; // C-(-5)
            if (num>119) num=119; // B-9

            if (key!=100 && key!=101 && key!=102) {
              previewNote(cursor.xCoarse,num);
            }
          }
          break;
        }
      }
    }
  } else if (ev->type==SDL_KEYUP) {
    stopPreviewNote(ev->key.keysym.scancode,true);
    if (wavePreviewOn) {
      if (ev->key.keysym.scancode==wavePreviewKey) {
        wavePreviewOn=false;
        e->stopWavePreview();
      }
    }
    if (samplePreviewOn) {
      if (ev->key.keysym.scancode==samplePreviewKey) {
        samplePreviewOn=false;
        e->stopSamplePreview();
      }
    }
  }
  return 1;
}

#define FIND_POINT(p,pid) \
  for (TouchPoint& i: activePoints) { \
    if (i.id==pid) { \
      p=&i; \
    } \
  }

void FurnaceGUI::processPoint(SDL_Event& ev) {
  switch (ev.type) {
    case SDL_MOUSEMOTION: {
      TouchPoint* point=NULL;
      FIND_POINT(point,-1);
      if (point!=NULL) {
        point->x=(double)ev.motion.x*((double)canvasW/(double)scrW);
        point->y=(double)ev.motion.y*((double)canvasH/(double)scrH);
      }
      break;
    }
    case SDL_MOUSEBUTTONDOWN: {
      if (ev.button.button!=SDL_BUTTON_LEFT) break;
      for (size_t i=0; i<activePoints.size(); i++) {
        TouchPoint& point=activePoints[i];
        if (point.id==-1) {
          releasedPoints.push_back(point);
          activePoints.erase(activePoints.begin()+i);
          break;
        }
      }
      TouchPoint newPoint(ev.button.x,ev.button.y);
#ifdef __APPLE__
      newPoint.x*=dpiScale;
      newPoint.y*=dpiScale;
#endif
      activePoints.push_back(newPoint);
      pressedPoints.push_back(newPoint);
      break;
    }
    case SDL_MOUSEBUTTONUP: {
      if (ev.button.button!=SDL_BUTTON_LEFT) break;
      for (size_t i=0; i<activePoints.size(); i++) {
        TouchPoint& point=activePoints[i];
        if (point.id==-1) {
          releasedPoints.push_back(point);
          activePoints.erase(activePoints.begin()+i);
          break;
        }
      }
      break;
    }
    case SDL_FINGERMOTION: {
      TouchPoint* point=NULL;
      FIND_POINT(point,ev.tfinger.fingerId);
      if (point!=NULL) {
        float prevX=point->x;
        float prevY=point->y;
        point->x=ev.tfinger.x*canvasW;
        point->y=ev.tfinger.y*canvasH;
        point->z=ev.tfinger.pressure;

        if (point->id==0) {
          ImGui::GetIO().AddMousePosEvent(point->x,point->y);
          pointMotion(point->x,point->y,point->x-prevX,point->y-prevY);
        }
      }
      break;
    }
    case SDL_FINGERDOWN: {
      for (size_t i=0; i<activePoints.size(); i++) {
        TouchPoint& point=activePoints[i];
        if (point.id==ev.tfinger.fingerId) {
          releasedPoints.push_back(point);
          activePoints.erase(activePoints.begin()+i);
          break;
        }
      }
      TouchPoint newPoint(ev.tfinger.fingerId,ev.tfinger.x*canvasW,ev.tfinger.y*canvasH,ev.tfinger.pressure);
      activePoints.push_back(newPoint);
      pressedPoints.push_back(newPoint);

      if (newPoint.id==0) {
        ImGui::GetIO().AddMousePosEvent(newPoint.x,newPoint.y);
        ImGui::GetIO().AddMouseButtonEvent(ImGuiMouseButton_Left,true);
        pointDown(newPoint.x,newPoint.y,0);
      }
      break;
    }
    case SDL_FINGERUP: {
      for (size_t i=0; i<activePoints.size(); i++) {
        TouchPoint& point=activePoints[i];
        if (point.id==ev.tfinger.fingerId) {
          if (point.id==0) {
            ImGui::GetIO().AddMouseButtonEvent(ImGuiMouseButton_Left,false);
            //ImGui::GetIO().AddMousePosEvent(-FLT_MAX,-FLT_MAX);
            pointUp(point.x,point.y,0);
          }

          releasedPoints.push_back(point);
          activePoints.erase(activePoints.begin()+i);

          break;
        }
      }
      break;
    }
  }
}

void FurnaceGUI::pointDown(int x, int y, int button) {
  aboutOpen=false;
  if (bindSetActive) {
    bindSetActive=false;
    bindSetPending=false;
    actionKeys[bindSetTarget]=bindSetPrevValue;
    bindSetTarget=0;
    bindSetPrevValue=0;
  }
  if (introPos<11.0 && !shortIntro) {
    introSkipDo=true;
  }
}

void FurnaceGUI::pointUp(int x, int y, int button) {
  if (macroDragActive || macroLoopDragActive || waveDragActive || (sampleDragActive && sampleDragMode && sampleDragTarget)) {
    MARK_MODIFIED;
  }
  if (macroDragActive && macroDragLineMode && !macroDragMouseMoved) {
    displayMacroMenu=true;
  }
  macroDragActive=false;
  macroDragBitMode=false;
  macroDragInitialValue=false;
  macroDragInitialValueSet=false;
  macroDragLastX=-1;
  macroDragLastY=-1;
  macroLoopDragActive=false;
  waveDragActive=false;
  if (introPos<11.0 && introSkip<0.5 && !shortIntro) {
    introSkipDo=false;
  }
  if (sampleDragActive) {
    logD("stopping sample drag");
    if (sampleDragMode) {
      e->renderSamplesP();
    } else {
      if (sampleSelStart>sampleSelEnd) {
        sampleSelStart^=sampleSelEnd;
        sampleSelEnd^=sampleSelStart;
        sampleSelStart^=sampleSelEnd;
      }
    }
  }
  sampleDragActive=false;
  if (orderScrollLocked) {
    int targetOrder=round(orderScroll);
    if (orderScrollTolerance) {
      targetOrder=round(orderScroll+(orderScrollRealOrigin.x-((float)canvasW/2.0f))/(40.0f*dpiScale));
    }
    if (targetOrder<0) targetOrder=0;
    if (targetOrder>e->curSubSong->ordersLen-1) targetOrder=e->curSubSong->ordersLen-1;
    if (curOrder!=targetOrder) setOrder(targetOrder);
  }
  orderScrollLocked=false;
  orderScrollTolerance=false;
  if (dragMobileMenu) {
    dragMobileMenu=false;
    if (mobileMenuOpen) {
      mobileMenuOpen=(mobileMenuPos>=0.85f);
    } else {
      mobileMenuOpen=(mobileMenuPos>=0.15f);
    }
  }
  if (dragMobileEditButton) {
    dragMobileEditButton=false;
  }
  if (selecting) {
    if (!selectingFull) cursor=selEnd;
    finishSelection();
    if (!mobileUI) {
      demandScrollX=true;
      if (cursor.xCoarse==selStart.xCoarse && cursor.xFine==selStart.xFine && cursor.y==selStart.y &&
          cursor.xCoarse==selEnd.xCoarse && cursor.xFine==selEnd.xFine && cursor.y==selEnd.y) {
        if (!settings.cursorMoveNoScroll) {
          updateScroll(cursor.y);
        }
      }
    }
  }
}

void FurnaceGUI::pointMotion(int x, int y, int xrel, int yrel) {
  if (selecting) {
    // detect whether we have to scroll
    if (y<patWindowPos.y+2.0f*dpiScale) {
      addScroll(-1);
    }
    if (y>patWindowPos.y+patWindowSize.y-2.0f*dpiScale) {
      addScroll(1);
    }
  }
  if (macroDragActive || macroLoopDragActive || waveDragActive || sampleDragActive || orderScrollLocked) {
    int distance=fabs((double)xrel);
    if (distance<1) distance=1;
    float start=x-xrel;
    float end=x;
    float startY=y-yrel;
    float endY=y;
    for (int i=0; i<=distance; i++) {
      float fraction=(float)i/(float)distance;
      float x=start+(end-start)*fraction;
      float y=startY+(endY-startY)*fraction;
      processDrags(x,y);
    }
  }
}

// how many pixels should be visible at least at x/y dir
#define OOB_PIXELS_SAFETY 25

bool FurnaceGUI::detectOutOfBoundsWindow(SDL_Rect& failing) {
  int count=SDL_GetNumVideoDisplays();
  if (count<1) {
    logW("bounds check: error: %s",SDL_GetError());
    return false;
  }

  SDL_Rect rect;
  for (int i=0; i<count; i++) {
    if (SDL_GetDisplayUsableBounds(i,&rect)!=0) {
      logW("bounds check: error in display %d: %s",i,SDL_GetError());
      continue;
    }

    bool xbound=((rect.x+OOB_PIXELS_SAFETY)<=(scrX+scrW)) && ((rect.x+rect.w-OOB_PIXELS_SAFETY)>=scrX);
    bool ybound=((rect.y+OOB_PIXELS_SAFETY)<=(scrY+scrH)) && ((rect.y+rect.h-OOB_PIXELS_SAFETY)>=scrY);
    logD("bounds check: display %d is at %dx%dx%dx%d: %s%s",i,rect.x+OOB_PIXELS_SAFETY,rect.y+OOB_PIXELS_SAFETY,rect.x+rect.w-OOB_PIXELS_SAFETY,rect.y+rect.h-OOB_PIXELS_SAFETY,xbound?"x":"",ybound?"y":"");

    if (xbound && ybound) {
      return true;
    }
  }

  failing=rect;
  return false;
}

#define DECLARE_METRIC(_n) \
  int __perfM##_n;

#define MEASURE_BEGIN(_n) \
  __perfM##_n=SDL_GetPerformanceCounter();

#define MEASURE_END(_n) \
  if (perfMetricsLen<64) { \
    perfMetrics[perfMetricsLen++]=FurnaceGUIPerfMetric(#_n,SDL_GetPerformanceCounter()-__perfM##_n); \
  }

#define MEASURE(_n,_x) \
  MEASURE_BEGIN(_n) \
  _x; \
  MEASURE_END(_n)

bool FurnaceGUI::loop() {
  DECLARE_METRIC(calcChanOsc)
  DECLARE_METRIC(mobileControls)
  DECLARE_METRIC(mobileOrderSel)
  DECLARE_METRIC(subSongs)
  DECLARE_METRIC(findReplace)
  DECLARE_METRIC(spoiler)
  DECLARE_METRIC(pattern)
  DECLARE_METRIC(editControls)
  DECLARE_METRIC(speed)
  DECLARE_METRIC(grooves)
  DECLARE_METRIC(songInfo)
  DECLARE_METRIC(orders)
  DECLARE_METRIC(intro)
  DECLARE_METRIC(sampleList)
  DECLARE_METRIC(sampleEdit)
  DECLARE_METRIC(waveList)
  DECLARE_METRIC(waveEdit)
  DECLARE_METRIC(insList)
  DECLARE_METRIC(insEdit)
  DECLARE_METRIC(mixer)
  DECLARE_METRIC(readOsc)
  DECLARE_METRIC(osc)
  DECLARE_METRIC(chanOsc)
  DECLARE_METRIC(volMeter)
  DECLARE_METRIC(settings)
  DECLARE_METRIC(debug)
  DECLARE_METRIC(stats)
  DECLARE_METRIC(compatFlags)
  DECLARE_METRIC(piano)
  DECLARE_METRIC(notes)
  DECLARE_METRIC(channels)
  DECLARE_METRIC(patManager)
  DECLARE_METRIC(sysManager)
  DECLARE_METRIC(clock)
  DECLARE_METRIC(regView)
  DECLARE_METRIC(log)
  DECLARE_METRIC(effectList)
  DECLARE_METRIC(popup)

#ifdef IS_MOBILE
  bool doThreadedInput=true;
#else
  bool doThreadedInput=!settings.noThreadedInput;
#endif
  if (doThreadedInput) {
    logD("key input: event filter");
    SDL_SetEventFilter(_processEvent,this);
  } else {
    logD("key input: main thread");
  }

  while (!quit) {
    SDL_Event ev;
    if (e->isPlaying()) {
      WAKE_UP;
    }
    if (--drawHalt<=0) {
      drawHalt=0;
      if (settings.powerSave) SDL_WaitEventTimeout(NULL,500);
    }

    memcpy(perfMetricsLast,perfMetrics,64*sizeof(FurnaceGUIPerfMetric));
    perfMetricsLastLen=perfMetricsLen;
    perfMetricsLen=0;

    eventTimeBegin=SDL_GetPerformanceCounter();
    bool updateWindow=false;
    if (injectBackUp) {
      ImGui::GetIO().AddKeyEvent(ImGuiKey_Backspace,false);
      injectBackUp=false;
    }
    while (SDL_PollEvent(&ev)) {
      WAKE_UP;
      ImGui_ImplSDL2_ProcessEvent(&ev);
      processPoint(ev);
      if (!doThreadedInput) processEvent(&ev);
      switch (ev.type) {
        case SDL_MOUSEMOTION: {
          int motionX=(double)ev.motion.x*((double)canvasW/(double)scrW);
          int motionY=(double)ev.motion.y*((double)canvasH/(double)scrH);
          int motionXrel=(double)ev.motion.xrel*((double)canvasW/(double)scrW);
          int motionYrel=(double)ev.motion.yrel*((double)canvasH/(double)scrH);
          pointMotion(motionX,motionY,motionXrel,motionYrel);
          break;
        }
        case SDL_MOUSEBUTTONUP:
          pointUp(ev.button.x,ev.button.y,ev.button.button);
          break;
        case SDL_MOUSEBUTTONDOWN:
          pointDown(ev.button.x,ev.button.y,ev.button.button);
          break;
        case SDL_MOUSEWHEEL:
          wheelX+=ev.wheel.x;
          wheelY+=ev.wheel.y;
          break;
        case SDL_WINDOWEVENT:
          switch (ev.window.event) {
            case SDL_WINDOWEVENT_RESIZED:
              scrW=ev.window.data1;
              scrH=ev.window.data2;
              portrait=(scrW<scrH);
              logV("portrait: %d (%dx%d)",portrait,scrW,scrH);
              logD("window resized to %dx%d",scrW,scrH);
              updateWindow=true;
              rend->resized(ev);
              break;
            case SDL_WINDOWEVENT_MOVED:
              scrX=ev.window.data1;
              scrY=ev.window.data2;
              updateWindow=true;
              logV("window moved to %dx%d",scrX,scrY);
              break;
            case SDL_WINDOWEVENT_SIZE_CHANGED:
              logV("window size changed to %dx%d",ev.window.data1,ev.window.data2);
              break;
            case SDL_WINDOWEVENT_MINIMIZED:
              logV("window minimized");
              break;
            case SDL_WINDOWEVENT_MAXIMIZED:
              scrMax=true;
              updateWindow=true;
              logV("window maximized");
              break;
            case SDL_WINDOWEVENT_RESTORED:
              scrMax=false;
              updateWindow=true;
              logV("window restored");
              break;
            case SDL_WINDOWEVENT_SHOWN:
              logV("window shown");
              break;
            case SDL_WINDOWEVENT_HIDDEN:
              logV("window hidden");
              break;
            case SDL_WINDOWEVENT_EXPOSED:
              logV("window exposed");
              break;
          }
          break;
#if SDL_VERSION_ATLEAST(2,0,4)
        case SDL_RENDER_DEVICE_RESET:
          killGraphics=true;
          break;
#endif
#if SDL_VERSION_ATLEAST(2,0,17)
        case SDL_DISPLAYEVENT: {
          switch (ev.display.event) {
            case SDL_DISPLAYEVENT_CONNECTED:
              logD("display %d connected!",ev.display.display);
              updateWindow=true;
              break;
            case SDL_DISPLAYEVENT_DISCONNECTED:
              logD("display %d disconnected!",ev.display.display);
              updateWindow=true;
              break;
            case SDL_DISPLAYEVENT_ORIENTATION:
              logD("display oriented to %d",ev.display.data1);
              updateWindow=true;
              break;
          }
          break;
        }
#endif
        case SDL_KEYDOWN:
          if (!ImGui::GetIO().WantCaptureKeyboard) {
            keyDown(ev);
          }
#ifdef IS_MOBILE
          injectBackUp=true;
#endif
          break;
        case SDL_KEYUP:
          // for now
          break;
        case SDL_DROPFILE:
          if (ev.drop.file!=NULL) {
            int sampleCountBefore=e->song.sampleLen;
            std::vector<DivInstrument*> instruments=e->instrumentFromFile(ev.drop.file);
            DivWavetable* droppedWave=NULL;
            DivSample* droppedSample=NULL;
            if (!instruments.empty()) {
              if (e->song.sampleLen!=sampleCountBefore) {
                e->renderSamplesP();
              }
              if (!e->getWarnings().empty()) {
                showWarning(e->getWarnings(),GUI_WARN_GENERIC);
              }
              for (DivInstrument* i: instruments) {
                e->addInstrumentPtr(i);
              }
              nextWindow=GUI_WINDOW_INS_LIST;
              MARK_MODIFIED;
            } else if ((droppedWave=e->waveFromFile(ev.drop.file,false))!=NULL) {
              e->addWavePtr(droppedWave);
              nextWindow=GUI_WINDOW_WAVE_LIST;
              MARK_MODIFIED;
            } else if ((droppedSample=e->sampleFromFile(ev.drop.file))!=NULL) {
              e->addSamplePtr(droppedSample);
              nextWindow=GUI_WINDOW_SAMPLE_LIST;
              MARK_MODIFIED;
            } else if (modified) {
              nextFile=ev.drop.file;
              showWarning("Unsaved changes! Save changes before opening file?",GUI_WARN_OPEN_DROP);
            } else {
              if (load(ev.drop.file)>0) {
                showError(fmt::sprintf("Error while loading file! (%s)",lastError));
              }
            }
            SDL_free(ev.drop.file);
          }
          break;
        case SDL_QUIT:
          if (modified) {
            showWarning("Unsaved changes! Save changes before quitting?",GUI_WARN_QUIT);
          } else {
            quit=true;
            return true;
          }
          break;
      }
    }

    // update config x/y/w/h values based on scrMax state
    if (updateWindow) {
      logV("updateWindow is true");
      if (!scrMax && !fullScreen) {
        logV("updating scrConf");
        scrConfX=scrX;
        scrConfY=scrY;
        scrConfW=scrW;
        scrConfH=scrH;
      }
    }
    // update canvas size as well
    if (!rend->getOutputSize(canvasW,canvasH)) {
      logW("loop: error while getting output size!");
    } else {
      //logV("updateWindow: canvas size %dx%d",canvasW,canvasH);
      // and therefore window size
      int prevScrW=scrW;
      int prevScrH=scrH;
      SDL_GetWindowSize(sdlWin,&scrW,&scrH);
      if (prevScrW!=scrW || prevScrH!=scrH) {
        logV("size change 2: %dx%d (from %dx%d)",scrW,scrH,prevScrW,prevScrH);
      }
    }

    wantCaptureKeyboard=ImGui::GetIO().WantTextInput;

    if (wantCaptureKeyboard!=oldWantCaptureKeyboard) {
      oldWantCaptureKeyboard=wantCaptureKeyboard;
      if (wantCaptureKeyboard) {
        SDL_StartTextInput();
      } else {
        SDL_StopTextInput();
      }
    }

    if (wantCaptureKeyboard) {
      WAKE_UP;
    }

    if (ImGui::GetIO().IsSomethingHappening) {
      WAKE_UP;
    }

    if (ImGui::GetIO().MouseDown[0] || ImGui::GetIO().MouseDown[1] || ImGui::GetIO().MouseDown[2] || ImGui::GetIO().MouseDown[3] || ImGui::GetIO().MouseDown[4]) {
      WAKE_UP;
    }

    while (true) {
      midiLock.lock();
      if (midiQueue.empty()) {
        midiLock.unlock();
        break;
      }
      TAMidiMessage msg=midiQueue.front();
      midiLock.unlock();

      if (msg.type==TA_MIDI_SYSEX) {
        unsigned char* data=msg.sysExData.get();
        for (size_t i=0; i<msg.sysExLen; i++) {
          if ((i&15)==0) printf("\n");
          printf("%.2x ",data[i]);
        }
        printf("\n");

        if (!parseSysEx(data,msg.sysExLen)) {
          logW("error while parsing SysEx data!");
        }
      }

      // parse message here
      if (learning!=-1) {
        if (learning>=0 && learning<(int)midiMap.binds.size()) {
          midiMap.binds[learning].type=msg.type>>4;
          midiMap.binds[learning].channel=msg.type&15;
          midiMap.binds[learning].data1=msg.data[0];
          switch (msg.type&0xf0) {
            case TA_MIDI_NOTE_OFF:
            case TA_MIDI_NOTE_ON:
            case TA_MIDI_AFTERTOUCH:
            case TA_MIDI_PITCH_BEND:
            case TA_MIDI_CONTROL:
              midiMap.binds[learning].data2=msg.data[1];
              break;
            default:
              midiMap.binds[learning].data2=128;
              break;
          }
        }
        learning=-1;
      } else {
        int action=midiMap.at(msg);
        if (action!=0) {
          doAction(action);
        } else switch (msg.type&0xf0) {
          case TA_MIDI_NOTE_ON:
            if (midiMap.valueInputStyle==0 || midiMap.valueInputStyle>3 || cursor.xFine==0) {
              if (midiMap.noteInput && edit && msg.data[1]!=0) {
                noteInput(
                  msg.data[0]-12,
                  0,
                  midiMap.volInput?((int)(pow((double)msg.data[1]/127.0,midiMap.volExp)*127.0)):-1
                );
              }
            } else {
              if (edit && msg.data[1]!=0) {
                switch (midiMap.valueInputStyle) {
                  case 1: {
                    int val=msg.data[0]%24;
                    if (val<16) {
                      valueInput(val);
                    }
                    break;
                  }
                  case 2:
                    valueInput(msg.data[0]&15);
                    break;
                  case 3:
                    int val=altValues[msg.data[0]%24];
                    if (val>=0) {
                      valueInput(val);
                    }
                    break;
                }
              }
            }
            break;
          case TA_MIDI_PROGRAM:
            if (midiMap.programChange) {
              curIns=msg.data[0];
              if (curIns>=(int)e->song.ins.size()) curIns=e->song.ins.size()-1;
              wavePreviewInit=true;
              updateFMPreview=true;
            }
            break;
          case TA_MIDI_CONTROL:
            bool gchanged=false;
            if (msg.data[0]==midiMap.valueInputControlMSB) {
              midiMap.valueInputCurMSB=msg.data[1];
              gchanged=true;
            }
            if (msg.data[0]==midiMap.valueInputControlLSB) {
              midiMap.valueInputCurLSB=msg.data[1];
              gchanged=true;
            }
            if (msg.data[0]==midiMap.valueInputControlSingle) {
              midiMap.valueInputCurSingle=msg.data[1];
              gchanged=true;
            }
            if (gchanged && cursor.xFine>0) {
              switch (midiMap.valueInputStyle) {
                case 4: // dual CC
                  valueInput(((midiMap.valueInputCurMSB>>3)<<4)|(midiMap.valueInputCurLSB>>3),true);
                  break;
                case 5: // 14-bit
                  valueInput((midiMap.valueInputCurMSB<<1)|(midiMap.valueInputCurLSB>>6),true);
                  break;
                case 6: // single CC
                  valueInput((midiMap.valueInputCurSingle*255)/127,true);
                  break;
              }
            }

            for (int i=0; i<18; i++) {
              bool changed=false;
              if (midiMap.valueInputSpecificStyle[i]!=0) {
                if (msg.data[0]==midiMap.valueInputSpecificMSB[i]) {
                  changed=true;
                  midiMap.valueInputCurMSBS[i]=msg.data[1];
                }
                if (msg.data[0]==midiMap.valueInputSpecificLSB[i]) {
                  changed=true;
                  midiMap.valueInputCurLSBS[i]=msg.data[1];
                }
                if (msg.data[0]==midiMap.valueInputSpecificSingle[i]) {
                  changed=true;
                  midiMap.valueInputCurSingleS[i]=msg.data[1];
                }

                if (changed) switch (midiMap.valueInputStyle) {
                  case 1: // dual CC
                    valueInput(((midiMap.valueInputCurMSBS[i]>>3)<<4)|(midiMap.valueInputCurLSBS[i]>>3),true,i+2);
                    break;
                  case 2: // 14-bit
                    valueInput((midiMap.valueInputCurMSBS[i]<<1)|(midiMap.valueInputCurLSBS[i]>>6),true,i+2);
                    break;
                  case 3: // single CC
                    valueInput((midiMap.valueInputCurSingleS[i]*255)/127,true,i+2);
                    break;
                }
              }
            }
            break;
        }
      }

      midiLock.lock();
      midiQueue.pop();
      midiLock.unlock();
    }

    if (notifyWaveChange) {
      notifyWaveChange=false;
      e->notifyWaveChange(curWave);
    }

    eventTimeEnd=SDL_GetPerformanceCounter();

    if (SDL_GetWindowFlags(sdlWin)&SDL_WINDOW_MINIMIZED) {
      SDL_Delay(30);
      drawHalt=0;
      continue;
    }

    if (firstFrame) {
      if (!tutorial.introPlayed || settings.alwaysPlayIntro==3 || (settings.alwaysPlayIntro==2 && curFileName.empty())) {
        unsigned char* introTemp=new unsigned char[intro_fur_len];
        memcpy(introTemp,intro_fur,intro_fur_len);
        e->load(introTemp,intro_fur_len);
      }
    }

    if (!e->isRunning()) {
      activeNotes.clear();
      memset(chanOscVol,0,DIV_MAX_CHANS*sizeof(float));
      memset(chanOscPitch,0,DIV_MAX_CHANS*sizeof(float));
      memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float));

      e->synchronized([this]() {
        for (int i=0; i<e->getTotalChannelCount(); i++) {
          DivDispatchOscBuffer* buf=e->getOscBuffer(i);
          if (buf!=NULL) {
            buf->needle=0;
            buf->readNeedle=0;
          }
        }
      });
    }

    // recover from dead graphics
    if (rend->isDead() || killGraphics) {
      killGraphics=false;

      logW("graphics are dead! restarting...");
      
      if (sampleTex!=NULL) {
        rend->destroyTexture(sampleTex);
        sampleTex=NULL;
      }

      if (chanOscGradTex!=NULL) {
        rend->destroyTexture(chanOscGradTex);
        chanOscGradTex=NULL;
      }

      for (auto& i: images) {
        if (i.second->tex!=NULL) {
          rend->destroyTexture(i.second->tex);
          i.second->tex=NULL;
        }
      }

      commitState();
      rend->quitGUI();
      rend->quit();
      ImGui_ImplSDL2_Shutdown();

      int initAttempts=0;

      SDL_Delay(500);

      logD("starting render backend...");
      while (++initAttempts<=5) {
        if (rend->init(sdlWin)) {
          break;
        }
        SDL_Delay(1000);
        logV("trying again...");
      }

      if (initAttempts>5) {
        reportError("can't keep going without graphics! Furnace will quit now.");
        quit=true;
        break;
      }

      rend->clear(ImVec4(0.0,0.0,0.0,1.0));
      rend->present();

      logD("preparing user interface...");
      rend->initGUI(sdlWin);

      logD("building font...");
      if (!ImGui::GetIO().Fonts->Build()) {
        logE("error while building font atlas!");
        showError("error while loading fonts! please check your settings.");
        ImGui::GetIO().Fonts->Clear();
        mainFont=ImGui::GetIO().Fonts->AddFontDefault();
        patFont=mainFont;
        bigFont=mainFont;
        headFont=mainFont;
        if (rend) rend->destroyFontsTexture();
        if (!ImGui::GetIO().Fonts->Build()) {
          logE("error again while building font atlas!");
        }
      }

      firstFrame=true;
      mustClear=2;
      initialScreenWipe=1.0f;

      continue;
    }

    bool fontsFailed=false;

    layoutTimeBegin=SDL_GetPerformanceCounter();

    if (!rend->newFrame()) {
      fontsFailed=true;
    }
    ImGui_ImplSDL2_NewFrame(sdlWin);
    ImGui::NewFrame();

    curWindowLast=curWindow;
    curWindow=GUI_WINDOW_NOTHING;
    editOptsVisible=false;

    if (!mobileUI) {
      ImGui::BeginMainMenuBar();
      if (ImGui::BeginMenu(settings.capitalMenuBar?"File":"file")) {
        if (ImGui::MenuItem("new...",BIND_FOR(GUI_ACTION_NEW))) {
          if (modified) {
            showWarning("Unsaved changes! Save changes before creating a new song?",GUI_WARN_NEW);
          } else {
            displayNew=true;
          }
        }
        if (ImGui::MenuItem("open...",BIND_FOR(GUI_ACTION_OPEN))) {
          if (modified) {
            showWarning("Unsaved changes! Save changes before opening another file?",GUI_WARN_OPEN);
          } else {
            openFileDialog(GUI_FILE_OPEN);
          }
        }
        if (ImGui::BeginMenu("open recent")) {
          exitDisabledTimer=1;
          for (int i=0; i<(int)recentFile.size(); i++) {
            String item=recentFile[i];
            if (ImGui::MenuItem(item.c_str())) {
              if (modified) {
                nextFile=item;
                showWarning("Unsaved changes! Save changes before opening file?",GUI_WARN_OPEN_DROP);
              } else {
                recentFile.erase(recentFile.begin()+i);
                i--;
                if (load(item)>0) {
                  showError(fmt::sprintf("Error while loading file! (%s)",lastError));
                }
              }
            }
          }
          if (recentFile.empty()) {
            ImGui::Text("nothing here yet");
          } else {
            ImGui::Separator();
            if (ImGui::MenuItem("clear history")) {
              showWarning("Are you sure you want to clear the recent file list?",GUI_WARN_CLEAR_HISTORY);
            }
          }
          ImGui::EndMenu();
        }
        ImGui::Separator();
        if (ImGui::MenuItem("save",BIND_FOR(GUI_ACTION_SAVE))) {
          if (curFileName=="" || (curFileName.find(backupPath)==0) || e->song.version>=0xff00) {
            openFileDialog(GUI_FILE_SAVE);
          } else {
            if (save(curFileName,e->song.isDMF?e->song.version:0)>0) {
              showError(fmt::sprintf("Error while saving file! (%s)",lastError));
            }
          }
        }
        if (ImGui::MenuItem("save as...",BIND_FOR(GUI_ACTION_SAVE_AS))) {
          openFileDialog(GUI_FILE_SAVE);
        }
        if (ImGui::MenuItem("save as .dmf (1.1.3+)...")) {
          openFileDialog(GUI_FILE_SAVE_DMF);
        }
        if (ImGui::MenuItem("save as .dmf (1.0/legacy)...")) {
          openFileDialog(GUI_FILE_SAVE_DMF_LEGACY);
        }
        ImGui::Separator();
        if (ImGui::BeginMenu("export audio...")) {
          exitDisabledTimer=1;
          if (ImGui::MenuItem("one file")) {
            openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE);
          }
          if (ImGui::MenuItem("multiple files (one per chip)")) {
            openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_SYS);
          }
          if (ImGui::MenuItem("multiple files (one per channel)")) {
            openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_CHANNEL);
          }
          if (ImGui::InputInt("Loops",&exportLoops,1,2)) {
            if (exportLoops<0) exportLoops=0;
          }
          if (ImGui::InputDouble("Fade out (seconds)",&exportFadeOut,1.0,2.0,"%.1f")) {
            if (exportFadeOut<0.0) exportFadeOut=0.0;
          }
          ImGui::EndMenu();
        }
        if (ImGui::BeginMenu("export VGM...")) {
          exitDisabledTimer=1;
          ImGui::Text("settings:");
          if (ImGui::BeginCombo("format version",fmt::sprintf("%d.%.2x",vgmExportVersion>>8,vgmExportVersion&0xff).c_str())) {
            for (int i=0; i<7; i++) {
              if (ImGui::Selectable(fmt::sprintf("%d.%.2x",vgmVersions[i]>>8,vgmVersions[i]&0xff).c_str(),vgmExportVersion==vgmVersions[i])) {
                vgmExportVersion=vgmVersions[i];
              }
            }
            ImGui::EndCombo();
          }
          ImGui::Checkbox("loop",&vgmExportLoop);
          if (vgmExportLoop && e->song.loopModality==2) {
            ImGui::Text("loop trail:");
            ImGui::Indent();
            if (ImGui::RadioButton("auto-detect",vgmExportTrailingTicks==-1)) {
              vgmExportTrailingTicks=-1;
            }
            if (ImGui::RadioButton("add one loop",vgmExportTrailingTicks==-2)) {
              vgmExportTrailingTicks=-2;
            }
            if (ImGui::RadioButton("custom",vgmExportTrailingTicks>=0)) {
              vgmExportTrailingTicks=0;
            }
            if (vgmExportTrailingTicks>=0) {
              ImGui::SameLine();
              if (ImGui::InputInt("##TrailTicks",&vgmExportTrailingTicks,1,100)) {
                if (vgmExportTrailingTicks<0) vgmExportTrailingTicks=0;
              }
            }
            ImGui::Unindent();
          }
          ImGui::Checkbox("add pattern change hints",&vgmExportPatternHints);
          if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip(
              "inserts data blocks on pattern changes.\n"
              "useful if you are writing a playback routine.\n\n"

              "the format of a pattern change data block is:\n"
              "67 66 FE ll ll ll ll 01 oo rr pp pp pp ...\n"
              "- ll: length, a 32-bit little-endian number\n"
              "- oo: order\n"
              "- rr: initial row (a 0Dxx effect is able to select a different row)\n"
              "- pp: pattern index (one per channel)\n\n"

              "pattern indexes are ordered as they appear in the song."
            );
          }
          ImGui::Checkbox("direct stream mode",&vgmExportDirectStream);
          if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip(
              "required for DualPCM and MSM6258 export.\n\n"
              "allows for volume/direction changes when playing samples,\n"
              "at the cost of a massive increase in file size."
            );
          }
          ImGui::Text("chips to export:");
          bool hasOneAtLeast=false;
          for (int i=0; i<e->song.systemLen; i++) {
            int minVersion=e->minVGMVersion(e->song.system[i]);
            ImGui::BeginDisabled(minVersion>vgmExportVersion || minVersion==0);
            ImGui::Checkbox(fmt::sprintf("%d. %s##_SYSV%d",i+1,getSystemName(e->song.system[i]),i).c_str(),&willExport[i]);
            ImGui::EndDisabled();
            if (minVersion>vgmExportVersion) {
              if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
                ImGui::SetTooltip("this chip is only available in VGM %d.%.2x and higher!",minVersion>>8,minVersion&0xff);
              }
            } else if (minVersion==0) {
              if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
                ImGui::SetTooltip("this chip is not supported by the VGM format!");
              }
            } else {
              if (willExport[i]) hasOneAtLeast=true;
            }
          }
          ImGui::Text("select the chip you wish to export,");
          ImGui::Text("but only up to %d of each type.",(vgmExportVersion>=0x151)?2:1);
          if (hasOneAtLeast) {
            if (ImGui::MenuItem("click to export")) {
              openFileDialog(GUI_FILE_EXPORT_VGM);
            }
          } else {
            ImGui::Text("nothing to export");
          }
          ImGui::EndMenu();
        }
        int numZSMCompat=0;
        for (int i=0; i<e->song.systemLen; i++) {
          if ((e->song.system[i] == DIV_SYSTEM_VERA) || (e->song.system[i] == DIV_SYSTEM_YM2151)) numZSMCompat++;
        }
        if (numZSMCompat > 0) {
          if (ImGui::BeginMenu("export ZSM...")) {
            exitDisabledTimer=1;
            ImGui::Text("Commander X16 Zsound Music File");
            if (ImGui::InputInt("Tick Rate (Hz)",&zsmExportTickRate,1,2)) {
              if (zsmExportTickRate<1) zsmExportTickRate=1;
              if (zsmExportTickRate>44100) zsmExportTickRate=44100;
            }
            ImGui::Checkbox("loop",&zsmExportLoop);
            ImGui::SameLine();
            if (ImGui::Button("Begin Export")) {
              openFileDialog(GUI_FILE_EXPORT_ZSM);
              ImGui::CloseCurrentPopup();
            }
            ImGui::EndMenu();
          }
        }
        int numAmiga=0;
        for (int i=0; i<e->song.systemLen; i++) {
          if (e->song.system[i]==DIV_SYSTEM_AMIGA) numAmiga++;
        }
        if (numAmiga && settings.iCannotWait) {
          if (ImGui::BeginMenu("export Amiga validation data...")) {
            exitDisabledTimer=1;
            ImGui::Text(
              "this is NOT ROM export! only use for making sure the\n"
              "Furnace Amiga emulator is working properly by\n"
              "comparing it with real Amiga output."
            );
            ImGui::AlignTextToFramePadding();
            ImGui::Text("Directory");
            ImGui::SameLine();
            ImGui::InputText("##AVDPath",&workingDirROMExport);
            if (ImGui::Button("Bake Data")) {
              std::vector<DivROMExportOutput> out=e->buildROM(DIV_ROM_AMIGA_VALIDATION);
              if (workingDirROMExport.size()>0) {
                if (workingDirROMExport[workingDirROMExport.size()-1]!=DIR_SEPARATOR) workingDirROMExport+=DIR_SEPARATOR_STR;
              }
              for (DivROMExportOutput& i: out) {
                String path=workingDirROMExport+i.name;
                FILE* outFile=ps_fopen(path.c_str(),"wb");
                if (outFile!=NULL) {
                  fwrite(i.data->getFinalBuf(),1,i.data->size(),outFile);
                  fclose(outFile);
                }
                i.data->finish();
                delete i.data;
              }
              showError(fmt::sprintf("Done! Baked %d files.",(int)out.size()));
              ImGui::CloseCurrentPopup();
            }
            ImGui::EndMenu();
          }
        }
        if (ImGui::BeginMenu("export command stream...")) {
          exitDisabledTimer=1;
          ImGui::Text(
            "this option exports a text or binary file which\n"
            "contains a dump of the internal command stream\n"
            "produced when playing the song.\n\n"

            "technical/development use only!"
          );
          if (ImGui::Button("export (binary)")) {
            openFileDialog(GUI_FILE_EXPORT_CMDSTREAM_BINARY);
          }
          if (ImGui::Button("export (text)")) {
            openFileDialog(GUI_FILE_EXPORT_CMDSTREAM);
          }
          ImGui::EndMenu();
        }
        ImGui::Separator();
        if (ImGui::BeginMenu("add chip...")) {
          exitDisabledTimer=1;
          DivSystem picked=systemPicker();
          if (picked!=DIV_SYSTEM_NULL) {
            if (!e->addSystem(picked)) {
              showError("cannot add chip! ("+e->getLastError()+")");
            } else {
              MARK_MODIFIED;
            }
            ImGui::CloseCurrentPopup();
            if (e->song.autoSystem) {
              autoDetectSystem();
            }
            updateWindowTitle();
          }
          ImGui::EndMenu();
        }
        if (ImGui::BeginMenu("configure chip...")) {
          exitDisabledTimer=1;
          for (int i=0; i<e->song.systemLen; i++) {
            if (ImGui::TreeNode(fmt::sprintf("%d. %s##_SYSP%d",i+1,getSystemName(e->song.system[i]),i).c_str())) {
              drawSysConf(i,e->song.system[i],e->song.systemFlags[i],true,true);
              ImGui::TreePop();
            }
          }
          ImGui::EndMenu();
        }
        if (ImGui::BeginMenu("change chip...")) {
          exitDisabledTimer=1;
          ImGui::Checkbox("Preserve channel positions",&preserveChanPos);
          for (int i=0; i<e->song.systemLen; i++) {
            if (ImGui::BeginMenu(fmt::sprintf("%d. %s##_SYSC%d",i+1,getSystemName(e->song.system[i]),i).c_str())) {
              DivSystem picked=systemPicker();
              if (picked!=DIV_SYSTEM_NULL) {
                e->changeSystem(i,picked,preserveChanPos);
                MARK_MODIFIED;
                if (e->song.autoSystem) {
                  autoDetectSystem();
                }
                updateWindowTitle();
                ImGui::CloseCurrentPopup();
              }
              ImGui::EndMenu();
            }
          }
          ImGui::EndMenu();
        }
        if (ImGui::BeginMenu("remove chip...")) {
          exitDisabledTimer=1;
          ImGui::Checkbox("Preserve channel positions",&preserveChanPos);
          for (int i=0; i<e->song.systemLen; i++) {
            if (ImGui::MenuItem(fmt::sprintf("%d. %s##_SYSR%d",i+1,getSystemName(e->song.system[i]),i).c_str())) {
              if (!e->removeSystem(i,preserveChanPos)) {
                showError("cannot remove chip! ("+e->getLastError()+")");
              } else {
                MARK_MODIFIED;
              }
              if (e->song.autoSystem) {
                autoDetectSystem();
                updateWindowTitle();
              }
            }
          }
          ImGui::EndMenu();
        }
        ImGui::BeginDisabled(exitDisabledTimer);
        ImGui::Separator();
        if (ImGui::MenuItem("restore backup",BIND_FOR(GUI_ACTION_OPEN_BACKUP))) {
          doAction(GUI_ACTION_OPEN_BACKUP);
        }
        ImGui::Separator();
        if (ImGui::MenuItem("exit")) {
          if (modified) {
            showWarning("Unsaved changes! Save before quitting?",GUI_WARN_QUIT);
          } else {
            quit=true;
          }
        }
        ImGui::EndDisabled();
        ImGui::EndMenu();
      } else {
        exitDisabledTimer=0;
      }
      if (ImGui::BeginMenu(settings.capitalMenuBar?"Edit":"edit")) {
        ImGui::Text("...");
        ImGui::Separator();
        if (ImGui::MenuItem("undo",BIND_FOR(GUI_ACTION_UNDO))) doUndo();
        if (ImGui::MenuItem("redo",BIND_FOR(GUI_ACTION_REDO))) doRedo();
        ImGui::Separator();
        editOptions(true);
        ImGui::Separator();
        if (ImGui::MenuItem("clear...")) {
          doAction(GUI_ACTION_CLEAR);
        }
        ImGui::EndMenu();
      }
      if (ImGui::BeginMenu(settings.capitalMenuBar?"Settings":"settings")) {
  #ifndef IS_MOBILE
        if (ImGui::MenuItem("full screen",BIND_FOR(GUI_ACTION_FULLSCREEN),fullScreen)) {
          doAction(GUI_ACTION_FULLSCREEN);
        }
  #endif
        if (ImGui::MenuItem("lock layout",NULL,lockLayout)) {
          lockLayout=!lockLayout;
        }
        if (ImGui::MenuItem("basic mode",NULL,basicMode)) {
          if (basicMode) {
            showWarning("prepare to unlock the full power of Furnace!",GUI_WARN_BASIC_MODE);
          } else {
            showWarning("are you sure? this will disable several things.",GUI_WARN_BASIC_MODE);
          }
        }
        if (ImGui::MenuItem("visualizer",NULL,fancyPattern)) {
          fancyPattern=!fancyPattern;
          e->enableCommandStream(fancyPattern);
          e->getCommandStream(cmdStream);
          cmdStream.clear();
        }
        if (ImGui::MenuItem("reset layout")) {
          showWarning("Are you sure you want to reset the workspace layout?",GUI_WARN_RESET_LAYOUT);
        }
#ifdef IS_MOBILE
        if (ImGui::MenuItem("switch to mobile view")) {
          toggleMobileUI(!mobileUI);
        }
#endif
        if (ImGui::MenuItem("settings...",BIND_FOR(GUI_ACTION_WINDOW_SETTINGS))) {
          syncSettings();
          settingsOpen=true;
        }
        ImGui::EndMenu();
      }
      if (ImGui::BeginMenu(settings.capitalMenuBar?"Window":"window")) {
        if (ImGui::MenuItem("song information",BIND_FOR(GUI_ACTION_WINDOW_SONG_INFO),songInfoOpen)) songInfoOpen=!songInfoOpen;
        if (ImGui::MenuItem("subsongs",BIND_FOR(GUI_ACTION_WINDOW_SUBSONGS),subSongsOpen)) subSongsOpen=!subSongsOpen;
        if (ImGui::MenuItem("speed",BIND_FOR(GUI_ACTION_WINDOW_SPEED),speedOpen)) speedOpen=!speedOpen;
        if (settings.unifiedDataView) {
          if (ImGui::MenuItem("assets",BIND_FOR(GUI_ACTION_WINDOW_INS_LIST),insListOpen)) insListOpen=!insListOpen;
        } else {
          if (ImGui::MenuItem("instruments",BIND_FOR(GUI_ACTION_WINDOW_INS_LIST),insListOpen)) insListOpen=!insListOpen;
          if (ImGui::MenuItem("wavetables",BIND_FOR(GUI_ACTION_WINDOW_WAVE_LIST),waveListOpen)) waveListOpen=!waveListOpen;
          if (ImGui::MenuItem("samples",BIND_FOR(GUI_ACTION_WINDOW_SAMPLE_LIST),sampleListOpen)) sampleListOpen=!sampleListOpen;
        }
        if (ImGui::MenuItem("orders",BIND_FOR(GUI_ACTION_WINDOW_ORDERS),ordersOpen)) ordersOpen=!ordersOpen;
        if (ImGui::MenuItem("pattern",BIND_FOR(GUI_ACTION_WINDOW_PATTERN),patternOpen)) patternOpen=!patternOpen;
        if (ImGui::MenuItem("mixer",BIND_FOR(GUI_ACTION_WINDOW_MIXER),mixerOpen)) mixerOpen=!mixerOpen;
        if (!basicMode) {
          if (ImGui::MenuItem("grooves",BIND_FOR(GUI_ACTION_WINDOW_GROOVES),groovesOpen)) groovesOpen=!groovesOpen;
          if (ImGui::MenuItem("channels",BIND_FOR(GUI_ACTION_WINDOW_CHANNELS),channelsOpen)) channelsOpen=!channelsOpen;
        }
        if (ImGui::MenuItem("pattern manager",BIND_FOR(GUI_ACTION_WINDOW_PAT_MANAGER),patManagerOpen)) patManagerOpen=!patManagerOpen;
        if (!basicMode) {
          if (ImGui::MenuItem("chip manager",BIND_FOR(GUI_ACTION_WINDOW_SYS_MANAGER),sysManagerOpen)) sysManagerOpen=!sysManagerOpen;
          if (ImGui::MenuItem("compatibility flags",BIND_FOR(GUI_ACTION_WINDOW_COMPAT_FLAGS),compatFlagsOpen)) compatFlagsOpen=!compatFlagsOpen;
        }
        if (ImGui::MenuItem("song comments",BIND_FOR(GUI_ACTION_WINDOW_NOTES),notesOpen)) notesOpen=!notesOpen;
        ImGui::Separator();
        if (ImGui::MenuItem("instrument editor",BIND_FOR(GUI_ACTION_WINDOW_INS_EDIT),insEditOpen)) insEditOpen=!insEditOpen;
        if (ImGui::MenuItem("wavetable editor",BIND_FOR(GUI_ACTION_WINDOW_WAVE_EDIT),waveEditOpen)) waveEditOpen=!waveEditOpen;
        if (ImGui::MenuItem("sample editor",BIND_FOR(GUI_ACTION_WINDOW_SAMPLE_EDIT),sampleEditOpen)) sampleEditOpen=!sampleEditOpen;
        ImGui::Separator();
        if (ImGui::MenuItem("play/edit controls",BIND_FOR(GUI_ACTION_WINDOW_EDIT_CONTROLS),editControlsOpen)) editControlsOpen=!editControlsOpen;
        if (ImGui::MenuItem("piano/input pad",BIND_FOR(GUI_ACTION_WINDOW_PIANO),pianoOpen)) pianoOpen=!pianoOpen;
        if (ImGui::MenuItem("oscilloscope (master)",BIND_FOR(GUI_ACTION_WINDOW_OSCILLOSCOPE),oscOpen)) oscOpen=!oscOpen;
        if (ImGui::MenuItem("oscilloscope (per-channel)",BIND_FOR(GUI_ACTION_WINDOW_CHAN_OSC),chanOscOpen)) chanOscOpen=!chanOscOpen;
        if (ImGui::MenuItem("volume meter",BIND_FOR(GUI_ACTION_WINDOW_VOL_METER),volMeterOpen)) volMeterOpen=!volMeterOpen;
        if (ImGui::MenuItem("clock",BIND_FOR(GUI_ACTION_WINDOW_CLOCK),clockOpen)) clockOpen=!clockOpen;
        if (ImGui::MenuItem("register view",BIND_FOR(GUI_ACTION_WINDOW_REGISTER_VIEW),regViewOpen)) regViewOpen=!regViewOpen;
        if (ImGui::MenuItem("log viewer",BIND_FOR(GUI_ACTION_WINDOW_LOG),logOpen)) logOpen=!logOpen;
        if (ImGui::MenuItem("statistics",BIND_FOR(GUI_ACTION_WINDOW_STATS),statsOpen)) statsOpen=!statsOpen;
        if (spoilerOpen) if (ImGui::MenuItem("spoiler",NULL,spoilerOpen)) spoilerOpen=!spoilerOpen;

        ImGui::EndMenu();
      }
      if (ImGui::BeginMenu(settings.capitalMenuBar?"Help":"help")) {
        if (ImGui::MenuItem("effect list",BIND_FOR(GUI_ACTION_WINDOW_EFFECT_LIST),effectListOpen)) effectListOpen=!effectListOpen;
        if (ImGui::MenuItem("debug menu",BIND_FOR(GUI_ACTION_WINDOW_DEBUG))) debugOpen=!debugOpen;
        if (ImGui::MenuItem("inspector",BIND_FOR(GUI_ACTION_WINDOW_DEBUG))) inspectorOpen=!inspectorOpen;
        if (ImGui::MenuItem("panic",BIND_FOR(GUI_ACTION_PANIC))) e->syncReset();
        if (ImGui::MenuItem("about...",BIND_FOR(GUI_ACTION_WINDOW_ABOUT))) {
          aboutOpen=true;
          aboutScroll=0;
        }
        ImGui::EndMenu();
      }
      ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PLAYBACK_STAT]);
      if (e->isPlaying()) {
        int totalTicks=e->getTotalTicks();
        int totalSeconds=e->getTotalSeconds();

        String info;

        DivGroovePattern gp=e->getSpeeds();
        if (gp.len==2) {
          info=fmt::sprintf("| Speed %d:%d",gp.val[0],gp.val[1]);
        } else if (gp.len==1) {
          info=fmt::sprintf("| Speed %d",gp.val[0]);
        } else {
          info="| Groove";
        }

        info+=fmt::sprintf(" @ %gHz (%g BPM) | Order %d/%d | Row %d/%d | %d:%.2d:%.2d.%.2d",e->getCurHz(),calcBPM(e->getSpeeds(),e->getCurHz(),e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD),e->getOrder(),e->curSubSong->ordersLen,e->getRow(),e->curSubSong->patLen,totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000);

        ImGui::TextUnformatted(info.c_str());
      } else {
        bool hasInfo=false;
        String info;
        if (cursor.xCoarse>=0 && cursor.xCoarse<e->getTotalChannelCount()) {
          DivPattern* p=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],false);
          if (cursor.xFine>=0) switch (cursor.xFine) {
            case 0: // note
              if (p->data[cursor.y][0]>0) {
                if (p->data[cursor.y][0]==100) {
                  info=fmt::sprintf("Note off (cut)");
                } else if (p->data[cursor.y][0]==101) {
                  info=fmt::sprintf("Note off (release)");
                } else if (p->data[cursor.y][0]==102) {
                  info=fmt::sprintf("Macro release only");
                } else {
                  info=fmt::sprintf("Note on: %s",noteName(p->data[cursor.y][0],p->data[cursor.y][1]));
                }
                hasInfo=true;
              }
              break;
            case 1: // instrument
              if (p->data[cursor.y][2]>-1) {
                if (p->data[cursor.y][2]>=(int)e->song.ins.size()) {
                  info=fmt::sprintf("Ins %d: <invalid>",p->data[cursor.y][2]);
                } else {
                  DivInstrument* ins=e->getIns(p->data[cursor.y][2]);
                  info=fmt::sprintf("Ins %d: %s",p->data[cursor.y][2],ins->name);
                }
                hasInfo=true;
              }
              break;
            case 2: // volume
              if (p->data[cursor.y][3]>-1) {
                int maxVol=e->getMaxVolumeChan(cursor.xCoarse);
                if (maxVol<1 || p->data[cursor.y][3]>maxVol) {
                  info=fmt::sprintf("Set volume: %d (%.2X, INVALID!)",p->data[cursor.y][3],p->data[cursor.y][3]);
                } else {
                  info=fmt::sprintf("Set volume: %d (%.2X, %d%%)",p->data[cursor.y][3],p->data[cursor.y][3],(p->data[cursor.y][3]*100)/maxVol);
                }
                hasInfo=true;
              }
              break;
            default: // effect
              int actualCursor=((cursor.xFine+1)&(~1));
              if (p->data[cursor.y][actualCursor]>-1) {
                info=e->getEffectDesc(p->data[cursor.y][actualCursor],cursor.xCoarse,true);
                hasInfo=true;
              }
              break;
          }
        }
        if (hasInfo && (settings.statusDisplay==0 || settings.statusDisplay==2)) {
          ImGui::Text("| %s",info.c_str());
        } else if (settings.statusDisplay==1 || settings.statusDisplay==2) {
          if (curFileName!="") ImGui::Text("| %s",curFileName.c_str());
        }
      }
      ImGui::PopStyleColor();
      if (modified) {
        ImGui::Text("| modified");
      }
      ImGui::EndMainMenuBar();
    }

    MEASURE(calcChanOsc,calcChanOsc());

    if (mobileUI) {
      globalWinFlags=ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoBringToFrontOnFocus;
      //globalWinFlags=ImGuiWindowFlags_NoTitleBar;
      // scene handling goes here!
      MEASURE(mobileControls,drawMobileControls());
      switch (mobScene) {
        case GUI_SCENE_ORDERS:
          ordersOpen=true;
          curWindow=GUI_WINDOW_ORDERS;
          MEASURE(orders,drawOrders());
          break;
        case GUI_SCENE_INSTRUMENT:
          insEditOpen=true;
          curWindow=GUI_WINDOW_INS_EDIT;
          MEASURE(insEdit,drawInsEdit());
          MEASURE(piano,drawPiano());
          break;
        case GUI_SCENE_WAVETABLE:
          waveEditOpen=true;
          curWindow=GUI_WINDOW_WAVE_EDIT;
          MEASURE(waveEdit,drawWaveEdit());
          MEASURE(piano,drawPiano());
          break;
        case GUI_SCENE_SAMPLE:
          sampleEditOpen=true;
          curWindow=GUI_WINDOW_SAMPLE_EDIT;
          MEASURE(sampleEdit,drawSampleEdit());
          MEASURE(piano,drawPiano());
          break;
        case GUI_SCENE_CHANNELS:
          channelsOpen=true;
          curWindow=GUI_WINDOW_CHANNELS;
          MEASURE(channels,drawChannels());
          break;
        case GUI_SCENE_CHIPS:
          sysManagerOpen=true;
          curWindow=GUI_WINDOW_SYS_MANAGER;
          MEASURE(sysManager,drawSysManager());
          break;
        case GUI_SCENE_MIXER:
          mixerOpen=true;
          curWindow=GUI_WINDOW_MIXER;
          MEASURE(mixer,drawMixer());
          break;
        default:
          patternOpen=true;
          curWindow=GUI_WINDOW_PATTERN;
          MEASURE(pattern,drawPattern());
          MEASURE(piano,drawPiano());
          MEASURE(mobileOrderSel,drawMobileOrderSel());

          globalWinFlags=0;
          MEASURE(findReplace,drawFindReplace());
          break;
      }

      globalWinFlags=0;
      MEASURE(settings,drawSettings());
      MEASURE(debug,drawDebug());
      MEASURE(log,drawLog());
      MEASURE(compatFlags,drawCompatFlags());
      MEASURE(stats,drawStats());
      MEASURE(chanOsc,drawChanOsc());
    } else {
      globalWinFlags=0;
      ImGui::DockSpaceOverViewport(NULL,lockLayout?(ImGuiDockNodeFlags_NoWindowMenuButton|ImGuiDockNodeFlags_NoMove|ImGuiDockNodeFlags_NoResize|ImGuiDockNodeFlags_NoCloseButton|ImGuiDockNodeFlags_NoDocking|ImGuiDockNodeFlags_NoDockingSplitMe|ImGuiDockNodeFlags_NoDockingSplitOther):0);

      MEASURE(subSongs,drawSubSongs());
      MEASURE(findReplace,drawFindReplace());
      MEASURE(spoiler,drawSpoiler());
      MEASURE(pattern,drawPattern());
      MEASURE(editControls,drawEditControls());
      MEASURE(speed,drawSpeed());
      if (!basicMode) {
        MEASURE(grooves,drawGrooves());
      }
      MEASURE(songInfo,drawSongInfo());
      MEASURE(orders,drawOrders());
      MEASURE(sampleList,drawSampleList());
      MEASURE(sampleEdit,drawSampleEdit());
      MEASURE(waveList,drawWaveList());
      MEASURE(waveEdit,drawWaveEdit());
      MEASURE(insList,drawInsList());
      MEASURE(insEdit,drawInsEdit());
      MEASURE(mixer,drawMixer());

      MEASURE(readOsc,readOsc());

      MEASURE(osc,drawOsc());
      MEASURE(chanOsc,drawChanOsc());
      MEASURE(volMeter,drawVolMeter());
      MEASURE(settings,drawSettings());
      MEASURE(debug,drawDebug());
      MEASURE(stats,drawStats());
      if (!basicMode) {
        MEASURE(compatFlags,drawCompatFlags());
      }
      MEASURE(piano,drawPiano());
      MEASURE(notes,drawNotes());
      if (!basicMode) {
        MEASURE(channels,drawChannels());
      }
      MEASURE(patManager,drawPatManager());
      if (!basicMode) {
        MEASURE(sysManager,drawSysManager());
      }
      MEASURE(clock,drawClock());
      MEASURE(regView,drawRegView());
      MEASURE(log,drawLog());
      MEASURE(effectList,drawEffectList());
    }

    for (int i=0; i<e->getTotalChannelCount(); i++) {
      keyHit1[i]-=0.2f;
      if (keyHit1[i]<0.0f) keyHit1[i]=0.0f;
    }

    activateTutorial(GUI_TUTORIAL_OVERVIEW);

    if (inspectorOpen) ImGui::ShowMetricsWindow(&inspectorOpen);

    if (firstFrame) {
      firstFrame=false;
#ifdef IS_MOBILE
      SDL_GetWindowSize(sdlWin,&scrW,&scrH);
      portrait=(scrW<scrH);
      logV("portrait: %d (%dx%d)",portrait,scrW,scrH);

      rend->getOutputSize(canvasW,canvasH);
#endif
      if (patternOpen) nextWindow=GUI_WINDOW_PATTERN;
#ifdef __APPLE__
      SDL_RaiseWindow(sdlWin);
#endif
    }

#ifndef NFD_NON_THREADED
    if (fileDialog->isOpen() && settings.sysFileDialog) {
      ImGui::OpenPopup("System File Dialog Pending");
    }

    if (ImGui::BeginPopupModal("System File Dialog Pending",NULL,ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoBackground|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove)) {
      if (!fileDialog->isOpen()) {
        ImGui::CloseCurrentPopup();
      }
      ImDrawList* dl=ImGui::GetForegroundDrawList();
      dl->AddRectFilled(ImVec2(0.0f,0.0f),ImVec2(canvasW,canvasH),ImGui::ColorConvertFloat4ToU32(uiColors[GUI_COLOR_MODAL_BACKDROP]));
      ImGui::EndPopup();
    }
#endif

    if (fileDialog->render(mobileUI?ImVec2(canvasW-(portrait?0:(60.0*dpiScale)),canvasH-60.0*dpiScale):ImVec2(600.0f*dpiScale,400.0f*dpiScale),ImVec2(canvasW-((mobileUI && !portrait)?(60.0*dpiScale):0),canvasH-(mobileUI?(60.0*dpiScale):0)))) {
      bool openOpen=false;
      //ImGui::GetIO().ConfigFlags&=~ImGuiConfigFlags_NavEnableKeyboard;
      if ((curFileDialog==GUI_FILE_INS_OPEN || curFileDialog==GUI_FILE_INS_OPEN_REPLACE) && prevIns!=-3) {
        if (curFileDialog==GUI_FILE_INS_OPEN_REPLACE) {
          if (prevInsData!=NULL) {
            if (prevIns>=0 && prevIns<(int)e->song.ins.size()) {
              *e->song.ins[prevIns]=*prevInsData;
            }
          }
        } else {
          curIns=prevIns;
          wavePreviewInit=true;
          updateFMPreview=true;
        }
        prevIns=-3;
      }
      switch (curFileDialog) {
        case GUI_FILE_OPEN:
        case GUI_FILE_SAVE:
        case GUI_FILE_SAVE_DMF:
        case GUI_FILE_SAVE_DMF_LEGACY:
          workingDirSong=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_INS_OPEN:
        case GUI_FILE_INS_OPEN_REPLACE:
        case GUI_FILE_INS_SAVE:
        case GUI_FILE_INS_SAVE_DMP:
          workingDirIns=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_WAVE_OPEN:
        case GUI_FILE_WAVE_OPEN_REPLACE:
        case GUI_FILE_WAVE_SAVE:
        case GUI_FILE_WAVE_SAVE_DMW:
        case GUI_FILE_WAVE_SAVE_RAW:
          workingDirWave=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_SAMPLE_OPEN:
        case GUI_FILE_SAMPLE_OPEN_RAW:
        case GUI_FILE_SAMPLE_OPEN_REPLACE:
        case GUI_FILE_SAMPLE_OPEN_REPLACE_RAW:
        case GUI_FILE_SAMPLE_SAVE:
        case GUI_FILE_SAMPLE_SAVE_RAW:
          workingDirSample=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_EXPORT_AUDIO_ONE:
        case GUI_FILE_EXPORT_AUDIO_PER_SYS:
        case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL:
          workingDirAudioExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_EXPORT_VGM:
          workingDirVGMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_EXPORT_ZSM:
          workingDirZSMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_EXPORT_ROM:
        case GUI_FILE_EXPORT_CMDSTREAM:
        case GUI_FILE_EXPORT_CMDSTREAM_BINARY:
          workingDirROMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_LOAD_MAIN_FONT:
        case GUI_FILE_LOAD_HEAD_FONT:
        case GUI_FILE_LOAD_PAT_FONT:
          workingDirFont=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_IMPORT_COLORS:
        case GUI_FILE_EXPORT_COLORS:
          workingDirColors=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_IMPORT_KEYBINDS:
        case GUI_FILE_EXPORT_KEYBINDS:
          workingDirKeybinds=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_IMPORT_LAYOUT:
        case GUI_FILE_EXPORT_LAYOUT:
          workingDirLayout=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_YRW801_ROM_OPEN:
        case GUI_FILE_TG100_ROM_OPEN:
        case GUI_FILE_MU5_ROM_OPEN:
          workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_CMDSTREAM_OPEN:
          workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_TEST_OPEN:
        case GUI_FILE_TEST_OPEN_MULTI:
        case GUI_FILE_TEST_SAVE:
          workingDirTest=fileDialog->getPath()+DIR_SEPARATOR_STR;
          break;
        case GUI_FILE_OPEN_BACKUP:
          break;
      }
      if (fileDialog->isError()) {
#if defined(_WIN32) || defined(__APPLE__)
        showError("there was an error in the file dialog! you may want to report this issue to:\nhttps://github.com/tildearrow/furnace/issues\ncheck the Log Viewer (window > log viewer) for more information.\n\nfor now please disable the system file picker in Settings > General.");
#else
#ifdef ANDROID
        showError("can't do anything without Storage permissions!");
#else
        showError("Zenity/KDialog not available!\nplease install one of these, or disable the system file picker in Settings > General.");
#endif
#endif
      }
      if (fileDialog->accepted()) {
        if (fileDialog->getFileName().empty()) {
          fileName="";
        } else {
          fileName=fileDialog->getFileName()[0];
        }
        if (fileName!="") {
          if (curFileDialog==GUI_FILE_SAVE) {
            checkExtension(".fur");
          }
          if (curFileDialog==GUI_FILE_SAVE_DMF) {
            checkExtension(".dmf");
          }
          if (curFileDialog==GUI_FILE_SAVE_DMF_LEGACY) {
            checkExtension(".dmf");
          }
          if (curFileDialog==GUI_FILE_SAMPLE_SAVE ||
              curFileDialog==GUI_FILE_EXPORT_AUDIO_ONE ||
              curFileDialog==GUI_FILE_EXPORT_AUDIO_PER_SYS ||
              curFileDialog==GUI_FILE_EXPORT_AUDIO_PER_CHANNEL) {
            checkExtension(".wav");
          }
          if (curFileDialog==GUI_FILE_INS_SAVE) {
            checkExtension(".fui");
          }
          if (curFileDialog==GUI_FILE_INS_SAVE_DMP) {
            checkExtension(".dmp");
          }
          if (curFileDialog==GUI_FILE_WAVE_SAVE) {
            checkExtension(".fuw");
          }
          if (curFileDialog==GUI_FILE_WAVE_SAVE_DMW) {
            checkExtension(".dmw");
          }
          if (curFileDialog==GUI_FILE_WAVE_SAVE_RAW) {
            checkExtension(".raw");
          }
          if (curFileDialog==GUI_FILE_EXPORT_VGM) {
            checkExtension(".vgm");
          }
          if (curFileDialog==GUI_FILE_EXPORT_ZSM) {
            checkExtension(".zsm");
          }
          if (curFileDialog==GUI_FILE_EXPORT_CMDSTREAM) {
            checkExtension(".txt");
          }
          if (curFileDialog==GUI_FILE_EXPORT_CMDSTREAM_BINARY) {
            checkExtension(".bin");
          }
          if (curFileDialog==GUI_FILE_EXPORT_COLORS) {
            checkExtension(".cfgc");
          }
          if (curFileDialog==GUI_FILE_EXPORT_KEYBINDS) {
            checkExtension(".cfgk");
          }
          if (curFileDialog==GUI_FILE_EXPORT_LAYOUT) {
            checkExtension(".ini");
          }
          String copyOfName=fileName;
          switch (curFileDialog) {
            case GUI_FILE_OPEN:
            case GUI_FILE_OPEN_BACKUP:
              if (load(copyOfName)>0) {
                showError(fmt::sprintf("Error while loading file! (%s)",lastError));
              }
              break;
            case GUI_FILE_SAVE: {
              bool saveWasSuccessful=true;
              if (save(copyOfName,0)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
                saveWasSuccessful=false;
              }
              if (saveWasSuccessful && postWarnAction!=GUI_WARN_GENERIC) {
                switch (postWarnAction) {
                  case GUI_WARN_QUIT:
                    quit=true;
                    break;
                  case GUI_WARN_NEW:
                    displayNew=true;
                    break;
                  case GUI_WARN_OPEN:
                    openOpen=true;
                    break;
                  case GUI_WARN_OPEN_DROP:
                    if (load(nextFile)>0) {
                      showError(fmt::sprintf("Error while loading file! (%s)",lastError));
                    }
                    nextFile="";
                    break;
                  case GUI_WARN_OPEN_BACKUP:
                    openFileDialog(GUI_FILE_OPEN_BACKUP);
                    break;
                  default:
                    break;
                }
                postWarnAction=GUI_WARN_GENERIC;
              } else if (postWarnAction==GUI_WARN_OPEN_DROP) {
                nextFile="";
              }
              break;
            }
            case GUI_FILE_SAVE_DMF:
              logD("saving: %s",copyOfName.c_str());
              if (save(copyOfName,26)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
              }
              break;
            case GUI_FILE_SAVE_DMF_LEGACY:
              logD("saving: %s",copyOfName.c_str());
              if (save(copyOfName,24)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
              }
              break;
            case GUI_FILE_INS_SAVE:
              if (curIns>=0 && curIns<(int)e->song.ins.size()) {
                e->song.ins[curIns]->save(copyOfName.c_str(),false,&e->song);
              }
              break;
            case GUI_FILE_INS_SAVE_DMP:
              if (curIns>=0 && curIns<(int)e->song.ins.size()) {
                if (!e->song.ins[curIns]->saveDMP(copyOfName.c_str())) {
                  showError("error while saving instrument! make sure your instrument is compatible.");
                }
              }
              break;
            case GUI_FILE_WAVE_SAVE:
              if (curWave>=0 && curWave<(int)e->song.wave.size()) {
                e->song.wave[curWave]->save(copyOfName.c_str());
              }
              break;
            case GUI_FILE_WAVE_SAVE_DMW:
              if (curWave>=0 && curWave<(int)e->song.wave.size()) {
                e->song.wave[curWave]->saveDMW(copyOfName.c_str());
              }
              break;
            case GUI_FILE_WAVE_SAVE_RAW:
              if (curWave>=0 && curWave<(int)e->song.wave.size()) {
                e->song.wave[curWave]->saveRaw(copyOfName.c_str());
              }
              break;
            case GUI_FILE_SAMPLE_OPEN: {
              String errs="there were some errors while loading wavetables:\n";
              bool warn=false;
              for (String i: fileDialog->getFileName()) {
                DivSample* s=e->sampleFromFile(i.c_str());
                if (s==NULL) {
                  if (fileDialog->getFileName().size()>1) {
                    warn=true;
                    errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError());
                  } else {
                    showError(e->getLastError());
                  }
                } else {
                  if (e->addSamplePtr(s)==-1) {
                    if (fileDialog->getFileName().size()>1) {
                      warn=true;
                      errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError());
                    } else {
                      showError(e->getLastError());
                    }
                  } else {
                    MARK_MODIFIED;
                  }
                }
              }
              if (warn) {
                showWarning(errs,GUI_WARN_GENERIC);
              }
              break;
            }
            case GUI_FILE_SAMPLE_OPEN_REPLACE: {
              DivSample* s=e->sampleFromFile(copyOfName.c_str());
              if (s==NULL) {
                showError(e->getLastError());
              } else {
                if (curSample>=0 && curSample<(int)e->song.sample.size()) {
                  e->lockEngine([this,s]() {
                    // if it crashes here please tell me...
                    DivSample* oldSample=e->song.sample[curSample];
                    e->song.sample[curSample]=s;
                    delete oldSample;
                    e->renderSamples();
                    MARK_MODIFIED;
                  });
                } else {
                  showError("...but you haven't selected a sample!");
                  delete s;
                }
              }
              break;
            }
            case GUI_FILE_SAMPLE_OPEN_RAW:
            case GUI_FILE_SAMPLE_OPEN_REPLACE_RAW:
              pendingRawSample=copyOfName;
              pendingRawSampleReplace=(curFileDialog==GUI_FILE_SAMPLE_OPEN_REPLACE_RAW);
              displayPendingRawSample=true;
              break;
            case GUI_FILE_SAMPLE_SAVE:
              if (curSample>=0 && curSample<(int)e->song.sample.size()) {
                if (!e->song.sample[curSample]->save(copyOfName.c_str())) {
                  showError("could not save sample! open Log Viewer for more information.");
                }
              }
              break;
            case GUI_FILE_SAMPLE_SAVE_RAW:
              if (curSample>=0 && curSample<(int)e->song.sample.size()) {
                if (!e->song.sample[curSample]->saveRaw(copyOfName.c_str())) {
                  showError("could not save sample! open Log Viewer for more information.");
                }
              }
              break;
            case GUI_FILE_EXPORT_AUDIO_ONE:
              exportAudio(copyOfName,DIV_EXPORT_MODE_ONE);
              break;
            case GUI_FILE_EXPORT_AUDIO_PER_SYS:
              exportAudio(copyOfName,DIV_EXPORT_MODE_MANY_SYS);
              break;
            case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL:
              exportAudio(copyOfName,DIV_EXPORT_MODE_MANY_CHAN);
              break;
            case GUI_FILE_INS_OPEN: {
              std::vector<DivInstrument*> instruments;
              bool ask=false;
              bool warn=false;
              String warns="there were some warnings/errors while loading instruments:\n";
              int sampleCountBefore=e->song.sampleLen;
              for (String i: fileDialog->getFileName()) {
                std::vector<DivInstrument*> insTemp=e->instrumentFromFile(i.c_str());
                if (insTemp.empty()) {
                  warn=true;
                  warns+=fmt::sprintf("> %s: cannot load instrument! (%s)\n",i,e->getLastError());
                } else if (!e->getWarnings().empty()) {
                  warn=true;
                  warns+=fmt::sprintf("> %s:\n%s\n",i,e->getWarnings());
                }
                if (insTemp.size()>1) ask=true;
                for (DivInstrument* j: insTemp) {
                  instruments.push_back(j);
                }
              }
              if (e->song.sampleLen!=sampleCountBefore) {
                e->renderSamplesP();
              }
              if (warn) {
                if (instruments.empty()) {
                  if (fileDialog->getFileName().size()>1) {
                    showError(warns);
                  } else {
                    showError("cannot load instrument! ("+e->getLastError()+")");
                  }
                } else {
                  showWarning(warns,GUI_WARN_GENERIC);
                }
              } else if (instruments.empty()) {
                showError("congratulations! you managed to load nothing.\nyou are entitled to a bug report.");
              }
              if (!instruments.empty()) {
                if (ask) { // ask which instruments to load
                  for (DivInstrument* i: instruments) {
                    pendingIns.push_back(std::make_pair(i,false));
                  }
                  displayPendingIns=true;
                  pendingInsSingle=false;
                } else { // load the only instrument
                  for (DivInstrument* i: instruments) {
                    e->addInstrumentPtr(i);
                  }
                }
              }
              break;
            }
            case GUI_FILE_INS_OPEN_REPLACE: {
              int sampleCountBefore=e->song.sampleLen;
              std::vector<DivInstrument*> instruments=e->instrumentFromFile(copyOfName.c_str());
              if (!instruments.empty()) {
                if (e->song.sampleLen!=sampleCountBefore) {
                  e->renderSamplesP();
                }
                if (!e->getWarnings().empty()) {
                  showWarning(e->getWarnings(),GUI_WARN_GENERIC);
                }
                if (instruments.size()>1) { // ask which instrument
                  for (DivInstrument* i: instruments) {
                    pendingIns.push_back(std::make_pair(i,false));
                  }
                  displayPendingIns=true;
                  pendingInsSingle=true;
                } else { // replace with the only instrument
                  if (curIns>=0 && curIns<(int)e->song.ins.size()) {
                    *e->song.ins[curIns]=*instruments[0];
                  } else {
                    showError("...but you haven't selected an instrument!");
                  }
                  for (DivInstrument* i: instruments) {
                    delete i;
                  }
                }
              } else {
                showError("cannot load instrument! ("+e->getLastError()+")");
              }
              break;
            }
            case GUI_FILE_WAVE_OPEN: {
              String errs="there were some errors while loading wavetables:\n";
              bool warn=false;
              for (String i: fileDialog->getFileName()) {
                DivWavetable* wave=e->waveFromFile(i.c_str());
                if (wave==NULL) {
                  if (fileDialog->getFileName().size()>1) {
                    warn=true;
                    errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError());
                  } else {
                    showError("cannot load wavetable! ("+e->getLastError()+")");
                  }
                } else {
                  if (e->addWavePtr(wave)==-1) {
                    if (fileDialog->getFileName().size()>1) {
                      warn=true;
                      errs+=fmt::sprintf("- %s: %s\n",i,e->getLastError());
                    } else {
                      showError("cannot load wavetable! ("+e->getLastError()+")");
                    }
                  } else {
                    MARK_MODIFIED;
                    RESET_WAVE_MACRO_ZOOM;
                  }
                }
              }
              if (warn) {
                showWarning(errs,GUI_WARN_GENERIC);
              }
              break;
            }
            case GUI_FILE_WAVE_OPEN_REPLACE: {
              DivWavetable* wave=e->waveFromFile(copyOfName.c_str());
              if (wave==NULL) {
                showError("cannot load wavetable! ("+e->getLastError()+")");
              } else {
                if (curWave>=0 && curWave<(int)e->song.wave.size()) {
                  e->lockEngine([this,wave]() {
                    *e->song.wave[curWave]=*wave;
                    MARK_MODIFIED;
                  });
                } else {
                  showError("...but you haven't selected a wavetable!");
                }
                delete wave;
              }
              break;
            }
            case GUI_FILE_EXPORT_VGM: {
              SafeWriter* w=e->saveVGM(willExport,vgmExportLoop,vgmExportVersion,vgmExportPatternHints,vgmExportDirectStream,vgmExportTrailingTicks);
              if (w!=NULL) {
                FILE* f=ps_fopen(copyOfName.c_str(),"wb");
                if (f!=NULL) {
                  fwrite(w->getFinalBuf(),1,w->size(),f);
                  fclose(f);
                } else {
                  showError("could not open file!");
                }
                w->finish();
                delete w;
                if (!e->getWarnings().empty()) {
                  showWarning(e->getWarnings(),GUI_WARN_GENERIC);
                }
              } else {
                showError(fmt::sprintf("could not write VGM! (%s)",e->getLastError()));
              }
              break;
            }
            case GUI_FILE_EXPORT_ZSM: {
              SafeWriter* w=e->saveZSM(zsmExportTickRate,zsmExportLoop);
              if (w!=NULL) {
                FILE* f=ps_fopen(copyOfName.c_str(),"wb");
                if (f!=NULL) {
                  fwrite(w->getFinalBuf(),1,w->size(),f);
                  fclose(f);
                } else {
                  showError("could not open file!");
                }
                w->finish();
                delete w;
                if (!e->getWarnings().empty()) {
                  showWarning(e->getWarnings(),GUI_WARN_GENERIC);
                }
              } else {
                showError(fmt::sprintf("Could not write ZSM! (%s)",e->getLastError()));
              }
              break;
            }
            case GUI_FILE_EXPORT_ROM:
              showError("Coming soon!");
              break;
            case GUI_FILE_EXPORT_CMDSTREAM:
            case GUI_FILE_EXPORT_CMDSTREAM_BINARY: {
              bool isBinary=(curFileDialog==GUI_FILE_EXPORT_CMDSTREAM_BINARY);

              SafeWriter* w=e->saveCommand(isBinary);
              if (w!=NULL) {
                FILE* f=ps_fopen(copyOfName.c_str(),"wb");
                if (f!=NULL) {
                  fwrite(w->getFinalBuf(),1,w->size(),f);
                  fclose(f);
                } else {
                  showError("could not open file!");
                }
                w->finish();
                delete w;
                if (!e->getWarnings().empty()) {
                  showWarning(e->getWarnings(),GUI_WARN_GENERIC);
                }
              } else {
                showError(fmt::sprintf("could not write command stream! (%s)",e->getLastError()));
              }
              break;
            }
            case GUI_FILE_LOAD_MAIN_FONT:
              settings.mainFontPath=copyOfName;
              break;
            case GUI_FILE_LOAD_HEAD_FONT:
              settings.headFontPath=copyOfName;
              break;
            case GUI_FILE_LOAD_PAT_FONT:
              settings.patFontPath=copyOfName;
              break;
            case GUI_FILE_IMPORT_COLORS:
              importColors(copyOfName);
              break;
            case GUI_FILE_IMPORT_KEYBINDS:
              importKeybinds(copyOfName);
              break;
            case GUI_FILE_IMPORT_LAYOUT:
              importLayout(copyOfName);
              break;
            case GUI_FILE_EXPORT_COLORS:
              exportColors(copyOfName);
              break;
            case GUI_FILE_EXPORT_KEYBINDS:
              exportKeybinds(copyOfName);
              break;
            case GUI_FILE_EXPORT_LAYOUT:
              exportLayout(copyOfName);
              break;
            case GUI_FILE_YRW801_ROM_OPEN:
              settings.yrw801Path=copyOfName;
              break;
            case GUI_FILE_TG100_ROM_OPEN:
              settings.tg100Path=copyOfName;
              break;
            case GUI_FILE_MU5_ROM_OPEN:
              settings.mu5Path=copyOfName;
              break;
            case GUI_FILE_CMDSTREAM_OPEN:
              if (loadStream(copyOfName)>0) {
                showError(fmt::sprintf("Error while loading file! (%s)",lastError));
              }
              break;
            case GUI_FILE_TEST_OPEN:
              showWarning(fmt::sprintf("You opened: %s",copyOfName),GUI_WARN_GENERIC);
              break;
            case GUI_FILE_TEST_OPEN_MULTI: {
              String msg="You opened:";
              for (String i: fileDialog->getFileName()) {
                msg+=fmt::sprintf("\n- %s",i);
              }
              showWarning(msg,GUI_WARN_GENERIC);
              break;
            }
            case GUI_FILE_TEST_SAVE:
              showWarning(fmt::sprintf("You saved: %s",copyOfName),GUI_WARN_GENERIC);
              break;
          }
          curFileDialog=GUI_FILE_OPEN;
        }
      }
      fileDialog->close();
      postWarnAction=GUI_WARN_GENERIC;

      if (openOpen) {
        openFileDialog(GUI_FILE_OPEN);
      }
    }

    if (warnQuit && introPos>=11.0) {
      warnQuit=false;
      ImGui::OpenPopup("Warning");
    }

    if (displayError && introPos>=11.0) {
      displayError=false;
      ImGui::OpenPopup("Error");
    }

    if (displayPendingIns) {
      displayPendingIns=false;
      ImGui::OpenPopup("Select Instrument");
    }

    if (displayPendingRawSample) {
      displayPendingRawSample=false;
      ImGui::OpenPopup("Import Raw Sample");
    }

    if (displayInsTypeList) {
      displayInsTypeList=false;
      ImGui::OpenPopup("InsTypeList");
    }

    if (displayExporting) {
      displayExporting=false;
      ImGui::OpenPopup("Rendering...");
    }

    if (displayNew) {
      newSongQuery="";
      newSongFirstFrame=true;
      displayNew=false;
      if (settings.newSongBehavior==1) {
        e->createNewFromDefaults();
        undoHist.clear();
        redoHist.clear();
        curFileName="";
        modified=false;
        curNibble=false;
        orderNibble=false;
        orderCursor=-1;
        samplePos=0;
        updateSampleTex=true;
        selStart=SelectionPoint();
        selEnd=SelectionPoint();
        cursor=SelectionPoint();
        updateWindowTitle();
      } else {
        ImGui::OpenPopup("New Song");
      }
    }

    if (displayEditString) {
      ImGui::OpenPopup("EditString");
    }

    if (nextWindow==GUI_WINDOW_ABOUT) {
      aboutOpen=true;
      nextWindow=GUI_WINDOW_NOTHING;
    }
    if (aboutOpen) drawAbout();

    MEASURE_BEGIN(popup);

    centerNextWindow("Rendering...",canvasW,canvasH);
    if (ImGui::BeginPopupModal("Rendering...",NULL,ImGuiWindowFlags_AlwaysAutoResize)) {
      ImGui::Text("Please wait...");
      if (ImGui::Button("Abort")) {
        if (e->haltAudioFile()) {
          ImGui::CloseCurrentPopup();
        }
      }
      if (!e->isExporting()) {
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndPopup();
    }

    drawTutorial();

    ImVec2 newSongMinSize=mobileUI?ImVec2(canvasW-(portrait?0:(60.0*dpiScale)),canvasH-60.0*dpiScale):ImVec2(400.0f*dpiScale,200.0f*dpiScale);
    ImVec2 newSongMaxSize=ImVec2(canvasW-((mobileUI && !portrait)?(60.0*dpiScale):0),canvasH-(mobileUI?(60.0*dpiScale):0));
    ImGui::SetNextWindowSizeConstraints(newSongMinSize,newSongMaxSize);
    if (ImGui::BeginPopupModal("New Song",NULL,ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoScrollWithMouse|ImGuiWindowFlags_NoScrollbar)) {
      ImGui::SetWindowPos(ImVec2(((canvasW)-ImGui::GetWindowSize().x)*0.5,((canvasH)-ImGui::GetWindowSize().y)*0.5));
      if (ImGui::GetWindowSize().x<newSongMinSize.x || ImGui::GetWindowSize().y<newSongMinSize.y) {
        ImGui::SetWindowSize(newSongMinSize,ImGuiCond_Always);
      }
      drawNewSong();
      ImGui::EndPopup();
    }

    centerNextWindow("Error",canvasW,canvasH);
    if (ImGui::BeginPopupModal("Error",NULL,ImGuiWindowFlags_AlwaysAutoResize)) {
      ImGui::Text("%s",errorString.c_str());
      if (ImGui::Button("OK")) {
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndPopup();
    }

    centerNextWindow("Warning",canvasW,canvasH);
    if (ImGui::BeginPopupModal("Warning",NULL,ImGuiWindowFlags_AlwaysAutoResize)) {
      ImGui::Text("%s",warnString.c_str());
      switch (warnAction) {
        case GUI_WARN_QUIT:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            if (curFileName=="" || curFileName.find(backupPath)==0 || e->song.version>=0xff00) {
              openFileDialog(GUI_FILE_SAVE);
              postWarnAction=GUI_WARN_QUIT;
            } else {
              if (save(curFileName,e->song.isDMF?e->song.version:0)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
              } else {
                quit=true;
              }
            }
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
            quit=true;
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_NEW:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            if (curFileName=="" || curFileName.find(backupPath)==0 || e->song.version>=0xff00) {
              openFileDialog(GUI_FILE_SAVE);
              postWarnAction=GUI_WARN_NEW;
            } else {
              if (save(curFileName,e->song.isDMF?e->song.version:0)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
              } else {
                displayNew=true;
              }
            }
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
            displayNew=true;
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_OPEN:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            if (curFileName=="" || curFileName.find(backupPath)==0 || e->song.version>=0xff00) {
              openFileDialog(GUI_FILE_SAVE);
              postWarnAction=GUI_WARN_OPEN;
            } else {
              if (save(curFileName,e->song.isDMF?e->song.version:0)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
              } else {
                openFileDialog(GUI_FILE_OPEN);
              }
            }
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
            openFileDialog(GUI_FILE_OPEN);
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_OPEN_BACKUP:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            if (curFileName=="" || curFileName.find(backupPath)==0 || e->song.version>=0xff00) {
              openFileDialog(GUI_FILE_SAVE);
              postWarnAction=GUI_WARN_OPEN_BACKUP;
            } else {
              if (save(curFileName,e->song.isDMF?e->song.version:0)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
              } else {
                openFileDialog(GUI_FILE_OPEN_BACKUP);
              }
            }
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
            openFileDialog(GUI_FILE_OPEN_BACKUP);
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_OPEN_DROP:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            if (curFileName=="" || curFileName.find(backupPath)==0 || e->song.version>=0xff00) {
              openFileDialog(GUI_FILE_SAVE);
              postWarnAction=GUI_WARN_OPEN_DROP;
            } else {
              if (save(curFileName,e->song.isDMF?e->song.version:0)>0) {
                showError(fmt::sprintf("Error while saving file! (%s)",lastError));
                nextFile="";
              } else {
                if (load(nextFile)>0) {
                  showError(fmt::sprintf("Error while loading file! (%s)",lastError));
                }
                nextFile="";
              }
            }
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
            if (load(nextFile)>0) {
              showError(fmt::sprintf("Error while loading file! (%s)",lastError));
            }
            nextFile="";
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
            nextFile="";
          }
          break;
        case GUI_WARN_RESET_LAYOUT:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            if (!mobileUI) {
              ImGui::LoadIniSettingsFromMemory(defaultLayout);
              if (!ImGui::SaveIniSettingsToDisk(finalLayoutPath,true)) {
                reportError(fmt::sprintf("could NOT save layout! %s",strerror(errno)));
              }
            }
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_RESET_KEYBINDS:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            resetKeybinds();
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_RESET_COLORS:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            resetColors();
            applyUISettings(false);
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_CLOSE_SETTINGS:
          if (ImGui::Button("Yes")) {
            ImGui::CloseCurrentPopup();
            settingsOpen=false;
            willCommit=true;
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
            settingsOpen=false;
            syncSettings();
          }
          ImGui::SameLine();
          if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_CLEAR:
          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->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*DIV_MAX_PATTERNS);
              e->curSubSong->ordersLen=1;
            });
            e->setOrder(0);
            curOrder=0;
            oldOrder=0;
            oldOrder1=0;
            MARK_MODIFIED;
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("Pattern")) {
            stop();
            e->lockEngine([this]() {
              for (int i=0; i<e->getTotalChannelCount(); i++) {
                DivPattern* pat=e->curPat[i].getPattern(e->curOrders->ord[i][curOrder],true);
                memset(pat->data,-1,DIV_MAX_ROWS*DIV_MAX_COLS*sizeof(short));
                for (int j=0; j<DIV_MAX_ROWS; j++) {
                  pat->data[j][0]=0;
                  pat->data[j][1]=0;
                }
              }
            });
            MARK_MODIFIED;
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("Instruments")) {
            stop();
            e->lockEngine([this]() {
              e->song.clearInstruments();
            });
            curIns=-1;
            MARK_MODIFIED;
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("Wavetables")) {
            stop();
            e->lockEngine([this]() {
              e->song.clearWavetables();
            });
            curWave=0;
            MARK_MODIFIED;
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("Samples")) {
            stop();
            e->lockEngine([this]() {
              e->song.clearSamples();
            });
            curSample=0;
            ImGui::CloseCurrentPopup();
          }

          if (ImGui::Button("Wait! What am I doing? Cancel!") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_SUBSONG_DEL:
          if (ImGui::Button("Yes")) {
            if (e->removeSubSong(e->getCurrentSubSong())) {
              undoHist.clear();
              redoHist.clear();
              updateScroll(0);
              oldOrder=0;
              oldOrder1=0;
              oldRow=0;
              cursor.xCoarse=0;
              cursor.xFine=0;
              cursor.y=0;
              selStart=cursor;
              selEnd=cursor;
              curOrder=0;
              MARK_MODIFIED;
            }
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_SYSTEM_DEL:
          if (ImGui::Button("Yes")) {
            e->removeSystem(sysToDelete,preserveChanPos);
            if (e->song.autoSystem) {
              autoDetectSystem();
              updateWindowTitle();
              MARK_MODIFIED;
            }
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_CLEAR_HISTORY:
          if (ImGui::Button("Yes")) {
            recentFile.clear();
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_BASIC_MODE:
          if (ImGui::Button("Yes")) {
            basicMode=!basicMode;
            ImGui::CloseCurrentPopup();
          }
          ImGui::SameLine();
          if (ImGui::Button("No")) {
            ImGui::CloseCurrentPopup();
          }
          break;
        case GUI_WARN_GENERIC:
          if (ImGui::Button("OK")) {
            ImGui::CloseCurrentPopup();
          }
          break;
      }
      ImGui::EndPopup();
    }

    if (ImGui::BeginPopup("InsTypeList",ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) {
      char temp[1024];
      for (DivInstrumentType& i: makeInsTypeList) {
        strncpy(temp,insTypes[i],1023);
        if (ImGui::MenuItem(temp)) {
          // create ins
          curIns=e->addInstrument(-1,i);
          if (curIns==-1) {
            showError("too many instruments!");
          } else {
            if (displayInsTypeListMakeInsSample>=0 && displayInsTypeListMakeInsSample<(int)e->song.sample.size()) {
              e->song.ins[curIns]->type=i;
              e->song.ins[curIns]->name=e->song.sample[displayInsTypeListMakeInsSample]->name;
              e->song.ins[curIns]->amiga.initSample=displayInsTypeListMakeInsSample;
              if (i!=DIV_INS_AMIGA) e->song.ins[curIns]->amiga.useSample=true;
              nextWindow=GUI_WINDOW_INS_EDIT;
              wavePreviewInit=true;
              updateFMPreview=true;
            }

            if (settings.blankIns) {
              e->song.ins[curIns]->fm.fb=0;
              for (int i=0; i<4; i++) {
                e->song.ins[curIns]->fm.op[i]=DivInstrumentFM::Operator();
                e->song.ins[curIns]->fm.op[i].ar=31;
                e->song.ins[curIns]->fm.op[i].dr=31;
                e->song.ins[curIns]->fm.op[i].rr=15;
                e->song.ins[curIns]->fm.op[i].tl=127;
                e->song.ins[curIns]->fm.op[i].dt=3;
              }
            }

            MARK_MODIFIED;
          }
        }
      }
      ImGui::EndPopup();
    }

    // TODO:
    // - multiple selection
    // - replace instrument
    centerNextWindow("Select Instrument",canvasW,canvasH);
    if (ImGui::BeginPopupModal("Select Instrument",NULL,ImGuiWindowFlags_AlwaysAutoResize)) {
      bool quitPlease=false;
      if (pendingInsSingle) {
        ImGui::Text("this is an instrument bank! select which one to use:");
      } else {
        ImGui::AlignTextToFramePadding();
        ImGui::Text("this is an instrument bank! select which ones to load:");
        ImGui::SameLine();
        if (ImGui::Button("All")) {
          for (std::pair<DivInstrument*,bool>& i: pendingIns) {
            i.second=true;
          }
        }
        ImGui::SameLine();
        if (ImGui::Button("None")) {
          for (std::pair<DivInstrument*,bool>& i: pendingIns) {
            i.second=false;
          }
        }
      }
      bool anySelected=false;
      float sizeY=ImGui::GetFrameHeightWithSpacing()*pendingIns.size();
      if (sizeY>(canvasH-180.0*dpiScale)) {
        sizeY=canvasH-180.0*dpiScale;
        if (sizeY<60.0*dpiScale) sizeY=60.0*dpiScale;
      }
      if (ImGui::BeginTable("PendingInsList",1,ImGuiTableFlags_ScrollY,ImVec2(0.0f,sizeY))) {
        for (size_t i=0; i<pendingIns.size(); i++) {
          ImGui::TableNextRow();
          ImGui::TableNextColumn();
          String id=fmt::sprintf("%d: %s",(int)i,pendingIns[i].first->name);
          if (pendingInsSingle) {
            if (ImGui::Selectable(id.c_str())) {
              pendingIns[i].second=true;
              quitPlease=true;
            }
          } else {
            ImGui::Checkbox(id.c_str(),&pendingIns[i].second);
          }
          if (pendingIns[i].second) anySelected=true;
        }
        ImGui::EndTable();
      }
      if (!pendingInsSingle) {
        ImGui::BeginDisabled(!anySelected);
        if (ImGui::Button("OK")) {
          quitPlease=true;
        }
        ImGui::EndDisabled();
        ImGui::SameLine();
      }
      if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
        for (std::pair<DivInstrument*,bool>& i: pendingIns) {
          i.second=false;
        }
        quitPlease=true;
      }
      if (quitPlease) {
        ImGui::CloseCurrentPopup();
        for (std::pair<DivInstrument*,bool>& i: pendingIns) {
          if (!i.second || pendingInsSingle) {
            if (i.second) {
              if (curIns>=0 && curIns<(int)e->song.ins.size()) {
                *e->song.ins[curIns]=*i.first;
              } else {
                showError("...but you haven't selected an instrument!");
              }
            }
            delete i.first;
          } else {
            e->addInstrumentPtr(i.first);
          }
        }
        pendingIns.clear();
      }
      ImGui::EndPopup();
    }

    centerNextWindow("Import Raw Sample",canvasW,canvasH);
    if (ImGui::BeginPopupModal("Import Raw Sample",NULL,ImGuiWindowFlags_AlwaysAutoResize)) {
      ImGui::Text("Data type:");
      for (int i=0; i<DIV_SAMPLE_DEPTH_MAX; i++) {
        if (sampleDepths[i]==NULL) continue;
       if (ImGui::RadioButton(sampleDepths[i],pendingRawSampleDepth==i)) pendingRawSampleDepth=i;
      }

      if (pendingRawSampleDepth!=DIV_SAMPLE_DEPTH_8BIT && pendingRawSampleDepth!=DIV_SAMPLE_DEPTH_16BIT) {
        pendingRawSampleChannels=1;
      }
      if (pendingRawSampleDepth!=DIV_SAMPLE_DEPTH_16BIT) {
        pendingRawSampleBigEndian=false;
      }

      ImGui::BeginDisabled(pendingRawSampleDepth!=DIV_SAMPLE_DEPTH_8BIT && pendingRawSampleDepth!=DIV_SAMPLE_DEPTH_16BIT);
      ImGui::AlignTextToFramePadding();
      ImGui::Text("Channels");
      ImGui::SameLine();
      if (ImGui::InputInt("##RSChans",&pendingRawSampleChannels)) {
      }
      ImGui::Text("(will be mixed down to mono)");
      ImGui::Checkbox("Unsigned",&pendingRawSampleUnsigned);
      ImGui::EndDisabled();

      ImGui::BeginDisabled(pendingRawSampleDepth!=DIV_SAMPLE_DEPTH_16BIT);
      ImGui::Checkbox("Big endian",&pendingRawSampleBigEndian);
      ImGui::EndDisabled();

      ImGui::BeginDisabled(pendingRawSampleDepth==DIV_SAMPLE_DEPTH_16BIT);
      ImGui::Checkbox("Swap nibbles",&pendingRawSampleSwapNibbles);
      ImGui::EndDisabled();

      if (ImGui::Button("OK")) {
        DivSample* s=e->sampleFromFileRaw(pendingRawSample.c_str(),(DivSampleDepth)pendingRawSampleDepth,pendingRawSampleChannels,pendingRawSampleBigEndian,pendingRawSampleUnsigned,pendingRawSampleSwapNibbles);
        if (s==NULL) {
          showError(e->getLastError());
        } else {
          if (pendingRawSampleReplace) {
            if (curSample>=0 && curSample<(int)e->song.sample.size()) {
              e->lockEngine([this,s]() {
                // if it crashes here please tell me...
                DivSample* oldSample=e->song.sample[curSample];
                e->song.sample[curSample]=s;
                delete oldSample;
                e->renderSamples();
                MARK_MODIFIED;
              });
            } else {
              showError("...but you haven't selected a sample!");
              delete s;
            }
          } else {
            if (e->addSamplePtr(s)==-1) {
              showError(e->getLastError());
            } else {
              MARK_MODIFIED;
            }
          }
        }
        ImGui::CloseCurrentPopup();
      }
      ImGui::SameLine();
      if (ImGui::Button("Cancel") || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndPopup();
    }

    if (ImGui::BeginPopup("EditString",ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) {
      if (editString==NULL) {
        ImGui::Text("Error! No string provided!");
      } else {
        if (displayEditString) {
          ImGui::SetItemDefaultFocus();
          ImGui::SetKeyboardFocusHere();
        }
        ImGui::InputText("##StringVal",editString);
      }
      displayEditString=false;
      ImGui::SameLine();
      if (ImGui::Button("OK") || ImGui::IsKeyPressed(ImGuiKey_Enter,false)) {
        editString=NULL;
        ImGui::CloseCurrentPopup();
      }
      ImGui::EndPopup();
    } else {
      editString=NULL;
    }

    MEASURE_END(popup);

    if (!tutorial.introPlayed || settings.alwaysPlayIntro!=0) {
      MEASURE_BEGIN(intro);
      initialScreenWipe=0;
      if (settings.alwaysPlayIntro==1) {
        shortIntro=true;
      }
      drawIntro(introPos);
      MEASURE_END(intro);
    } else {
      introPos=12.0;
    }

#ifdef DIV_UNSTABLE
    {
      ImDrawList* dl=ImGui::GetForegroundDrawList();
      ImVec2 markPos=ImVec2(canvasW-ImGui::CalcTextSize(DIV_VERSION).x-6.0*dpiScale,4.0*dpiScale);
      ImVec4 markColor=uiColors[GUI_COLOR_TEXT];
      markColor.w=0.67f;

      dl->AddText(markPos,ImGui::ColorConvertFloat4ToU32(markColor),DIV_VERSION);
    }
#endif

    layoutTimeEnd=SDL_GetPerformanceCounter();

    // backup trigger
    if (modified) {
      if (backupTimer>0) {
        backupTimer=(backupTimer-ImGui::GetIO().DeltaTime);
        if (backupTimer<=0) {
          backupTask=std::async(std::launch::async,[this]() -> bool {
            backupLock.lock();
            logV("backupPath: %s",backupPath);
            logV("curFileName: %s",curFileName);
            if (curFileName.find(backupPath)==0) {
              logD("backup file open. not saving backup.");
              backupTimer=30.0;
              backupLock.unlock();
              return true;
            }
            if (!dirExists(backupPath.c_str())) {
              if (!makeDir(backupPath.c_str())) {
                logW("could not create backup directory!");
                backupTimer=30.0;
                backupLock.unlock();
                return false;
              }
            }
            logD("saving backup...");
            SafeWriter* w=e->saveFur(true,true);
            logV("writing file...");

            if (w!=NULL) {
              size_t sepPos=curFileName.rfind(DIR_SEPARATOR);
              String backupPreBaseName;
              String backupBaseName;
              String backupFileName;
              if (sepPos==String::npos) {
                backupPreBaseName=curFileName;
              } else {
                backupPreBaseName=curFileName.substr(sepPos+1);
              }

              size_t dotPos=backupPreBaseName.rfind('.');
              if (dotPos!=String::npos) {
                backupPreBaseName=backupPreBaseName.substr(0,dotPos);
              }

              for (char i: backupPreBaseName) {
                if (backupBaseName.size()>=48) break;
                if ((i>='0' && i<='9') || (i>='A' && i<='Z') || (i>='a' && i<='z') || i=='_' || i=='-' || i==' ') backupBaseName+=i;
              }

              if (backupBaseName.empty()) backupBaseName="untitled";

              backupFileName=backupBaseName;

              time_t curTime=time(NULL);
              struct tm curTM;
#ifdef _WIN32
              struct tm* tempTM=localtime(&curTime);
              if (tempTM==NULL) {
                backupFileName+="-unknownTime.fur";
              } else {
                curTM=*tempTM;
                backupFileName+=fmt::sprintf("-%d%.2d%.2d-%.2d%.2d%.2d.fur",curTM.tm_year+1900,curTM.tm_mon+1,curTM.tm_mday,curTM.tm_hour,curTM.tm_min,curTM.tm_sec);
              }
#else
              if (localtime_r(&curTime,&curTM)==NULL) {
                backupFileName+="-unknownTime.fur";
              } else {
                backupFileName+=fmt::sprintf("-%d%.2d%.2d-%.2d%.2d%.2d.fur",curTM.tm_year+1900,curTM.tm_mon+1,curTM.tm_mday,curTM.tm_hour,curTM.tm_min,curTM.tm_sec);
              }
#endif

              String finalPath=backupPath+String(DIR_SEPARATOR_STR)+backupFileName;
              
              FILE* outFile=ps_fopen(finalPath.c_str(),"wb");
              if (outFile!=NULL) {
                if (fwrite(w->getFinalBuf(),1,w->size(),outFile)!=w->size()) {
                  logW("did not write backup entirely: %s!",strerror(errno));
                }
                fclose(outFile);
              } else {
                logW("could not save backup: %s!",strerror(errno));
              }
              w->finish();

              // delete previous backup if there are too many
              delFirstBackup(backupBaseName);
            }
            logD("backup saved.");
            backupTimer=30.0;
            backupLock.unlock();
            return true;
          });
        }
      }
    }

    sampleMapWaitingInput=(curWindow==GUI_WINDOW_INS_EDIT && sampleMapFocused);
    
    curWindowThreadSafe=curWindow;

    if (curWindow!=curWindowLast) {
      int curWindowCat=0;
      int lastWindowCat=0;

      switch (curWindow) {
        case GUI_WINDOW_WAVE_LIST:
        case GUI_WINDOW_WAVE_EDIT:
          curWindowCat=1;
          break;
        case GUI_WINDOW_SAMPLE_LIST:
        case GUI_WINDOW_SAMPLE_EDIT:
          curWindowCat=2;
          break;
        default:
          curWindowCat=0;
          break;
      }
      switch (curWindowLast) {
        case GUI_WINDOW_WAVE_LIST:
        case GUI_WINDOW_WAVE_EDIT:
          lastWindowCat=1;
          break;
        case GUI_WINDOW_SAMPLE_LIST:
        case GUI_WINDOW_SAMPLE_EDIT:
          lastWindowCat=2;
          break;
        default:
          lastWindowCat=0;
          break;
      }

      if (curWindowCat!=lastWindowCat) {
        switch (lastWindowCat) {
          case 0:
            e->autoNoteOffAll();
            break;
          case 1:
            e->stopWavePreview();
            break;
          case 2:
            e->stopSamplePreview();
            break;
        }
      }
    }

    if (!settings.renderClearPos) {
      rend->clear(uiColors[GUI_COLOR_BACKGROUND]);
    }
    renderTimeBegin=SDL_GetPerformanceCounter();
    ImGui::Render();
    renderTimeEnd=SDL_GetPerformanceCounter();
    drawTimeBegin=SDL_GetPerformanceCounter();
    rend->renderGUI();
    if (mustClear) {
      rend->clear(ImVec4(0,0,0,0));
      mustClear--;
    } else {
      if (initialScreenWipe>0.0f && !settings.disableFadeIn) {
        WAKE_UP;
        initialScreenWipe-=ImGui::GetIO().DeltaTime*5.0f;
        if (initialScreenWipe>0.0f) {
          rend->wipe(pow(initialScreenWipe,2.0f));
        }
      }
    }
    drawTimeEnd=SDL_GetPerformanceCounter();
    rend->present();
    if (settings.renderClearPos) {
      rend->clear(uiColors[GUI_COLOR_BACKGROUND]);
    }

    layoutTimeDelta=layoutTimeEnd-layoutTimeBegin;
    renderTimeDelta=renderTimeEnd-renderTimeBegin;
    drawTimeDelta=drawTimeEnd-drawTimeBegin;
    eventTimeDelta=eventTimeEnd-eventTimeBegin;

    soloTimeout-=ImGui::GetIO().DeltaTime;
    if (soloTimeout<0) {
      soloTimeout=0;
    } else {
      WAKE_UP;
    }

    if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
      exitDisabledTimer=0;
    }

    wheelX=0;
    wheelY=0;
    wantScrollList=false;

    pressedPoints.clear();
    releasedPoints.clear();

    if (willCommit) {
      commitSettings();
      willCommit=false;
    }

    if (fontsFailed) {
      showError("it appears I couldn't load these fonts. any setting you can check?");
      logE("couldn't load fonts");
      ImGui::GetIO().Fonts->Clear();
      mainFont=ImGui::GetIO().Fonts->AddFontDefault();
      patFont=mainFont;
      bigFont=mainFont;
      headFont=mainFont;
      if (rend) rend->destroyFontsTexture();
      if (!ImGui::GetIO().Fonts->Build()) {
        logE("error again while building font atlas!");
      } else {
        rend->createFontsTexture();
      }
    }

    if (!editOptsVisible) {
      latchTarget=0;
      latchNibble=false;
    }

    if (SDL_GetWindowFlags(sdlWin)&SDL_WINDOW_MINIMIZED) {
      SDL_Delay(100);
    }
  }
  return false;
}

bool FurnaceGUI::init() {
  logI("initializing GUI.");

  String homeDir=getHomeDir();
  workingDir=e->getConfString("lastDir",homeDir);
  workingDirSong=e->getConfString("lastDirSong",workingDir);
  workingDirIns=e->getConfString("lastDirIns",workingDir);
  workingDirWave=e->getConfString("lastDirWave",workingDir);
  workingDirSample=e->getConfString("lastDirSample",workingDir);
  workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir);
  workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir);
  workingDirZSMExport=e->getConfString("lastDirZSMExport",workingDir);
  workingDirROMExport=e->getConfString("lastDirROMExport",workingDir);
  workingDirFont=e->getConfString("lastDirFont",workingDir);
  workingDirColors=e->getConfString("lastDirColors",workingDir);
  workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir);
  workingDirLayout=e->getConfString("lastDirLayout",workingDir);
  workingDirTest=e->getConfString("lastDirTest",workingDir);

  editControlsOpen=e->getConfBool("editControlsOpen",true);
  ordersOpen=e->getConfBool("ordersOpen",true);
  insListOpen=e->getConfBool("insListOpen",true);
  songInfoOpen=e->getConfBool("songInfoOpen",true);
  patternOpen=e->getConfBool("patternOpen",true);
  insEditOpen=e->getConfBool("insEditOpen",false);
  waveListOpen=e->getConfBool("waveListOpen",true);
  waveEditOpen=e->getConfBool("waveEditOpen",false);
  sampleListOpen=e->getConfBool("sampleListOpen",true);
  sampleEditOpen=e->getConfBool("sampleEditOpen",false);
  settingsOpen=e->getConfBool("settingsOpen",false);
  mixerOpen=e->getConfBool("mixerOpen",false);
  oscOpen=e->getConfBool("oscOpen",true);
  chanOscOpen=e->getConfBool("chanOscOpen",false);
  volMeterOpen=e->getConfBool("volMeterOpen",true);
  statsOpen=e->getConfBool("statsOpen",false);
  compatFlagsOpen=e->getConfBool("compatFlagsOpen",false);
#ifdef IS_MOBILE
  pianoOpen=e->getConfBool("pianoOpen",true);
#else
  pianoOpen=e->getConfBool("pianoOpen",false);
#endif
  notesOpen=e->getConfBool("notesOpen",false);
  channelsOpen=e->getConfBool("channelsOpen",false);
  patManagerOpen=e->getConfBool("patManagerOpen",false);
  sysManagerOpen=e->getConfBool("sysManagerOpen",false);
  clockOpen=e->getConfBool("clockOpen",false);
  speedOpen=e->getConfBool("speedOpen",true);
  groovesOpen=e->getConfBool("groovesOpen",false);
  regViewOpen=e->getConfBool("regViewOpen",false);
  logOpen=e->getConfBool("logOpen",false);
  effectListOpen=e->getConfBool("effectListOpen",true);
  subSongsOpen=e->getConfBool("subSongsOpen",true);
  findOpen=e->getConfBool("findOpen",false);
  spoilerOpen=e->getConfBool("spoilerOpen",false);

  if (e->hasConf("lastDir")) {
    basicMode=e->getConfBool("basicMode",false);
  } else {
    basicMode=true;
  }

  insListDir=e->getConfBool("insListDir",false);
  waveListDir=e->getConfBool("waveListDir",false);
  sampleListDir=e->getConfBool("sampleListDir",false);

  tempoView=e->getConfBool("tempoView",true);
  waveHex=e->getConfBool("waveHex",false);
  waveSigned=e->getConfBool("waveSigned",false);
  waveGenVisible=e->getConfBool("waveGenVisible",false);
  waveEditStyle=e->getConfInt("waveEditStyle",0);
  extraChannelButtons=e->getConfInt("extraChannelButtons",0);
  lockLayout=e->getConfBool("lockLayout",false);
#ifdef IS_MOBILE
  fullScreen=true;
#else
  fullScreen=e->getConfBool("fullScreen",false);
#endif
  mobileUI=e->getConfBool("mobileUI",MOBILE_UI_DEFAULT);
  edit=e->getConfBool("edit",false);
  followOrders=e->getConfBool("followOrders",true);
  followPattern=e->getConfBool("followPattern",true);
  noteInputPoly=e->getConfBool("noteInputPoly",true);
  exportLoops=e->getConfInt("exportLoops",0);
  if (exportLoops<0) exportLoops=0;
  exportFadeOut=e->getConfDouble("exportFadeOut",0.0);
  if (exportFadeOut<0.0) exportFadeOut=0.0;
  orderEditMode=e->getConfInt("orderEditMode",0);
  if (orderEditMode<0) orderEditMode=0;
  if (orderEditMode>3) orderEditMode=3;

  oscZoom=e->getConfFloat("oscZoom",0.5f);
  oscZoomSlider=e->getConfBool("oscZoomSlider",false);
  oscWindowSize=e->getConfFloat("oscWindowSize",20.0f);

  pianoOctaves=e->getConfInt("pianoOctaves",pianoOctaves);
  pianoOctavesEdit=e->getConfInt("pianoOctavesEdit",pianoOctavesEdit);
  pianoOptions=e->getConfBool("pianoOptions",pianoOptions);
  pianoSharePosition=e->getConfBool("pianoSharePosition",pianoSharePosition);
  pianoOptionsSet=e->getConfBool("pianoOptionsSet",pianoOptionsSet);
  pianoReadonly=e->getConfBool("pianoReadonly",false);
  pianoOffset=e->getConfInt("pianoOffset",pianoOffset);
  pianoOffsetEdit=e->getConfInt("pianoOffsetEdit",pianoOffsetEdit);
  pianoView=e->getConfInt("pianoView",pianoView);
  pianoInputPadMode=e->getConfInt("pianoInputPadMode",pianoInputPadMode);

  chanOscCols=e->getConfInt("chanOscCols",3);
  chanOscAutoColsType=e->getConfInt("chanOscAutoColsType",0);
  chanOscColorX=e->getConfInt("chanOscColorX",GUI_OSCREF_CENTER);
  chanOscColorY=e->getConfInt("chanOscColorY",GUI_OSCREF_CENTER);
  chanOscTextX=e->getConfFloat("chanOscTextX",0.0f);
  chanOscTextY=e->getConfFloat("chanOscTextY",0.0f);
  chanOscAmplify=e->getConfFloat("chanOscAmplify",0.95f);
  chanOscWindowSize=e->getConfFloat("chanOscWindowSize",20.0f);
  chanOscWaveCorr=e->getConfBool("chanOscWaveCorr",true);
  chanOscOptions=e->getConfBool("chanOscOptions",false);
  chanOscNormalize=e->getConfBool("chanOscNormalize",false);
  chanOscTextFormat=e->getConfString("chanOscTextFormat","%c");
  chanOscColor.x=e->getConfFloat("chanOscColorR",1.0f);
  chanOscColor.y=e->getConfFloat("chanOscColorG",1.0f);
  chanOscColor.z=e->getConfFloat("chanOscColorB",1.0f);
  chanOscColor.w=e->getConfFloat("chanOscColorA",1.0f);
  chanOscTextColor.x=e->getConfFloat("chanOscTextColorR",1.0f);
  chanOscTextColor.y=e->getConfFloat("chanOscTextColorG",1.0f);
  chanOscTextColor.z=e->getConfFloat("chanOscTextColorB",1.0f);
  chanOscTextColor.w=e->getConfFloat("chanOscTextColorA",0.75f);
  chanOscUseGrad=e->getConfBool("chanOscUseGrad",false);
  chanOscGrad.fromString(e->getConfString("chanOscGrad",""));
  chanOscGrad.render();

  syncSettings();
  syncTutorial();

  if (!settings.persistFadeOut) {
    exportLoops=settings.exportLoops;
    exportFadeOut=settings.exportFadeOut;
  }

  for (int i=0; i<settings.maxRecentFile; i++) {
    String r=e->getConfString(fmt::sprintf("recentFile%d",i),"");
    if (!r.empty()) {
      recentFile.push_back(r);
    }
  }

  initSystemPresets();
  initTutorial();

  e->setAutoNotePoly(noteInputPoly);

  SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER,"1");
#if SDL_VERSION_ATLEAST(2,0,17)
  SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS,"0");
#endif
  SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS,"0");
  // don't disable compositing on KWin
#if SDL_VERSION_ATLEAST(2,0,22)
  logV("setting window type to NORMAL.");
  SDL_SetHint(SDL_HINT_X11_WINDOW_TYPE,"_NET_WM_WINDOW_TYPE_NORMAL");
#endif

  // initialize SDL
  logD("initializing video...");
  if (SDL_Init(SDL_INIT_VIDEO)!=0) {
    logE("could not initialize video! %s",SDL_GetError());
    return false;
  }

#ifdef IS_MOBILE
  logD("initializing haptic...");
  if (SDL_Init(SDL_INIT_HAPTIC)!=0) {
    logW("could not initialize haptic! %s",SDL_GetError());
  }
#endif

  const char* videoBackend=SDL_GetCurrentVideoDriver();
  if (videoBackend!=NULL) {
    logV("video backend: %s",videoBackend);
    if (strcmp(videoBackend,"wayland")==0 ||
        strcmp(videoBackend,"cocoa")==0 ||
        strcmp(videoBackend,"uikit")==0) {
      sysManagedScale=true;
      logV("scaling managed by system.");
    } else {
      logV("scaling managed by application.");
    }
  } else {
    logV("could not get video backend name!");
  }

  // get scale factor
  if (settings.dpiScale>=0.5f) {
    logD("setting UI scale factor from config (%f).",settings.dpiScale);
    dpiScale=settings.dpiScale;
  } else {
    logD("auto-detecting UI scale factor.");
    dpiScale=getScaleFactor(videoBackend);
    logD("scale factor: %f",dpiScale);
    if (dpiScale<0.1f) {
      logW("scale what?");
      dpiScale=1.0f;
    }
  }

#if !(defined(__APPLE__) || defined(_WIN32))
  // get the icon (on macOS and Windows the icon is bundled with the app)
  const FurnaceGUIImage* furIcon=getImage(GUI_IMAGE_ICON);
  SDL_Surface* icon=NULL;
  if (furIcon!=NULL) {
    icon=SDL_CreateRGBSurfaceFrom(furIcon->data,furIcon->width,furIcon->height,32,256*4,0xff,0xff00,0xff0000,0xff000000);
  } else {
    logE("furIcon is NULL!");
  }
#endif

#ifdef IS_MOBILE
  scrW=960;
  scrH=540;
  scrX=0;
  scrY=0;
#else
  scrW=scrConfW=e->getConfInt("lastWindowWidth",1280);
  scrH=scrConfH=e->getConfInt("lastWindowHeight",800);
  scrX=scrConfX=e->getConfInt("lastWindowX",SDL_WINDOWPOS_CENTERED);
  scrY=scrConfY=e->getConfInt("lastWindowY",SDL_WINDOWPOS_CENTERED);
  scrMax=e->getConfBool("lastWindowMax",false);
#endif
  portrait=(scrW<scrH);
  logV("portrait: %d (%dx%d)",portrait,scrW,scrH);

  // if old config, scale size as it was stored unscaled before
  if (e->getConfInt("configVersion",0)<122 && !sysManagedScale) {
    logD("scaling window size to scale factor because configVersion is not present.");
    scrW*=dpiScale;
    scrH*=dpiScale;
  }

  // predict the canvas size
  if (sysManagedScale) {
    canvasW=scrW*dpiScale;
    canvasH=scrH*dpiScale;
  } else {
    canvasW=scrW;
    canvasH=scrH;
  }

#ifndef IS_MOBILE
  SDL_Rect displaySize;
#endif

#ifndef IS_MOBILE
  // if window would spawn out of bounds, force it to be get default position
  SDL_Rect bounds;
  if (!detectOutOfBoundsWindow(bounds)) {
    scrMax=false;
    scrX=scrConfX=SDL_WINDOWPOS_CENTERED;
    scrY=scrConfY=SDL_WINDOWPOS_CENTERED;

    // make sure our window isn't big
    /*if (bounds.w<scrW) {
      logD("resizing width because it does not fit");
      scrW=bounds.w-OOB_PIXELS_SAFETY*2;
      if (scrW<200) scrW=200;
    }
    if (bounds.h<scrH) {
      logD("resizing height because it does not fit");
      scrH=bounds.h-OOB_PIXELS_SAFETY*2;
      if (scrH<100) scrH=100;
    }*/
  }
#endif

  logV("window size: %dx%d",scrW,scrH);

  if (!initRender()) {
    if (settings.renderBackend!="SDL") {
      settings.renderBackend="SDL";
      e->setConf("renderBackend","SDL");
      e->saveConf();
      lastError=fmt::sprintf("could not init renderer!\r\nthe render backend has been set to a safe value. please restart Furnace.");
    } else {
      lastError=fmt::sprintf("could not init renderer! %s",SDL_GetError());
      if (!settings.renderDriver.empty()) {
        settings.renderDriver="";
        e->setConf("renderDriver","");
        e->saveConf();
        lastError+=fmt::sprintf("\r\nthe render driver has been set to a safe value. please restart Furnace.");
      }
    }
    return false;
  }

  rend->preInit();

  logD("creating window...");
  sdlWin=SDL_CreateWindow("Furnace",scrX,scrY,scrW,scrH,SDL_WINDOW_RESIZABLE|SDL_WINDOW_ALLOW_HIGHDPI|(scrMax?SDL_WINDOW_MAXIMIZED:0)|(fullScreen?SDL_WINDOW_FULLSCREEN_DESKTOP:0)|rend->getWindowFlags());
  if (sdlWin==NULL) {
    lastError=fmt::sprintf("could not open window! %s",SDL_GetError());
    return false;
  }

#ifndef IS_MOBILE
  if (SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(sdlWin),&displaySize)==0) {
    bool mustChange=false;
    if (scrW>((displaySize.w)-48) && scrH>((displaySize.h)-64)) {
      // maximize
      SDL_MaximizeWindow(sdlWin);
      logD("maximizing as it doesn't fit (%dx%d+%d+%d).",displaySize.w,displaySize.h,displaySize.x,displaySize.y);
    }
    if (scrW>displaySize.w) {
      scrW=(displaySize.w)-32;
      mustChange=true;
    }
    if (scrH>displaySize.h) {
      scrH=(displaySize.h)-32;
      mustChange=true;
    }
    if (mustChange) {
      portrait=(scrW<scrH);
      logV("portrait: %d (%dx%d)",portrait,scrW,scrH);
      if (!fullScreen) {
        logD("setting window size to %dx%d as it goes off bounds (%dx%d+%d+%d).",scrW,scrH,displaySize.w,displaySize.h,displaySize.x,displaySize.y);
        SDL_SetWindowSize(sdlWin,scrW,scrH);
      }
    }
  }
#endif

#ifdef IS_MOBILE
  SDL_GetWindowSize(sdlWin,&scrW,&scrH);
  portrait=(scrW<scrH);
  logV("portrait: %d (%dx%d)",portrait,scrW,scrH);
#endif

#if !(defined(__APPLE__) || defined(_WIN32))
  if (icon!=NULL) {
    SDL_SetWindowIcon(sdlWin,icon);
    SDL_FreeSurface(icon);
  } else {
    logW("could not create icon!");
  }
#endif

  int numDriversA=SDL_GetNumAudioDrivers();
  if (numDriversA<0) {
    logW("could not list audio drivers! %s",SDL_GetError());
  } else {
    for (int i=0; i<numDriversA; i++) {
      const char* r=SDL_GetAudioDriver(i);
      if (r==NULL) continue;
      if (strcmp(r,"disk")==0) continue;
      if (strcmp(r,"dummy")==0) continue;
      availAudioDrivers.push_back(String(r));
    }
  }

  int numDrivers=SDL_GetNumRenderDrivers();
  if (numDrivers<0) {
    logW("could not list render drivers! %s",SDL_GetError());
  } else {
    SDL_RendererInfo ri;
    for (int i=0; i<numDrivers; i++) {
      int r=SDL_GetRenderDriverInfo(i,&ri);
      if (r!=0) continue;
      availRenderDrivers.push_back(String(ri.name));
    }
  }

  if (!settings.renderDriver.empty()) {
    SDL_SetHint(SDL_HINT_RENDER_DRIVER,settings.renderDriver.c_str());
  }

  logD("starting render backend...");
  if (!rend->init(sdlWin)) {
    if (settings.renderBackend!="SDL") {
      settings.renderBackend="SDL";
      e->setConf("renderBackend","SDL");
      e->saveConf();
      lastError=fmt::sprintf("could not init renderer!\r\nthe render backend has been set to a safe value. please restart Furnace.");
    } else {
      lastError=fmt::sprintf("could not init renderer! %s",SDL_GetError());
      if (!settings.renderDriver.empty()) {
        settings.renderDriver="";
        e->setConf("renderDriver","");
        e->saveConf();
        lastError+=fmt::sprintf("\r\nthe render driver has been set to a safe value. please restart Furnace.");
      }
    }
    return false;
  }

  // try acquiring the canvas size
  if (!rend->getOutputSize(canvasW,canvasH)) {
    logW("could not get renderer output size!");
  } else {
    logV("canvas size: %dx%d",canvasW,canvasH);
  }

  // special consideration for Wayland
  if (settings.dpiScale<0.5f) {
    if (strcmp(videoBackend,"wayland")==0) {
      int realW=scrW;
      int realH=scrH;

      SDL_GetWindowSize(sdlWin,&realW,&realH);

      if (realW<1) {
        logW("screen width is zero!\n");
        dpiScale=1.0;
      } else {
        dpiScale=(double)canvasW/(double)realW;
        logV("we're on Wayland... scaling factor: %f",dpiScale);
      }
    }
  }

  updateWindowTitle();

  rend->clear(ImVec4(0.0,0.0,0.0,1.0));
  rend->present();

  logD("preparing user interface...");
  IMGUI_CHECKVERSION();
  ImGui::CreateContext();
  rend->initGUI(sdlWin);

  applyUISettings();

  logD("building font...");
  if (!ImGui::GetIO().Fonts->Build()) {
    logE("error while building font atlas!");
    showError("error while loading fonts! please check your settings.");
    ImGui::GetIO().Fonts->Clear();
    mainFont=ImGui::GetIO().Fonts->AddFontDefault();
    patFont=mainFont;
    bigFont=mainFont;
    headFont=mainFont;
    if (rend) rend->destroyFontsTexture();
    if (!ImGui::GetIO().Fonts->Build()) {
      logE("error again while building font atlas!");
    }
  }

  logD("preparing layout...");
  strncpy(finalLayoutPath,(e->getConfigPath()+String(LAYOUT_INI)).c_str(),4095);
  backupPath=e->getConfigPath();
  if (backupPath.size()>0) {
    if (backupPath[backupPath.size()-1]==DIR_SEPARATOR) backupPath.resize(backupPath.size()-1);
  }
  backupPath+=String(BACKUPS_DIR);
  prepareLayout();

  ImGui::GetIO().ConfigFlags|=ImGuiConfigFlags_DockingEnable;
  toggleMobileUI(mobileUI,true);

  for (int i=0; i<DIV_MAX_CHANS; i++) {
    oldPat[i]=new DivPattern;
  }

  firstFrame=true;

  // TODO: MIDI mapping time!
  e->setMidiCallback([this](const TAMidiMessage& msg) -> int {
    if (introPos<11.0) return -2;
    midiLock.lock();
    midiQueue.push(msg);
    midiLock.unlock();
    e->setMidiBaseChan(cursor.xCoarse);
    if (msg.type==TA_MIDI_SYSEX) return -2;
    if (midiMap.valueInputStyle!=0 && cursor.xFine!=0 && edit) return -2;
    if (!midiMap.noteInput) return -2;
    if (learning!=-1) return -2;
    if (midiMap.at(msg)) return -2;

    if (curWindowThreadSafe==GUI_WINDOW_WAVE_EDIT || curWindowThreadSafe==GUI_WINDOW_WAVE_LIST) {
      if ((msg.type&0xf0)==TA_MIDI_NOTE_ON) {
        e->previewWaveNoLock(curWave,msg.data[0]-12);
        wavePreviewNote=msg.data[0]-12;
      } else if ((msg.type&0xf0)==TA_MIDI_NOTE_OFF) {
        if (wavePreviewNote==msg.data[0]-12) {
          e->stopWavePreviewNoLock();
        }
      }
      return -2;
    }

    if (curWindowThreadSafe==GUI_WINDOW_SAMPLE_EDIT || curWindowThreadSafe==GUI_WINDOW_SAMPLE_LIST) {
      if ((msg.type&0xf0)==TA_MIDI_NOTE_ON) {
        e->previewSampleNoLock(curSample,msg.data[0]-12);
        samplePreviewNote=msg.data[0]-12;
      } else if ((msg.type&0xf0)==TA_MIDI_NOTE_OFF) {
        if (samplePreviewNote==msg.data[0]-12) {
          e->stopSamplePreviewNoLock();
        }
      }
      return -2;
    }

    return curIns;
  });

#ifdef IS_MOBILE
  vibrator=SDL_HapticOpen(0);
  if (vibrator==NULL) {
    logD("could not open vibration device: %s",SDL_GetError());
  } else {
    if (SDL_HapticRumbleInit(vibrator)==0) {
      vibratorAvailable=true;
    } else {
      logD("vibration not available: %s",SDL_GetError());
    }
  }
#endif

  logI("done!");
  return true;
}

void FurnaceGUI::commitState() {
  if (!mobileUI) {
    if (!ImGui::SaveIniSettingsToDisk(finalLayoutPath,true)) {
      reportError(fmt::sprintf("could NOT save layout! %s",strerror(errno)));
    }
  }

  e->setConf("configVersion",(int)DIV_ENGINE_VERSION);

  e->setConf("lastDir",workingDir);
  e->setConf("lastDirSong",workingDirSong);
  e->setConf("lastDirIns",workingDirIns);
  e->setConf("lastDirWave",workingDirWave);
  e->setConf("lastDirSample",workingDirSample);
  e->setConf("lastDirAudioExport",workingDirAudioExport);
  e->setConf("lastDirVGMExport",workingDirVGMExport);
  e->setConf("lastDirZSMExport",workingDirZSMExport);
  e->setConf("lastDirROMExport",workingDirROMExport);
  e->setConf("lastDirFont",workingDirFont);
  e->setConf("lastDirColors",workingDirColors);
  e->setConf("lastDirKeybinds",workingDirKeybinds);
  e->setConf("lastDirLayout",workingDirLayout);
  e->setConf("lastDirTest",workingDirTest);

  // commit last open windows
  e->setConf("editControlsOpen",editControlsOpen);
  e->setConf("ordersOpen",ordersOpen);
  e->setConf("insListOpen",insListOpen);
  e->setConf("songInfoOpen",songInfoOpen);
  e->setConf("patternOpen",patternOpen);
  e->setConf("insEditOpen",insEditOpen);
  e->setConf("waveListOpen",waveListOpen);
  e->setConf("waveEditOpen",waveEditOpen);
  e->setConf("sampleListOpen",sampleListOpen);
  e->setConf("sampleEditOpen",sampleEditOpen);
  e->setConf("settingsOpen",settingsOpen);
  e->setConf("mixerOpen",mixerOpen);
  e->setConf("oscOpen",oscOpen);
  e->setConf("chanOscOpen",chanOscOpen);
  e->setConf("volMeterOpen",volMeterOpen);
  e->setConf("statsOpen",statsOpen);
  e->setConf("compatFlagsOpen",compatFlagsOpen);
  e->setConf("pianoOpen",pianoOpen);
  e->setConf("notesOpen",notesOpen);
  e->setConf("channelsOpen",channelsOpen);
  e->setConf("patManagerOpen",patManagerOpen);
  e->setConf("sysManagerOpen",sysManagerOpen);
  e->setConf("clockOpen",clockOpen);
  e->setConf("speedOpen",speedOpen);
  e->setConf("groovesOpen",groovesOpen);
  e->setConf("regViewOpen",regViewOpen);
  e->setConf("logOpen",logOpen);
  e->setConf("effectListOpen",effectListOpen);
  e->setConf("subSongsOpen",subSongsOpen);
  e->setConf("findOpen",findOpen);
  e->setConf("spoilerOpen",spoilerOpen);
  e->setConf("basicMode",basicMode);

  // commit dir state
  e->setConf("insListDir",insListDir);
  e->setConf("waveListDir",waveListDir);
  e->setConf("sampleListDir",sampleListDir);

  // commit last window size
  e->setConf("lastWindowWidth",scrConfW);
  e->setConf("lastWindowHeight",scrConfH);
  e->setConf("lastWindowX",settings.saveWindowPos?scrConfX:(int)SDL_WINDOWPOS_CENTERED);
  e->setConf("lastWindowY",settings.saveWindowPos?scrConfY:(int)SDL_WINDOWPOS_CENTERED);
  e->setConf("lastWindowMax",scrMax);

  e->setConf("tempoView",tempoView);
  e->setConf("waveHex",waveHex);
  e->setConf("waveSigned",waveSigned);
  e->setConf("waveGenVisible",waveGenVisible);
  e->setConf("waveEditStyle",waveEditStyle);
  e->setConf("extraChannelButtons",extraChannelButtons);
  e->setConf("lockLayout",lockLayout);
  e->setConf("fullScreen",fullScreen);
  e->setConf("mobileUI",mobileUI);
  e->setConf("edit",edit);
  e->setConf("followOrders",followOrders);
  e->setConf("followPattern",followPattern);
  e->setConf("orderEditMode",orderEditMode);
  e->setConf("noteInputPoly",noteInputPoly);
  if (settings.persistFadeOut) {
    e->setConf("exportLoops",exportLoops);
    e->setConf("exportFadeOut",exportFadeOut);
  }

  // commit oscilloscope state
  e->setConf("oscZoom",oscZoom);
  e->setConf("oscZoomSlider",oscZoomSlider);
  e->setConf("oscWindowSize",oscWindowSize);

  // commit piano state
  e->setConf("pianoOctaves",pianoOctaves);
  e->setConf("pianoOctavesEdit",pianoOctavesEdit);
  e->setConf("pianoOptions",pianoOptions);
  e->setConf("pianoSharePosition",pianoSharePosition);
  e->setConf("pianoOptionsSet",pianoOptionsSet);
  e->setConf("pianoReadonly",pianoReadonly);
  e->setConf("pianoOffset",pianoOffset);
  e->setConf("pianoOffsetEdit",pianoOffsetEdit);
  e->setConf("pianoView",pianoView);
  e->setConf("pianoInputPadMode",pianoInputPadMode);

  // commit per-chan osc state
  e->setConf("chanOscCols",chanOscCols);
  e->setConf("chanOscAutoColsType",chanOscAutoColsType);
  e->setConf("chanOscColorX",chanOscColorX);
  e->setConf("chanOscColorY",chanOscColorY);
  e->setConf("chanOscTextX",chanOscTextX);
  e->setConf("chanOscTextY",chanOscTextY);
  e->setConf("chanOscAmplify",chanOscAmplify);
  e->setConf("chanOscWindowSize",chanOscWindowSize);
  e->setConf("chanOscWaveCorr",chanOscWaveCorr);
  e->setConf("chanOscOptions",chanOscOptions);
  e->setConf("chanOscNormalize",chanOscNormalize);
  e->setConf("chanOscTextFormat",chanOscTextFormat);
  e->setConf("chanOscColorR",chanOscColor.x);
  e->setConf("chanOscColorG",chanOscColor.y);
  e->setConf("chanOscColorB",chanOscColor.z);
  e->setConf("chanOscColorA",chanOscColor.w);
  e->setConf("chanOscTextColorR",chanOscTextColor.x);
  e->setConf("chanOscTextColorG",chanOscTextColor.y);
  e->setConf("chanOscTextColorB",chanOscTextColor.z);
  e->setConf("chanOscTextColorA",chanOscTextColor.w);
  e->setConf("chanOscUseGrad",chanOscUseGrad);
  e->setConf("chanOscGrad",chanOscGrad.toString());

  // commit recent files
  for (int i=0; i<30; i++) {
    String key=fmt::sprintf("recentFile%d",i);
    if (i>=settings.maxRecentFile || i>=(int)recentFile.size()) {
      e->setConf(key,"");
    } else {
      e->setConf(key,recentFile[i]);
    }
  }
}

bool FurnaceGUI::finish() {
  commitState();
  rend->quitGUI();
  ImGui_ImplSDL2_Shutdown();
  quitRender();
  ImGui::DestroyContext();
  SDL_DestroyWindow(sdlWin);

  if (vibrator) {
    SDL_HapticClose(vibrator);
  }

  for (int i=0; i<DIV_MAX_OUTPUTS; i++) {
    if (oscValues[i]) {
      delete[] oscValues[i];
      oscValues[i]=NULL;
    }
  }

  for (int i=0; i<DIV_MAX_CHANS; i++) {
    delete oldPat[i];
  }

  if (backupTask.valid()) {
    backupTask.get();
  }

  return true;
}

void FurnaceGUI::requestQuit() {
  quit=true;
}

FurnaceGUI::FurnaceGUI():
  e(NULL),
  renderBackend(GUI_BACKEND_SDL),
  rend(NULL),
  sdlWin(NULL),
  vibrator(NULL),
  vibratorAvailable(false),
  sampleTex(NULL),
  sampleTexW(0),
  sampleTexH(0),
  updateSampleTex(true),
  quit(false),
  warnQuit(false),
  willCommit(false),
  edit(false),
  modified(false),
  displayError(false),
  displayExporting(false),
  vgmExportLoop(true),
  zsmExportLoop(true),
  vgmExportPatternHints(false),
  vgmExportDirectStream(false),
  displayInsTypeList(false),
  portrait(false),
  injectBackUp(false),
  mobileMenuOpen(false),
  warnColorPushed(false),
  wantCaptureKeyboard(false),
  oldWantCaptureKeyboard(false),
  displayMacroMenu(false),
  displayNew(false),
  fullScreen(false),
  preserveChanPos(false),
  wantScrollList(false),
  noteInputPoly(true),
  notifyWaveChange(false),
  displayPendingIns(false),
  pendingInsSingle(false),
  displayPendingRawSample(false),
  snesFilterHex(false),
  modTableHex(false),
  displayEditString(false),
  mobileEdit(false),
  killGraphics(false),
  vgmExportVersion(0x171),
  vgmExportTrailingTicks(-1),
  drawHalt(10),
  zsmExportTickRate(60),
  macroPointSize(16),
  waveEditStyle(0),
  displayInsTypeListMakeInsSample(-1),
  mobileEditPage(0),
  mobileMenuPos(0.0f),
  autoButtonSize(0.0f),
  mobileEditAnim(0.0f),
  mobileEditButtonPos(0.7f,0.7f),
  mobileEditButtonSize(60.0f,60.0f),
  curSysSection(NULL),
  updateFMPreview(true),
  fmPreviewOn(false),
  fmPreviewPaused(false),
  fmPreviewOPN(NULL),
  editString(NULL),
  pendingRawSampleDepth(8),
  pendingRawSampleChannels(1),
  pendingRawSampleUnsigned(false),
  pendingRawSampleBigEndian(false),
  pendingRawSampleSwapNibbles(false),
  pendingRawSampleReplace(false),
  globalWinFlags(0),
  curFileDialog(GUI_FILE_OPEN),
  warnAction(GUI_WARN_OPEN),
  postWarnAction(GUI_WARN_GENERIC),
  mobScene(GUI_SCENE_PATTERN),
  fileDialog(NULL),
  scrW(1280),
  scrH(800),
  scrConfW(1280),
  scrConfH(800),
  canvasW(1280),
  canvasH(800),
  scrX(SDL_WINDOWPOS_CENTERED),
  scrY(SDL_WINDOWPOS_CENTERED),
  scrConfX(SDL_WINDOWPOS_CENTERED),
  scrConfY(SDL_WINDOWPOS_CENTERED),
  scrMax(false),
  sysManagedScale(false),
  dpiScale(1),
  aboutScroll(0),
  aboutSin(0),
  aboutHue(0.0f),
  backupTimer(15.0),
  learning(-1),
  mainFont(NULL),
  iconFont(NULL),
  patFont(NULL),
  bigFont(NULL),
  headFont(NULL),
  fontRange(NULL),
  prevInsData(NULL),
  curIns(0),
  curWave(0),
  curSample(0),
  curOctave(3),
  curOrder(0),
  prevIns(0),
  oldRow(0),
  oldOrder(0),
  oldOrder1(0),
  editStep(1),
  exportLoops(0),
  soloChan(-1),
  orderEditMode(0),
  orderCursor(-1),
  loopOrder(-1),
  loopRow(-1),
  loopEnd(-1),
  isClipping(0),
  extraChannelButtons(0),
  newSongCategory(0),
  latchTarget(0),
  wheelX(0),
  wheelY(0),
  dragSourceX(0),
  dragSourceY(0),
  dragDestinationX(0),
  dragDestinationY(0),
  oldBeat(-1),
  oldBar(-1),
  curGroove(-1),
  exitDisabledTimer(0),
  soloTimeout(0.0f),
  exportFadeOut(5.0),
  editControlsOpen(true),
  ordersOpen(true),
  insListOpen(true),
  songInfoOpen(true),
  patternOpen(true),
  insEditOpen(false),
  waveListOpen(true),
  waveEditOpen(false),
  sampleListOpen(true),
  sampleEditOpen(false),
  aboutOpen(false),
  settingsOpen(false),
  mixerOpen(false),
  debugOpen(false),
  inspectorOpen(false),
  oscOpen(true),
  volMeterOpen(true),
  statsOpen(false),
  compatFlagsOpen(false),
  pianoOpen(false),
  notesOpen(false),
  channelsOpen(false),
  regViewOpen(false),
  logOpen(false),
  effectListOpen(false),
  chanOscOpen(false),
  subSongsOpen(true),
  findOpen(false),
  spoilerOpen(false),
  patManagerOpen(false),
  sysManagerOpen(false),
  clockOpen(false),
  speedOpen(true),
  groovesOpen(false),
  basicMode(true),
  shortIntro(false),
  insListDir(false),
  waveListDir(false),
  sampleListDir(false),
  clockShowReal(true),
  clockShowRow(true),
  clockShowBeat(true),
  clockShowMetro(true),
  clockShowTime(true),
  selecting(false),
  selectingFull(false),
  dragging(false),
  curNibble(false),
  orderNibble(false),
  followOrders(true),
  followPattern(true),
  changeAllOrders(false),
  mobileUI(MOBILE_UI_DEFAULT),
  collapseWindow(false),
  demandScrollX(false),
  fancyPattern(false),
  firstFrame(true),
  tempoView(true),
  waveHex(false),
  waveSigned(false),
  waveGenVisible(false),
  lockLayout(false),
  editOptsVisible(false),
  latchNibble(false),
  nonLatchNibble(false),
  keepLoopAlive(false),
  keepGrooveAlive(false),
  orderScrollLocked(false),
  orderScrollTolerance(false),
  dragMobileMenu(false),
  dragMobileEditButton(false),
  wantGrooveListFocus(false),
  lastAssetType(0),
  curWindow(GUI_WINDOW_NOTHING),
  nextWindow(GUI_WINDOW_NOTHING),
  curWindowLast(GUI_WINDOW_NOTHING),
  curWindowThreadSafe(GUI_WINDOW_NOTHING),
  lastPatternWidth(0.0f),
  longThreshold(0.48f),
  buttonLongThreshold(0.20f),
  latchNote(-1),
  latchIns(-2),
  latchVol(-1),
  latchEffect(-1),
  latchEffectVal(-1),
  wavePreviewLen(32),
  wavePreviewHeight(255),
  wavePreviewInit(true),
  wavePreviewPaused(false),
  pgSys(0),
  pgAddr(0),
  pgVal(0),
  curQueryRangeX(false),
  curQueryBackwards(false),
  curQueryRangeXMin(0), curQueryRangeXMax(0),
  curQueryRangeY(0),
  curQueryEffectPos(0),
  queryReplaceEffectCount(0),
  queryReplaceEffectPos(1),
  queryReplaceNoteMode(0),
  queryReplaceInsMode(0),
  queryReplaceVolMode(0),
  queryReplaceNote(0),
  queryReplaceIns(0),
  queryReplaceVol(0),
  queryReplaceNoteDo(false),
  queryReplaceInsDo(false),
  queryReplaceVolDo(false),
  queryViewingResults(false),
  wavePreviewOn(false),
  wavePreviewKey((SDL_Scancode)0),
  wavePreviewNote(0),
  samplePreviewOn(false),
  samplePreviewKey((SDL_Scancode)0),
  samplePreviewNote(0),
  sampleMapSelStart(-1),
  sampleMapSelEnd(-1),
  sampleMapDigit(0),
  sampleMapColumn(0),
  sampleMapFocused(false),
  sampleMapWaitingInput(false),
  macroDragStart(0,0),
  macroDragAreaSize(0,0),
  macroDragCTarget(NULL),
  macroDragTarget(NULL),
  macroDragLen(0),
  macroDragMin(0),
  macroDragMax(0),
  macroDragLastX(-1),
  macroDragLastY(-1),
  macroDragBitOff(0),
  macroDragScroll(0),
  macroDragBitMode(false),
  macroDragInitialValueSet(false),
  macroDragInitialValue(false),
  macroDragChar(false),
  macroDragBit30(false),
  macroDragSettingBit30(false),
  macroDragLineMode(false),
  macroDragMouseMoved(false),
  macroDragLineInitial(0,0),
  macroDragLineInitialV(0,0),
  macroDragActive(false),
  lastMacroDesc(NULL,NULL,0,0,0.0f),
  macroOffX(0),
  macroOffY(0),
  macroScaleX(100.0f),
  macroScaleY(100.0f),
  macroRandMin(0),
  macroRandMax(0),
  macroLoopDragStart(0,0),
  macroLoopDragAreaSize(0,0),
  macroLoopDragTarget(NULL),
  macroLoopDragLen(0),
  macroLoopDragActive(false),
  waveDragStart(0,0),
  waveDragAreaSize(0,0),
  waveDragTarget(NULL),
  waveDragLen(0),
  waveDragMin(0),
  waveDragMax(0),
  waveDragActive(false),
  bindSetTarget(0),
  bindSetPrevValue(0),
  bindSetActive(false),
  bindSetPending(false),
  nextScroll(-1.0f),
  nextAddScroll(0.0f),
  orderScroll(0.0f),
  orderScrollSlideOrigin(0.0f),
  orderScrollRealOrigin(0.0f,0.0f),
  dragMobileMenuOrigin(0.0f,0.0f),
  layoutTimeBegin(0),
  layoutTimeEnd(0),
  layoutTimeDelta(0),
  renderTimeBegin(0),
  renderTimeEnd(0),
  renderTimeDelta(0),
  drawTimeBegin(0),
  drawTimeEnd(0),
  drawTimeDelta(0),
  eventTimeBegin(0),
  eventTimeEnd(0),
  eventTimeDelta(0),
  perfMetricsLen(0),
  chanToMove(-1),
  sysToMove(-1),
  sysToDelete(-1),
  opToMove(-1),
  assetToMove(-1),
  dirToMove(-1),
  transposeAmount(0),
  randomizeMin(0),
  randomizeMax(255),
  fadeMin(0),
  fadeMax(255),
  collapseAmount(2),
  scaleMax(100.0f),
  fadeMode(false),
  randomMode(false),
  haveHitBounds(false),
  pendingStepUpdate(0),
  oldOrdersLen(0),
  sampleZoom(1.0),
  prevSampleZoom(1.0),
  minSampleZoom(1.0),
  samplePos(0),
  resizeSize(1024),
  silenceSize(1024),
  resampleTarget(32000),
  resampleStrat(5),
  amplifyVol(100.0),
  sampleSelStart(-1),
  sampleSelEnd(-1),
  sampleInfo(true),
  sampleCompatRate(false),
  sampleDragActive(false),
  sampleDragMode(false),
  sampleDrag16(false),
  sampleZoomAuto(true),
  sampleSelTarget(0),
  sampleDragTarget(NULL),
  sampleDragStart(0,0),
  sampleDragAreaSize(0,0),
  sampleDragLen(0),
  sampleFilterL(1.0f),
  sampleFilterB(0.0f),
  sampleFilterH(0.0f),
  sampleFilterRes(0.25f),
  sampleFilterCutStart(16000.0f),
  sampleFilterCutEnd(100.0f),
  sampleFilterPower(1),
  sampleClipboard(NULL),
  sampleClipboardLen(0),
  openSampleResizeOpt(false),
  openSampleResampleOpt(false),
  openSampleAmplifyOpt(false),
  openSampleSilenceOpt(false),
  openSampleFilterOpt(false),
  selectedPortSet(0x1fff),
  selectedSubPort(-1),
  hoveredPortSet(0x1fff),
  hoveredSubPort(-1),
  portDragActive(false),
  displayHiddenPorts(false),
  displayInternalPorts(false),
  subPortPos(0.0f,0.0f),
  oscTotal(0),
  oscWidth(512),
  oscZoom(0.5f),
  oscWindowSize(20.0f),
  oscInput(0.0f),
  oscInput1(0.0f),
  oscZoomSlider(false),
  chanOscCols(3),
  chanOscAutoColsType(0),
  chanOscColorX(GUI_OSCREF_CENTER),
  chanOscColorY(GUI_OSCREF_CENTER),
  chanOscWindowSize(20.0f),
  chanOscTextX(0.0f),
  chanOscTextY(0.0f),
  chanOscAmplify(0.95f),
  chanOscWaveCorr(true),
  chanOscOptions(false),
  updateChanOscGradTex(true),
  chanOscUseGrad(false),
  chanOscNormalize(false),
  chanOscTextFormat("%c"),
  chanOscColor(1.0f,1.0f,1.0f,1.0f),
  chanOscTextColor(1.0f,1.0f,1.0f,0.75f),
  chanOscGrad(64,64),
  chanOscGradTex(NULL),
  followLog(true),
#ifdef IS_MOBILE
  pianoOctaves(7),
  pianoOctavesEdit(2),
  pianoOptions(true),
  pianoSharePosition(false),
  pianoOptionsSet(false),
  pianoReadonly(false),
  pianoOffset(6),
  pianoOffsetEdit(9),
  pianoView(PIANO_LAYOUT_AUTOMATIC),
  pianoInputPadMode(PIANO_INPUT_PAD_SPLIT_AUTO),
#else
  pianoOctaves(7),
  pianoOctavesEdit(4),
  pianoOptions(false),
  pianoSharePosition(true),
  pianoReadonly(false),
  pianoOffset(6),
  pianoOffsetEdit(6),
  pianoView(PIANO_LAYOUT_STANDARD),
  pianoInputPadMode(PIANO_INPUT_PAD_DISABLE),
#endif
  hasACED(false),
  waveGenBaseShape(0),
  waveInterpolation(0),
  waveGenDuty(0.5f),
  waveGenPower(1),
  waveGenInvertPoint(1.0f),
  waveGenScaleX(32),
  waveGenScaleY(32),
  waveGenOffsetX(0),
  waveGenOffsetY(0),
  waveGenSmooth(1),
  waveGenAmplify(1.0f),
  waveGenFM(false),
  introPos(0.0),
  introSkip(0.0),
  monitorPos(0.0),
  mustClear(2),
  initialScreenWipe(1.0f),
  introSkipDo(false),
  introStopped(false),
  curTutorial(-1),
  curTutorialStep(0) {
  // value keys
  valueKeys[SDLK_0]=0;
  valueKeys[SDLK_1]=1;
  valueKeys[SDLK_2]=2;
  valueKeys[SDLK_3]=3;
  valueKeys[SDLK_4]=4;
  valueKeys[SDLK_5]=5;
  valueKeys[SDLK_6]=6;
  valueKeys[SDLK_7]=7;
  valueKeys[SDLK_8]=8;
  valueKeys[SDLK_9]=9;
  valueKeys[SDLK_a]=10;
  valueKeys[SDLK_b]=11;
  valueKeys[SDLK_c]=12;
  valueKeys[SDLK_d]=13;
  valueKeys[SDLK_e]=14;
  valueKeys[SDLK_f]=15;
  valueKeys[SDLK_KP_0]=0;
  valueKeys[SDLK_KP_1]=1;
  valueKeys[SDLK_KP_2]=2;
  valueKeys[SDLK_KP_3]=3;
  valueKeys[SDLK_KP_4]=4;
  valueKeys[SDLK_KP_5]=5;
  valueKeys[SDLK_KP_6]=6;
  valueKeys[SDLK_KP_7]=7;
  valueKeys[SDLK_KP_8]=8;
  valueKeys[SDLK_KP_9]=9;

  memset(willExport,1,DIV_MAX_CHIPS*sizeof(bool));

  memset(peak,0,DIV_MAX_OUTPUTS*sizeof(float));

  opMaskTransposeNote.note=true;
  opMaskTransposeNote.ins=false;
  opMaskTransposeNote.vol=false;
  opMaskTransposeNote.effect=false;
  opMaskTransposeNote.effectVal=false;

  opMaskTransposeValue.note=false;
  opMaskTransposeValue.ins=true;
  opMaskTransposeValue.vol=true;
  opMaskTransposeValue.effect=false;
  opMaskTransposeValue.effectVal=true;

  memset(actionKeys,0,GUI_ACTION_MAX*sizeof(int));

  memset(patChanX,0,sizeof(float)*(DIV_MAX_CHANS+1));
  memset(patChanSlideY,0,sizeof(float)*(DIV_MAX_CHANS+1));
  memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS);
  memset(oscValues,0,sizeof(void*)*DIV_MAX_OUTPUTS);

  memset(chanOscLP0,0,sizeof(float)*DIV_MAX_CHANS);
  memset(chanOscLP1,0,sizeof(float)*DIV_MAX_CHANS);
  memset(chanOscVol,0,sizeof(float)*DIV_MAX_CHANS);
  memset(chanOscPitch,0,sizeof(float)*DIV_MAX_CHANS);
  memset(chanOscBright,0,sizeof(float)*DIV_MAX_CHANS);
  memset(lastCorrPos,0,sizeof(short)*DIV_MAX_CHANS);

  memset(acedData,0,23);

  memset(waveGenAmp,0,sizeof(float)*16);
  memset(waveGenPhase,0,sizeof(float)*16);
  waveGenTL[0]=0.0f;
  waveGenTL[1]=0.0f;
  waveGenTL[2]=0.0f;
  waveGenTL[3]=1.0f;
  fmWaveform[0]=0;
  fmWaveform[1]=0;
  fmWaveform[2]=0;
  fmWaveform[3]=0;
  waveGenMult[0]=1;
  waveGenMult[1]=1;
  waveGenMult[2]=1;
  waveGenMult[3]=1;
  memset(waveGenFB,0,sizeof(int)*4);
  memset(waveGenFMCon0,0,sizeof(bool)*5);
  memset(waveGenFMCon1,0,sizeof(bool)*5);
  memset(waveGenFMCon2,0,sizeof(bool)*5);
  memset(waveGenFMCon3, 0, sizeof(bool) * 5);
  memset(waveGenFMCon4,0,sizeof(bool)*5);

  waveGenAmp[0]=1.0f;
  waveGenFMCon0[0]=false;
  waveGenFMCon1[0]= true;
  waveGenFMCon2[1]= true;
  waveGenFMCon3[2] = true;
  waveGenFMCon4[0]= false;

  waveGenFMCon0[4] = false;
  waveGenFMCon1[4] = false;
  waveGenFMCon2[4] = false;
  waveGenFMCon3[4] = true;

  memset(keyHit,0,sizeof(float)*DIV_MAX_CHANS);
  memset(keyHit1,0,sizeof(float)*DIV_MAX_CHANS);

  memset(pianoKeyHit,0,sizeof(float)*180);
  memset(pianoKeyPressed,0,sizeof(bool)*180);

  memset(queryReplaceEffectMode,0,sizeof(int)*8);
  memset(queryReplaceEffectValMode,0,sizeof(int)*8);
  memset(queryReplaceEffect,0,sizeof(int)*8);
  memset(queryReplaceEffectVal,0,sizeof(int)*8);
  memset(queryReplaceEffectDo,0,sizeof(bool)*8);
  memset(queryReplaceEffectValDo,0,sizeof(bool)*8);

  chanOscGrad.bgColor=ImVec4(0.0f,0.0f,0.0f,1.0f);

  memset(noteOffLabel,0,32);
  memset(noteRelLabel,0,32);
  memset(macroRelLabel,0,32);
  memset(emptyLabel,0,32);
  memset(emptyLabel2,0,32);
  //effect sorting
  memset(effectsShow,1,sizeof(bool)*10);

  strncpy(noteOffLabel,"OFF",32);
  strncpy(noteRelLabel,"===",32);
  strncpy(macroRelLabel,"REL",32);
  strncpy(emptyLabel,"...",32);
  strncpy(emptyLabel2,"..",32);
}
