From bcc94fd4594d683ecfaa087b37fa26dab4139d29 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 1 Oct 2022 23:59:23 -0500 Subject: [PATCH 01/57] truly fix .dmp arp macro saving (hopefully) --- src/engine/fileOps.cpp | 18 ++++++++++++++---- src/engine/instrument.cpp | 25 ++++++++++++++++++------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 3ffb586e..5033e3ec 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -4977,8 +4977,6 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { } } - // TODO: take care of new arp macro format - w->writeC(i->std.arpMacro.len); bool arpMacroMode=false; int arpMacroHowManyFixed=0; int realArpMacroLen=i->std.arpMacro.len; @@ -4996,13 +4994,25 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) { } } + if (realArpMacroLen>127) realArpMacroLen=127; + + w->writeC(realArpMacroLen); + if (arpMacroMode) { for (int j=0; jwriteI(i->std.arpMacro.val[j]); + if ((i->std.arpMacro.val[j]&0xc0000000)==0x40000000 || (i->std.arpMacro.val[j]&0xc0000000)==0x80000000) { + w->writeI(i->std.arpMacro.val[j]^0x40000000); + } else { + w->writeI(i->std.arpMacro.val[j]); + } } } else { for (int j=0; jwriteI(i->std.arpMacro.val[j]+12); + if ((i->std.arpMacro.val[j]&0xc0000000)==0x40000000 || (i->std.arpMacro.val[j]&0xc0000000)==0x80000000) { + w->writeI((i->std.arpMacro.val[j]^0x40000000)+12); + } else { + w->writeI(i->std.arpMacro.val[j]+12); + } } } if (realArpMacroLen>0) { diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index abb57edb..fe420bd5 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -1501,12 +1501,11 @@ bool DivInstrument::saveDMP(const char* path) { if (std.volMacro.len>0) w->writeC(std.volMacro.loop); } - w->writeC(std.arpMacro.len); bool arpMacroMode=false; int arpMacroHowManyFixed=0; int realArpMacroLen=std.arpMacro.len; - for (int i=0; i127) realArpMacroLen=127; + + w->writeC(realArpMacroLen); + if (arpMacroMode) { - for (int i=0; iwriteI(std.arpMacro.val[i]); + for (int j=0; jwriteI(std.arpMacro.val[j]^0x40000000); + } else { + w->writeI(std.arpMacro.val[j]); + } } } else { - for (int i=0; iwriteI(std.arpMacro.val[i]+12); + for (int j=0; jwriteI((std.arpMacro.val[j]^0x40000000)+12); + } else { + w->writeI(std.arpMacro.val[j]+12); + } } } if (realArpMacroLen>0) { From 92d552569276bb730d6c5b244d4b1a6a7a6d10aa Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 00:06:06 -0500 Subject: [PATCH 02/57] Game Boy: fix Synchronize --- src/engine/fileOps.cpp | 3 +++ src/engine/platform/gb.cpp | 4 ++++ src/engine/platform/gb.h | 1 + src/gui/sysConf.cpp | 5 +++++ 4 files changed, 13 insertions(+) diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 5033e3ec..c2888e34 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -206,6 +206,9 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.tuning=433.2; }*/ + // Game Boy arp+soundLen screwery + ds.systemFlags[0].set("enoughAlready",true); + logI("reading module data..."); if (ds.version>0x0c) { ds.subsong[0]->hilightA=reader.readC(); diff --git a/src/engine/platform/gb.cpp b/src/engine/platform/gb.cpp index 0c9ab7b3..8a4cabc4 100644 --- a/src/engine/platform/gb.cpp +++ b/src/engine/platform/gb.cpp @@ -313,6 +313,9 @@ void DivPlatformGB::tick(bool sysTick) { rWrite(16+i*5+3,(2048-chan[i].freq)&0xff); rWrite(16+i*5+4,(((2048-chan[i].freq)>>8)&7)|((chan[i].keyOn||chan[i].keyOff)?0x80:0x00)|((chan[i].soundLen<63)<<6)); } + if (enoughAlready) { // more compat garbage + rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(chan[i].soundLen&63))); + } if (chan[i].keyOn) chan[i].keyOn=false; if (chan[i].keyOff) chan[i].keyOff=false; chan[i].freqChanged=false; @@ -645,6 +648,7 @@ void DivPlatformGB::setFlags(const DivConfig& flags) { model=GB_MODEL_AGB; break; } + enoughAlready=flags.getBool("enoughAlready",false); } int DivPlatformGB::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { diff --git a/src/engine/platform/gb.h b/src/engine/platform/gb.h index d0a2c1f3..17a17da1 100644 --- a/src/engine/platform/gb.h +++ b/src/engine/platform/gb.h @@ -76,6 +76,7 @@ class DivPlatformGB: public DivDispatch { DivDispatchOscBuffer* oscBuf[4]; bool isMuted[4]; bool antiClickEnabled; + bool enoughAlready; unsigned char lastPan; DivWaveSynth ws; struct QueuedWrite { diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index ad9d4fec..a1dc3181 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -270,6 +270,7 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo case DIV_SYSTEM_GB: { int chipType=flags.getInt("chipType",0); bool noAntiClick=flags.getBool("noAntiClick",false); + bool enoughAlready=flags.getBool("enoughAlready",false); if (ImGui::Checkbox("Disable anti-click",&noAntiClick)) { altered=true; @@ -291,11 +292,15 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo chipType=3; altered=true; } + if (ImGui::Checkbox("Pretty please one more compat flag when I use arpeggio and my sound length",&enoughAlready)) { + altered=true; + } if (altered) { e->lockSave([&]() { flags.set("chipType",chipType); flags.set("noAntiClick",noAntiClick); + flags.set("enoughAlready",enoughAlready); }); } break; From 85f739497f08f8d4fb5b5ee0894e900fed28012f Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 01:02:01 -0500 Subject: [PATCH 03/57] fix bug when seeking with VT num>den --- src/engine/engine.cpp | 37 +++++++++++++++++++++++++++++++++++++ src/engine/engine.h | 3 +++ src/gui/debugWindow.cpp | 7 ++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index afcdcebc..4a166739 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1578,6 +1578,40 @@ String DivEngine::getWarnings() { return warnings; } +String DivEngine::getPlaybackDebugInfo() { + return fmt::sprintf( + "curOrder: %d\n" + "prevOrder: %d\n" + "curRow: %d\n" + "prevRow: %d\n" + "ticks: %d\n" + "subticks: %d\n" + "totalLoops: %d\n" + "lastLoopPos: %d\n" + "nextSpeed: %d\n" + "divider: %f\n" + "cycles: %d\n" + "clockDrift: %f\n" + "changeOrd: %d\n" + "changePos: %d\n" + "totalSeconds: %d\n" + "totalTicks: %d\n" + "totalTicksR: %d\n" + "totalCmds: %d\n" + "lastCmds: %d\n" + "cmdsPerSecond: %d\n" + "globalPitch: %d\n" + "extValue: %d\n" + "speed1: %d\n" + "speed2: %d\n" + "tempoAccum: %d\n" + "totalProcessed: %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 + ); +} + DivInstrument* DivEngine::getIns(int index, DivInstrumentType fallbackType) { if (index==-2 && tempIns!=NULL) { return tempIns; @@ -1701,6 +1735,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { skipping=true; memset(walked,0,8192); for (int i=0; isetSkipRegisterWrites(true); + logV("goal: %d goalRow: %d",goal,goalRow); while (playing && curOrdervirtualTempoN)/MAX(1,curSubSong->virtualTempoD))<1 && curRow>=goalRow) break; } for (int i=0; isetSkipRegisterWrites(false); if (goal>0 || goalRow>0) { @@ -1734,6 +1770,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { subticks=1; prevOrder=curOrder; prevRow=curRow; + tempoAccum=0; } skipping=false; cmdStream.clear(); diff --git a/src/engine/engine.h b/src/engine/engine.h index 3a03c427..648ebfb4 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -955,6 +955,9 @@ class DivEngine { // get warnings String getWarnings(); + // get debug info + String getPlaybackDebugInfo(); + // switch master bool switchMaster(); diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index a8f75008..9358c06f 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -98,7 +98,7 @@ void FurnaceGUI::drawDebug() { ImGui::Columns(); ImGui::TreePop(); } - if (ImGui::TreeNode("Playback Status")) { + if (ImGui::TreeNode("Channel Status")) { ImGui::Text("for best results set latency to minimum or use the Frame Advance button."); ImGui::Columns(e->getTotalChannelCount()); for (int i=0; igetTotalChannelCount(); i++) { @@ -160,6 +160,11 @@ void FurnaceGUI::drawDebug() { ImGui::Columns(); ImGui::TreePop(); } + if (ImGui::TreeNode("Playback Status")) { + String pdi=e->getPlaybackDebugInfo(); + ImGui::TextWrapped("%s",pdi.c_str()); + ImGui::TreePop(); + } if (ImGui::TreeNode("Sample Debug")) { for (int i=0; isong.sampleLen; i++) { DivSample* sample=e->getSample(i); From 0c79280aae24b3668602366b59a113c2ba7f8803 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 01:32:12 -0500 Subject: [PATCH 04/57] GUI: fix loop not updating samples on SNES --- src/engine/engine.cpp | 12 ++++++++++++ src/engine/engine.h | 3 +++ src/engine/playback.cpp | 2 +- src/gui/sampleEdit.cpp | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 4a166739..f3a300e7 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -985,6 +985,16 @@ int DivEngine::loadSampleROM(String path, ssize_t expectedSize, unsigned char*& return 0; } +unsigned int DivEngine::getSampleFormatMask() { + unsigned int formatMask=1U<<16; // 16-bit is always on + for (int i=0; isampleFormatMask; + } + return formatMask; +} + int DivEngine::loadSampleROMs() { if (yrw801ROM!=NULL) { delete[] yrw801ROM; @@ -1016,6 +1026,8 @@ void DivEngine::renderSamples() { sPreview.pos=0; sPreview.dir=false; + logD("rendering samples..."); + // step 0: make sample format mask unsigned int formatMask=1U<<16; // 16-bit is always on for (int i=0; iloopEnd=sample->samples; } updateSampleTex=true; + if (e->getSampleFormatMask()&(1U<renderSamplesP(); + } } if (ImGui::IsItemHovered() && sample->depth==DIV_SAMPLE_DEPTH_BRR) { ImGui::SetTooltip("changing the loop in a BRR sample may result in glitches!"); @@ -156,6 +159,9 @@ void FurnaceGUI::drawSampleEdit() { sample->loopStart=sample->loopEnd; } updateSampleTex=true; + if (e->getSampleFormatMask()&(1U<renderSamplesP(); + } } if (ImGui::IsItemActive()) { keepLoopAlive=true; @@ -175,6 +181,9 @@ void FurnaceGUI::drawSampleEdit() { sample->loopEnd=sample->samples; } updateSampleTex=true; + if (e->getSampleFormatMask()&(1U<renderSamplesP(); + } } if (ImGui::IsItemActive()) { keepLoopAlive=true; @@ -693,6 +702,9 @@ void FurnaceGUI::drawSampleEdit() { sample->loopEnd=sample->samples; } updateSampleTex=true; + if (e->getSampleFormatMask()&(1U<renderSamplesP(); + } } if (ImGui::IsItemHovered() && sample->depth==DIV_SAMPLE_DEPTH_BRR) { ImGui::SetTooltip("changing the loop in a BRR sample may result in glitches!"); @@ -729,6 +741,9 @@ void FurnaceGUI::drawSampleEdit() { if (sample->loopStart>sample->loopEnd) { sample->loopStart=sample->loopEnd; } + if (e->getSampleFormatMask()&(1U<renderSamplesP(); + } updateSampleTex=true; } if (ImGui::IsItemActive()) { @@ -748,6 +763,9 @@ void FurnaceGUI::drawSampleEdit() { if (sample->loopEnd>=(int)sample->samples) { sample->loopEnd=sample->samples; } + if (e->getSampleFormatMask()&(1U<renderSamplesP(); + } updateSampleTex=true; } if (ImGui::IsItemActive()) { From 492b1a8347150013378c9c6a0097afccde2114ec Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 01:36:37 -0500 Subject: [PATCH 05/57] GUI: implement clear recent file list option --- src/gui/gui.cpp | 15 +++++++++++++++ src/gui/gui.h | 1 + 2 files changed, 16 insertions(+) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 63a6fbff..72246f10 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -3291,6 +3291,11 @@ bool FurnaceGUI::loop() { } 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(); } @@ -4630,6 +4635,16 @@ bool FurnaceGUI::loop() { 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_GENERIC: if (ImGui::Button("OK")) { ImGui::CloseCurrentPopup(); diff --git a/src/gui/gui.h b/src/gui/gui.h index fa00d76c..31f09890 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -355,6 +355,7 @@ enum FurnaceGUIWarnings { GUI_WARN_CLEAR, GUI_WARN_SUBSONG_DEL, GUI_WARN_SYSTEM_DEL, + GUI_WARN_CLEAR_HISTORY, GUI_WARN_GENERIC }; From 76f1717b14d8cae4092fb3401f281daebced8c07 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 01:54:31 -0500 Subject: [PATCH 06/57] the cursor can't get tired --- src/engine/playback.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index fd8f87bc..00b1e31f 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -1143,6 +1143,8 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { break; } } + // under no circumstances shall the accumulator become this large + if (tempoAccum>1023) tempoAccum=1023; } // process stuff for (int i=0; i Date: Sun, 2 Oct 2022 02:00:31 -0500 Subject: [PATCH 07/57] call renderSamples() when moving samples --- src/engine/engine.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index f3a300e7..323463f9 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -3376,6 +3376,7 @@ bool DivEngine::moveWaveDown(int which) { song.wave[which]=song.wave[which+1]; song.wave[which+1]=prev; saveLock.unlock(); + renderSamples(); BUSY_END; return true; } @@ -3391,6 +3392,7 @@ bool DivEngine::moveSampleDown(int which) { song.sample[which]=song.sample[which+1]; song.sample[which+1]=prev; saveLock.unlock(); + renderSamples(); BUSY_END; return true; } From 8b9b452fbd79c19fb63cc5a93990b89738fdd51d Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 02:03:18 -0500 Subject: [PATCH 08/57] SoundUnit: fix echo resolution being ignored --- src/engine/platform/su.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/platform/su.cpp b/src/engine/platform/su.cpp index 66b91ef5..c00847fc 100644 --- a/src/engine/platform/su.cpp +++ b/src/engine/platform/su.cpp @@ -518,7 +518,7 @@ void DivPlatformSoundUnit::setFlags(const DivConfig& flags) { bool echoOn=flags.getBool("echo",false); initIlCtrl=3|(echoOn?4:0); initIlSize=((flags.getInt("echoDelay",0))&63)|(echoOn?0x40:0)|(flags.getBool("swapEcho",false)?0x80:0); - initFil1=flags.getInt("echoFeedback",0); + initFil1=flags.getInt("echoFeedback",0)|(flags.getInt("echoResolution",0)<<4); initEchoVol=flags.getInt("echoVol",0); sampleMemSize=flags.getInt("sampleMemSize",0); From 8de9e98a45ce525e336836dc2fbc982b959de375 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 02:08:33 -0500 Subject: [PATCH 09/57] I may be drunk --- src/engine/engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 323463f9..4f2c3a04 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -3351,6 +3351,7 @@ bool DivEngine::moveSampleUp(int which) { song.sample[which]=song.sample[which-1]; song.sample[which-1]=prev; saveLock.unlock(); + renderSamples(); BUSY_END; return true; } @@ -3376,7 +3377,6 @@ bool DivEngine::moveWaveDown(int which) { song.wave[which]=song.wave[which+1]; song.wave[which+1]=prev; saveLock.unlock(); - renderSamples(); BUSY_END; return true; } From cef1fa9d99dc754ae1dc53e63fe8e125c7d4902c Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 02:14:12 -0500 Subject: [PATCH 10/57] GUI: fix glitch when removing orders --- src/gui/doAction.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp index 31d0ff48..6b35f912 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -1439,6 +1439,12 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_ORDERS_REMOVE: prepareUndo(GUI_UNDO_CHANGE_ORDER); e->deleteOrder(); + if (curOrder>=e->curSubSong->ordersLen) { + curOrder=e->curSubSong->ordersLen-1; + oldOrder=curOrder; + oldOrder1=curOrder; + e->setOrder(curOrder); + } makeUndo(GUI_UNDO_CHANGE_ORDER); break; case GUI_ACTION_ORDERS_MOVE_UP: From 19cd491e5fa415d0a3ae4150eae90055bbe96764 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 02:30:22 -0500 Subject: [PATCH 11/57] MSM5232: effects --- src/engine/platform/msm5232.cpp | 12 ++++++++++++ src/engine/sysDef.cpp | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/engine/platform/msm5232.cpp b/src/engine/platform/msm5232.cpp index f311b113..462ecc76 100644 --- a/src/engine/platform/msm5232.cpp +++ b/src/engine/platform/msm5232.cpp @@ -229,10 +229,22 @@ int DivPlatformMSM5232::dispatch(DivCommand c) { } break; } + case DIV_CMD_WAVE: + groupControl[c.chan>>2]=c.value&0x1f; + updateGroup[c.chan>>2]=true; + break; case DIV_CMD_STD_NOISE_MODE: chan[c.chan].noise=c.value; chan[c.chan].freqChanged=true; break; + case DIV_CMD_FM_AR: + groupAR[c.chan>>2]=attackMap[c.value&7]; + updateGroupAR[c.chan>>2]=true; + break; + case DIV_CMD_FM_DR: + groupDR[c.chan>>2]=decayMap[c.value&15]; + updateGroupDR[c.chan>>2]=true; + break; case DIV_CMD_LEGATO: chan[c.chan].baseFreq=NOTE_LINEAR(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); chan[c.chan].freqChanged=true; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index faf75757..cb2a3da4 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1610,7 +1610,15 @@ void DivEngine::registerSystems() { {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8"}, {"CH1", "CH2", "CH3", "CH4", "CH5", "CH6", "CH7", "CH8"}, {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, - {DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232} + {DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232, DIV_INS_MSM5232}, + {}, + {}, + { + {0x10, {DIV_CMD_WAVE, "10xy: Set group control (x: sustain; y: part toggle bitmask)"}}, + {0x11, {DIV_CMD_STD_NOISE_MODE, "11xx: Set noise mode"}}, + {0x12, {DIV_CMD_FM_AR, "12xx: Set group attack (0 to 5)"}}, + {0x13, {DIV_CMD_FM_DR, "13xx: Set group decay (0 to 11)"}} + } ); sysDefs[DIV_SYSTEM_YM2612_FRAC]=new DivSysDef( From 255547188d1c466d50fa166778a8cf8d84ee489f Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 03:02:28 -0500 Subject: [PATCH 12/57] here's a to-do list for pre2 --- TODO.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..ca2ca143 --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +# to-do for 0.6pre2 + +- POKEY +- Pokémon Mini +- Virtual Boy +- T6W28 +- (maybe) YM2612 CSM (no DualPCM) +- port presets to new format +- bug fixes +- (maybe) ExtCh FM macros? +- (maybe) advanced linear arpeggio? (run arp+slide simultaneously) From 54183ce4a22a914d4cdf6fd2366b0733cb851783 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 16:12:02 -0500 Subject: [PATCH 13/57] GUI fail error report --- src/gui/gui.cpp | 8 ++++++-- src/gui/gui.h | 1 + src/main.cpp | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 72246f10..2adbf89b 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1993,6 +1993,10 @@ void FurnaceGUI::showError(String 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) @@ -5017,7 +5021,7 @@ bool FurnaceGUI::init() { sdlWin=SDL_CreateWindow("Furnace",scrX,scrY,scrW*dpiScale,scrH*dpiScale,SDL_WINDOW_RESIZABLE|SDL_WINDOW_ALLOW_HIGHDPI|(scrMax?SDL_WINDOW_MAXIMIZED:0)|(fullScreen?SDL_WINDOW_FULLSCREEN_DESKTOP:0)); if (sdlWin==NULL) { - logE("could not open window! %s",SDL_GetError()); + lastError=fmt::sprintf("could not open window! %s",SDL_GetError()); return false; } @@ -5072,7 +5076,7 @@ bool FurnaceGUI::init() { sdlRend=SDL_CreateRenderer(sdlWin,-1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC|SDL_RENDERER_TARGETTEXTURE); if (sdlRend==NULL) { - logE("could not init renderer! %s",SDL_GetError()); + lastError=fmt::sprintf("could not init renderer! %s",SDL_GetError()); return false; } diff --git a/src/gui/gui.h b/src/gui/gui.h index 31f09890..203fd20e 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1802,6 +1802,7 @@ class FurnaceGUI { public: void showWarning(String what, FurnaceGUIWarnings type); void showError(String what); + String getLastError(); const char* noteNameNormal(short note, short octave); const char* noteName(short note, short octave); bool decodeNote(const char* what, short& note, short& octave); diff --git a/src/main.cpp b/src/main.cpp index 79229621..e59d6e03 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -549,7 +549,7 @@ int main(int argc, char** argv) { #ifdef HAVE_GUI g.bindEngine(&e); if (!g.init()) { - reportError("error while starting GUI!"); + reportError(g.getLastError()); return 1; } From 02e87236cef74dadb8defd3cae4b31a9788bce14 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 2 Oct 2022 19:12:31 -0500 Subject: [PATCH 14/57] credits --- src/gui/about.cpp | 2 ++ src/main.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 7e5dfdf2..d14cd4d1 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -129,11 +129,13 @@ const char* aboutLine[]={ "MAME SAA1099 by Juergen Buchmueller and Manuel Abadia", "MAME Namco WSG by Nicola Salmoria and Aaron Giles", "MAME RF5C68 core by Olivier Galibert and Aaron Giles", + "MAME MSM5232 core by Jarek Burczynski and Hiromitsu Shioya", "MAME MSM6258 core by Barry Rodewald", "MAME YMZ280B core by Aaron Giles", "SAASound by Dave Hooper and Simon Owen", "SameBoy by Lior Halphon", "Mednafen PCE and WonderSwan audio cores", + "SNES DSP core by Blargg", "puNES (NES, MMC5 and FDS) by FHorse", "NSFPlay (NES and FDS) by Brad Smith and Brezza", "reSID by Dag Lem", diff --git a/src/main.cpp b/src/main.cpp index e59d6e03..aadc8ded 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -171,6 +171,7 @@ TAParamResult pVersion(String) { printf("- MAME SAA1099 emulation core by Juergen Buchmueller and Manuel Abadia (BSD 3-clause)\n"); printf("- MAME Namco WSG by Nicola Salmoria and Aaron Giles (BSD 3-clause)\n"); printf("- MAME RF5C68 core by Olivier Galibert and Aaron Giles (BSD 3-clause)\n"); + printf("- MAME MSM5232 core by Jarek Burczynski and Hiromitsu Shioya (GPLv2)\n"); printf("- MAME MSM6258 core by Barry Rodewald (BSD 3-clause)\n"); printf("- MAME YMZ280B core by Aaron Giles (BSD 3-clause)\n"); printf("- QSound core by superctr (BSD 3-clause)\n"); @@ -179,6 +180,7 @@ TAParamResult pVersion(String) { printf("- SAASound by Dave Hooper and Simon Owen (BSD 3-clause)\n"); printf("- SameBoy by Lior Halphon (MIT)\n"); printf("- Mednafen PCE and WonderSwan by Mednafen Team (GPLv2)\n"); + printf("- SNES DSP core by Blargg (LGPLv2.1)\n"); printf("- puNES by FHorse (GPLv2)\n"); printf("- NSFPlay by Brad Smith and Brezza (unknown open-source license)\n"); printf("- reSID by Dag Lem (GPLv2)\n"); From 3761383f8dfd9669ff51f63571cd7fd69d32b7db Mon Sep 17 00:00:00 2001 From: cam900 Date: Mon, 3 Oct 2022 11:02:34 +0900 Subject: [PATCH 15/57] Fix 1701 command for AYPCM --- src/engine/platform/ay.cpp | 4 +++- src/engine/platform/ay8930.cpp | 4 +++- src/engine/sysDef.cpp | 8 ++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index b01d6a9f..eeeb5485 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -441,12 +441,14 @@ int DivPlatformAY8910::dispatch(DivCommand c) { } chan[c.chan].dac.pos=0; chan[c.chan].dac.period=0; - chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*2048; + chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate; if (dumpWrites) { rWrite(0x08+c.chan,0); addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate); } chan[c.chan].dac.furnaceDAC=false; + chan[c.chan].active=true; + //chan[c.chan].keyOn=true; } chan[c.chan].curPSGMode.dac=chan[c.chan].nextPSGMode.dac; break; diff --git a/src/engine/platform/ay8930.cpp b/src/engine/platform/ay8930.cpp index 821fa4fe..3d658e03 100644 --- a/src/engine/platform/ay8930.cpp +++ b/src/engine/platform/ay8930.cpp @@ -442,12 +442,14 @@ int DivPlatformAY8930::dispatch(DivCommand c) { } chan[c.chan].dac.pos=0; chan[c.chan].dac.period=0; - chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*4096; + chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate; if (dumpWrites) { rWrite(0x08+c.chan,0); addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate); } chan[c.chan].dac.furnaceDAC=false; + chan[c.chan].active=true; + //chan[c.chan].keyOn=true; } chan[c.chan].curPSGMode.dac=chan[c.chan].nextPSGMode.dac; break; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index cb2a3da4..e97a2b89 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -422,6 +422,10 @@ void DivEngine::registerSystems() { // Common effect handler maps + EffectHandlerMap ayPreEffectHandlerMap={ + {0x17, {DIV_CMD_SAMPLE_MODE, "17xx: Toggle PCM mode"}}, + }; + EffectHandlerMap ayPostEffectHandlerMap={ {0x20, {DIV_CMD_STD_NOISE_MODE, "20xx: Set channel mode (bit 0: square; bit 1: noise; bit 2: envelope)"}}, {0x21, {DIV_CMD_STD_NOISE_FREQ, "21xx: Set noise frequency (0 to 1F)"}}, @@ -771,7 +775,7 @@ void DivEngine::registerSystems() { {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, {DIV_INS_AY, DIV_INS_AY, DIV_INS_AY}, {}, - {}, + ayPreEffectHandlerMap, ayPostEffectHandlerMap ); @@ -851,7 +855,7 @@ void DivEngine::registerSystems() { {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, {DIV_INS_AY8930, DIV_INS_AY8930, DIV_INS_AY8930}, {}, - {}, + ayPreEffectHandlerMap, ay8930PostEffectHandlerMap ); From b23fdb4b3dcab70456a50a1a30a496e6c52728ee Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 01:23:43 -0500 Subject: [PATCH 16/57] Revert "Fix 1701 command for AYPCM" This reverts commit 3761383f8dfd9669ff51f63571cd7fd69d32b7db. --- src/engine/platform/ay.cpp | 4 +--- src/engine/platform/ay8930.cpp | 4 +--- src/engine/sysDef.cpp | 8 ++------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index eeeb5485..b01d6a9f 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -441,14 +441,12 @@ int DivPlatformAY8910::dispatch(DivCommand c) { } chan[c.chan].dac.pos=0; chan[c.chan].dac.period=0; - chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate; + chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*2048; if (dumpWrites) { rWrite(0x08+c.chan,0); addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate); } chan[c.chan].dac.furnaceDAC=false; - chan[c.chan].active=true; - //chan[c.chan].keyOn=true; } chan[c.chan].curPSGMode.dac=chan[c.chan].nextPSGMode.dac; break; diff --git a/src/engine/platform/ay8930.cpp b/src/engine/platform/ay8930.cpp index 3d658e03..821fa4fe 100644 --- a/src/engine/platform/ay8930.cpp +++ b/src/engine/platform/ay8930.cpp @@ -442,14 +442,12 @@ int DivPlatformAY8930::dispatch(DivCommand c) { } chan[c.chan].dac.pos=0; chan[c.chan].dac.period=0; - chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate; + chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*4096; if (dumpWrites) { rWrite(0x08+c.chan,0); addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate); } chan[c.chan].dac.furnaceDAC=false; - chan[c.chan].active=true; - //chan[c.chan].keyOn=true; } chan[c.chan].curPSGMode.dac=chan[c.chan].nextPSGMode.dac; break; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index e97a2b89..cb2a3da4 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -422,10 +422,6 @@ void DivEngine::registerSystems() { // Common effect handler maps - EffectHandlerMap ayPreEffectHandlerMap={ - {0x17, {DIV_CMD_SAMPLE_MODE, "17xx: Toggle PCM mode"}}, - }; - EffectHandlerMap ayPostEffectHandlerMap={ {0x20, {DIV_CMD_STD_NOISE_MODE, "20xx: Set channel mode (bit 0: square; bit 1: noise; bit 2: envelope)"}}, {0x21, {DIV_CMD_STD_NOISE_FREQ, "21xx: Set noise frequency (0 to 1F)"}}, @@ -775,7 +771,7 @@ void DivEngine::registerSystems() { {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, {DIV_INS_AY, DIV_INS_AY, DIV_INS_AY}, {}, - ayPreEffectHandlerMap, + {}, ayPostEffectHandlerMap ); @@ -855,7 +851,7 @@ void DivEngine::registerSystems() { {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, {DIV_INS_AY8930, DIV_INS_AY8930, DIV_INS_AY8930}, {}, - ayPreEffectHandlerMap, + {}, ay8930PostEffectHandlerMap ); From 140997956137140521ed13b224777e6089b3a712 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 12:16:50 -0500 Subject: [PATCH 17/57] fix instrument movement not relaying to sub-songs properly when they have different pattern lengths --- src/engine/engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 4f2c3a04..ea5ee5d0 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -3303,7 +3303,7 @@ void DivEngine::exchangeIns(int one, int two) { for (size_t j=0; jpat[i].data[k]==NULL) continue; - for (int l=0; lpatLen; l++) { + for (int l=0; lpatLen; l++) { if (song.subsong[j]->pat[i].data[k]->data[l][2]==one) { song.subsong[j]->pat[i].data[k]->data[l][2]=two; } else if (song.subsong[j]->pat[i].data[k]->data[l][2]==two) { From 4eed3dbc4d340c703ea00f9c414fc24a39c6879e Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 18:40:07 -0500 Subject: [PATCH 18/57] SNES: gain macro --- src/engine/platform/snes.cpp | 23 +++++++++++++++++++++++ src/gui/insEdit.cpp | 25 +++++++++++++++++++------ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index 0edc88aa..fb278f2a 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -161,6 +161,29 @@ void DivPlatformSNES::tick(bool sysTick) { if (chan[i].std.vol.had || chan[i].std.panL.had || chan[i].std.panR.had || hasInverted) { writeOutVol(i); } + if (chan[i].std.ex2.had) { + if (chan[i].std.ex2.val&0x80) { + switch (chan[i].std.ex2.val&0x60) { + case 0x00: + chan[i].state.gainMode=DivInstrumentSNES::GAIN_MODE_DEC_LINEAR; + break; + case 0x20: + chan[i].state.gainMode=DivInstrumentSNES::GAIN_MODE_DEC_LOG; + break; + case 0x40: + chan[i].state.gainMode=DivInstrumentSNES::GAIN_MODE_INC_LINEAR; + break; + case 0x60: + chan[i].state.gainMode=DivInstrumentSNES::GAIN_MODE_INC_INVLOG; + break; + } + chan[i].state.gain=chan[i].std.ex2.val&31; + } else { + chan[i].state.gainMode=DivInstrumentSNES::GAIN_MODE_DIRECT; + chan[i].state.gain=chan[i].std.ex2.val&127; + } + writeEnv(i); + } if (chan[i].setPos) { // force keyon chan[i].keyOn=true; diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index a67856bf..399ff526 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -342,7 +342,7 @@ String macroHoverNote(int id, float val, void* u) { } String macroHover(int id, float val, void* u) { - return fmt::sprintf("%d: %d",id,val); + return fmt::sprintf("%d: %d",id,(int)val); } String macroHoverLoop(int id, float val, void* u) { @@ -356,6 +356,22 @@ String macroHoverBit30(int id, float val, void* u) { return "Relative"; } +String macroHoverGain(int id, float val, void* u) { + if (val>=224.0f) { + return fmt::sprintf("%d: +%d (exponential)",id,(int)(val-224)); + } + if (val>=192.0f) { + return fmt::sprintf("%d: +%d (linear)",id,(int)(val-192)); + } + if (val>=160.0f) { + return fmt::sprintf("%d: -%d (exponential)",id,(int)(val-160)); + } + if (val>=128.0f) { + return fmt::sprintf("%d: -%d (linear)",id,(int)(val-128)); + } + return fmt::sprintf("%d: %d (direct)",id,(int)val); +} + String macroHoverES5506FilterMode(int id, float val, void* u) { String mode="???"; switch (((int)val)&3) { @@ -4528,7 +4544,7 @@ void FurnaceGUI::drawInsEdit() { } if (ins->type==DIV_INS_SNES) { ex1Max=5; - ex2Max=5; + ex2Max=255; } if (ins->type==DIV_INS_MSM5232) { ex1Max=5; @@ -4704,7 +4720,7 @@ void FurnaceGUI::drawInsEdit() { } else if (ins->type==DIV_INS_QSOUND) { macroList.push_back(FurnaceGUIMacroDesc("Echo Length",&ins->std.ex2Macro,0,ex2Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } else if (ins->type==DIV_INS_SNES) { - macroList.push_back(FurnaceGUIMacroDesc("Gain Mode",&ins->std.ex2Macro,0,ex2Max,64,uiColors[GUI_COLOR_MACRO_VOLUME],false,NULL,NULL,false,snesGainModes)); + macroList.push_back(FurnaceGUIMacroDesc("Gain",&ins->std.ex2Macro,0,ex2Max,256,uiColors[GUI_COLOR_MACRO_VOLUME],false,NULL,macroHoverGain,false)); } else if (ins->type==DIV_INS_MSM5232) { macroList.push_back(FurnaceGUIMacroDesc("Group Decay",&ins->std.ex2Macro,0,ex2Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } else { @@ -4746,9 +4762,6 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Envelope mode",&ins->std.ex8Macro,0,2,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,es5506EnvelopeModes)); macroList.push_back(FurnaceGUIMacroDesc("Control",&ins->std.algMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,es5506ControlModes)); } - if (ins->type==DIV_INS_SNES) { - macroList.push_back(FurnaceGUIMacroDesc("Gain Rate",&ins->std.ex3Macro,0,127,160,uiColors[GUI_COLOR_MACRO_VOLUME])); - } if (ins->type==DIV_INS_MSM5232) { macroList.push_back(FurnaceGUIMacroDesc("Noise",&ins->std.ex3Macro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); } From 992b8f4b40e0206350118335863012dcf96305e1 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 18:56:18 -0500 Subject: [PATCH 19/57] GUI: fix blurry text in ins list when non-AA font --- src/gui/dataList.cpp | 106 ++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp index defb68d0..7199e513 100644 --- a/src/gui/dataList.cpp +++ b/src/gui/dataList.cpp @@ -199,7 +199,7 @@ void FurnaceGUI::drawInsList(bool asChild) { int curRow=0; for (int i=-1; i<(int)e->song.ins.size(); i++) { ImGui::PushID(i); - String name=ICON_FA_CIRCLE_O " - None -"; + String name=ICON_FA_CIRCLE_O; const char* insType="Bug!"; if (i>=0) { DivInstrument* ins=e->song.ins[i]; @@ -208,183 +208,183 @@ void FurnaceGUI::drawInsList(bool asChild) { switch (ins->type) { case DIV_INS_FM: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FM]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_AREA_CHART "##_INS%d",i); break; case DIV_INS_STD: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_STD]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_GB: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_GB]); - name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_GAMEPAD "##_INS%d",i); break; case DIV_INS_C64: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_C64]); - name=fmt::sprintf(ICON_FA_KEYBOARD_O " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_KEYBOARD_O "##_INS%d",i); break; case DIV_INS_AMIGA: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AMIGA]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_PCE: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_PCE]); - name=fmt::sprintf(ICON_FA_ID_BADGE " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_ID_BADGE "##_INS%d",i); break; case DIV_INS_AY: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AY]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_AY8930: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_AY8930]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_TIA: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_TIA]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_SAA1099: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SAA1099]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_VIC: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VIC]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_PET: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_PET]); - name=fmt::sprintf(ICON_FA_SQUARE " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_SQUARE "##_INS%d",i); break; case DIV_INS_VRC6: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VRC6]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_VRC6_SAW: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VRC6_SAW]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_OPLL: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPLL]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_AREA_CHART "##_INS%d",i); break; case DIV_INS_OPL: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPL]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_AREA_CHART "##_INS%d",i); break; case DIV_INS_FDS: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_FDS]); - name=fmt::sprintf(ICON_FA_FLOPPY_O " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_FLOPPY_O "##_INS%d",i); break; case DIV_INS_VBOY: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VBOY]); - name=fmt::sprintf(ICON_FA_BINOCULARS " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BINOCULARS "##_INS%d",i); break; case DIV_INS_N163: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_N163]); - name=fmt::sprintf(ICON_FA_CALCULATOR " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_CALCULATOR "##_INS%d",i); break; case DIV_INS_SCC: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SCC]); - name=fmt::sprintf(ICON_FA_CALCULATOR " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_CALCULATOR "##_INS%d",i); break; case DIV_INS_OPZ: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPZ]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_AREA_CHART "##_INS%d",i); break; case DIV_INS_POKEY: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_POKEY]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_BEEPER: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_BEEPER]); - name=fmt::sprintf(ICON_FA_SQUARE " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_SQUARE "##_INS%d",i); break; case DIV_INS_SWAN: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SWAN]); - name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_GAMEPAD "##_INS%d",i); break; case DIV_INS_MIKEY: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MIKEY]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_VERA: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_VERA]); - name=fmt::sprintf(ICON_FA_KEYBOARD_O " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_KEYBOARD_O "##_INS%d",i); break; case DIV_INS_X1_010: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_X1_010]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; case DIV_INS_ES5506: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_ES5506]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_MULTIPCM: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MULTIPCM]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_SNES: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SNES]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_SU: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SU]); - name=fmt::sprintf(ICON_FA_MICROCHIP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_MICROCHIP "##_INS%d",i); break; case DIV_INS_NAMCO: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_NAMCO]); - name=fmt::sprintf(ICON_FA_PIE_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_PIE_CHART "##_INS%d",i); break; case DIV_INS_OPL_DRUMS: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPL_DRUMS]); - name=fmt::sprintf(ICON_FA_COFFEE " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_COFFEE "##_INS%d",i); break; case DIV_INS_OPM: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_OPM]); - name=fmt::sprintf(ICON_FA_AREA_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_AREA_CHART "##_INS%d",i); break; case DIV_INS_NES: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_NES]); - name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_GAMEPAD "##_INS%d",i); break; case DIV_INS_MSM6258: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MSM6258]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_MSM6295: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MSM6295]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_ADPCMA: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_ADPCMA]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_ADPCMB: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_ADPCMB]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_SEGAPCM: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SEGAPCM]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_QSOUND: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_QSOUND]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_YMZ280B: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_YMZ280B]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_RF5C68: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_RF5C68]); - name=fmt::sprintf(ICON_FA_VOLUME_UP " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_VOLUME_UP "##_INS%d",i); break; case DIV_INS_MSM5232: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MSM5232]); - name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_BAR_CHART "##_INS%d",i); break; default: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_UNKNOWN]); - name=fmt::sprintf(ICON_FA_QUESTION " %.2X: %s##_INS%d",i,ins->name,i); + name=fmt::sprintf(ICON_FA_QUESTION "##_INS%d",i); break; } } else { @@ -400,7 +400,6 @@ void FurnaceGUI::drawInsList(bool asChild) { curIns=i; wavePreviewInit=true; } - ImGui::PopStyleColor(); if (wantScrollList && curIns==i) ImGui::SetScrollHereY(); if (settings.insFocusesPattern && patternOpen && ImGui::IsItemActivated()) { nextWindow=GUI_WINDOW_PATTERN; @@ -408,7 +407,9 @@ void FurnaceGUI::drawInsList(bool asChild) { wavePreviewInit=true; } if (ImGui::IsItemHovered() && i>=0) { + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); ImGui::SetTooltip("%s",insType); + ImGui::PopStyleColor(); if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { insEditOpen=true; nextWindow=GUI_WINDOW_INS_EDIT; @@ -417,6 +418,7 @@ void FurnaceGUI::drawInsList(bool asChild) { if (i>=0) { if (ImGui::BeginPopupContextItem("InsRightMenu")) { curIns=i; + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); if (ImGui::MenuItem("replace...")) { doAction((curIns>=0 && curIns<(int)e->song.ins.size())?GUI_ACTION_INS_LIST_OPEN_REPLACE:GUI_ACTION_INS_LIST_OPEN); } @@ -429,9 +431,19 @@ void FurnaceGUI::drawInsList(bool asChild) { if (ImGui::MenuItem("delete")) { doAction(GUI_ACTION_INS_LIST_DELETE); } + ImGui::PopStyleColor(); ImGui::EndPopup(); } } + if (i>=0) { + DivInstrument* ins=e->song.ins[i]; + ImGui::SameLine(); + ImGui::Text("%.2X: %s",i,ins->name.c_str()); + } else { + ImGui::SameLine(); + ImGui::Text("- None -"); + } + ImGui::PopStyleColor(); if (settings.horizontalDataView) { if (++curRow>=availableRows) curRow=0; } From c63ff7320ed2b70e5c589ff3359d9cdc23816192 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 19:08:32 -0500 Subject: [PATCH 20/57] GUI: improve "no instrument seleted" prompt --- src/gui/insEdit.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 399ff526..a4384765 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -1689,7 +1689,41 @@ void FurnaceGUI::drawInsEdit() { } if (ImGui::Begin("Instrument Editor",&insEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking))) { if (curIns<0 || curIns>=(int)e->song.ins.size()) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()*2.0f)*0.5f); + CENTER_TEXT("no instrument selected"); ImGui::Text("no instrument selected"); + if (ImGui::BeginTable("noAssetCenter",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.5f); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.5f); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + + if (e->song.ins.size()>0) { + if (ImGui::BeginCombo("##InsSelect","select one...")) { + String name; + for (size_t i=0; isong.ins.size(); i++) { + name=fmt::sprintf("%.2X: %s##_INSS%d",i,e->song.ins[i]->name,i); + if (ImGui::Selectable(name.c_str(),curIns==(int)i)) { + curIns=i; + wavePreviewInit=true; + } + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::TextUnformatted("or"); + ImGui::SameLine(); + } + if (ImGui::Button("Create New")) { + doAction(GUI_ACTION_INS_LIST_ADD); + } + + ImGui::TableNextColumn(); + ImGui::EndTable(); + } } else { DivInstrument* ins=e->song.ins[curIns]; if (settings.insEditColorize) { From 70e0b4ab529b8423fc081fd5fb840912654320fc Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 19:22:24 -0500 Subject: [PATCH 21/57] GUI: add option to center pattern - INCOMPLETE if you right click on the left area it doesn't work --- src/gui/gui.cpp | 1 + src/gui/gui.h | 3 +++ src/gui/pattern.cpp | 8 ++++++++ src/gui/settings.cpp | 8 ++++++++ 4 files changed, 20 insertions(+) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 2adbf89b..f9e1237f 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -5420,6 +5420,7 @@ FurnaceGUI::FurnaceGUI(): curWindow(GUI_WINDOW_NOTHING), nextWindow(GUI_WINDOW_NOTHING), curWindowLast(GUI_WINDOW_NOTHING), + lastPatternWidth(0.0f), nextDesc(NULL), latchNote(-1), latchIns(-2), diff --git a/src/gui/gui.h b/src/gui/gui.h index 203fd20e..bb23ccee 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1209,6 +1209,7 @@ class FurnaceGUI { int midiOutClock; int midiOutMode; int maxRecentFile; + int centerPattern; unsigned int maxUndoSteps; String mainFontPath; String patFontPath; @@ -1334,6 +1335,7 @@ class FurnaceGUI { midiOutClock(0), midiOutMode(1), maxRecentFile(10), + centerPattern(0), maxUndoSteps(100), mainFontPath(""), patFontPath(""), @@ -1373,6 +1375,7 @@ class FurnaceGUI { float peak[2]; float patChanX[DIV_MAX_CHANS+1]; float patChanSlideY[DIV_MAX_CHANS+1]; + float lastPatternWidth; const int* nextDesc; String nextDescName; diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index ae194925..fb65cee8 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -403,6 +403,12 @@ void FurnaceGUI::drawPattern() { ImGui::PushStyleColor(ImGuiCol_Header,uiColors[GUI_COLOR_PATTERN_SELECTION]); ImGui::PushStyleColor(ImGuiCol_HeaderHovered,uiColors[GUI_COLOR_PATTERN_SELECTION_HOVER]); ImGui::PushStyleColor(ImGuiCol_HeaderActive,uiColors[GUI_COLOR_PATTERN_SELECTION_ACTIVE]); + if (settings.centerPattern) { + float centerOff=(ImGui::GetContentRegionAvail().x-lastPatternWidth)*0.5; + if (centerOff>0.0f) { + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+centerOff); + } + } if (ImGui::BeginTable("PatternView",displayChans+2,ImGuiTableFlags_BordersInnerV|ImGuiTableFlags_ScrollX|ImGuiTableFlags_ScrollY|ImGuiTableFlags_NoPadInnerX|ImGuiTableFlags_NoBordersInFrozenArea)) { ImGui::TableSetupColumn("pos",ImGuiTableColumnFlags_WidthFixed); char chanID[2048]; @@ -427,6 +433,7 @@ void FurnaceGUI::drawPattern() { } ImGui::TableNextRow(); ImGui::TableNextColumn(); + float lpwStart=ImGui::GetCursorPosX(); if (ImGui::Selectable((extraChannelButtons==2)?" --##ExtraChannelButtons":" ++##ExtraChannelButtons",false,ImGuiSelectableFlags_NoPadWithHalfSpacing,ImVec2(0.0f,lineHeight+1.0f*dpiScale))) { if (++extraChannelButtons>2) extraChannelButtons=0; } @@ -839,6 +846,7 @@ void FurnaceGUI::drawPattern() { } } ImGui::TableNextColumn(); + lastPatternWidth=ImGui::GetCursorPosX()-lpwStart+ImGui::GetStyle().ScrollbarSize; if (e->hasExtValue()) { ImGui::TextColored(uiColors[GUI_COLOR_EE_VALUE]," %.2X",e->getExtValue()); } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index e4c40cae..f16f2588 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -1443,6 +1443,11 @@ void FurnaceGUI::drawSettings() { settings.germanNotation=germanNotationB; } + bool centerPatternB=settings.centerPattern; + if (ImGui::Checkbox("Center pattern view",¢erPatternB)) { + settings.centerPattern=centerPatternB; + } + bool unsignedDetuneB=settings.unsignedDetune; if (ImGui::Checkbox("Unsigned FM detune values",&unsignedDetuneB)) { settings.unsignedDetune=unsignedDetuneB; @@ -2363,6 +2368,7 @@ void FurnaceGUI::syncSettings() { settings.maxRecentFile=e->getConfInt("maxRecentFile",10); settings.midiOutClock=e->getConfInt("midiOutClock",0); settings.midiOutMode=e->getConfInt("midiOutMode",1); + settings.centerPattern=e->getConfInt("centerPattern",0); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.patFontSize,2,96); @@ -2466,6 +2472,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.maxRecentFile,0,30); clampSetting(settings.midiOutClock,0,1); clampSetting(settings.midiOutMode,0,2); + clampSetting(settings.centerPattern,0,1); String initialSys2=e->getConfString("initialSys2",""); if (initialSys2.empty()) { @@ -2630,6 +2637,7 @@ void FurnaceGUI::commitSettings() { e->setConf("maxRecentFile",settings.maxRecentFile); e->setConf("midiOutClock",settings.midiOutClock); e->setConf("midiOutMode",settings.midiOutMode); + e->setConf("centerPattern",settings.centerPattern); // colors for (int i=0; i Date: Tue, 4 Oct 2022 11:17:03 +0900 Subject: [PATCH 22/57] Fix 1701 command for X1-010 --- src/engine/platform/x1_010.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/engine/platform/x1_010.cpp b/src/engine/platform/x1_010.cpp index 29e5edbd..b7c27b2d 100644 --- a/src/engine/platform/x1_010.cpp +++ b/src/engine/platform/x1_010.cpp @@ -240,15 +240,15 @@ u8 DivPlatformX1_010::read_byte(u32 address) { } double DivPlatformX1_010::NoteX1_010(int ch, int note) { - if (chan[ch].pcm) { // PCM note - double off=8192.0; + if (chan[ch].furnacePCM) { // PCM note + double off=4194304.0; int sample=chan[ch].sample; if (sample>=0 && samplesong.sampleLen) { DivSample* s=parent->getSample(sample); if (s->centerRate<1) { - off=8192.0; + off=4194304.0; } else { - off=8192.0*(s->centerRate/8363.0); + off=4194304.0*(s->centerRate/8363.0); } } return parent->calcBaseFreq(chipClock,off,note,false); @@ -463,20 +463,21 @@ void DivPlatformX1_010::tick(bool sysTick) { chan[i].envChanged=false; } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { - double off=8192.0; - if (chan[i].pcm) { + if (chan[i].furnacePCM) { + double off=4194304.0; int sample=chan[i].sample; if (sample>=0 && samplesong.sampleLen) { DivSample* s=parent->getSample(sample); if (s->centerRate<1) { - off=8192.0; + off=4194304.0; } else { - off=8192.0*(s->centerRate/8363.0); + off=4194304.0*(s->centerRate/8363.0); } } } - chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,chan[i].pcm?off:CHIP_FREQBASE); + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE); if (chan[i].pcm) { + chan[i].freq>>=8; if (chan[i].freq<1) chan[i].freq=1; if (chan[i].freq>255) chan[i].freq=255; chWrite(i,2,chan[i].freq&0xff); @@ -591,7 +592,7 @@ int DivPlatformX1_010::dispatch(DivCommand c) { int end=(sampleOffX1[chan[c.chan].sample]+s->length8+0xfff)&~0xfff; // padded chWrite(c.chan,5,(0x100-(end>>12))&0xff); } - chan[c.chan].baseFreq=(((unsigned int)s->rate)<<4)/(chipClock/512); + chan[c.chan].baseFreq=(((unsigned int)s->rate)<<14)/(chipClock/384); chan[c.chan].freqChanged=true; } } else if (c.value!=DIV_NOTE_NULL) { From 204af474214cb5fddf2ad70e5ca669faddc90fec Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 3 Oct 2022 23:52:52 -0500 Subject: [PATCH 23/57] Revert "Fix 1701 command for X1-010" This reverts commit ece4eb9a571d3e478c85026eb7fc3e33e055dc89. --- src/engine/platform/x1_010.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/engine/platform/x1_010.cpp b/src/engine/platform/x1_010.cpp index b7c27b2d..29e5edbd 100644 --- a/src/engine/platform/x1_010.cpp +++ b/src/engine/platform/x1_010.cpp @@ -240,15 +240,15 @@ u8 DivPlatformX1_010::read_byte(u32 address) { } double DivPlatformX1_010::NoteX1_010(int ch, int note) { - if (chan[ch].furnacePCM) { // PCM note - double off=4194304.0; + if (chan[ch].pcm) { // PCM note + double off=8192.0; int sample=chan[ch].sample; if (sample>=0 && samplesong.sampleLen) { DivSample* s=parent->getSample(sample); if (s->centerRate<1) { - off=4194304.0; + off=8192.0; } else { - off=4194304.0*(s->centerRate/8363.0); + off=8192.0*(s->centerRate/8363.0); } } return parent->calcBaseFreq(chipClock,off,note,false); @@ -463,21 +463,20 @@ void DivPlatformX1_010::tick(bool sysTick) { chan[i].envChanged=false; } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { - if (chan[i].furnacePCM) { - double off=4194304.0; + double off=8192.0; + if (chan[i].pcm) { int sample=chan[i].sample; if (sample>=0 && samplesong.sampleLen) { DivSample* s=parent->getSample(sample); if (s->centerRate<1) { - off=4194304.0; + off=8192.0; } else { - off=4194304.0*(s->centerRate/8363.0); + off=8192.0*(s->centerRate/8363.0); } } } - chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE); + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,chan[i].pcm?off:CHIP_FREQBASE); if (chan[i].pcm) { - chan[i].freq>>=8; if (chan[i].freq<1) chan[i].freq=1; if (chan[i].freq>255) chan[i].freq=255; chWrite(i,2,chan[i].freq&0xff); @@ -592,7 +591,7 @@ int DivPlatformX1_010::dispatch(DivCommand c) { int end=(sampleOffX1[chan[c.chan].sample]+s->length8+0xfff)&~0xfff; // padded chWrite(c.chan,5,(0x100-(end>>12))&0xff); } - chan[c.chan].baseFreq=(((unsigned int)s->rate)<<14)/(chipClock/384); + chan[c.chan].baseFreq=(((unsigned int)s->rate)<<4)/(chipClock/512); chan[c.chan].freqChanged=true; } } else if (c.value!=DIV_NOTE_NULL) { From 4695659ae44a07e39ecdbf83276e8e2661f83430 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 4 Oct 2022 00:04:41 -0500 Subject: [PATCH 24/57] X1-010: fix 17xx PCM - take 2 --- src/engine/platform/x1_010.cpp | 11 ++++++++--- src/engine/platform/x1_010.h | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/engine/platform/x1_010.cpp b/src/engine/platform/x1_010.cpp index 29e5edbd..371e311b 100644 --- a/src/engine/platform/x1_010.cpp +++ b/src/engine/platform/x1_010.cpp @@ -476,6 +476,7 @@ void DivPlatformX1_010::tick(bool sysTick) { } } chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,chan[i].pcm?off:CHIP_FREQBASE); + if (chan[i].fixedFreq) chan[i].freq=chan[i].fixedFreq; if (chan[i].pcm) { if (chan[i].freq<1) chan[i].freq=1; if (chan[i].freq>255) chan[i].freq=255; @@ -554,6 +555,7 @@ int DivPlatformX1_010::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note); + chan[c.chan].fixedFreq=0; chan[c.chan].freqChanged=true; } } else { @@ -571,7 +573,8 @@ int DivPlatformX1_010::dispatch(DivCommand c) { } else { chan[c.chan].macroInit(NULL); chan[c.chan].outVol=chan[c.chan].vol; - if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + chan[c.chan].sample=12*sampleBank+c.value%12; + if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { chWrite(c.chan,0,0); // reset chWrite(c.chan,1,0); chWrite(c.chan,2,0); @@ -579,7 +582,7 @@ int DivPlatformX1_010::dispatch(DivCommand c) { chWrite(c.chan,5,0); break; } - DivSample* s=parent->getSample(12*sampleBank+c.value%12); + DivSample* s=parent->getSample(chan[c.chan].sample); if (isBanked) { bankSlot[chan[c.chan].bankSlot]=sampleOffX1[chan[c.chan].sample]>>17; unsigned int bankedOffs=(chan[c.chan].bankSlot<<17)|(sampleOffX1[chan[c.chan].sample]&0x1ffff); @@ -591,12 +594,14 @@ int DivPlatformX1_010::dispatch(DivCommand c) { int end=(sampleOffX1[chan[c.chan].sample]+s->length8+0xfff)&~0xfff; // padded chWrite(c.chan,5,(0x100-(end>>12))&0xff); } - chan[c.chan].baseFreq=(((unsigned int)s->rate)<<4)/(chipClock/512); + // ???? + chan[c.chan].fixedFreq=(((unsigned int)s->rate)<<4)/(chipClock/512); chan[c.chan].freqChanged=true; } } else if (c.value!=DIV_NOTE_NULL) { chan[c.chan].note=c.value; chan[c.chan].baseFreq=NoteX1_010(c.chan,chan[c.chan].note); + chan[c.chan].fixedFreq=0; chan[c.chan].freqChanged=true; } chan[c.chan].active=true; diff --git a/src/engine/platform/x1_010.h b/src/engine/platform/x1_010.h index 94d03011..ff5557bb 100644 --- a/src/engine/platform/x1_010.h +++ b/src/engine/platform/x1_010.h @@ -68,7 +68,7 @@ class DivPlatformX1_010: public DivDispatch, public vgsound_emu_mem_intf { slide(0), slidefrac(0) {} }; - int freq, baseFreq, pitch, pitch2, note; + int freq, baseFreq, fixedFreq, pitch, pitch2, note; int wave, sample, ins; unsigned char pan, autoEnvNum, autoEnvDen; bool active, insChanged, envChanged, freqChanged, keyOn, keyOff, inPorta, furnacePCM, pcm; @@ -95,7 +95,7 @@ class DivPlatformX1_010: public DivDispatch, public vgsound_emu_mem_intf { pitch2=0; } Channel(): - freq(0), baseFreq(0), pitch(0), pitch2(0), note(0), + freq(0), baseFreq(0), fixedFreq(0), pitch(0), pitch2(0), note(0), wave(-1), sample(-1), ins(-1), pan(255), autoEnvNum(0), autoEnvDen(0), active(false), insChanged(true), envChanged(true), freqChanged(false), keyOn(false), keyOff(false), inPorta(false), furnacePCM(false), pcm(false), From d981e59c59d25c24f4bc50fd37685ef5d6e4e168 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 4 Oct 2022 00:46:16 -0500 Subject: [PATCH 25/57] (12*sampleBank+c.value%12) --- src/engine/platform/ay.cpp | 1 + src/engine/platform/msm6258.cpp | 2 +- src/engine/platform/msm6295.cpp | 2 +- src/engine/platform/x1_010.cpp | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index b01d6a9f..ab2c2c72 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -776,6 +776,7 @@ void DivPlatformAY8910::setFlags(const DivConfig& flags) { if (extMode) { chipClock=extClock; rate=chipClock/extDiv; + clockSel=false; } else { clockSel=flags.getBool("halfClock",false); switch (flags.getInt("clockSel",0)) { diff --git a/src/engine/platform/msm6258.cpp b/src/engine/platform/msm6258.cpp index 3d8ec38c..17e88ae6 100644 --- a/src/engine/platform/msm6258.cpp +++ b/src/engine/platform/msm6258.cpp @@ -167,7 +167,7 @@ int DivPlatformMSM6258::dispatch(DivCommand c) { chan[c.chan].sample=-1; chan[c.chan].macroInit(NULL); chan[c.chan].outVol=chan[c.chan].vol; - if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + if ((12*sampleBank+c.value%12)<0 || (12*sampleBank+c.value%12)>=parent->song.sampleLen) { break; } //DivSample* s=parent->getSample(12*sampleBank+c.value%12); diff --git a/src/engine/platform/msm6295.cpp b/src/engine/platform/msm6295.cpp index 0f570d9c..d22aabd5 100644 --- a/src/engine/platform/msm6295.cpp +++ b/src/engine/platform/msm6295.cpp @@ -158,7 +158,7 @@ int DivPlatformMSM6295::dispatch(DivCommand c) { chan[c.chan].sample=-1; chan[c.chan].macroInit(NULL); chan[c.chan].outVol=chan[c.chan].vol; - if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { + if ((12*sampleBank+c.value%12)<0 || (12*sampleBank+c.value%12)>=parent->song.sampleLen) { break; } //DivSample* s=parent->getSample(12*sampleBank+c.value%12); diff --git a/src/engine/platform/x1_010.cpp b/src/engine/platform/x1_010.cpp index 371e311b..6f220242 100644 --- a/src/engine/platform/x1_010.cpp +++ b/src/engine/platform/x1_010.cpp @@ -561,6 +561,7 @@ int DivPlatformX1_010::dispatch(DivCommand c) { } else { chan[c.chan].macroInit(NULL); chan[c.chan].outVol=chan[c.chan].vol; + // huh? if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) { chWrite(c.chan,0,0); // reset chWrite(c.chan,1,0); From c2b75d26d73e9394dd98f949709591a5197ae434 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 4 Oct 2022 01:20:26 -0500 Subject: [PATCH 26/57] SNES: loop injection --- src/engine/platform/snes.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index fb278f2a..8be1692e 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -783,6 +783,10 @@ void DivPlatformSNES::renderSamples() { if (actualLength>0) { sampleOff[i]=memPos; memcpy(©OfSampleMem[memPos],s->dataBRR,actualLength); + // inject loop if needed + if (s->loop) { + copyOfSampleMem[memPos+actualLength-9]|=3; + } memPos+=actualLength; } if (actualLength Date: Tue, 4 Oct 2022 02:35:32 -0500 Subject: [PATCH 27/57] MSM5232: per-chan osc --- src/engine/platform/msm5232.cpp | 12 +++++++++--- src/engine/platform/sound/oki/msm5232.cpp | 8 ++++---- src/engine/platform/sound/oki/msm5232.h | 5 +++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/engine/platform/msm5232.cpp b/src/engine/platform/msm5232.cpp index 462ecc76..d19719db 100644 --- a/src/engine/platform/msm5232.cpp +++ b/src/engine/platform/msm5232.cpp @@ -54,9 +54,15 @@ void DivPlatformMSM5232::acquire(short* bufL, short* bufR, size_t start, size_t } memset(temp,0,16*sizeof(short)); - /*for (int i=0; i<8; i++) { - oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP((pce->channel[i].blip_prev_samp[0]+pce->channel[i].blip_prev_samp[1])<<1,-32768,32767); - }*/ + for (int i=0; i<8; i++) { + int o=( + ((regPool[12+(i>>4)]&1)?((msm->vo16[i]*partVolume[3+(i&4)])>>8):0)+ + ((regPool[12+(i>>4)]&2)?((msm->vo8[i]*partVolume[2+(i&4)])>>8):0)+ + ((regPool[12+(i>>4)]&4)?((msm->vo4[i]*partVolume[1+(i&4)])>>8):0)+ + ((regPool[12+(i>>4)]&8)?((msm->vo2[i]*partVolume[i&4])>>8):0) + )<<3; + oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP(o,-32768,32767); + } msm->sound_stream_update(temp); diff --git a/src/engine/platform/sound/oki/msm5232.cpp b/src/engine/platform/sound/oki/msm5232.cpp index 59c001ae..0aad02af 100644 --- a/src/engine/platform/sound/oki/msm5232.cpp +++ b/src/engine/platform/sound/oki/msm5232.cpp @@ -596,10 +596,10 @@ void msm5232_device::TG_group_advance(int groupidx) /* calculate signed output */ if (!voi->mute) { - o16 += ( (out16-(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; - o8 += ( (out8 -(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; - o4 += ( (out4 -(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; - o2 += ( (out2 -(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; + o16 += vo16[groupidx*4+(4-i)] = ( (out16-(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; + o8 += vo8 [groupidx*4+(4-i)] = ( (out8 -(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; + o4 += vo4 [groupidx*4+(4-i)] = ( (out4 -(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; + o2 += vo2 [groupidx*4+(4-i)] = ( (out2 -(1<<(STEP_SH-1))) * voi->egvol) >> STEP_SH; if (i == 1 && groupidx == 1) { diff --git a/src/engine/platform/sound/oki/msm5232.h b/src/engine/platform/sound/oki/msm5232.h index ecf8d1e8..641b460e 100644 --- a/src/engine/platform/sound/oki/msm5232.h +++ b/src/engine/platform/sound/oki/msm5232.h @@ -33,6 +33,11 @@ public: int get_rate(); + int vo16[8]; + int vo8[8]; + int vo4[8]; + int vo2[8]; + private: struct VOICE { uint8_t mode; From 263982719daf46964e2714d56f66a2759edd074d Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 4 Oct 2022 03:34:38 -0500 Subject: [PATCH 28/57] MSM5232: oh come on why didn't I commit this? --- src/engine/platform/msm5232.cpp | 18 ++++++++++++++++-- src/engine/platform/msm5232.h | 4 +++- src/gui/sysConf.cpp | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/engine/platform/msm5232.cpp b/src/engine/platform/msm5232.cpp index d19719db..74a063ba 100644 --- a/src/engine/platform/msm5232.cpp +++ b/src/engine/platform/msm5232.cpp @@ -52,7 +52,6 @@ void DivPlatformMSM5232::acquire(short* bufL, short* bufR, size_t start, size_t regPool[w.addr&0x0f]=w.val; writes.pop(); } - memset(temp,0,16*sizeof(short)); for (int i=0; i<8; i++) { int o=( @@ -64,7 +63,15 @@ void DivPlatformMSM5232::acquire(short* bufL, short* bufR, size_t start, size_t oscBuf[i]->data[oscBuf[i]->needle++]=CLAMP(o,-32768,32767); } - msm->sound_stream_update(temp); + clockDriftLFOPos+=clockDriftLFOSpeed; + clockDriftLFOPos&=(1U<<21)-1; + clockDriftAccum+=clockDriftLFOWave[clockDriftLFOPos>>13]; + if (clockDriftAccum>=2048) { + clockDriftAccum-=2048; + } else { + memset(temp,0,16*sizeof(short)); + msm->sound_stream_update(temp); + } //printf("tempL: %d tempR: %d\n",tempL,tempR); bufL[h]=0; @@ -328,6 +335,8 @@ void DivPlatformMSM5232::reset() { cycles=0; curChan=-1; delay=500; + clockDriftLFOPos=0; + clockDriftAccum=0; for (int i=0; i<2; i++) { groupControl[i]=15|(groupEnv[i]?0x20:0); @@ -399,6 +408,11 @@ void DivPlatformMSM5232::setFlags(const DivConfig& flags) { capacitance[6]*0.000000001, capacitance[7]*0.000000001 ); + + for (int i=0; i<256; i++) { + clockDriftLFOWave[i]=(1.0+sin(M_PI*(double)i/128.0))*flags.getInt("vibDepth",0.0f); + } + clockDriftLFOSpeed=flags.getInt("vibSpeed",0); } void DivPlatformMSM5232::poke(unsigned int addr, unsigned short val) { diff --git a/src/engine/platform/msm5232.h b/src/engine/platform/msm5232.h index 34412b9a..3e2bf71a 100644 --- a/src/engine/platform/msm5232.h +++ b/src/engine/platform/msm5232.h @@ -57,6 +57,7 @@ class DivPlatformMSM5232: public DivDispatch { DivDispatchOscBuffer* oscBuf[8]; int partVolume[8]; int initPartVolume[8]; + int clockDriftLFOWave[256]; double capacitance[8]; bool isMuted[8]; bool updateGroup[2]; @@ -73,7 +74,8 @@ class DivPlatformMSM5232: public DivDispatch { }; std::queue writes; - int cycles, curChan, delay, detune; + int cycles, curChan, delay, detune, clockDriftAccum; + unsigned int clockDriftLFOPos, clockDriftLFOSpeed; short temp[16]; msm5232_device* msm; unsigned char regPool[128]; diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index a1dc3181..9e2d165f 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -1298,6 +1298,8 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo } case DIV_SYSTEM_MSM5232: { int detune=flags.getInt("detune",0); + int vibSpeed=flags.getInt("vibSpeed",0); + float vibDepth=flags.getFloat("vibDepth",0.0f); bool groupEnv[2]; int groupVol[8]; float capValue[8]; @@ -1377,8 +1379,23 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo altered=true; } + ImGui::Text("Global vibrato:"); + + if (CWSliderInt("Speed",&vibSpeed,0,256)) { + if (vibSpeed<0) vibSpeed=0; + if (vibSpeed>256) vibSpeed=256; + altered=true; + } rightClickable + if (CWSliderFloat("Depth",&vibDepth,0.0f,256.0f)) { + if (vibDepth<0) vibDepth=0; + if (vibDepth>256) vibDepth=256; + altered=true; + } rightClickable + if (altered) { flags.set("detune",detune); + flags.set("vibSpeed",vibSpeed); + flags.set("vibDepth",vibDepth); flags.set("capValue0",capValue[0]); flags.set("capValue1",capValue[1]); From 1fbf5929944caf6b576270d9f847bfe184e230bf Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 4 Oct 2022 13:18:18 -0500 Subject: [PATCH 29/57] fix build on Windows --- src/engine/platform/msm5232.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/platform/msm5232.cpp b/src/engine/platform/msm5232.cpp index 74a063ba..84e8d91b 100644 --- a/src/engine/platform/msm5232.cpp +++ b/src/engine/platform/msm5232.cpp @@ -17,6 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#define _USE_MATH_DEFINES #include "msm5232.h" #include "../engine.h" #include From 16b752dc8a7111ff1eca4574682dd1db7ad15e72 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 4 Oct 2022 18:57:04 -0500 Subject: [PATCH 30/57] experimental split command stream --- src/engine/engine.cpp | 76 ++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index ea5ee5d0..433bdc2d 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -274,23 +274,26 @@ double DivEngine::benchmarkSeek() { return tAvg; } -#define WRITE_TICK \ - if (!wroteTick) { \ - wroteTick=true; \ - if (binary) { \ - if (tick-lastTick>255) { \ - w->writeC(0xfc); \ - w->writeS(tick-lastTick); \ - } else if (tick-lastTick>1) { \ - w->writeC(0xfd); \ - w->writeC(tick-lastTick); \ +#define WRITE_TICK(x) \ + if (binary) { \ + if (!wroteTick[x]) { \ + wroteTick[x]=true; \ + if (tick-lastTick[x]>255) { \ + chanStream[x]->writeC(0xfc); \ + chanStream[x]->writeS(tick-lastTick[x]); \ + } else if (tick-lastTick[x]>1) { \ + chanStream[x]->writeC(0xfd); \ + chanStream[x]->writeC(tick-lastTick[x]); \ } else { \ - w->writeC(0xfe); \ + chanStream[x]->writeC(0xfe); \ } \ - } else { \ + lastTick[x]=tick; \ + } \ + } else { \ + if (!wroteTickGlobal) { \ + wroteTickGlobal=true; \ w->writeText(fmt::sprintf(">> TICK %d\n",tick)); \ } \ - lastTick=tick; \ } void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { @@ -410,6 +413,13 @@ SafeWriter* DivEngine::saveCommand(bool binary) { int loopEnd=0; walkSong(loopOrder,loopRow,loopEnd); logI("loop point: %d %d",loopOrder,loopRow); + + SafeWriter* chanStream[DIV_MAX_CHANS]; + unsigned int chanStreamOff[DIV_MAX_CHANS]; + bool wroteTick[DIV_MAX_CHANS]; + + memset(chanStream,0,DIV_MAX_CHANS*sizeof(void*)); + memset(chanStreamOff,0,DIV_MAX_CHANS*sizeof(unsigned int)); SafeWriter* w=new SafeWriter; w->init(); @@ -417,6 +427,13 @@ SafeWriter* DivEngine::saveCommand(bool binary) { // write header if (binary) { w->write("FCS",4); + w->writeI(chans); + // offsets + for (int i=0; iinit(); + w->writeI(0); + } } else { w->writeText("# Furnace Command Stream\n\n"); @@ -451,19 +468,22 @@ SafeWriter* DivEngine::saveCommand(bool binary) { bool oldCmdStreamEnabled=cmdStreamEnabled; cmdStreamEnabled=true; double curDivider=divider; - int lastTick=0; + int lastTick[DIV_MAX_CHANS]; + + memset(lastTick,0,DIV_MAX_CHANS*sizeof(int)); while (!done) { if (nextTick(false,true) || !playing) { done=true; } // get command stream - bool wroteTick=false; + bool wroteTickGlobal=false; + memset(wroteTick,0,DIV_MAX_CHANS*sizeof(bool)); if (curDivider!=divider) { curDivider=divider; - WRITE_TICK; + WRITE_TICK(0); if (binary) { - w->writeC(0xfb); - w->writeI((int)(curDivider*65536)); + chanStream[0]->writeC(0xfb); + chanStream[0]->writeI((int)(curDivider*65536)); } else { w->writeText(fmt::sprintf(">> SET_RATE %f\n",curDivider)); } @@ -486,10 +506,9 @@ SafeWriter* DivEngine::saveCommand(bool binary) { case DIV_CMD_PRE_NOTE: break; default: - WRITE_TICK; + WRITE_TICK(i.chan); if (binary) { - w->writeC(i.chan); - writePackedCommandValues(w,i); + writePackedCommandValues(chanStream[i.chan],i); } else { w->writeText(fmt::sprintf(" %d: %s %d %d\n",i.chan,cmdName[i.cmd],i.value,i.value2)); } @@ -502,7 +521,20 @@ SafeWriter* DivEngine::saveCommand(bool binary) { cmdStreamEnabled=oldCmdStreamEnabled; if (binary) { - w->writeC(0xff); + for (int i=0; iwriteC(0xff); + + chanStreamOff[i]=w->tell(); + logI("- %d: off %x size %ld",i,chanStreamOff[i],chanStream[i]->size()); + w->write(chanStream[i]->getFinalBuf(),chanStream[i]->size()); + chanStream[i]->finish(); + delete chanStream[i]; + } + + w->seek(8,SEEK_SET); + for (int i=0; iwriteI(chanStreamOff[i]); + } } else { if (!playing) { w->writeText(">> END\n"); From 2d3d7c0716d51503312e2b1808fe8e87cdaeb00b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 6 Oct 2022 04:51:52 -0500 Subject: [PATCH 31/57] update export-tech.md --- papers/export-tech.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/papers/export-tech.md b/papers/export-tech.md index dbfadece..9f4a7242 100644 --- a/papers/export-tech.md +++ b/papers/export-tech.md @@ -13,6 +13,20 @@ then read data. ## binary command stream +Furnace Command Stream, split version. + +``` +size | description +-----|------------------------------------ + 4 | "FCS\0" format magic + 4 | channel count + 4?? | pointers to channel data + 2?? | preset delays + | - 16 values + 1?? | speed dial commands + | - 16 values +``` + read channel, command and values. if channel is 80 or higher, then it is a special command: From 85eaf91591e39c54ce383fb5c753c9c3e52899e0 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 6 Oct 2022 14:37:42 -0500 Subject: [PATCH 32/57] FCS: some optimization speed dial commands and preset delays FCS = Furnace Command Stream (binary) --- papers/export-tech.md | 60 ++++++++-- src/engine/engine.cpp | 247 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 288 insertions(+), 19 deletions(-) diff --git a/papers/export-tech.md b/papers/export-tech.md index 9f4a7242..95dd7c76 100644 --- a/papers/export-tech.md +++ b/papers/export-tech.md @@ -21,20 +21,62 @@ size | description 4 | "FCS\0" format magic 4 | channel count 4?? | pointers to channel data - 2?? | preset delays + 1?? | preset delays | - 16 values 1?? | speed dial commands | - 16 values + ??? | channel data ``` -read channel, command and values. - -if channel is 80 or higher, then it is a special command: +read command and values (if any). +the list of commands follows. ``` -fb xx xx xx xx: set tick rate -fc xx xx: wait xxxx ticks -fd xx: wait xx ticks -fe: wait one tick -ff: stop +hex | description +----|------------------------------------ + 00 | note on: C-(-5) + 01 | note on: C#(-5) + 02 | note on: D-(-5) + .. | ... + b1 | note on: A-9 + b2 | note on: A#9 + b3 | note on: B-9 + b4 | note on: null +----|------------------------------------ + b5 | note off + b6 | note off env + b7 | env release + b8 | instrument // (ins, force) + be | panning // (left, right) + c0 | pre porta // (inporta, isportaorslide) + c2 | vibrato // (speed, depth) + c3 | vibrato range // (range) + c4 | vibrato shape // (shape) + c5 | pitch // (pitch) + c6 | arpeggio // (note1, note2) + c7 | volume // (vol) + c8 | vol slide // (amount, onetick) + c9 | porta // (target, speed) + ca | legato // (note) +----|------------------------------------ + d0 | speed dial command 0 + d1 | speed dial command 1 + .. | ... + df | speed dial command 15 +----|------------------------------------ + e0 | preset delay 0 + e1 | preset delay 1 + .. | ... + ef | preset delay 15 +----|------------------------------------ + f7 | full command (command and data follows) + f8 | go to sub-block (offset follows) + f9 | return from sub-block + fa | jump (offset follows) + fb | set tick rate (4 bytes) + fc | wait (16-bit) + fd | wait (8-bit) + fe | wait one tick + ff | stop ``` + diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 433bdc2d..c813c4a0 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -282,9 +282,10 @@ double DivEngine::benchmarkSeek() { chanStream[x]->writeC(0xfc); \ chanStream[x]->writeS(tick-lastTick[x]); \ } else if (tick-lastTick[x]>1) { \ + delayPopularity[tick-lastTick[x]]++; \ chanStream[x]->writeC(0xfd); \ chanStream[x]->writeC(tick-lastTick[x]); \ - } else { \ + } else if (tick-lastTick[x]>0) { \ chanStream[x]->writeC(0xfe); \ } \ lastTick[x]=tick; \ @@ -297,9 +298,37 @@ double DivEngine::benchmarkSeek() { } void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { - w->writeC(c.cmd); switch (c.cmd) { case DIV_CMD_NOTE_ON: + if (c.value==DIV_NOTE_NULL) { + w->writeC(0xb4); + } else { + w->writeC(CLAMP(c.value+60,0,0xb3)); + } + break; + case DIV_CMD_NOTE_OFF: + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + case DIV_CMD_INSTRUMENT: + case DIV_CMD_PANNING: + case DIV_CMD_PRE_PORTA: + case DIV_CMD_HINT_VIBRATO: + case DIV_CMD_HINT_VIBRATO_RANGE: + case DIV_CMD_HINT_VIBRATO_SHAPE: + case DIV_CMD_HINT_PITCH: + case DIV_CMD_HINT_ARPEGGIO: + case DIV_CMD_HINT_VOLUME: + case DIV_CMD_HINT_PORTA: + case DIV_CMD_HINT_VOL_SLIDE: + case DIV_CMD_HINT_LEGATO: + w->writeC((unsigned char)c.cmd+0xb4); + break; + default: + w->writeC(0xf0); // unoptimized extended command + w->writeC(c.cmd); + break; + } + switch (c.cmd) { case DIV_CMD_HINT_LEGATO: if (c.value==DIV_NOTE_NULL) { w->writeC(0xff); @@ -307,6 +336,7 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { w->writeC(c.value+60); } break; + case DIV_CMD_NOTE_ON: case DIV_CMD_NOTE_OFF: case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: @@ -316,6 +346,21 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { case DIV_CMD_HINT_VIBRATO_SHAPE: case DIV_CMD_HINT_PITCH: case DIV_CMD_HINT_VOLUME: + w->writeC(c.value); + break; + case DIV_CMD_PANNING: + case DIV_CMD_HINT_VIBRATO: + case DIV_CMD_HINT_ARPEGGIO: + case DIV_CMD_HINT_PORTA: + w->writeC(c.value); + w->writeC(c.value2); + break; + case DIV_CMD_PRE_PORTA: + w->writeC((c.value?0x80:0)|(c.value2?0x40:0)); + break; + case DIV_CMD_HINT_VOL_SLIDE: + w->writeS(c.value); + break; case DIV_CMD_SAMPLE_MODE: case DIV_CMD_SAMPLE_FREQ: case DIV_CMD_SAMPLE_BANK: @@ -351,12 +396,9 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { case DIV_CMD_AY_NOISE_MASK_AND: case DIV_CMD_AY_NOISE_MASK_OR: case DIV_CMD_AY_AUTO_ENVELOPE: + w->writeC(1); // length w->writeC(c.value); break; - case DIV_CMD_PANNING: - case DIV_CMD_HINT_VIBRATO: - case DIV_CMD_HINT_ARPEGGIO: - case DIV_CMD_HINT_PORTA: case DIV_CMD_FM_TL: case DIV_CMD_FM_AM: case DIV_CMD_FM_AR: @@ -378,25 +420,26 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { case DIV_CMD_FM_FINE: case DIV_CMD_AY_IO_WRITE: case DIV_CMD_AY_AUTO_PWM: + w->writeC(2); // length w->writeC(c.value); w->writeC(c.value2); break; - case DIV_CMD_PRE_PORTA: - w->writeC((c.value?0x80:0)|(c.value2?0x40:0)); - break; - case DIV_CMD_HINT_VOL_SLIDE: case DIV_CMD_C64_FINE_DUTY: case DIV_CMD_C64_FINE_CUTOFF: + w->writeC(2); // length w->writeS(c.value); break; case DIV_CMD_FM_FIXFREQ: + w->writeC(2); // length w->writeS((c.value<<12)|(c.value2&0x7ff)); break; case DIV_CMD_NES_SWEEP: + w->writeC(1); // length w->writeC((c.value?8:0)|(c.value2&0x77)); break; default: logW("unimplemented command %s!",cmdName[c.cmd]); + w->writeC(0); // length break; } } @@ -413,13 +456,27 @@ SafeWriter* DivEngine::saveCommand(bool binary) { int loopEnd=0; walkSong(loopOrder,loopRow,loopEnd); logI("loop point: %d %d",loopOrder,loopRow); + + int cmdPopularity[256]; + int delayPopularity[256]; + + int sortedCmdPopularity[16]; + int sortedDelayPopularity[16]; + unsigned char sortedCmd[16]; + unsigned char sortedDelay[16]; SafeWriter* chanStream[DIV_MAX_CHANS]; unsigned int chanStreamOff[DIV_MAX_CHANS]; bool wroteTick[DIV_MAX_CHANS]; + memset(cmdPopularity,0,256*sizeof(int)); + memset(delayPopularity,0,256*sizeof(int)); memset(chanStream,0,DIV_MAX_CHANS*sizeof(void*)); memset(chanStreamOff,0,DIV_MAX_CHANS*sizeof(unsigned int)); + memset(sortedCmdPopularity,0,16*sizeof(int)); + memset(sortedDelayPopularity,0,16*sizeof(int)); + memset(sortedCmd,0,16); + memset(sortedDelay,0,16); SafeWriter* w=new SafeWriter; w->init(); @@ -434,6 +491,10 @@ SafeWriter* DivEngine::saveCommand(bool binary) { chanStream[i]->init(); w->writeI(0); } + // preset delays and speed dial + for (int i=0; i<32; i++) { + w->writeC(0); + } } else { w->writeText("# Furnace Command Stream\n\n"); @@ -508,6 +569,7 @@ SafeWriter* DivEngine::saveCommand(bool binary) { default: WRITE_TICK(i.chan); if (binary) { + cmdPopularity[i.cmd]++; writePackedCommandValues(chanStream[i.chan],i); } else { w->writeText(fmt::sprintf(" %d: %s %d %d\n",i.chan,cmdName[i.cmd],i.value,i.value2)); @@ -521,9 +583,162 @@ SafeWriter* DivEngine::saveCommand(bool binary) { cmdStreamEnabled=oldCmdStreamEnabled; if (binary) { + int sortCand=-1; + int sortPos=0; + while (sortPos<16) { + sortCand=-1; + for (int i=DIV_CMD_SAMPLE_MODE; i<256; i++) { + if (cmdPopularity[i]) { + if (sortCand==-1) { + sortCand=i; + } else if (cmdPopularity[sortCand]writeC(0xff); + // optimize stream + SafeWriter* oldStream=chanStream[i]; + SafeReader* reader=oldStream->toReader(); + chanStream[i]=new SafeWriter; + chanStream[i]->init(); + while (1) { + try { + unsigned char next=reader->readC(); + switch (next) { + case 0xb8: // instrument + case 0xc0: // pre porta + case 0xc3: // vibrato range + case 0xc4: // vibrato shape + case 0xc5: // pitch + case 0xc7: // volume + case 0xca: // legato + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + break; + case 0xbe: // panning + case 0xc2: // vibrato + case 0xc6: // arpeggio + case 0xc8: // vol slide + case 0xc9: // porta + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + break; + case 0xf0: { // full command (pre) + unsigned char cmd=reader->readC(); + bool foundShort=false; + for (int j=0; j<16; j++) { + if (sortedCmd[j]==cmd) { + chanStream[i]->writeC(0xd0+j); + foundShort=true; + break; + } + } + if (!foundShort) { + chanStream[i]->writeC(0xf7); // full command + chanStream[i]->writeC(cmd); + } + + unsigned char cmdLen=reader->readC(); + logD("cmdLen: %d",cmdLen); + for (unsigned char j=0; jreadC(); + chanStream[i]->writeC(next); + } + break; + } + case 0xfb: // tick rate + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + next=reader->readC(); + chanStream[i]->writeC(next); + break; + case 0xfc: { // 16-bit wait + unsigned short delay=reader->readS(); + bool foundShort=false; + for (int j=0; j<16; j++) { + if (sortedDelay[j]==delay) { + chanStream[i]->writeC(0xe0+j); + foundShort=true; + break; + } + } + if (!foundShort) { + chanStream[i]->writeC(next); + chanStream[i]->writeS(delay); + } + break; + } + case 0xfd: { // 8-bit wait + unsigned char delay=reader->readC(); + bool foundShort=false; + for (int j=0; j<16; j++) { + if (sortedDelay[j]==delay) { + chanStream[i]->writeC(0xe0+j); + foundShort=true; + break; + } + } + if (!foundShort) { + chanStream[i]->writeC(next); + chanStream[i]->writeC(delay); + } + break; + } + default: + chanStream[i]->writeC(next); + break; + } + } catch (EndOfFileException& e) { + break; + } + } + + oldStream->finish(); + delete oldStream; + } + + for (int i=0; itell(); logI("- %d: off %x size %ld",i,chanStreamOff[i],chanStream[i]->size()); w->write(chanStream[i]->getFinalBuf(),chanStream[i]->size()); @@ -535,6 +750,18 @@ SafeWriter* DivEngine::saveCommand(bool binary) { for (int i=0; iwriteI(chanStreamOff[i]); } + + logD("delay popularity:"); + for (int i=0; i<16; i++) { + w->writeC(sortedDelay[i]); + if (sortedDelayPopularity[i]) logD("- %d: %d",sortedDelay[i],sortedDelayPopularity[i]); + } + + logD("command popularity:"); + for (int i=0; i<16; i++) { + w->writeC(sortedCmd[i]); + if (sortedCmdPopularity[i]) logD("- %s: %d",cmdName[sortedCmd[i]],sortedCmdPopularity[i]); + } } else { if (!playing) { w->writeText(">> END\n"); From 68d962fcdb024ce35baff2ef22e710c109f24d29 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 6 Oct 2022 15:37:54 -0500 Subject: [PATCH 33/57] FCS: implement more commands --- src/engine/engine.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index c813c4a0..417345e9 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -396,6 +396,15 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { case DIV_CMD_AY_NOISE_MASK_AND: case DIV_CMD_AY_NOISE_MASK_OR: case DIV_CMD_AY_AUTO_ENVELOPE: + case DIV_CMD_FDS_MOD_DEPTH: + case DIV_CMD_FDS_MOD_HIGH: + case DIV_CMD_FDS_MOD_LOW: + case DIV_CMD_FDS_MOD_POS: + case DIV_CMD_FDS_MOD_WAVE: + case DIV_CMD_SAA_ENVELOPE: + case DIV_CMD_AMIGA_FILTER: + case DIV_CMD_AMIGA_AM: + case DIV_CMD_AMIGA_PM: w->writeC(1); // length w->writeC(c.value); break; @@ -426,6 +435,7 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { break; case DIV_CMD_C64_FINE_DUTY: case DIV_CMD_C64_FINE_CUTOFF: + case DIV_CMD_LYNX_LFSR_LOAD: w->writeC(2); // length w->writeS(c.value); break; From 2cebd752368fa89b3a2e10cee0f09be3a5538464 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 6 Oct 2022 15:38:08 -0500 Subject: [PATCH 34/57] GUI: loop range buttons crs inc/dec should be 16 IT IS HARD TO FIT A DESCRIPTION IN 50 CHARS --- src/gui/sampleEdit.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 2c58a99e..aa3c4cb3 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -151,7 +151,7 @@ void FurnaceGUI::drawSampleEdit() { ImGui::Text("Loop Start"); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##LoopStartPosition",&sample->loopStart,1,10)) { MARK_MODIFIED + if (ImGui::InputInt("##LoopStartPosition",&sample->loopStart,1,16)) { MARK_MODIFIED if (sample->loopStart<0) { sample->loopStart=0; } @@ -173,7 +173,7 @@ void FurnaceGUI::drawSampleEdit() { ImGui::Text("Loop End"); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##LoopEndPosition",&sample->loopEnd,1,10)) { MARK_MODIFIED + if (ImGui::InputInt("##LoopEndPosition",&sample->loopEnd,1,16)) { MARK_MODIFIED if (sample->loopEndloopStart) { sample->loopEnd=sample->loopStart; } @@ -734,7 +734,7 @@ void FurnaceGUI::drawSampleEdit() { ImGui::Text("Loop Start"); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##LoopStartPosition",&sample->loopStart,1,10)) { MARK_MODIFIED + if (ImGui::InputInt("##LoopStartPosition",&sample->loopStart,1,16)) { MARK_MODIFIED if (sample->loopStart<0) { sample->loopStart=0; } @@ -756,7 +756,7 @@ void FurnaceGUI::drawSampleEdit() { ImGui::Text("Loop End"); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::InputInt("##LoopEndPosition",&sample->loopEnd,1,10)) { MARK_MODIFIED + if (ImGui::InputInt("##LoopEndPosition",&sample->loopEnd,1,16)) { MARK_MODIFIED if (sample->loopEndloopStart) { sample->loopEnd=sample->loopStart; } From a979bc244ddbac670398b91219e749ff62bda361 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 04:11:45 -0500 Subject: [PATCH 35/57] start working on ADSR macro mode --- papers/format.md | 33 +++ src/engine/engine.h | 4 +- src/engine/instrument.h | 2 +- src/engine/macroInt.cpp | 85 +++++-- src/engine/macroInt.h | 7 +- src/gui/insEdit.cpp | 512 +++++++++++++++++++++++++--------------- 6 files changed, 436 insertions(+), 207 deletions(-) diff --git a/papers/format.md b/papers/format.md index 03ef41f2..ee76ce70 100644 --- a/papers/format.md +++ b/papers/format.md @@ -437,6 +437,12 @@ notes: - the entire instrument is stored, regardless of instrument type. - the macro range varies depending on the instrument type. - "macro open" indicates whether the macro is collapsed or not in the instrument editor. + - as of format version 120, bit 1-2 indicates macro mode: + - 0: sequence (normal) + - 1: ADSR + - 2: LFO + - 3: ADSR+LFO + - see sub-section for information on how to interpret parameters. - FM operator order is: - 1/3/2/4 (internal order) for OPN, OPM, OPZ and OPL 4-op - 1/2/?/? (? = unused) for OPL 2-op and OPLL @@ -1024,6 +1030,33 @@ size | description 1 | KSR macro delay ``` +## interpreting macro mode values + +- sequence (normal): I think this is obvious... +- ADSR: + - `val[0]`: bottom + - `val[1]`: top + - `val[2]`: attack + - `val[3]`: hold time + - `val[4]`: decay + - `val[5]`: sustain level + - `val[6]`: sustain hold time + - `val[7]`: decay 2 + - `val[8]`: release +- LFO: + - `val[9]`: bottom + - `val[10]`: top + - `val[11]`: speed + - `val[12]`: waveform + - 0: triangle + - 1: sine + - 2: saw + - 3: pulse + - `val[13]`: phase + - `val[14]`: loop + - `val[15]`: global (not sure how will I implement this) +- for ADSR+LFO just interpret both ADSR and LFO params. + # wavetable ``` diff --git a/src/engine/engine.h b/src/engine/engine.h index 8dae58e7..4a7e894a 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 "dev119" -#define DIV_ENGINE_VERSION 119 +#define DIV_VERSION "dev120" +#define DIV_ENGINE_VERSION 120 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 6be0efd2..6587fecc 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -177,7 +177,7 @@ struct DivInstrumentMacro { String name; int val[256]; unsigned int mode; - bool open; + unsigned char open; unsigned char len, delay, speed, loop, rel; // the following variables are used by the GUI and not saved in the file diff --git a/src/engine/macroInt.cpp b/src/engine/macroInt.cpp index 955481ce..90a4a6aa 100644 --- a/src/engine/macroInt.cpp +++ b/src/engine/macroInt.cpp @@ -21,9 +21,20 @@ #include "instrument.h" #include "engine.h" +#define ADSR_LOW source.val[0] +#define ADSR_HIGH source.val[1] +#define ADSR_AR source.val[2] +#define ADSR_HT source.val[3] +#define ADSR_DR source.val[4] +#define ADSR_SL source.val[5] +#define ADSR_ST source.val[6] +#define ADSR_SR source.val[7] +#define ADSR_RR source.val[8] + void DivMacroStruct::prepare(DivInstrumentMacro& source, DivEngine* e) { has=had=actualHad=will=true; mode=source.mode; + type=(source.open>>1)&3; linger=(source.name=="vol" && e->song.volMacroLinger); } @@ -53,24 +64,70 @@ void DivMacroStruct::doMacro(DivInstrumentMacro& source, bool released, bool tic } actualHad=has; had=actualHad; + if (has) { - lastPos=pos; - val=source.val[pos++]; - if (pos>source.rel && !released) { - if (source.loopsource.rel && !released) { + if (source.loop=source.len) { + if (source.loop=source.rel || source.rel>=source.len)) { + pos=source.loop; + } else if (linger) { + pos--; + } else { + has=false; + } } } - if (pos>=source.len) { - if (source.loop=source.rel || source.rel>=source.len)) { - pos=source.loop; - } else if (linger) { - pos--; - } else { - has=false; + if (type==1 || type==3) { // ADSR + if (released && lastPos<3) lastPos=3; + switch (lastPos) { + case 0: // attack + pos+=ADSR_AR; + if (pos>255) { + pos=255; + lastPos=1; + delay=ADSR_HT; + } + break; + case 1: // decay + pos-=ADSR_DR; + if (pos<=ADSR_SL) { + pos=ADSR_SL; + lastPos=2; + delay=ADSR_ST; + } + break; + case 2: // sustain + pos-=ADSR_SR; + if (pos<0) { + pos=0; + lastPos=4; + } + break; + case 3: // release + pos-=ADSR_RR; + if (pos<0) { + pos=0; + lastPos=4; + } + break; + case 4: // end + pos=0; + if (!linger) has=false; + break; } + val=ADSR_LOW+((pos+(ADSR_HIGH-ADSR_LOW)*pos)>>8); + } + if (type==2 || type==3) { // LFO + } } } diff --git a/src/engine/macroInt.h b/src/engine/macroInt.h index 5208dc54..64d81e1b 100644 --- a/src/engine/macroInt.h +++ b/src/engine/macroInt.h @@ -28,10 +28,10 @@ struct DivMacroStruct { int pos, lastPos, delay; int val; bool has, had, actualHad, finished, will, linger, began; - unsigned int mode; + unsigned int mode, type; void doMacro(DivInstrumentMacro& source, bool released, bool tick); void init() { - pos=lastPos=mode=delay=0; + pos=lastPos=mode=type=delay=0; has=had=actualHad=will=false; linger=false; began=true; @@ -51,7 +51,8 @@ struct DivMacroStruct { will(false), linger(false), began(true), - mode(0) {} + mode(0), + type(0) {} }; class DivMacroInt { diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index a4384765..ab2312c5 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -203,6 +203,13 @@ enum FMParams { #define FM_NAME(x) fmParamNames[settings.fmNames][x] #define FM_SHORT_NAME(x) fmParamShortNames[settings.fmNames][x] +const char* macroTypeLabels[4]={ + ICON_FA_BAR_CHART "##IMacroType", + ICON_FA_AREA_CHART "##IMacroType", + ICON_FA_LINE_CHART "##IMacroType", + ICON_FA_SIGN_OUT "##IMacroType" +}; + const char* fmOperatorBits[5]={ "op1", "op2", "op3", "op4", NULL }; @@ -1326,22 +1333,44 @@ void FurnaceGUI::drawMacros(std::vector& macros) { ImGui::TableNextColumn(); ImGui::Text("%s",i.displayName); ImGui::SameLine(); - if (ImGui::SmallButton(i.macro->open?(ICON_FA_CHEVRON_UP "##IMacroOpen"):(ICON_FA_CHEVRON_DOWN "##IMacroOpen"))) { - i.macro->open=!i.macro->open; + if (ImGui::SmallButton((i.macro->open&1)?(ICON_FA_CHEVRON_UP "##IMacroOpen"):(ICON_FA_CHEVRON_DOWN "##IMacroOpen"))) { + i.macro->open^=1; } - if (i.macro->open) { - ImGui::SetNextItemWidth(lenAvail); - int macroLen=i.macro->len; - if (ImGui::InputScalar("##IMacroLen",ImGuiDataType_U8,¯oLen,&_ONE,&_THREE)) { MARK_MODIFIED - if (macroLen<0) macroLen=0; - if (macroLen>255) macroLen=255; - i.macro->len=macroLen; + if (i.macro->open&1) { + if ((i.macro->open&6)==0) { + ImGui::SetNextItemWidth(lenAvail); + int macroLen=i.macro->len; + if (ImGui::InputScalar("##IMacroLen",ImGuiDataType_U8,¯oLen,&_ONE,&_THREE)) { MARK_MODIFIED + if (macroLen<0) macroLen=0; + if (macroLen>255) macroLen=255; + i.macro->len=macroLen; + } } - if (ImGui::Button(ICON_FA_BAR_CHART "##IMacroType")) { - + if (ImGui::Button(macroTypeLabels[(i.macro->open>>1)&3])) { + i.macro->open+=2; + if (i.macro->open>=8) { + i.macro->open-=8; + } + PARAMETER; } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Coming soon!"); + switch (i.macro->open&6) { + case 0: + ImGui::SetTooltip("Macro type: Sequence"); + break; + case 2: + ImGui::SetTooltip("Macro type: ADSR"); + break; + case 4: + ImGui::SetTooltip("Macro type: LFO"); + break; + case 6: + ImGui::SetTooltip("Macro type: ADSR+LFO"); + break; + default: + ImGui::SetTooltip("Macro type: What's going on here?"); + break; + } } ImGui::SameLine(); ImGui::Button(ICON_FA_ELLIPSIS_H "##IMacroSet"); @@ -1372,204 +1401,313 @@ void FurnaceGUI::drawMacros(std::vector& macros) { // macro area ImGui::TableNextColumn(); - for (int j=0; j<256; j++) { - bit30Indicator[j]=0; - if (j+macroDragScroll>=i.macro->len) { - asFloat[j]=0; - asInt[j]=0; - } else { - asFloat[j]=deBit30(i.macro->val[j+macroDragScroll]); - asInt[j]=deBit30(i.macro->val[j+macroDragScroll])+i.bitOffset; - if (i.bit30) bit30Indicator[j]=enBit30(i.macro->val[j+macroDragScroll]); + if ((i.macro->open&6)==0) { + for (int j=0; j<256; j++) { + bit30Indicator[j]=0; + if (j+macroDragScroll>=i.macro->len) { + asFloat[j]=0; + asInt[j]=0; + } else { + asFloat[j]=deBit30(i.macro->val[j+macroDragScroll]); + asInt[j]=deBit30(i.macro->val[j+macroDragScroll])+i.bitOffset; + if (i.bit30) bit30Indicator[j]=enBit30(i.macro->val[j+macroDragScroll]); + } + if (j+macroDragScroll>=i.macro->len || (j+macroDragScroll>i.macro->rel && i.macro->looprel)) { + loopIndicator[j]=0; + } else { + loopIndicator[j]=((i.macro->loop!=255 && (j+macroDragScroll)>=i.macro->loop))|((i.macro->rel!=255 && (j+macroDragScroll)==i.macro->rel)<<1); + } } - if (j+macroDragScroll>=i.macro->len || (j+macroDragScroll>i.macro->rel && i.macro->looprel)) { - loopIndicator[j]=0; - } else { - loopIndicator[j]=((i.macro->loop!=255 && (j+macroDragScroll)>=i.macro->loop))|((i.macro->rel!=255 && (j+macroDragScroll)==i.macro->rel)<<1); - } - } - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); - if (i.macro->vZoom<1) { - if (i.macro->name=="arp") { - i.macro->vZoom=24; - i.macro->vScroll=120-12; - } else if (i.macro->name=="pitch") { - i.macro->vZoom=128; - i.macro->vScroll=2048-64; - } else { + if (i.macro->vZoom<1) { + if (i.macro->name=="arp") { + i.macro->vZoom=24; + i.macro->vScroll=120-12; + } else if (i.macro->name=="pitch") { + i.macro->vZoom=128; + i.macro->vScroll=2048-64; + } else { + i.macro->vZoom=i.max-i.min; + i.macro->vScroll=0; + } + } + if (i.macro->vZoom>(i.max-i.min)) { i.macro->vZoom=i.max-i.min; - i.macro->vScroll=0; } - } - if (i.macro->vZoom>(i.max-i.min)) { - i.macro->vZoom=i.max-i.min; - } - memset(doHighlight,0,256*sizeof(bool)); - if (e->isRunning()) for (int j=0; jgetTotalChannelCount(); j++) { - DivChannelState* chanState=e->getChanState(j); - if (chanState==NULL) continue; + memset(doHighlight,0,256*sizeof(bool)); + if (e->isRunning()) for (int j=0; jgetTotalChannelCount(); j++) { + DivChannelState* chanState=e->getChanState(j); + if (chanState==NULL) continue; - if (chanState->keyOff) continue; - if (chanState->lastIns!=curIns) continue; + if (chanState->keyOff) continue; + if (chanState->lastIns!=curIns) continue; - DivMacroInt* macroInt=e->getMacroInt(j); - if (macroInt==NULL) continue; + DivMacroInt* macroInt=e->getMacroInt(j); + if (macroInt==NULL) continue; - DivMacroStruct* macroStruct=macroInt->structByName(i.macro->name); - if (macroStruct==NULL) continue; + DivMacroStruct* macroStruct=macroInt->structByName(i.macro->name); + if (macroStruct==NULL) continue; - if (macroStruct->lastPos>i.macro->len) continue; - if (macroStruct->lastPoslastPos>255) continue; - if (!macroStruct->actualHad) continue; + if (macroStruct->lastPos>i.macro->len) continue; + if (macroStruct->lastPoslastPos>255) continue; + if (!macroStruct->actualHad) continue; - doHighlight[macroStruct->lastPos-macroDragScroll]=true; - } + doHighlight[macroStruct->lastPos-macroDragScroll]=true; + } - if (i.isBitfield) { - PlotBitfield("##IMacro",asInt,totalFit,0,i.bitfieldBits,i.max,ImVec2(availableWidth,(i.macro->open)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),doHighlight); - } else { - PlotCustom("##IMacro",asFloat,totalFit,macroDragScroll,NULL,i.min+i.macro->vScroll,i.min+i.macro->vScroll+i.macro->vZoom,ImVec2(availableWidth,(i.macro->open)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),i.color,i.macro->len-macroDragScroll,i.hoverFunc,i.hoverFuncUser,i.blockMode,i.macro->open?genericGuide:NULL,doHighlight); - } - if (i.macro->open && (ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right))) { - macroDragStart=ImGui::GetItemRectMin(); - macroDragAreaSize=ImVec2(availableWidth,i.height*dpiScale); if (i.isBitfield) { - macroDragMin=i.min; - macroDragMax=i.max; + PlotBitfield("##IMacro",asInt,totalFit,0,i.bitfieldBits,i.max,ImVec2(availableWidth,(i.macro->open&1)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),doHighlight); } else { - macroDragMin=i.min+i.macro->vScroll; - macroDragMax=i.min+i.macro->vScroll+i.macro->vZoom; + PlotCustom("##IMacro",asFloat,totalFit,macroDragScroll,NULL,i.min+i.macro->vScroll,i.min+i.macro->vScroll+i.macro->vZoom,ImVec2(availableWidth,(i.macro->open&1)?(i.height*dpiScale):(32.0f*dpiScale)),sizeof(float),i.color,i.macro->len-macroDragScroll,i.hoverFunc,i.hoverFuncUser,i.blockMode,(i.macro->open&1)?genericGuide:NULL,doHighlight); } - macroDragBitOff=i.bitOffset; - macroDragBitMode=i.isBitfield; - macroDragInitialValueSet=false; - macroDragInitialValue=false; - macroDragLen=totalFit; - macroDragActive=true; - macroDragBit30=i.bit30; - macroDragSettingBit30=false; - macroDragTarget=i.macro->val; - macroDragChar=false; - macroDragLineMode=(i.isBitfield)?false:ImGui::IsItemClicked(ImGuiMouseButton_Right); - macroDragLineInitial=ImVec2(0,0); - lastMacroDesc=i; - processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); - } - if (i.macro->open) { - if (ImGui::IsItemHovered()) { - if (ctrlWheeling) { - if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { - i.macro->vZoom+=wheelY*(1+(i.macro->vZoom>>4)); - if (i.macro->vZoom<1) i.macro->vZoom=1; - if (i.macro->vZoom>(i.max-i.min)) i.macro->vZoom=i.max-i.min; - if ((i.macro->vScroll+i.macro->vZoom)>(i.max-i.min)) { - i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; - } - } else { - macroPointSize+=wheelY; - if (macroPointSize<1) macroPointSize=1; - if (macroPointSize>256) macroPointSize=256; - } - } else if ((ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) && wheelY!=0) { - i.macro->vScroll+=wheelY*(1+(i.macro->vZoom>>4)); - if (i.macro->vScroll<0) i.macro->vScroll=0; - if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; - } - } - - // slider - if (!i.isBitfield) { - if (settings.oldMacroVSlider) { - ImGui::SameLine(0.0f); - if (ImGui::VSliderInt("IMacroVScroll",ImVec2(20.0f*dpiScale,i.height*dpiScale),&i.macro->vScroll,0,(i.max-i.min)-i.macro->vZoom,"")) { - if (i.macro->vScroll<0) i.macro->vScroll=0; - if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; - } - if (ImGui::IsItemHovered() && ctrlWheeling) { - i.macro->vScroll+=wheelY*(1+(i.macro->vZoom>>4)); - if (i.macro->vScroll<0) i.macro->vScroll=0; - if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; - } + if ((i.macro->open&1) && (ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right))) { + macroDragStart=ImGui::GetItemRectMin(); + macroDragAreaSize=ImVec2(availableWidth,i.height*dpiScale); + if (i.isBitfield) { + macroDragMin=i.min; + macroDragMax=i.max; } else { - ImS64 scrollV=(i.max-i.min-i.macro->vZoom)-i.macro->vScroll; - ImS64 availV=i.macro->vZoom; - ImS64 contentsV=(i.max-i.min); - - ImGui::SameLine(0.0f); - ImGui::SetCursorPosX(ImGui::GetCursorPosX()-ImGui::GetStyle().ItemSpacing.x); - ImRect scrollbarPos=ImRect(ImGui::GetCursorScreenPos(),ImGui::GetCursorScreenPos()); - scrollbarPos.Max.x+=ImGui::GetStyle().ScrollbarSize; - scrollbarPos.Max.y+=i.height*dpiScale; - ImGui::Dummy(ImVec2(ImGui::GetStyle().ScrollbarSize,i.height*dpiScale)); - if (ImGui::IsItemHovered() && ctrlWheeling) { - i.macro->vScroll+=wheelY*(1+(i.macro->vZoom>>4)); - if (i.macro->vScroll<0) i.macro->vScroll=0; - if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; - } - - ImGuiID scrollbarID=ImGui::GetID("IMacroVScroll"); - ImGui::KeepAliveID(scrollbarID); - if (ImGui::ScrollbarEx(scrollbarPos,scrollbarID,ImGuiAxis_Y,&scrollV,availV,contentsV,0)) { - i.macro->vScroll=(i.max-i.min-i.macro->vZoom)-scrollV; - } + macroDragMin=i.min+i.macro->vScroll; + macroDragMax=i.min+i.macro->vScroll+i.macro->vZoom; } - } - - // bit 30 area - if (i.bit30) { - PlotCustom("##IMacroBit30",bit30Indicator,totalFit,macroDragScroll,NULL,0,1,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),i.color,i.macro->len-macroDragScroll,¯oHoverBit30); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - macroDragStart=ImGui::GetItemRectMin(); - macroDragAreaSize=ImVec2(availableWidth,12.0f*dpiScale); - macroDragInitialValueSet=false; - macroDragInitialValue=false; - macroDragLen=totalFit; - macroDragActive=true; - macroDragBit30=i.bit30; - macroDragSettingBit30=true; - macroDragTarget=i.macro->val; - macroDragChar=false; - macroDragLineMode=false; - macroDragLineInitial=ImVec2(0,0); - lastMacroDesc=i; - processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); - } - } - - // loop area - PlotCustom("##IMacroLoop",loopIndicator,totalFit,macroDragScroll,NULL,0,2,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),i.color,i.macro->len-macroDragScroll,¯oHoverLoop); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - macroLoopDragStart=ImGui::GetItemRectMin(); - macroLoopDragAreaSize=ImVec2(availableWidth,12.0f*dpiScale); - macroLoopDragLen=totalFit; - if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { - macroLoopDragTarget=&i.macro->rel; - } else { - macroLoopDragTarget=&i.macro->loop; - } - macroLoopDragActive=true; + macroDragBitOff=i.bitOffset; + macroDragBitMode=i.isBitfield; + macroDragInitialValueSet=false; + macroDragInitialValue=false; + macroDragLen=totalFit; + macroDragActive=true; + macroDragBit30=i.bit30; + macroDragSettingBit30=false; + macroDragTarget=i.macro->val; + macroDragChar=false; + macroDragLineMode=(i.isBitfield)?false:ImGui::IsItemClicked(ImGuiMouseButton_Right); + macroDragLineInitial=ImVec2(0,0); + lastMacroDesc=i; processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { - i.macro->rel=255; - } else { - i.macro->loop=255; + if ((i.macro->open&1)) { + if (ImGui::IsItemHovered()) { + if (ctrlWheeling) { + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + i.macro->vZoom+=wheelY*(1+(i.macro->vZoom>>4)); + if (i.macro->vZoom<1) i.macro->vZoom=1; + if (i.macro->vZoom>(i.max-i.min)) i.macro->vZoom=i.max-i.min; + if ((i.macro->vScroll+i.macro->vZoom)>(i.max-i.min)) { + i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; + } + } else { + macroPointSize+=wheelY; + if (macroPointSize<1) macroPointSize=1; + if (macroPointSize>256) macroPointSize=256; + } + } else if ((ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) && wheelY!=0) { + i.macro->vScroll+=wheelY*(1+(i.macro->vZoom>>4)); + if (i.macro->vScroll<0) i.macro->vScroll=0; + if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; + } + } + + // slider + if (!i.isBitfield) { + if (settings.oldMacroVSlider) { + ImGui::SameLine(0.0f); + if (ImGui::VSliderInt("IMacroVScroll",ImVec2(20.0f*dpiScale,i.height*dpiScale),&i.macro->vScroll,0,(i.max-i.min)-i.macro->vZoom,"")) { + if (i.macro->vScroll<0) i.macro->vScroll=0; + if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; + } + if (ImGui::IsItemHovered() && ctrlWheeling) { + i.macro->vScroll+=wheelY*(1+(i.macro->vZoom>>4)); + if (i.macro->vScroll<0) i.macro->vScroll=0; + if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; + } + } else { + ImS64 scrollV=(i.max-i.min-i.macro->vZoom)-i.macro->vScroll; + ImS64 availV=i.macro->vZoom; + ImS64 contentsV=(i.max-i.min); + + ImGui::SameLine(0.0f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX()-ImGui::GetStyle().ItemSpacing.x); + ImRect scrollbarPos=ImRect(ImGui::GetCursorScreenPos(),ImGui::GetCursorScreenPos()); + scrollbarPos.Max.x+=ImGui::GetStyle().ScrollbarSize; + scrollbarPos.Max.y+=i.height*dpiScale; + ImGui::Dummy(ImVec2(ImGui::GetStyle().ScrollbarSize,i.height*dpiScale)); + if (ImGui::IsItemHovered() && ctrlWheeling) { + i.macro->vScroll+=wheelY*(1+(i.macro->vZoom>>4)); + if (i.macro->vScroll<0) i.macro->vScroll=0; + if (i.macro->vScroll>((i.max-i.min)-i.macro->vZoom)) i.macro->vScroll=(i.max-i.min)-i.macro->vZoom; + } + + ImGuiID scrollbarID=ImGui::GetID("IMacroVScroll"); + ImGui::KeepAliveID(scrollbarID); + if (ImGui::ScrollbarEx(scrollbarPos,scrollbarID,ImGuiAxis_Y,&scrollV,availV,contentsV,0)) { + i.macro->vScroll=(i.max-i.min-i.macro->vZoom)-scrollV; + } + } + } + + // bit 30 area + if (i.bit30) { + PlotCustom("##IMacroBit30",bit30Indicator,totalFit,macroDragScroll,NULL,0,1,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),i.color,i.macro->len-macroDragScroll,¯oHoverBit30); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + macroDragStart=ImGui::GetItemRectMin(); + macroDragAreaSize=ImVec2(availableWidth,12.0f*dpiScale); + macroDragInitialValueSet=false; + macroDragInitialValue=false; + macroDragLen=totalFit; + macroDragActive=true; + macroDragBit30=i.bit30; + macroDragSettingBit30=true; + macroDragTarget=i.macro->val; + macroDragChar=false; + macroDragLineMode=false; + macroDragLineInitial=ImVec2(0,0); + lastMacroDesc=i; + processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); + } + } + + // loop area + PlotCustom("##IMacroLoop",loopIndicator,totalFit,macroDragScroll,NULL,0,2,ImVec2(availableWidth,12.0f*dpiScale),sizeof(float),i.color,i.macro->len-macroDragScroll,¯oHoverLoop); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + macroLoopDragStart=ImGui::GetItemRectMin(); + macroLoopDragAreaSize=ImVec2(availableWidth,12.0f*dpiScale); + macroLoopDragLen=totalFit; + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + macroLoopDragTarget=&i.macro->rel; + } else { + macroLoopDragTarget=&i.macro->loop; + } + macroLoopDragActive=true; + processDrags(ImGui::GetMousePos().x,ImGui::GetMousePos().y); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + i.macro->rel=255; + } else { + i.macro->loop=255; + } + } + ImGui::SetNextItemWidth(availableWidth); + String& mmlStr=mmlString[index]; + if (ImGui::InputText("##IMacroMML",&mmlStr)) { + decodeMMLStr(mmlStr,i.macro->val,i.macro->len,i.macro->loop,i.min,(i.isBitfield)?((1<<(i.isBitfield?i.max:0))-1):i.max,i.macro->rel,i.bit30); + } + if (!ImGui::IsItemActive()) { + encodeMMLStr(mmlStr,i.macro->val,i.macro->len,i.macro->loop,i.macro->rel,false,i.bit30); } } - ImGui::SetNextItemWidth(availableWidth); - String& mmlStr=mmlString[index]; - if (ImGui::InputText("##IMacroMML",&mmlStr)) { - decodeMMLStr(mmlStr,i.macro->val,i.macro->len,i.macro->loop,i.min,(i.isBitfield)?((1<<(i.isBitfield?i.max:0))-1):i.max,i.macro->rel,i.bit30); + ImGui::PopStyleVar(); + } else { + if (i.macro->open&2) { + if (ImGui::BeginTable("MacroADSR",4)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.3); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch,0.3); + //ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch,0.4); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Bottom"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##MABottom",&i.macro->val[0],1,16)) { PARAMETER + if (i.macro->val[0]val[0]=i.min; + if (i.macro->val[0]>i.max) i.macro->val[0]=i.max; + } + + ImGui::TableNextColumn(); + ImGui::Text("Top"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##MATop",&i.macro->val[1],1,16)) { PARAMETER + if (i.macro->val[1]val[1]=i.min; + if (i.macro->val[1]>i.max) i.macro->val[1]=i.max; + } + + /*ImGui::TableNextColumn(); + ImGui::Text("the envelope goes here");*/ + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Attack"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MAAR",&i.macro->val[2],0,255)) { PARAMETER + if (i.macro->val[2]<0) i.macro->val[2]=0; + if (i.macro->val[2]>255) i.macro->val[2]=255; + } + + ImGui::TableNextColumn(); + ImGui::Text("Sustain"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MASL",&i.macro->val[5],0,255)) { PARAMETER + if (i.macro->val[5]<0) i.macro->val[5]=0; + if (i.macro->val[5]>255) i.macro->val[5]=255; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Hold"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MAHT",&i.macro->val[3],0,255)) { PARAMETER + if (i.macro->val[3]<0) i.macro->val[3]=0; + if (i.macro->val[3]>255) i.macro->val[3]=255; + } + + ImGui::TableNextColumn(); + ImGui::Text("SusTime"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MAST",&i.macro->val[6],0,255)) { PARAMETER + if (i.macro->val[6]<0) i.macro->val[6]=0; + if (i.macro->val[6]>255) i.macro->val[6]=255; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Decay"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MADR",&i.macro->val[4],0,255)) { PARAMETER + if (i.macro->val[4]<0) i.macro->val[4]=0; + if (i.macro->val[4]>255) i.macro->val[4]=255; + } + + ImGui::TableNextColumn(); + ImGui::Text("SusDecay"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MASR",&i.macro->val[7],0,255)) { PARAMETER + if (i.macro->val[7]<0) i.macro->val[7]=0; + if (i.macro->val[7]>255) i.macro->val[7]=255; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + + ImGui::TableNextColumn(); + ImGui::Text("Release"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MARR",&i.macro->val[8],0,255)) { PARAMETER + if (i.macro->val[8]<0) i.macro->val[8]=0; + if (i.macro->val[8]>255) i.macro->val[8]=255; + } + + ImGui::EndTable(); + } } - if (!ImGui::IsItemActive()) { - encodeMMLStr(mmlStr,i.macro->val,i.macro->len,i.macro->loop,i.macro->rel,false,i.bit30); + if (i.macro->open&4) { + ImGui::Text("LFO..."); } } - ImGui::PopStyleVar(); ImGui::PopID(); index++; } From a965433bbae9631aac807426e4f49bad272ba84b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 14:17:25 -0500 Subject: [PATCH 36/57] start working on LFO macro mode --- papers/format.md | 12 ++++-------- src/engine/macroInt.cpp | 28 +++++++++++++++++++++++++--- src/engine/macroInt.h | 5 +++-- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/papers/format.md b/papers/format.md index ee76ce70..2649cd29 100644 --- a/papers/format.md +++ b/papers/format.md @@ -32,7 +32,8 @@ these fields are 0 in format versions prior to 100 (0.6pre1). the format versions are: -- 119: Furnace dev119 (still not released) +- 120: Furnace dev120 +- 119: Furnace dev119 - 118: Furnace dev118 - 117: Furnace dev117 - 116: Furnace 0.6pre1.5 @@ -441,7 +442,6 @@ notes: - 0: sequence (normal) - 1: ADSR - 2: LFO - - 3: ADSR+LFO - see sub-section for information on how to interpret parameters. - FM operator order is: - 1/3/2/4 (internal order) for OPN, OPM, OPZ and OPL 4-op @@ -1044,18 +1044,14 @@ size | description - `val[7]`: decay 2 - `val[8]`: release - LFO: - - `val[9]`: bottom - - `val[10]`: top - `val[11]`: speed - `val[12]`: waveform - 0: triangle - - 1: sine - - 2: saw - - 3: pulse + - 1: saw + - 2: pulse - `val[13]`: phase - `val[14]`: loop - `val[15]`: global (not sure how will I implement this) -- for ADSR+LFO just interpret both ADSR and LFO params. # wavetable diff --git a/src/engine/macroInt.cpp b/src/engine/macroInt.cpp index 90a4a6aa..c463e7b2 100644 --- a/src/engine/macroInt.cpp +++ b/src/engine/macroInt.cpp @@ -31,11 +31,18 @@ #define ADSR_SR source.val[7] #define ADSR_RR source.val[8] +#define LFO_SPEED source.val[11] +#define LFO_WAVE source.val[12] +#define LFO_PHASE source.val[13] +#define LFO_LOOP source.val[14] +#define LFO_GLOBAL source.val[15] + void DivMacroStruct::prepare(DivInstrumentMacro& source, DivEngine* e) { has=had=actualHad=will=true; mode=source.mode; type=(source.open>>1)&3; linger=(source.name=="vol" && e->song.volMacroLinger); + lfoPos=LFO_PHASE; } void DivMacroStruct::doMacro(DivInstrumentMacro& source, bool released, bool tick) { @@ -86,7 +93,7 @@ void DivMacroStruct::doMacro(DivInstrumentMacro& source, bool released, bool tic } } } - if (type==1 || type==3) { // ADSR + if (type==1) { // ADSR if (released && lastPos<3) lastPos=3; switch (lastPos) { case 0: // attack @@ -126,8 +133,23 @@ void DivMacroStruct::doMacro(DivInstrumentMacro& source, bool released, bool tic } val=ADSR_LOW+((pos+(ADSR_HIGH-ADSR_LOW)*pos)>>8); } - if (type==2 || type==3) { // LFO - + if (type==2) { // LFO + lfoPos+=LFO_SPEED; + lfoPos&=1023; + + int lfoOut=0; + switch (LFO_WAVE&3) { + case 0: // triangle + lfoOut=((lfoPos&512)?(1023-lfoPos):(lfoPos))>>1; + break; + case 1: // saw + lfoOut=lfoPos>>2; + break; + case 2: // pulse + lfoOut=(lfoPos&512)?255:0; + break; + } + val=ADSR_LOW+((lfoOut+(ADSR_HIGH-ADSR_LOW)*lfoOut)>>8); } } } diff --git a/src/engine/macroInt.h b/src/engine/macroInt.h index 64d81e1b..ac829a57 100644 --- a/src/engine/macroInt.h +++ b/src/engine/macroInt.h @@ -25,13 +25,13 @@ class DivEngine; struct DivMacroStruct { - int pos, lastPos, delay; + int pos, lastPos, lfoPos, delay; int val; bool has, had, actualHad, finished, will, linger, began; unsigned int mode, type; void doMacro(DivInstrumentMacro& source, bool released, bool tick); void init() { - pos=lastPos=mode=type=delay=0; + pos=lastPos=lfoPos=mode=type=delay=0; has=had=actualHad=will=false; linger=false; began=true; @@ -42,6 +42,7 @@ struct DivMacroStruct { DivMacroStruct(): pos(0), lastPos(0), + lfoPos(0), delay(0), val(0), has(false), From 3ffe2571586ec6f24ed19df40b547b5bd2ff4ff2 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 15:06:04 -0500 Subject: [PATCH 37/57] GUI: LFO macro UI --- src/gui/insEdit.cpp | 67 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index ab2312c5..3609bc2d 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -1348,8 +1348,8 @@ void FurnaceGUI::drawMacros(std::vector& macros) { } if (ImGui::Button(macroTypeLabels[(i.macro->open>>1)&3])) { i.macro->open+=2; - if (i.macro->open>=8) { - i.macro->open-=8; + if (i.macro->open>=6) { + i.macro->open-=6; } PARAMETER; } @@ -1364,9 +1364,6 @@ void FurnaceGUI::drawMacros(std::vector& macros) { case 4: ImGui::SetTooltip("Macro type: LFO"); break; - case 6: - ImGui::SetTooltip("Macro type: ADSR+LFO"); - break; default: ImGui::SetTooltip("Macro type: What's going on here?"); break; @@ -1705,7 +1702,65 @@ void FurnaceGUI::drawMacros(std::vector& macros) { } } if (i.macro->open&4) { - ImGui::Text("LFO..."); + if (ImGui::BeginTable("MacroLFO",4)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch,0.3); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch,0.3); + //ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch,0.4); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Bottom"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##MABottom",&i.macro->val[0],1,16)) { PARAMETER + if (i.macro->val[0]val[0]=i.min; + if (i.macro->val[0]>i.max) i.macro->val[0]=i.max; + } + + ImGui::TableNextColumn(); + ImGui::Text("Top"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt("##MATop",&i.macro->val[1],1,16)) { PARAMETER + if (i.macro->val[1]val[1]=i.min; + if (i.macro->val[1]>i.max) i.macro->val[1]=i.max; + } + + /*ImGui::TableNextColumn(); + ImGui::Text("the envelope goes here");*/ + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Speed"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MLSpeed",&i.macro->val[11],0,255)) { PARAMETER + if (i.macro->val[11]<0) i.macro->val[11]=0; + if (i.macro->val[11]>255) i.macro->val[11]=255; + } + + ImGui::TableNextColumn(); + ImGui::Text("Phase"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MLPhase",&i.macro->val[13],0,1023)) { PARAMETER + if (i.macro->val[13]<0) i.macro->val[13]=0; + if (i.macro->val[13]>1023) i.macro->val[13]=1023; + } + + ImGui::TableNextColumn(); + ImGui::Text("Shape"); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (CWSliderInt("##MLShape",&i.macro->val[12],0,2)) { PARAMETER + if (i.macro->val[12]<0) i.macro->val[12]=0; + if (i.macro->val[12]>2) i.macro->val[12]=2; + } + + ImGui::EndTable(); + } } } ImGui::PopID(); From 66234df6368647d591fa0f50db699db5235b376d Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 16:47:18 -0500 Subject: [PATCH 38/57] finish work on ADSR/LFO macro type --- src/engine/instrument.h | 6 +++++- src/engine/macroInt.cpp | 9 ++++++++- src/gui/insEdit.cpp | 32 +++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 6587fecc..a63b2975 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -182,6 +182,8 @@ struct DivInstrumentMacro { // the following variables are used by the GUI and not saved in the file int vScroll, vZoom; + int typeMemory[16]; + unsigned char lenMemory; explicit DivInstrumentMacro(const String& n, bool initOpen=false): name(n), @@ -193,8 +195,10 @@ struct DivInstrumentMacro { loop(255), rel(255), vScroll(0), - vZoom(-1) { + vZoom(-1), + lenMemory(0) { memset(val,0,256*sizeof(int)); + memset(typeMemory,0,16*sizeof(int)); } }; diff --git a/src/engine/macroInt.cpp b/src/engine/macroInt.cpp index c463e7b2..9e619cb8 100644 --- a/src/engine/macroInt.cpp +++ b/src/engine/macroInt.cpp @@ -332,7 +332,14 @@ void DivMacroInt::init(DivInstrument* which) { for (size_t i=0; iprepare(*macroSource[i],e); - hasRelease=(macroSource[i]->rellen); + // check ADSR mode + if ((macroSource[i]->open&6)==4) { + hasRelease=false; + } else if ((macroSource[i]->open&6)==2) { + hasRelease=true; + } else { + hasRelease=(macroSource[i]->rellen); + } } else { hasRelease=false; } diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 3609bc2d..166bd8a5 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -210,6 +210,10 @@ const char* macroTypeLabels[4]={ ICON_FA_SIGN_OUT "##IMacroType" }; +const char* macroLFOShapes[4]={ + "Triangle", "Saw", "Square", "How did you even" +}; + const char* fmOperatorBits[5]={ "op1", "op2", "op3", "op4", NULL }; @@ -1347,10 +1351,33 @@ void FurnaceGUI::drawMacros(std::vector& macros) { } } if (ImGui::Button(macroTypeLabels[(i.macro->open>>1)&3])) { + unsigned char prevOpen=i.macro->open; i.macro->open+=2; if (i.macro->open>=6) { i.macro->open-=6; } + + // check whether macro type is now ADSR/LFO or sequence + if (((prevOpen&6)?1:0)!=((i.macro->open&6)?1:0)) { + // swap memory + // this way the macro isn't corrupted if the user decides to go + // back to sequence mode + i.macro->len^=i.macro->lenMemory; + i.macro->lenMemory^=i.macro->len; + i.macro->len^=i.macro->lenMemory; + + for (int j=0; j<16; j++) { + i.macro->val[j]^=i.macro->typeMemory[j]; + i.macro->typeMemory[j]^=i.macro->val[j]; + i.macro->val[j]^=i.macro->typeMemory[j]; + } + + // if ADSR/LFO, populate min/max + if (i.macro->open&6) { + i.macro->val[0]=i.min; + i.macro->val[1]=i.max; + } + } PARAMETER; } if (ImGui::IsItemHovered()) { @@ -1369,6 +1396,9 @@ void FurnaceGUI::drawMacros(std::vector& macros) { break; } } + if (i.macro->open&6) { + i.macro->len=16; + } ImGui::SameLine(); ImGui::Button(ICON_FA_ELLIPSIS_H "##IMacroSet"); if (ImGui::IsItemHovered()) { @@ -1754,7 +1784,7 @@ void FurnaceGUI::drawMacros(std::vector& macros) { ImGui::Text("Shape"); ImGui::TableNextColumn(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (CWSliderInt("##MLShape",&i.macro->val[12],0,2)) { PARAMETER + if (CWSliderInt("##MLShape",&i.macro->val[12],0,2,macroLFOShapes[i.macro->val[12]&3])) { PARAMETER if (i.macro->val[12]<0) i.macro->val[12]=0; if (i.macro->val[12]>2) i.macro->val[12]=2; } From a73ccdae41360ef3e4e7e316037bdbd4f390ad55 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 17:11:13 -0500 Subject: [PATCH 39/57] GUI: fix paste not updating sel if cursor moves --- src/gui/editing.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 153d70da..17d2886a 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -565,6 +565,8 @@ void FurnaceGUI::doPaste(PasteMode mode, int arg) { if (settings.cursorPastePos) { cursor.y=j; if (cursor.y>=e->curSubSong->patLen) cursor.y=e->curSubSong->patLen-1; + selStart=cursor; + selEnd=cursor; updateScroll(cursor.y); } From 80f8ccf627455d8283ee73b9a51a590796bd53ed Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 17:21:53 -0500 Subject: [PATCH 40/57] C64: partially fix wave after gate --- src/engine/platform/c64.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/engine/platform/c64.cpp b/src/engine/platform/c64.cpp index 6e98fd8a..9fb10d41 100644 --- a/src/engine/platform/c64.cpp +++ b/src/engine/platform/c64.cpp @@ -129,18 +129,6 @@ void DivPlatformC64::tick(bool sysTick) { rWrite(i*7+2,chan[i].duty&0xff); rWrite(i*7+3,chan[i].duty>>8); } - if (sysTick) { - if (chan[i].testWhen>0) { - if (--chan[i].testWhen<1) { - if (!chan[i].resetMask && !chan[i].inPorta) { - DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_C64); - rWrite(i*7+5,0); - rWrite(i*7+6,0); - rWrite(i*7+4,(chan[i].wave<<4)|(ins->c64.noTest?0:8)|(chan[i].test<<3)|(chan[i].ring<<2)|(chan[i].sync<<1)); - } - } - } - } if (chan[i].std.wave.had) { chan[i].wave=chan[i].std.wave.val; rWrite(i*7+4,(chan[i].wave<<4)|(chan[i].test<<3)|(chan[i].ring<<2)|(chan[i].sync<<1)|(int)(chan[i].active)); @@ -173,6 +161,19 @@ void DivPlatformC64::tick(bool sysTick) { rWrite(i*7+4,(chan[i].wave<<4)|(chan[i].test<<3)|(chan[i].ring<<2)|(chan[i].sync<<1)|(int)(chan[i].active)); } + if (sysTick) { + if (chan[i].testWhen>0) { + if (--chan[i].testWhen<1) { + if (!chan[i].resetMask && !chan[i].inPorta) { + DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_C64); + rWrite(i*7+5,0); + rWrite(i*7+6,0); + rWrite(i*7+4,(chan[i].wave<<4)|(ins->c64.noTest?0:8)|(chan[i].test<<3)|(chan[i].ring<<2)|(chan[i].sync<<1)); + } + } + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,8,chan[i].pitch2,chipClock,CHIP_FREQBASE); if (chan[i].freq>0xffff) chan[i].freq=0xffff; From 55c97c9529c28e54b3e27e635cd031bc07216107 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 18:14:25 -0500 Subject: [PATCH 41/57] GUI: fx rgtclk if cntr pat opt & RC n left win gap --- src/gui/pattern.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index fb65cee8..385441d4 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -1210,7 +1210,7 @@ void FurnaceGUI::drawPattern() { } ImGui::PopStyleVar(); if (patternOpen) { - if (!inhibitMenu && ImGui::IsItemClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("patternActionMenu"); + if (!inhibitMenu && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ImGui::OpenPopup("patternActionMenu"); if (ImGui::BeginPopup("patternActionMenu",ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) { editOptions(false); ImGui::EndPopup(); From 5726ffc7409f34ffd5bc70daeb0c94f418d77da6 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 7 Oct 2022 23:37:56 -0500 Subject: [PATCH 42/57] Game Boy: fix porta regression --- src/engine/platform/gb.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/engine/platform/gb.cpp b/src/engine/platform/gb.cpp index 8a4cabc4..6a88d0f7 100644 --- a/src/engine/platform/gb.cpp +++ b/src/engine/platform/gb.cpp @@ -169,7 +169,9 @@ void DivPlatformGB::tick(bool sysTick) { if (chan[i].baseFreq>255) chan[i].baseFreq=255; if (chan[i].baseFreq<0) chan[i].baseFreq=0; } else { - chan[i].baseFreq=NOTE_PERIODIC(parent->calcArp(chan[i].note,chan[i].std.arp.val,24)); + if (!chan[i].inPorta) { + chan[i].baseFreq=NOTE_PERIODIC(parent->calcArp(chan[i].note,chan[i].std.arp.val,24)); + } } chan[i].freqChanged=true; } From ea7f8e11549d75426d7f42f35dfc38efa1db39e1 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 00:01:26 -0500 Subject: [PATCH 43/57] NES/SN: Defle compat fixes --- src/engine/platform/nes.cpp | 10 +++++----- src/engine/platform/sms.cpp | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 5749167b..1ca95245 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -415,11 +415,11 @@ int DivPlatformNES::dispatch(DivCommand c) { chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_STD)); if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; - } - if (c.chan==2) { - rWrite(0x4000+c.chan*4,0xff); - } else { - rWrite(0x4000+c.chan*4,0x30|chan[c.chan].vol|((chan[c.chan].duty&3)<<6)); + if (c.chan==2) { + rWrite(0x4000+c.chan*4,0xff); + } else { + rWrite(0x4000+c.chan*4,0x30|chan[c.chan].vol|((chan[c.chan].duty&3)<<6)); + } } break; case DIV_CMD_NOTE_OFF: diff --git a/src/engine/platform/sms.cpp b/src/engine/platform/sms.cpp index ffafc0cb..5e484c5d 100644 --- a/src/engine/platform/sms.cpp +++ b/src/engine/platform/sms.cpp @@ -19,6 +19,7 @@ #include "sms.h" #include "../engine.h" +#include "../../ta-log.h" #include #define rWrite(a,v) {if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(0x200+a,v);}}} @@ -248,7 +249,9 @@ int DivPlatformSMS::dispatch(DivCommand c) { chan[c.chan].actualNote=c.value; } chan[c.chan].active=true; - rWrite(0,0x90|c.chan<<5|(isMuted[c.chan]?15:(15-(chan[c.chan].vol&15)))); + if (!parent->song.brokenOutVol) { + rWrite(0,0x90|c.chan<<5|(isMuted[c.chan]?15:(15-(chan[c.chan].vol&15)))); + } chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_STD)); if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; From 75d75f68e6de77cdc8a54beb24598e4d6e9b88a5 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 00:53:01 -0500 Subject: [PATCH 44/57] dev121 - NES/SN: Defle compat fixes FOR REAL THIS IS MOST LIKELY THE LAST DEFLE COMPAT FLAG I ADD ...besides future "no arp+porta in linear pitch" compat flag --- papers/format.md | 5 ++++- src/engine/fileOps.cpp | 16 +++++++++++++--- src/engine/platform/nes.cpp | 2 ++ src/engine/platform/sms.cpp | 2 +- src/engine/song.h | 2 ++ src/gui/compatFlags.cpp | 4 ++++ 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/papers/format.md b/papers/format.md index 2649cd29..6e4954e8 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: +- 121: Furnace dev121 - 120: Furnace dev120 - 119: Furnace dev119 - 118: Furnace dev118 @@ -351,7 +352,9 @@ size | description 1 | 0B/0D effect treatment (>=113) or reserved 1 | automatic system name detection (>=115) or reserved | - this one isn't a compatibility flag, but it's here for convenience... - 3 | reserved + 1 | disable sample macro (>=117) or reserved + 1 | broken outVol episode 2 (>=121) or reserved + 1 | reserved --- | **virtual tempo data** 2 | virtual tempo numerator of first song (>=96) or reserved 2 | virtual tempo denominator of first song (>=96) or reserved diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index c2888e34..41ec55e6 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -174,7 +174,8 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.noOPN2Vol=true; ds.newVolumeScaling=false; ds.volMacroLinger=false; - ds.brokenOutVol=true; // ??? + ds.brokenOutVol=true; + ds.brokenOutVol2=true; ds.e1e2StopOnSameNote=true; ds.brokenPortaArp=false; ds.snNoLowPeriods=true; @@ -1689,6 +1690,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<117) { ds.disableSampleMacro=true; } + if (ds.version<121) { + ds.brokenOutVol2=false; + } ds.isDMF=false; reader.readS(); // reserved @@ -2126,7 +2130,12 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } else { reader.readC(); } - for (int i=0; i<2; i++) { + if (ds.version>=121) { + ds.brokenOutVol2=reader.readC(); + } else { + reader.readC(); + } + for (int i=0; i<1; i++) { reader.readC(); } } @@ -4464,7 +4473,8 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { w->writeC(song.jumpTreatment); w->writeC(song.autoSystem); w->writeC(song.disableSampleMacro); - for (int i=0; i<2; i++) { + w->writeC(song.brokenOutVol2); + for (int i=0; i<1; i++) { w->writeC(0); } diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 1ca95245..a338cca8 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -415,6 +415,8 @@ int DivPlatformNES::dispatch(DivCommand c) { chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_STD)); if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; + } + if (!parent->song.brokenOutVol2) { if (c.chan==2) { rWrite(0x4000+c.chan*4,0xff); } else { diff --git a/src/engine/platform/sms.cpp b/src/engine/platform/sms.cpp index 5e484c5d..83ce707f 100644 --- a/src/engine/platform/sms.cpp +++ b/src/engine/platform/sms.cpp @@ -249,7 +249,7 @@ int DivPlatformSMS::dispatch(DivCommand c) { chan[c.chan].actualNote=c.value; } chan[c.chan].active=true; - if (!parent->song.brokenOutVol) { + if (!parent->song.brokenOutVol2) { rWrite(0,0x90|c.chan<<5|(isMuted[c.chan]?15:(15-(chan[c.chan].vol&15)))); } chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_STD)); diff --git a/src/engine/song.h b/src/engine/song.h index ba66337b..f87160aa 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -314,6 +314,7 @@ struct DivSong { bool newVolumeScaling; bool volMacroLinger; bool brokenOutVol; + bool brokenOutVol2; bool e1e2StopOnSameNote; bool brokenPortaArp; bool snNoLowPeriods; @@ -420,6 +421,7 @@ struct DivSong { newVolumeScaling(true), volMacroLinger(true), brokenOutVol(false), + brokenOutVol2(false), e1e2StopOnSameNote(false), brokenPortaArp(false), snNoLowPeriods(false), diff --git a/src/gui/compatFlags.cpp b/src/gui/compatFlags.cpp index fcea0fa0..e3ad642c 100644 --- a/src/gui/compatFlags.cpp +++ b/src/gui/compatFlags.cpp @@ -139,6 +139,10 @@ void FurnaceGUI::drawCompatFlags() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("if enabled, no checks for the presence of a volume macro will be made.\nthis will cause the last macro value to linger unless a value in the volume column is present."); } + ImGui::Checkbox("Broken output volume - Episode 2 (PLEASE KEEP ME DISABLED)",&e->song.brokenOutVol2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("these compatibility flags are getting SO damn ridiculous and out of control.\nas you may have guessed, this one exists due to yet ANOTHER DefleMask-specific behavior.\nplease keep this off at all costs, because I will not support it when ROM export comes.\noh, and don't start an argument out of it. Furnace isn't a DefleMask replacement, and no,\nI am not trying to make it look like one with all these flags.\n\noh, and what about the other flags that don't have to do with DefleMask?\nthose are for .mod import, future FamiTracker import and personal taste!\n\nend of rant"); + } ImGui::Checkbox("Treat SN76489 periods under 8 as 1",&e->song.snNoLowPeriods); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("when enabled, any SN period under 8 will be written as 1 instead.\nthis replicates DefleMask behavior, but reduces available period range."); From bf75603c23ae07990c2d182507287a878c778500 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 00:53:58 -0500 Subject: [PATCH 45/57] oh wait I forgot to update version number! --- src/engine/engine.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/engine.h b/src/engine/engine.h index 4a7e894a..e524f0a4 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 "dev120" -#define DIV_ENGINE_VERSION 120 +#define DIV_VERSION "dev121" +#define DIV_ENGINE_VERSION 121 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 From 74ca8bbd2c1d045edb83a63d2d5df8b9c6d28d72 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 01:16:44 -0500 Subject: [PATCH 46/57] demos/Solar_Man_Genesis.fur submitted by brickblock369 --- demos/Solar_Man_Genesis.fur | Bin 0 -> 101402 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 demos/Solar_Man_Genesis.fur diff --git a/demos/Solar_Man_Genesis.fur b/demos/Solar_Man_Genesis.fur new file mode 100644 index 0000000000000000000000000000000000000000..a50427a8758d1422c9274d91026248bf127ca6f8 GIT binary patch literal 101402 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

