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/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 9503106f1..d52aa877c 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -201,15 +201,9 @@ 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) { +void DivEngine::calcSongTimestamps() { 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); } } @@ -566,6 +560,7 @@ void DivEngine::createNew(const char* description, String sysName, bool inBase64 BUSY_BEGIN; renderSamples(); reset(); + calcSongTimestamps(); BUSY_END; } @@ -603,6 +598,7 @@ void DivEngine::createNewFromDefaults() { BUSY_BEGIN; renderSamples(); reset(); + calcSongTimestamps(); BUSY_END; } @@ -1648,6 +1644,47 @@ 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; +} + +bool DivEngine::getFilePlayerSync() { + return filePlayerSync; +} + +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; + 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(); @@ -1678,6 +1715,7 @@ void DivEngine::playSub(bool preserveDrift, int goalRow) { midiTimeDrift=0; if (!preserveDrift) { ticks=1; + subticks=0; tempoAccum=0; totalTicks=0; totalTicksOff=0; @@ -1771,6 +1809,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); } /* @@ -1998,6 +2037,12 @@ bool DivEngine::play() { output->midiOut->send(TAMidiMessage(TA_MIDI_MACHINE_PLAY,0,0)); } bool didItPlay=playing; + if (didItPlay) { + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } + } BUSY_END; return didItPlay; } @@ -2014,15 +2059,28 @@ bool DivEngine::playToRow(int row) { keyHit[i]=false; } bool didItPlay=playing; + if (didItPlay) { + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } + } BUSY_END; return didItPlay; } void DivEngine::stepOne(int row) { + if (curFilePlayer && filePlayerSync) { + curFilePlayer->stop(); + } + if (!isPlaying()) { BUSY_BEGIN_SOFT; freelance=false; playSub(false,row); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + } for (int i=0; istop(); + } + // reset all chan oscs for (int i=0; igetOscBuffer(dispatchChanOfChan[i]); @@ -3100,6 +3162,10 @@ void DivEngine::addOrder(int pos, bool duplicate, bool where) { prevOrder=curOrder; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } } BUSY_END; @@ -3152,6 +3218,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; @@ -3172,6 +3242,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; } @@ -3195,6 +3269,10 @@ void DivEngine::moveOrderUp(int& pos) { pos--; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } BUSY_END; } @@ -3218,6 +3296,10 @@ void DivEngine::moveOrderDown(int& pos) { pos++; if (playing && !freelance) { playSub(false); + if (curFilePlayer && filePlayerSync) { + syncFilePlayer(); + curFilePlayer->play(); + } } BUSY_END; } @@ -3419,6 +3501,12 @@ void DivEngine::autoPatchbay() { } } + // file player + song.patchbay.reserve(DIV_MAX_OUTPUTS); + for (unsigned int j=0; jplay(); + } } BUSY_END; } @@ -3650,6 +3743,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(); } @@ -3909,6 +4006,9 @@ void DivEngine::quitDispatch() { totalCmds=0; lastCmds=0; cmdsPerSecond=0; + if (filePlayerSync) { + if (curFilePlayer!=NULL) curFilePlayer->stop(); + } for (int i=0; i @@ -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,15 @@ class DivEngine { float metroVol; float previewVol; + float* filePlayerBuf[DIV_MAX_OUTPUTS]; + size_t filePlayerBufLen; + DivFilePlayer* curFilePlayer; + bool filePlayerSync; + ssize_t filePlayerCueSeconds; + unsigned int filePlayerCueMicros; + int filePlayerLoopTrail; + int curFilePlayerTrail; + size_t totalProcessed; unsigned int renderPoolThreads; @@ -738,12 +749,25 @@ 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(); + // 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); // save as .fur. @@ -851,11 +875,8 @@ 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 - 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(); // play (returns whether successful) bool play(); @@ -1552,6 +1573,13 @@ class DivEngine { metroAmp(0.0f), metroVol(1.0f), previewVol(1.0f), + filePlayerBufLen(0), + curFilePlayer(NULL), + filePlayerSync(false), + filePlayerCueSeconds(0), + filePlayerCueMicros(0), + filePlayerLoopTrail(0), + curFilePlayerTrail(0), totalProcessed(0), renderPoolThreads(0), renderPool(NULL), @@ -1584,6 +1612,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; iwalkSong(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/filePlayer.cpp b/src/engine/filePlayer.cpp new file mode 100644 index 000000000..9c627ce84 --- /dev/null +++ b/src/engine/filePlayer.cpp @@ -0,0 +1,556 @@ +/** + * 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" +#include "filter.h" +#include "../ta-log.h" +#include +#include + +#define DIV_FPCACHE_BLOCK_SHIFT 15 +#define DIV_FPCACHE_BLOCK_SIZE (1<>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 (ssize_t i=firstBlock; i<=lastBlock; i++) { + if (i<0 || i>=(ssize_t)numBlocks) continue; + if (!blocks[i]) { + needToFill=true; + firstBlock=i; + break; + } + } + + if (!needToFill) return; + + // check whether we need to seek + sf_count_t curSeek=sf_seek(sf,0,SEEK_CUR); + if (curSeek==-1) { + // I/O error + fileError=true; + return; + } + + 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 + // (usually in lossy codecs) + sf_count_t seekWhere=firstBlock<>=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) { + for (int i=0; i1.0f) actualVolume=1.0f; + + if (wantBlock!=DIV_NO_BLOCK) { + cacheCV.notify_one(); + } + + for (unsigned int i=0; i>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] + )*actualVolume; + + for (int j=0; j=si.channels) { + buf[j][i]=0.0f; + continue; + } + + 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] + )*actualVolume; + } + + // advance + rateAccum+=si.samplerate; + while (rateAccum>=outRate) { + rateAccum-=outRate; + playPos++; + /*if (playPos>=(ssize_t)si.frames) { + playPos=0; + }*/ + } + } else { + for (int j=0; j>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() { + return (sf!=NULL); +} + +bool DivFilePlayer::isPlaying() { + return playing; +} + +void DivFilePlayer::play(unsigned int offset) { + if (offset!=UINT_MAX) { + pendingPlayOffset=offset; + logV("DivFilePlayer: playing (offset: %u)",offset); + } else { + playing=true; + logV("DivFilePlayer: playing"); + } +} + +void DivFilePlayer::stop(unsigned int offset) { + if (offset!=UINT_MAX) { + pendingStopOffset=offset; + logV("DivFilePlayer: stopping (offset: %u)",offset); + } else { + playing=false; + logV("DivFilePlayer: stopping"); + } +} + +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; + lastWantBlock=DIV_NO_BLOCK; + rateAccum=0; + fileError=false; + + // read the entire file if not seekable + if (!si.seekable) { + logV("file not seekable - reading..."); + for (size_t i=0; i +#include +#include + +#ifdef HAVE_SNDFILE +#include "sfWrapper.h" +#else +typedef void SNDFILE; +struct SF_INFO { + int invalid; +}; +#endif + +class DivFilePlayer { + float* sincTable; + float* discardBuf; + float** blocks; + bool* priorityBlock; + size_t numBlocks; + String lastError; + SFWrapper sfw; + SNDFILE* sf; + SF_INFO si; + + ssize_t playPos; + ssize_t lastWantBlock; + ssize_t wantBlock; + int outRate; + int rateAccum; + float volume; + bool playing; + bool fileError; + bool quitThread; + 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; + std::condition_variable cacheCV; + + 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); + ssize_t getPos(); + 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=UINT_MAX); + void stop(unsigned int offset=UINT_MAX); + bool closeFile(); + bool loadFile(const char* path); + + String getLastError(); + const SF_INFO& getFileInfo(); + void setOutputRate(int rate); + float getVolume(); + void setVolume(float vol); + bool getActive(); + void setActive(bool active); + + DivFilePlayer(); + ~DivFilePlayer(); +}; + +#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 bfd74f1f0..e7804fc83 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(); @@ -2572,10 +2580,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; @@ -3102,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) { @@ -3237,6 +3257,23 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi renderPool->wait(); } + // process file player + // resize file player audio buffer if necessary + if (filePlayerBufLenmix(filePlayerBuf,outChans,size); + } else { + for (int i=0; igetActive()) { + refPlayerVol=1.0f-curFilePlayer->getVolume(); + if (refPlayerVol<0.0f) refPlayerVol=0.0f; + 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). @@ -3295,7 +3345,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) { @@ -3317,6 +3367,11 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi out[destSubPort][j]+=((float)disCont[srcPortSet].bbOut[srcSubPort][j]/32768.0)*vol; } } + } else if (srcPortSet==0xffc) { + // file player + for (size_t j=0; j +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]; +} -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]; +DivSongTimestamps::DivSongTimestamps(): + totalSeconds(0), + totalMicros(0), + totalTicks(0), + totalRows(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. + std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); + + // reset state + ts.totalSeconds=0; + ts.totalMicros=0; + ts.totalTicks=0; + ts.totalRows=0; + ts.isLoopDefined=true; + ts.isLoopable=true; + + memset(ts.maxRow,0,DIV_MAX_PATTERNS); + + for (int i=0; i0) { 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) { + 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 songWillEnd=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=[&,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* 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=p->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) curVirtualTempoN=effectVal; + break; + case 0xfe: // virtual tempo den + if (effectVal>0) curVirtualTempoD=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) { - if ((inewData[j][DIV_PAT_FX(l)]==0x0b) { - if (nextOrder==-1 || jumpTreatment==0) { - nextOrder=effectVal; - if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) { - nextRow=0; + 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*(timeBase+1))); + if (delayBehavior==2) comparison=true; + if (comparison) { + // set the delay row, order and timer + 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; } - changingOrder=true; } - } + break; } } + // stop processing if EDxx was found + if (returnAfterPre) return; + } else { + //logV("honoring delay at position %d",whatRow); + } - wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7); + + // effects + 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; + 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; + break; + case 0xff: // stop song + shallStopSched=true; + break; + } + } + }; + + auto tinyNextRow=[&,this]() { + // store the previous position + prevOrder=curOrder; + prevRow=curRow; + + if (songWillEnd) { + endOfSong=true; + } + + 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; + songWillEnd=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; + 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. + memset(wsWalked,0,8192); + curOrder=0; + } + } + } + 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 (!songWillEnd && wsWalked[((curOrder<<5)+(curRow>>3))&8191]&(1<<(curRow&7)) && !shallStopSched) { + logV("loop reached"); + songWillEnd=true; + memset(wsWalked,0,8192); + } + // perform speed alternation + // COMPAT FLAG: broken speed alternation + if (brokenSpeedSel) { + unsigned char speed2=(curSpeeds.len>=2)?curSpeeds.val[1]:curSpeeds.val[0]; + unsigned char speed1=curSpeeds.val[0]; - if (nextOrder!=-1) { - i=nextOrder-1; - nextOrder=-1; + // 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]; + } + + if (songWillEnd && !endOfSong) { + ts.loopEnd.order=prevOrder; + ts.loopEnd.row=prevRow; + } + }; + + // MAKE IT WORK + while (!endOfSong) { + // 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; } - } - 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; - } - } + // log row time here + if (rowChanged && !endOfSong) { + if (ts.orders[prevOrder]==NULL) { + ts.orders[prevOrder]=new DivSongTimestamps::Timestamp[DIV_MAX_ROWS]; + for (int i=0; iloopOrder || (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++; - } + if (!endOfSong) { + // 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++; } - - 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 (ts.totalMicros>=1000000) { + ts.totalMicros-=1000000; + // who's gonna play a song for 68 years? + if (ts.totalSeconds<0x7fffffff) ts.totalSeconds++; } } - 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 - } + if (ts.maxRow[curOrder](timeEnd-timeStart).count()); } void DivSubSong::clearData() { @@ -400,24 +560,23 @@ 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); + curStart=i->ts.loopEnd.order; } // if this is the only song, quit diff --git a/src/engine/song.h b/src/engine/song.h index 30121a6ac..d3fca3a64 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -167,6 +167,48 @@ struct DivGroovePattern { } }; +struct DivSongTimestamps { + // song duration (in seconds and microseconds) + int totalSeconds; + int totalMicros; + int totalTicks; + int totalRows; + + // loop region (order/row positions) + struct Position { + int order, row; + Position(): + order(0), row(0) {} + } loopStart, loopEnd; + // set to true if a 0Bxx effect is found and it defines a loop region + bool isLoopDefined; + // set to false if FFxx is found + bool isLoopable; + + // timestamp of a row + // DO NOT ACCESS DIRECTLY! use the functions instead. + struct Timestamp { + // if seconds is -1, it means this row is not touched at all. + int seconds, micros; + Timestamp(int s, int u): + seconds(s), micros(u) {} + Timestamp(): + seconds(0), micros(0) {} + }; + Timestamp* orders[DIV_MAX_PATTERNS]; + Timestamp loopStartTime; + + // the furthest row that the playhead goes through in an order. + unsigned char maxRow[DIV_MAX_PATTERNS]; + + // call this function to get the timestamp of a row. + Timestamp getTimes(int order, int row); + + DivSongTimestamps(); + ~DivSongTimestamps(); +}; + + struct DivSubSong { String name, notes; unsigned char hilightA, hilightB; @@ -185,15 +227,13 @@ struct DivSubSong { String chanName[DIV_MAX_CHANS]; String chanShortName[DIV_MAX_CHANS]; - /** - * 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); + // song timestamps + DivSongTimestamps ts; /** - * find song length in rows (up to specified loop point). + * calculate timestamps (loop position, song length and more). */ - 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); + void calcTimestamps(int chans, std::vector& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat=0); void clearData(); void removeUnusedPatterns(); 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=""; 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/debugWindow.cpp b/src/gui/debugWindow.cpp index fc0d94f32..5d29a0a6a 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -199,6 +199,39 @@ void FurnaceGUI::drawDebug() { ImGui::Text("patScroll: %f",patScroll); ImGui::TreePop(); } + if (ImGui::TreeNode("Song Timestamps")) { + if (ImGui::Button("Recalculate")) { + e->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); + + if (ImGui::TreeNode("Maximum rows")) { + for (int i=0; icurSubSong->ordersLen; i++) { + ImGui::Text("- Order %d: %d",i,ts.maxRow[i]); + } + ImGui::TreePop(); + } + + 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/doAction.cpp b/src/gui/doAction.cpp index 8df11453c..7061396dc 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -357,6 +357,9 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_WINDOW_USER_PRESETS: nextWindow=GUI_WINDOW_USER_PRESETS; break; + case GUI_ACTION_WINDOW_REF_PLAYER: + nextWindow=GUI_WINDOW_REF_PLAYER; + break; case GUI_ACTION_COLLAPSE_WINDOW: collapseWindow=true; @@ -464,6 +467,10 @@ void FurnaceGUI::doAction(int what) { break; case GUI_WINDOW_USER_PRESETS: userPresetsOpen=false; + break; + case GUI_WINDOW_REF_PLAYER: + refPlayerOpen=false; + break; default: break; } diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index 1804b4b80..40e18565b 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -633,6 +633,10 @@ void FurnaceGUI::drawMobileControls() { if (ImGui::Button(_("EffectList"))) { effectListOpen=!effectListOpen; } + ImGui::SameLine(); + if (ImGui::Button(_("RefPlayer"))) { + refPlayerOpen=!refPlayerOpen; + } if (ImGui::Button(_("Switch to Desktop Mode"))) { toggleMobileUI(!mobileUI); } diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 882506163..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->walkSong(loopOrder,loopRow,loopEnd); - } // 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->walkSong(loopOrder,loopRow,loopEnd); break; } + recalcTimestamps=true; + bool shallReplay=false; for (UndoOtherData& i: us.other) { switch (i.target) { @@ -2197,10 +2214,11 @@ void FurnaceGUI::doRedo() { } } } - e->walkSong(loopOrder,loopRow,loopEnd); 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 6a726d4fe..bde08b038 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->walkSong(loopOrder,loopRow,loopEnd); 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->walkSong(loopOrder,loopRow,loopEnd); 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->walkSong(loopOrder,loopRow,loopEnd); makeUndo(GUI_UNDO_CHANGE_ORDER); } } @@ -2311,6 +2310,17 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { dpiScale ); break; + case GUI_FILE_MUSIC_OPEN: + if (!dirExists(workingDirMusic)) workingDirMusic=getHomeDir(); + hasOpened=fileDialog->openLoad( + _("Open Audio File"), + audioLoadFormats, + workingDirMusic, + dpiScale, + NULL, + false + ); + break; case GUI_FILE_TEST_OPEN: if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); hasOpened=fileDialog->openLoad( @@ -2548,7 +2558,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)) { @@ -2706,20 +2716,12 @@ int FurnaceGUI::loadStream(String path) { void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { - songOrdersLengths.clear(); + e->calcSongTimestamps(); - 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); @@ -3841,6 +3843,7 @@ bool FurnaceGUI::loop() { DECLARE_METRIC(log) DECLARE_METRIC(effectList) DECLARE_METRIC(userPresets) + DECLARE_METRIC(refPlayer) DECLARE_METRIC(popup) #ifdef IS_MOBILE @@ -3862,6 +3865,7 @@ bool FurnaceGUI::loop() { while (!quit) { SDL_Event ev; + SelectionPoint prevCursor=cursor; if (e->isPlaying()) { WAKE_UP; } @@ -4453,6 +4457,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) { @@ -4639,6 +4644,7 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError())); } else { MARK_MODIFIED; + recalcTimestamps=true; } ImGui::CloseCurrentPopup(); if (e->song.autoSystem) { @@ -4668,6 +4674,7 @@ bool FurnaceGUI::loop() { if (picked!=DIV_SYSTEM_NULL) { if (e->changeSystem(i,picked,preserveChanPos)) { MARK_MODIFIED; + recalcTimestamps=true; if (e->song.autoSystem) { autoDetectSystem(); } @@ -4692,6 +4699,7 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("cannot remove chip! (%s)"),e->getLastError())); } else { MARK_MODIFIED; + recalcTimestamps=true; } if (e->song.autoSystem) { autoDetectSystem(); @@ -4821,6 +4829,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 +5052,7 @@ bool FurnaceGUI::loop() { MEASURE(memory,drawMemory()); MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); + MEASURE(refPlayer,drawRefPlayer()); MEASURE(patManager,drawPatManager()); } else { @@ -5088,6 +5098,7 @@ bool FurnaceGUI::loop() { MEASURE(log,drawLog()); MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); + MEASURE(refPlayer,drawRefPlayer()); } @@ -5239,6 +5250,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 +5891,17 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("Error while loading file! (%s)"),lastError)); } break; + case GUI_FILE_MUSIC_OPEN: + 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); break; @@ -6015,43 +6040,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()); @@ -6502,6 +6505,7 @@ bool FurnaceGUI::loop() { selStart.order=0; selEnd.order=0; MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Current subsong"))) { @@ -6515,6 +6519,7 @@ bool FurnaceGUI::loop() { selStart.order=0; selEnd.order=0; MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Orders"))) { @@ -6529,6 +6534,7 @@ bool FurnaceGUI::loop() { selStart.order=0; selEnd.order=0; MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Pattern"))) { @@ -6540,6 +6546,7 @@ bool FurnaceGUI::loop() { } }); MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Instruments"))) { @@ -6583,6 +6590,7 @@ bool FurnaceGUI::loop() { e->curSubSong->rearrangePatterns(); }); MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Remove unused patterns"))) { @@ -6591,6 +6599,7 @@ bool FurnaceGUI::loop() { e->curSubSong->removeUnusedPatterns(); }); MARK_MODIFIED; + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } if (ImGui::Button(_("Remove unused instruments"))) { @@ -6644,6 +6653,7 @@ bool FurnaceGUI::loop() { selStart=cursor; selEnd=cursor; curOrder=0; + recalcTimestamps=true; MARK_MODIFIED; } ImGui::CloseCurrentPopup(); @@ -6662,6 +6672,7 @@ bool FurnaceGUI::loop() { MARK_MODIFIED; } updateROMExportAvail(); + recalcTimestamps=true; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); @@ -7374,6 +7385,35 @@ bool FurnaceGUI::loop() { } } + if (recalcTimestamps) { + logV("need to recalc timestamps..."); + e->calcSongTimestamps(); + recalcTimestamps=false; + } + + if (!e->isPlaying() && e->getFilePlayerSync()) { + 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()) { + 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); + } + } + } + } + sampleMapWaitingInput=(curWindow==GUI_WINDOW_INS_EDIT && sampleMapFocused); curWindowThreadSafe=curWindow; @@ -8170,11 +8210,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 +8258,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); @@ -8251,6 +8294,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); @@ -8333,11 +8377,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 +8422,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); @@ -8407,6 +8454,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); @@ -8582,6 +8630,7 @@ FurnaceGUI::FurnaceGUI(): noteInputPoly(true), notifyWaveChange(false), notifySampleChange(false), + recalcTimestamps(true), wantScrollListIns(false), wantScrollListWave(false), wantScrollListSample(false), @@ -8603,9 +8652,11 @@ FurnaceGUI::FurnaceGUI(): safeMode(false), midiWakeUp(true), makeDrumkitMode(false), + filePlayerSync(true), audioEngineChanged(false), settingsChanged(false), debugFFT(false), + debugRowTimestamps(false), vgmExportVersion(0x171), vgmExportTrailingTicks(-1), vgmExportCorrectedRate(44100), @@ -8707,9 +8758,6 @@ FurnaceGUI::FurnaceGUI(): soloChan(-1), orderEditMode(0), orderCursor(-1), - loopOrder(-1), - loopRow(-1), - loopEnd(-1), isClipping(0), newSongCategory(0), latchTarget(0), @@ -8779,6 +8827,7 @@ FurnaceGUI::FurnaceGUI(): csPlayerOpen(false), cvOpen(false), userPresetsOpen(false), + refPlayerOpen(false), cvNotSerious(false), shortIntro(false), insListDir(false), @@ -9242,8 +9291,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 6aaaf741e..50cb9943a 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; @@ -1711,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; @@ -1723,7 +1727,8 @@ class FurnaceGUI { bool safeMode; bool midiWakeUp; bool makeDrumkitMode; - bool audioEngineChanged, settingsChanged, debugFFT; + bool filePlayerSync; + bool audioEngineChanged, settingsChanged, debugFFT, debugRowTimestamps; bool willExport[DIV_MAX_CHIPS]; int vgmExportVersion; int vgmExportTrailingTicks; @@ -1818,7 +1823,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 @@ -2375,7 +2379,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; @@ -2392,7 +2396,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 +2996,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/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/orders.cpp b/src/gui/orders.cpp index ed4a823d9..406d0237c 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,6 @@ 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); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { orderCursor=j; @@ -400,7 +399,6 @@ void FurnaceGUI::drawOrders() { } } else { setOrder(i); - e->walkSong(loopOrder,loopRow,loopEnd); 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->walkSong(loopOrder,loopRow,loopEnd); makeUndo(GUI_UNDO_CHANGE_ORDER); } else { orderCursor=j; @@ -449,7 +446,6 @@ void FurnaceGUI::drawOrders() { } } else { setOrder(i); - e->walkSong(loopOrder,loopRow,loopEnd); if (orderEditMode!=0) { orderCursor=j; curNibble=false; 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() { diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp new file mode 100644 index 000000000..aec5a0a3d --- /dev/null +++ b/src/gui/refPlayer.cpp @@ -0,0 +1,272 @@ +/** + * 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() { + 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"))) { + 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; + 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; + 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::TextUnformatted(_("no file loaded")); + } + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::SliderScalar("##Position",ImGuiDataType_U64,&playPos,&minPos,&maxPos,"")) { + fp->setPos(playPos); + } + + 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_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::TextUnformatted(_("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(); + } + 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()) { + pushToggleColors(true); + if (ImGui::Button(ICON_FA_PAUSE "##Pause")) { + fp->stop(); + } + 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")) { + fp->play(); + } + ImGui::SetItemTooltip(_("play")); + } + 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. try 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; + } + ImGui::SetItemTooltip(_("synchronize playback with tracker playback")); + 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,-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::SetItemTooltip(_("right click to reset")); + + //ImGui::Text("Memory usage: %" PRIu64 "K",fp->getMemUsage()>>10); + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER; + ImGui::End(); + + + if (!refPlayerOpen) { + fp->stop(); + e->setFilePlayerSync(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/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; 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(); 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)); }