From 6589cd04b3c390d81ce504c7c6337259fdd60f1e Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 26 Oct 2025 21:02:05 -0500 Subject: [PATCH 01/34] prepare to add a reference player --- src/engine/filePlayer.cpp | 87 +++++++++++++++++++++++++++++++++++++++ src/engine/filePlayer.h | 69 +++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/engine/filePlayer.cpp create mode 100644 src/engine/filePlayer.h diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp new file mode 100644 index 000000000..dda2e9066 --- /dev/null +++ b/src/engine/filePlayer.cpp @@ -0,0 +1,87 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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 "filePlayer.h" + +#define DIV_FPCACHE_BLOCK_SHIFT 16 +#define DIV_FPCACHE_BLOCK_SIZE (1U< + +#ifdef HAVE_SNDFILE +#include "sfWrapper.h" +#else +typedef void SNDFILE; +struct SF_INFO { + int invalid; +}; +#endif + +class DivFilePlayer { + float** blocks; + String lastError; + SNDFILE* sf; + SF_INFO si; + + size_t playPos; + int outRate; + int rateAccum; + float volume; + bool playing; + bool fileError; + + std::mutex cacheMutex; + + void fillBlocksNear(size_t pos); + + public: + void mix(float** buf, int chans, unsigned int size); + size_t getPos(); + size_t setPos(size_t newPos, unsigned int offset=0); + + bool isPlaying(); + void play(unsigned int offset=0); + void stop(unsigned int offset=0); + bool closeFile(); + bool loadFile(const char* path); + + String getLastError(); + void setOutputRate(int rate); + float getVolume(); + void setVolume(float vol); + + DivFilePlayer(); + ~DivFilePlayer(); +}; From 9bbfdc6f439d78eaa965d968e31e9a857c8acae7 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 27 Oct 2025 01:43:03 -0500 Subject: [PATCH 02/34] ................... --- src/engine/filePlayer.cpp | 109 +++++++++++++++++++++++++++++++++++--- src/engine/filePlayer.h | 3 ++ 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index dda2e9066..94e335dc6 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -23,11 +23,63 @@ #define DIV_FPCACHE_BLOCK_SIZE (1U<>DIV_FPCACHE_BLOCK_SHIFT; + size_t lastBlock=firstBlock+DIV_FPCACHE_BLOCKS_FROM_FILL; + if (lastBlock>=numBlocks) lastBlock=numBlocks-1; + + // don't read if we're after end of file + if (firstBlock>lastBlock) return; + + bool needToFill=false; + for (size_t i=firstBlock; i<=lastBlock; i++) { + if (!blocks[i]) { + needToFill=true; + firstBlock=blocks[i]; + break; + } + } + + if (!needToFill) return; + + // check whether we need to seek + sf_count_seek curSeek=sf_seek(sf,0,SEEK_CUR); + if (curSeek==-1) { + // I/O error + fileError=true; + return; + } + + + + // read blocks + for (size_t i=firstBlock; i<=lastBlock; i++) { + if (!blocks[i]) { + blocks[i]=new float[DIV_FPCACHE_BLOCK_SIZE*si.channels]; + memset(blocks[i],0, + } + sf_count_t totalRead=sf_readf_float(sf,blocks[i],DIV_FPCACHE_BLOCK_SIZE); + if (totalRead>DIV_FPCACHE_BLOCK_SHIFT; + blocks=new float*[numBlocks]; + memset(blocks,0,numBlocks*sizeof(void*)); + + playPos=0; + rateAccum=0; + fileError=false; + + // read the entire file if not seekable + if (!si.seekable) { + for (size_t i=0; i Date: Mon, 27 Oct 2025 05:15:47 -0500 Subject: [PATCH 03/34] reference player prototype poor performance no playback sync yet --- CMakeLists.txt | 2 + papers/format.md | 1 + src/engine/engine.cpp | 20 ++++++ src/engine/engine.h | 18 ++++- src/engine/filePlayer.cpp | 142 +++++++++++++++++++++++++++++++++++--- src/engine/filePlayer.h | 6 ++ src/engine/playback.cpp | 18 +++++ src/gui/doAction.cpp | 7 ++ src/gui/gui.cpp | 31 +++++++++ src/gui/gui.h | 8 ++- src/gui/guiConst.cpp | 1 + src/gui/mixer.cpp | 15 +++- src/gui/refPlayer.cpp | 82 ++++++++++++++++++++++ 13 files changed, 336 insertions(+), 15 deletions(-) create mode 100644 src/gui/refPlayer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f64e92b0b..eff3bd2f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -791,6 +791,7 @@ src/engine/export.cpp src/engine/exportDef.cpp src/engine/fileOpsIns.cpp src/engine/fileOpsSample.cpp +src/engine/filePlayer.cpp src/engine/filter.cpp src/engine/instrument.cpp src/engine/macroInt.cpp @@ -994,6 +995,7 @@ src/gui/patManager.cpp src/gui/pattern.cpp src/gui/piano.cpp src/gui/presets.cpp +src/gui/refPlayer.cpp src/gui/regView.cpp src/gui/sampleEdit.cpp src/gui/scaling.cpp diff --git a/papers/format.md b/papers/format.md index 196f8078c..c92c7da3f 100644 --- a/papers/format.md +++ b/papers/format.md @@ -432,6 +432,7 @@ reserved input portsets: reserved output portsets: - `000` through `01F`: chip outputs +- `FFC`: reference file/music player (>=238) - `FFD`: wave/sample preview - `FFE`: metronome - `FFF`: "null" portset diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 9503106f1..190d9918d 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1648,6 +1648,16 @@ void DivEngine::getCommandStream(std::vector& where) { BUSY_END; } +DivFilePlayer* DivEngine::getFilePlayer() { + if (curFilePlayer==NULL) { + BUSY_BEGIN_SOFT; + curFilePlayer=new DivFilePlayer; + curFilePlayer->setOutputRate(got.rate); + BUSY_END; + } + return curFilePlayer; +} + void DivEngine::playSub(bool preserveDrift, int goalRow) { logV("playSub() called"); std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); @@ -3419,6 +3429,12 @@ void DivEngine::autoPatchbay() { } } + // file player + song.patchbay.reserve(DIV_MAX_OUTPUTS); + for (unsigned int j=0; j @@ -54,8 +55,8 @@ class DivWorkPool; #define DIV_UNSTABLE -#define DIV_VERSION "dev237" -#define DIV_ENGINE_VERSION 237 +#define DIV_VERSION "dev238" +#define DIV_ENGINE_VERSION 238 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 @@ -587,6 +588,7 @@ class DivEngine { int samp_temp, samp_prevSample; short* samp_bbIn; short* samp_bbOut; + unsigned char* metroTick; size_t metroTickLen; float* metroBuf; @@ -596,6 +598,10 @@ class DivEngine { float metroVol; float previewVol; + float* filePlayerBuf[DIV_MAX_OUTPUTS]; + size_t filePlayerBufLen; + DivFilePlayer* curFilePlayer; + size_t totalProcessed; unsigned int renderPoolThreads; @@ -738,12 +744,17 @@ class DivEngine { void createNewFromDefaults(); // load a file. bool load(unsigned char* f, size_t length, const char* nameHint=NULL); + // play a binary command stream. bool playStream(unsigned char* f, size_t length); // get the playing stream. DivCSPlayer* getStreamPlayer(); // destroy command stream player. bool killStream(); + + // get the audio file player. + DivFilePlayer* getFilePlayer(); + // save as .dmf. SafeWriter* saveDMF(unsigned char version); // save as .fur. @@ -1552,6 +1563,8 @@ class DivEngine { metroAmp(0.0f), metroVol(1.0f), previewVol(1.0f), + filePlayerBufLen(0), + curFilePlayer(NULL), totalProcessed(0), renderPoolThreads(0), renderPool(NULL), @@ -1584,6 +1597,7 @@ class DivEngine { memset(oscBuf,0,DIV_MAX_OUTPUTS*(sizeof(float*))); memset(exportChannelMask,1,DIV_MAX_CHANS*sizeof(bool)); memset(chipPeak,0,DIV_MAX_CHIPS*DIV_MAX_OUTPUTS*sizeof(float)); + memset(filePlayerBuf,0,DIV_MAX_OUTPUTS*sizeof(float)); for (int i=0; i #define DIV_FPCACHE_BLOCK_SHIFT 16 #define DIV_FPCACHE_BLOCK_SIZE (1U<>DIV_FPCACHE_BLOCK_SHIFT)!=firstBlock) { + // we need to seek + logV("- seeking"); + // we seek to a previous position in order to compensate for possible decoding differences when seeking + // (usually in lossy codecs) + sf_count_t seekWhere=firstBlock<>DIV_FPCACHE_BLOCK_SHIFT; + if (blockIndex!=lastBlock) { + fillBlocksNear(playPos); + lastBlock=blockIndex; + } + if (blockIndex>=numBlocks) { + // stop here + for (int j=0; j=si.channels) { + buf[j][i]=0.0f; + } else { + buf[j][i]=block[posInBlock++]*volume; + } + } + } + + // advance + rateAccum+=si.samplerate; + while (rateAccum>=outRate) { + rateAccum-=outRate; + playPos++; + if (playPos>=(size_t)si.frames) { + playPos=0; + } + } + } else { + for (int j=0; j>DIV_FPCACHE_BLOCK_SHIFT]!=NULL); +} + +bool DivFilePlayer::isLoaded() { + return (sf!=NULL); +} + +bool DivFilePlayer::isPlaying() { + return playing; +} + +void DivFilePlayer::play(unsigned int offset) { + logV("DivFilePlayer: playing"); playing=true; } -void DivFilePlayer::stop(unsigned int offset=0) { +void DivFilePlayer::stop(unsigned int offset) { + logV("DivFilePlayer: stopping"); playing=false; } @@ -116,24 +215,36 @@ bool DivFilePlayer::closeFile() { delete[] discardBuf; discardBuf=NULL; + + return true; } bool DivFilePlayer::loadFile(const char* path) { + if (sf!=NULL) closeFile(); + + logD("DivFilePlayer: opening file..."); sf=sfw.doOpen(path,SFM_READ,&si); if (sf==NULL) { + logE("could not open file!"); return false; } + logV("- samples: %d",si.frames); + logV("- channels: %d",si.channels); + logV("- rate: %d",si.samplerate); + numBlocks=(DIV_FPCACHE_BLOCK_MASK+si.frames)>>DIV_FPCACHE_BLOCK_SHIFT; blocks=new float*[numBlocks]; memset(blocks,0,numBlocks*sizeof(void*)); playPos=0; + lastBlock=SIZE_MAX; rateAccum=0; fileError=false; // read the entire file if not seekable if (!si.seekable) { + logV("file not seekable - reading..."); for (size_t i=0; iwait(); } + // process file player + // resize file player audio buffer if necessary + if (filePlayerBufLenmix(filePlayerBuf,outChans,size); + } + // process metronome // resize the metronome's audio buffer if necessary if (metroBufLenopenLoad( + _("Open Audio File"), + audioLoadFormats, + workingDirMusic, + dpiScale, + NULL, + false + ); + break; case GUI_FILE_TEST_OPEN: if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); hasOpened=fileDialog->openLoad( @@ -3841,6 +3852,7 @@ bool FurnaceGUI::loop() { DECLARE_METRIC(log) DECLARE_METRIC(effectList) DECLARE_METRIC(userPresets) + DECLARE_METRIC(refPlayer) DECLARE_METRIC(popup) #ifdef IS_MOBILE @@ -4453,6 +4465,7 @@ bool FurnaceGUI::loop() { IMPORT_CLOSE(memoryOpen); IMPORT_CLOSE(csPlayerOpen); IMPORT_CLOSE(userPresetsOpen); + IMPORT_CLOSE(refPlayerOpen); } else if (pendingLayoutImportStep==1) { // let the UI settle } else if (pendingLayoutImportStep==2) { @@ -4821,6 +4834,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem(_("effect list"),BIND_FOR(GUI_ACTION_WINDOW_EFFECT_LIST),effectListOpen)) effectListOpen=!effectListOpen; if (ImGui::MenuItem(_("play/edit controls"),BIND_FOR(GUI_ACTION_WINDOW_EDIT_CONTROLS),editControlsOpen)) editControlsOpen=!editControlsOpen; if (ImGui::MenuItem(_("piano/input pad"),BIND_FOR(GUI_ACTION_WINDOW_PIANO),pianoOpen)) pianoOpen=!pianoOpen; + if (ImGui::MenuItem(_("reference music player"),BIND_FOR(GUI_ACTION_WINDOW_REF_PLAYER),refPlayerOpen)) refPlayerOpen=!refPlayerOpen; if (spoilerOpen) if (ImGui::MenuItem(_("spoiler"),NULL,spoilerOpen)) spoilerOpen=!spoilerOpen; ImGui::EndMenu(); @@ -5043,6 +5057,7 @@ bool FurnaceGUI::loop() { MEASURE(memory,drawMemory()); MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); + MEASURE(refPlayer,drawRefPlayer()); MEASURE(patManager,drawPatManager()); } else { @@ -5088,6 +5103,7 @@ bool FurnaceGUI::loop() { MEASURE(log,drawLog()); MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); + MEASURE(refPlayer,drawRefPlayer()); } @@ -5239,6 +5255,9 @@ bool FurnaceGUI::loop() { case GUI_FILE_CMDSTREAM_OPEN: workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; break; + case GUI_FILE_MUSIC_OPEN: + workingDirMusic=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; case GUI_FILE_TEST_OPEN: case GUI_FILE_TEST_OPEN_MULTI: case GUI_FILE_TEST_SAVE: @@ -5877,6 +5896,11 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("Error while loading file! (%s)"),lastError)); } break; + case GUI_FILE_MUSIC_OPEN: + if (!e->getFilePlayer()->loadFile(copyOfName.c_str())) { + showError(fmt::sprintf(_("Error while loading file!"))); + } + break; case GUI_FILE_TEST_OPEN: showWarning(fmt::sprintf(_("You opened: %s"),copyOfName),GUI_WARN_GENERIC); break; @@ -8170,11 +8194,13 @@ void FurnaceGUI::syncState() { workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir); workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir); workingDirROMExport=e->getConfString("lastDirROMExport",workingDir); + workingDirROM=e->getConfString("lastDirROM",workingDir); workingDirFont=e->getConfString("lastDirFont",workingDir); workingDirColors=e->getConfString("lastDirColors",workingDir); workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir); workingDirLayout=e->getConfString("lastDirLayout",workingDir); workingDirConfig=e->getConfString("lastDirConfig",workingDir); + workingDirMusic=e->getConfString("lastDirMusic",workingDir); workingDirTest=e->getConfString("lastDirTest",workingDir); editControlsOpen=e->getConfBool("editControlsOpen",true); @@ -8216,6 +8242,7 @@ void FurnaceGUI::syncState() { findOpen=e->getConfBool("findOpen",false); spoilerOpen=e->getConfBool("spoilerOpen",false); userPresetsOpen=e->getConfBool("userPresetsOpen",false); + refPlayerOpen=e->getConfBool("refPlayerOpen",false); insListDir=e->getConfBool("insListDir",false); waveListDir=e->getConfBool("waveListDir",false); @@ -8333,11 +8360,13 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("lastDirAudioExport",workingDirAudioExport); conf.set("lastDirVGMExport",workingDirVGMExport); conf.set("lastDirROMExport",workingDirROMExport); + conf.set("lastDirROM",workingDirROM); conf.set("lastDirFont",workingDirFont); conf.set("lastDirColors",workingDirColors); conf.set("lastDirKeybinds",workingDirKeybinds); conf.set("lastDirLayout",workingDirLayout); conf.set("lastDirConfig",workingDirConfig); + conf.set("lastDirMusic",workingDirMusic); conf.set("lastDirTest",workingDirTest); // commit last open windows @@ -8376,6 +8405,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("findOpen",findOpen); conf.set("spoilerOpen",spoilerOpen); conf.set("userPresetsOpen",userPresetsOpen); + conf.set("refPlayerOpen",refPlayerOpen); // commit dir state conf.set("insListDir",insListDir); @@ -8779,6 +8809,7 @@ FurnaceGUI::FurnaceGUI(): csPlayerOpen(false), cvOpen(false), userPresetsOpen(false), + refPlayerOpen(false), cvNotSerious(false), shortIntro(false), insListDir(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 6aaaf741e..1d96a7047 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -567,6 +567,7 @@ enum FurnaceGUIWindows { GUI_WINDOW_MEMORY, GUI_WINDOW_CS_PLAYER, GUI_WINDOW_USER_PRESETS, + GUI_WINDOW_REF_PLAYER, GUI_WINDOW_SPOILER }; @@ -647,6 +648,7 @@ enum FurnaceGUIFileDialogs { GUI_FILE_TG100_ROM_OPEN, GUI_FILE_MU5_ROM_OPEN, GUI_FILE_CMDSTREAM_OPEN, + GUI_FILE_MUSIC_OPEN, GUI_FILE_TEST_OPEN, GUI_FILE_TEST_OPEN_MULTI, @@ -771,6 +773,7 @@ enum FurnaceGUIActions { GUI_ACTION_WINDOW_MEMORY, GUI_ACTION_WINDOW_CS_PLAYER, GUI_ACTION_WINDOW_USER_PRESETS, + GUI_ACTION_WINDOW_REF_PLAYER, GUI_ACTION_COLLAPSE_WINDOW, GUI_ACTION_CLOSE_WINDOW, @@ -1688,7 +1691,7 @@ class FurnaceGUI { String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport; String workingDirVGMExport, workingDirROMExport; String workingDirFont, workingDirColors, workingDirKeybinds; - String workingDirLayout, workingDirROM, workingDirTest; + String workingDirLayout, workingDirROM, workingDirMusic, workingDirTest; String workingDirConfig; String mmlString[32]; String mmlStringW, grooveString, grooveListString, mmlStringModTable; @@ -2392,7 +2395,7 @@ class FurnaceGUI { bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen; bool pianoOpen, notesOpen, channelsOpen, regViewOpen, logOpen, effectListOpen, chanOscOpen; bool subSongsOpen, findOpen, spoilerOpen, patManagerOpen, sysManagerOpen, clockOpen, speedOpen; - bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen; + bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen, refPlayerOpen; bool cvNotSerious; @@ -2992,6 +2995,7 @@ class FurnaceGUI { void drawTutorial(); void drawXYOsc(); void drawUserPresets(); + void drawRefPlayer(); float drawSystemChannelInfo(const DivSysDef* whichDef, int keyHitOffset=-1, float width=-1.0f); void drawSystemChannelInfoText(const DivSysDef* whichDef); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 8d1af66dc..f34292887 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -656,6 +656,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WINDOW_MEMORY", _N("Memory Composition"), 0), D("WINDOW_CS_PLAYER", _N("Command Stream Player"), 0), D("WINDOW_USER_PRESETS", _N("User Presets"), 0), + D("WINDOW_REF_PLAYER", _N("Reference Music Player"), 0), D("COLLAPSE_WINDOW", _N("Collapse/expand current window"), 0), D("CLOSE_WINDOW", _N("Close current window"), FURKMOD_SHIFT|SDLK_ESCAPE), diff --git a/src/gui/mixer.cpp b/src/gui/mixer.cpp index 4cb561723..cfc33a33a 100644 --- a/src/gui/mixer.cpp +++ b/src/gui/mixer.cpp @@ -333,8 +333,21 @@ void FurnaceGUI::drawMixer() { } } - // metronome/sample preview + // file player/metronome/sample preview if (displayInternalPorts) { + if (portSet(_("Music Player"),0xffc,0,16,0,16,selectedSubPort,portPos)) { + selectedPortSet=0xffc; + if (selectedSubPort>=0) { + portDragActive=true; + ImGui::InhibitInertialScroll(); + auto subPortI=portPos.find((selectedPortSet<<4)|selectedSubPort); + if (subPortI!=portPos.cend()) { + subPortPos=subPortI->second; + } else { + portDragActive=false; + } + } + } if (portSet(_("Sample Preview"),0xffd,0,1,0,1,selectedSubPort,portPos)) { selectedPortSet=0xffd; if (selectedSubPort>=0) { diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp new file mode 100644 index 000000000..3ef5a65a0 --- /dev/null +++ b/src/gui/refPlayer.cpp @@ -0,0 +1,82 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "gui.h" +#include +#include "imgui.h" +#include "IconsFontAwesome4.h" + +void FurnaceGUI::drawRefPlayer() { + if (nextWindow==GUI_WINDOW_REF_PLAYER) { + refPlayerOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!refPlayerOpen) return; + if (ImGui::Begin("Music Player",&refPlayerOpen,globalWinFlags,_("Music Player"))) { + DivFilePlayer* fp=e->getFilePlayer(); + + size_t playPos=fp->getPos(); + size_t minPos=0; + size_t maxPos=fp->getFileInfo().frames; + int fileRate=fp->getFileInfo().samplerate; + if (fileRate<1) fileRate=1; + int posHours=(playPos/fileRate)/3600; + int posMinutes=((playPos/fileRate)/60)%60; + int posSeconds=(playPos/fileRate)%60; + int posMillis=(1000*(playPos%fileRate))/fileRate; + ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::SliderScalar("##Position",ImGuiDataType_U64,&playPos,&minPos,&maxPos,"")) { + fp->setPos(playPos); + } + + if (ImGui::Button("Open")) { + openFileDialog(GUI_FILE_MUSIC_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FAST_BACKWARD)) { + fp->stop(); + fp->setPos(0); + } + ImGui::SameLine(); + if (fp->isPlaying()) { + pushToggleColors(true); + if (ImGui::Button(ICON_FA_PAUSE "##Pause")) { + fp->stop(); + } + popToggleColors(); + } else { + if (ImGui::Button(ICON_FA_PLAY "##Play")) { + fp->play(); + } + } + ImGui::SameLine(); + + float vol=fp->getVolume(); + if (ImGui::SliderFloat("Volume",&vol,0.0f,1.0f)) { + if (vol<0.0f) vol=0.0f; + if (vol>1.0f) vol=1.0f; + fp->setVolume(vol); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER; + ImGui::End(); +} From 7a7a871198efe4f75f4e70e270b470ee9219665f Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 27 Oct 2025 14:24:16 -0500 Subject: [PATCH 04/34] reference player work memory usage cap, garbage collection and sinc interpolation also ability to set playback position to a negative value --- src/engine/filePlayer.cpp | 264 +++++++++++++++++++++++++++++++------- src/engine/filePlayer.h | 32 ++++- src/engine/filter.cpp | 2 +- src/engine/playback.cpp | 4 + src/gui/editControls.cpp | 4 + src/gui/refPlayer.cpp | 2 + src/log.cpp | 2 +- 7 files changed, 254 insertions(+), 56 deletions(-) diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index 2542ce282..f27ba5705 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -18,17 +18,24 @@ */ #include "filePlayer.h" +#include "filter.h" #include "../ta-log.h" #include +#include -#define DIV_FPCACHE_BLOCK_SHIFT 16 -#define DIV_FPCACHE_BLOCK_SIZE (1U<>DIV_FPCACHE_BLOCK_SHIFT; - size_t lastBlock=firstBlock+DIV_FPCACHE_BLOCKS_FROM_FILL; - if (lastBlock>=numBlocks) lastBlock=numBlocks-1; + ssize_t firstBlock=pos>>DIV_FPCACHE_BLOCK_SHIFT; + ssize_t lastBlock=firstBlock+DIV_FPCACHE_BLOCKS_FROM_FILL; + if (firstBlock<0) firstBlock=0; + if (firstBlock>=(ssize_t)numBlocks) firstBlock=numBlocks-1; + if (lastBlock<0) lastBlock=0; + if (lastBlock>=(ssize_t)numBlocks) lastBlock=numBlocks-1; // don't read if we're after end of file if (firstBlock>lastBlock) return; bool needToFill=false; - for (size_t i=firstBlock; i<=lastBlock; i++) { + for (ssize_t i=firstBlock; i<=lastBlock; i++) { + if (i<0 || i>=(ssize_t)numBlocks) continue; if (!blocks[i]) { needToFill=true; firstBlock=i; @@ -67,7 +78,7 @@ void DivFilePlayer::fillBlocksNear(size_t pos) { return; } - if ((curSeek&DIV_FPCACHE_BLOCK_MASK)!=0 || (((size_t)curSeek)>>DIV_FPCACHE_BLOCK_SHIFT)!=firstBlock) { + if ((curSeek&DIV_FPCACHE_BLOCK_MASK)!=0 || (curSeek>>DIV_FPCACHE_BLOCK_SHIFT)!=firstBlock) { // we need to seek logV("- seeking"); // we seek to a previous position in order to compensate for possible decoding differences when seeking @@ -90,7 +101,7 @@ void DivFilePlayer::fillBlocksNear(size_t pos) { } // read blocks - for (size_t i=firstBlock; i<=lastBlock; i++) { + for (ssize_t i=firstBlock; i<=lastBlock; i++) { if (!blocks[i]) { blocks[i]=new float[DIV_FPCACHE_BLOCK_SIZE*si.channels]; memset(blocks[i],0,DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float)); @@ -103,6 +114,82 @@ void DivFilePlayer::fillBlocksNear(size_t pos) { } } +void DivFilePlayer::collectGarbage(ssize_t pos) { + // don't if file isn't present + if (!blocks) return; + + // don't if there was an I/O error + if (fileError) return; + + // don't if we cannot seek + if (!si.seekable) return; + + size_t memUsage=getMemUsage(); + if (memUsage>=DIV_FPCACHE_BLOCK_SHIFT; + if (pos<0) pos=0; + if (pos>=(ssize_t)numBlocks) pos=numBlocks-1; + + // collect garbage + // start with blocks before the given position + // then try with blocks after given position + // prioritize far away ones + // do not destroy priority blocks + for (ssize_t i=0; ipos+DIV_FPCACHE_BLOCKS_FROM_FILL; i--) { + if (!blocks[i]) continue; + if (priorityBlock[i]) continue; + logV("erasing block %d",(int)i); + float* block=blocks[i]; + blocks[i]=NULL; + delete[] block; + + memUsage-=DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float); + if (memUsage lock(cacheThreadLock); + + while (!quitThread) { + ssize_t wantBlockC=wantBlock; + if (wantBlockC!=DIV_NO_BLOCK) { + wantBlock=DIV_NO_BLOCK; + logV("thread fill %" PRIu64,wantBlockC); + fillBlocksNear(wantBlockC); + collectGarbage(wantBlockC); + } + cacheCV.wait(lock); + } + + threadHasQuit=true; + logV("DivFilePlayer: cache thread over."); +} + +float DivFilePlayer::getSampleAt(ssize_t pos, int ch) { + if (blocks==NULL) return 0.0f; + ssize_t blockIndex=pos>>DIV_FPCACHE_BLOCK_SHIFT; + if (blockIndex<0 || blockIndex>=(ssize_t)numBlocks) return 0.0f; + + float* block=blocks[blockIndex]; + size_t posInBlock=(pos&DIV_FPCACHE_BLOCK_MASK)*si.channels+ch; + if (block==NULL) return 0.0f; + + return block[posInBlock]; +} + void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { // fill with zero if we don't have a file if (sf==NULL) { @@ -112,40 +199,65 @@ void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { return; } - for (unsigned int i=0; i>DIV_FPCACHE_BLOCK_SHIFT; - if (blockIndex!=lastBlock) { - fillBlocksNear(playPos); - lastBlock=blockIndex; - } - if (blockIndex>=numBlocks) { - // stop here - for (int j=0; j>DIV_FPCACHE_BLOCK_SHIFT; + if (blockIndex!=lastWantBlock) { + wantBlock=playPos; + cacheCV.notify_one(); + lastWantBlock=blockIndex; + } + + if (playing) { + // sinc interpolation + float x[8]; + + unsigned int n=(8192*rateAccum)/outRate; + n&=8191; + float* t1=&sincTable[(8191-n)<<2]; + float* t2=&sincTable[n<<2]; + if (si.channels==1) { + // mono optimization + for (int k=0; k<8; k++) { + x[k]=getSampleAt(playPos+k-3,0); + } + + float s=( + x[0]*t2[3]+ + x[1]*t2[2]+ + x[2]*t2[1]+ + x[3]*t2[0]+ + x[4]*t1[0]+ + x[5]*t1[1]+ + x[6]*t1[2]+ + x[7]*t1[3] + )*volume; - // put - if (block==NULL) { for (int j=0; j=si.channels) { buf[j][i]=0.0f; + continue; } - } else if (si.channels==1) { - for (int j=0; j=si.channels) { - buf[j][i]=0.0f; - } else { - buf[j][i]=block[posInBlock++]*volume; - } + + for (int k=0; k<8; k++) { + x[k]=getSampleAt(playPos+k-3,j); } + buf[j][i]=( + x[0]*t2[3]+ + x[1]*t2[2]+ + x[2]*t2[1]+ + x[3]*t2[0]+ + x[4]*t1[0]+ + x[5]*t1[1]+ + x[6]*t1[2]+ + x[7]*t1[3] + )*volume; } // advance @@ -153,7 +265,7 @@ void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { while (rateAccum>=outRate) { rateAccum-=outRate; playPos++; - if (playPos>=(size_t)si.frames) { + if (playPos>=(ssize_t)si.frames) { playPos=0; } } @@ -165,18 +277,37 @@ void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { } } -size_t DivFilePlayer::getPos() { +ssize_t DivFilePlayer::getPos() { return playPos; } -size_t DivFilePlayer::setPos(size_t newPos, unsigned int offset) { +ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) { playPos=newPos; return playPos; } -bool DivFilePlayer::isBlockPresent(size_t pos) { +size_t DivFilePlayer::getMemUsage() { + if (blocks==NULL) return 0; + size_t ret=0; + for (size_t i=0; i>DIV_FPCACHE_BLOCK_SHIFT]!=NULL); + ssize_t which=pos>>DIV_FPCACHE_BLOCK_SHIFT; + if (which<0 || which>=(ssize_t)numBlocks) return false; + return (blocks[which]!=NULL); +} + +bool DivFilePlayer::setBlockPriority(ssize_t pos, bool priority) { + if (priorityBlock==NULL) return false; + ssize_t which=pos>>DIV_FPCACHE_BLOCK_SHIFT; + if (which<0 || which>=(ssize_t)numBlocks) return false; + priorityBlock[which]=priority; + return priority; } bool DivFilePlayer::isLoaded() { @@ -199,9 +330,27 @@ void DivFilePlayer::stop(unsigned int offset) { bool DivFilePlayer::closeFile() { if (sf==NULL) return false; + + logD("DivFilePlayer: closing file."); + + if (cacheThread) { + quitThread=true; + while (!threadHasQuit) { + cacheCV.notify_one(); + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // this join is guaranteed to work + cacheThread->join(); + delete cacheThread; + cacheThread=NULL; + } + sfw.doClose(); sf=NULL; playing=false; + quitThread=false; + threadHasQuit=false; for (size_t i=0; i>DIV_FPCACHE_BLOCK_SHIFT; blocks=new float*[numBlocks]; + priorityBlock=new bool[numBlocks]; memset(blocks,0,numBlocks*sizeof(void*)); + memset(priorityBlock,0,numBlocks*sizeof(bool)); + + // mark the first blocks as important + for (size_t i=0; i=numBlocks) break; + priorityBlock[i]=true; + } playPos=0; - lastBlock=SIZE_MAX; + lastWantBlock=DIV_NO_BLOCK; rateAccum=0; fileError=false; @@ -262,6 +419,13 @@ bool DivFilePlayer::loadFile(const char* path) { } discardBuf=new float[DIV_FPCACHE_DISCARD_SIZE*si.channels]; + + // stsrt the block cache thread + if (cacheThread==NULL) { + quitThread=false; + threadHasQuit=false; + cacheThread=new std::thread(&DivFilePlayer::runCacheThread,this); + } return true; } @@ -289,16 +453,22 @@ void DivFilePlayer::setVolume(float vol) { DivFilePlayer::DivFilePlayer(): discardBuf(NULL), blocks(NULL), + priorityBlock(NULL), numBlocks(0), sf(NULL), playPos(0), - lastBlock(SIZE_MAX), + lastWantBlock(DIV_NO_BLOCK), + wantBlock(DIV_NO_BLOCK), outRate(44100), rateAccum(0), volume(1.0f), playing(false), - fileError(false) { + fileError(false), + quitThread(false), + threadHasQuit(false), + cacheThread(NULL) { memset(&si,0,sizeof(SF_INFO)); + sincTable=DivFilterTables::getSincTable8(); } DivFilePlayer::~DivFilePlayer() { diff --git a/src/engine/filePlayer.h b/src/engine/filePlayer.h index dd4e72311..d0d57be74 100644 --- a/src/engine/filePlayer.h +++ b/src/engine/filePlayer.h @@ -20,7 +20,10 @@ #ifndef _FILEPLAYER_H #define _FILEPLAYER_H +#include "../ta-utils.h" +#include #include +#include #ifdef HAVE_SNDFILE #include "sfWrapper.h" @@ -32,32 +35,47 @@ struct SF_INFO { #endif class DivFilePlayer { + float* sincTable; float* discardBuf; float** blocks; + bool* priorityBlock; size_t numBlocks; String lastError; SFWrapper sfw; SNDFILE* sf; SF_INFO si; - size_t playPos; - size_t lastBlock; + ssize_t playPos; + ssize_t lastWantBlock; + ssize_t wantBlock; int outRate; int rateAccum; float volume; bool playing; bool fileError; + bool quitThread; + bool threadHasQuit; + std::thread* cacheThread; std::mutex cacheMutex; + std::mutex cacheThreadLock; + std::condition_variable cacheCV; - void fillBlocksNear(size_t pos); + void fillBlocksNear(ssize_t pos); + void collectGarbage(ssize_t pos); + float getSampleAt(ssize_t pos, int ch); public: + void runCacheThread(); + + size_t getMemUsage(); + void mix(float** buf, int chans, unsigned int size); - size_t getPos(); - size_t setPos(size_t newPos, unsigned int offset=0); + ssize_t getPos(); + ssize_t setPos(ssize_t newPos, unsigned int offset=0); - bool isBlockPresent(size_t pos); + bool isBlockPresent(ssize_t pos); + bool setBlockPriority(ssize_t pos, bool priority); bool isLoaded(); bool isPlaying(); void play(unsigned int offset=0); @@ -75,4 +93,4 @@ class DivFilePlayer { ~DivFilePlayer(); }; -#endif \ No newline at end of file +#endif diff --git a/src/engine/filter.cpp b/src/engine/filter.cpp index feac741aa..f0d60e7d5 100644 --- a/src/engine/filter.cpp +++ b/src/engine/filter.cpp @@ -128,4 +128,4 @@ float* DivFilterTables::getSincIntegralSmallTable() { } } return sincIntegralSmallTable; -} \ No newline at end of file +} diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index cb7fc01a8..a3887b01a 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -3248,6 +3248,10 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi } if (curFilePlayer!=NULL) { curFilePlayer->mix(filePlayerBuf,outChans,size); + } else { + for (int i=0; i1.0f) vol=1.0f; fp->setVolume(vol); } + + ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER; ImGui::End(); diff --git a/src/log.cpp b/src/log.cpp index 617a567b6..215b4ddfc 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -253,8 +253,8 @@ bool finishLogFile() { // flush logFileLockI.lock(); - logFileNotify.notify_one(); while (!iAmReallyDead) { + logFileNotify.notify_one(); std::this_thread::yield(); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } From 394c6c35aa4e8ac323d93b5e659bb0738e630d6c Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 27 Oct 2025 19:34:21 -0500 Subject: [PATCH 05/34] earliest implementation of sync does not loop correctly --- src/engine/engine.cpp | 29 +++++++++++++++++++++++++++++ src/engine/engine.h | 9 +++++++++ src/engine/filePlayer.cpp | 11 +++++++++++ src/engine/filePlayer.h | 1 + src/engine/playback.cpp | 2 +- src/gui/refPlayer.cpp | 22 +++++++++++++++++++--- 6 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 190d9918d..2ef21be66 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1658,6 +1658,14 @@ DivFilePlayer* DivEngine::getFilePlayer() { return curFilePlayer; } +bool DivEngine::getFilePlayerSync() { + return filePlayerSync; +} + +void DivEngine::setFilePlayerSync(bool doSync) { + filePlayerSync=doSync; +} + void DivEngine::playSub(bool preserveDrift, int goalRow) { logV("playSub() called"); std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); @@ -2008,6 +2016,12 @@ bool DivEngine::play() { output->midiOut->send(TAMidiMessage(TA_MIDI_MACHINE_PLAY,0,0)); } bool didItPlay=playing; + if (didItPlay) { + if (curFilePlayer && filePlayerSync) { + curFilePlayer->setPosSeconds(totalSeconds,totalTicks); + curFilePlayer->play(); + } + } BUSY_END; return didItPlay; } @@ -2024,6 +2038,12 @@ bool DivEngine::playToRow(int row) { keyHit[i]=false; } bool didItPlay=playing; + if (didItPlay) { + if (curFilePlayer && filePlayerSync) { + curFilePlayer->setPosSeconds(totalSeconds,totalTicks); + curFilePlayer->play(); + } + } BUSY_END; return didItPlay; } @@ -2079,6 +2099,10 @@ void DivEngine::stop() { } } + if (curFilePlayer && filePlayerSync) { + curFilePlayer->stop(); + } + // reset all chan oscs for (int i=0; igetOscBuffer(dispatchChanOfChan[i]); @@ -3646,6 +3670,11 @@ void DivEngine::setOrder(unsigned char order) { prevOrder=curOrder; if (playing && !freelance) { playSub(false); + + if (curFilePlayer && filePlayerSync) { + curFilePlayer->setPosSeconds(totalSeconds,totalTicks); + curFilePlayer->play(); + } } BUSY_END; } diff --git a/src/engine/engine.h b/src/engine/engine.h index 18dd4f822..07f07f10e 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -601,6 +601,9 @@ class DivEngine { float* filePlayerBuf[DIV_MAX_OUTPUTS]; size_t filePlayerBufLen; DivFilePlayer* curFilePlayer; + bool filePlayerSync; + ssize_t filePlayerCueSeconds; + unsigned int filePlayerCueMillis; size_t totalProcessed; @@ -754,6 +757,9 @@ class DivEngine { // get the audio file player. DivFilePlayer* getFilePlayer(); + // get whether the player is synchronized with song playback. + bool getFilePlayerSync(); + void setFilePlayerSync(bool doSync); // save as .dmf. SafeWriter* saveDMF(unsigned char version); @@ -1565,6 +1571,9 @@ class DivEngine { previewVol(1.0f), filePlayerBufLen(0), curFilePlayer(NULL), + filePlayerSync(true), + filePlayerCueSeconds(0), + filePlayerCueMillis(0), totalProcessed(0), renderPoolThreads(0), renderPool(NULL), diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index f27ba5705..c101908db 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -283,6 +283,17 @@ ssize_t DivFilePlayer::getPos() { ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) { playPos=newPos; + rateAccum=0; + wantBlock=playPos; + return playPos; +} + +ssize_t DivFilePlayer::setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset) { + if (sf==NULL) return 0; + double microsD=(double)si.samplerate*((double)micros/1000000.0); + playPos=seconds*si.samplerate+(int)microsD; + rateAccum=0; + wantBlock=playPos; return playPos; } diff --git a/src/engine/filePlayer.h b/src/engine/filePlayer.h index d0d57be74..61648264f 100644 --- a/src/engine/filePlayer.h +++ b/src/engine/filePlayer.h @@ -73,6 +73,7 @@ class DivFilePlayer { void mix(float** buf, int chans, unsigned int size); ssize_t getPos(); ssize_t setPos(ssize_t newPos, unsigned int offset=0); + ssize_t setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset=0); bool isBlockPresent(ssize_t pos); bool setBlockPriority(ssize_t pos, bool priority); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index a3887b01a..c8bb9df27 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -3246,7 +3246,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi } filePlayerBufLen=size; } - if (curFilePlayer!=NULL) { + if (curFilePlayer!=NULL && !exporting) { curFilePlayer->mix(filePlayerBuf,outChans,size); } else { for (int i=0; igetFilePlayer(); - size_t playPos=fp->getPos(); + bool playPosNegative=false; + ssize_t playPos=fp->getPos(); + if (playPos<0) { + playPos=-playPos; + playPosNegative=true; + } size_t minPos=0; size_t maxPos=fp->getFileInfo().frames; int fileRate=fp->getFileInfo().samplerate; @@ -41,7 +46,11 @@ void FurnaceGUI::drawRefPlayer() { int posMinutes=((playPos/fileRate)/60)%60; int posSeconds=(playPos/fileRate)%60; int posMillis=(1000*(playPos%fileRate))/fileRate; - ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + if (playPosNegative) { + ImGui::Text("-%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + } else { + ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + } ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::SliderScalar("##Position",ImGuiDataType_U64,&playPos,&minPos,&maxPos,"")) { @@ -69,9 +78,16 @@ void FurnaceGUI::drawRefPlayer() { } } ImGui::SameLine(); + + pushToggleColors(e->getFilePlayerSync()); + if (ImGui::Button(_("Sync"))) { + e->setFilePlayerSync(!e->getFilePlayerSync()); + } + popToggleColors(); float vol=fp->getVolume(); - if (ImGui::SliderFloat("Volume",&vol,0.0f,1.0f)) { + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::SliderFloat("##Volume",&vol,0.0f,1.0f)) { if (vol<0.0f) vol=0.0f; if (vol>1.0f) vol=1.0f; fp->setVolume(vol); From 25cb78b306b3c239cef1408db3d09e9fc6fb0b77 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 28 Oct 2025 03:05:43 -0500 Subject: [PATCH 06/34] fix playSub() inconsistency in low-latency mode --- src/engine/engine.cpp | 2 ++ src/engine/filePlayer.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 2ef21be66..43b1f8cf6 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1696,6 +1696,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { midiTimeDrift=0; if (!preserveDrift) { ticks=1; + subticks=0; tempoAccum=0; totalTicks=0; totalTicksOff=0; @@ -1789,6 +1790,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { cmdStream.clear(); std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); logV("playSub() took %dµs",std::chrono::duration_cast(timeEnd-timeStart).count()); + logV("and landed us at %d.%06d (%d ticks, %d:%d.%d)",totalSeconds,totalTicks,totalTicksR,curOrder,curRow,ticks); } /* diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index c101908db..c3c1d4857 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -285,6 +285,7 @@ ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) { playPos=newPos; rateAccum=0; wantBlock=playPos; + logD("DivFilePlayer: setPos(%" PRIi64 ")",newPos); return playPos; } @@ -294,6 +295,7 @@ ssize_t DivFilePlayer::setPosSeconds(ssize_t seconds, unsigned int micros, unsig playPos=seconds*si.samplerate+(int)microsD; rateAccum=0; wantBlock=playPos; + logD("DivFilePlayer: setPosSeconds(%" PRIi64 ".%06d)",seconds,micros); return playPos; } From d3c85ae748626e20b8bb7ff011d87185b0c30338 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 28 Oct 2025 05:31:50 -0500 Subject: [PATCH 07/34] prepare for DivSongTimestamps this will replace walkSong and findSongLength while offering more features --- src/engine/engine.cpp | 46 ++++++++++++++++++++++++++++++++++++++++--- src/engine/engine.h | 12 +++++++++-- src/engine/song.h | 7 +++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 43b1f8cf6..77ee45a0d 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1666,6 +1666,19 @@ void DivEngine::setFilePlayerSync(bool doSync) { filePlayerSync=doSync; } +void DivEngine::syncFilePlayer() { + if (curFilePlayer==NULL) return; + int finalSeconds=totalSeconds+filePlayerCueSeconds; + int finalMicros=totalTicks+filePlayerCueMicros; + + while (finalMicros>=1000000) { + finalMicros-=1000000; + finalSeconds++; + } + + curFilePlayer->setPosSeconds(finalSeconds,finalMicros); +} + void DivEngine::playSub(bool preserveDrift, int goalRow) { logV("playSub() called"); std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); @@ -2020,7 +2033,7 @@ bool DivEngine::play() { bool didItPlay=playing; if (didItPlay) { if (curFilePlayer && filePlayerSync) { - curFilePlayer->setPosSeconds(totalSeconds,totalTicks); + syncFilePlayer(); curFilePlayer->play(); } } @@ -2042,7 +2055,7 @@ bool DivEngine::playToRow(int row) { bool didItPlay=playing; if (didItPlay) { if (curFilePlayer && filePlayerSync) { - curFilePlayer->setPosSeconds(totalSeconds,totalTicks); + syncFilePlayer(); curFilePlayer->play(); } } @@ -2055,6 +2068,9 @@ void DivEngine::stepOne(int row) { BUSY_BEGIN_SOFT; freelance=false; playSub(false,row); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + } for (int i=0; iplay(); + } } } BUSY_END; @@ -3188,6 +3208,10 @@ void DivEngine::deepCloneOrder(int pos, bool where) { if (pos<=curOrder) curOrder++; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } } BUSY_END; @@ -3208,6 +3232,10 @@ void DivEngine::deleteOrder(int pos) { if (curOrder>=curSubSong->ordersLen) curOrder=curSubSong->ordersLen-1; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } BUSY_END; } @@ -3231,6 +3259,10 @@ void DivEngine::moveOrderUp(int& pos) { pos--; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } BUSY_END; } @@ -3254,6 +3286,10 @@ void DivEngine::moveOrderDown(int& pos) { pos++; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } BUSY_END; } @@ -3674,7 +3710,7 @@ void DivEngine::setOrder(unsigned char order) { playSub(false); if (curFilePlayer && filePlayerSync) { - curFilePlayer->setPosSeconds(totalSeconds,totalTicks); + syncFilePlayer(); curFilePlayer->play(); } } @@ -3697,6 +3733,10 @@ void DivEngine::updateSysFlags(int system, bool restart, bool render) { if (restart) { if (isPlaying()) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } else if (freelance) { reset(); } diff --git a/src/engine/engine.h b/src/engine/engine.h index 07f07f10e..6d3c54363 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -603,13 +603,16 @@ class DivEngine { DivFilePlayer* curFilePlayer; bool filePlayerSync; ssize_t filePlayerCueSeconds; - unsigned int filePlayerCueMillis; + unsigned int filePlayerCueMicros; size_t totalProcessed; unsigned int renderPoolThreads; DivWorkPool* renderPool; + // song timestamps + DivSongTimestamps* songTimestamps; + // MIDI stuff std::function midiCallback=[](const TAMidiMessage&) -> int {return -3;}; @@ -630,6 +633,8 @@ class DivEngine { void runMidiTime(int totalCycles=1); bool shallSwitchCores(); + void syncFilePlayer(); + void testFunction(); bool loadDMF(unsigned char* file, size_t len); @@ -874,6 +879,9 @@ class DivEngine { // find song length in rows (up to specified loop point), and find length of every order void findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector& orders, int& length); + // calculate all song timestamps + void calcSongTimestamps(); + // play (returns whether successful) bool play(); @@ -1573,7 +1581,7 @@ class DivEngine { curFilePlayer(NULL), filePlayerSync(true), filePlayerCueSeconds(0), - filePlayerCueMillis(0), + filePlayerCueMicros(0), totalProcessed(0), renderPoolThreads(0), renderPool(NULL), diff --git a/src/engine/song.h b/src/engine/song.h index 30121a6ac..3ebe7a831 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -245,6 +245,13 @@ struct DivEffectStorage { storageLen(0) {} }; +struct DivSongTimestamps { + int totalSeconds; + int totalMicros; + + // TODO +}; + struct DivSong { unsigned short version; bool isDMF; From 8c1c338e91b1b9cbdec26e3855f73a398787374b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 28 Oct 2025 21:07:21 -0500 Subject: [PATCH 08/34] DivSongTimestamps, part 1 this is actually a refactor it will replace walkSong and the other function and fix bugs in the process --- src/engine/engine.cpp | 6 + src/engine/engine.h | 3 - src/engine/song.cpp | 402 ++++++++++++++++++++++++++++++++++++++++++ src/engine/song.h | 55 +++++- 4 files changed, 456 insertions(+), 10 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 77ee45a0d..b4d34ce5a 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -213,6 +213,12 @@ void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, in } } +void DivEngine::calcSongTimestamps() { + if (curSubSong!=NULL) { + curSubSong->calcTimestamps(chans,song.grooves.song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior); + } +} + #define EXPORT_BUFSIZE 2048 double DivEngine::benchmarkPlayback() { diff --git a/src/engine/engine.h b/src/engine/engine.h index 6d3c54363..c62b2d13b 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -610,9 +610,6 @@ class DivEngine { unsigned int renderPoolThreads; DivWorkPool* renderPool; - // song timestamps - DivSongTimestamps* songTimestamps; - // MIDI stuff std::function midiCallback=[](const TAMidiMessage&) -> int {return -3;}; diff --git a/src/engine/song.cpp b/src/engine/song.cpp index 05d402055..7d33d6a82 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -20,7 +20,32 @@ #include "song.h" #include "../ta-log.h" +DivSongTimestamps::Timestamp DivSongTimestamps::getTimes(int order, int row) { + if (order<0 || order>=DIV_MAX_PATTERNS) return Timestamp(-1,0); + if (row<0 || row>=DIV_MAX_ROWS) return Timestamp(-1,0); + Timestamp* t=orders[order]; + if (t==NULL) return Timestamp(-1,0); + return t[row]; +} +DivSongTimestamps::DivSongTimestamps(): + totalSeconds(0), + totalMicros(0), + totalTicks(0), + isLoopDefined(false), + isLoopable(true) { + memset(orders,0,DIV_MAX_PATTERNS*sizeof(void*)); + memset(maxRow,0,DIV_MAX_PATTERNS); +} + +DivSongTimestamps::~DivSongTimestamps() { + for (int i=0; i& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat) { + // reduced version of the playback routine for calculation. + + // reset state + ts.totalSeconds=0; + ts.totalMicros=0; + ts.totalTicks=0; + ts.isLoopDefined=true; + ts.isLoopable=true; + + memset(ts.maxRow,0,DIV_MAX_PATTERNS); + + for (int i=0; i0) { + memset(wsWalked,255,32*firstPat); + } + int curOrder=firstPat; + int curRow=0; + int prevOrder=firstPat; + int prevRow=0; + DivGroovePattern curSpeeds=speeds; + int curVirtualTempoN=virtualTempoN; + int curVirtualTempoD=virtualTempoD; + int nextSpeed=curSpeeds.val[0]; + double divider=hz; + double totalMicrosOff=0.0; + int ticks=1; + int tempoAccum=0; + int curSpeed=0; + int changeOrd=-1; + int changePos=0; + unsigned char rowDelay[DIV_MAX_CHANS]; + unsigned char delayOrder[DIV_MAX_CHANS]; + unsigned char delayRow[DIV_MAX_CHANS]; + bool shallStopSched=false; + bool shallStop=false; + bool endOfSong=false; + bool rowChanged=false; + + memset(rowDelay,0,DIV_MAX_CHANS); + memset(delayOrder,0,DIV_MAX_CHANS); + memset(delayRow,0,DIV_MAX_CHANS); + if (divider<1) divider=1; + + auto tinyProcessRow=[&](int i, bool afterDelay) { + // if this is after delay, use the order/row where delay occurred + int whatOrder=afterDelay?delayOrder[i]:curOrder; + int whatRow=afterDelay?delayRow[i]:curRow; + DivPattern* pat=pat[i].getPattern(orders.ord[i][whatOrder],false); + // pre effects + if (!afterDelay) { + // set to true if we found an EDxx effect + bool returnAfterPre=false; + // check all effects + for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; + short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + + // empty effect value is the same as zero + if (effectVal==-1) effectVal=0; + effectVal&=255; + + switch (effect) { + case 0x09: // select groove pattern/speed 1 + if (grooves.empty()) { + // special case: sets speed 1 if the song lacks groove patterns + if (effectVal>0) curSpeeds.val[0]=effectVal; + } else { + // sets the groove pattern and resets current speed index + if (effectVal<(short)grooves.size()) { + curSpeeds=grooves[effectVal]; + curSpeed=0; + } + } + break; + case 0x0f: // speed 1/speed 2 + // if the value is 0 then ignore it + if (curSpeeds.len==2 && grooves.empty()) { + // if there are two speeds and no groove patterns, set the second speed + if (effectVal>0) curSpeeds.val[1]=effectVal; + } else { + // otherwise set the first speed + if (effectVal>0) curSpeeds.val[0]=effectVal; + } + break; + case 0xfd: // virtual tempo num + if (effectVal>0) virtualTempoN=effectVal; + break; + case 0xfe: // virtual tempo den + if (effectVal>0) virtualTempoD=effectVal; + break; + case 0x0b: // change order + // this actually schedules an order change + // we perform this change at the end of nextRow() + + // COMPAT FLAG: simultaneous jump treatment + if (changeOrd==-1 || jumpTreatment==0) { + changeOrd=effectVal; + if (jumpTreatment==1 || jumpTreatment==2) { + changePos=0; + } + } + break; + case 0x0d: // next order + // COMPAT FLAG: simultaneous jump treatment + if (jumpTreatment==2) { + // - 2: DefleMask (jump to next order unless it is the last one and ignoreJumpAtEnd is on) + if ((curOrder<(curSubSong->ordersLen-1) || !ignoreJumpAtEnd)) { + // changeOrd -2 means increase order by 1 + // it overrides a previous 0Bxx effect + changeOrd=-2; + changePos=effectVal; + } + } else if (jumpTreatment==1) { + // - 1: old Furnace (same as 2 but ignored if 0Bxx is present) + if (changeOrd<0 && (curOrder<(curSubSong->ordersLen-1) || !ignoreJumpAtEnd)) { + changeOrd=-2; + changePos=effectVal; + } + } else { + // - 0: normal + if (curOrder<(curSubSong->ordersLen-1) || !ignoreJumpAtEnd) { + // set the target order if not set, allowing you to use 0B and 0D regardless of position + if (changeOrd<0) { + changeOrd=-2; + } + changePos=effectVal; + } + } + break; + case 0xed: // delay + if (effectVal!=0) { + // COMPAT FLAG: cut/delay effect policy (delayBehavior) + // - 0: strict + // - delays equal or greater to the speed * timeBase are ignored + // - 1: strict old + // - delays equal or greater to the speed are ignored + // - 2: lax (default) + // - no delay is ever ignored unless overridden by another + bool comparison=(delayBehavior==1)?(effectVal<=nextSpeed):(effectVal<(nextSpeed*(curSubSong->timeBase+1))); + if (delayBehavior==2) comparison=true; + if (comparison) { + // set the delay row, order and timer + chan[i].rowDelay=effectVal; + chan[i].delayOrder=whatOrder; + chan[i].delayRow=whatRow; + + // this here was a compatibility hack for DefleMask... + // if the delay time happens to be equal to the speed, it'll + // result in "delay lock" which halts all row processing + // until another good EDxx effect is found + // for some reason this didn't occur on Neo Geo... + // this hack is disabled due to its dirtiness and the fact I + // don't feel like being compatible with a buggy tracker any further + if (effectVal==nextSpeed) { + //if (sysOfChan[i]!=DIV_SYSTEM_YM2610 && sysOfChan[i]!=DIV_SYSTEM_YM2610_EXT) chan[i].delayLocked=true; + } else { + chan[i].delayLocked=false; + } + + // once we're done with pre-effects, get out and don't process any further + returnAfterPre=true; + } else { + logV("higher than nextSpeed! %d>%d",effectVal,nextSpeed); + chan[i].delayLocked=false; + } + } + break; + } + } + // stop processing if EDxx was found + if (returnAfterPre) return; + } else { + //logV("honoring delay at position %d",whatRow); + } + + + // effects + for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; + short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + + // an empty effect value is treated as zero + if (effectVal==-1) effectVal=0; + effectVal&=255; + + // tempo/tick rate effects + switch (effect) { + case 0xc0: case 0xc1: case 0xc2: case 0xc3: // set tick rate + // Cxxx, where `xxx` is between 1 and 1023 + divider=(double)(((effect&0x3)<<8)|effectVal); + if (divider<1) divider=1; + break; + case 0xf0: // set tempo + divider=(double)effectVal*2.0/5.0; + if (divider<1) divider=1; + cycles=got.rate/divider; + break; + case 0xff: // stop song + shallStopSched=true; + break; + } + } + }; + + auto tinyNextRow=[&]() { + for (int i=0; i>3))&8191]|=1<<(curRow&7); + + // commit a pending jump if there is one + // otherwise, advance row position + if (changeOrd!=-1) { + // jump to order and reset position + curRow=changePos; + changePos=0; + // jump to next order if it is -2 + if (changeOrd==-2) changeOrd=curOrder+1; + curOrder=changeOrd; + + // if we're out of bounds, return to the beginning + // if this happens we're guaranteed to loop + if (curOrder>=ordersLen) { + curOrder=0; + ts.isLoopDefined=false; + endOfSong=true; + memset(wsWalked,0,8192); + } + changeOrd=-1; + } else if (++curRow>=patLen) { + // if we are here it means we reached the end of this pattern, so + // advance to next order unless the song is about to stop + if (shallStopSched) { + curRow=patLen-1; + } else { + // go to next order + curRow=0; + if (++curOrder>=ordersLen) { + logV("end of orders reached"); + ts.isLoopDefined=false; + endOfSong=true; + // the walked array is used for loop detection + // since we've reached the end, we are guaranteed to loop here, so + // just reset it. + memset(wsWalked,0,8192); + curOrder=0; + } + } + } + rowChanged=true; + + // new loop detection routine + // if we're stepping on a row we've already walked over, we found loop + // if the song is going to stop though, don't do anything + if (!endOfSong && wsWalked[((curOrder<<5)+(curRow>>3))&8191]&(1<<(curRow&7)) && !shallStopSched) { + logV("loop reached"); + endOfSong=true; + memset(wsWalked,0,8192); + } + + // perform speed alternation + // COMPAT FLAG: broken speed alternation + if (song.brokenSpeedSel) { + unsigned char speed2=(curSpeeds.len>=2)?curSpeeds.val[1]:curSpeeds.val[0]; + unsigned char speed1=curSpeeds.val[0]; + + // if the pattern length is odd and the current order is odd, use speed 2 for even rows and speed 1 for odd ones + // we subtract firstPat from curOrder as firstPat is used by a function which finds sub-songs + // (the beginning of a new sub-song will be in order 0) + if ((patLen&1) && (curOrder-firstPat)&1) { + ticks=((curRow&1)?speed2:speed1)*(timeBase+1); + nextSpeed=(curRow&1)?speed1:speed2; + } else { + ticks=((curRow&1)?speed1:speed2)*(timeBase+1); + nextSpeed=(curRow&1)?speed2:speed1; + } + } else { + // normal speed alternation + // set the number of ticks and cycle to the next speed + ticks=curSpeeds.val[curSpeed]*(timeBase+1); + curSpeed++; + if (curSpeed>=curSpeeds.len) curSpeed=0; + // cache the next speed for future operations + nextSpeed=curSpeeds.val[curSpeed]; + } + }; + + // MAKE IT WORK + while (!endOfSong) { + // store the previous position + prevOrder=curOrder; + prevRow=curRow; + + // cycle channels to find a tick rate/tempo change effect after delay + // (unfortunately Cxxx and F0xx are not pre-effects and obey EDxx) + for (int i=0; i0) { + if (--rowDelay[i]==0) { + tinyProcessRow(i,true); + } + } + } + + // run virtual tempo + tempoAccum+=curVirtualTempoN; + while (tempoAccum>=curVirtualTempoD) { + tempoAccum-=curVirtualTempoD; + // tick counter + if (--ticks<=0) { + if (shallStopSched) { + shallStop=true; + break; + } else if (endOfSong) { + break; + } + + // next row + tinyNextRow(); + break; + } + + // limit tempo accumulator + if (tempoAccum>1023) tempoAccum=1023; + } + + if (shallStop) { + // FFxx found - the song doesn't loop + ts.isLoopable=false; + ts.isLoopDefined=false; + break; + } + + // update playback time + double dt=divider*((double)virtualTempoN/(double)MAX(1,virtualTempoD)); + ts.totalTicks++; + + ts.totalMicros+=1000000/dt; + totalMicrosOff+=fmod(1000000.0,dt); + while (totalMicrosOff>=dt) { + totalMicrosOff-=dt; + ts.totalMicros++; + } + if (ts.totalMicros>=1000000) { + ts.totalMicros-=1000000; + // who's gonna play a song for 68 years? + if (ts.totalSeconds<0x7fffffff) ts.totalSeconds++; + } + + // log row time here + if (!endOfSong) { + if (rowChanged) { + if (ts.orders[curOrder]==NULL) ts.orders[curOrder]=new Timestamp[DIV_MAX_ROWS]; + ts.orders[curOrder][curRow]=Timestamp(ts.totalSeconds,ts.totalMicros); + rowChanged=false; + } + } + if (maxRow[curOrder]& orders, std::vector& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0); + /** + * calculate timestamps (loop position, song length and more). + */ + void calcTimestamps(int chans, std::vector& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat=0); + void clearData(); void removeUnusedPatterns(); void optimizePatterns(); @@ -245,13 +293,6 @@ struct DivEffectStorage { storageLen(0) {} }; -struct DivSongTimestamps { - int totalSeconds; - int totalMicros; - - // TODO -}; - struct DivSong { unsigned short version; bool isDMF; From 2f11128c8d3b947c38394739ec16505d5f83ea01 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 28 Oct 2025 21:13:32 -0500 Subject: [PATCH 09/34] DivSongTimestamps, part 2 compilation fixes --- src/engine/engine.cpp | 2 +- src/engine/song.cpp | 61 ++++++++++++++++--------------------------- 2 files changed, 23 insertions(+), 40 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index b4d34ce5a..c9474f494 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -215,7 +215,7 @@ void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, in void DivEngine::calcSongTimestamps() { if (curSubSong!=NULL) { - curSubSong->calcTimestamps(chans,song.grooves.song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior); + curSubSong->calcTimestamps(chans,song.grooves,song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior); } } diff --git a/src/engine/song.cpp b/src/engine/song.cpp index 7d33d6a82..de61d6a8a 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -347,19 +347,19 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove memset(delayRow,0,DIV_MAX_CHANS); if (divider<1) divider=1; - auto tinyProcessRow=[&](int i, bool afterDelay) { + auto tinyProcessRow=[&,this](int i, bool afterDelay) { // if this is after delay, use the order/row where delay occurred int whatOrder=afterDelay?delayOrder[i]:curOrder; int whatRow=afterDelay?delayRow[i]:curRow; - DivPattern* pat=pat[i].getPattern(orders.ord[i][whatOrder],false); + DivPattern* p=pat[i].getPattern(orders.ord[i][whatOrder],false); // pre effects if (!afterDelay) { // set to true if we found an EDxx effect bool returnAfterPre=false; // check all effects - for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; - short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; + short effectVal=p->newData[whatRow][DIV_PAT_FXVAL(j)]; // empty effect value is the same as zero if (effectVal==-1) effectVal=0; @@ -389,10 +389,10 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove } break; case 0xfd: // virtual tempo num - if (effectVal>0) virtualTempoN=effectVal; + if (effectVal>0) curVirtualTempoN=effectVal; break; case 0xfe: // virtual tempo den - if (effectVal>0) virtualTempoD=effectVal; + if (effectVal>0) curVirtualTempoD=effectVal; break; case 0x0b: // change order // this actually schedules an order change @@ -410,7 +410,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // COMPAT FLAG: simultaneous jump treatment if (jumpTreatment==2) { // - 2: DefleMask (jump to next order unless it is the last one and ignoreJumpAtEnd is on) - if ((curOrder<(curSubSong->ordersLen-1) || !ignoreJumpAtEnd)) { + if ((curOrder<(ordersLen-1) || !ignoreJumpAtEnd)) { // changeOrd -2 means increase order by 1 // it overrides a previous 0Bxx effect changeOrd=-2; @@ -418,13 +418,13 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove } } else if (jumpTreatment==1) { // - 1: old Furnace (same as 2 but ignored if 0Bxx is present) - if (changeOrd<0 && (curOrder<(curSubSong->ordersLen-1) || !ignoreJumpAtEnd)) { + if (changeOrd<0 && (curOrder<(ordersLen-1) || !ignoreJumpAtEnd)) { changeOrd=-2; changePos=effectVal; } } else { // - 0: normal - if (curOrder<(curSubSong->ordersLen-1) || !ignoreJumpAtEnd) { + if (curOrder<(ordersLen-1) || !ignoreJumpAtEnd) { // set the target order if not set, allowing you to use 0B and 0D regardless of position if (changeOrd<0) { changeOrd=-2; @@ -442,32 +442,16 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // - delays equal or greater to the speed are ignored // - 2: lax (default) // - no delay is ever ignored unless overridden by another - bool comparison=(delayBehavior==1)?(effectVal<=nextSpeed):(effectVal<(nextSpeed*(curSubSong->timeBase+1))); + bool comparison=(delayBehavior==1)?(effectVal<=nextSpeed):(effectVal<(nextSpeed*(timeBase+1))); if (delayBehavior==2) comparison=true; if (comparison) { // set the delay row, order and timer - chan[i].rowDelay=effectVal; - chan[i].delayOrder=whatOrder; - chan[i].delayRow=whatRow; - - // this here was a compatibility hack for DefleMask... - // if the delay time happens to be equal to the speed, it'll - // result in "delay lock" which halts all row processing - // until another good EDxx effect is found - // for some reason this didn't occur on Neo Geo... - // this hack is disabled due to its dirtiness and the fact I - // don't feel like being compatible with a buggy tracker any further - if (effectVal==nextSpeed) { - //if (sysOfChan[i]!=DIV_SYSTEM_YM2610 && sysOfChan[i]!=DIV_SYSTEM_YM2610_EXT) chan[i].delayLocked=true; - } else { - chan[i].delayLocked=false; - } + rowDelay[i]=effectVal; + delayOrder[i]=whatOrder; + delayRow[i]=whatRow; // once we're done with pre-effects, get out and don't process any further returnAfterPre=true; - } else { - logV("higher than nextSpeed! %d>%d",effectVal,nextSpeed); - chan[i].delayLocked=false; } } break; @@ -481,9 +465,9 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // effects - for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; - short effectVal=pat->newData[whatRow][DIV_PAT_FXVAL(j)]; + for (int j=0; jnewData[whatRow][DIV_PAT_FX(j)]; + short effectVal=p->newData[whatRow][DIV_PAT_FXVAL(j)]; // an empty effect value is treated as zero if (effectVal==-1) effectVal=0; @@ -499,7 +483,6 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove case 0xf0: // set tempo divider=(double)effectVal*2.0/5.0; if (divider<1) divider=1; - cycles=got.rate/divider; break; case 0xff: // stop song shallStopSched=true; @@ -508,7 +491,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove } }; - auto tinyNextRow=[&]() { + auto tinyNextRow=[&,this]() { for (int i=0; i& groove // perform speed alternation // COMPAT FLAG: broken speed alternation - if (song.brokenSpeedSel) { + if (brokenSpeedSel) { unsigned char speed2=(curSpeeds.len>=2)?curSpeeds.val[1]:curSpeeds.val[0]; unsigned char speed1=curSpeeds.val[0]; @@ -657,12 +640,12 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // log row time here if (!endOfSong) { if (rowChanged) { - if (ts.orders[curOrder]==NULL) ts.orders[curOrder]=new Timestamp[DIV_MAX_ROWS]; - ts.orders[curOrder][curRow]=Timestamp(ts.totalSeconds,ts.totalMicros); + if (ts.orders[curOrder]==NULL) ts.orders[curOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; + ts.orders[curOrder][curRow]=DivSongTimestamps::Timestamp(ts.totalSeconds,ts.totalMicros); rowChanged=false; } } - if (maxRow[curOrder] Date: Wed, 29 Oct 2025 02:39:52 -0500 Subject: [PATCH 10/34] DivSongTimestamps, part 3 --- src/engine/song.cpp | 18 +++++++++--------- src/engine/song.h | 2 ++ src/gui/debugWindow.cpp | 26 ++++++++++++++++++++++++++ src/gui/gui.cpp | 1 + src/gui/gui.h | 2 +- src/gui/pattern.cpp | 9 +++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/engine/song.cpp b/src/engine/song.cpp index de61d6a8a..f9810b8b4 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -621,6 +621,15 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove break; } + // log row time here + if (!endOfSong) { + if (rowChanged) { + if (ts.orders[prevOrder]==NULL) ts.orders[prevOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; + ts.orders[prevOrder][prevRow]=DivSongTimestamps::Timestamp(ts.totalSeconds,ts.totalMicros); + rowChanged=false; + } + } + // update playback time double dt=divider*((double)virtualTempoN/(double)MAX(1,virtualTempoD)); ts.totalTicks++; @@ -636,15 +645,6 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // who's gonna play a song for 68 years? if (ts.totalSeconds<0x7fffffff) ts.totalSeconds++; } - - // log row time here - if (!endOfSong) { - if (rowChanged) { - if (ts.orders[curOrder]==NULL) ts.orders[curOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; - ts.orders[curOrder][curRow]=DivSongTimestamps::Timestamp(ts.totalSeconds,ts.totalMicros); - rowChanged=false; - } - } if (ts.maxRow[curOrder]calcSongTimestamps(); + } + + DivSongTimestamps& ts=e->curSubSong->ts; + + ImGui::Text("song duration: %d.%06d (%d ticks; %d rows)",ts.totalSeconds,ts.totalMicros,ts.totalTicks,ts.totalRows); + if (ts.isLoopDefined) { + ImGui::Text("loop region is defined"); + } else { + ImGui::Text("no loop region"); + } + if (ts.isLoopable) { + ImGui::Text("song can loop"); + } else { + ImGui::Text("song will stop"); + } + + ImGui::Text("loop region: %d:%d - %d:%d",ts.loopStart.order,ts.loopStart.row,ts.loopEnd.order,ts.loopEnd.row); + ImGui::Text("loop start time: %d.%06d",ts.loopStartTime.seconds,ts.loopStartTime.micros); + + ImGui::Checkbox("Enable row timestamps (in pattern view)",&debugRowTimestamps); + + ImGui::TreePop(); + } if (ImGui::TreeNode("Sample Debug")) { for (int i=0; isong.sampleLen; i++) { DivSample* sample=e->getSample(i); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 12aa83ffc..1df5551ce 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -8636,6 +8636,7 @@ FurnaceGUI::FurnaceGUI(): audioEngineChanged(false), settingsChanged(false), debugFFT(false), + debugRowTimestamps(false), vgmExportVersion(0x171), vgmExportTrailingTicks(-1), vgmExportCorrectedRate(44100), diff --git a/src/gui/gui.h b/src/gui/gui.h index 1d96a7047..39bb08435 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1726,7 +1726,7 @@ class FurnaceGUI { bool safeMode; bool midiWakeUp; bool makeDrumkitMode; - bool audioEngineChanged, settingsChanged, debugFFT; + bool audioEngineChanged, settingsChanged, debugFFT, debugRowTimestamps; bool willExport[DIV_MAX_CHIPS]; int vgmExportVersion; int vgmExportTrailingTicks; diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index 94564817d..bd19d5f01 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -406,6 +406,15 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int for (int k=mustSetXOf; k<=chans; k++) { patChanX[k]=ImGui::GetCursorScreenPos().x; } + + if (debugRowTimestamps) { + DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(ord,i); + if (rowTS.seconds==-1) { + ImGui::Text("---"); + } else { + ImGui::Text("%d.%06d",rowTS.seconds,rowTS.micros); + } + } } void FurnaceGUI::drawPattern() { From cc5b1d150f944a98d232345a70f20ebb58a76255 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 03:55:57 -0500 Subject: [PATCH 11/34] DivSongTimestamps, part 4 --- src/engine/song.cpp | 79 +++++++++++++++++++++++++---------------- src/gui/debugWindow.cpp | 7 ++++ 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/engine/song.cpp b/src/engine/song.cpp index f9810b8b4..0552d9164 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -32,6 +32,7 @@ DivSongTimestamps::DivSongTimestamps(): totalSeconds(0), totalMicros(0), totalTicks(0), + totalRows(0), isLoopDefined(false), isLoopable(true) { memset(orders,0,DIV_MAX_PATTERNS*sizeof(void*)); @@ -301,6 +302,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove ts.totalSeconds=0; ts.totalMicros=0; ts.totalTicks=0; + ts.totalRows=0; ts.isLoopDefined=true; ts.isLoopable=true; @@ -339,6 +341,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove unsigned char delayRow[DIV_MAX_CHANS]; bool shallStopSched=false; bool shallStop=false; + bool songWillEnd=false; bool endOfSong=false; bool rowChanged=false; @@ -492,6 +495,14 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove }; auto tinyNextRow=[&,this]() { + // store the previous position + prevOrder=curOrder; + prevRow=curRow; + + if (songWillEnd) { + endOfSong=true; + } + for (int i=0; i& groove if (curOrder>=ordersLen) { curOrder=0; ts.isLoopDefined=false; - endOfSong=true; + songWillEnd=true; memset(wsWalked,0,8192); } changeOrd=-1; @@ -529,7 +540,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove if (++curOrder>=ordersLen) { logV("end of orders reached"); ts.isLoopDefined=false; - endOfSong=true; + songWillEnd=true; // the walked array is used for loop detection // since we've reached the end, we are guaranteed to loop here, so // just reset it. @@ -539,16 +550,16 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove } } rowChanged=true; + ts.totalRows++; // new loop detection routine // if we're stepping on a row we've already walked over, we found loop // if the song is going to stop though, don't do anything - if (!endOfSong && wsWalked[((curOrder<<5)+(curRow>>3))&8191]&(1<<(curRow&7)) && !shallStopSched) { + if (!songWillEnd && wsWalked[((curOrder<<5)+(curRow>>3))&8191]&(1<<(curRow&7)) && !shallStopSched) { logV("loop reached"); - endOfSong=true; + songWillEnd=true; memset(wsWalked,0,8192); } - // perform speed alternation // COMPAT FLAG: broken speed alternation if (brokenSpeedSel) { @@ -574,14 +585,15 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // cache the next speed for future operations nextSpeed=curSpeeds.val[curSpeed]; } + + if (songWillEnd && !endOfSong) { + ts.loopEnd.order=prevOrder; + ts.loopEnd.row=prevRow; + } }; // MAKE IT WORK while (!endOfSong) { - // store the previous position - prevOrder=curOrder; - prevRow=curRow; - // cycle channels to find a tick rate/tempo change effect after delay // (unfortunately Cxxx and F0xx are not pre-effects and obey EDxx) for (int i=0; i& groove } // log row time here - if (!endOfSong) { - if (rowChanged) { - if (ts.orders[prevOrder]==NULL) ts.orders[prevOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; - ts.orders[prevOrder][prevRow]=DivSongTimestamps::Timestamp(ts.totalSeconds,ts.totalMicros); - rowChanged=false; + if (rowChanged && !endOfSong) { + if (ts.orders[prevOrder]==NULL) { + ts.orders[prevOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; + for (int i=0; i=dt) { - totalMicrosOff-=dt; - ts.totalMicros++; - } - if (ts.totalMicros>=1000000) { - ts.totalMicros-=1000000; - // who's gonna play a song for 68 years? - if (ts.totalSeconds<0x7fffffff) ts.totalSeconds++; + ts.totalMicros+=1000000/dt; + totalMicrosOff+=fmod(1000000.0,dt); + while (totalMicrosOff>=dt) { + totalMicrosOff-=dt; + ts.totalMicros++; + } + if (ts.totalMicros>=1000000) { + ts.totalMicros-=1000000; + // who's gonna play a song for 68 years? + if (ts.totalSeconds<0x7fffffff) ts.totalSeconds++; + } } if (ts.maxRow[curOrder]curSubSong->ordersLen; i++) { + ImGui::Text("- Order %d: %d",i,ts.maxRow[i]); + } + ImGui::TreePop(); + } + ImGui::Checkbox("Enable row timestamps (in pattern view)",&debugRowTimestamps); ImGui::TreePop(); From c1175bcc35b9c6f88a75ae0a04b39ead5362ed77 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 04:21:31 -0500 Subject: [PATCH 12/34] DivSongTimestamps, part 5 deprecate walkSong and remove findSongLength --- src/engine/engine.cpp | 11 +- src/engine/engine.h | 5 +- src/engine/song.cpp | 265 ++---------------------------------------- src/engine/song.h | 10 -- src/gui/gui.cpp | 47 ++------ src/gui/gui.h | 1 - 6 files changed, 24 insertions(+), 315 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index c9474f494..ee9f599ff 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -203,13 +203,10 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) { if (curSubSong!=NULL) { - curSubSong->walk(loopOrder,loopRow,loopEnd,chans,song.jumpTreatment,song.ignoreJumpAtEnd); - } -} - -void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector& orders, int& length) { - if (curSubSong!=NULL) { - curSubSong->findLength(loopOrder,loopRow,fadeoutLen,rowsForFadeout,hasFFxx,orders,song.grooves,length,chans,song.jumpTreatment,song.ignoreJumpAtEnd); + curSubSong->calcTimestamps(chans,song.grooves,song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior); + loopOrder=curSubSong->ts.loopStart.order; + loopRow=curSubSong->ts.loopStart.row; + loopEnd=curSubSong->ts.loopEnd.order; } } diff --git a/src/engine/engine.h b/src/engine/engine.h index c62b2d13b..c44121166 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -870,12 +870,9 @@ class DivEngine { int convertPanSplitToLinearLR(unsigned char left, unsigned char right, int range); unsigned int convertPanLinearToSplit(int val, unsigned char bits, int range); - // find song loop position + // DEPRECATED - find song loop position void walkSong(int& loopOrder, int& loopRow, int& loopEnd); - // find song length in rows (up to specified loop point), and find length of every order - void findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector& orders, int& length); - // calculate all song timestamps void calcSongTimestamps(); diff --git a/src/engine/song.cpp b/src/engine/song.cpp index 0552d9164..c2bf52263 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -47,256 +47,10 @@ DivSongTimestamps::~DivSongTimestamps() { } } } -bool DivSubSong::walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat) { - loopOrder=0; - loopRow=0; - loopEnd=-1; - int nextOrder=-1; - int nextRow=0; - int effectVal=0; - int lastSuspectedLoopEnd=-1; - DivPattern* subPat[DIV_MAX_CHANS]; - unsigned char wsWalked[8192]; - memset(wsWalked,0,8192); - if (firstPat>0) { - memset(wsWalked,255,32*firstPat); - } - for (int i=firstPat; ilastSuspectedLoopEnd) { - lastSuspectedLoopEnd=i; - } - for (int j=nextRow; j>3))&8191]&(1<<(j&7))) { - loopOrder=i; - loopRow=j; - loopEnd=lastSuspectedLoopEnd; - return true; - } - for (int k=0; knewData[j][DIV_PAT_FXVAL(l)]; - if (effectVal<0) effectVal=0; - if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0x0d) { - if (jumpTreatment==2) { - if ((inewData[j][DIV_PAT_FX(l)]==0x0b) { - if (nextOrder==-1 || jumpTreatment==0) { - nextOrder=effectVal; - if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) { - nextRow=0; - } - changingOrder=true; - } - } - } - } - - wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7); - - if (nextOrder!=-1) { - i=nextOrder-1; - nextOrder=-1; - break; - } - } - } - return false; -} - -double calcRowLenInSeconds(const DivGroovePattern& speeds, float hz, int vN, int vD, int timeBaseFromSong) { - double hl=1; //count for 1 row - if (hl<=0.0) hl=4.0; - double timeBase=timeBaseFromSong+1; - double speedSum=0; - for (int i=0; i& orders_vec, std::vector& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat) { - length=0; - hasFFxx=false; - rowsForFadeout=0; - - float secondsPerThisRow=0.0f; - - DivGroovePattern curSpeeds=speeds; //simulate that we are playing the song, track all speed/BPM/tempo/engine rate changes - short curVirtualTempoN=virtualTempoN; - short curVirtualTempoD=virtualTempoD; - float curHz=hz; - double curDivider=(double)timeBase; - - double curLen=0.0; //how many seconds passed since the start of song - - int nextOrder=-1; - int nextRow=0; - int effectVal=0; - int lastSuspectedLoopEnd=-1; - DivPattern* subPat[DIV_MAX_CHANS]; - unsigned char wsWalked[8192]; - memset(wsWalked,0,8192); - if (firstPat>0) { - memset(wsWalked,255,32*firstPat); - } - for (int i=firstPat; ilastSuspectedLoopEnd) { - lastSuspectedLoopEnd=i; - } - for (int j=nextRow; j>3))&8191]&(1<<(j&7))) { - return; - } - for (int k=0; knewData[j][DIV_PAT_FXVAL(l)]; - if (effectVal<0) effectVal=0; - - if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0xff) { - hasFFxx=true; - - // FFxx makes YOU SHALL NOT PASS!!! move - orders_vec.push_back(j+1); // order len - length+=j+1; // add length of order to song length - - return; - } - - switch (subPat[k]->newData[j][DIV_PAT_FX(l)]) { - case 0x09: { // select groove pattern/speed 1 - if (grooves.empty()) { - if (effectVal>0) curSpeeds.val[0]=effectVal; - } else { - if (effectVal<(short)grooves.size()) { - curSpeeds=grooves[effectVal]; - //curSpeed=0; - } - } - break; - } - case 0x0f: { // speed 1/speed 2 - if (curSpeeds.len==2 && grooves.empty()) { - if (effectVal>0) curSpeeds.val[1]=effectVal; - } else { - if (effectVal>0) curSpeeds.val[0]=effectVal; - } - break; - } - case 0xfd: { // virtual tempo num - if (effectVal>0) curVirtualTempoN=effectVal; - break; - } - case 0xfe: { // virtual tempo den - if (effectVal>0) curVirtualTempoD=effectVal; - break; - } - case 0xf0: { // set Hz by tempo (set bpm) - curDivider=(double)effectVal*2.0/5.0; - if (curDivider<1) curDivider=1; - break; - } - } - - if (subPat[k]->newData[j][DIV_PAT_FX(l)]==0x0d) { - if (jumpTreatment==2) { - if ((inewData[j][DIV_PAT_FX(l)]==0x0b) { - if (nextOrder==-1 || jumpTreatment==0) { - nextOrder=effectVal; - if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) { - nextRow=0; - } - changingOrder=true; - } - } - } - } - - if (i>loopOrder || (i==loopOrder && j>loopRow)) { - // we count each row fadeout lasts. When our time is greater than fadeout length we successfully counted the number of fadeout rows - if (curLen<=fadeoutLen && fadeoutLen>0.0) { - secondsPerThisRow=calcRowLenInSeconds(speeds,curHz,curVirtualTempoN,curVirtualTempoD,curDivider); - curLen+=secondsPerThisRow; - rowsForFadeout++; - } - } - - wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7); - - if (nextOrder!=-1) { - i=nextOrder-1; - orders_vec.push_back(j+1); // order len - length+=j+1; // add length of order to song length - jumped=true; - nextOrder=-1; - break; - } - } - if (!jumped) { // if no jump occured we add full pattern length - orders_vec.push_back(patLen); // order len - length+=patLen; // add length of order to song length - } - } -} void DivSubSong::calcTimestamps(int chans, std::vector& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat) { // reduced version of the playback routine for calculation. + std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); // reset state ts.totalSeconds=0; @@ -647,7 +401,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove if (!endOfSong) { // update playback time - double dt=divider*((double)virtualTempoN/(double)MAX(1,virtualTempoD)); + double dt=divider;//*((double)virtualTempoN/(double)MAX(1,virtualTempoD)); ts.totalTicks++; ts.totalMicros+=1000000/dt; @@ -669,6 +423,9 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove ts.loopStart.order=prevOrder; ts.loopStart.row=prevRow; ts.loopStartTime=ts.getTimes(ts.loopStart.order,ts.loopStart.row); + + std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); + logV("calcTimestamps() took %dµs",std::chrono::duration_cast(timeEnd-timeStart).count()); } void DivSubSong::clearData() { @@ -802,24 +559,22 @@ void DivSong::findSubSongs(int chans) { for (DivSubSong* i: subsong) { std::vector subSongStart; std::vector subSongEnd; - int loopOrder=0; - int loopRow=0; - int loopEnd=-1; int curStart=-1; // find possible subsongs logD("finding subsongs..."); while (++curStartordersLen) { - if (!i->walk(loopOrder,loopRow,loopEnd,chans,jumpTreatment,ignoreJumpAtEnd,curStart)) break; + i->calcTimestamps(chans,grooves,jumpTreatment,ignoreJumpAtEnd,brokenSpeedSel,delayBehavior,curStart); + if (!i->ts.isLoopable) break; // make sure we don't pick the same range twice if (!subSongEnd.empty()) { - if (subSongEnd.back()==loopEnd) continue; + if (subSongEnd.back()==i->ts.loopEnd.order) continue; } - logV("found a subsong: %d-%d",curStart,loopEnd); + logV("found a subsong: %d-%d",curStart,i->ts.loopEnd.order); subSongStart.push_back(curStart); - subSongEnd.push_back(loopEnd); + subSongEnd.push_back(i->ts.loopEnd.order); } // if this is the only song, quit diff --git a/src/engine/song.h b/src/engine/song.h index 302519615..d3fca3a64 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -230,16 +230,6 @@ struct DivSubSong { // song timestamps DivSongTimestamps ts; - /** - * walk through the song and determine loop position. - */ - bool walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0); - - /** - * find song length in rows (up to specified loop point). - */ - void findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector& orders, std::vector& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0); - /** * calculate timestamps (loop position, song length and more). */ diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 1df5551ce..1e20e46ce 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -2717,20 +2717,15 @@ int FurnaceGUI::loadStream(String path) { void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { - songOrdersLengths.clear(); - int loopOrder=0; int loopRow=0; int loopEnd=0; e->walkSong(loopOrder,loopRow,loopEnd); - e->findSongLength(loopOrder,loopRow,audioExportOptions.fadeOut,songFadeoutSectionLength,songHasSongEndCommand,songOrdersLengths,songLength); // for progress estimation - - songLoopedSectionLength=songLength; - for (int i=0; icurSubSong->ts.isLoopable; + songLength=e->curSubSong->ts.totalRows; e->saveAudio(path.c_str(),audioExportOptions); @@ -6039,43 +6034,21 @@ bool FurnaceGUI::loop() { float* progressLambda=&curProgress; int curPosInRows=0; int* curPosInRowsLambda=&curPosInRows; - int loopsLeft=0; - int* loopsLeftLambda=&loopsLeft; - int totalLoops=0; - int* totalLoopsLambda=&totalLoops; int curFile=0; int* curFileLambda=&curFile; if (e->isExporting()) { e->lockEngine( - [this, progressLambda, curPosInRowsLambda, curFileLambda, loopsLeftLambda, totalLoopsLambda] () { - int curRow=0; int curOrder=0; - e->getCurSongPos(curRow, curOrder); + [this, progressLambda, curPosInRowsLambda, curFileLambda] () { *curFileLambda=0; e->getCurFileIndex(*curFileLambda); - *curPosInRowsLambda=curRow; - for (int i=0; igetLoopsLeft(*loopsLeftLambda); - e->getTotalLoops(*totalLoopsLambda); - if ((*totalLoopsLambda)!=(*loopsLeftLambda)) { // we are going 2nd, 3rd, etc. time through the song - *curPosInRowsLambda-=(songLength-songLoopedSectionLength); // a hack so progress bar does not jump? - } - if (e->getIsFadingOut()) { // we are in fadeout??? why it works like that bruh - // LIVE WITH IT damn it - *curPosInRowsLambda-=(songLength-songLoopedSectionLength); // a hack so progress bar does not jump? - } - } - if (totalLength<0.1) { - // DON'T - *progressLambda=0; - } else { - *progressLambda=(float)((*curPosInRowsLambda)+((*totalLoopsLambda)-(*loopsLeftLambda))*songLength+lengthOfOneFile*(*curFileLambda))/(float)totalLength; - } + *progressLambda=(double)e->getTotalSeconds()/(double)MAX(1,e->curSubSong->ts.totalSeconds); + // TODO: fix + *curPosInRowsLambda=0; } ); } - ImGui::Text(_("Row %d of %d"),curPosInRows+((totalLoops)-(loopsLeft))*songLength,lengthOfOneFile); + ImGui::Text(_("Row %d of %d"),songLength,lengthOfOneFile); if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles); ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0),fmt::sprintf("%.2f%%",curProgress*100.0f).c_str()); @@ -9274,8 +9247,6 @@ FurnaceGUI::FurnaceGUI(): memset(romExportAvail,0,sizeof(bool)*DIV_ROM_MAX); - songOrdersLengths.clear(); - strncpy(noteOffLabel,"OFF",32); strncpy(noteRelLabel,"===",32); strncpy(macroRelLabel,"REL",32); diff --git a/src/gui/gui.h b/src/gui/gui.h index 39bb08435..88232b8e2 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1821,7 +1821,6 @@ class FurnaceGUI { char emptyLabel[32]; char emptyLabel2[32]; - std::vector songOrdersLengths; // lengths of all orders (for drawing song export progress) int songLength; // length of all the song in rows int songLoopedSectionLength; // length of looped part of the song int songFadeoutSectionLength; // length of fading part of the song From 3b93c4e0ec597d97f0f943419f2cac8d1951ffca Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 04:53:00 -0500 Subject: [PATCH 13/34] DivSongTimestamps, part 6 replace walkSong with calcSongTimestamps in GUI --- src/gui/editing.cpp | 6 +++--- src/gui/gui.cpp | 16 +++++----------- src/gui/gui.h | 2 +- src/gui/orders.cpp | 10 +++++----- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 882506163..5a5af9db7 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -259,7 +259,7 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) { if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front(); } if (shallWalk) { - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); } // garbage collection @@ -2119,7 +2119,7 @@ void FurnaceGUI::doUndo() { } } } - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); break; } @@ -2197,7 +2197,7 @@ void FurnaceGUI::doRedo() { } } } - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); break; } diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 1e20e46ce..a175c85c0 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1278,7 +1278,7 @@ void FurnaceGUI::play(int row) { chanOscChan[i].pitch=0.0f; } memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float)); - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS); if (wasFollowing) { followPattern=true; @@ -1309,7 +1309,7 @@ void FurnaceGUI::setOrder(unsigned char order, bool forced) { void FurnaceGUI::stop() { bool wasPlaying=e->isPlaying(); - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); e->stop(); curNibble=false; orderNibble=false; @@ -1538,7 +1538,7 @@ void FurnaceGUI::orderInput(int num) { } } } - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); makeUndo(GUI_UNDO_CHANGE_ORDER); } } @@ -2559,7 +2559,7 @@ int FurnaceGUI::load(String path) { } pushRecentFile(path); // walk song - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); // do not auto-play a backup if (path.find(backupPath)!=0) { if (settings.playOnLoad==2 || (settings.playOnLoad==1 && wasPlaying)) { @@ -2717,10 +2717,7 @@ int FurnaceGUI::loadStream(String path) { void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); // TODO: fix! songFadeoutSectionLength=0; @@ -8711,9 +8708,6 @@ FurnaceGUI::FurnaceGUI(): soloChan(-1), orderEditMode(0), orderCursor(-1), - loopOrder(-1), - loopRow(-1), - loopEnd(-1), isClipping(0), newSongCategory(0), latchTarget(0), diff --git a/src/gui/gui.h b/src/gui/gui.h index 88232b8e2..60cc32da9 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2377,7 +2377,7 @@ class FurnaceGUI { FixedQueue pendingLayoutImportReopen; int curIns, curWave, curSample, curOctave, curOrder, playOrder, prevIns, oldRow, editStep, editStepCoarse, soloChan, orderEditMode, orderCursor; - int loopOrder, loopRow, loopEnd, isClipping, newSongCategory, latchTarget, undoOrder; + int isClipping, newSongCategory, latchTarget, undoOrder; int wheelX, wheelY, dragSourceX, dragSourceXFine, dragSourceY, dragSourceOrder, dragDestinationX, dragDestinationXFine, dragDestinationY, dragDestinationOrder, oldBeat, oldBar; int curGroove, exitDisabledTimer; int curPaletteChoice, curPaletteType; diff --git a/src/gui/orders.cpp b/src/gui/orders.cpp index ed4a823d9..2cd8acb7a 100644 --- a/src/gui/orders.cpp +++ b/src/gui/orders.cpp @@ -344,7 +344,7 @@ void FurnaceGUI::drawOrders() { ImGui::PopClipRect(); } ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); - bool highlightLoop=(i>=loopOrder && i<=loopEnd); + bool highlightLoop=(i>=e->curSubSong->ts.loopStart.order && i<=e->curSubSong->ts.loopEnd.order && e->curSubSong->ts.isLoopDefined); if (highlightLoop) ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,ImGui::GetColorU32(uiColors[GUI_COLOR_SONG_LOOP])); if (settings.orderRowsBase==1) { snprintf(selID,4096,"%.2X##O_S%.2x",i,i); @@ -392,7 +392,7 @@ void FurnaceGUI::drawOrders() { if (e->curOrders->ord[j][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[j][i]++; } }); - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { orderCursor=j; @@ -400,7 +400,7 @@ void FurnaceGUI::drawOrders() { } } else { setOrder(i); - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); if (orderEditMode!=0) { orderCursor=j; curNibble=false; @@ -441,7 +441,7 @@ void FurnaceGUI::drawOrders() { if (e->curOrders->ord[j][i]>0) e->curOrders->ord[j][i]--; } }); - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { orderCursor=j; @@ -449,7 +449,7 @@ void FurnaceGUI::drawOrders() { } } else { setOrder(i); - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); if (orderEditMode!=0) { orderCursor=j; curNibble=false; From f990dee0c1f5527614a47bc0d0840e71b206dadd Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 17:56:25 -0500 Subject: [PATCH 14/34] DivSongTimestamps, part 7 calculate timestamps on every change that is likely to require recalculation (inserting/altering/removing song control/speed effects, changing song speed, changing orders and so on) --- src/engine/playback.cpp | 4 ---- src/gui/commandPalette.cpp | 1 + src/gui/editing.cpp | 44 +++++++++++++++++++++++++++----------- src/gui/findReplace.cpp | 1 + src/gui/gui.cpp | 25 ++++++++++++++++++---- src/gui/gui.h | 1 + src/gui/orders.cpp | 4 ---- src/gui/speed.cpp | 12 +++++++++++ src/gui/sysManager.cpp | 2 ++ 9 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index d964a94f5..4b6e6f0ed 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -2572,10 +2572,6 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (stepPlay!=1) { if (!noAccum) { double dt=divider*tickMult; - // TODO: is this responsible for timing differences when skipping? - if (skipping) { - dt*=(double)virtualTempoN/(double)MAX(1,virtualTempoD); - } totalTicksR++; // despite the name, totalTicks is in microseconds... totalTicks+=1000000/dt; diff --git a/src/gui/commandPalette.cpp b/src/gui/commandPalette.cpp index f0586fe5f..f55673db8 100644 --- a/src/gui/commandPalette.cpp +++ b/src/gui/commandPalette.cpp @@ -381,6 +381,7 @@ void FurnaceGUI::drawPalette() { showError("cannot add chip! ("+e->getLastError()+")"); } else { MARK_MODIFIED; + recalcTimestamps=true; } ImGui::CloseCurrentPopup(); if (e->song.autoSystem) { diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 5a5af9db7..9df50c377 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -129,7 +129,6 @@ void FurnaceGUI::prepareUndo(ActionType action, UndoRegion region) { void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) { bool doPush=false; - bool shallWalk=false; UndoStep s; s.type=action; s.oldCursor=undoCursor; @@ -184,6 +183,7 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) { if (!s.ord.empty()) { doPush=true; } + recalcTimestamps=true; break; case GUI_UNDO_PATTERN_EDIT: case GUI_UNDO_PATTERN_DELETE: @@ -227,13 +227,29 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) { s.pat.push_back(UndoPatternData(subSong,i,e->curOrders->ord[i][h],j,k,op->newData[j][k],p->newData[j][k])); if (k>=DIV_PAT_FX(0)) { - if (op->newData[j][k&(~1)]==0x0b || - p->newData[j][k&(~1)]==0x0b || - op->newData[j][k&(~1)]==0x0d || - p->newData[j][k&(~1)]==0x0d || - op->newData[j][k&(~1)]==0xff || - p->newData[j][k&(~1)]==0xff) { - shallWalk=true; + int fxCol=(k&1)?k:(k-1); + if (op->newData[j][fxCol]==0x09 || + op->newData[j][fxCol]==0x0b || + op->newData[j][fxCol]==0x0d || + op->newData[j][fxCol]==0x0f || + op->newData[j][fxCol]==0xc0 || + op->newData[j][fxCol]==0xc1 || + op->newData[j][fxCol]==0xc2 || + op->newData[j][fxCol]==0xc3 || + op->newData[j][fxCol]==0xf0 || + op->newData[j][fxCol]==0xff || + p->newData[j][fxCol]==0x09 || + p->newData[j][fxCol]==0x0b || + p->newData[j][fxCol]==0x0d || + p->newData[j][fxCol]==0x0f || + p->newData[j][fxCol]==0xc0 || + p->newData[j][fxCol]==0xc1 || + p->newData[j][fxCol]==0xc2 || + p->newData[j][fxCol]==0xc3 || + p->newData[j][fxCol]==0xf0 || + p->newData[j][fxCol]==0xff) { + logV("recalcTimestamps due to speed effect."); + recalcTimestamps=true; } } @@ -258,9 +274,6 @@ void FurnaceGUI::makeUndo(ActionType action, UndoRegion region) { redoHist.clear(); if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front(); } - if (shallWalk) { - e->calcSongTimestamps(); - } // garbage collection for (std::pair i: oldPatMap) { @@ -1798,6 +1811,7 @@ void FurnaceGUI::doCollapseSong(int divider) { redoHist.clear(); if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front(); } + recalcTimestamps=true; if (e->isPlaying()) e->play(); } @@ -1874,6 +1888,7 @@ void FurnaceGUI::doExpandSong(int multiplier) { redoHist.clear(); if (undoHist.size()>settings.maxUndoSteps) undoHist.pop_front(); } + recalcTimestamps=true; if (e->isPlaying()) e->play(); } @@ -2065,6 +2080,7 @@ void FurnaceGUI::moveSelected(int x, int y) { // replace cursor=selStart; doPaste(GUI_PASTE_MODE_OVERFLOW,0,false,c); + recalcTimestamps=true; makeUndo(GUI_UNDO_PATTERN_DRAG,UndoRegion(firstOrder,0,0,lastOrder,e->getTotalChannelCount()-1,e->curSubSong->patLen-1)); } @@ -2119,10 +2135,11 @@ void FurnaceGUI::doUndo() { } } } - e->calcSongTimestamps(); break; } + recalcTimestamps=true; + bool shallReplay=false; for (UndoOtherData& i: us.other) { switch (i.target) { @@ -2197,10 +2214,11 @@ void FurnaceGUI::doRedo() { } } } - e->calcSongTimestamps(); break; } + recalcTimestamps=true; + bool shallReplay=false; for (UndoOtherData& i: us.other) { switch (i.target) { diff --git a/src/gui/findReplace.cpp b/src/gui/findReplace.cpp index 2c595993d..d48567534 100644 --- a/src/gui/findReplace.cpp +++ b/src/gui/findReplace.cpp @@ -459,6 +459,7 @@ void FurnaceGUI::doReplace() { if (!curQueryResults.empty()) { MARK_MODIFIED; } + recalcTimestamps=true; if (!us.pat.empty()) { undoHist.push_back(us); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index a175c85c0..bc4444a9b 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1278,7 +1278,6 @@ void FurnaceGUI::play(int row) { chanOscChan[i].pitch=0.0f; } memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float)); - e->calcSongTimestamps(); memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS); if (wasFollowing) { followPattern=true; @@ -1309,7 +1308,6 @@ void FurnaceGUI::setOrder(unsigned char order, bool forced) { void FurnaceGUI::stop() { bool wasPlaying=e->isPlaying(); - e->calcSongTimestamps(); e->stop(); curNibble=false; orderNibble=false; @@ -1375,7 +1373,7 @@ void FurnaceGUI::noteInput(int num, int key, int vol) { } } - logV("chan %d, %d:%d %d/%d",ch,ord,y,tick,speed); + logV("noteInput: chan %d, %d:%d %d/%d",ch,ord,y,tick,speed); DivPattern* pat=e->curPat[ch].getPattern(e->curOrders->ord[ch][ord],true); bool removeIns=false; @@ -1428,6 +1426,8 @@ void FurnaceGUI::valueInput(int num, bool direct, int target) { int ord=curOrder; int y=cursor.y; + logV("valueInput: chan %d, %d:%d",ch,ord,y); + if (e->isPlaying() && !e->isStepping() && followPattern) { e->getPlayPos(ord,y); } @@ -1538,7 +1538,6 @@ void FurnaceGUI::orderInput(int num) { } } } - e->calcSongTimestamps(); makeUndo(GUI_UNDO_CHANGE_ORDER); } } @@ -4644,6 +4643,7 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError())); } else { MARK_MODIFIED; + recalcTimestamps=true; } ImGui::CloseCurrentPopup(); if (e->song.autoSystem) { @@ -4673,6 +4673,7 @@ bool FurnaceGUI::loop() { if (picked!=DIV_SYSTEM_NULL) { if (e->changeSystem(i,picked,preserveChanPos)) { MARK_MODIFIED; + recalcTimestamps=true; if (e->song.autoSystem) { autoDetectSystem(); } @@ -4697,6 +4698,7 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("cannot remove chip! (%s)"),e->getLastError())); } else { MARK_MODIFIED; + recalcTimestamps=true; } if (e->song.autoSystem) { autoDetectSystem(); @@ -6496,6 +6498,7 @@ bool FurnaceGUI::loop() { selStart.order=0; selEnd.order=0; MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Current subsong"))) { @@ -6509,6 +6512,7 @@ bool FurnaceGUI::loop() { selStart.order=0; selEnd.order=0; MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Orders"))) { @@ -6523,6 +6527,7 @@ bool FurnaceGUI::loop() { selStart.order=0; selEnd.order=0; MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Pattern"))) { @@ -6534,6 +6539,7 @@ bool FurnaceGUI::loop() { } }); MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Instruments"))) { @@ -6577,6 +6583,7 @@ bool FurnaceGUI::loop() { e->curSubSong->rearrangePatterns(); }); MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Remove unused patterns"))) { @@ -6585,6 +6592,7 @@ bool FurnaceGUI::loop() { e->curSubSong->removeUnusedPatterns(); }); MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Remove unused instruments"))) { @@ -6638,6 +6646,7 @@ bool FurnaceGUI::loop() { selStart=cursor; selEnd=cursor; curOrder=0; + recalcTimestamps=true; MARK_MODIFIED; } ImGui::CloseCurrentPopup(); @@ -6656,6 +6665,7 @@ bool FurnaceGUI::loop() { MARK_MODIFIED; } updateROMExportAvail(); + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); @@ -7368,6 +7378,12 @@ bool FurnaceGUI::loop() { } } + if (recalcTimestamps) { + logV("need to recalc timestamps..."); + e->calcSongTimestamps(); + recalcTimestamps=false; + } + sampleMapWaitingInput=(curWindow==GUI_WINDOW_INS_EDIT && sampleMapFocused); curWindowThreadSafe=curWindow; @@ -8582,6 +8598,7 @@ FurnaceGUI::FurnaceGUI(): noteInputPoly(true), notifyWaveChange(false), notifySampleChange(false), + recalcTimestamps(false), wantScrollListIns(false), wantScrollListWave(false), wantScrollListSample(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 60cc32da9..826909d71 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1714,6 +1714,7 @@ class FurnaceGUI { bool portrait, injectBackUp, mobileMenuOpen, warnColorPushed; bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu; bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, notifyWaveChange, notifySampleChange; + bool recalcTimestamps; bool wantScrollListIns, wantScrollListWave, wantScrollListSample; bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString; bool displayPendingSamples, replacePendingSample; diff --git a/src/gui/orders.cpp b/src/gui/orders.cpp index 2cd8acb7a..406d0237c 100644 --- a/src/gui/orders.cpp +++ b/src/gui/orders.cpp @@ -392,7 +392,6 @@ void FurnaceGUI::drawOrders() { if (e->curOrders->ord[j][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[j][i]++; } }); - e->calcSongTimestamps(); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { orderCursor=j; @@ -400,7 +399,6 @@ void FurnaceGUI::drawOrders() { } } else { setOrder(i); - e->calcSongTimestamps(); if (orderEditMode!=0) { orderCursor=j; curNibble=false; @@ -441,7 +439,6 @@ void FurnaceGUI::drawOrders() { if (e->curOrders->ord[j][i]>0) e->curOrders->ord[j][i]--; } }); - e->calcSongTimestamps(); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { orderCursor=j; @@ -449,7 +446,6 @@ void FurnaceGUI::drawOrders() { } } else { setOrder(i); - e->calcSongTimestamps(); if (orderEditMode!=0) { orderCursor=j; curNibble=false; diff --git a/src/gui/speed.cpp b/src/gui/speed.cpp index 02e04481a..c680b506f 100644 --- a/src/gui/speed.cpp +++ b/src/gui/speed.cpp @@ -58,6 +58,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (setHz<1) setHz=1; if (setHz>999) setHz=999; e->setSongRate(setHz); + recalcTimestamps=true; } if (tempoView) { ImGui::SameLine(); @@ -82,6 +83,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { e->curSubSong->speeds.len=1; }); if (e->isPlaying()) play(); + recalcTimestamps=true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("click for one speed")); @@ -94,6 +96,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { e->curSubSong->speeds.val[3]=e->curSubSong->speeds.val[1]; }); if (e->isPlaying()) play(); + recalcTimestamps=true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("click for groove pattern")); @@ -105,6 +108,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { e->curSubSong->speeds.val[1]=e->curSubSong->speeds.val[0]; }); if (e->isPlaying()) play(); + recalcTimestamps=true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("click for two (alternating) speeds")); @@ -138,6 +142,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { e->curSubSong->speeds.val[i]=intVersion[i]; } }); + recalcTimestamps=true; if (e->isPlaying()) play(); MARK_MODIFIED; } @@ -151,6 +156,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (ImGui::InputScalar("##Speed1",ImGuiDataType_U8,&e->curSubSong->speeds.val[0],&_ONE,&_THREE)) { MARK_MODIFIED if (e->curSubSong->speeds.val[0]<1) e->curSubSong->speeds.val[0]=1; if (e->isPlaying()) play(); + recalcTimestamps=true; } if (e->curSubSong->speeds.len>1) { ImGui::SameLine(); @@ -158,6 +164,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (ImGui::InputScalar("##Speed2",ImGuiDataType_U8,&e->curSubSong->speeds.val[1],&_ONE,&_THREE)) { MARK_MODIFIED if (e->curSubSong->speeds.val[1]<1) e->curSubSong->speeds.val[1]=1; if (e->isPlaying()) play(); + recalcTimestamps=true; } } } @@ -172,6 +179,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (e->curSubSong->virtualTempoN<1) e->curSubSong->virtualTempoN=1; if (e->curSubSong->virtualTempoN>255) e->curSubSong->virtualTempoN=255; e->virtualTempoChanged(); + recalcTimestamps=true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("Numerator")); @@ -182,6 +190,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (e->curSubSong->virtualTempoD<1) e->curSubSong->virtualTempoD=1; if (e->curSubSong->virtualTempoD>255) e->curSubSong->virtualTempoD=255; e->virtualTempoChanged(); + recalcTimestamps=true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("Denominator (set to base tempo)")); @@ -198,6 +207,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (realTB<1) realTB=1; if (realTB>16) realTB=16; e->curSubSong->timeBase=realTB-1; + recalcTimestamps=true; } ImGui::SameLine(); ImGui::Text("%.2f BPM",calcBPM(e->curSubSong->speeds,e->curSubSong->hz,e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD)); @@ -237,6 +247,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (patLen<1) patLen=1; if (patLen>DIV_MAX_PATTERNS) patLen=DIV_MAX_PATTERNS; e->curSubSong->patLen=patLen; + recalcTimestamps=true; } ImGui::TableNextRow(); @@ -253,6 +264,7 @@ void FurnaceGUI::drawSpeed(bool asChild) { if (curOrder>=ordLen) { setOrder(ordLen-1); } + recalcTimestamps=true; } ImGui::EndTable(); diff --git a/src/gui/sysManager.cpp b/src/gui/sysManager.cpp index 9f9de0318..4ebb1f888 100644 --- a/src/gui/sysManager.cpp +++ b/src/gui/sysManager.cpp @@ -110,6 +110,7 @@ void FurnaceGUI::drawSysManager() { if (picked!=DIV_SYSTEM_NULL) { if (e->changeSystem(i,picked,preserveChanPos)) { MARK_MODIFIED; + recalcTimestamps=true; if (e->song.autoSystem) { autoDetectSystem(); } @@ -179,6 +180,7 @@ void FurnaceGUI::drawSysManager() { showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError())); } else { MARK_MODIFIED; + recalcTimestamps=true; } if (e->song.autoSystem) { autoDetectSystem(); From c7fe8fea80ddc1f20ec8c4a42d7452d4a9f8c634 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 18:15:47 -0500 Subject: [PATCH 15/34] speed up sub-song detection --- src/engine/song.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/song.cpp b/src/engine/song.cpp index c2bf52263..d22022dd1 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -575,6 +575,7 @@ void DivSong::findSubSongs(int chans) { logV("found a subsong: %d-%d",curStart,i->ts.loopEnd.order); subSongStart.push_back(curStart); subSongEnd.push_back(i->ts.loopEnd.order); + curStart=i->ts.loopEnd.order; } // if this is the only song, quit From a49306b4bd1fda05e26b212d82eb413963726b7b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 19:25:08 -0500 Subject: [PATCH 16/34] more reference player work --- src/engine/engine.cpp | 5 +++++ src/engine/engine.h | 2 +- src/engine/filePlayer.cpp | 4 ++-- src/gui/gui.cpp | 16 +++++++++++++++- src/gui/newSong.cpp | 1 + src/gui/refPlayer.cpp | 8 +++++++- src/gui/subSongs.cpp | 3 +++ 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index ee9f599ff..e0e3e2b5b 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -569,6 +569,7 @@ void DivEngine::createNew(const char* description, String sysName, bool inBase64 BUSY_BEGIN; renderSamples(); reset(); + calcSongTimestamps(); BUSY_END; } @@ -606,6 +607,7 @@ void DivEngine::createNewFromDefaults() { BUSY_BEGIN; renderSamples(); reset(); + calcSongTimestamps(); BUSY_END; } @@ -3999,6 +4001,9 @@ void DivEngine::quitDispatch() { totalCmds=0; lastCmds=0; cmdsPerSecond=0; + if (filePlayerSync) { + if (curFilePlayer!=NULL) curFilePlayer->stop(); + } for (int i=0; i=outRate) { rateAccum-=outRate; playPos++; - if (playPos>=(ssize_t)si.frames) { + /*if (playPos>=(ssize_t)si.frames) { playPos=0; - } + }*/ } } else { for (int j=0; jisPlaying()) { WAKE_UP; } @@ -7384,6 +7385,19 @@ bool FurnaceGUI::loop() { recalcTimestamps=false; } + if (!e->isPlaying() && e->getFilePlayerSync()) { + if (cursor.y!=prevCursor.y || cursor.order) { + DivFilePlayer* fp=e->getFilePlayer(); + logV("cursor moved to %d:%d",cursor.order,cursor.y); + if (!fp->isPlaying()) { + DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); + if (rowTS.seconds!=-1) { + fp->setPosSeconds(rowTS.seconds,rowTS.micros); + } + } + } + } + sampleMapWaitingInput=(curWindow==GUI_WINDOW_INS_EDIT && sampleMapFocused); curWindowThreadSafe=curWindow; @@ -8598,7 +8612,7 @@ FurnaceGUI::FurnaceGUI(): noteInputPoly(true), notifyWaveChange(false), notifySampleChange(false), - recalcTimestamps(false), + recalcTimestamps(true), wantScrollListIns(false), wantScrollListWave(false), wantScrollListSample(false), diff --git a/src/gui/newSong.cpp b/src/gui/newSong.cpp index bb92c81d1..b7c582c0c 100644 --- a/src/gui/newSong.cpp +++ b/src/gui/newSong.cpp @@ -290,6 +290,7 @@ void FurnaceGUI::drawNewSong() { samplePos=0; updateSampleTex=true; notifySampleChange=true; + e->calcSongTimestamps(); selStart=SelectionPoint(); selEnd=SelectionPoint(); cursor=SelectionPoint(); diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index 70812c4e7..ca0b3d2ff 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -86,6 +86,7 @@ void FurnaceGUI::drawRefPlayer() { popToggleColors(); float vol=fp->getVolume(); + ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); if (ImGui::SliderFloat("##Volume",&vol,0.0f,1.0f)) { if (vol<0.0f) vol=0.0f; @@ -93,7 +94,12 @@ void FurnaceGUI::drawRefPlayer() { fp->setVolume(vol); } - ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10); + //ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10); + + if (!refPlayerOpen) { + fp->stop(); + e->setFilePlayerSync(false); + } } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER; ImGui::End(); diff --git a/src/gui/subSongs.cpp b/src/gui/subSongs.cpp index 97732c027..b21861251 100644 --- a/src/gui/subSongs.cpp +++ b/src/gui/subSongs.cpp @@ -38,6 +38,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) { if (ImGui::Selectable(id,i==e->getCurrentSubSong())) { makeCursorUndo(); e->changeSongP(i); + recalcTimestamps=true; updateScroll(0); oldRow=0; cursor.xCoarse=0; @@ -76,6 +77,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) { } else { makeCursorUndo(); e->changeSongP(e->song.subsong.size()-1); + recalcTimestamps=true; updateScroll(0); oldRow=0; cursor.xCoarse=0; @@ -98,6 +100,7 @@ void FurnaceGUI::drawSubSongs(bool asChild) { } else { makeCursorUndo(); e->changeSongP(e->song.subsong.size()-1); + recalcTimestamps=true; updateScroll(0); oldRow=0; cursor.xCoarse=0; From cb220d41ec2108d7276d0e236f5ef916abe5977a Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 19:39:53 -0500 Subject: [PATCH 17/34] DivSongTimestamps, part 8 now remove walkSong() --- src/engine/cmdStreamOps.cpp | 7 +++---- src/engine/engine.cpp | 9 --------- src/engine/engine.h | 3 --- src/engine/export/amigaValidation.cpp | 7 +++---- src/engine/export/grub.cpp | 7 +++---- src/engine/export/ipod.cpp | 7 +++---- src/engine/export/sapr.cpp | 7 +++---- src/engine/export/tiuna.cpp | 9 ++++----- src/engine/export/zsm.cpp | 7 +++---- src/engine/vgmOps.cpp | 7 +++---- 10 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/engine/cmdStreamOps.cpp b/src/engine/cmdStreamOps.cpp index 4a7dc1718..51c47e020 100644 --- a/src/engine/cmdStreamOps.cpp +++ b/src/engine/cmdStreamOps.cpp @@ -1252,10 +1252,9 @@ SafeWriter* DivEngine::saveCommand(DivCSProgress* progress, DivCSOptions options setOrder(0); BUSY_BEGIN_SOFT; // determine loop point - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - walkSong(loopOrder,loopRow,loopEnd); + calcSongTimestamps(); + int loopOrder=curSubSong->ts.loopStart.order; + int loopRow=curSubSong->ts.loopStart.row; logI("loop point: %d %d",loopOrder,loopRow); int cmdPopularity[256]; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index e0e3e2b5b..0616f4f3b 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -201,15 +201,6 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul return notNull?_("Invalid effect"):NULL; } -void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) { - if (curSubSong!=NULL) { - curSubSong->calcTimestamps(chans,song.grooves,song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior); - loopOrder=curSubSong->ts.loopStart.order; - loopRow=curSubSong->ts.loopStart.row; - loopEnd=curSubSong->ts.loopEnd.order; - } -} - void DivEngine::calcSongTimestamps() { if (curSubSong!=NULL) { curSubSong->calcTimestamps(chans,song.grooves,song.jumpTreatment,song.ignoreJumpAtEnd,song.brokenSpeedSel,song.delayBehavior); diff --git a/src/engine/engine.h b/src/engine/engine.h index 6601825ab..da98f565d 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -870,9 +870,6 @@ class DivEngine { int convertPanSplitToLinearLR(unsigned char left, unsigned char right, int range); unsigned int convertPanLinearToSplit(int val, unsigned char bits, int range); - // DEPRECATED - find song loop position - void walkSong(int& loopOrder, int& loopRow, int& loopEnd); - // calculate all song timestamps void calcSongTimestamps(); diff --git a/src/engine/export/amigaValidation.cpp b/src/engine/export/amigaValidation.cpp index b79694a51..30224e84f 100644 --- a/src/engine/export/amigaValidation.cpp +++ b/src/engine/export/amigaValidation.cpp @@ -55,10 +55,9 @@ void DivExportAmigaValidation::run() { EXTERN_BUSY_BEGIN_SOFT; // determine loop point - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); + int loopOrder=e->curSubSong->ts.loopStart.order; + int loopRow=e->curSubSong->ts.loopStart.row; e->curOrder=0; e->freelance=false; diff --git a/src/engine/export/grub.cpp b/src/engine/export/grub.cpp index fc8bf6b2f..e48ae5195 100644 --- a/src/engine/export/grub.cpp +++ b/src/engine/export/grub.cpp @@ -76,10 +76,9 @@ void DivExportGRUB::run() { e->got.rate=rate; // Determine loop point. - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); + int loopOrder=e->curSubSong->ts.loopStart.order; + int loopRow=e->curSubSong->ts.loopStart.row; logAppendf("loop point: %d %d",loopOrder,loopRow); e->warnings=""; diff --git a/src/engine/export/ipod.cpp b/src/engine/export/ipod.cpp index 5ffe98970..e05d5c074 100644 --- a/src/engine/export/ipod.cpp +++ b/src/engine/export/ipod.cpp @@ -75,10 +75,9 @@ void DivExportiPod::run() { e->got.rate=rate; // Determine loop point. - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); + int loopOrder=e->curSubSong->ts.loopStart.order; + int loopRow=e->curSubSong->ts.loopStart.row; logAppendf("loop point: %d %d",loopOrder,loopRow); e->warnings=""; diff --git a/src/engine/export/sapr.cpp b/src/engine/export/sapr.cpp index c99c1254e..f638cfaab 100644 --- a/src/engine/export/sapr.cpp +++ b/src/engine/export/sapr.cpp @@ -92,10 +92,9 @@ void DivExportSAPR::run() { e->got.rate=sapRate; // Determine loop point. - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); + int loopOrder=e->curSubSong->ts.loopStart.order; + int loopRow=e->curSubSong->ts.loopStart.row; logAppendf("loop point: %d %d",loopOrder,loopRow); e->warnings=""; diff --git a/src/engine/export/tiuna.cpp b/src/engine/export/tiuna.cpp index 104c38193..6a212c23f 100644 --- a/src/engine/export/tiuna.cpp +++ b/src/engine/export/tiuna.cpp @@ -181,7 +181,7 @@ static void writeCmd(std::vector& cmds, TiunaCmd& cmd, unsigned char } void DivExportTiuna::run() { - int loopOrder, loopOrderRow, loopEnd; + int loopOrder, loopOrderRow; int tick=0; SafeWriter* w; std::map allCmds[2]; @@ -199,10 +199,9 @@ void DivExportTiuna::run() { e->synchronizedSoft([&]() { // determine loop point // bool stopped=false; - loopOrder=0; - loopOrderRow=0; - loopEnd=0; - e->walkSong(loopOrder,loopOrderRow,loopEnd); + e->calcSongTimestamps(); + loopOrder=e->curSubSong->ts.loopStart.order; + loopOrderRow=e->curSubSong->ts.loopStart.row; logAppendf("loop point: %d %d",loopOrder,loopOrderRow); w=new SafeWriter; diff --git a/src/engine/export/zsm.cpp b/src/engine/export/zsm.cpp index 109e0e754..708340539 100644 --- a/src/engine/export/zsm.cpp +++ b/src/engine/export/zsm.cpp @@ -574,10 +574,9 @@ void DivExportZSM::run() { e->got.rate=zsmrate&0xffff; // determine loop point - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - e->walkSong(loopOrder,loopRow,loopEnd); + e->calcSongTimestamps(); + int loopOrder=e->curSubSong->ts.loopStart.order; + int loopRow=e->curSubSong->ts.loopStart.row; logAppendf("loop point: %d %d",loopOrder,loopRow); zsm.init(zsmrate); diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index f48f2db7e..c7d1cbfde 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -1272,10 +1272,9 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p double origRate=got.rate; got.rate=correctedRate; // determine loop point - int loopOrder=0; - int loopRow=0; - int loopEnd=0; - walkSong(loopOrder,loopRow,loopEnd); + calcSongTimestamps(); + int loopOrder=curSubSong->ts.loopStart.order; + int loopRow=curSubSong->ts.loopStart.row; logI("loop point: %d %d",loopOrder,loopRow); warnings=""; From a78c59e17e9d4ac8806f1e12fb82240b28a3d831 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 29 Oct 2025 20:00:08 -0500 Subject: [PATCH 18/34] reference player sync state now in GUI --- src/gui/gui.cpp | 3 +++ src/gui/gui.h | 1 + src/gui/refPlayer.cpp | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index ea3faa9c3..a27fdcf1a 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -8278,6 +8278,7 @@ void FurnaceGUI::syncState() { followOrders=e->getConfBool("followOrders",true); followPattern=e->getConfBool("followPattern",true); noteInputPoly=e->getConfBool("noteInputPoly",true); + filePlayerSync=e->getConfBool("filePlayerSync",true); audioExportOptions.loops=e->getConfInt("exportLoops",0); if (audioExportOptions.loops<0) audioExportOptions.loops=0; audioExportOptions.fadeOut=e->getConfDouble("exportFadeOut",0.0); @@ -8437,6 +8438,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("followPattern",followPattern); conf.set("orderEditMode",orderEditMode); conf.set("noteInputPoly",noteInputPoly); + conf.set("filePlayerSync",filePlayerSync); if (settings.persistFadeOut) { conf.set("exportLoops",audioExportOptions.loops); conf.set("exportFadeOut",audioExportOptions.fadeOut); @@ -8634,6 +8636,7 @@ FurnaceGUI::FurnaceGUI(): safeMode(false), midiWakeUp(true), makeDrumkitMode(false), + filePlayerSync(true), audioEngineChanged(false), settingsChanged(false), debugFFT(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 826909d71..50cb9943a 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1727,6 +1727,7 @@ class FurnaceGUI { bool safeMode; bool midiWakeUp; bool makeDrumkitMode; + bool filePlayerSync; bool audioEngineChanged, settingsChanged, debugFFT, debugRowTimestamps; bool willExport[DIV_MAX_CHIPS]; int vgmExportVersion; diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index ca0b3d2ff..32190735e 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -79,11 +79,12 @@ void FurnaceGUI::drawRefPlayer() { } ImGui::SameLine(); - pushToggleColors(e->getFilePlayerSync()); + pushToggleColors(filePlayerSync); if (ImGui::Button(_("Sync"))) { - e->setFilePlayerSync(!e->getFilePlayerSync()); + filePlayerSync=!filePlayerSync; } popToggleColors(); + e->setFilePlayerSync(filePlayerSync); float vol=fp->getVolume(); ImGui::SameLine(); From a6d06d3728630c1ad7e98d95e0820ca4e710a59b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 01:02:26 -0500 Subject: [PATCH 19/34] fix cursor movement check --- src/gui/gui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index a27fdcf1a..34752df2b 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -7386,7 +7386,7 @@ bool FurnaceGUI::loop() { } if (!e->isPlaying() && e->getFilePlayerSync()) { - if (cursor.y!=prevCursor.y || cursor.order) { + if (cursor.y!=prevCursor.y || cursor.order!=prevCursor.order) { DivFilePlayer* fp=e->getFilePlayer(); logV("cursor moved to %d:%d",cursor.order,cursor.y); if (!fp->isPlaying()) { From 64b8a8f71402e7b94fdd266842b966f84b35bd7b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 01:30:48 -0500 Subject: [PATCH 20/34] turn volume slider into mix slider it's far more useful --- src/engine/filePlayer.cpp | 19 ++++++++++++++++--- src/engine/filePlayer.h | 3 +++ src/engine/playback.cpp | 14 +++++++++++++- src/gui/refPlayer.cpp | 26 +++++++++++++++++--------- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index 1c4394d7a..2b2b493ca 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -199,6 +199,10 @@ void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { return; } + float actualVolume=volume+1.0f; + if (actualVolume<0.0f) actualVolume=0.0f; + if (actualVolume>1.0f) actualVolume=1.0f; + if (wantBlock!=DIV_NO_BLOCK) { cacheCV.notify_one(); } @@ -234,7 +238,7 @@ void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { x[5]*t1[1]+ x[6]*t1[2]+ x[7]*t1[3] - )*volume; + )*actualVolume; for (int j=0; jgetActive()) { + refPlayerVol=1.0f-curFilePlayer->getVolume(); + if (refPlayerVol>1.0f) refPlayerVol=1.0f; + } + } + // now mix everything (resolve patchbay) for (unsigned int i: song.patchbay) { // there are 4096 portsets. each portset may have up to 16 outputs (subports). @@ -3308,7 +3320,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // chip outputs if (srcPortSetgetOutputCount()) { - float vol=song.systemVol[srcPortSet]*disCont[srcPortSet].dispatch->getPostAmp()*song.masterVol; + float vol=song.systemVol[srcPortSet]*disCont[srcPortSet].dispatch->getPostAmp()*song.masterVol*refPlayerVol; // apply volume and panning switch (destSubPort&3) { diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index 32190735e..c2099f163 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -23,15 +23,16 @@ #include "IconsFontAwesome4.h" void FurnaceGUI::drawRefPlayer() { + DivFilePlayer* fp=e->getFilePlayer(); if (nextWindow==GUI_WINDOW_REF_PLAYER) { refPlayerOpen=true; ImGui::SetNextWindowFocus(); nextWindow=GUI_WINDOW_NOTHING; } + fp->setActive(refPlayerOpen); if (!refPlayerOpen) return; - if (ImGui::Begin("Music Player",&refPlayerOpen,globalWinFlags,_("Music Player"))) { - DivFilePlayer* fp=e->getFilePlayer(); + if (ImGui::Begin("Music Player",&refPlayerOpen,globalWinFlags,_("Music Player"))) { bool playPosNegative=false; ssize_t playPos=fp->getPos(); if (playPos<0) { @@ -85,23 +86,30 @@ void FurnaceGUI::drawRefPlayer() { } popToggleColors(); e->setFilePlayerSync(filePlayerSync); + + ImGui::SameLine(); + ImGui::Text(_("Mix:")); float vol=fp->getVolume(); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if (ImGui::SliderFloat("##Volume",&vol,0.0f,1.0f)) { - if (vol<0.0f) vol=0.0f; + if (ImGui::SliderFloat("##Volume",&vol,-1.0f,1.0f,_("<-- Tracker / Reference -->"))) { + if (vol<-1.0f) vol=-1.0f; if (vol>1.0f) vol=1.0f; fp->setVolume(vol); } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + fp->setVolume(0.0f); + } //ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10); - - if (!refPlayerOpen) { - fp->stop(); - e->setFilePlayerSync(false); - } } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER; ImGui::End(); + + + if (!refPlayerOpen) { + fp->stop(); + e->setFilePlayerSync(false); + } } From 3c106f78611ca756b99bc00e6d49806ec6942e50 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 01:49:02 -0500 Subject: [PATCH 21/34] improve time tracking on step play now it fetches row time from song timestamps also syncs file player! --- src/engine/engine.cpp | 4 ++++ src/engine/playback.cpp | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 0616f4f3b..9c1825f4e 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -2060,6 +2060,10 @@ bool DivEngine::playToRow(int row) { } void DivEngine::stepOne(int row) { + if (curFilePlayer && filePlayerSync) { + curFilePlayer->stop(); + } + if (!isPlaying()) { BUSY_BEGIN_SOFT; freelance=false; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 7d1217d51..70595e3b0 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -2169,6 +2169,14 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { prevOrder=curOrder; prevRow=curRow; playPosLock.unlock(); + + // also set the playback position and sync file player if necessary + DivSongTimestamps::Timestamp rowTS=curSubSong->ts.getTimes(curOrder,curRow); + totalSeconds=rowTS.seconds; + totalTicks=rowTS.micros; + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + } } // ...and now process the next row! nextRow(); @@ -3295,6 +3303,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // only if the player window is open if (curFilePlayer->getActive()) { refPlayerVol=1.0f-curFilePlayer->getVolume(); + if (refPlayerVol<0.0f) refPlayerVol=0.0f; if (refPlayerVol>1.0f) refPlayerVol=1.0f; } } From 319da2d3916100a51aaf21df442142a143ae8fce Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 04:07:27 -0500 Subject: [PATCH 22/34] file player cue point and loop tracking no loop trail yet --- src/engine/engine.cpp | 10 +++++ src/engine/engine.h | 11 ++++- src/engine/filePlayer.cpp | 82 +++++++++++++++++++++++++++++------- src/engine/filePlayer.h | 14 +++++-- src/engine/playback.cpp | 16 +++++++ src/gui/gui.cpp | 22 ++++++++-- src/gui/refPlayer.cpp | 88 +++++++++++++++++++++++++++++++++++---- 7 files changed, 213 insertions(+), 30 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 9c1825f4e..d52aa877c 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1662,6 +1662,16 @@ void DivEngine::setFilePlayerSync(bool doSync) { filePlayerSync=doSync; } +void DivEngine::getFilePlayerCue(int& seconds, int& micros) { + seconds=filePlayerCueSeconds; + micros=filePlayerCueMicros; +} + +void DivEngine::setFilePlayerCue(int seconds, int micros) { + filePlayerCueSeconds=seconds; + filePlayerCueMicros=micros; +} + void DivEngine::syncFilePlayer() { if (curFilePlayer==NULL) return; int finalSeconds=totalSeconds+filePlayerCueSeconds; diff --git a/src/engine/engine.h b/src/engine/engine.h index da98f565d..9cda7433e 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -604,6 +604,8 @@ class DivEngine { bool filePlayerSync; ssize_t filePlayerCueSeconds; unsigned int filePlayerCueMicros; + int filePlayerLoopTrail; + int curFilePlayerTrail; size_t totalProcessed; @@ -630,8 +632,6 @@ class DivEngine { void runMidiTime(int totalCycles=1); bool shallSwitchCores(); - void syncFilePlayer(); - void testFunction(); bool loadDMF(unsigned char* file, size_t len); @@ -762,6 +762,11 @@ class DivEngine { // get whether the player is synchronized with song playback. bool getFilePlayerSync(); void setFilePlayerSync(bool doSync); + // get/set file player cue position. + void getFilePlayerCue(int& seconds, int& micros); + void setFilePlayerCue(int seconds, int micros); + // UNSAFE - sync file player to current playback position. + void syncFilePlayer(); // save as .dmf. SafeWriter* saveDMF(unsigned char version); @@ -1573,6 +1578,8 @@ class DivEngine { filePlayerSync(false), filePlayerCueSeconds(0), filePlayerCueMicros(0), + filePlayerLoopTrail(0), + curFilePlayerTrail(0), totalProcessed(0), renderPoolThreads(0), renderPool(NULL), diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index 2b2b493ca..9c627ce84 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -208,6 +208,19 @@ void DivFilePlayer::mix(float** buf, int chans, unsigned int size) { } for (unsigned int i=0; i>DIV_FPCACHE_BLOCK_SHIFT; if (blockIndex!=lastWantBlock) { wantBlock=playPos; @@ -285,22 +298,49 @@ ssize_t DivFilePlayer::getPos() { return playPos; } +void DivFilePlayer::getPosSeconds(ssize_t& seconds, unsigned int& micros) { + if (sf==NULL) { + seconds=0; + micros=0; + return; + } + double microsD=playPos%si.samplerate; + seconds=playPos/si.samplerate; + micros=(int)((1000000.0*microsD)/(double)si.samplerate); +} + ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) { - playPos=newPos; - rateAccum=0; - wantBlock=playPos; - logD("DivFilePlayer: setPos(%" PRIi64 ")",newPos); - return playPos; + if (offset==UINT_MAX) { + playPos=newPos; + rateAccum=0; + wantBlock=playPos; + logD("DivFilePlayer: setPos(%" PRIi64 ")",newPos); + return playPos; + } else { + pendingPosOffset=offset; + pendingPos=newPos; + wantBlock=playPos; + logD("DivFilePlayer: offset %u setPos(%" PRIi64 ")",offset,newPos); + return newPos; + } } ssize_t DivFilePlayer::setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset) { if (sf==NULL) return 0; double microsD=(double)si.samplerate*((double)micros/1000000.0); - playPos=seconds*si.samplerate+(int)microsD; - rateAccum=0; - wantBlock=playPos; - logD("DivFilePlayer: setPosSeconds(%" PRIi64 ".%06d)",seconds,micros); - return playPos; + if (offset==UINT_MAX) { + playPos=seconds*si.samplerate+(int)microsD; + rateAccum=0; + wantBlock=playPos; + logD("DivFilePlayer: setPosSeconds(%" PRIi64 ".%06d)",seconds,micros); + return playPos; + } else { + pendingPosOffset=offset; + pendingPos=seconds*si.samplerate+(int)microsD; + wantBlock=pendingPos; + logD("DivFilePlayer: offset %u setPosSeconds(%" PRIi64 ".%06d)",offset,seconds,micros); + return pendingPos; + } } size_t DivFilePlayer::getMemUsage() { @@ -336,13 +376,23 @@ bool DivFilePlayer::isPlaying() { } void DivFilePlayer::play(unsigned int offset) { - logV("DivFilePlayer: playing"); - playing=true; + if (offset!=UINT_MAX) { + pendingPlayOffset=offset; + logV("DivFilePlayer: playing (offset: %u)",offset); + } else { + playing=true; + logV("DivFilePlayer: playing"); + } } void DivFilePlayer::stop(unsigned int offset) { - logV("DivFilePlayer: stopping"); - playing=false; + if (offset!=UINT_MAX) { + pendingStopOffset=offset; + logV("DivFilePlayer: stopping (offset: %u)",offset); + } else { + playing=false; + logV("DivFilePlayer: stopping"); + } } bool DivFilePlayer::closeFile() { @@ -492,6 +542,10 @@ DivFilePlayer::DivFilePlayer(): quitThread(false), threadHasQuit(false), isActive(false), + pendingPos(0), + pendingPosOffset(UINT_MAX), + pendingPlayOffset(UINT_MAX), + pendingStopOffset(UINT_MAX), cacheThread(NULL) { memset(&si,0,sizeof(SF_INFO)); sincTable=DivFilterTables::getSincTable8(); diff --git a/src/engine/filePlayer.h b/src/engine/filePlayer.h index 197d5cc2c..0ab310179 100644 --- a/src/engine/filePlayer.h +++ b/src/engine/filePlayer.h @@ -57,6 +57,11 @@ class DivFilePlayer { bool threadHasQuit; bool isActive; + ssize_t pendingPos; + unsigned int pendingPosOffset; + unsigned int pendingPlayOffset; + unsigned int pendingStopOffset; + std::thread* cacheThread; std::mutex cacheMutex; std::mutex cacheThreadLock; @@ -73,15 +78,16 @@ class DivFilePlayer { void mix(float** buf, int chans, unsigned int size); ssize_t getPos(); - ssize_t setPos(ssize_t newPos, unsigned int offset=0); - ssize_t setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset=0); + void getPosSeconds(ssize_t& seconds, unsigned int& micros); + ssize_t setPos(ssize_t newPos, unsigned int offset=UINT_MAX); + ssize_t setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset=UINT_MAX); bool isBlockPresent(ssize_t pos); bool setBlockPriority(ssize_t pos, bool priority); bool isLoaded(); bool isPlaying(); - void play(unsigned int offset=0); - void stop(unsigned int offset=0); + void play(unsigned int offset=UINT_MAX); + void stop(unsigned int offset=UINT_MAX); bool closeFile(); bool loadFile(const char* path); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 70595e3b0..e7804fc83 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -3106,6 +3106,22 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // used by audio export to determine how many samples to write (otherwise it'll add silence at the end) lastLoopPos=size-runLeftG; logD("last loop pos: %d for a size of %d and runLeftG of %d",lastLoopPos,size,runLeftG); + // if file player is synchronized then set its position to that of the loop row + if (curFilePlayer && filePlayerSync) { + if (curFilePlayer->isPlaying()) { + DivSongTimestamps::Timestamp rowTS=curSubSong->ts.loopStartTime; + int finalSeconds=rowTS.seconds+filePlayerCueSeconds; + int finalMicros=rowTS.micros+filePlayerCueMicros; + + while (finalMicros>=1000000) { + finalMicros-=1000000; + finalSeconds++; + } + + curFilePlayer->setPosSeconds(finalSeconds,finalMicros,lastLoopPos); + } + } + // increase total loop count totalLoops++; // stop playing once we hit a specific number of loops (set during audio export) if (remainingLoops>0) { diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 34752df2b..bde08b038 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -5892,9 +5892,15 @@ bool FurnaceGUI::loop() { } break; case GUI_FILE_MUSIC_OPEN: - if (!e->getFilePlayer()->loadFile(copyOfName.c_str())) { - showError(fmt::sprintf(_("Error while loading file!"))); - } + e->synchronizedSoft([this,copyOfName]() { + bool wasPlaying=e->getFilePlayer()->isPlaying(); + if (!e->getFilePlayer()->loadFile(copyOfName.c_str())) { + showError(fmt::sprintf(_("Error while loading file!"))); + } else if (wasPlaying && filePlayerSync && refPlayerOpen && e->isPlaying()) { + e->syncFilePlayer(); + e->getFilePlayer()->play(); + } + }); break; case GUI_FILE_TEST_OPEN: showWarning(fmt::sprintf(_("You opened: %s"),copyOfName),GUI_WARN_GENERIC); @@ -7392,6 +7398,16 @@ bool FurnaceGUI::loop() { if (!fp->isPlaying()) { DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); if (rowTS.seconds!=-1) { + int cueSeconds=0; + int cueMicros=0; + e->getFilePlayerCue(cueSeconds,cueMicros); + rowTS.seconds+=cueSeconds; + rowTS.micros+=cueMicros; + while (rowTS.micros>=1000000) { + rowTS.micros-=1000000; + rowTS.seconds++; + } + fp->setPosSeconds(rowTS.seconds,rowTS.micros); } } diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index c2099f163..716a3cfc3 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -47,10 +47,14 @@ void FurnaceGUI::drawRefPlayer() { int posMinutes=((playPos/fileRate)/60)%60; int posSeconds=(playPos/fileRate)%60; int posMillis=(1000*(playPos%fileRate))/fileRate; - if (playPosNegative) { - ImGui::Text("-%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + if (fp->isLoaded()) { + if (playPosNegative) { + ImGui::Text("-%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + } else { + ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + } } else { - ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + ImGui::TextUnformatted(_("no file loaded")); } ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); @@ -58,13 +62,80 @@ void FurnaceGUI::drawRefPlayer() { fp->setPos(playPos); } - if (ImGui::Button("Open")) { + if (ImGui::Button(ICON_FA_FOLDER_OPEN "##Open")) { openFileDialog(GUI_FILE_MUSIC_OPEN); } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + e->synchronizedSoft([this,fp]() { + if (!fp->closeFile()) { + showError(_("you haven't loaded a file!")); + } + }); + } + ImGui::SetItemTooltip(_("open file\n(right click to unload current file)")); ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FAST_BACKWARD)) { - fp->stop(); - fp->setPos(0); + if (ImGui::Button(ICON_FA_STEP_BACKWARD)) { + // handled outside + } + if (fp->isPlaying()) { + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + int cueSeconds=0; + int cueMicros=0; + fp->stop(); + e->getFilePlayerCue(cueSeconds,cueMicros); + fp->setPosSeconds(cueSeconds,cueMicros); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { + fp->stop(); + fp->setPos(0); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + int cueSeconds=0; + int cueMicros=0; + e->getFilePlayerCue(cueSeconds,cueMicros); + fp->setPosSeconds(cueSeconds,cueMicros); + } + ImGui::SetItemTooltip( + _("left click: go to cue position\n" + "middle click: go to beginning\n" + "right click: go to cue position (but don't stop)") + ); + } else { + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + // try setting cue pos + ssize_t curSeconds=0; + unsigned int curMicros=0; + fp->getPosSeconds(curSeconds,curMicros); + DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(curOrder,0); + if (rowTS.seconds==-1) { + showError("the first row of this order isn't going to play."); + } else { + // calculate difference and set cue pos + curSeconds-=rowTS.seconds; + int curMicrosI=curMicros-rowTS.micros; + while (curMicrosI<0) { + curMicrosI+=1000000; + curSeconds--; + } + e->setFilePlayerCue(curSeconds,curMicrosI); + } + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { + fp->setPos(0); + } + if (ImGui::BeginPopupContextItem("Edit Cue Position",ImGuiPopupFlags_MouseButtonRight)) { + ImGui::Text("Edit me"); + if (ImGui::Button("OK")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::SetItemTooltip( + _("left click: set cue position here\n" + " - current playback time becomes position at first row of current order\n" + "middle click: go to beginning\n" + "right click: fine edit cue position") + ); } ImGui::SameLine(); if (fp->isPlaying()) { @@ -72,11 +143,13 @@ void FurnaceGUI::drawRefPlayer() { if (ImGui::Button(ICON_FA_PAUSE "##Pause")) { fp->stop(); } + ImGui::SetItemTooltip(_("pause")); popToggleColors(); } else { if (ImGui::Button(ICON_FA_PLAY "##Play")) { fp->play(); } + ImGui::SetItemTooltip(_("play")); } ImGui::SameLine(); @@ -84,6 +157,7 @@ void FurnaceGUI::drawRefPlayer() { if (ImGui::Button(_("Sync"))) { filePlayerSync=!filePlayerSync; } + ImGui::SetItemTooltip(_("synchronize playback with tracker playback")); popToggleColors(); e->setFilePlayerSync(filePlayerSync); From 98165f5ed273e7e65ffcd97c1613caf5cb7927ed Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 04:35:44 -0500 Subject: [PATCH 23/34] ugly cue position editor --- src/gui/refPlayer.cpp | 45 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index 716a3cfc3..d3ddd1303 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -124,7 +124,27 @@ void FurnaceGUI::drawRefPlayer() { fp->setPos(0); } if (ImGui::BeginPopupContextItem("Edit Cue Position",ImGuiPopupFlags_MouseButtonRight)) { - ImGui::Text("Edit me"); + ImGui::Text("Set cue position at first order:"); + int cueSeconds=0; + int cueMicros=0; + bool altered=false; + e->getFilePlayerCue(cueSeconds,cueMicros); + // TODO: improve this... + ImGui::SetNextItemWidth(240.0f*dpiScale); + if (ImGui::InputInt(_("Seconds##CuePosS"),&cueSeconds)) { + if (cueSeconds<-3600) cueSeconds=-3600; + if (cueSeconds>3600) cueSeconds=3600; + altered=true; + } + ImGui::SetNextItemWidth(240.0f*dpiScale); + if (ImGui::InputInt(_("Microseconds##CuePosM"),&cueMicros,1000,10000)) { + if (cueMicros<0) cueMicros=0; + if (cueMicros>999999) cueMicros=999999; + altered=true; + } + if (altered) { + e->setFilePlayerCue(cueSeconds,cueMicros); + } if (ImGui::Button("OK")) { ImGui::CloseCurrentPopup(); } @@ -143,7 +163,27 @@ void FurnaceGUI::drawRefPlayer() { if (ImGui::Button(ICON_FA_PAUSE "##Pause")) { fp->stop(); } - ImGui::SetItemTooltip(_("pause")); + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + // try setting cue pos + ssize_t curSeconds=0; + unsigned int curMicros=0; + fp->getPosSeconds(curSeconds,curMicros); + DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(curOrder,0); + if (rowTS.seconds==-1) { + showError("the first row of this order isn't going to play."); + } else { + // calculate difference and set cue pos + curSeconds-=rowTS.seconds; + int curMicrosI=curMicros-rowTS.micros; + while (curMicrosI<0) { + curMicrosI+=1000000; + curSeconds--; + } + e->setFilePlayerCue(curSeconds,curMicrosI); + fp->stop(); + } + } + ImGui::SetItemTooltip(_("pause\n(right click to set cue position and pause)")); popToggleColors(); } else { if (ImGui::Button(ICON_FA_PLAY "##Play")) { @@ -175,6 +215,7 @@ void FurnaceGUI::drawRefPlayer() { if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { fp->setVolume(0.0f); } + ImGui::SetItemTooltip(_("right click to reset")); //ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10); } From d3e93669481af0c106b97f72a2d95773ac5e8149 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 04:53:17 -0500 Subject: [PATCH 24/34] add a useful button --- src/gui/refPlayer.cpp | 50 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index d3ddd1303..fbf2a2eaf 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -108,7 +108,7 @@ void FurnaceGUI::drawRefPlayer() { fp->getPosSeconds(curSeconds,curMicros); DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(curOrder,0); if (rowTS.seconds==-1) { - showError("the first row of this order isn't going to play."); + showError(_("the first row of this order isn't going to play.")); } else { // calculate difference and set cue pos curSeconds-=rowTS.seconds; @@ -124,7 +124,7 @@ void FurnaceGUI::drawRefPlayer() { fp->setPos(0); } if (ImGui::BeginPopupContextItem("Edit Cue Position",ImGuiPopupFlags_MouseButtonRight)) { - ImGui::Text("Set cue position at first order:"); + ImGui::TextUnformatted(_("Set cue position at first order:")); int cueSeconds=0; int cueMicros=0; bool altered=false; @@ -145,7 +145,7 @@ void FurnaceGUI::drawRefPlayer() { if (altered) { e->setFilePlayerCue(cueSeconds,cueMicros); } - if (ImGui::Button("OK")) { + if (ImGui::Button(_("OK"))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); @@ -170,7 +170,7 @@ void FurnaceGUI::drawRefPlayer() { fp->getPosSeconds(curSeconds,curMicros); DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(curOrder,0); if (rowTS.seconds==-1) { - showError("the first row of this order isn't going to play."); + showError(_("the first row of this order isn't going to play.")); } else { // calculate difference and set cue pos curSeconds-=rowTS.seconds; @@ -193,6 +193,48 @@ void FurnaceGUI::drawRefPlayer() { } ImGui::SameLine(); + if (ImGui::Button(ICON_FA_STEP_FORWARD "##PlayPos")) { + // handled outside + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + DivSongTimestamps::Timestamp rowTS; + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); + } else { + rowTS=e->curSubSong->ts.getTimes(curOrder,0); + } + int cueSeconds=0; + int cueMicros=0; + e->getFilePlayerCue(cueSeconds,cueMicros); + if (rowTS.seconds==-1) { + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + showError(_("the row that the pattern cursor is at isn't going to play. trying moving the cursor.")); + } else { + showError(_("the first row of this order isn't going to play. try another order.")); + } + } else { + int finalSeconds=rowTS.seconds+cueSeconds; + int finalMicros=rowTS.micros+cueMicros; + + while (finalMicros>=1000000) { + finalMicros-=1000000; + finalSeconds++; + } + + fp->setPosSeconds(finalSeconds,finalMicros); + fp->play(); + } + } + if (ImGui::IsItemHovered() && (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || ImGui::IsMouseReleased(ImGuiMouseButton_Right))) { + fp->stop(); + } + ImGui::SetItemTooltip(_( + "hold left click to play from current order\n" + "hold right click to play from pattern cursor position\n" + "release mouse button to stop" + )); + ImGui::SameLine(); + pushToggleColors(filePlayerSync); if (ImGui::Button(_("Sync"))) { filePlayerSync=!filePlayerSync; From 3a0fe2dec74d612e49254f168f9a7bca524311dc Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 04:59:14 -0500 Subject: [PATCH 25/34] fix typo --- src/gui/refPlayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index fbf2a2eaf..aec5a0a3d 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -208,7 +208,7 @@ void FurnaceGUI::drawRefPlayer() { e->getFilePlayerCue(cueSeconds,cueMicros); if (rowTS.seconds==-1) { if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - showError(_("the row that the pattern cursor is at isn't going to play. trying moving the cursor.")); + showError(_("the row that the pattern cursor is at isn't going to play. try moving the cursor.")); } else { showError(_("the first row of this order isn't going to play. try another order.")); } From 1720a519f097bdea1ae3f24eb98223330f78b447 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 05:02:54 -0500 Subject: [PATCH 26/34] what are you on, MSVC? --- src/engine/song.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/song.cpp b/src/engine/song.cpp index d22022dd1..0da55424a 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -19,6 +19,7 @@ #include "song.h" #include "../ta-log.h" +#include DivSongTimestamps::Timestamp DivSongTimestamps::getTimes(int order, int row) { if (order<0 || order>=DIV_MAX_PATTERNS) return Timestamp(-1,0); From 0ae9151b479f1bcdb1b1a8460cffac8e9e3dda81 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 14:59:48 -0500 Subject: [PATCH 27/34] GUI: fix audio export progress bar issue #2454 --- src/gui/gui.cpp | 40 ++++++++++++++-------------------------- src/gui/gui.h | 8 ++------ 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index bde08b038..98a1e921a 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -2717,11 +2717,10 @@ int FurnaceGUI::loadStream(String path) { void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->calcSongTimestamps(); + DivSongTimestamps& ts=e->curSubSong->ts; - // TODO: fix! - songFadeoutSectionLength=0; - songHasSongEndCommand=!e->curSubSong->ts.isLoopable; - songLength=e->curSubSong->ts.totalRows; + songLength=ts.totalSeconds+(double)ts.totalMicros/1000000.0; + double loopLength=songLength-(ts.loopStartTime.seconds+(double)ts.loopStartTime.micros/1000000.0); e->saveAudio(path.c_str(),audioExportOptions); @@ -2729,19 +2728,15 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->getTotalAudioFiles(totalFiles); int totalLoops=0; - lengthOfOneFile=songLength; - - if (!songHasSongEndCommand) { + if (ts.isLoopable) { e->getTotalLoops(totalLoops); - - lengthOfOneFile+=songLoopedSectionLength*totalLoops; - lengthOfOneFile+=songFadeoutSectionLength; // account for fadeout + songLength+=loopLength*totalLoops; + songLength+=audioExportOptions.fadeOut; } - totalLength=lengthOfOneFile*totalFiles; + totalLength=songLength*totalFiles; curProgress=0.0f; - displayExporting=true; } @@ -6037,24 +6032,21 @@ bool FurnaceGUI::loop() { if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_CHAN) { ImGui::Text(_("Please wait...")); } - float* progressLambda=&curProgress; - int curPosInRows=0; - int* curPosInRowsLambda=&curPosInRows; int curFile=0; int* curFileLambda=&curFile; if (e->isExporting()) { e->lockEngine( - [this, progressLambda, curPosInRowsLambda, curFileLambda] () { + [this, curFileLambda] () { *curFileLambda=0; e->getCurFileIndex(*curFileLambda); - *progressLambda=(double)e->getTotalSeconds()/(double)MAX(1,e->curSubSong->ts.totalSeconds); - // TODO: fix - *curPosInRowsLambda=0; + curProgress=(((double)e->getTotalSeconds()+(double)e->getTotalTicks()/1000000.0)+(songLength*(*curFileLambda)))/totalLength; } ); } - ImGui::Text(_("Row %d of %d"),songLength,lengthOfOneFile); + if (curProgress<0.0f) curProgress=0.0f; + if (curProgress>1.0f) curProgress=1.0f; + if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles); ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0),fmt::sprintf("%.2f%%",curProgress*100.0f).c_str()); @@ -8727,12 +8719,8 @@ FurnaceGUI::FurnaceGUI(): patFont(NULL), bigFont(NULL), headFont(NULL), - songLength(0), - songLoopedSectionLength(0), - songFadeoutSectionLength(0), - songHasSongEndCommand(false), - lengthOfOneFile(0), - totalLength(0), + songLength(0.0), + totalLength(0.0), curProgress(0.0f), totalFiles(0), localeRequiresJapanese(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 50cb9943a..10f23d976 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1823,12 +1823,8 @@ class FurnaceGUI { char emptyLabel[32]; char emptyLabel2[32]; - int songLength; // length of all the song in rows - int songLoopedSectionLength; // length of looped part of the song - int songFadeoutSectionLength; // length of fading part of the song - bool songHasSongEndCommand; // song has "Song end" command (FFxx) - int lengthOfOneFile; // length of one rendering pass. song length times num of loops + fadeout - int totalLength; // total length of render (lengthOfOneFile times num of files for per-channel export) + double songLength; // length of the song in seconds + double totalLength; // total length of render (songLength times num of files for per-channel export) float curProgress; int totalFiles; From e579ba8ee85b70b3eff08fae6952a8cc10dc0c28 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 17:14:31 -0500 Subject: [PATCH 28/34] GUI: add option to offset sample in the amplify one --- src/gui/gui.cpp | 1 + src/gui/gui.h | 2 +- src/gui/sampleEdit.cpp | 13 ++++++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 98a1e921a..53db802cc 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -9017,6 +9017,7 @@ FurnaceGUI::FurnaceGUI(): resampleTarget(32000), resampleStrat(5), amplifyVol(100.0), + amplifyOff(0.0), sampleSelStart(-1), sampleSelEnd(-1), sampleInfo(true), diff --git a/src/gui/gui.h b/src/gui/gui.h index 10f23d976..c606c07fd 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2621,7 +2621,7 @@ class FurnaceGUI { int resizeSize, silenceSize; double resampleTarget; int resampleStrat; - float amplifyVol; + float amplifyVol, amplifyOff; int sampleSelStart, sampleSelEnd; bool sampleInfo, sampleCompatRate; bool sampleDragActive, sampleDragMode, sampleDrag16, sampleZoomAuto; diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 441c0294a..bf2152aa2 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -1212,7 +1212,7 @@ void FurnaceGUI::drawSampleEdit() { sameLineMaybe(); ImGui::Button(ICON_FA_VOLUME_UP "##SAmplify"); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(_("Amplify")); + ImGui::SetTooltip(_("Amplify/Offset")); } if (openSampleAmplifyOpt) { openSampleAmplifyOpt=false; @@ -1226,6 +1226,11 @@ void FurnaceGUI::drawSampleEdit() { } ImGui::SameLine(); ImGui::Text("(%.1fdB)",20.0*log10(amplifyVol/100.0f)); + ImGui::Text(_("DC offset")); + if (ImGui::InputFloat("##Offset",&lifyOff,-100.0,100.0,"%g%%")) { + if (amplifyOff<-100) amplifyOff=-100; + if (amplifyOff>100) amplifyOff=100; + } if (ImGui::Button(_("Apply"))) { sample->prepareUndo(true); e->lockEngine([this,sample]() { @@ -1233,15 +1238,17 @@ void FurnaceGUI::drawSampleEdit() { float vol=amplifyVol/100.0f; if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) { + float off=32767.0f*(amplifyOff/100.0f); for (unsigned int i=start; idata16[i]*vol; + float val=off+sample->data16[i]*vol; if (val<-32768) val=-32768; if (val>32767) val=32767; sample->data16[i]=val; } } else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) { + float off=127.0f*(amplifyOff/100.0f); for (unsigned int i=start; idata8[i]*vol; + float val=off+sample->data8[i]*vol; if (val<-128) val=-128; if (val>127) val=127; sample->data8[i]=val; From b218bdea7ae14f9741bee59b62853a4bfe053181 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 18:44:59 -0500 Subject: [PATCH 29/34] TimeMicros struct --- CMakeLists.txt | 1 + src/engine/playback.cpp | 4 +- src/engine/song.cpp | 14 +++--- src/engine/song.h | 16 ++---- src/gui/gui.cpp | 2 +- src/gui/pattern.cpp | 2 +- src/gui/refPlayer.cpp | 6 +-- src/timeutils.cpp | 107 ++++++++++++++++++++++++++++++++++++++++ src/timeutils.h | 61 +++++++++++++++++++++++ 9 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 src/timeutils.cpp create mode 100644 src/timeutils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eff3bd2f1..d37334bdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -554,6 +554,7 @@ set(ENGINE_SOURCES src/log.cpp src/baseutils.cpp src/fileutils.cpp +src/timeutils.cpp src/utfutils.cpp extern/itcompress/compression.c diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index e7804fc83..1c94ad275 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -2171,7 +2171,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { playPosLock.unlock(); // also set the playback position and sync file player if necessary - DivSongTimestamps::Timestamp rowTS=curSubSong->ts.getTimes(curOrder,curRow); + TimeMicros rowTS=curSubSong->ts.getTimes(curOrder,curRow); totalSeconds=rowTS.seconds; totalTicks=rowTS.micros; if (curFilePlayer && filePlayerSync) { @@ -3109,7 +3109,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // if file player is synchronized then set its position to that of the loop row if (curFilePlayer && filePlayerSync) { if (curFilePlayer->isPlaying()) { - DivSongTimestamps::Timestamp rowTS=curSubSong->ts.loopStartTime; + TimeMicros rowTS=curSubSong->ts.loopStartTime; int finalSeconds=rowTS.seconds+filePlayerCueSeconds; int finalMicros=rowTS.micros+filePlayerCueMicros; diff --git a/src/engine/song.cpp b/src/engine/song.cpp index 0da55424a..9d70c5283 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -21,11 +21,11 @@ #include "../ta-log.h" #include -DivSongTimestamps::Timestamp DivSongTimestamps::getTimes(int order, int row) { - if (order<0 || order>=DIV_MAX_PATTERNS) return Timestamp(-1,0); - if (row<0 || row>=DIV_MAX_ROWS) return Timestamp(-1,0); - Timestamp* t=orders[order]; - if (t==NULL) return Timestamp(-1,0); +TimeMicros DivSongTimestamps::getTimes(int order, int row) { + if (order<0 || order>=DIV_MAX_PATTERNS) return TimeMicros(-1,0); + if (row<0 || row>=DIV_MAX_ROWS) return TimeMicros(-1,0); + TimeMicros* t=orders[order]; + if (t==NULL) return TimeMicros(-1,0); return t[row]; } @@ -391,12 +391,12 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove // log row time here if (rowChanged && !endOfSong) { if (ts.orders[prevOrder]==NULL) { - ts.orders[prevOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; + ts.orders[prevOrder]=new TimeMicros[DIV_MAX_ROWS]; for (int i=0; igetFilePlayer(); logV("cursor moved to %d:%d",cursor.order,cursor.y); if (!fp->isPlaying()) { - DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); + TimeMicros rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); if (rowTS.seconds!=-1) { int cueSeconds=0; int cueMicros=0; diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index bd19d5f01..aeea36b63 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -408,7 +408,7 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int } if (debugRowTimestamps) { - DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(ord,i); + TimeMicros rowTS=e->curSubSong->ts.getTimes(ord,i); if (rowTS.seconds==-1) { ImGui::Text("---"); } else { diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index aec5a0a3d..de0884078 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -106,7 +106,7 @@ void FurnaceGUI::drawRefPlayer() { ssize_t curSeconds=0; unsigned int curMicros=0; fp->getPosSeconds(curSeconds,curMicros); - DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(curOrder,0); + TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0); if (rowTS.seconds==-1) { showError(_("the first row of this order isn't going to play.")); } else { @@ -168,7 +168,7 @@ void FurnaceGUI::drawRefPlayer() { ssize_t curSeconds=0; unsigned int curMicros=0; fp->getPosSeconds(curSeconds,curMicros); - DivSongTimestamps::Timestamp rowTS=e->curSubSong->ts.getTimes(curOrder,0); + TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0); if (rowTS.seconds==-1) { showError(_("the first row of this order isn't going to play.")); } else { @@ -197,7 +197,7 @@ void FurnaceGUI::drawRefPlayer() { // handled outside } if (ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - DivSongTimestamps::Timestamp rowTS; + TimeMicros rowTS; if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); } else { diff --git a/src/timeutils.cpp b/src/timeutils.cpp new file mode 100644 index 000000000..0c9c058d8 --- /dev/null +++ b/src/timeutils.cpp @@ -0,0 +1,107 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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 "timeutils.h" +#include + +String TimeMicros::toString(signed char prec, unsigned char hms) { + String ret; + int actualSeconds=seconds; + int actualMicros=micros; + bool negative=(seconds<0); + if (seconds<0 && micros!=0) { + actualSeconds++; + actualMicros=1000000-micros; + } + if (negative) actualSeconds=-actualSeconds; + + switch (hms) { + case 2: { // h:mm:ss + int h=actualSeconds/3600; + int m=(actualSeconds/60)%60; + int s=actualSeconds%60; + + if (negative) { + ret=fmt::sprintf("-%d:%02d:%02d",h,m,s); + } else { + ret=fmt::sprintf("%d:%02d:%02d",h,m,s); + } + break; + } + case 1: { // m:ss + int m=actualSeconds/60; + int s=actualSeconds%60; + + if (negative) { + ret=fmt::sprintf("-%d:%02d",m,s); + } else { + ret=fmt::sprintf("%d:%02d",m,s); + } + break; + } + default: { // seconds + if (negative) { + ret=fmt::sprintf("-%d",actualSeconds); + } else { + ret=fmt::sprintf("%d",actualSeconds); + } + break; + } + } + + if (prec<0) { + // automatic precision + prec=6; + int precMicros=actualMicros; + while ((precMicros%10)==0 && prec>1) { + prec--; + precMicros/=10; + } + } + + if (prec>0) { + if (prec>6) prec=6; + switch (prec) { + case 1: + ret+=fmt::sprintf(".%01d",actualMicros/100000); + break; + case 2: + ret+=fmt::sprintf(".%02d",actualMicros/10000); + break; + case 3: + ret+=fmt::sprintf(".%03d",actualMicros/1000); + break; + case 4: + ret+=fmt::sprintf(".%04d",actualMicros/100); + break; + case 5: + ret+=fmt::sprintf(".%05d",actualMicros/10); + break; + case 6: + ret+=fmt::sprintf(".%06d",actualMicros); + break; + } + } + + return ret; +} + +TimeMicros TimeMicros::fromString(String s) { + return TimeMicros(0,0); +} diff --git a/src/timeutils.h b/src/timeutils.h new file mode 100644 index 000000000..0bf07ccf5 --- /dev/null +++ b/src/timeutils.h @@ -0,0 +1,61 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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 _TIMEUTILS_H +#define _TIMEUTILS_H + +#ifdef __unix__ +#include +#include +#endif + +#include "ta-utils.h" + +struct TimeMicros { + int seconds, micros; + + inline float toFloat() { + return seconds+(float)micros/1000000.0f; + } + inline double toDouble() { + return seconds+(double)micros/1000000.0; + } + + /** + * convert this TimeMicros to a String. + * @param prec maximum digit precision (0-6). use -1 for automatic precision. + * @param hms how many denominations to include (0: seconds, 1: minutes, 2: hours). + * @return formatted TimeMicros. + */ + String toString(signed char prec=-1, unsigned char hms=0); + static TimeMicros fromString(String s); + + TimeMicros(int s, int u): + seconds(s), micros(u) {} +#ifdef __unix__ + TimeMicros(struct timeval tv): + seconds(tv.tv_sec), micros(tv.tv_usec) {} + TimeMicros(struct timespec ts): + seconds(ts.tv_sec), micros(ts.tv_nsec/1000) {} +#endif + TimeMicros(): + seconds(0), micros(0) {} +}; + +#endif From a2b56b5b64f34c246a95201f9248e80e9cce7dbf Mon Sep 17 00:00:00 2001 From: Redas Jefisovas <114405740+holoflash@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:52:03 +0100 Subject: [PATCH 30/34] feat: add clearer explanations to effects --- po/de.po | 8 ++++---- po/es.po | 16 ++++++++-------- po/fi.po | 8 ++++---- po/fr.po | 16 ++++++++-------- po/furnace.pot | 8 ++++---- po/hy.po | 8 ++++---- po/id.po | 16 ++++++++-------- po/ja.po | 8 ++++---- po/ko.po | 16 ++++++++-------- po/nl.po | 8 ++++---- po/pl.po | 16 ++++++++-------- po/pt_BR.po | 16 ++++++++-------- po/ru.po | 14 +++++++------- po/sk.po | 16 ++++++++-------- po/staging/ko-new.po | 16 ++++++++-------- po/staging/ko-old.po | 16 ++++++++-------- po/staging/pt_BR-after.po | 16 ++++++++-------- po/sv.po | 16 ++++++++-------- po/th.po | 16 ++++++++-------- po/tr.po | 8 ++++---- po/uk.po | 8 ++++---- po/zh.po | 16 ++++++++-------- po/zh_HK.po | 16 ++++++++-------- src/engine/engine.cpp | 8 ++++---- 24 files changed, 155 insertions(+), 155 deletions(-) diff --git a/po/de.po b/po/de.po index 312af88b6..19da48295 100644 --- a/po/de.po +++ b/po/de.po @@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/es.po b/po/es.po index 075592dd9..bb1a06239 100644 --- a/po/es.po +++ b/po/es.po @@ -4023,20 +4023,20 @@ msgid "this is a system designed for testing purposes." msgstr "un sistema diseñado para propósitos de prueba." #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpegio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpegio (x: semitonos; y: semitonos)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Deslizamiento de tono hacia arriba" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Deslizamiento de tono hacia arriba (xx: velocidad)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Deslizamiento de tono hacia abajo" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Deslizamiento de tono hacia abajo (xx: velocidad)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Portamento (xx: velocidad)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/fi.po b/po/fi.po index 0164e5345..65e57b90f 100644 --- a/po/fi.po +++ b/po/fi.po @@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/fr.po b/po/fr.po index 6a8c24c99..62e0a910c 100644 --- a/po/fr.po +++ b/po/fr.po @@ -3763,20 +3763,20 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpège" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpège (x: demi-tons; y: demi-tons)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Glissement d'hauteur vers le haut" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Glissement d'hauteur vers le haut (xx: vitesse)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Glissement d'hauteur vers le bas" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Glissement d'hauteur vers le bas (xx: vitesse)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Palier" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Palier (xx: vitesse)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/furnace.pot b/po/furnace.pot index 3bdce21b5..3e00f8225 100644 --- a/po/furnace.pot +++ b/po/furnace.pot @@ -3759,19 +3759,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/hy.po b/po/hy.po index e30f221aa..b47b87289 100644 --- a/po/hy.po +++ b/po/hy.po @@ -3767,19 +3767,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/id.po b/po/id.po index b5a68e00a..fd66c1066 100644 --- a/po/id.po +++ b/po/id.po @@ -3768,20 +3768,20 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpegio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpegio (x: banyaknya semitone; y: banyaknya semitone)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Geser nada ke atas" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Geser nada ke atas (xx: kecepatan)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Geser nada ke bawah" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Geser nada ke bawah (xx: kecepatan)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Portamento (xx: kecepatan)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/ja.po b/po/ja.po index 4279d277b..bdced83ec 100644 --- a/po/ja.po +++ b/po/ja.po @@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/ko.po b/po/ko.po index ab6c2677b..c367af161 100644 --- a/po/ko.po +++ b/po/ko.po @@ -3981,20 +3981,20 @@ msgid "this is a system designed for testing purposes." msgstr "테스트 목적으로 설계된 시스템입니다." #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: 아르페지오" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: 아르페지오 (x: 반음 수; y: 반음 수)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: 피치 슬라이드 업" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: 피치 슬라이드 업 (xx: 속도)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: 피치 슬라이드 다운" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: 피치 슬라이드 다운 (xx: 속도)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: 포르타멘토" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: 포르타멘토 (xx: 속도)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/nl.po b/po/nl.po index d321a1ecb..78fbd046f 100644 --- a/po/nl.po +++ b/po/nl.po @@ -3806,19 +3806,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/pl.po b/po/pl.po index d171e93eb..92698817b 100644 --- a/po/pl.po +++ b/po/pl.po @@ -4091,20 +4091,20 @@ msgid "this is a system designed for testing purposes." msgstr "ten system jest przeznaczony do testowania." #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpeggio (x: półtony; y: półtony)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Portamento w górę" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Portamento w górę (xx: szybkość)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Portamento w dół" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Portamento w dół (xx: szybkość)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Auto-portamento (do wskazanej nuty)" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Auto-portamento (do wskazanej nuty; xx: szybkość)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/pt_BR.po b/po/pt_BR.po index 5d7ee7dff..5751433e7 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -4145,20 +4145,20 @@ msgid "this is a system designed for testing purposes." msgstr "este é um sistema desenvolvido para propósito de testes." #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpejo" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpejo (x: semitons; y: semitons)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Slide de tom para cima" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Slide de tom para cima (xx: velocidade)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Slide de tom para baixo" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Slide de tom para baixo (xx: velocidade)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Portamento (xx: velocidade)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/ru.po b/po/ru.po index 61c30a61b..94d056dde 100644 --- a/po/ru.po +++ b/po/ru.po @@ -4080,20 +4080,20 @@ msgid "this is a system designed for testing purposes." msgstr "это система, разработанная для тестирования." #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "00xy: Арпеджио" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Портаменто вверх" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Портаменто вверх (xx: скорость)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Портаменто вниз" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Портаменто вниз (xx: скорость)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Авто-портаменто (до указ. ноты)" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Авто-портаменто (до указ. ноты; xx: скорость)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/sk.po b/po/sk.po index 72488a9fd..878729303 100644 --- a/po/sk.po +++ b/po/sk.po @@ -3767,20 +3767,20 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy Arpeggio (x: poltóny; y: poltóny)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx Tónovy šmyk nahor" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx Tónovy šmyk nahor (xx: rýchlosť)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx Tónovy šmyk nadol" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx Tónovy šmyk nadol (xx: rýchlosť)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx Portamento" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx Portamento (xx: rýchlosť)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/staging/ko-new.po b/po/staging/ko-new.po index e80472c9d..b4037304e 100644 --- a/po/staging/ko-new.po +++ b/po/staging/ko-new.po @@ -3528,20 +3528,20 @@ msgid "this is a system designed for testing purposes." msgstr "테스트 목적으로 설계된 시스템입니다." #: src/engine/engine.cpp:51 -msgid "00xy: Arpeggio" -msgstr "00xy: 아르페지오" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: 아르페지오 (x: 반음 수; y: 반음 수)" #: src/engine/engine.cpp:53 -msgid "01xx: Pitch slide up" -msgstr "01xx: 피치 슬라이드 업" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: 피치 슬라이드 업 (xx: 속도)" #: src/engine/engine.cpp:55 -msgid "02xx: Pitch slide down" -msgstr "02xx: 피치 슬라이드 다운" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: 피치 슬라이드 다운 (xx: 속도)" #: src/engine/engine.cpp:57 -msgid "03xx: Portamento" -msgstr "03xx: 포르타멘토" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: 포르타멘토 (xx: 속도)" #: src/engine/engine.cpp:59 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/staging/ko-old.po b/po/staging/ko-old.po index dda06f041..d4507075a 100644 --- a/po/staging/ko-old.po +++ b/po/staging/ko-old.po @@ -3524,20 +3524,20 @@ msgid "this is a system designed for testing purposes." msgstr "이것은 테스트 목적으로 설계된 시스템입니다." #: src/engine/engine.cpp:51 -msgid "00xy: Arpeggio" -msgstr "00xy: 아르페지오" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: 아르페지오 (x: 반음; y: 반음)" #: src/engine/engine.cpp:53 -msgid "01xx: Pitch slide up" -msgstr "01xx: 피치 슬라이드 업" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: 피치 슬라이드 업 (xx: 속도)" #: src/engine/engine.cpp:55 -msgid "02xx: Pitch slide down" -msgstr "02xx: 피치 슬라이드 다운" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: 피치 슬라이드 다운 (xx: 속도)" #: src/engine/engine.cpp:57 -msgid "03xx: Portamento" -msgstr "03xx: 포르타멘토" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: 포르타멘토 (xx: 속도)" #: src/engine/engine.cpp:59 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/staging/pt_BR-after.po b/po/staging/pt_BR-after.po index 55f95a4cd..615419416 100644 --- a/po/staging/pt_BR-after.po +++ b/po/staging/pt_BR-after.po @@ -3558,20 +3558,20 @@ msgid "this is a system designed for testing purposes." msgstr "este é um sistema desenvolvido para propósito de testes." #: src/engine/engine.cpp:51 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpejo" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpejo (x: semitons; y: semitons)" #: src/engine/engine.cpp:53 -msgid "01xx: Pitch slide up" -msgstr "01xx: Slide de tom para cima" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Slide de tom para cima (xx: velocidade)" #: src/engine/engine.cpp:55 -msgid "02xx: Pitch slide down" -msgstr "02xx: Slide de tom para baixo" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Slide de tom para baixo (xx: velocidade)" #: src/engine/engine.cpp:57 -msgid "03xx: Portamento" -msgstr "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Portamento (xx: velocidade)" #: src/engine/engine.cpp:59 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/sv.po b/po/sv.po index 9ba9c2779..d2006d3a2 100644 --- a/po/sv.po +++ b/po/sv.po @@ -4025,20 +4025,20 @@ msgid "this is a system designed for testing purposes." msgstr "detta är ett system designat för teständamål." #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: Arpeggio (x: halvtoner; y: halvtoner)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: Tonhöjd glider upp" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: Tonhöjd glider upp (xx: hastighet)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: Tonhöjd glider ner" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: Tonhöjd glider ner (xx: hastighet)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: Portamento (xx: hastighet)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/th.po b/po/th.po index 5e4d472cc..da428d75f 100644 --- a/po/th.po +++ b/po/th.po @@ -3762,20 +3762,20 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: อาร์เปจโจ" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: อาร์เปจโจ (x: ครึ่งเสียง; y: ครึ่งเสียง)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: เลื่อนระดับเสียงขึ้น" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: เลื่อนระดับเสียงขึ้น (xx: ความเร็ว)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: เลื่อนระดับเสียงลง" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: เลื่อนระดับเสียงลง (xx: ความเร็ว)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: เสียงเลื่อนไหล" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: เสียงเลื่อนไหล (xx: ความเร็ว)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/tr.po b/po/tr.po index fa1a2e797..0fdf208e0 100644 --- a/po/tr.po +++ b/po/tr.po @@ -3763,19 +3763,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/uk.po b/po/uk.po index 6914bf909..a59fc5d77 100644 --- a/po/uk.po +++ b/po/uk.po @@ -3764,19 +3764,19 @@ msgid "this is a system designed for testing purposes." msgstr "" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" msgstr "" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" +msgid "01xx: Pitch slide up (xx: speed)" msgstr "" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" +msgid "02xx: Pitch slide down (xx: speed)" msgstr "" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" +msgid "03xx: Portamento (xx: speed)" msgstr "" #: src/engine/engine.cpp:60 diff --git a/po/zh.po b/po/zh.po index 6d7d48273..587db3936 100644 --- a/po/zh.po +++ b/po/zh.po @@ -3798,20 +3798,20 @@ msgid "this is a system designed for testing purposes." msgstr "此系统只是为了测试" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: 琶音" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: 琶音 (x: 半音; y: 半音)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: 音高上滑" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: 音高上滑 (xx: 速率)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: 音高下滑" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: 音高下滑 (xx: 速率)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: 滑音" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: 滑音 (xx: 速率)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/po/zh_HK.po b/po/zh_HK.po index 1cf342840..0832d1fe7 100644 --- a/po/zh_HK.po +++ b/po/zh_HK.po @@ -3798,20 +3798,20 @@ msgid "this is a system designed for testing purposes." msgstr "此系統只是為了測試" #: src/engine/engine.cpp:52 -msgid "00xy: Arpeggio" -msgstr "00xy: 琶音" +msgid "00xy: Arpeggio (x: semitones; y: semitones)" +msgstr "00xy: 琶音 (x: 半音; y: 半音)" #: src/engine/engine.cpp:54 -msgid "01xx: Pitch slide up" -msgstr "01xx: 音高上滑" +msgid "01xx: Pitch slide up (xx: speed)" +msgstr "01xx: 音高上滑 (xx: 速率)" #: src/engine/engine.cpp:56 -msgid "02xx: Pitch slide down" -msgstr "02xx: 音高下滑" +msgid "02xx: Pitch slide down (xx: speed)" +msgstr "02xx: 音高下滑 (xx: 速率)" #: src/engine/engine.cpp:58 -msgid "03xx: Portamento" -msgstr "03xx: 滑音" +msgid "03xx: Portamento (xx: speed)" +msgstr "03xx: 滑音 (xx: 速率)" #: src/engine/engine.cpp:60 msgid "04xy: Vibrato (x: speed; y: depth)" diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index d52aa877c..d2b9d9f61 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -52,13 +52,13 @@ void process(void* u, float** in, float** out, int inChans, int outChans, unsign const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNull) { switch (effect) { case 0x00: - return _("00xy: Arpeggio"); + return _("00xy: Arpeggio (x: semitones; y: semitones)"); case 0x01: - return _("01xx: Pitch slide up"); + return _("01xx: Pitch slide up (xx: speed)"); case 0x02: - return _("02xx: Pitch slide down"); + return _("02xx: Pitch slide down (xx: speed)"); case 0x03: - return _("03xx: Portamento"); + return _("03xx: Portamento (xx: speed)"); case 0x04: return _("04xy: Vibrato (x: speed; y: depth)"); case 0x05: From 5ff81aef3324d0720bba1486c177ba9ff01a4f56 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 20:35:14 -0500 Subject: [PATCH 31/34] some time refactors no more weird totalTicks name code looks better --- src/engine/engine.cpp | 41 +++++---------- src/engine/engine.h | 18 +++---- src/engine/filePlayer.cpp | 24 ++++----- src/engine/filePlayer.h | 5 +- src/engine/playback.cpp | 32 ++++++------ src/engine/song.cpp | 18 +++---- src/engine/song.h | 3 +- src/gui/clock.cpp | 5 +- src/gui/debugWindow.cpp | 6 ++- src/gui/gui.cpp | 46 +++-------------- src/gui/orders.cpp | 25 ++------- src/gui/pattern.cpp | 3 +- src/gui/refPlayer.cpp | 68 ++++++------------------- src/gui/tutorial.cpp | 2 +- src/timeutils.cpp | 103 +++++++++++++++++++++++++++++++++++--- src/timeutils.h | 68 ++++++++++++++++++++++++- 16 files changed, 256 insertions(+), 211 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index d2b9d9f61..b2b87277e 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1511,8 +1511,7 @@ String DivEngine::getPlaybackDebugInfo() { "midiTimeDrift: %f\n" "changeOrd: %d\n" "changePos: %d\n" - "totalSeconds: %d\n" - "totalTicks: %d\n" + "totalTime: %s\n" "totalTicksR: %d\n" "curMidiClock: %d\n" "curMidiTime: %d\n" @@ -1524,7 +1523,7 @@ String DivEngine::getPlaybackDebugInfo() { "totalProcessed: %d\n" "bufferPos: %d\n", curOrder,prevOrder,curRow,prevRow,ticks,subticks,totalLoops,lastLoopPos,nextSpeed,divider,cycles,clockDrift, - midiClockCycles,midiClockDrift,midiTimeCycles,midiTimeDrift,changeOrd,changePos,totalSeconds,totalTicks, + midiClockCycles,midiClockDrift,midiTimeCycles,midiTimeDrift,changeOrd,changePos,totalTime.toString(), totalTicksR,curMidiClock,curMidiTime,totalCmds,lastCmds,cmdsPerSecond, (int)extValue,(int)tempoAccum,(int)totalProcessed,(int)bufferPos ); @@ -1662,27 +1661,17 @@ void DivEngine::setFilePlayerSync(bool doSync) { filePlayerSync=doSync; } -void DivEngine::getFilePlayerCue(int& seconds, int& micros) { - seconds=filePlayerCueSeconds; - micros=filePlayerCueMicros; +TimeMicros DivEngine::getFilePlayerCue() { + return filePlayerCue; } -void DivEngine::setFilePlayerCue(int seconds, int micros) { - filePlayerCueSeconds=seconds; - filePlayerCueMicros=micros; +void DivEngine::setFilePlayerCue(TimeMicros cue) { + filePlayerCue=cue; } void DivEngine::syncFilePlayer() { if (curFilePlayer==NULL) return; - int finalSeconds=totalSeconds+filePlayerCueSeconds; - int finalMicros=totalTicks+filePlayerCueMicros; - - while (finalMicros>=1000000) { - finalMicros-=1000000; - finalSeconds++; - } - - curFilePlayer->setPosSeconds(finalSeconds,finalMicros); + curFilePlayer->setPosSeconds(totalTime+filePlayerCue); } void DivEngine::playSub(bool preserveDrift, int goalRow) { @@ -1717,9 +1706,8 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { ticks=1; subticks=0; tempoAccum=0; - totalTicks=0; + totalTime=TimeMicros(0,0); totalTicksOff=0; - totalSeconds=0; totalTicksR=0; curMidiClock=0; curMidiTime=0; @@ -1809,7 +1797,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { cmdStream.clear(); std::chrono::high_resolution_clock::time_point timeEnd=std::chrono::high_resolution_clock::now(); logV("playSub() took %dµs",std::chrono::duration_cast(timeEnd-timeStart).count()); - logV("and landed us at %d.%06d (%d ticks, %d:%d.%d)",totalSeconds,totalTicks,totalTicksR,curOrder,curRow,ticks); + logV("and landed us at %s (%d ticks, %d:%d.%d)",totalTime.toString(),totalTicksR,curOrder,curRow,ticks); } /* @@ -2569,12 +2557,8 @@ void DivEngine::virtualTempoChanged() { BUSY_END; } -int DivEngine::getTotalSeconds() { - return totalSeconds; -} - -int DivEngine::getTotalTicks() { - return totalTicks; +TimeMicros DivEngine::getCurTime() { + return totalTime; } bool DivEngine::getRepeatPattern() { @@ -3995,9 +3979,8 @@ void DivEngine::quitDispatch() { nextSpeed=3; changeOrd=-1; changePos=0; - totalTicks=0; + totalTime=TimeMicros(0,0); totalTicksOff=0; - totalSeconds=0; totalTicksR=0; curMidiClock=0; curMidiTime=0; diff --git a/src/engine/engine.h b/src/engine/engine.h index 9cda7433e..804fdc3b4 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -510,7 +510,8 @@ class DivEngine { int midiTimeCycles; double midiTimeDrift; int stepPlay; - int changeOrd, changePos, totalSeconds, totalTicks, totalTicksR, curMidiClock, curMidiTime, totalCmds, lastCmds, cmdsPerSecond; + int changeOrd, changePos, totalTicksR, curMidiClock, curMidiTime, totalCmds, lastCmds, cmdsPerSecond; + TimeMicros totalTime; double totalTicksOff; int curMidiTimePiece, curMidiTimeCode; unsigned char extValue, pendingMetroTick; @@ -602,8 +603,7 @@ class DivEngine { size_t filePlayerBufLen; DivFilePlayer* curFilePlayer; bool filePlayerSync; - ssize_t filePlayerCueSeconds; - unsigned int filePlayerCueMicros; + TimeMicros filePlayerCue; int filePlayerLoopTrail; int curFilePlayerTrail; @@ -763,8 +763,8 @@ class DivEngine { bool getFilePlayerSync(); void setFilePlayerSync(bool doSync); // get/set file player cue position. - void getFilePlayerCue(int& seconds, int& micros); - void setFilePlayerCue(int seconds, int micros); + TimeMicros getFilePlayerCue(); + void setFilePlayerCue(TimeMicros cue); // UNSAFE - sync file player to current playback position. void syncFilePlayer(); @@ -1049,8 +1049,7 @@ class DivEngine { void virtualTempoChanged(); // get time - int getTotalTicks(); // 1/1000000th of a second - int getTotalSeconds(); + TimeMicros getCurTime(); // get repeat pattern bool getRepeatPattern(); @@ -1525,8 +1524,6 @@ class DivEngine { stepPlay(0), changeOrd(-1), changePos(0), - totalSeconds(0), - totalTicks(0), totalTicksR(0), curMidiClock(0), curMidiTime(0), @@ -1576,8 +1573,7 @@ class DivEngine { filePlayerBufLen(0), curFilePlayer(NULL), filePlayerSync(false), - filePlayerCueSeconds(0), - filePlayerCueMicros(0), + filePlayerCue(0,0), filePlayerLoopTrail(0), curFilePlayerTrail(0), totalProcessed(0), diff --git a/src/engine/filePlayer.cpp b/src/engine/filePlayer.cpp index 9c627ce84..62dac52de 100644 --- a/src/engine/filePlayer.cpp +++ b/src/engine/filePlayer.cpp @@ -298,15 +298,15 @@ ssize_t DivFilePlayer::getPos() { return playPos; } -void DivFilePlayer::getPosSeconds(ssize_t& seconds, unsigned int& micros) { +TimeMicros DivFilePlayer::getPosSeconds() { if (sf==NULL) { - seconds=0; - micros=0; - return; + return TimeMicros(0,0); } double microsD=playPos%si.samplerate; - seconds=playPos/si.samplerate; - micros=(int)((1000000.0*microsD)/(double)si.samplerate); + return TimeMicros( + playPos/si.samplerate, // seconds + (int)((1000000.0*microsD)/(double)si.samplerate) // microseconds + ); } ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) { @@ -325,20 +325,20 @@ ssize_t DivFilePlayer::setPos(ssize_t newPos, unsigned int offset) { } } -ssize_t DivFilePlayer::setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset) { +ssize_t DivFilePlayer::setPosSeconds(TimeMicros newTime, unsigned int offset) { if (sf==NULL) return 0; - double microsD=(double)si.samplerate*((double)micros/1000000.0); + double microsD=(double)si.samplerate*((double)newTime.micros/1000000.0); if (offset==UINT_MAX) { - playPos=seconds*si.samplerate+(int)microsD; + playPos=(ssize_t)newTime.seconds*(ssize_t)si.samplerate+(int)microsD; rateAccum=0; wantBlock=playPos; - logD("DivFilePlayer: setPosSeconds(%" PRIi64 ".%06d)",seconds,micros); + logD("DivFilePlayer: setPosSeconds(%s)",newTime.toString()); return playPos; } else { pendingPosOffset=offset; - pendingPos=seconds*si.samplerate+(int)microsD; + pendingPos=(ssize_t)newTime.seconds*(ssize_t)si.samplerate+(int)microsD; wantBlock=pendingPos; - logD("DivFilePlayer: offset %u setPosSeconds(%" PRIi64 ".%06d)",offset,seconds,micros); + logD("DivFilePlayer: offset %u setPosSeconds(%s)",offset,newTime.toString()); return pendingPos; } } diff --git a/src/engine/filePlayer.h b/src/engine/filePlayer.h index 0ab310179..50fac93ad 100644 --- a/src/engine/filePlayer.h +++ b/src/engine/filePlayer.h @@ -21,6 +21,7 @@ #define _FILEPLAYER_H #include "../ta-utils.h" +#include "../timeutils.h" #include #include #include @@ -78,9 +79,9 @@ class DivFilePlayer { void mix(float** buf, int chans, unsigned int size); ssize_t getPos(); - void getPosSeconds(ssize_t& seconds, unsigned int& micros); + TimeMicros getPosSeconds(); ssize_t setPos(ssize_t newPos, unsigned int offset=UINT_MAX); - ssize_t setPosSeconds(ssize_t seconds, unsigned int micros, unsigned int offset=UINT_MAX); + ssize_t setPosSeconds(TimeMicros newTime, unsigned int offset=UINT_MAX); bool isBlockPresent(ssize_t pos); bool setBlockPriority(ssize_t pos, bool priority); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 1c94ad275..d52ed1144 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -2172,8 +2172,9 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { // also set the playback position and sync file player if necessary TimeMicros rowTS=curSubSong->ts.getTimes(curOrder,curRow); - totalSeconds=rowTS.seconds; - totalTicks=rowTS.micros; + if (rowTS.seconds!=-1) { + totalTime=rowTS; + } if (curFilePlayer && filePlayerSync) { syncFilePlayer(); } @@ -2581,25 +2582,27 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { if (!noAccum) { double dt=divider*tickMult; totalTicksR++; - // despite the name, totalTicks is in microseconds... - totalTicks+=1000000/dt; + totalTime.micros+=1000000/dt; totalTicksOff+=fmod(1000000.0,dt); while (totalTicksOff>=dt) { totalTicksOff-=dt; - totalTicks++; + totalTime.micros++; } } - if (totalTicks>=1000000) { - totalTicks-=1000000; + if (totalTime.micros>=1000000) { + totalTime.micros-=1000000; // who's gonna play a song for 68 years? - if (totalSeconds<0x7fffffff) totalSeconds++; + if (totalTime.seconds<0x7fffffff) totalTime.seconds++; cmdsPerSecond=totalCmds-lastCmds; lastCmds=totalCmds; } } // print status in console mode - if (consoleMode && !disableStatusOut && subticks<=1 && !skipping) fprintf(stderr,"\x1b[2K> %d:%.2d:%.2d.%.2d %.2x/%.2x:%.3d/%.3d %4dcmd/s\x1b[G",totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000,curOrder,curSubSong->ordersLen,curRow,curSubSong->patLen,cmdsPerSecond); + if (consoleMode && !disableStatusOut && subticks<=1 && !skipping) { + String timeFormatted=totalTime.toString(2,TA_TIME_FORMAT_HMS); + fprintf(stderr,"\x1b[2K> %s %.2x/%.2x:%.3d/%.3d %4dcmd/s\x1b[G",timeFormatted.c_str(),curOrder,curSubSong->ordersLen,curRow,curSubSong->patLen,cmdsPerSecond); + } } @@ -3110,15 +3113,12 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi if (curFilePlayer && filePlayerSync) { if (curFilePlayer->isPlaying()) { TimeMicros rowTS=curSubSong->ts.loopStartTime; - int finalSeconds=rowTS.seconds+filePlayerCueSeconds; - int finalMicros=rowTS.micros+filePlayerCueMicros; - while (finalMicros>=1000000) { - finalMicros-=1000000; - finalSeconds++; + if (rowTS.seconds==-1) { + logW("that row isn't supposed to play. report this now!"); } - curFilePlayer->setPosSeconds(finalSeconds,finalMicros,lastLoopPos); + curFilePlayer->setPosSeconds(rowTS+filePlayerCue,lastLoopPos); } } // increase total loop count @@ -3232,7 +3232,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // complain and stop playback if we believe the engine has stalled //logD("attempts: %d",attempts); if (attempts>=(int)(size+10)) { - logE("hang detected! stopping! at %d seconds %d micro (%d>=%d)",totalSeconds,totalTicks,attempts,(int)size); + logE("hang detected! stopping! at %s (%d>=%d)",totalTime.toString(),attempts,(int)size); freelance=false; playing=false; extValuePresent=false; diff --git a/src/engine/song.cpp b/src/engine/song.cpp index 9d70c5283..116815da7 100644 --- a/src/engine/song.cpp +++ b/src/engine/song.cpp @@ -30,8 +30,7 @@ TimeMicros DivSongTimestamps::getTimes(int order, int row) { } DivSongTimestamps::DivSongTimestamps(): - totalSeconds(0), - totalMicros(0), + totalTime(0,0), totalTicks(0), totalRows(0), isLoopDefined(false), @@ -54,8 +53,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); // reset state - ts.totalSeconds=0; - ts.totalMicros=0; + ts.totalTime=TimeMicros(0,0); ts.totalTicks=0; ts.totalRows=0; ts.isLoopDefined=true; @@ -396,7 +394,7 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove ts.orders[prevOrder][i].seconds=-1; } } - ts.orders[prevOrder][prevRow]=TimeMicros(ts.totalSeconds,ts.totalMicros); + ts.orders[prevOrder][prevRow]=ts.totalTime; rowChanged=false; } @@ -405,16 +403,16 @@ void DivSubSong::calcTimestamps(int chans, std::vector& groove double dt=divider;//*((double)virtualTempoN/(double)MAX(1,virtualTempoD)); ts.totalTicks++; - ts.totalMicros+=1000000/dt; + ts.totalTime.micros+=1000000/dt; totalMicrosOff+=fmod(1000000.0,dt); while (totalMicrosOff>=dt) { totalMicrosOff-=dt; - ts.totalMicros++; + ts.totalTime.micros++; } - if (ts.totalMicros>=1000000) { - ts.totalMicros-=1000000; + if (ts.totalTime.micros>=1000000) { + ts.totalTime.micros-=1000000; // who's gonna play a song for 68 years? - if (ts.totalSeconds<0x7fffffff) ts.totalSeconds++; + if (ts.totalTime.seconds<0x7fffffff) ts.totalTime.seconds++; } } if (ts.maxRow[curOrder]getTotalTicks(); - int totalSeconds=e->getTotalSeconds(); + String timeFormatted=e->getCurTime().toString(2,TA_TIME_FORMAT_MS_ZERO); ImGui::PushFont(bigFont); - ImGui::Text("%.2d:%.2d.%.2d",(totalSeconds/60),totalSeconds%60,totalTicks/10000); + ImGui::TextUnformatted(timeFormatted.c_str()); ImGui::PopFont(); } } diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 5d29a0a6a..63fc23973 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -206,7 +206,8 @@ void FurnaceGUI::drawDebug() { DivSongTimestamps& ts=e->curSubSong->ts; - ImGui::Text("song duration: %d.%06d (%d ticks; %d rows)",ts.totalSeconds,ts.totalMicros,ts.totalTicks,ts.totalRows); + String timeFormatted=ts.totalTime.toString(-1,TA_TIME_FORMAT_AUTO); + ImGui::Text("song duration: %s (%d ticks; %d rows)",timeFormatted.c_str(),ts.totalTicks,ts.totalRows); if (ts.isLoopDefined) { ImGui::Text("loop region is defined"); } else { @@ -219,7 +220,8 @@ void FurnaceGUI::drawDebug() { } ImGui::Text("loop region: %d:%d - %d:%d",ts.loopStart.order,ts.loopStart.row,ts.loopEnd.order,ts.loopEnd.row); - ImGui::Text("loop start time: %d.%06d",ts.loopStartTime.seconds,ts.loopStartTime.micros); + timeFormatted=ts.loopStartTime.toString(-1,TA_TIME_FORMAT_AUTO); + ImGui::Text("loop start time: %s",timeFormatted.c_str()); if (ImGui::TreeNode("Maximum rows")) { for (int i=0; icurSubSong->ordersLen; i++) { diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 35ee384b0..a4081350f 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -2719,7 +2719,7 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->calcSongTimestamps(); DivSongTimestamps& ts=e->curSubSong->ts; - songLength=ts.totalSeconds+(double)ts.totalMicros/1000000.0; + songLength=ts.totalTime.toDouble(); double loopLength=songLength-(ts.loopStartTime.seconds+(double)ts.loopStartTime.micros/1000000.0); e->saveAudio(path.c_str(),audioExportOptions); @@ -4843,8 +4843,7 @@ bool FurnaceGUI::loop() { } ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PLAYBACK_STAT]); if (e->isPlaying() && settings.playbackTime) { - int totalTicks=e->getTotalTicks(); - int totalSeconds=e->getTotalSeconds(); + TimeMicros totalTime=e->getCurTime(); String info; @@ -4873,32 +4872,10 @@ bool FurnaceGUI::loop() { info+=_("| "); - if (totalSeconds==0x7fffffff) { + if (totalTime.seconds==0x7fffffff) { info+=_("Don't you have anything better to do?"); } else { - if (totalSeconds>=86400) { - int totalDays=totalSeconds/86400; - int totalYears=totalDays/365; - totalDays%=365; - int totalMonths=totalDays/30; - totalDays%=30; - -#ifdef HAVE_LOCALE - info+=fmt::sprintf(ngettext("%d year ","%d years ",totalYears),totalYears); - info+=fmt::sprintf(ngettext("%d month ","%d months ",totalMonths),totalMonths); - info+=fmt::sprintf(ngettext("%d day ","%d days ",totalDays),totalDays); -#else - info+=fmt::sprintf(_GN("%d year ","%d years ",totalYears),totalYears); - info+=fmt::sprintf(_GN("%d month ","%d months ",totalMonths),totalMonths); - info+=fmt::sprintf(_GN("%d day ","%d days ",totalDays),totalDays); -#endif - } - - if (totalSeconds>=3600) { - info+=fmt::sprintf("%.2d:",(totalSeconds/3600)%24); - } - - info+=fmt::sprintf("%.2d:%.2d.%.2d",(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000); + info+=totalTime.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO); } ImGui::TextUnformatted(info.c_str()); @@ -6039,7 +6016,7 @@ bool FurnaceGUI::loop() { [this, curFileLambda] () { *curFileLambda=0; e->getCurFileIndex(*curFileLambda); - curProgress=(((double)e->getTotalSeconds()+(double)e->getTotalTicks()/1000000.0)+(songLength*(*curFileLambda)))/totalLength; + curProgress=(e->getCurTime().toDouble()+(songLength*(*curFileLambda)))/totalLength; } ); } @@ -7390,17 +7367,8 @@ bool FurnaceGUI::loop() { if (!fp->isPlaying()) { TimeMicros rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y); if (rowTS.seconds!=-1) { - int cueSeconds=0; - int cueMicros=0; - e->getFilePlayerCue(cueSeconds,cueMicros); - rowTS.seconds+=cueSeconds; - rowTS.micros+=cueMicros; - while (rowTS.micros>=1000000) { - rowTS.micros-=1000000; - rowTS.seconds++; - } - - fp->setPosSeconds(rowTS.seconds,rowTS.micros); + TimeMicros cueTime=e->getFilePlayerCue(); + fp->setPosSeconds(cueTime+rowTS); } } } diff --git a/src/gui/orders.cpp b/src/gui/orders.cpp index 406d0237c..2f85a69b0 100644 --- a/src/gui/orders.cpp +++ b/src/gui/orders.cpp @@ -90,30 +90,11 @@ void FurnaceGUI::drawMobileOrderSel() { // time if (e->isPlaying() && settings.playbackTime) { - int totalTicks=e->getTotalTicks(); - int totalSeconds=e->getTotalSeconds(); - String info=""; + TimeMicros totalTime=e->getCurTime(); + String info=totalTime.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO); - if (totalSeconds==0x7fffffff) { + if (totalTime.seconds==0x7fffffff) { info="∞"; - } else { - if (totalSeconds>=86400) { - int totalDays=totalSeconds/86400; - int totalYears=totalDays/365; - totalDays%=365; - int totalMonths=totalDays/30; - totalDays%=30; - - info+=fmt::sprintf("%dy",totalYears); - info+=fmt::sprintf("%dm",totalMonths); - info+=fmt::sprintf("%dd",totalDays); - } - - if (totalSeconds>=3600) { - info+=fmt::sprintf("%.2d:",(totalSeconds/3600)%24); - } - - info+=fmt::sprintf("%.2d:%.2d.%.2d",(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000); } ImVec2 textSize=ImGui::CalcTextSize(info.c_str()); diff --git a/src/gui/pattern.cpp b/src/gui/pattern.cpp index aeea36b63..46f799c27 100644 --- a/src/gui/pattern.cpp +++ b/src/gui/pattern.cpp @@ -412,7 +412,8 @@ inline void FurnaceGUI::patternRow(int i, bool isPlaying, float lineHeight, int if (rowTS.seconds==-1) { ImGui::Text("---"); } else { - ImGui::Text("%d.%06d",rowTS.seconds,rowTS.micros); + String timeFormatted=rowTS.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO); + ImGui::TextUnformatted(timeFormatted.c_str()); } } } diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index de0884078..be2f12067 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -79,21 +79,15 @@ void FurnaceGUI::drawRefPlayer() { } if (fp->isPlaying()) { if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - int cueSeconds=0; - int cueMicros=0; fp->stop(); - e->getFilePlayerCue(cueSeconds,cueMicros); - fp->setPosSeconds(cueSeconds,cueMicros); + fp->setPosSeconds(e->getFilePlayerCue()); } if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { fp->stop(); fp->setPos(0); } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - int cueSeconds=0; - int cueMicros=0; - e->getFilePlayerCue(cueSeconds,cueMicros); - fp->setPosSeconds(cueSeconds,cueMicros); + fp->setPosSeconds(e->getFilePlayerCue()); } ImGui::SetItemTooltip( _("left click: go to cue position\n" @@ -103,21 +97,12 @@ void FurnaceGUI::drawRefPlayer() { } else { if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { // try setting cue pos - ssize_t curSeconds=0; - unsigned int curMicros=0; - fp->getPosSeconds(curSeconds,curMicros); + TimeMicros curPos=fp->getPosSeconds(); TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0); if (rowTS.seconds==-1) { showError(_("the first row of this order isn't going to play.")); } else { - // calculate difference and set cue pos - curSeconds-=rowTS.seconds; - int curMicrosI=curMicros-rowTS.micros; - while (curMicrosI<0) { - curMicrosI+=1000000; - curSeconds--; - } - e->setFilePlayerCue(curSeconds,curMicrosI); + e->setFilePlayerCue(curPos-rowTS); } } if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { @@ -125,25 +110,23 @@ void FurnaceGUI::drawRefPlayer() { } if (ImGui::BeginPopupContextItem("Edit Cue Position",ImGuiPopupFlags_MouseButtonRight)) { ImGui::TextUnformatted(_("Set cue position at first order:")); - int cueSeconds=0; - int cueMicros=0; + TimeMicros cueTime=e->getFilePlayerCue(); bool altered=false; - e->getFilePlayerCue(cueSeconds,cueMicros); // TODO: improve this... ImGui::SetNextItemWidth(240.0f*dpiScale); - if (ImGui::InputInt(_("Seconds##CuePosS"),&cueSeconds)) { - if (cueSeconds<-3600) cueSeconds=-3600; - if (cueSeconds>3600) cueSeconds=3600; + if (ImGui::InputInt(_("Seconds##CuePosS"),&cueTime.seconds)) { + if (cueTime.seconds<-3600) cueTime.seconds=-3600; + if (cueTime.seconds>3600) cueTime.seconds=3600; altered=true; } ImGui::SetNextItemWidth(240.0f*dpiScale); - if (ImGui::InputInt(_("Microseconds##CuePosM"),&cueMicros,1000,10000)) { - if (cueMicros<0) cueMicros=0; - if (cueMicros>999999) cueMicros=999999; + if (ImGui::InputInt(_("Microseconds##CuePosM"),&cueTime.micros,1000,10000)) { + if (cueTime.micros<0) cueTime.micros=0; + if (cueTime.micros>999999) cueTime.micros=999999; altered=true; } if (altered) { - e->setFilePlayerCue(cueSeconds,cueMicros); + e->setFilePlayerCue(cueTime); } if (ImGui::Button(_("OK"))) { ImGui::CloseCurrentPopup(); @@ -165,21 +148,12 @@ void FurnaceGUI::drawRefPlayer() { } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { // try setting cue pos - ssize_t curSeconds=0; - unsigned int curMicros=0; - fp->getPosSeconds(curSeconds,curMicros); + TimeMicros curPos=fp->getPosSeconds(); TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0); if (rowTS.seconds==-1) { showError(_("the first row of this order isn't going to play.")); } else { - // calculate difference and set cue pos - curSeconds-=rowTS.seconds; - int curMicrosI=curMicros-rowTS.micros; - while (curMicrosI<0) { - curMicrosI+=1000000; - curSeconds--; - } - e->setFilePlayerCue(curSeconds,curMicrosI); + e->setFilePlayerCue(curPos-rowTS); fp->stop(); } } @@ -203,9 +177,7 @@ void FurnaceGUI::drawRefPlayer() { } else { rowTS=e->curSubSong->ts.getTimes(curOrder,0); } - int cueSeconds=0; - int cueMicros=0; - e->getFilePlayerCue(cueSeconds,cueMicros); + TimeMicros cueTime=e->getFilePlayerCue(); if (rowTS.seconds==-1) { if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { showError(_("the row that the pattern cursor is at isn't going to play. try moving the cursor.")); @@ -213,15 +185,7 @@ void FurnaceGUI::drawRefPlayer() { showError(_("the first row of this order isn't going to play. try another order.")); } } else { - int finalSeconds=rowTS.seconds+cueSeconds; - int finalMicros=rowTS.micros+cueMicros; - - while (finalMicros>=1000000) { - finalMicros-=1000000; - finalSeconds++; - } - - fp->setPosSeconds(finalSeconds,finalMicros); + fp->setPosSeconds(rowTS+cueTime); fp->play(); } } diff --git a/src/gui/tutorial.cpp b/src/gui/tutorial.cpp index 2b6d0eb2c..50812fbd2 100644 --- a/src/gui/tutorial.cpp +++ b/src/gui/tutorial.cpp @@ -1082,7 +1082,7 @@ void FurnaceGUI::drawTutorial() { oneQuarter=(oneQuarter*e->curSubSong->virtualTempoN)/e->curSubSong->virtualTempoD; oneQuarter/=e->curSubSong->hz; oneQuarter/=4; - if (cv->playSongs && e->getTotalSeconds()>=oneQuarter) { + if (cv->playSongs && e->getCurTime().seconds>=oneQuarter) { if (loadRandomDemoSong()) { cv->loadInstruments(); e->changeSongP(0); diff --git a/src/timeutils.cpp b/src/timeutils.cpp index 0c9c058d8..158fdd42c 100644 --- a/src/timeutils.cpp +++ b/src/timeutils.cpp @@ -20,7 +20,7 @@ #include "timeutils.h" #include -String TimeMicros::toString(signed char prec, unsigned char hms) { +String TimeMicros::toString(signed char prec, TATimeFormats hms) { String ret; int actualSeconds=seconds; int actualMicros=micros; @@ -31,8 +31,55 @@ String TimeMicros::toString(signed char prec, unsigned char hms) { } if (negative) actualSeconds=-actualSeconds; + // handle auto formats switch (hms) { - case 2: { // h:mm:ss + case TA_TIME_FORMAT_AUTO: + hms=TA_TIME_FORMAT_SECONDS; + if (actualSeconds>60) hms=TA_TIME_FORMAT_MS; + if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS; + if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS; + break; + case TA_TIME_FORMAT_AUTO_ZERO: + hms=TA_TIME_FORMAT_SECONDS; + if (actualSeconds>60) hms=TA_TIME_FORMAT_MS_ZERO; + if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS_ZERO; + if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS_ZERO; + break; + case TA_TIME_FORMAT_AUTO_MS: + hms=TA_TIME_FORMAT_MS; + if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS; + if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS; + break; + case TA_TIME_FORMAT_AUTO_MS_ZERO: + hms=TA_TIME_FORMAT_MS_ZERO; + if (actualSeconds>3600) hms=TA_TIME_FORMAT_HMS_ZERO; + if (actualSeconds>86400) hms=TA_TIME_FORMAT_DAYS_HMS_ZERO; + break; + default: + break; + } + + switch (hms) { + case TA_TIME_FORMAT_SECONDS: { // seconds + if (negative) { + ret=fmt::sprintf("-%d",actualSeconds); + } else { + ret=fmt::sprintf("%d",actualSeconds); + } + break; + } + case TA_TIME_FORMAT_MS: { // m:ss + int m=actualSeconds/60; + int s=actualSeconds%60; + + if (negative) { + ret=fmt::sprintf("-%d:%02d",m,s); + } else { + ret=fmt::sprintf("%d:%02d",m,s); + } + break; + } + case TA_TIME_FORMAT_HMS: { // h:mm:ss int h=actualSeconds/3600; int m=(actualSeconds/60)%60; int s=actualSeconds%60; @@ -44,25 +91,65 @@ String TimeMicros::toString(signed char prec, unsigned char hms) { } break; } - case 1: { // m:ss + case TA_TIME_FORMAT_MS_ZERO: { // mm:ss int m=actualSeconds/60; int s=actualSeconds%60; if (negative) { - ret=fmt::sprintf("-%d:%02d",m,s); + ret=fmt::sprintf("-%02d:%02d",m,s); } else { - ret=fmt::sprintf("%d:%02d",m,s); + ret=fmt::sprintf("%02d:%02d",m,s); } break; } - default: { // seconds + case TA_TIME_FORMAT_HMS_ZERO: { // hh:mm:ss + int h=actualSeconds/3600; + int m=(actualSeconds/60)%60; + int s=actualSeconds%60; + if (negative) { - ret=fmt::sprintf("-%d",actualSeconds); + ret=fmt::sprintf("-%02d:%02d:%02d",h,m,s); } else { - ret=fmt::sprintf("%d",actualSeconds); + ret=fmt::sprintf("%02d:%02d:%02d",h,m,s); } break; } + case TA_TIME_FORMAT_DAYS_HMS: { // ?y?m?d h:mm:ss + int days=actualSeconds/86400; + int years=days/365; + days%=365; + int months=days/30; + days%=30; + int h=(actualSeconds/3600)%24; + int m=(actualSeconds/60)%60; + int s=actualSeconds%60; + + if (negative) { + ret=fmt::sprintf("-%dy%dm%dd %d:%02d:%02d",years,months,days,h,m,s); + } else { + ret=fmt::sprintf("%dy%dm%dd %d:%02d:%02d",years,months,days,h,m,s); + } + break; + } + case TA_TIME_FORMAT_DAYS_HMS_ZERO: { // ?y?m?d hh:mm:ss + int days=actualSeconds/86400; + int years=days/365; + days%=365; + int months=days/30; + days%=30; + int h=(actualSeconds/3600)%24; + int m=(actualSeconds/60)%60; + int s=actualSeconds%60; + + if (negative) { + ret=fmt::sprintf("-%dy%dm%dd %02d:%02d:%02d",years,months,days,h,m,s); + } else { + ret=fmt::sprintf("%dy%dm%dd %02d:%02d:%02d",years,months,days,h,m,s); + } + break; + } + default: + return "ERROR!"; } if (prec<0) { diff --git a/src/timeutils.h b/src/timeutils.h index 0bf07ccf5..7da4f92d8 100644 --- a/src/timeutils.h +++ b/src/timeutils.h @@ -27,6 +27,23 @@ #include "ta-utils.h" +enum TATimeFormats: unsigned char { + TA_TIME_FORMAT_SECONDS=0, // seconds only + TA_TIME_FORMAT_MS, // m:ss + TA_TIME_FORMAT_HMS, // h:mm:ss + + TA_TIME_FORMAT_MS_ZERO, // mm:ss + TA_TIME_FORMAT_HMS_ZERO, // hh:mm:ss + + TA_TIME_FORMAT_DAYS_HMS, // years, months, days, h:mm:ss + TA_TIME_FORMAT_DAYS_HMS_ZERO, // years, months, days, hh:mm:ss + + TA_TIME_FORMAT_AUTO, // automatic + TA_TIME_FORMAT_AUTO_ZERO, // automatic (zero) + TA_TIME_FORMAT_AUTO_MS, // automatic (from minutes) + TA_TIME_FORMAT_AUTO_MS_ZERO, // automatic (from minutes; zero) +}; + struct TimeMicros { int seconds, micros; @@ -37,13 +54,62 @@ struct TimeMicros { return seconds+(double)micros/1000000.0; } + // operators + inline TimeMicros& operator+(TimeMicros& other) { + seconds+=other.seconds; + micros+=other.micros; + while (micros>=1000000) { + micros-=1000000; + seconds++; + } + return *this; + } + inline TimeMicros& operator+(int other) { + seconds+=other; + return *this; + } + inline TimeMicros& operator-(TimeMicros& other) { + seconds-=other.seconds; + micros-=other.micros; + while (micros<0) { + micros+=1000000; + seconds--; + } + return *this; + } + inline TimeMicros& operator-(int other) { + seconds-=other; + return *this; + } + + // comparison operators + inline bool operator==(TimeMicros& other) { + return (seconds==other.seconds && micros==other.micros); + } + inline bool operator>(TimeMicros& other) { + return (seconds>other.seconds || (seconds==other.seconds && micros>other.micros)); + } + inline bool operator>=(TimeMicros& other) { + return (seconds>other.seconds || (seconds==other.seconds && micros>=other.micros)); + } + + inline bool operator!=(TimeMicros& other) { + return !(*this==other); + } + inline bool operator<(TimeMicros& other) { + return !(*this>=other); + } + inline bool operator<=(TimeMicros& other) { + return !(*this>other); + } + /** * convert this TimeMicros to a String. * @param prec maximum digit precision (0-6). use -1 for automatic precision. * @param hms how many denominations to include (0: seconds, 1: minutes, 2: hours). * @return formatted TimeMicros. */ - String toString(signed char prec=-1, unsigned char hms=0); + String toString(signed char prec=6, TATimeFormats hms=TA_TIME_FORMAT_SECONDS); static TimeMicros fromString(String s); TimeMicros(int s, int u): From 3516245d2eb624c345ea2624b40421582ad0f651 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 30 Oct 2025 20:40:22 -0500 Subject: [PATCH 32/34] rename totalTicksOff to totalTimeDrift --- src/engine/engine.cpp | 4 ++-- src/engine/engine.h | 4 ++-- src/engine/playback.cpp | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index b2b87277e..6a58b86de 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1707,7 +1707,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { subticks=0; tempoAccum=0; totalTime=TimeMicros(0,0); - totalTicksOff=0; + totalTimeDrift=0; totalTicksR=0; curMidiClock=0; curMidiTime=0; @@ -3980,7 +3980,7 @@ void DivEngine::quitDispatch() { changeOrd=-1; changePos=0; totalTime=TimeMicros(0,0); - totalTicksOff=0; + totalTimeDrift=0; totalTicksR=0; curMidiClock=0; curMidiTime=0; diff --git a/src/engine/engine.h b/src/engine/engine.h index 804fdc3b4..d272e8c14 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -512,7 +512,7 @@ class DivEngine { int stepPlay; int changeOrd, changePos, totalTicksR, curMidiClock, curMidiTime, totalCmds, lastCmds, cmdsPerSecond; TimeMicros totalTime; - double totalTicksOff; + double totalTimeDrift; int curMidiTimePiece, curMidiTimeCode; unsigned char extValue, pendingMetroTick; DivGroovePattern speeds; @@ -1530,7 +1530,7 @@ class DivEngine { totalCmds(0), lastCmds(0), cmdsPerSecond(0), - totalTicksOff(0.0), + totalTimeDrift(0.0), curMidiTimePiece(0), curMidiTimeCode(0), extValue(0), diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index d52ed1144..72852444d 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -2583,9 +2583,9 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { double dt=divider*tickMult; totalTicksR++; totalTime.micros+=1000000/dt; - totalTicksOff+=fmod(1000000.0,dt); - while (totalTicksOff>=dt) { - totalTicksOff-=dt; + totalTimeDrift+=fmod(1000000.0,dt); + while (totalTimeDrift>=dt) { + totalTimeDrift-=dt; totalTime.micros++; } } From e8aeb45a12d4a42c70aa5b256d872e5e51e0a5c9 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 03:42:43 -0500 Subject: [PATCH 33/34] TimeMicros::fromString() and improve the cue position editor a bit --- src/gui/debugWindow.cpp | 20 ++++ src/gui/gui.cpp | 3 + src/gui/gui.h | 5 + src/gui/refPlayer.cpp | 29 +++--- src/timeutils.cpp | 214 +++++++++++++++++++++++++++++++++++++++- src/timeutils.h | 2 +- 6 files changed, 259 insertions(+), 14 deletions(-) diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 63fc23973..1b3ba9259 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -25,6 +25,7 @@ #include #include "imgui.h" #include "imgui_internal.h" +#include "misc/cpp/imgui_stdlib.h" PendingDrawOsc _debugDo; static float oscDebugData[2048]; @@ -369,6 +370,25 @@ void FurnaceGUI::drawDebug() { ImGui::Unindent(); ImGui::TreePop(); } + if (ImGui::TreeNode("TimeMicros Test")) { + static TimeMicros testTS; + static String testTSIn; + String testTSFormatted=testTS.toString(); + ImGui::Text("Current Value: %s",testTSFormatted.c_str()); + + if (ImGui::InputText("fromString",&testTSIn)) { + try { + testTS=TimeMicros::fromString(testTSIn); + } catch (std::invalid_argument& e) { + ImGui::Text("COULD NOT! (%s)",e.what()); + } + } + + ImGui::InputInt("seconds",&testTS.seconds); + ImGui::InputInt("micros",&testTS.micros); + + ImGui::TreePop(); + } if (ImGui::TreeNode("New File Picker Test")) { static bool check0, check1, check2, check3, check4, check5; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index a4081350f..8cf76bfa7 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -9069,6 +9069,9 @@ FurnaceGUI::FurnaceGUI(): xyOscDecayTime(10.0f), xyOscIntensity(2.0f), xyOscThickness(2.0f), + fpCueInput(""), + fpCueInputFailed(false), + fpCueInputFailReason(""), followLog(true), #ifdef IS_MOBILE pianoOctaves(7), diff --git a/src/gui/gui.h b/src/gui/gui.h index c606c07fd..77df3318c 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2734,6 +2734,11 @@ class FurnaceGUI { float keyHit1[DIV_MAX_CHANS]; int lastIns[DIV_MAX_CHANS]; + // file player temp variables + String fpCueInput; + bool fpCueInputFailed; + String fpCueInputFailReason; + // log window bool followLog; diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp index be2f12067..f7f46cd2c 100644 --- a/src/gui/refPlayer.cpp +++ b/src/gui/refPlayer.cpp @@ -21,6 +21,7 @@ #include #include "imgui.h" #include "IconsFontAwesome4.h" +#include "misc/cpp/imgui_stdlib.h" void FurnaceGUI::drawRefPlayer() { DivFilePlayer* fp=e->getFilePlayer(); @@ -112,19 +113,25 @@ void FurnaceGUI::drawRefPlayer() { ImGui::TextUnformatted(_("Set cue position at first order:")); TimeMicros cueTime=e->getFilePlayerCue(); bool altered=false; - // TODO: improve this... - ImGui::SetNextItemWidth(240.0f*dpiScale); - if (ImGui::InputInt(_("Seconds##CuePosS"),&cueTime.seconds)) { - if (cueTime.seconds<-3600) cueTime.seconds=-3600; - if (cueTime.seconds>3600) cueTime.seconds=3600; - altered=true; + fpCueInput=cueTime.toString(-1,TA_TIME_FORMAT_AUTO); + pushWarningColor(false,fpCueInputFailed); + if (ImGui::InputText("##CuePos",&fpCueInput)) { + try { + cueTime=TimeMicros::fromString(fpCueInput); + altered=true; + fpCueInputFailed=false; + } catch (std::invalid_argument& e) { + fpCueInputFailed=true; + fpCueInputFailReason=e.what(); + } } - ImGui::SetNextItemWidth(240.0f*dpiScale); - if (ImGui::InputInt(_("Microseconds##CuePosM"),&cueTime.micros,1000,10000)) { - if (cueTime.micros<0) cueTime.micros=0; - if (cueTime.micros>999999) cueTime.micros=999999; - altered=true; + if (!ImGui::IsItemActive()) { + fpCueInputFailed=false; } + if (ImGui::IsItemHovered() && fpCueInputFailed) { + ImGui::SetTooltip("%s",fpCueInputFailReason.c_str()); + } + popWarningColor(); if (altered) { e->setFilePlayerCue(cueTime); } diff --git a/src/timeutils.cpp b/src/timeutils.cpp index 158fdd42c..8a0e27649 100644 --- a/src/timeutils.cpp +++ b/src/timeutils.cpp @@ -19,6 +19,7 @@ #include "timeutils.h" #include +#include String TimeMicros::toString(signed char prec, TATimeFormats hms) { String ret; @@ -189,6 +190,215 @@ String TimeMicros::toString(signed char prec, TATimeFormats hms) { return ret; } -TimeMicros TimeMicros::fromString(String s) { - return TimeMicros(0,0); +TimeMicros TimeMicros::fromString(const String& s) { + bool seenYears=false; + bool seenMonths=false; + bool seenDays=false; + bool seenDecimal=false; + bool needSomething=true; + bool canNegative=true; + bool isNegative=false; + unsigned int element[4]; + unsigned int elementDigits[4]; + int elementCount=0; + int curElement=0; + int curElementDigits=0; + + int years=0; + int months=0; + int days=0; + + element[0]=0; + element[1]=0; + element[2]=0; + element[3]=0; + elementDigits[0]=0; + elementDigits[1]=0; + elementDigits[2]=0; + elementDigits[3]=0; + + for (char i: s) { + switch (i) { + // numbers + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + if (curElementDigits<9) { + curElement*=10; + curElement+=(i-'0'); + curElementDigits++; + needSomething=false; + } else { + throw std::invalid_argument("value out of range"); + } + break; + + // negative indicator + case '-': + if (!canNegative) { + throw std::invalid_argument("negative symbol shall be at the beginning"); + } + isNegative=true; + break; + + // element separator + case ':': + if (needSomething) { + throw std::invalid_argument("missing value"); + } + if (seenDecimal) { + throw std::invalid_argument("after decimal"); + } + if (elementCount>=3) { + throw std::invalid_argument("too many elements"); + } + element[elementCount]=curElement; + elementDigits[elementCount++]=curElementDigits; + curElement=0; + curElementDigits=0; + break; + + // decimal separator + case '.': case ',': + if (needSomething && elementCount!=0) { + throw std::invalid_argument("wrong decimal separator"); + } + if (elementCount>=3) { + throw std::invalid_argument("too many elements"); + } + element[elementCount]=curElement; + elementDigits[elementCount++]=curElementDigits; + curElement=0; + curElementDigits=0; + needSomething=true; + seenDecimal=true; + break; + + // year/month/day marker + case 'Y': case 'y': + if (seenYears) { + throw std::invalid_argument("already have years"); + } + if (elementCount!=0) { + throw std::invalid_argument("years must be before time"); + } + years=curElement; + curElement=0; + curElementDigits=0; + needSomething=true; + seenYears=true; + break; + case 'M': case 'm': + if (seenMonths) { + throw std::invalid_argument("already have months"); + } + if (elementCount!=0) { + throw std::invalid_argument("months must be before time"); + } + months=curElement; + curElement=0; + curElementDigits=0; + needSomething=true; + seenMonths=true; + break; + case 'D': case 'd': + if (seenDays) { + throw std::invalid_argument("already have days"); + } + if (elementCount!=0) { + throw std::invalid_argument("days must be before time"); + } + days=curElement; + curElement=0; + curElementDigits=0; + needSomething=true; + seenDays=true; + break; + + // ignore spaces + case ' ': + break; + + // fail if any other character is seen + default: + throw std::invalid_argument("invalid character"); + break; + } + canNegative=false; + } + + // fail if you didn't provide an element + if (needSomething) { + throw std::invalid_argument("missing value at the end"); + } + + element[elementCount]=curElement; + elementDigits[elementCount]=curElementDigits; + + // safety checks (yeah I know these are a bit off but whatever) + if (years>68 || months>828 || (years*365+months*30+days)>24855) { + throw std::invalid_argument("years/months/days out of range"); + } + + // now combine elements + TimeMicros ret; + ret.seconds=86400*(years*365+months*30+days); + if (seenDecimal || elementCount==3) { + if (elementDigits[elementCount]<6) { + for (int i=elementDigits[elementCount]; i<6; i++) { + element[elementCount]*=10; + } + } else if (elementDigits[elementCount]>6) { + for (int i=elementDigits[elementCount]; i>6; i--) { + element[elementCount]/=10; + } + } + ret.micros=element[elementCount]; + + elementCount--; + } + switch (elementCount) { + case 0: // seconds only + ret.seconds+=element[0]; + break; + case 1: // minutes and seconds + if (element[0]>=35791394) { + throw std::invalid_argument("minutes out of range"); + } + if (element[1]>=60) { + throw std::invalid_argument("seconds out of range"); + } + ret.seconds+=(element[0]*60)+element[1]; + break; + case 2: // hours, minutes and seconds + if (seenYears || seenMonths || seenDays) { + if (element[0]>=24) { + throw std::invalid_argument("hours out of range"); + } + } else { + if (element[0]>=596523) { + throw std::invalid_argument("hours out of range"); + } + } + if (element[1]>=60) { + throw std::invalid_argument("minutes out of range"); + } + if (element[2]>=60) { + throw std::invalid_argument("seconds out of range"); + } + ret.seconds+=(element[0]*3600)+(element[1]*60)+element[2]; + break; + default: + throw std::invalid_argument("shouldn't happen"); + break; + } + + if (isNegative) { + ret.seconds=-ret.seconds; + if (ret.micros!=0) { + ret.micros=1000000-ret.micros; + ret.seconds--; + } + } + + return ret; } diff --git a/src/timeutils.h b/src/timeutils.h index 7da4f92d8..48b1d3cbe 100644 --- a/src/timeutils.h +++ b/src/timeutils.h @@ -110,7 +110,7 @@ struct TimeMicros { * @return formatted TimeMicros. */ String toString(signed char prec=6, TATimeFormats hms=TA_TIME_FORMAT_SECONDS); - static TimeMicros fromString(String s); + static TimeMicros fromString(const String& s); TimeMicros(int s, int u): seconds(s), micros(u) {} From 08a27be76fbaa26baa947eb4abce5c3eeba5f2d9 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 05:03:58 -0500 Subject: [PATCH 34/34] GUI: chord input looks like a horrid hack, but it works I'll improve some of the code at some point --- src/engine/engine.cpp | 67 ++++++++++++++++++++++++++++++++++++++++ src/engine/engine.h | 4 +++ src/gui/editControls.cpp | 56 ++++++++++++++++++++++++++++----- src/gui/gui.cpp | 50 +++++++++++++++++++++++++----- src/gui/gui.h | 6 ++-- 5 files changed, 165 insertions(+), 18 deletions(-) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 6a58b86de..98b518456 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -3598,6 +3598,73 @@ void DivEngine::noteOff(int chan) { BUSY_END; } +int DivEngine::getViableChannel(int chan, int off, int ins) { + // if the offset is zero, we don't have to do anything + if (off==0) return chan; + + // if there isn't an instrument, just offset chan by off + if (ins==-1) { + return (chan+off)%chans; + } + + bool isViable[DIV_MAX_CHANS]; + bool isAtLeastOneViable=false; + int finalChan=chan; + int finalChanType=getChannelType(finalChan); + + // this is a copy of the routine in autoNoteOn...... I am lazy + DivInstrument* insInst=getIns(ins); + for (int i=0; i=song.insLen || getPreferInsType(i)==insInst->type || (getPreferInsType(i)==DIV_INS_NULL && finalChanType==DIV_CH_NOISE) || getPreferInsSecondType(i)==insInst->type) { + if (insInst->type==DIV_INS_OPL) { + if (insInst->fm.ops==2 || getChannelType(i)==DIV_CH_OP) { + isViable[i]=true; + isAtLeastOneViable=true; + } else { + isViable[i]=false; + } + } else { + isViable[i]=true; + isAtLeastOneViable=true; + } + } else { + isViable[i]=false; + } + } + + // screw it if none of the channels are viable + if (!isAtLeastOneViable) { + return (chan+off)%chans; + } + + // now offset (confined to viable channels) + int channelsCycled=0; + int i=(chan+1)%chans; + int attempts=0; + while (true) { + if (isViable[i]) { + channelsCycled++; + if (channelsCycled==off) { + // we found it + return i; + } + } + + if (++i>=chans) { + i=0; + } + + // fail-safe + if (++attempts>1024) { + logE("getViableChannel(): too many attempts!"); + break; + } + } + + // fail-safe + return (chan+off)%chans; +} + bool DivEngine::autoNoteOn(int ch, int ins, int note, int vol) { bool isViable[DIV_MAX_CHANS]; bool canPlayAnyway=false; diff --git a/src/engine/engine.h b/src/engine/engine.h index d272e8c14..81698fdf6 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -1202,6 +1202,10 @@ class DivEngine { // set whether autoNoteIn is mono or poly void setAutoNotePoly(bool poly); + // get next viable channel with an offset + // chan is the base channel, off is the offset and ins is the instrument. + int getViableChannel(int chan, int off, int ins); + // go to order void setOrder(unsigned char order); diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index 40e18565b..d0c0fd833 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -759,8 +759,18 @@ void FurnaceGUI::drawEditControls() { ImGui::SameLine(); pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?(_("Poly##PolyInput")):(_("Mono##PolyInput")))) { - noteInputPoly=!noteInputPoly; + if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { + if (noteInputPoly) { + if (noteInputChord) { + noteInputPoly=false; + noteInputChord=false; + } else { + noteInputChord=true; + } + } else { + noteInputPoly=true; + noteInputChord=false; + } e->setAutoNotePoly(noteInputPoly); } if (ImGui::IsItemHovered()) { @@ -889,8 +899,18 @@ void FurnaceGUI::drawEditControls() { ImGui::SameLine(); pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) { - noteInputPoly=!noteInputPoly; + if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { + if (noteInputPoly) { + if (noteInputChord) { + noteInputPoly=false; + noteInputChord=false; + } else { + noteInputChord=true; + } + } else { + noteInputPoly=true; + noteInputChord=false; + } e->setAutoNotePoly(noteInputPoly); } if (ImGui::IsItemHovered()) { @@ -1027,8 +1047,18 @@ void FurnaceGUI::drawEditControls() { popToggleColors(); pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) { - noteInputPoly=!noteInputPoly; + if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { + if (noteInputPoly) { + if (noteInputChord) { + noteInputPoly=false; + noteInputChord=false; + } else { + noteInputChord=true; + } + } else { + noteInputPoly=true; + noteInputChord=false; + } e->setAutoNotePoly(noteInputPoly); } if (ImGui::IsItemHovered()) { @@ -1127,8 +1157,18 @@ void FurnaceGUI::drawEditControls() { ImGui::SameLine(); pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) { - noteInputPoly=!noteInputPoly; + if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { + if (noteInputPoly) { + if (noteInputChord) { + noteInputPoly=false; + noteInputChord=false; + } else { + noteInputChord=true; + } + } else { + noteInputPoly=true; + noteInputChord=false; + } e->setAutoNotePoly(noteInputPoly); } if (ImGui::IsItemHovered()) { diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 8cf76bfa7..302b07dfa 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1296,6 +1296,7 @@ void FurnaceGUI::play(int row) { } curNibble=false; orderNibble=false; + chordInputOffset=0; activeNotes.clear(); } @@ -1311,6 +1312,7 @@ void FurnaceGUI::stop() { e->stop(); curNibble=false; orderNibble=false; + chordInputOffset=0; if (followPattern && wasPlaying) { nextScroll=-1.0f; nextAddScroll=0.0f; @@ -1353,13 +1355,24 @@ void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode, bool autoNote) { } } -void FurnaceGUI::noteInput(int num, int key, int vol) { +void FurnaceGUI::noteInput(int num, int key, int vol, int chanOff) { int ch=cursor.xCoarse; int ord=curOrder; int y=cursor.y; int tick=0; int speed=0; + if (chanOff>0 && noteInputChord) { + ch=e->getViableChannel(ch,chanOff,curIns); + if ((!e->isPlaying() || !followPattern)) { + y-=editStep; + while (y<0) { + if (--ord<0) ord=0; + y+=e->curSubSong->patLen; + } + } + } + if (e->isPlaying() && !e->isStepping() && followPattern) { e->getPlayPosTick(ord,y,tick,speed); if (tick<=(speed/2)) { // round @@ -1373,12 +1386,12 @@ void FurnaceGUI::noteInput(int num, int key, int vol) { } } - logV("noteInput: chan %d, %d:%d %d/%d",ch,ord,y,tick,speed); + logV("noteInput: chan %d, offset %d, %d:%d %d/%d",ch,chanOff,ord,y,tick,speed); DivPattern* pat=e->curPat[ch].getPattern(e->curOrders->ord[ch][ord],true); bool removeIns=false; - prepareUndo(GUI_UNDO_PATTERN_EDIT); + prepareUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y)); if (key==GUI_NOTE_OFF) { // note off pat->newData[y][DIV_PAT_NOTE]=DIV_NOTE_OFF; @@ -1416,8 +1429,10 @@ void FurnaceGUI::noteInput(int num, int key, int vol) { pat->newData[y][DIV_PAT_VOL]=-1; } } - editAdvance(); - makeUndo(GUI_UNDO_PATTERN_EDIT); + if ((!e->isPlaying() || !followPattern) && (chanOff<1 || !noteInputChord)) { + editAdvance(); + } + makeUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y)); curNibble=false; } @@ -1736,8 +1751,9 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { if (num>119) num=119; // B-9 if (edit) { - noteInput(num,key); + noteInput(num,key,-1,chordInputOffset); } + chordInputOffset++; } } else if (edit) { // value auto it=valueKeys.find(ev.key.keysym.sym); @@ -1833,7 +1849,10 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { } void FurnaceGUI::keyUp(SDL_Event& ev) { - // nothing for now + // this is very, very lazy... + if (--chordInputOffset<0) { + chordInputOffset=0; + } } bool dirExists(String s) { @@ -3995,6 +4014,9 @@ bool FurnaceGUI::loop() { break; case SDL_KEYUP: // for now + if (!ImGui::GetIO().WantCaptureKeyboard || (newFilePicker->isOpened() && !ImGui::GetIO().WantTextInput)) { + keyUp(ev); + } insEditMayBeDirty=true; if (introPos<11.0 && introSkip<0.5 && !shortIntro) { introSkipDo=false; @@ -4178,14 +4200,19 @@ bool FurnaceGUI::loop() { if (action!=0) { doAction(action); } else switch (msg.type&0xf0) { + case TA_MIDI_NOTE_OFF: + if (--chordInputOffset<0) chordInputOffset=0; + break; case TA_MIDI_NOTE_ON: if (midiMap.valueInputStyle==0 || midiMap.valueInputStyle>3 || cursor.xFine==0) { if (midiMap.noteInput && edit && msg.data[1]!=0) { noteInput( msg.data[0]-12, 0, - midiMap.volInput?msg.data[1]:-1 + midiMap.volInput?msg.data[1]:-1, + chordInputOffset ); + chordInputOffset++; } } else { if (edit && msg.data[1]!=0) { @@ -7423,6 +7450,9 @@ bool FurnaceGUI::loop() { break; } } + + // reset chord count just in case + chordInputOffset=0; } if (!settings.renderClearPos || renderBackend==GUI_BACKEND_METAL) { @@ -8254,6 +8284,7 @@ void FurnaceGUI::syncState() { followOrders=e->getConfBool("followOrders",true); followPattern=e->getConfBool("followPattern",true); noteInputPoly=e->getConfBool("noteInputPoly",true); + noteInputChord=e->getConfBool("noteInputChord",false); filePlayerSync=e->getConfBool("filePlayerSync",true); audioExportOptions.loops=e->getConfInt("exportLoops",0); if (audioExportOptions.loops<0) audioExportOptions.loops=0; @@ -8414,6 +8445,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("followPattern",followPattern); conf.set("orderEditMode",orderEditMode); conf.set("noteInputPoly",noteInputPoly); + conf.set("noteInputChord",noteInputChord); conf.set("filePlayerSync",filePlayerSync); if (settings.persistFadeOut) { conf.set("exportLoops",audioExportOptions.loops); @@ -8588,6 +8620,7 @@ FurnaceGUI::FurnaceGUI(): sysDupCloneChannels(true), sysDupEnd(false), noteInputPoly(true), + noteInputChord(false), notifyWaveChange(false), notifySampleChange(false), recalcTimestamps(true), @@ -8623,6 +8656,7 @@ FurnaceGUI::FurnaceGUI(): drawHalt(10), macroPointSize(16), waveEditStyle(0), + chordInputOffset(0), displayInsTypeListMakeInsSample(-1), makeDrumkitOctave(3), mobileEditPage(0), diff --git a/src/gui/gui.h b/src/gui/gui.h index 77df3318c..f9ebdac2f 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1713,7 +1713,8 @@ class FurnaceGUI { bool vgmExportDirectStream, displayInsTypeList, displayWaveSizeList; bool portrait, injectBackUp, mobileMenuOpen, warnColorPushed; bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu; - bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, notifyWaveChange, notifySampleChange; + bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, noteInputChord; + bool notifyWaveChange, notifySampleChange; bool recalcTimestamps; bool wantScrollListIns, wantScrollListWave, wantScrollListSample; bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString; @@ -1737,6 +1738,7 @@ class FurnaceGUI { int drawHalt; int macroPointSize; int waveEditStyle; + int chordInputOffset; int displayInsTypeListMakeInsSample; int makeDrumkitOctave; int mobileEditPage; @@ -3080,7 +3082,7 @@ class FurnaceGUI { void doDrag(bool copy=false); void editOptions(bool topMenu); DivSystem systemPicker(bool fullWidth); - void noteInput(int num, int key, int vol=-1); + void noteInput(int num, int key, int vol=-1, int chanOff=0); void valueInput(int num, bool direct=false, int target=-1); void orderInput(int num);