P-22*#H>=B+-0kqs&HIC%TrT~+ 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 YG)$YePtv~Jn+a literal 0 HcmV?d00001 From d3718ccaf0e42d2f0d3db5ce5e17f85c567b34e4 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 02:03:09 -0500 Subject: [PATCH 47/57] YMU759: fix 4-op ins loading in .dmf --- src/engine/fileOps.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 41ec55e6..a13182e3 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -494,6 +494,19 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ins->fm.op[j].ssgEnv ); } + + // swap alg operator 2 and 3 if YMU759 + if (ds.system[0]==DIV_SYSTEM_YMU759 && ins->fm.ops==4) { + DivInstrumentFM::Operator oldOp=ins->fm.op[2]; + ins->fm.op[2]=ins->fm.op[1]; + ins->fm.op[1]=oldOp; + + if (ins->fm.alg==1) { + ins->fm.alg=2; + } else if (ins->fm.alg==2) { + ins->fm.alg=1; + } + } } else { // STD if (ds.system[0]!=DIV_SYSTEM_GB || ds.version<0x12) { ins->std.volMacro.len=reader.readC(); From f169ffa8dc3ce1f927711970d3497d656b9255c2 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 17:41:34 -0500 Subject: [PATCH 48/57] GUI: fix assert fail when removing ins thru rgtclk --- src/gui/dataList.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp index 7199e513..9a1740be 100644 --- a/src/gui/dataList.cpp +++ b/src/gui/dataList.cpp @@ -436,9 +436,11 @@ void FurnaceGUI::drawInsList(bool asChild) { } } if (i>=0) { - DivInstrument* ins=e->song.ins[i]; - ImGui::SameLine(); - ImGui::Text("%.2X: %s",i,ins->name.c_str()); + if (i<(int)e->song.ins.size()) { + DivInstrument* ins=e->song.ins[i]; + ImGui::SameLine(); + ImGui::Text("%.2X: %s",i,ins->name.c_str()); + } } else { ImGui::SameLine(); ImGui::Text("- None -"); From 70361c44caf9fc00abf806bd1247c5cfe57f1d46 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 17:57:14 -0500 Subject: [PATCH 49/57] GUI: editor improvements when no asset is selected --- src/gui/insEdit.cpp | 6 ++++++ src/gui/sampleEdit.cpp | 36 ++++++++++++++++++++++++++++++++++++ src/gui/waveEdit.cpp | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 166bd8a5..101b71ad 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -1940,6 +1940,12 @@ void FurnaceGUI::drawInsEdit() { ImGui::TextUnformatted("or"); ImGui::SameLine(); } + if (ImGui::Button("Open")) { + doAction(GUI_ACTION_INS_LIST_OPEN); + } + ImGui::SameLine(); + ImGui::TextUnformatted("or"); + ImGui::SameLine(); if (ImGui::Button("Create New")) { doAction(GUI_ACTION_INS_LIST_ADD); } diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index aa3c4cb3..ac784542 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -28,6 +28,9 @@ #include "sampleUtil.h" #include "util.h" +#define CENTER_TEXT(text) \ + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+0.5*(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(text).x)); + void FurnaceGUI::drawSampleEdit() { if (nextWindow==GUI_WINDOW_SAMPLE_EDIT) { sampleEditOpen=true; @@ -43,7 +46,40 @@ void FurnaceGUI::drawSampleEdit() { } if (ImGui::Begin("Sample Editor",&sampleEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking))) { if (curSample<0 || curSample>=(int)e->song.sample.size()) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()*2.0f)*0.5f); + CENTER_TEXT("no sample selected"); ImGui::Text("no sample selected"); + if (ImGui::BeginTable("noAssetCenter",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.5f); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.5f); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + + if (e->song.sample.size()>0) { + if (ImGui::BeginCombo("##SampleSelect","select one...")) { + actualSampleList(); + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::TextUnformatted("or"); + ImGui::SameLine(); + } + if (ImGui::Button("Open")) { + doAction(GUI_ACTION_SAMPLE_LIST_OPEN); + } + ImGui::SameLine(); + ImGui::TextUnformatted("or"); + ImGui::SameLine(); + if (ImGui::Button("Create New")) { + doAction(GUI_ACTION_SAMPLE_LIST_ADD); + } + + ImGui::TableNextColumn(); + ImGui::EndTable(); + } } else { DivSample* sample=e->song.sample[curSample]; String sampleType="Invalid"; diff --git a/src/gui/waveEdit.cpp b/src/gui/waveEdit.cpp index 87f5ad38..b9b8d65b 100644 --- a/src/gui/waveEdit.cpp +++ b/src/gui/waveEdit.cpp @@ -22,6 +22,7 @@ #include "plot_nolerp.h" #include "IconsFontAwesome4.h" #include "misc/cpp/imgui_stdlib.h" +#include #include #include @@ -175,7 +176,40 @@ void FurnaceGUI::drawWaveEdit() { } if (ImGui::Begin("Wavetable Editor",&waveEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking))) { if (curWave<0 || curWave>=(int)e->song.wave.size()) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()*2.0f)*0.5f); + CENTER_TEXT("no wavetable selected"); ImGui::Text("no wavetable selected"); + if (ImGui::BeginTable("noAssetCenter",3)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch,0.5f); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch,0.5f); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + + if (e->song.wave.size()>0) { + if (ImGui::BeginCombo("##WaveSelect","select one...")) { + actualWaveList(); + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::TextUnformatted("or"); + ImGui::SameLine(); + } + if (ImGui::Button("Open")) { + doAction(GUI_ACTION_WAVE_LIST_OPEN); + } + ImGui::SameLine(); + ImGui::TextUnformatted("or"); + ImGui::SameLine(); + if (ImGui::Button("Create New")) { + doAction(GUI_ACTION_WAVE_LIST_ADD); + } + + ImGui::TableNextColumn(); + ImGui::EndTable(); + } } else { DivWavetable* wave=e->song.wave[curWave]; From f76e4044c77150ff888de85d0047305ea524f1c4 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 19:37:22 -0500 Subject: [PATCH 50/57] prepare for Virtual Boy --- CMakeLists.txt | 3 + src/engine/dispatchContainer.cpp | 4 + src/engine/platform/sound/t6w28/T6W28_Apu.cpp | 431 +++++++++++++ src/engine/platform/sound/t6w28/T6W28_Apu.h | 94 +++ src/engine/platform/sound/t6w28/T6W28_Oscs.h | 50 ++ src/engine/platform/sound/vsu.cpp | 499 +++++++++++++++ src/engine/platform/sound/vsu.h | 97 +++ src/engine/platform/vb.cpp | 604 ++++++++++++++++++ src/engine/platform/vb.h | 121 ++++ src/engine/sysDef.cpp | 4 +- src/gui/guiConst.cpp | 2 + 11 files changed, 1908 insertions(+), 1 deletion(-) create mode 100644 src/engine/platform/sound/t6w28/T6W28_Apu.cpp create mode 100644 src/engine/platform/sound/t6w28/T6W28_Apu.h create mode 100644 src/engine/platform/sound/t6w28/T6W28_Oscs.h create mode 100644 src/engine/platform/sound/vsu.cpp create mode 100644 src/engine/platform/sound/vsu.h create mode 100644 src/engine/platform/vb.cpp create mode 100644 src/engine/platform/vb.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ca10cf91..105b576d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -433,6 +433,8 @@ src/engine/platform/sound/vic20sound.c src/engine/platform/sound/ymz280b.cpp +src/engine/platform/sound/vsu.cpp + src/engine/platform/sound/rf5c68.cpp src/engine/platform/sound/oki/msm5232.cpp @@ -504,6 +506,7 @@ src/engine/platform/x1_010.cpp src/engine/platform/lynx.cpp src/engine/platform/su.cpp src/engine/platform/swan.cpp +src/engine/platform/vb.cpp src/engine/platform/vera.cpp src/engine/platform/zxbeeper.cpp src/engine/platform/bubsyswsg.cpp diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 28b4114a..db985a9a 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -67,6 +67,7 @@ #include "platform/ymz280b.h" #include "platform/rf5c68.h" #include "platform/snes.h" +#include "platform/vb.h" #include "platform/pcmdac.h" #include "platform/dummy.h" #include "../ta-log.h" @@ -342,6 +343,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_SWAN: dispatch=new DivPlatformSwan; break; + case DIV_SYSTEM_VBOY: + dispatch=new DivPlatformVB; + break; case DIV_SYSTEM_VERA: dispatch=new DivPlatformVERA; break; diff --git a/src/engine/platform/sound/t6w28/T6W28_Apu.cpp b/src/engine/platform/sound/t6w28/T6W28_Apu.cpp new file mode 100644 index 00000000..4414f619 --- /dev/null +++ b/src/engine/platform/sound/t6w28/T6W28_Apu.cpp @@ -0,0 +1,431 @@ +// T6W28_Snd_Emu + +#include "T6W28_Apu.h" + +#undef require +#define require( expr ) assert( expr ) + +/* Copyright (C) 2003-2006 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module 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 Lesser General Public License for +more details. You should have received a copy of the GNU Lesser General +Public License along with this module; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +// T6W28_Osc + +namespace MDFN_IEN_NGP +{ + +T6W28_Osc::T6W28_Osc() +{ + outputs [0] = NULL; // always stays NULL + outputs [1] = NULL; + outputs [2] = NULL; + outputs [3] = NULL; +} + +void T6W28_Osc::reset() +{ + delay = 0; + last_amp_left = 0; + last_amp_right = 0; + + volume_left = 0; + volume_right = 0; +} + +// T6W28_Square + +blip_inline void T6W28_Square::reset() +{ + period = 0; + phase = 0; + T6W28_Osc::reset(); +} + +void T6W28_Square::run( sms_time_t time, sms_time_t end_time ) +{ + if ((!volume_left && !volume_right) || period <= 128 ) + { + // ignore 16kHz and higher + if ( last_amp_left ) + { + synth->offset( time, -last_amp_left, outputs[2] ); + last_amp_left = 0; + } + + if ( last_amp_right ) + { + synth->offset( time, -last_amp_right, outputs[1] ); + last_amp_right = 0; + } + + time += delay; + if ( !period ) + { + time = end_time; + } + else if ( time < end_time ) + { + // keep calculating phase + int count = (end_time - time + period - 1) / period; + phase = (phase + count) & 1; + time += count * period; + } + } + else + { + int amp_left = phase ? volume_left : -volume_left; + int amp_right = phase ? volume_right : -volume_right; + + { + int delta_left = amp_left - last_amp_left; + int delta_right = amp_right - last_amp_right; + + if ( delta_left ) + { + last_amp_left = amp_left; + synth->offset( time, delta_left, outputs[2] ); + } + + if ( delta_right ) + { + last_amp_right = amp_right; + synth->offset( time, delta_right, outputs[1] ); + } + } + + time += delay; + if ( time < end_time ) + { + Blip_Buffer* const output_left = this->outputs[2]; + Blip_Buffer* const output_right = this->outputs[1]; + + int delta_left = amp_left * 2; + int delta_right = amp_right * 2; + do + { + delta_left = -delta_left; + delta_right = -delta_right; + + synth->offset_inline( time, delta_left, output_left ); + synth->offset_inline( time, delta_right, output_right ); + time += period; + phase ^= 1; + } + while ( time < end_time ); + + this->last_amp_left = phase ? volume_left : -volume_left; + this->last_amp_right = phase ? volume_right : -volume_right; + } + } + delay = time - end_time; +} + +// T6W28_Noise + +static const int noise_periods [3] = { 0x100, 0x200, 0x400 }; + +blip_inline void T6W28_Noise::reset() +{ + period = &noise_periods [0]; + shifter = 0x4000; + tap = 13; + period_extra = 0; + T6W28_Osc::reset(); +} + +void T6W28_Noise::run( sms_time_t time, sms_time_t end_time ) +{ + int amp_left = volume_left; + int amp_right = volume_right; + + if ( shifter & 1 ) + { + amp_left = -amp_left; + amp_right = -amp_right; + } + + { + int delta_left = amp_left - last_amp_left; + int delta_right = amp_right - last_amp_right; + + if ( delta_left ) + { + last_amp_left = amp_left; + synth.offset( time, delta_left, outputs[2] ); + } + + if ( delta_right ) + { + last_amp_right = amp_right; + synth.offset( time, delta_right, outputs[1] ); + } + } + + time += delay; + + if ( !volume_left && !volume_right ) + time = end_time; + + if ( time < end_time ) + { + Blip_Buffer* const output_left = this->outputs[2]; + Blip_Buffer* const output_right = this->outputs[1]; + + unsigned l_shifter = this->shifter; + int delta_left = amp_left * 2; + int delta_right = amp_right * 2; + + int l_period = *this->period * 2; + if ( !l_period ) + l_period = 16; + + do + { + int changed = (l_shifter + 1) & 2; // set if prev and next bits differ + l_shifter = (((l_shifter << 14) ^ (l_shifter << tap)) & 0x4000) | (l_shifter >> 1); + if ( changed ) + { + delta_left = -delta_left; + synth.offset_inline( time, delta_left, output_left ); + + delta_right = -delta_right; + synth.offset_inline( time, delta_right, output_right ); + } + time += l_period; + } + while ( time < end_time ); + + this->shifter = l_shifter; + this->last_amp_left = delta_left >> 1; + this->last_amp_right = delta_right >> 1; + } + delay = time - end_time; +} + +// T6W28_Apu + +T6W28_Apu::T6W28_Apu() +{ + for ( int i = 0; i < 3; i++ ) + { + squares [i].synth = &square_synth; + oscs [i] = &squares [i]; + } + oscs [3] = &noise; + + volume( 1.0 ); + reset(); +} + +T6W28_Apu::~T6W28_Apu() +{ +} + +void T6W28_Apu::volume( double vol ) +{ + vol *= 0.85 / (osc_count * 64 * 2); + square_synth.volume( vol ); + noise.synth.volume( vol ); +} + +void T6W28_Apu::treble_eq( const blip_eq_t& eq ) +{ + square_synth.treble_eq( eq ); + noise.synth.treble_eq( eq ); +} + +void T6W28_Apu::osc_output( int index, Blip_Buffer* center, Blip_Buffer* left, Blip_Buffer* right ) +{ + require( (unsigned) index < osc_count ); + require( (center && left && right) || (!center && !left && !right) ); + T6W28_Osc& osc = *oscs [index]; + osc.outputs [1] = right; + osc.outputs [2] = left; + osc.outputs [3] = center; +} + +void T6W28_Apu::output( Blip_Buffer* center, Blip_Buffer* left, Blip_Buffer* right ) +{ + for ( int i = 0; i < osc_count; i++ ) + osc_output( i, center, left, right ); +} + +void T6W28_Apu::reset() +{ + last_time = 0; + latch_left = 0; + latch_right = 0; + + squares [0].reset(); + squares [1].reset(); + squares [2].reset(); + noise.reset(); +} + +void T6W28_Apu::run_until( sms_time_t end_time ) +{ + require( end_time >= last_time ); // end_time must not be before previous time + + if ( end_time > last_time ) + { + // run oscillators + for ( int i = 0; i < osc_count; ++i ) + { + T6W28_Osc& osc = *oscs [i]; + if ( osc.outputs[1] ) + { + if ( i < 3 ) + squares [i].run( last_time, end_time ); + else + noise.run( last_time, end_time ); + } + } + + last_time = end_time; + } +} + +bool T6W28_Apu::end_frame( sms_time_t end_time ) +{ + if ( end_time > last_time ) + run_until( end_time ); + + assert( last_time >= end_time ); + last_time -= end_time; + + return(1); +} + +static const unsigned char volumes [16] = { + // volumes [i] = 64 * pow( 1.26, 15 - i ) / pow( 1.26, 15 ) + 64, 50, 39, 31, 24, 19, 15, 12, 9, 7, 5, 4, 3, 2, 1, 0 +}; + +void T6W28_Apu::write_data_left( sms_time_t time, int data ) +{ + require( (unsigned) data <= 0xFF ); + + run_until( time ); + + if ( data & 0x80 ) + latch_left = data; + + int index = (latch_left >> 5) & 3; + + if ( latch_left & 0x10 ) + { + oscs [index]->volume_left = volumes [data & 15]; + } + else if ( index < 3 ) + { + T6W28_Square& sq = squares [index]; + if ( data & 0x80 ) + sq.period = (sq.period & 0xFF00) | (data << 4 & 0x00FF); + else + sq.period = (sq.period & 0x00FF) | (data << 8 & 0x3F00); + } +} + +void T6W28_Apu::write_data_right( sms_time_t time, int data ) +{ + require( (unsigned) data <= 0xFF ); + + run_until( time ); + + if ( data & 0x80 ) + latch_right = data; + + int index = (latch_right >> 5) & 3; + //printf("%d\n", index); + + if ( latch_right & 0x10 ) + { + oscs [index]->volume_right = volumes [data & 15]; + } + else if ( index == 2 ) + { + if ( data & 0x80 ) + noise.period_extra = (noise.period_extra & 0xFF00) | (data << 4 & 0x00FF); + else + noise.period_extra = (noise.period_extra & 0x00FF) | (data << 8 & 0x3F00); + } + else if(index == 3) + { + int select = data & 3; + if ( select < 3 ) + noise.period = &noise_periods [select]; + else + noise.period = &noise.period_extra; + + int const tap_disabled = 16; + noise.tap = (data & 0x04) ? 13 : tap_disabled; + noise.shifter = 0x4000; + } +} + + +void T6W28_Apu::save_state(T6W28_ApuState *ret) +{ + for(int x = 0; x < 4; x++) + { + ret->delay[x] = oscs[x]->delay; + ret->volume_left[x] = oscs[x]->volume_left; + ret->volume_right[x] = oscs[x]->volume_right; + } + for(int x = 0; x < 3; x++) + { + ret->sq_period[x] = squares[x].period; + ret->sq_phase[x] = squares[x].phase; + } + ret->noise_shifter = noise.shifter; + ret->noise_tap = noise.tap; + ret->noise_period_extra = noise.period_extra; + + if(noise.period == &noise_periods[0]) + ret->noise_period = 0; + else if(noise.period == &noise_periods[1]) + ret->noise_period = 1; + else if(noise.period == &noise_periods[2]) + ret->noise_period = 2; + else ret->noise_period = 3; + + ret->latch_left = latch_left; + ret->latch_right = latch_right; +} + +void T6W28_Apu::load_state(const T6W28_ApuState *state) +{ + for(int x = 0; x < 4; x++) + { + oscs[x]->delay = state->delay[x] & ((x == 3) ? 0x7FFF : 0x3FFF); + oscs[x]->volume_left = state->volume_left[x]; + oscs[x]->volume_right = state->volume_right[x]; + } + for(int x = 0; x < 3; x++) + { + squares[x].period = state->sq_period[x] & 0x3FFF; + squares[x].phase = state->sq_phase[x]; + } + noise.shifter = state->noise_shifter; + noise.tap = state->noise_tap; + noise.period_extra = state->noise_period_extra & 0x3FFF; + + unsigned select = state->noise_period; + + if ( select < 3 ) + noise.period = &noise_periods [select]; + else + noise.period = &noise.period_extra; + + latch_left = state->latch_left; + latch_right = state->latch_right; +} + +} diff --git a/src/engine/platform/sound/t6w28/T6W28_Apu.h b/src/engine/platform/sound/t6w28/T6W28_Apu.h new file mode 100644 index 00000000..bd173948 --- /dev/null +++ b/src/engine/platform/sound/t6w28/T6W28_Apu.h @@ -0,0 +1,94 @@ +// T6W28_Snd_Emu + +#ifndef SMS_APU_H +#define SMS_APU_H + +namespace MDFN_IEN_NGP +{ + +typedef long sms_time_t; // clock cycle count + +} + +#include "T6W28_Oscs.h" + +namespace MDFN_IEN_NGP +{ + +typedef struct +{ + int sq_period[3]; + int sq_phase[3]; + unsigned int noise_period; + unsigned int noise_period_extra; + unsigned int noise_shifter; + unsigned int noise_tap; + + int delay[4]; + int volume_left[4]; + int volume_right[4]; + unsigned char latch_left, latch_right; +} T6W28_ApuState; + +class T6W28_Apu { +public: + // Set overall volume of all oscillators, where 1.0 is full volume + void volume( double ); + + // Set treble equalization + void treble_eq( const blip_eq_t& ); + + // Outputs can be assigned to a single buffer for mono output, or to three + // buffers for stereo output (using Stereo_Buffer to do the mixing). + + // Assign all oscillator outputs to specified buffer(s). If buffer + // is NULL, silences all oscillators. + void output( Blip_Buffer* mono ); + void output( Blip_Buffer* center, Blip_Buffer* left, Blip_Buffer* right ); + + // Assign single oscillator output to buffer(s). Valid indicies are 0 to 3, + // which refer to Square 1, Square 2, Square 3, and Noise. If buffer is NULL, + // silences oscillator. + enum { osc_count = 4 }; + void osc_output( int index, Blip_Buffer* mono ); + void osc_output( int index, Blip_Buffer* center, Blip_Buffer* left, Blip_Buffer* right ); + + // Reset oscillators and internal state + void reset(); + + // Write to data port + void write_data_left( sms_time_t, int ); + void write_data_right( sms_time_t, int ); + + // Run all oscillators up to specified time, end current frame, then + // start a new frame at time 0. Returns true if any oscillators added + // sound to one of the left/right buffers, false if they only added + // to the center buffer. + bool end_frame( sms_time_t ); + +public: + T6W28_Apu(); + ~T6W28_Apu(); +private: + // noncopyable + T6W28_Apu( const T6W28_Apu& ); + T6W28_Apu& operator = ( const T6W28_Apu& ); + + T6W28_Osc* oscs [osc_count]; + T6W28_Square squares [3]; + //T6W28_Square::Synth square_synth; // used by squares + sms_time_t last_time; + int latch_left, latch_right; + T6W28_Noise noise; + + void run_until( sms_time_t ); +}; + +inline void T6W28_Apu::output( Blip_Buffer* b ) { output( b, b, b ); } + +inline void T6W28_Apu::osc_output( int i, Blip_Buffer* b ) { osc_output( i, b, b, b ); } + +} + +#endif + diff --git a/src/engine/platform/sound/t6w28/T6W28_Oscs.h b/src/engine/platform/sound/t6w28/T6W28_Oscs.h new file mode 100644 index 00000000..876b3ed1 --- /dev/null +++ b/src/engine/platform/sound/t6w28/T6W28_Oscs.h @@ -0,0 +1,50 @@ + +// Private oscillators used by T6W28_Apu + +// T6W28_Snd_Emu + +#ifndef SMS_OSCS_H +#define SMS_OSCS_H + +namespace MDFN_IEN_NGP +{ + +struct T6W28_Osc +{ + int output_select; + + int delay; + int last_amp_left; + int last_amp_right; + + int volume_left; + int volume_right; + + T6W28_Osc(); + void reset(); +}; + +struct T6W28_Square : T6W28_Osc +{ + int period; + int phase; + + void reset(); + void run( sms_time_t, sms_time_t ); +}; + +struct T6W28_Noise : T6W28_Osc +{ + const int* period; + int period_extra; + unsigned shifter; + unsigned tap; + + void reset(); + void run( sms_time_t, sms_time_t ); +}; + +} + +#endif + diff --git a/src/engine/platform/sound/vsu.cpp b/src/engine/platform/sound/vsu.cpp new file mode 100644 index 00000000..b2b6518e --- /dev/null +++ b/src/engine/platform/sound/vsu.cpp @@ -0,0 +1,499 @@ +/******************************************************************************/ +/* Mednafen Virtual Boy Emulation Module */ +/******************************************************************************/ +/* vsu.cpp: +** Copyright (C) 2010-2016 Mednafen Team +** +** 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 "vsu.h" +#include +#include +#include + +static const unsigned int Tap_LUT[8] = { 15 - 1, 11 - 1, 14 - 1, 5 - 1, 9 - 1, 7 - 1, 10 - 1, 12 - 1 }; + +#define MDFN_UNLIKELY(x) x + +VSU::VSU() +{ + for(int ch = 0; ch < 6; ch++) + { + for(int lr = 0; lr < 2; lr++) + last_output[ch][lr] = 0; + } +} + +VSU::~VSU() +{ + +} + +void VSU::SetSoundRate(double rate) +{ + /* + for(int y = 0; y < 2; y++) + { + sbuf[y].set_sample_rate(rate ? rate : 44100, 50); + sbuf[y].clock_rate((long)(VB_MASTER_CLOCK / 4)); + sbuf[y].bass_freq(20); + } + */ +} + +void VSU::Power(void) +{ + SweepControl = 0; + SweepModCounter = 0; + SweepModClockDivider = 1; + + for(int ch = 0; ch < 6; ch++) + { + IntlControl[ch] = 0; + LeftLevel[ch] = 0; + RightLevel[ch] = 0; + Frequency[ch] = 0; + EnvControl[ch] = 0; + RAMAddress[ch] = 0; + + EffFreq[ch] = 0; + Envelope[ch] = 0; + WavePos[ch] = 0; + FreqCounter[ch] = 1; + IntervalCounter[ch] = 0; + EnvelopeCounter[ch] = 1; + + EffectsClockDivider[ch] = 4800; + IntervalClockDivider[ch] = 4; + EnvelopeClockDivider[ch] = 4; + + LatcherClockDivider[ch] = 120; + } + + ModWavePos = 0; + + NoiseLatcherClockDivider = 120; + NoiseLatcher = 0; + + lfsr = 0; + + memset(WaveData, 0, sizeof(WaveData)); + memset(ModData, 0, sizeof(ModData)); + + last_ts = 0; +} + +void VSU::Write(int timestamp, unsigned int A, unsigned char V) +{ + if(MDFN_UNLIKELY(A & 0x3)) + { + return; + } + // + // + A &= 0x7FF; + + //Update(timestamp); + + printf("VSU Write: %d, %08x %02x\n", timestamp, A, V); + + if(A < 0x280) + WaveData[A >> 7][(A >> 2) & 0x1F] = V & 0x3F; + else if(A < 0x400) + { + if(A >= 0x300) + printf("Modulation mirror write? %08x %02x\n", A, V); + ModData[(A >> 2) & 0x1F] = V; + } + else if(A < 0x600) + { + int ch = (A >> 6) & 0xF; + + //if(ch < 6) + printf("Ch: %d, Reg: %d, Value: %02x\n", ch, (A >> 2) & 0xF, V); + + if(ch > 5) + { + if(A == 0x580 && (V & 1)) + { + //puts("STOP, HAMMER TIME"); + for(int i = 0; i < 6; i++) + IntlControl[i] &= ~0x80; + } + } + else + switch((A >> 2) & 0xF) + { + case 0x0: IntlControl[ch] = V & ~0x40; + + if(V & 0x80) + { + EffFreq[ch] = Frequency[ch]; + if(ch == 5) + FreqCounter[ch] = 10 * (2048 - EffFreq[ch]); + else + FreqCounter[ch] = 2048 - EffFreq[ch]; + IntervalCounter[ch] = (V & 0x1F) + 1; + EnvelopeCounter[ch] = (EnvControl[ch] & 0x7) + 1; + + if(ch == 4) + { + SweepModCounter = (SweepControl >> 4) & 7; + SweepModClockDivider = (SweepControl & 0x80) ? 8 : 1; + ModWavePos = 0; + } + + WavePos[ch] = 0; + + if(ch == 5) // Not sure if this is correct. + lfsr = 1; + + //if(!(IntlControl[ch] & 0x80)) + // Envelope[ch] = (EnvControl[ch] >> 4) & 0xF; + + EffectsClockDivider[ch] = 4800; + IntervalClockDivider[ch] = 4; + EnvelopeClockDivider[ch] = 4; + } + break; + + case 0x1: LeftLevel[ch] = (V >> 4) & 0xF; + RightLevel[ch] = (V >> 0) & 0xF; + break; + + case 0x2: Frequency[ch] &= 0xFF00; + Frequency[ch] |= V << 0; + EffFreq[ch] &= 0xFF00; + EffFreq[ch] |= V << 0; + break; + + case 0x3: Frequency[ch] &= 0x00FF; + Frequency[ch] |= (V & 0x7) << 8; + EffFreq[ch] &= 0x00FF; + EffFreq[ch] |= (V & 0x7) << 8; + break; + + case 0x4: EnvControl[ch] &= 0xFF00; + EnvControl[ch] |= V << 0; + + Envelope[ch] = (V >> 4) & 0xF; + break; + + case 0x5: EnvControl[ch] &= 0x00FF; + if(ch == 4) + EnvControl[ch] |= (V & 0x73) << 8; + else if(ch == 5) + { + EnvControl[ch] |= (V & 0x73) << 8; + lfsr = 1; + } + else + EnvControl[ch] |= (V & 0x03) << 8; + break; + + case 0x6: RAMAddress[ch] = V & 0xF; + break; + + case 0x7: if(ch == 4) + { + SweepControl = V; + } + break; + } + } +} + +inline void VSU::CalcCurrentOutput(int ch, int &left, int &right) +{ + if(!(IntlControl[ch] & 0x80)) + { + left = right = 0; + return; + } + + int WD; + int l_ol, r_ol; + + if(ch == 5) + WD = NoiseLatcher; //(NoiseLatcher << 6) - NoiseLatcher; + else + { + if(RAMAddress[ch] > 4) + WD = 0; + else + WD = WaveData[RAMAddress[ch]][WavePos[ch]]; // - 0x20; + } + l_ol = Envelope[ch] * LeftLevel[ch]; + if(l_ol) + { + l_ol >>= 3; + l_ol += 1; + } + + r_ol = Envelope[ch] * RightLevel[ch]; + if(r_ol) + { + r_ol >>= 3; + r_ol += 1; + } + + left = WD * l_ol; + right = WD * r_ol; +} + +void VSU::Update(int timestamp) +{ + //puts("VSU Start"); + int left, right; + + for(int ch = 0; ch < 6; ch++) + { + int clocks = timestamp - last_ts; + //int running_timestamp = last_ts; + + // Output sound here + CalcCurrentOutput(ch, left, right); + /*Synth.offset_inline(running_timestamp, left - last_output[ch][0], &sbuf[0]); + Synth.offset_inline(running_timestamp, right - last_output[ch][1], &sbuf[1]);*/ + last_output[ch][0] = left; + last_output[ch][1] = right; + + if(!(IntlControl[ch] & 0x80)) + continue; + + while(clocks > 0) + { + int chunk_clocks = clocks; + + if(chunk_clocks > EffectsClockDivider[ch]) + chunk_clocks = EffectsClockDivider[ch]; + + if(ch == 5) + { + if(chunk_clocks > NoiseLatcherClockDivider) + chunk_clocks = NoiseLatcherClockDivider; + } + else + { + if(EffFreq[ch] >= 2040) + { + if(chunk_clocks > LatcherClockDivider[ch]) + chunk_clocks = LatcherClockDivider[ch]; + } + else + { + if(chunk_clocks > FreqCounter[ch]) + chunk_clocks = FreqCounter[ch]; + } + } + + if(ch == 5 && chunk_clocks > NoiseLatcherClockDivider) + chunk_clocks = NoiseLatcherClockDivider; + + FreqCounter[ch] -= chunk_clocks; + while(FreqCounter[ch] <= 0) + { + if(ch == 5) + { + int feedback = ((lfsr >> 7) & 1) ^ ((lfsr >> Tap_LUT[(EnvControl[5] >> 12) & 0x7]) & 1) ^ 1; + lfsr = ((lfsr << 1) & 0x7FFF) | feedback; + + FreqCounter[ch] += 10 * (2048 - EffFreq[ch]); + } + else + { + FreqCounter[ch] += 2048 - EffFreq[ch]; + WavePos[ch] = (WavePos[ch] + 1) & 0x1F; + } + } + + LatcherClockDivider[ch] -= chunk_clocks; + while(LatcherClockDivider[ch] <= 0) + LatcherClockDivider[ch] += 120; + + if(ch == 5) + { + NoiseLatcherClockDivider -= chunk_clocks; + if(!NoiseLatcherClockDivider) + { + NoiseLatcherClockDivider = 120; + NoiseLatcher = ((lfsr & 1) << 6) - (lfsr & 1); + } + } + + EffectsClockDivider[ch] -= chunk_clocks; + while(EffectsClockDivider[ch] <= 0) + { + EffectsClockDivider[ch] += 4800; + + IntervalClockDivider[ch]--; + while(IntervalClockDivider[ch] <= 0) + { + IntervalClockDivider[ch] += 4; + + if(IntlControl[ch] & 0x20) + { + IntervalCounter[ch]--; + if(!IntervalCounter[ch]) + { + IntlControl[ch] &= ~0x80; + } + } + + EnvelopeClockDivider[ch]--; + while(EnvelopeClockDivider[ch] <= 0) + { + EnvelopeClockDivider[ch] += 4; + + if(EnvControl[ch] & 0x0100) // Enveloping enabled? + { + EnvelopeCounter[ch]--; + if(!EnvelopeCounter[ch]) + { + EnvelopeCounter[ch] = (EnvControl[ch] & 0x7) + 1; + + if(EnvControl[ch] & 0x0008) // Grow + { + if(Envelope[ch] < 0xF || (EnvControl[ch] & 0x200)) + Envelope[ch] = (Envelope[ch] + 1) & 0xF; + } + else // Decay + { + if(Envelope[ch] > 0 || (EnvControl[ch] & 0x200)) + Envelope[ch] = (Envelope[ch] - 1) & 0xF; + } + } + } + + } // end while(EnvelopeClockDivider[ch] <= 0) + } // end while(IntervalClockDivider[ch] <= 0) + + if(ch == 4) + { + SweepModClockDivider--; + while(SweepModClockDivider <= 0) + { + SweepModClockDivider += (SweepControl & 0x80) ? 8 : 1; + + if(((SweepControl >> 4) & 0x7) && (EnvControl[ch] & 0x4000)) + { + if(SweepModCounter) + SweepModCounter--; + + if(!SweepModCounter) + { + SweepModCounter = (SweepControl >> 4) & 0x7; + + if(EnvControl[ch] & 0x1000) // Modulation + { + if(ModWavePos < 32 || (EnvControl[ch] & 0x2000)) + { + ModWavePos &= 0x1F; + + EffFreq[ch] = (Frequency[ch] + (signed char)ModData[ModWavePos]) & 0x7FF; + ModWavePos++; + } + } + else // Sweep + { + int delta = EffFreq[ch] >> (SweepControl & 0x7); + int NewFreq = EffFreq[ch] + ((SweepControl & 0x8) ? delta : -delta); + + //printf("Sweep(%d): Old: %d, New: %d\n", ch, EffFreq[ch], NewFreq); + + if(NewFreq < 0) + EffFreq[ch] = 0; + else if(NewFreq > 0x7FF) + { + //EffFreq[ch] = 0x7FF; + IntlControl[ch] &= ~0x80; + } + else + EffFreq[ch] = NewFreq; + } + } + } + } // end while(SweepModClockDivider <= 0) + } // end if(ch == 4) + } // end while(EffectsClockDivider[ch] <= 0) + clocks -= chunk_clocks; + //running_timestamp += chunk_clocks; + + // Output sound here too. + CalcCurrentOutput(ch, left, right); + /* + Synth.offset_inline(running_timestamp, left - last_output[ch][0], &sbuf[0]); + Synth.offset_inline(running_timestamp, right - last_output[ch][1], &sbuf[1]); + */ + last_output[ch][0] = left; + last_output[ch][1] = right; + } + } + last_ts = timestamp; + //puts("VSU End"); +} + +int VSU::EndFrame(int timestamp) +{ + int ret = 0; + + Update(timestamp); + last_ts = 0; + + /* + if(SoundBuf) + { + for(int y = 0; y < 2; y++) + { + sbuf[y].end_frame(timestamp); + ret = sbuf[y].read_samples(SoundBuf + y, SoundBufMaxSize, 1); + } + } + */ + + return ret; +} + +unsigned char VSU::PeekWave(const unsigned int which, unsigned int Address) +{ + assert(which <= 4); + + Address &= 0x1F; + + return(WaveData[which][Address]); +} + +void VSU::PokeWave(const unsigned int which, unsigned int Address, unsigned char value) +{ + assert(which <= 4); + + Address &= 0x1F; + + WaveData[which][Address] = value & 0x3F; +} + +unsigned char VSU::PeekModWave(unsigned int Address) +{ + Address &= 0x1F; + return(ModData[Address]); +} + +void VSU::PokeModWave(unsigned int Address, unsigned char value) +{ + Address &= 0x1F; + + ModData[Address] = value & 0xFF; +} diff --git a/src/engine/platform/sound/vsu.h b/src/engine/platform/sound/vsu.h new file mode 100644 index 00000000..ab43cb65 --- /dev/null +++ b/src/engine/platform/sound/vsu.h @@ -0,0 +1,97 @@ +/******************************************************************************/ +/* Mednafen Virtual Boy Emulation Module */ +/******************************************************************************/ +/* vsu.h: +** Copyright (C) 2010-2016 Mednafen Team +** +** 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. +*/ + +#ifndef __VB_VSU_H +#define __VB_VSU_H + +class VSU +{ + public: + + int last_output[6][2]; + + VSU(); + ~VSU(); + + void SetSoundRate(double rate); + + void Power(void); + + void Write(int timestamp, unsigned int A, unsigned char V); + + int EndFrame(int timestamp); + + unsigned char PeekWave(const unsigned int which, unsigned int Address); + void PokeWave(const unsigned int which, unsigned int Address, unsigned char value); + + unsigned char PeekModWave(unsigned int Address); + void PokeModWave(unsigned int Address, unsigned char value); + + private: + + void CalcCurrentOutput(int ch, int &left, int &right); + + void Update(int timestamp); + + unsigned char IntlControl[6]; + unsigned char LeftLevel[6]; + unsigned char RightLevel[6]; + unsigned short Frequency[6]; + unsigned short EnvControl[6]; // Channel 5/6 extra functionality tacked on too. + + unsigned char RAMAddress[6]; + + unsigned char SweepControl; + + unsigned char WaveData[5][0x20]; + + unsigned char ModData[0x20]; + + // + // + // + int EffFreq[6]; + int Envelope[6]; + + int WavePos[6]; + int ModWavePos; + + int LatcherClockDivider[6]; + + int FreqCounter[6]; + int IntervalCounter[6]; + int EnvelopeCounter[6]; + int SweepModCounter; + + int EffectsClockDivider[6]; + int IntervalClockDivider[6]; + int EnvelopeClockDivider[6]; + int SweepModClockDivider; + + int NoiseLatcherClockDivider; + unsigned int NoiseLatcher; + + unsigned int lfsr; + + int last_ts; +}; + +#endif diff --git a/src/engine/platform/vb.cpp b/src/engine/platform/vb.cpp new file mode 100644 index 00000000..dcdeffff --- /dev/null +++ b/src/engine/platform/vb.cpp @@ -0,0 +1,604 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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 "vb.h" +#include "../engine.h" +#include + +//#define rWrite(a,v) pendingWrites[a]=v; +#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define chWrite(c,a,v) rWrite(0x400+((c)<<6)+((a)<<2),v); + +#define CHIP_DIVIDER 64 + +const char* regCheatSheetVB[]={ + "Select", "0", + "MasterVol", "1", + "FreqL", "2", + "FreqH", "3", + "DataCtl", "4", + "ChanVol", "5", + "WaveCtl", "6", + "NoiseCtl", "7", + "LFOFreq", "8", + "LFOCtl", "9", + NULL +}; + +const char** DivPlatformVB::getRegisterSheet() { + return regCheatSheetVB; +} + +void DivPlatformVB::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t h=start; hrate) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->samples<=0) { + chan[i].dacSample=-1; + continue; + } + chWrite(i,0x07,0); + signed char dacData=((signed char)((unsigned char)s->data8[chan[i].dacPos]^0x80))>>3; + chan[i].dacOut=CLAMP(dacData,-16,15); + if (!isMuted[i]) { + chWrite(i,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[i].outVol)); + chWrite(i,0x06,chan[i].dacOut&0x1f); + } else { + chWrite(i,0x04,0xc0); + chWrite(i,0x06,0x10); + } + chan[i].dacPos++; + if (s->isLoopable() && chan[i].dacPos>=(unsigned int)s->loopEnd) { + chan[i].dacPos=s->loopStart; + } else if (chan[i].dacPos>=s->samples) { + chan[i].dacSample=-1; + } + chan[i].dacPeriod-=rate; + } + } + } + */ + + // VB part + cycles=0; + while (!writes.empty()) { + QueuedWrite w=writes.front(); + vb->Write(cycles,w.addr,w.val); + regPool[w.addr]=w.val; + //cycles+=2; + writes.pop(); + } + vb->EndFrame(4); + + tempL=0; + tempR=0; + for (int i=0; i<6; i++) { + oscBuf[i]->data[oscBuf[i]->needle++]=(vb->last_output[i][0]+vb->last_output[i][1])<<3; + tempL+=vb->last_output[i][0]<<3; + tempR+=vb->last_output[i][1]<<3; + } + + //tempL/=6; + //tempR/=6; + + if (tempL<-32768) tempL=-32768; + if (tempL>32767) tempL=32767; + if (tempR<-32768) tempR=-32768; + if (tempR>32767) tempR=32767; + + //printf("tempL: %d tempR: %d\n",tempL,tempR); + bufL[h]=tempL; + bufR[h]=tempR; + } +} + +void DivPlatformVB::updateWave(int ch) { + if (ch>=5) return; + + if (chan[ch].pcm) { + chan[ch].deferredWaveUpdate=true; + return; + } + for (int i=0; i<32; i++) { + rWrite((ch<<7)+(i<<2),chan[ch].ws.output[i]); + //chWrite(ch,0x06,chan[ch].ws.output[(i+chan[ch].antiClickWavePos)&31]); + } + chan[ch].antiClickWavePos&=31; + if (chan[ch].active) { + //chWrite(ch,0x04,0x80|chan[ch].outVol); + } + if (chan[ch].deferredWaveUpdate) { + chan[ch].deferredWaveUpdate=false; + } +} + +// TODO: in octave 6 the noise table changes to a tonal one +static unsigned char noiseFreq[12]={ + 4,13,15,18,21,23,25,27,29,31,0,2 +}; + +void DivPlatformVB::tick(bool sysTick) { + for (int i=0; i<6; i++) { + // anti-click + if (antiClickEnabled && sysTick && chan[i].freq>0) { + chan[i].antiClickPeriodCount+=(chipClock/MAX(parent->getCurHz(),1.0f)); + chan[i].antiClickWavePos+=chan[i].antiClickPeriodCount/chan[i].freq; + chan[i].antiClickPeriodCount%=chan[i].freq; + } + + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=VOL_SCALE_LOG(chan[i].vol&31,MIN(31,chan[i].std.vol.val),31); + if (chan[i].furnaceDac && chan[i].pcm) { + // ignore for now + } else { + //chWrite(i,0x04,0x80|chan[i].outVol); + } + } + if (chan[i].std.duty.had && i>=4) { + chan[i].noise=chan[i].std.duty.val; + chan[i].freqChanged=true; + int noiseSeek=chan[i].note; + if (noiseSeek<0) noiseSeek=0; + chWrite(i,0x07,chan[i].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + int noiseSeek=parent->calcArp(chan[i].note,chan[i].std.arp.val); + chan[i].baseFreq=NOTE_PERIODIC(noiseSeek); + if (noiseSeek<0) noiseSeek=0; + chWrite(i,0x07,chan[i].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); + } + chan[i].freqChanged=true; + } + if (chan[i].std.wave.had && !chan[i].pcm) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + chan[i].ws.changeWave1(chan[i].wave); + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + if (chan[i].std.panL.had) { + chan[i].pan&=0x0f; + chan[i].pan|=(chan[i].std.panL.val&15)<<4; + } + if (chan[i].std.panR.had) { + chan[i].pan&=0xf0; + chan[i].pan|=chan[i].std.panR.val&15; + } + if (chan[i].std.panL.had || chan[i].std.panR.had) { + //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); + } + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-32768,32767); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + if (chan[i].std.phaseReset.had && chan[i].std.phaseReset.val==1) { + if (chan[i].furnaceDac && chan[i].pcm) { + if (chan[i].active && chan[i].dacSample>=0 && chan[i].dacSamplesong.sampleLen) { + chan[i].dacPos=0; + chan[i].dacPeriod=0; + //chWrite(i,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[i].vol)); + addWrite(0xffff0000+(i<<8),chan[i].dacSample); + chan[i].keyOn=true; + } + } + chan[i].antiClickWavePos=0; + chan[i].antiClickPeriodCount=0; + } + if (chan[i].active) { + if (chan[i].ws.tick() || (chan[i].std.phaseReset.had && chan[i].std.phaseReset.val==1) || chan[i].deferredWaveUpdate) { + updateWave(i); + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + //DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_PCE); + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true,0,chan[i].pitch2,chipClock,CHIP_DIVIDER); + if (chan[i].furnaceDac && chan[i].pcm) { + double off=1.0; + if (chan[i].dacSample>=0 && chan[i].dacSamplesong.sampleLen) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + chan[i].dacRate=((double)chipClock/2)/MAX(1,off*chan[i].freq); + if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].dacRate); + } + if (chan[i].freq>2047) chan[i].freq=2047; + chan[i].freq=2047-chan[i].freq; + chWrite(i,0x02,chan[i].freq&0xff); + chWrite(i,0x03,chan[i].freq>>8); + if (chan[i].keyOn) { + //rWrite(16+i*5,0x80); + //chWrite(i,0x04,0x80|chan[i].vol); + chWrite(i,0x01,0xff); + //chWrite(i,0x00,0xff); + chWrite(i,0x04,0xf0); + chWrite(i,0x05,0x00); + chWrite(i,0x00,0x80); + } + if (chan[i].keyOff) { + chWrite(i,0x01,0); + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformVB::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_PCE); + chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:31; + if (ins->type==DIV_INS_AMIGA || ins->amiga.useSample) { + chan[c.chan].pcm=true; + } else if (chan[c.chan].furnaceDac) { + chan[c.chan].pcm=false; + } + if (chan[c.chan].pcm) { + if (ins->type==DIV_INS_AMIGA || ins->amiga.useSample) { + chan[c.chan].furnaceDac=true; + if (skipRegisterWrites) break; + chan[c.chan].dacSample=ins->amiga.getSample(c.value); + if (chan[c.chan].dacSample<0 || chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + break; + } else { + if (dumpWrites) { + //chWrite(c.chan,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[c.chan].vol)); + addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample); + } + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + //chan[c.chan].keyOn=true; + } else { + chan[c.chan].furnaceDac=false; + if (skipRegisterWrites) break; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + } + chan[c.chan].dacSample=12*sampleBank+chan[c.chan].note%12; + if (chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + break; + } else { + if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample); + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + chan[c.chan].dacRate=parent->getSample(chan[c.chan].dacSample)->rate; + if (dumpWrites) { + //chWrite(c.chan,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[c.chan].vol)); + addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dacRate); + } + } + break; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + int noiseSeek=chan[c.chan].note; + if (noiseSeek<0) noiseSeek=0; + chWrite(c.chan,0x07,chan[c.chan].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + //chWrite(c.chan,0x04,0x80|chan[c.chan].vol); + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + chan[c.chan].ws.init(ins,32,63,chan[c.chan].insChanged); + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + chan[c.chan].pcm=false; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].macroInit(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + if (chan[c.chan].active && !chan[c.chan].pcm) { + //chWrite(c.chan,0x04,0x80|chan[c.chan].outVol); + } + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + chan[c.chan].keyOn=true; + break; + case DIV_CMD_PCE_LFO_MODE: + if (c.value==0) { + lfoMode=0; + } else { + lfoMode=c.value; + } + rWrite(0x08,lfoSpeed); + rWrite(0x09,lfoMode); + break; + case DIV_CMD_PCE_LFO_SPEED: + lfoSpeed=255-c.value; + rWrite(0x08,lfoSpeed); + rWrite(0x09,lfoMode); + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + chan[c.chan].noise=c.value; + chWrite(c.chan,0x07,chan[c.chan].noise?(0x80|chan[c.chan].note):0); + break; + case DIV_CMD_SAMPLE_MODE: + chan[c.chan].pcm=c.value; + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + break; + case DIV_CMD_PANNING: { + chan[c.chan].pan=(c.value&0xf0)|(c.value2>>4); + //chWrite(c.chan,0x05,isMuted[c.chan]?0:chan[c.chan].pan); + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_PCE)); + } + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 31; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformVB::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + //chWrite(ch,0x05,isMuted[ch]?0:chan[ch].pan); + if (!isMuted[ch] && (chan[ch].pcm && chan[ch].dacSample!=-1)) { + //chWrite(ch,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[ch].outVol)); + //chWrite(ch,0x06,chan[ch].dacOut&0x1f); + } +} + +void DivPlatformVB::forceIns() { + for (int i=0; i<6; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + updateWave(i); + //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); + } +} + +void* DivPlatformVB::getChanState(int ch) { + return &chan[ch]; +} + +DivMacroInt* DivPlatformVB::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +DivDispatchOscBuffer* DivPlatformVB::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +unsigned char* DivPlatformVB::getRegisterPool() { + return regPool; +} + +int DivPlatformVB::getRegisterPoolSize() { + return 0x600; +} + +void DivPlatformVB::reset() { + while (!writes.empty()) writes.pop(); + memset(regPool,0,0x600); + for (int i=0; i<6; i++) { + chan[i]=DivPlatformVB::Channel(); + chan[i].std.setEngine(parent); + chan[i].ws.setEngine(parent); + chan[i].ws.init(NULL,32,63,false); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + vb->Power(); + lastPan=0xff; + tempL=0; + tempR=0; + cycles=0; + curChan=-1; + sampleBank=0; + lfoMode=0; + lfoSpeed=255; + // set per-channel initial panning + for (int i=0; i<6; i++) { + //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); + } + delay=500; +} + +bool DivPlatformVB::isStereo() { + return true; +} + +bool DivPlatformVB::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformVB::notifyWaveChange(int wave) { + for (int i=0; i<6; i++) { + if (chan[i].wave==wave) { + chan[i].ws.changeWave1(wave); + updateWave(i); + } + } +} + +void DivPlatformVB::notifyInsDeletion(void* ins) { + for (int i=0; i<6; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformVB::setFlags(const DivConfig& flags) { + chipClock=20000000.0; + antiClickEnabled=!flags.getBool("noAntiClick",false); + rate=chipClock/16; + for (int i=0; i<6; i++) { + oscBuf[i]->rate=rate; + } + + if (vb!=NULL) { + delete vb; + vb=NULL; + } + vb=new VSU; +} + +void DivPlatformVB::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformVB::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformVB::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<6; i++) { + isMuted[i]=false; + oscBuf[i]=new DivDispatchOscBuffer; + } + vb=NULL; + setFlags(flags); + reset(); + return 6; +} + +void DivPlatformVB::quit() { + for (int i=0; i<6; i++) { + delete oscBuf[i]; + } + if (vb!=NULL) { + delete vb; + vb=NULL; + } +} + +DivPlatformVB::~DivPlatformVB() { +} diff --git a/src/engine/platform/vb.h b/src/engine/platform/vb.h new file mode 100644 index 00000000..7bb22965 --- /dev/null +++ b/src/engine/platform/vb.h @@ -0,0 +1,121 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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. + */ + +#ifndef _PLATFORM_VB_H +#define _PLATFORM_VB_H + +#include "../dispatch.h" +#include +#include "../macroInt.h" +#include "../waveSynth.h" +#include "sound/vsu.h" + +class DivPlatformVB: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, pitch2, note, antiClickPeriodCount, antiClickWavePos; + int dacPeriod, dacRate, dacOut; + unsigned int dacPos; + int dacSample, ins; + unsigned char pan; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, noise, pcm, furnaceDac, deferredWaveUpdate; + signed char vol, outVol, wave; + int macroVolMul; + DivMacroInt std; + DivWaveSynth ws; + void macroInit(DivInstrument* which) { + std.init(which); + pitch2=0; + } + Channel(): + freq(0), + baseFreq(0), + pitch(0), + pitch2(0), + note(0), + antiClickPeriodCount(0), + antiClickWavePos(0), + dacPeriod(0), + dacRate(0), + dacOut(0), + dacPos(0), + dacSample(-1), + ins(-1), + pan(255), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + noise(false), + pcm(false), + furnaceDac(false), + deferredWaveUpdate(false), + vol(31), + outVol(31), + wave(-1), + macroVolMul(31) {} + }; + Channel chan[6]; + DivDispatchOscBuffer* oscBuf[6]; + bool isMuted[6]; + bool antiClickEnabled; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v) {} + }; + std::queue writes; + unsigned char lastPan; + + int cycles, curChan, delay; + int tempL; + int tempR; + unsigned char sampleBank, lfoMode, lfoSpeed; + VSU* vb; + unsigned char regPool[0x600]; + void updateWave(int ch); + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivMacroInt* getChanMacroInt(int ch); + DivDispatchOscBuffer* getOscBuffer(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + bool isStereo(); + bool keyOffAffectsArp(int ch); + void setFlags(const DivConfig& flags); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags); + void quit(); + ~DivPlatformVB(); +}; + +#endif diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index cb2a3da4..86620976 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1178,7 +1178,9 @@ void DivEngine::registerSystems() { {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Noise"}, {"CH1", "CH2", "CH3", "CH4", "CH5", "NO"}, {DIV_CH_WAVE, DIV_CH_WAVE, DIV_CH_WAVE, DIV_CH_WAVE, DIV_CH_WAVE, DIV_CH_NOISE}, - {DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY} + {DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY, DIV_INS_VBOY}, + {}, + waveOnlyEffectHandlerMap ); sysDefs[DIV_SYSTEM_VRC7]=new DivSysDef( diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 6b314963..b365ca98 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -936,6 +936,7 @@ const int availableSystems[]={ DIV_SYSTEM_QSOUND, DIV_SYSTEM_X1_010, DIV_SYSTEM_SWAN, + DIV_SYSTEM_VBOY, DIV_SYSTEM_VERA, DIV_SYSTEM_BUBSYS_WSG, DIV_SYSTEM_N163, @@ -1008,6 +1009,7 @@ const int chipsWave[]={ DIV_SYSTEM_PCE, DIV_SYSTEM_X1_010, DIV_SYSTEM_SWAN, + DIV_SYSTEM_VBOY, DIV_SYSTEM_BUBSYS_WSG, DIV_SYSTEM_N163, DIV_SYSTEM_FDS, From ec22150fb58b38d7615423dcc87938677d894c25 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 21:45:48 -0500 Subject: [PATCH 51/57] GUI: add pitch table calculator --- src/gui/debugWindow.cpp | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 9358c06f..38275c74 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -30,6 +30,12 @@ void FurnaceGUI::drawDebug() { static int bpRow; static int bpTick; static bool bpOn; + + static double ptcClock; + static double ptcDivider; + static int ptcOctave; + static int ptcMode; + static int ptcBlockBits; if (nextWindow==GUI_WINDOW_DEBUG) { debugOpen=true; ImGui::SetNextWindowFocus(); @@ -300,6 +306,78 @@ void FurnaceGUI::drawDebug() { } ImGui::TreePop(); } + if (ImGui::TreeNode("Pitch Table Calculator")) { + ImGui::InputDouble("Clock",&ptcClock); + ImGui::InputDouble("Divider/FreqBase",&ptcDivider); + ImGui::InputInt("Octave",&ptcOctave); + if (ImGui::RadioButton("Frequency",ptcMode==0)) ptcMode=0; + ImGui::SameLine(); + if (ImGui::RadioButton("Period",ptcMode==1)) ptcMode=1; + ImGui::SameLine(); + if (ImGui::RadioButton("FreqNum/Block",ptcMode==2)) ptcMode=2; + + if (ptcMode==2) { + if (ImGui::InputInt("FreqNum Bits",&ptcBlockBits)) { + if (ptcBlockBits<0) ptcBlockBits=0; + if (ptcBlockBits>13) ptcBlockBits=13; + } + } + + if (ImGui::BeginTable("PitchTable",7)) { + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableNextColumn(); + ImGui::Text("Note"); + ImGui::TableNextColumn(); + ImGui::Text("Pitch"); + + ImGui::TableNextColumn(); + ImGui::Text("Base"); + ImGui::TableNextColumn(); + ImGui::Text("Hex"); + + ImGui::TableNextColumn(); + ImGui::Text("Final"); + + ImGui::TableNextColumn(); + ImGui::Text("Hex"); + + ImGui::TableNextColumn(); + ImGui::Text("Delta"); + + int lastFinal=0; + for (int i=0; i<12; i++) { + int note=(12*ptcOctave)+i; + int pitch=0; + + int base=e->calcBaseFreq(ptcClock,ptcDivider,note,ptcMode==1); + int final=e->calcFreq(base,pitch,ptcMode==1,0,0,ptcClock,ptcDivider,(ptcMode==2)?ptcBlockBits:0); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d",note); + ImGui::TableNextColumn(); + ImGui::Text("%d",pitch); + + ImGui::TableNextColumn(); + ImGui::Text("%d",base); + ImGui::TableNextColumn(); + ImGui::Text("%x",base); + + ImGui::TableNextColumn(); + ImGui::Text("%d",final); + ImGui::TableNextColumn(); + ImGui::Text("%x",final); + + ImGui::TableNextColumn(); + ImGui::Text("%d",final-lastFinal); + + lastFinal=final; + } + + ImGui::EndTable(); + } + ImGui::TreePop(); + } if (ImGui::TreeNode("Playground")) { if (pgSys<0 || pgSys>=e->song.systemLen) pgSys=0; if (ImGui::BeginCombo("Chip",fmt::sprintf("%d. %s",pgSys+1,e->getSystemName(e->song.system[pgSys])).c_str())) { From 6179ef493cd4f96a5894aa0dcd7ff1b00946a3c7 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 22:22:01 -0500 Subject: [PATCH 52/57] SNES: fix some clicking issues --- src/engine/platform/snes.cpp | 69 +++++++++++++++++++++++------------- src/engine/platform/snes.h | 5 ++- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index 8be1692e..8ea97c93 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -69,14 +69,16 @@ void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len short out[2]; short chOut[16]; for (size_t h=start; hgetSample(chan[i].sample); double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; @@ -228,6 +232,7 @@ void DivPlatformSNES::tick(bool sysTick) { sampleMem[tabAddr+2]=loop&0xff; sampleMem[tabAddr+3]=loop>>8; kon|=(1<>1; chan[c.chan].panR=c.value2>>1; - writeOutVol(c.chan); + chan[c.chan].shallWriteVol=true; break; case DIV_CMD_PITCH: chan[c.chan].pitch=c.value; @@ -441,7 +462,7 @@ int DivPlatformSNES::dispatch(DivCommand c) { case DIV_CMD_SNES_INVERT: chan[c.chan].invertL=(c.value>>4); chan[c.chan].invertR=c.chan&15; - writeOutVol(c.chan); + chan[c.chan].shallWriteVol=true; break; case DIV_CMD_SNES_GAIN_MODE: if (c.value) { @@ -466,7 +487,7 @@ int DivPlatformSNES::dispatch(DivCommand c) { } else { chan[c.chan].state.useEnv=true; } - writeEnv(c.chan); + chan[c.chan].shallWriteEnv=true; break; case DIV_CMD_SNES_GAIN: if (chan[c.chan].state.gainMode==DivInstrumentSNES::GAIN_MODE_DIRECT) { @@ -474,7 +495,7 @@ int DivPlatformSNES::dispatch(DivCommand c) { } else { chan[c.chan].state.gain=c.value&0x1f; } - if (!chan[c.chan].state.useEnv) writeEnv(c.chan); + if (!chan[c.chan].state.useEnv) chan[c.chan].shallWriteEnv=true; break; case DIV_CMD_STD_NOISE_FREQ: noiseFreq=c.value&0x1f; @@ -482,19 +503,19 @@ int DivPlatformSNES::dispatch(DivCommand c) { break; case DIV_CMD_FM_AR: chan[c.chan].state.a=c.value&15; - if (chan[c.chan].state.useEnv) writeEnv(c.chan); + if (chan[c.chan].state.useEnv) chan[c.chan].shallWriteEnv=true; break; case DIV_CMD_FM_DR: chan[c.chan].state.d=c.value&7; - if (chan[c.chan].state.useEnv) writeEnv(c.chan); + if (chan[c.chan].state.useEnv) chan[c.chan].shallWriteEnv=true; break; case DIV_CMD_FM_SL: chan[c.chan].state.s=c.value&7; - if (chan[c.chan].state.useEnv) writeEnv(c.chan); + if (chan[c.chan].state.useEnv) chan[c.chan].shallWriteEnv=true; break; case DIV_CMD_FM_RR: chan[c.chan].state.r=c.value&0x1f; - if (chan[c.chan].state.useEnv) writeEnv(c.chan); + if (chan[c.chan].state.useEnv) chan[c.chan].shallWriteEnv=true; break; case DIV_CMD_SNES_ECHO: chan[c.chan].echo=c.value; @@ -605,7 +626,7 @@ void DivPlatformSNES::writeEnv(int ch) { void DivPlatformSNES::muteChannel(int ch, bool mute) { isMuted[ch]=mute; - writeOutVol(ch); + chan[ch].shallWriteVol=true; } void DivPlatformSNES::forceIns() { diff --git a/src/engine/platform/snes.h b/src/engine/platform/snes.h index ac53a51f..a21db150 100644 --- a/src/engine/platform/snes.h +++ b/src/engine/platform/snes.h @@ -33,7 +33,7 @@ class DivPlatformSNES: public DivDispatch { int sample, wave, ins; int note; int panL, panR; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos, noise, echo, pitchMod, invertL, invertR; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos, noise, echo, pitchMod, invertL, invertR, shallWriteVol, shallWriteEnv; int vol, outVol; int wtLen; DivInstrumentSNES state; @@ -68,6 +68,8 @@ class DivPlatformSNES: public DivDispatch { pitchMod(false), invertL(false), invertR(false), + shallWriteVol(false), + shallWriteEnv(false), vol(127), outVol(127), wtLen(16) {} @@ -77,6 +79,7 @@ class DivPlatformSNES: public DivDispatch { bool isMuted[8]; int globalVolL, globalVolR; unsigned char noiseFreq; + signed char delay; signed char echoVolL, echoVolR, echoFeedback; signed char echoFIR[8]; unsigned char echoDelay; From 504778d975ad6d289145e0db6aee337cee59b650 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 8 Oct 2022 23:25:15 -0500 Subject: [PATCH 53/57] Virtual Boy: more work --- src/engine/platform/sound/vsu.cpp | 8 +- src/engine/platform/vb.cpp | 124 ++++++++++++++++++++---------- src/engine/platform/vb.h | 2 + src/gui/insEdit.cpp | 7 +- 4 files changed, 95 insertions(+), 46 deletions(-) diff --git a/src/engine/platform/sound/vsu.cpp b/src/engine/platform/sound/vsu.cpp index b2b6518e..9e3832f9 100644 --- a/src/engine/platform/sound/vsu.cpp +++ b/src/engine/platform/sound/vsu.cpp @@ -108,14 +108,14 @@ void VSU::Write(int timestamp, unsigned int A, unsigned char V) //Update(timestamp); - printf("VSU Write: %d, %08x %02x\n", timestamp, A, V); + //printf("VSU Write: %d, %08x %02x\n", timestamp, A, V); if(A < 0x280) WaveData[A >> 7][(A >> 2) & 0x1F] = V & 0x3F; else if(A < 0x400) { - if(A >= 0x300) - printf("Modulation mirror write? %08x %02x\n", A, V); + //if(A >= 0x300) + //printf("Modulation mirror write? %08x %02x\n", A, V); ModData[(A >> 2) & 0x1F] = V; } else if(A < 0x600) @@ -123,7 +123,7 @@ void VSU::Write(int timestamp, unsigned int A, unsigned char V) int ch = (A >> 6) & 0xF; //if(ch < 6) - printf("Ch: %d, Reg: %d, Value: %02x\n", ch, (A >> 2) & 0xF, V); + //printf("Ch: %d, Reg: %d, Value: %02x\n", ch, (A >> 2) & 0xF, V); if(ch > 5) { diff --git a/src/engine/platform/vb.cpp b/src/engine/platform/vb.cpp index dcdeffff..7f3893f2 100644 --- a/src/engine/platform/vb.cpp +++ b/src/engine/platform/vb.cpp @@ -28,16 +28,62 @@ #define CHIP_DIVIDER 64 const char* regCheatSheetVB[]={ - "Select", "0", - "MasterVol", "1", - "FreqL", "2", - "FreqH", "3", - "DataCtl", "4", - "ChanVol", "5", - "WaveCtl", "6", - "NoiseCtl", "7", - "LFOFreq", "8", - "LFOCtl", "9", + "Wave0", "000", + "Wave1", "080", + "Wave2", "100", + "Wave3", "180", + "Wave4", "200", + "ModTable", "280", + + "S0INT", "400", + "S0LRV", "404", + "S0FQL", "408", + "S0FQH", "40C", + "S0EV0", "410", + "S0EV1", "414", + "S0RAM", "418", + + "S1INT", "440", + "S1LRV", "444", + "S1FQL", "448", + "S1FQH", "44C", + "S1EV0", "450", + "S1EV1", "454", + "S1RAM", "458", + + "S2INT", "480", + "S2LRV", "484", + "S2FQL", "488", + "S2FQH", "48C", + "S2EV0", "480", + "S2EV1", "484", + "S2RAM", "488", + + "S3INT", "4C0", + "S3LRV", "4C4", + "S3FQL", "4C8", + "S3FQH", "4CC", + "S3EV0", "4C0", + "S3EV1", "4C4", + "S3RAM", "4C8", + + "S4INT", "500", + "S4LRV", "504", + "S4FQL", "508", + "S4FQH", "50C", + "S4EV0", "510", + "S4EV1", "514", + "S4RAM", "518", + + "S5SWP", "51C", + + "S5INT", "540", + "S5LRV", "544", + "S5FQL", "548", + "S5FQH", "54C", + "S5EV0", "550", + "S5EV1", "554", + "S5RAM", "558", NULL }; @@ -89,25 +135,21 @@ void DivPlatformVB::acquire(short* bufL, short* bufR, size_t start, size_t len) //cycles+=2; writes.pop(); } - vb->EndFrame(4); + vb->EndFrame(16); tempL=0; tempR=0; for (int i=0; i<6; i++) { - oscBuf[i]->data[oscBuf[i]->needle++]=(vb->last_output[i][0]+vb->last_output[i][1])<<3; - tempL+=vb->last_output[i][0]<<3; - tempR+=vb->last_output[i][1]<<3; + oscBuf[i]->data[oscBuf[i]->needle++]=(vb->last_output[i][0]+vb->last_output[i][1])*8; + tempL+=vb->last_output[i][0]; + tempR+=vb->last_output[i][1]; } - //tempL/=6; - //tempR/=6; - if (tempL<-32768) tempL=-32768; if (tempL>32767) tempL=32767; if (tempR<-32768) tempR=-32768; if (tempR>32767) tempR=32767; - //printf("tempL: %d tempR: %d\n",tempL,tempR); bufL[h]=tempL; bufR[h]=tempR; } @@ -149,11 +191,11 @@ void DivPlatformVB::tick(bool sysTick) { chan[i].std.next(); if (chan[i].std.vol.had) { - chan[i].outVol=VOL_SCALE_LOG(chan[i].vol&31,MIN(31,chan[i].std.vol.val),31); + chan[i].outVol=VOL_SCALE_LINEAR(chan[i].vol&15,MIN(15,chan[i].std.vol.val),15); if (chan[i].furnaceDac && chan[i].pcm) { // ignore for now } else { - //chWrite(i,0x04,0x80|chan[i].outVol); + chWrite(i,0x04,chan[i].outVol<<4); } } if (chan[i].std.duty.had && i>=4) { @@ -188,7 +230,7 @@ void DivPlatformVB::tick(bool sysTick) { chan[i].pan|=chan[i].std.panR.val&15; } if (chan[i].std.panL.had || chan[i].std.panR.had) { - //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); + chWrite(i,0x01,isMuted[i]?0:chan[i].pan); } if (chan[i].std.pitch.had) { if (chan[i].std.pitch.mode) { @@ -233,21 +275,15 @@ void DivPlatformVB::tick(bool sysTick) { chan[i].dacRate=((double)chipClock/2)/MAX(1,off*chan[i].freq); if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].dacRate); } + if (chan[i].freq<1) chan[i].freq=1; if (chan[i].freq>2047) chan[i].freq=2047; - chan[i].freq=2047-chan[i].freq; + chan[i].freq=2048-chan[i].freq; chWrite(i,0x02,chan[i].freq&0xff); chWrite(i,0x03,chan[i].freq>>8); if (chan[i].keyOn) { - //rWrite(16+i*5,0x80); - //chWrite(i,0x04,0x80|chan[i].vol); - chWrite(i,0x01,0xff); - //chWrite(i,0x00,0xff); - chWrite(i,0x04,0xf0); - chWrite(i,0x05,0x00); - chWrite(i,0x00,0x80); } if (chan[i].keyOff) { - chWrite(i,0x01,0); + chWrite(i,0x04,0); } if (chan[i].keyOn) chan[i].keyOn=false; if (chan[i].keyOff) chan[i].keyOff=false; @@ -322,13 +358,9 @@ int DivPlatformVB::dispatch(DivCommand c) { chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; - int noiseSeek=chan[c.chan].note; - if (noiseSeek<0) noiseSeek=0; - chWrite(c.chan,0x07,chan[c.chan].noise?(0x80|(parent->song.properNoiseLayout?(noiseSeek&31):noiseFreq[noiseSeek%12])):0); } chan[c.chan].active=true; chan[c.chan].keyOn=true; - //chWrite(c.chan,0x04,0x80|chan[c.chan].vol); chan[c.chan].macroInit(ins); if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; @@ -365,7 +397,7 @@ int DivPlatformVB::dispatch(DivCommand c) { if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; if (chan[c.chan].active && !chan[c.chan].pcm) { - //chWrite(c.chan,0x04,0x80|chan[c.chan].outVol); + chWrite(c.chan,0x04,chan[c.chan].outVol<<4); } } } @@ -437,7 +469,7 @@ int DivPlatformVB::dispatch(DivCommand c) { break; case DIV_CMD_PANNING: { chan[c.chan].pan=(c.value&0xf0)|(c.value2>>4); - //chWrite(c.chan,0x05,isMuted[c.chan]?0:chan[c.chan].pan); + chWrite(c.chan,0x01,isMuted[c.chan]?0:chan[c.chan].pan); break; } case DIV_CMD_LEGATO: @@ -453,7 +485,7 @@ int DivPlatformVB::dispatch(DivCommand c) { chan[c.chan].inPorta=c.value; break; case DIV_CMD_GET_VOLMAX: - return 31; + return 15; break; case DIV_ALWAYS_SET_VOLUME: return 1; @@ -466,7 +498,7 @@ int DivPlatformVB::dispatch(DivCommand c) { void DivPlatformVB::muteChannel(int ch, bool mute) { isMuted[ch]=mute; - //chWrite(ch,0x05,isMuted[ch]?0:chan[ch].pan); + chWrite(ch,0x01,isMuted[ch]?0:chan[ch].pan); if (!isMuted[ch] && (chan[ch].pcm && chan[ch].dacSample!=-1)) { //chWrite(ch,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[ch].outVol)); //chWrite(ch,0x06,chan[ch].dacOut&0x1f); @@ -478,7 +510,7 @@ void DivPlatformVB::forceIns() { chan[i].insChanged=true; chan[i].freqChanged=true; updateWave(i); - //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); + chWrite(i,0x01,isMuted[i]?0:chan[i].pan); } } @@ -502,6 +534,10 @@ int DivPlatformVB::getRegisterPoolSize() { return 0x600; } +int DivPlatformVB::getRegisterPoolDepth() { + return 8; +} + void DivPlatformVB::reset() { while (!writes.empty()) writes.pop(); memset(regPool,0,0x600); @@ -525,6 +561,10 @@ void DivPlatformVB::reset() { lfoSpeed=255; // set per-channel initial panning for (int i=0; i<6; i++) { + chWrite(i,0x01,0xff); + chWrite(i,0x05,0x00); + chWrite(i,0x00,0x80); + chWrite(i,0x06,i); //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); } delay=500; @@ -538,6 +578,10 @@ bool DivPlatformVB::keyOffAffectsArp(int ch) { return true; } +float DivPlatformVB::getPostAmp() { + return 6.0f; +} + void DivPlatformVB::notifyWaveChange(int wave) { for (int i=0; i<6; i++) { if (chan[i].wave==wave) { @@ -556,7 +600,7 @@ void DivPlatformVB::notifyInsDeletion(void* ins) { void DivPlatformVB::setFlags(const DivConfig& flags) { chipClock=20000000.0; antiClickEnabled=!flags.getBool("noAntiClick",false); - rate=chipClock/16; + rate=chipClock/64; for (int i=0; i<6; i++) { oscBuf[i]->rate=rate; } diff --git a/src/engine/platform/vb.h b/src/engine/platform/vb.h index 7bb22965..d01b9c05 100644 --- a/src/engine/platform/vb.h +++ b/src/engine/platform/vb.h @@ -101,12 +101,14 @@ class DivPlatformVB: public DivDispatch { DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); + int getRegisterPoolDepth(); void reset(); void forceIns(); void tick(bool sysTick=true); void muteChannel(int ch, bool mute); bool isStereo(); bool keyOffAffectsArp(int ch); + float getPostAmp(); void setFlags(const DivConfig& flags); void notifyWaveChange(int wave); void notifyInsDeletion(void* ins); diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 101b71ad..51312eb1 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -4415,6 +4415,7 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_FDS || (ins->type==DIV_INS_SWAN && !ins->amiga.useSample) || (ins->type==DIV_INS_PCE && !ins->amiga.useSample) || + (ins->type==DIV_INS_VBOY) || ins->type==DIV_INS_SCC || ins->type==DIV_INS_SNES || ins->type==DIV_INS_NAMCO) { @@ -4666,7 +4667,7 @@ void FurnaceGUI::drawInsEdit() { } if (ins->type==DIV_INS_TIA || ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_SCC || ins->type==DIV_INS_PET || ins->type==DIV_INS_VIC || ins->type==DIV_INS_SEGAPCM || - ins->type==DIV_INS_FM) { + ins->type==DIV_INS_FM || ins->type==DIV_INS_VBOY) { dutyMax=0; } if (ins->type==DIV_INS_PCE || ins->type==DIV_INS_NAMCO) { @@ -4832,7 +4833,9 @@ void FurnaceGUI::drawInsEdit() { panMax=1; panSingle=true; } - if (ins->type==DIV_INS_X1_010 || ins->type==DIV_INS_PCE || ins->type==DIV_INS_MIKEY || ins->type==DIV_INS_SAA1099 || ins->type==DIV_INS_NAMCO || ins->type==DIV_INS_RF5C68) { + if (ins->type==DIV_INS_X1_010 || ins->type==DIV_INS_PCE || ins->type==DIV_INS_MIKEY || + ins->type==DIV_INS_SAA1099 || ins->type==DIV_INS_NAMCO || ins->type==DIV_INS_RF5C68 || + ins->type==DIV_INS_VBOY) { panMax=15; } if (ins->type==DIV_INS_SEGAPCM) { From b53319354cebc3a5a82716781648ac4f6d2a3d31 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 9 Oct 2022 00:00:00 -0500 Subject: [PATCH 54/57] Virtual Boy: VGM export --- src/engine/platform/vb.cpp | 8 ++++---- src/engine/sysDef.cpp | 2 +- src/engine/vgmOps.cpp | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/engine/platform/vb.cpp b/src/engine/platform/vb.cpp index 7f3893f2..9df6842b 100644 --- a/src/engine/platform/vb.cpp +++ b/src/engine/platform/vb.cpp @@ -25,7 +25,7 @@ #define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } #define chWrite(c,a,v) rWrite(0x400+((c)<<6)+((a)<<2),v); -#define CHIP_DIVIDER 64 +#define CHIP_DIVIDER 16 const char* regCheatSheetVB[]={ "Wave0", "000", @@ -75,7 +75,7 @@ const char* regCheatSheetVB[]={ "S4EV1", "514", "S4RAM", "518", - "S5SWP", "51C", + "S4SWP", "51C", "S5INT", "540", "S5LRV", "544", @@ -598,9 +598,9 @@ void DivPlatformVB::notifyInsDeletion(void* ins) { } void DivPlatformVB::setFlags(const DivConfig& flags) { - chipClock=20000000.0; + chipClock=5000000.0; antiClickEnabled=!flags.getBool("noAntiClick",false); - rate=chipClock/64; + rate=chipClock/16; for (int i=0; i<6; i++) { oscBuf[i]->rate=rate; } diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 86620976..2ba7098f 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1173,7 +1173,7 @@ void DivEngine::registerSystems() { ); sysDefs[DIV_SYSTEM_VBOY]=new DivSysDef( - "Virtual Boy", NULL, 0x9c, 0, 6, false, true, 0, false, 1U<writeC(write.val&0xff); } break; + case DIV_SYSTEM_VBOY: + w->writeC(0xc7); + w->writeS_BE(baseAddr2S|(write.addr>>2)); + w->writeC(write.val&0xff); + break; case DIV_SYSTEM_OPL: case DIV_SYSTEM_OPL_DRUMS: w->writeC(0x0b|baseAddr1); @@ -1217,6 +1222,17 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p howManyChips++; } break; + case DIV_SYSTEM_VBOY: + if (!hasVSU) { + hasVSU=disCont[i].dispatch->chipClock; + willExport[i]=true; + } else if (!(hasVSU&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasVSU|=0x40000000; + howManyChips++; + } + break; case DIV_SYSTEM_OPL: case DIV_SYSTEM_OPL_DRUMS: if (!hasOPL) { From 9c22b4671c8af7bdb7743b8deb27b9056045be6f Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 9 Oct 2022 01:14:02 -0500 Subject: [PATCH 55/57] improve playback hang detection logic --- src/engine/playback.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 00b1e31f..3912ebcb 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -1573,7 +1573,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi int attempts=0; int runLeftG=size<=100) { + if (attempts>=(int)size) { logE("hang detected! stopping! at %d seconds %d micro",totalSeconds,totalTicks); freelance=false; playing=false; From 9ff4d89c490ceeab09d31e8c43bcbf6517c86464 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 9 Oct 2022 04:04:43 -0500 Subject: [PATCH 56/57] Virtual Boy: fix mute --- src/engine/platform/vb.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/engine/platform/vb.cpp b/src/engine/platform/vb.cpp index 9df6842b..e3d5a6a0 100644 --- a/src/engine/platform/vb.cpp +++ b/src/engine/platform/vb.cpp @@ -559,13 +559,12 @@ void DivPlatformVB::reset() { sampleBank=0; lfoMode=0; lfoSpeed=255; - // set per-channel initial panning + // set per-channel initial values for (int i=0; i<6; i++) { - chWrite(i,0x01,0xff); + chWrite(i,0x01,isMuted[i]?0:chan[i].pan); chWrite(i,0x05,0x00); chWrite(i,0x00,0x80); chWrite(i,0x06,i); - //chWrite(i,0x05,isMuted[i]?0:chan[i].pan); } delay=500; } From 9ea72a321f68da1e7c592622924234d17eac476d Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 9 Oct 2022 04:05:44 -0500 Subject: [PATCH 57/57] I don't understand how am I going to do this this code looks too glued to Blip_Buffer --- src/engine/platform/sound/t6w28/T6W28_Apu.cpp | 70 ------------------- src/engine/platform/sound/t6w28/T6W28_Apu.h | 5 -- 2 files changed, 75 deletions(-) diff --git a/src/engine/platform/sound/t6w28/T6W28_Apu.cpp b/src/engine/platform/sound/t6w28/T6W28_Apu.cpp index 4414f619..110bb5ae 100644 --- a/src/engine/platform/sound/t6w28/T6W28_Apu.cpp +++ b/src/engine/platform/sound/t6w28/T6W28_Apu.cpp @@ -228,18 +228,6 @@ T6W28_Apu::~T6W28_Apu() { } -void T6W28_Apu::volume( double vol ) -{ - vol *= 0.85 / (osc_count * 64 * 2); - square_synth.volume( vol ); - noise.synth.volume( vol ); -} - -void T6W28_Apu::treble_eq( const blip_eq_t& eq ) -{ - square_synth.treble_eq( eq ); - noise.synth.treble_eq( eq ); -} void T6W28_Apu::osc_output( int index, Blip_Buffer* center, Blip_Buffer* left, Blip_Buffer* right ) { @@ -370,62 +358,4 @@ void T6W28_Apu::write_data_right( sms_time_t time, int data ) } } - -void T6W28_Apu::save_state(T6W28_ApuState *ret) -{ - for(int x = 0; x < 4; x++) - { - ret->delay[x] = oscs[x]->delay; - ret->volume_left[x] = oscs[x]->volume_left; - ret->volume_right[x] = oscs[x]->volume_right; - } - for(int x = 0; x < 3; x++) - { - ret->sq_period[x] = squares[x].period; - ret->sq_phase[x] = squares[x].phase; - } - ret->noise_shifter = noise.shifter; - ret->noise_tap = noise.tap; - ret->noise_period_extra = noise.period_extra; - - if(noise.period == &noise_periods[0]) - ret->noise_period = 0; - else if(noise.period == &noise_periods[1]) - ret->noise_period = 1; - else if(noise.period == &noise_periods[2]) - ret->noise_period = 2; - else ret->noise_period = 3; - - ret->latch_left = latch_left; - ret->latch_right = latch_right; -} - -void T6W28_Apu::load_state(const T6W28_ApuState *state) -{ - for(int x = 0; x < 4; x++) - { - oscs[x]->delay = state->delay[x] & ((x == 3) ? 0x7FFF : 0x3FFF); - oscs[x]->volume_left = state->volume_left[x]; - oscs[x]->volume_right = state->volume_right[x]; - } - for(int x = 0; x < 3; x++) - { - squares[x].period = state->sq_period[x] & 0x3FFF; - squares[x].phase = state->sq_phase[x]; - } - noise.shifter = state->noise_shifter; - noise.tap = state->noise_tap; - noise.period_extra = state->noise_period_extra & 0x3FFF; - - unsigned select = state->noise_period; - - if ( select < 3 ) - noise.period = &noise_periods [select]; - else - noise.period = &noise.period_extra; - - latch_left = state->latch_left; - latch_right = state->latch_right; -} - } diff --git a/src/engine/platform/sound/t6w28/T6W28_Apu.h b/src/engine/platform/sound/t6w28/T6W28_Apu.h index bd173948..e783fa81 100644 --- a/src/engine/platform/sound/t6w28/T6W28_Apu.h +++ b/src/engine/platform/sound/t6w28/T6W28_Apu.h @@ -32,11 +32,6 @@ typedef struct class T6W28_Apu { public: - // Set overall volume of all oscillators, where 1.0 is full volume - void volume( double ); - - // Set treble equalization - void treble_eq( const blip_eq_t& ); // Outputs can be assigned to a single buffer for mono output, or to three // buffers for stereo output (using Stereo_Buffer to do the mixing).