diff --git a/CMakeLists.txt b/CMakeLists.txt index 458cd448e..f994fe8bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -596,6 +596,7 @@ src/gui/editControls.cpp src/gui/effectList.cpp src/gui/findReplace.cpp src/gui/gradient.cpp +src/gui/grooves.cpp src/gui/insEdit.cpp src/gui/log.cpp src/gui/mixer.cpp diff --git a/papers/format.md b/papers/format.md index 389b6e61e..3f93f451d 100644 --- a/papers/format.md +++ b/papers/format.md @@ -32,6 +32,7 @@ these fields are 0 in format versions prior to 100 (0.6pre1). the format versions are: +- 139: Furnace dev139 - 138: Furnace dev138 - 137: Furnace dev137 - 136: Furnace dev136 @@ -403,6 +404,14 @@ size | description --- | **a couple more compat flags** (>=138) 1 | broken portamento during legato 7 | reserved + --- | **speed pattern of first song** (>=139) + 1 | length of speed pattern (fail if this is lower than 0 or higher than 16) + 16 | speed pattern (this overrides speed 1 and speed 2 settings) + --- | **groove list** (>=139) + 1 | number of entries + ??? | groove entries. the format is: + | - 1 byte: length of groove + | - 16 bytes: groove pattern ``` # patchbay @@ -472,6 +481,9 @@ size | description | - a list of channelCount C strings S?? | channel short names | - same as above + --- | **speed pattern** (>=139) + 1 | length of speed pattern (fail if this is lower than 0 or higher than 16) + 16 | speed pattern (this overrides speed 1 and speed 2 settings) ``` # chip flags diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 17926e0ec..34e7c6b04 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -60,7 +60,7 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul case 0x08: return "08xy: Set panning (x: left; y: right)"; case 0x09: - return "09xx: Set speed 1"; + return "09xx: Set groove pattern (speed 1 if no grooves exist)"; case 0x0a: return "0Axy: Volume slide (0y: down; x0: up)"; case 0x0b: @@ -70,7 +70,7 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul case 0x0d: return "0Dxx: Jump to next pattern"; case 0x0f: - return "0Fxx: Set speed 2"; + return "0Fxx: Set speed (speed 2 if no grooves exist)"; case 0x80: return "80xx: Set panning (00: left; 80: center; FF: right)"; case 0x81: @@ -1959,14 +1959,12 @@ String DivEngine::getPlaybackDebugInfo() { "cmdsPerSecond: %d\n" "globalPitch: %d\n" "extValue: %d\n" - "speed1: %d\n" - "speed2: %d\n" "tempoAccum: %d\n" "totalProcessed: %d\n" "bufferPos: %d\n", curOrder,prevOrder,curRow,prevRow,ticks,subticks,totalLoops,lastLoopPos,nextSpeed,divider,cycles,clockDrift, changeOrd,changePos,totalSeconds,totalTicks,totalTicksR,totalCmds,lastCmds,cmdsPerSecond,globalPitch, - (int)extValue,(int)speed1,(int)speed2,(int)tempoAccum,(int)totalProcessed,(int)bufferPos + (int)extValue,(int)tempoAccum,(int)totalProcessed,(int)bufferPos ); } @@ -2091,7 +2089,8 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { lastLoopPos=-1; } endOfSong=false; - speedAB=false; + // whaaaaa? + curSpeed=0; playing=true; skipping=true; memset(walked,0,8192); @@ -2439,15 +2438,14 @@ void DivEngine::reset() { } extValue=0; extValuePresent=0; - speed1=curSubSong->speed1; - speed2=curSubSong->speed2; + speeds=curSubSong->speeds; firstTick=false; shallStop=false; shallStopSched=false; pendingMetroTick=0; elapsedBars=0; elapsedBeats=0; - nextSpeed=speed1; + nextSpeed=speeds.val[0]; divider=60; if (curSubSong->customTempo) { divider=curSubSong->hz; @@ -2647,12 +2645,8 @@ size_t DivEngine::getCurrentSubSong() { return curSubSongIndex; } -unsigned char DivEngine::getSpeed1() { - return speed1; -} - -unsigned char DivEngine::getSpeed2() { - return speed2; +const DivGroovePattern& DivEngine::getSpeeds() { + return speeds; } float DivEngine::getHz() { @@ -4234,7 +4228,7 @@ void DivEngine::quitDispatch() { clockDrift=0; chans=0; playing=false; - speedAB=false; + curSpeed=0; endOfSong=false; ticks=0; tempoAccum=0; diff --git a/src/engine/engine.h b/src/engine/engine.h index fc552cacc..b3604baa3 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -47,8 +47,8 @@ #define BUSY_BEGIN_SOFT softLocked=true; isBusy.lock(); #define BUSY_END isBusy.unlock(); softLocked=false; -#define DIV_VERSION "dev138" -#define DIV_ENGINE_VERSION 138 +#define DIV_VERSION "dev139" +#define DIV_ENGINE_VERSION 139 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 @@ -337,7 +337,6 @@ class DivEngine { bool playing; bool freelance; bool shallStop, shallStopSched; - bool speedAB; bool endOfSong; bool consoleMode; bool extValuePresent; @@ -359,7 +358,7 @@ class DivEngine { bool midiOutClock; int midiOutMode; int softLockCount; - int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, nextSpeed, elapsedBars, elapsedBeats; + int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, nextSpeed, elapsedBars, elapsedBeats, curSpeed; size_t curSubSongIndex; size_t bufferPos; double divider; @@ -368,7 +367,7 @@ class DivEngine { int stepPlay; int changeOrd, changePos, totalSeconds, totalTicks, totalTicksR, totalCmds, lastCmds, cmdsPerSecond, globalPitch; unsigned char extValue, pendingMetroTick; - unsigned char speed1, speed2; + DivGroovePattern speeds; short tempoAccum; DivStatusView view; DivHaltPositions haltOn; @@ -730,11 +729,8 @@ class DivEngine { // get current subsong size_t getCurrentSubSong(); - // get speed 1 - unsigned char getSpeed1(); - - // get speed 2 - unsigned char getSpeed2(); + // get speeds + const DivGroovePattern& getSpeeds(); // get Hz float getHz(); @@ -1065,7 +1061,6 @@ class DivEngine { freelance(false), shallStop(false), shallStopSched(false), - speedAB(false), endOfSong(false), consoleMode(false), extValuePresent(false), @@ -1099,6 +1094,7 @@ class DivEngine { nextSpeed(3), elapsedBars(0), elapsedBeats(0), + curSpeed(0), curSubSongIndex(0), bufferPos(0), divider(60), @@ -1116,8 +1112,6 @@ class DivEngine { globalPitch(0), extValue(0), pendingMetroTick(0), - speed1(3), - speed2(3), tempoAccum(0), view(DIV_STATUS_NOTHING), haltOn(DIV_HALT_NONE), diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index e2629dfa0..aac777f34 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -83,7 +83,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } ds.version=(unsigned char)reader.readC(); logI("module version %d (0x%.2x)",ds.version,ds.version); - if (ds.version>0x1a) { + if (ds.version>0x1b) { logE("this version is not supported by Furnace yet!"); lastError="this version is not supported by Furnace yet"; delete[] file; @@ -219,14 +219,15 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { } ds.subsong[0]->timeBase=reader.readC(); - ds.subsong[0]->speed1=reader.readC(); + ds.subsong[0]->speeds.len=2; + ds.subsong[0]->speeds.val[0]=reader.readC(); if (ds.version>0x07) { - ds.subsong[0]->speed2=reader.readC(); + ds.subsong[0]->speeds.val[1]=reader.readC(); ds.subsong[0]->pal=reader.readC(); ds.subsong[0]->hz=(ds.subsong[0]->pal)?60:50; ds.subsong[0]->customTempo=reader.readC(); } else { - ds.subsong[0]->speed2=ds.subsong[0]->speed1; + ds.subsong[0]->speeds.len=1; } if (ds.version>0x0a) { String hz=reader.readString(3); @@ -827,6 +828,8 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { for (int i=0; idepth=DIV_SAMPLE_DEPTH_YMZ_ADPCM; } } + if (ds.version>=0x1a) { + // what the hell man... + cutStart=reader.readI(); + cutEnd=reader.readI(); + if (cutStart<0 || cutStart>length) { + logE("cutStart is out of range! (%d)",cutStart); + lastError="file is corrupt or unreadable at samples"; + delete[] file; + return false; + } + if (cutEnd<0 || cutEnd>length) { + logE("cutEnd is out of range! (%d)",cutEnd); + lastError="file is corrupt or unreadable at samples"; + delete[] file; + return false; + } + if (cutEnd0) { if (ds.version>0x08) { if (ds.version<0x0b) { @@ -876,6 +902,19 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { data=new short[length]; reader.read(data,length*2); } + + if (ds.version>0x1a) { + if (cutStart!=0 || cutEnd!=length) { + // cut data + short* newData=new short[cutEnd-cutStart]; + memcpy(newData,&data[cutStart],(cutEnd-cutStart)*sizeof(short)); + delete[] data; + data=newData; + length=cutEnd-cutStart; + cutStart=0; + cutEnd=length; + } + } #ifdef TA_BIG_ENDIAN // convert to big-endian @@ -1742,8 +1781,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { reader.readI(); subSong->timeBase=reader.readC(); - subSong->speed1=reader.readC(); - subSong->speed2=reader.readC(); + subSong->speeds.len=2; + subSong->speeds.val[0]=reader.readC(); + subSong->speeds.val[1]=reader.readC(); subSong->arpLen=reader.readC(); subSong->hz=reader.readF(); subSong->pal=(subSong->hz>=53); @@ -2231,6 +2271,25 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } } + if (ds.version>=139) { + subSong->speeds.len=reader.readC(); + for (int i=0; i<16; i++) { + subSong->speeds.val[i]=reader.readC(); + } + + // grooves + unsigned char grooveCount=reader.readC(); + for (int i=0; i=119) { logD("reading chip flags..."); @@ -2289,8 +2348,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { subSong=ds.subsong[i+1]; subSong->timeBase=reader.readC(); - subSong->speed1=reader.readC(); - subSong->speed2=reader.readC(); + subSong->speeds.len=2; + subSong->speeds.val[0]=reader.readC(); + subSong->speeds.val[1]=reader.readC(); subSong->arpLen=reader.readC(); subSong->hz=reader.readF(); subSong->pal=(subSong->hz>=53); @@ -2338,6 +2398,13 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { for (int i=0; ichanShortName[i]=reader.readString(); } + + if (ds.version>=139) { + subSong->speeds.len=reader.readC(); + for (int i=0; i<16; i++) { + subSong->speeds.val[i]=reader.readC(); + } + } } } @@ -2956,7 +3023,6 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { if (fxVal>0x20 && ds.name!="klisje paa klisje") { writeFxCol(0xf0,fxVal); } else { - writeFxCol(0x09,fxVal); writeFxCol(0x0f,fxVal); } break; @@ -3435,8 +3501,8 @@ bool DivEngine::loadFC(unsigned char* file, size_t len) { ds.subsong[0]->pal=true; ds.subsong[0]->customTempo=true; ds.subsong[0]->pat[3].effectCols=3; - ds.subsong[0]->speed1=3; - ds.subsong[0]->speed2=3; + ds.subsong[0]->speeds.val[0]=3; + ds.subsong[0]->speeds.len=1; int lastIns[4]; int lastNote[4]; @@ -3453,10 +3519,8 @@ bool DivEngine::loadFC(unsigned char* file, size_t len) { ds.subsong[0]->orders.ord[j][i]=i; DivPattern* p=ds.subsong[0]->pat[j].getPattern(i,true); if (j==3 && seq[i].speed) { - p->data[0][6]=0x09; + p->data[0][6]=0x0f; p->data[0][7]=seq[i].speed; - p->data[0][8]=0x0f; - p->data[0][9]=seq[i].speed; } bool ignoreNext=false; @@ -4343,8 +4407,9 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeI(0); w->writeC(subSong->timeBase); - w->writeC(subSong->speed1); - w->writeC(subSong->speed2); + // these are for compatibility + w->writeC(subSong->speeds.val[0]); + w->writeC((subSong->speeds.len>=2)?subSong->speeds.val[1]:subSong->speeds.val[0]); w->writeC(subSong->arpLen); w->writeF(subSong->hz); w->writeS(subSong->patLen); @@ -4531,6 +4596,21 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeC(0); } + // speeds of first song + w->writeC(subSong->speeds.len); + for (int i=0; i<16; i++) { + w->writeC(subSong->speeds.val[i]); + } + + // groove list + w->writeC((unsigned char)song.grooves.size()); + for (const DivGroovePattern& i: song.grooves) { + w->writeC(i.len); + for (int j=0; j<16; j++) { + w->writeC(i.val[j]); + } + } + blockEndSeek=w->tell(); w->seek(blockStartSeek,SEEK_SET); w->writeI(blockEndSeek-blockStartSeek-4); @@ -4545,8 +4625,8 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeI(0); w->writeC(subSong->timeBase); - w->writeC(subSong->speed1); - w->writeC(subSong->speed2); + w->writeC(subSong->speeds.val[0]); + w->writeC((subSong->speeds.len>=2)?subSong->speeds.val[1]:subSong->speeds.val[0]); w->writeC(subSong->arpLen); w->writeF(subSong->hz); w->writeS(subSong->patLen); @@ -4585,6 +4665,12 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeString(subSong->chanShortName[i],false); } + // speeds + w->writeC(subSong->speeds.len); + for (int i=0; i<16; i++) { + w->writeC(subSong->speeds.val[i]); + } + blockEndSeek=w->tell(); w->seek(blockStartSeek,SEEK_SET); w->writeI(blockEndSeek-blockStartSeek-4); @@ -4840,8 +4926,8 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { w->writeC(curSubSong->hilightB); w->writeC(curSubSong->timeBase); - w->writeC(curSubSong->speed1); - w->writeC(curSubSong->speed2); + w->writeC(curSubSong->speeds.val[0]); + w->writeC((curSubSong->speeds.len>=2)?curSubSong->speeds.val[1]:curSubSong->speeds.val[0]); w->writeC(curSubSong->pal); w->writeC(curSubSong->customTempo); char customHz[4]; @@ -4865,6 +4951,14 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { addWarning("only the currently selected subsong will be saved"); } + if (!song.grooves.empty()) { + addWarning("grooves will not be saved"); + } + + if (curSubSong->speeds.len>2) { + addWarning("only the first two speeds will be effective"); + } + if (curSubSong->virtualTempoD!=curSubSong->virtualTempoN) { addWarning(".dmf format does not support virtual tempo"); } diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index abcb8dc56..823b2f8a9 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -400,11 +400,22 @@ void DivEngine::processRow(int i, bool afterDelay) { if (effectVal==-1) effectVal=0; switch (effect) { - case 0x09: // speed 1 - if (effectVal>0) speed1=effectVal; + case 0x09: // select groove pattern/speed 1 + if (song.grooves.empty()) { + if (effectVal>0) speeds.val[0]=effectVal; + } else { + if (effectVal<(short)song.grooves.size()) { + speeds=song.grooves[effectVal]; + curSpeed=0; + } + } break; - case 0x0f: // speed 2 - if (effectVal>0) speed2=effectVal; + case 0x0f: // speed 1/speed 2 + if (speeds.len==2 && song.grooves.empty()) { + if (effectVal>0) speeds.val[1]=effectVal; + } else { + if (effectVal>0) speeds.val[0]=effectVal; + } break; case 0x0b: // change order if (changeOrd==-1 || song.jumpTreatment==0) { @@ -1071,6 +1082,9 @@ void DivEngine::nextRow() { } if (song.brokenSpeedSel) { + unsigned char speed2=(speeds.len>=2)?speeds.val[1]:speeds.val[0]; + unsigned char speed1=speeds.val[0]; + if ((curSubSong->patLen&1) && curOrder&1) { ticks=((curRow&1)?speed2:speed1)*(curSubSong->timeBase+1); nextSpeed=(curRow&1)?speed1:speed2; @@ -1079,14 +1093,10 @@ void DivEngine::nextRow() { nextSpeed=(curRow&1)?speed2:speed1; } } else { - if (speedAB) { - ticks=speed2*(curSubSong->timeBase+1); - nextSpeed=speed1; - } else { - ticks=speed1*(curSubSong->timeBase+1); - nextSpeed=speed2; - } - speedAB=!speedAB; + ticks=speeds.val[curSpeed]*(curSubSong->timeBase+1); + curSpeed++; + if (curSpeed>=speeds.len) curSpeed=0; + nextSpeed=speeds.val[curSpeed]; } // post row details diff --git a/src/engine/song.h b/src/engine/song.h index 6aaec7c12..72fbea0ef 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -127,10 +127,20 @@ enum DivSystem { DIV_SYSTEM_YM2608_CSM }; +struct DivGroovePattern { + unsigned char val[16]; + unsigned char len; + DivGroovePattern(): + len(1) { + memset(val,6,16); + } +}; + struct DivSubSong { String name, notes; unsigned char hilightA, hilightB; - unsigned char timeBase, speed1, speed2, arpLen; + unsigned char timeBase, arpLen; + DivGroovePattern speeds; short virtualTempoN, virtualTempoD; bool pal; bool customTempo; @@ -153,8 +163,6 @@ struct DivSubSong { hilightA(4), hilightB(16), timeBase(0), - speed1(6), - speed2(6), arpLen(1), virtualTempoN(150), virtualTempoD(150), @@ -338,6 +346,7 @@ struct DivSong { std::vector subsong; std::vector patchbay; + std::vector grooves; DivInstrument nullIns, nullInsOPLL, nullInsOPL, nullInsOPLDrums, nullInsQSound; DivWavetable nullWave; diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp index 60511f09e..83ab4e37e 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -265,6 +265,9 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_WINDOW_FIND: nextWindow=GUI_WINDOW_FIND; break; + case GUI_ACTION_WINDOW_GROOVES: + nextWindow=GUI_WINDOW_GROOVES; + break; case GUI_ACTION_COLLAPSE_WINDOW: collapseWindow=true; @@ -358,6 +361,9 @@ void FurnaceGUI::doAction(int what) { case GUI_WINDOW_FIND: findOpen=false; break; + case GUI_WINDOW_GROOVES: + groovesOpen=false; + break; default: break; } diff --git a/src/gui/grooves.cpp b/src/gui/grooves.cpp new file mode 100644 index 000000000..560a09e68 --- /dev/null +++ b/src/gui/grooves.cpp @@ -0,0 +1,154 @@ +/** + * 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. + */ + +#include "gui.h" +#include "imgui.h" +#include "misc/cpp/imgui_stdlib.h" +#include "IconsFontAwesome4.h" +#include +#include "intConst.h" + +void FurnaceGUI::drawGrooves() { + if (nextWindow==GUI_WINDOW_GROOVES) { + groovesOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!groovesOpen) return; + ImGui::SetNextWindowSizeConstraints(ImVec2(64.0f*dpiScale,32.0f*dpiScale),ImVec2(canvasW,canvasH)); + if (ImGui::Begin("Grooves",&groovesOpen,globalWinFlags)) { + int delGroove=-1; + + ImGui::Text("use effect 09xx to select a groove pattern."); + if (!e->song.grooves.empty()) if (ImGui::BeginTable("GrooveList",3,ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed); + + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("#"); + ImGui::TableNextColumn(); + ImGui::Text("pattern"); + ImGui::TableNextColumn(); + ImGui::Text("remove"); + + int index=0; + for (DivGroovePattern& i: e->song.grooves) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushFont(patFont); + ImGui::Text("%.2X",index); + ImGui::PopFont(); + + ImGui::TableNextColumn(); + + String grooveStr; + + if (curGroove==index) { + int intVersion[256]; + unsigned char intVersionLen=i.len; + unsigned char ignoredLoop=0; + unsigned char ignoredRel=0; + memset(intVersion,0,sizeof(int)); + for (int j=0; j<16; j++) { + intVersion[j]=i.val[j]; + } + if (intVersionLen>16) intVersionLen=16; + grooveStr=fmt::sprintf("##_GRI%d",index); + bool wantedFocus=wantGrooveListFocus; + if (wantGrooveListFocus) { + wantGrooveListFocus=false; + ImGui::SetItemDefaultFocus(); + ImGui::SetKeyboardFocusHere(); + } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputText(grooveStr.c_str(),&grooveListString)) { + decodeMMLStr(grooveListString,intVersion,intVersionLen,ignoredLoop,1,255,ignoredRel); + if (intVersionLen<1) { + intVersionLen=1; + intVersion[0]=6; + } + if (intVersionLen>16) intVersionLen=16; + e->lockEngine([&i,intVersion,intVersionLen]() { + i.len=intVersionLen; + for (int j=0; j<16; j++) { + i.val[j]=intVersion[j]; + } + }); + MARK_MODIFIED; + } + if (!ImGui::IsItemActive() && !wantedFocus) { + curGroove=-1; + //encodeMMLStr(grooveListString,intVersion,intVersionLen,-1,-1,false); + } + } else { + String grooveStr; + + for (int j=0; j0) { + grooveStr+=' '; + } + grooveStr+=fmt::sprintf("%d",(int)i.val[j]); + } + + size_t groovePrevLen=grooveStr.size(); + + grooveStr+=fmt::sprintf("##_GR%d",index); + + if (ImGui::Selectable(grooveStr.c_str(),false)) { + curGroove=index; + grooveListString=grooveStr.substr(0,groovePrevLen); + wantGrooveListFocus=true; + } + } + + ImGui::TableNextColumn(); + String grooveID=fmt::sprintf(ICON_FA_TIMES "##GRR%d",index); + if (ImGui::Button(grooveID.c_str())) { + delGroove=index; + } + + index++; + } + + ImGui::EndTable(); + } + + if (delGroove>=0) { + e->lockEngine([this,delGroove]() { + e->song.grooves.erase(e->song.grooves.begin()+delGroove); + }); + MARK_MODIFIED; + } + + if (ImGui::Button(ICON_FA_PLUS "##AddGroove")) { + e->lockEngine([this]() { + e->song.grooves.push_back(DivGroovePattern()); + }); + MARK_MODIFIED; + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) { + curWindow=GUI_WINDOW_GROOVES; + } else { + curGroove=-1; + } + ImGui::End(); +} diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 67df8c87e..5b20c47eb 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -988,15 +988,19 @@ void FurnaceGUI::prepareLayout() { fclose(check); } -float FurnaceGUI::calcBPM(int s1, int s2, float hz, int vN, int vD) { +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=s1+s2; + float speedSum=0; + for (int i=0; iisPlaying()) { int totalTicks=e->getTotalTicks(); int totalSeconds=e->getTotalSeconds(); - ImGui::Text("| Speed %d:%d @ %gHz (%g BPM) | Order %d/%d | Row %d/%d | %d:%.2d:%.2d.%.2d",e->getSpeed1(),e->getSpeed2(),e->getCurHz(),calcBPM(e->getSpeed1(),e->getSpeed2(),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); + + 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; @@ -3917,6 +3936,7 @@ bool FurnaceGUI::loop() { drawPattern(); drawEditControls(); drawSpeed(); + drawGrooves(); drawSongInfo(); drawOrders(); drawSampleList(); @@ -5248,6 +5268,7 @@ bool FurnaceGUI::init() { 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",false); @@ -5623,6 +5644,7 @@ void FurnaceGUI::commitState() { 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); @@ -5835,6 +5857,7 @@ FurnaceGUI::FurnaceGUI(): dragDestinationY(0), oldBeat(-1), oldBar(-1), + curGroove(-1), soloTimeout(0.0f), exportFadeOut(5.0), editControlsOpen(true), @@ -5870,6 +5893,7 @@ FurnaceGUI::FurnaceGUI(): sysManagerOpen(false), clockOpen(false), speedOpen(true), + groovesOpen(false), clockShowReal(true), clockShowRow(true), clockShowBeat(true), @@ -5898,10 +5922,12 @@ FurnaceGUI::FurnaceGUI(): latchNibble(false), nonLatchNibble(false), keepLoopAlive(false), + keepGrooveAlive(false), orderScrollLocked(false), orderScrollTolerance(false), dragMobileMenu(false), dragMobileEditButton(false), + wantGrooveListFocus(false), curWindow(GUI_WINDOW_NOTHING), nextWindow(GUI_WINDOW_NOTHING), curWindowLast(GUI_WINDOW_NOTHING), diff --git a/src/gui/gui.h b/src/gui/gui.h index 440581a7b..ba8dc933f 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -321,6 +321,7 @@ enum FurnaceGUIWindows { GUI_WINDOW_SUBSONGS, GUI_WINDOW_FIND, GUI_WINDOW_CLOCK, + GUI_WINDOW_GROOVES, GUI_WINDOW_SPOILER }; @@ -466,6 +467,7 @@ enum FurnaceGUIActions { GUI_ACTION_WINDOW_SUBSONGS, GUI_ACTION_WINDOW_FIND, GUI_ACTION_WINDOW_CLOCK, + GUI_ACTION_WINDOW_GROOVES, GUI_ACTION_COLLAPSE_WINDOW, GUI_ACTION_CLOSE_WINDOW, @@ -1097,7 +1099,7 @@ class FurnaceGUI { String workingDirVGMExport, workingDirZSMExport, workingDirROMExport, workingDirFont, workingDirColors, workingDirKeybinds; String workingDirLayout, workingDirROM, workingDirTest; String mmlString[32]; - String mmlStringW, mmlStringSNES; + String mmlStringW, mmlStringSNES, grooveString, grooveListString; std::vector sysSearchResults; std::vector newSongSearchResults; @@ -1456,6 +1458,7 @@ class FurnaceGUI { int curIns, curWave, curSample, curOctave, curOrder, prevIns, oldRow, oldOrder, oldOrder1, editStep, exportLoops, soloChan,orderEditMode, orderCursor; int loopOrder, loopRow, loopEnd, isClipping, extraChannelButtons, patNameTarget, newSongCategory, latchTarget; int wheelX, wheelY, dragSourceX, dragSourceXFine, dragSourceY, dragDestinationX, dragDestinationXFine, dragDestinationY, oldBeat, oldBar; + int curGroove; float soloTimeout; double exportFadeOut; @@ -1465,6 +1468,7 @@ class FurnaceGUI { bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen; bool pianoOpen, notesOpen, channelsOpen, regViewOpen, logOpen, effectListOpen, chanOscOpen; bool subSongsOpen, findOpen, spoilerOpen, patManagerOpen, sysManagerOpen, clockOpen, speedOpen; + bool groovesOpen; bool clockShowReal, clockShowRow, clockShowBeat, clockShowMetro, clockShowTime; float clockMetroTick[16]; @@ -1472,7 +1476,7 @@ class FurnaceGUI { SelectionPoint selStart, selEnd, cursor, cursorDrag, dragStart, dragEnd; bool selecting, selectingFull, dragging, curNibble, orderNibble, followOrders, followPattern, changeAllOrders, mobileUI; bool collapseWindow, demandScrollX, fancyPattern, wantPatName, firstFrame, tempoView, waveHex, waveSigned, waveGenVisible, lockLayout, editOptsVisible, latchNibble, nonLatchNibble; - bool keepLoopAlive, orderScrollLocked, orderScrollTolerance, dragMobileMenu, dragMobileEditButton; + bool keepLoopAlive, keepGrooveAlive, orderScrollLocked, orderScrollTolerance, dragMobileMenu, dragMobileEditButton, wantGrooveListFocus; FurnaceGUIWindows curWindow, nextWindow, curWindowLast; std::atomic curWindowThreadSafe; float peak[DIV_MAX_OUTPUTS]; @@ -1801,7 +1805,7 @@ class FurnaceGUI { void pushAccentColors(const ImVec4& one, const ImVec4& two, const ImVec4& border, const ImVec4& borderShadow); void popAccentColors(); - float calcBPM(int s1, int s2, float hz, int vN, int vD); + float calcBPM(const DivGroovePattern& speeds, float hz, int vN, int vD); void patternRow(int i, bool isPlaying, float lineHeight, int chans, int ord, const DivPattern** patCache, bool inhibitSel); @@ -1821,6 +1825,7 @@ class FurnaceGUI { void drawEditControls(); void drawSongInfo(bool asChild=false); void drawSpeed(bool asChild=false); + void drawGrooves(); void drawOrders(); void drawPattern(); void drawInsList(bool asChild=false); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 2ffdbbbc3..0445704ed 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -520,6 +520,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WINDOW_SUBSONGS", "Subsongs", 0), D("WINDOW_FIND", "Find/Replace", FURKMOD_CMD|SDLK_f), D("WINDOW_CLOCK", "Clock", 0), + D("WINDOW_GROOVES", "Grooves", 0), D("COLLAPSE_WINDOW", "Collapse/expand current window", 0), D("CLOSE_WINDOW", "Close current window", FURKMOD_SHIFT|SDLK_ESCAPE), diff --git a/src/gui/speed.cpp b/src/gui/speed.cpp index 1d09331b4..796f0ec19 100644 --- a/src/gui/speed.cpp +++ b/src/gui/speed.cpp @@ -31,19 +31,26 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (!speedOpen && !asChild) return; bool began=asChild?ImGui::BeginChild("Speed"):ImGui::Begin("Speed",&speedOpen,globalWinFlags); if (began) { - if (ImGui::BeginTable("Props",3,ImGuiTableFlags_SizingStretchProp)) { + if (ImGui::BeginTable("Props",2,ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,0.0); ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.0); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.0); ImGui::TableNextRow(); ImGui::TableNextColumn(); - if (ImGui::Selectable(tempoView?"Base Tempo##TempoOrHz":"Tick Rate##TempoOrHz")) { + if (ImGui::SmallButton(tempoView?"Base Tempo##TempoOrHz":"Tick Rate##TempoOrHz")) { tempoView=!tempoView; } + if (ImGui::IsItemHovered()) { + if (tempoView) { + ImGui::SetTooltip("click to display tick rate"); + } else { + ImGui::SetTooltip("click to display base tempo"); + } + } ImGui::TableNextColumn(); float avail=ImGui::GetContentRegionAvail().x; - ImGui::SetNextItemWidth(avail); + float halfAvail=(avail-ImGui::GetStyle().ItemSpacing.x)*0.5; + ImGui::SetNextItemWidth(halfAvail); float setHz=tempoView?e->curSubSong->hz*2.5:e->curSubSong->hz; if (ImGui::InputFloat("##Rate",&setHz,1.0f,1.0f,"%g")) { MARK_MODIFIED if (tempoView) setHz/=2.5; @@ -52,40 +59,112 @@ void FurnaceGUI::drawSpeed(bool asChild) { e->setSongRate(setHz,setHz<52); } if (tempoView) { - ImGui::TableNextColumn(); + ImGui::SameLine(); ImGui::Text("= %gHz",e->curSubSong->hz); } else { if (e->curSubSong->hz>=49.98 && e->curSubSong->hz<=50.02) { - ImGui::TableNextColumn(); + ImGui::SameLine(); ImGui::Text("PAL"); } if (e->curSubSong->hz>=59.9 && e->curSubSong->hz<=60.11) { - ImGui::TableNextColumn(); + ImGui::SameLine(); ImGui::Text("NTSC"); } } ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::Text("Speed"); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - if (ImGui::InputScalar("##Speed1",ImGuiDataType_U8,&e->curSubSong->speed1,&_ONE,&_THREE)) { MARK_MODIFIED - if (e->curSubSong->speed1<1) e->curSubSong->speed1=1; - if (e->isPlaying()) play(); + if (keepGrooveAlive || e->curSubSong->speeds.len>2) { + if (ImGui::SmallButton("Groove")) { + e->lockEngine([this]() { + e->curSubSong->speeds.len=1; + }); + if (e->isPlaying()) play(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("click for one speed"); + } + } else if (e->curSubSong->speeds.len>1) { + if (ImGui::SmallButton("Speeds")) { + e->lockEngine([this]() { + e->curSubSong->speeds.len=4; + e->curSubSong->speeds.val[2]=e->curSubSong->speeds.val[0]; + e->curSubSong->speeds.val[3]=e->curSubSong->speeds.val[1]; + }); + if (e->isPlaying()) play(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("click for groove pattern"); + } + } else { + if (ImGui::SmallButton("Speed")) { + e->lockEngine([this]() { + e->curSubSong->speeds.len=2; + e->curSubSong->speeds.val[1]=e->curSubSong->speeds.val[0]; + }); + if (e->isPlaying()) play(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("click for two (alternating) speeds"); + } } ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); - if (ImGui::InputScalar("##Speed2",ImGuiDataType_U8,&e->curSubSong->speed2,&_ONE,&_THREE)) { MARK_MODIFIED - if (e->curSubSong->speed2<1) e->curSubSong->speed2=1; - if (e->isPlaying()) play(); + if (keepGrooveAlive || e->curSubSong->speeds.len>2) { + int intVersion[256]; + unsigned char intVersionLen=e->curSubSong->speeds.len; + unsigned char ignoredLoop=0; + unsigned char ignoredRel=0; + memset(intVersion,0,sizeof(int)); + for (int i=0; i<16; i++) { + intVersion[i]=e->curSubSong->speeds.val[i]; + } + if (intVersionLen>16) intVersionLen=16; + + keepGrooveAlive=false; + + ImGui::SetNextItemWidth(avail); + if (ImGui::InputText("##SpeedG",&grooveString)) { + decodeMMLStr(grooveString,intVersion,intVersionLen,ignoredLoop,1,255,ignoredRel); + if (intVersionLen<1) { + intVersionLen=1; + intVersion[0]=6; + } + if (intVersionLen>16) intVersionLen=16; + e->lockEngine([this,intVersion,intVersionLen]() { + e->curSubSong->speeds.len=intVersionLen; + for (int i=0; i<16; i++) { + e->curSubSong->speeds.val[i]=intVersion[i]; + } + }); + if (e->isPlaying()) play(); + MARK_MODIFIED; + } + if (!ImGui::IsItemActive()) { + encodeMMLStr(grooveString,intVersion,intVersionLen,-1,-1,false); + } else { + keepGrooveAlive=true; + } + } else { + ImGui::SetNextItemWidth(halfAvail); + if (ImGui::InputScalar("##Speed1",ImGuiDataType_U8,&e->curSubSong->speeds.val[0],&_ONE,&_THREE)) { MARK_MODIFIED + if (e->curSubSong->speeds.val[0]<1) e->curSubSong->speeds.val[0]=1; + if (e->isPlaying()) play(); + } + if (e->curSubSong->speeds.len>1) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(halfAvail); + if (ImGui::InputScalar("##Speed2",ImGuiDataType_U8,&e->curSubSong->speeds.val[1],&_ONE,&_THREE)) { MARK_MODIFIED + if (e->curSubSong->speeds.val[1]<1) e->curSubSong->speeds.val[1]=1; + if (e->isPlaying()) play(); + } + } } ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Virtual Tempo"); ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); + ImGui::SetNextItemWidth(halfAvail); if (ImGui::InputScalar("##VTempoN",ImGuiDataType_S16,&e->curSubSong->virtualTempoN,&_ONE,&_THREE)) { MARK_MODIFIED if (e->curSubSong->virtualTempoN<1) e->curSubSong->virtualTempoN=1; if (e->curSubSong->virtualTempoN>255) e->curSubSong->virtualTempoN=255; @@ -93,8 +172,8 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Numerator"); } - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); + ImGui::SameLine(); + ImGui::SetNextItemWidth(halfAvail); if (ImGui::InputScalar("##VTempoD",ImGuiDataType_S16,&e->curSubSong->virtualTempoD,&_ONE,&_THREE)) { MARK_MODIFIED if (e->curSubSong->virtualTempoD<1) e->curSubSong->virtualTempoD=1; if (e->curSubSong->virtualTempoD>255) e->curSubSong->virtualTempoD=255; @@ -105,28 +184,28 @@ void FurnaceGUI::drawSpeed(bool asChild) { ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::Text("TimeBase"); + ImGui::Text("Divider"); ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); + ImGui::SetNextItemWidth(halfAvail); unsigned char realTB=e->curSubSong->timeBase+1; if (ImGui::InputScalar("##TimeBase",ImGuiDataType_U8,&realTB,&_ONE,&_THREE)) { MARK_MODIFIED if (realTB<1) realTB=1; if (realTB>16) realTB=16; e->curSubSong->timeBase=realTB-1; } - ImGui::TableNextColumn(); - ImGui::Text("%.2f BPM",calcBPM(e->curSubSong->speed1,e->curSubSong->speed2,e->curSubSong->hz,e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD)); + ImGui::SameLine(); + ImGui::Text("%.2f BPM",calcBPM(e->curSubSong->speeds,e->curSubSong->hz,e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD)); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("Highlight"); ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); + ImGui::SetNextItemWidth(halfAvail); if (ImGui::InputScalar("##Highlight1",ImGuiDataType_U8,&e->curSubSong->hilightA,&_ONE,&_THREE)) { MARK_MODIFIED; } - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(avail); + ImGui::SameLine(); + ImGui::SetNextItemWidth(halfAvail); if (ImGui::InputScalar("##Highlight2",ImGuiDataType_U8,&e->curSubSong->hilightB,&_ONE,&_THREE)) { MARK_MODIFIED; }