dev238 - Merge branch 'filePlayer'

this introduces a new audio file player with the ability to synchronize
with tracker playback.

it additionally removes the walkSong() and findSongLength() methods.
use calcSongTimestamps() instead, which will fill in
DivEngine::curSubSong->ts (a DivSongTimestamps struct with song time
information). see src/engine/song.h for the struct definition.
This commit is contained in:
tildearrow 2025-10-30 05:11:59 -05:00
commit 0c8cddf356
36 changed files with 1825 additions and 356 deletions

View file

@ -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

View file

@ -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

View file

@ -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];

View file

@ -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<int>& 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<DivCommand>& 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<std::chrono::microseconds>(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; i<DIV_MAX_CHANS; i++) {
keyHit[i]=false;
}
@ -2069,6 +2127,10 @@ void DivEngine::stop() {
}
}
if (curFilePlayer && filePlayerSync) {
curFilePlayer->stop();
}
// reset all chan oscs
for (int i=0; i<chans; i++) {
DivDispatchOscBuffer* buf=disCont[dispatchOfChan[i]].dispatch->getOscBuffer(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; j<DIV_MAX_OUTPUTS; j++) {
song.patchbay.push_back(0xffc00000|j|(j<<16));
}
// wave/sample preview
song.patchbay.reserve(DIV_MAX_OUTPUTS);
for (unsigned int j=0; j<DIV_MAX_OUTPUTS; j++) {
@ -3630,6 +3718,11 @@ void DivEngine::setOrder(unsigned char order) {
prevOrder=curOrder;
if (playing && !freelance) {
playSub(false);
if (curFilePlayer && filePlayerSync) {
syncFilePlayer();
curFilePlayer->play();
}
}
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<DIV_MAX_CHANS; i++) {
isMuted[i]=0;
}
@ -4302,6 +4402,10 @@ bool DivEngine::quit(bool saveConfig) {
metroBuf=NULL;
metroBufLen=0;
}
if (curFilePlayer!=NULL) {
delete curFilePlayer;
curFilePlayer=NULL;
}
if (yrw801ROM!=NULL) delete[] yrw801ROM;
if (tg100ROM!=NULL) delete[] tg100ROM;
if (mu5ROM!=NULL) delete[] mu5ROM;

View file

@ -28,6 +28,7 @@
#include "dataErrors.h"
#include "safeWriter.h"
#include "cmdStream.h"
#include "filePlayer.h"
#include "../audio/taAudio.h"
#include "blip_buf.h"
#include <functional>
@ -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<int>& 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; i<DIV_MAX_CHIP_DEFS; i++) {
sysFileMapFur[i]=DIV_SYSTEM_NULL;

View file

@ -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;

View file

@ -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="";

View file

@ -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="";

View file

@ -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="";

View file

@ -181,7 +181,7 @@ static void writeCmd(std::vector<TiunaBytes>& cmds, TiunaCmd& cmd, unsigned char
}
void DivExportTiuna::run() {
int loopOrder, loopOrderRow, loopEnd;
int loopOrder, loopOrderRow;
int tick=0;
SafeWriter* w;
std::map<int,TiunaCmd> 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;

View file

@ -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);

556
src/engine/filePlayer.cpp Normal file
View file

@ -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 <inttypes.h>
#include <chrono>
#define DIV_FPCACHE_BLOCK_SHIFT 15
#define DIV_FPCACHE_BLOCK_SIZE (1<<DIV_FPCACHE_BLOCK_SHIFT)
#define DIV_FPCACHE_BLOCK_MASK (DIV_FPCACHE_BLOCK_SIZE-1)
#define DIV_FPCACHE_BLOCKS_FROM_FILL 3
#define DIV_FPCACHE_DISCARD_SIZE 4096
// 5MB should be enough
#define DIV_MAX_MEMORY (5<<20)
#define DIV_NO_BLOCK (-10)
void DivFilePlayer::fillBlocksNear(ssize_t pos) {
logV("DivFilePlayer: fillBlocksNear(%" PRIu64 ")",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 read anything if we cannot seek
// (if this is set the file is already read in its entirety)
if (!si.seekable) return;
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 (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 (seekWhere<DIV_FPCACHE_DISCARD_SIZE) {
curSeek=sf_seek(sf,0,SEEK_SET);
// discard
if (sf_readf_float(sf,discardBuf,seekWhere)!=seekWhere) {
// this is a problem
}
} else {
seekWhere-=DIV_FPCACHE_DISCARD_SIZE;
curSeek=sf_seek(sf,seekWhere,SEEK_SET);
// discard
if (sf_readf_float(sf,discardBuf,DIV_FPCACHE_DISCARD_SIZE)!=DIV_FPCACHE_DISCARD_SIZE) {
// this is a problem
}
}
}
// read blocks
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));
}
logV("- reading block %" PRIu64,i);
sf_count_t totalRead=sf_readf_float(sf,blocks[i],DIV_FPCACHE_BLOCK_SIZE);
if (totalRead<DIV_FPCACHE_BLOCK_SIZE) {
// we've reached end of file
}
}
}
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_MAX_MEMORY) return;
pos>>=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; i<pos-2; 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<DIV_MAX_MEMORY) return;
}
for (ssize_t i=numBlocks-1; i>pos+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<DIV_MAX_MEMORY) return;
}
}
void DivFilePlayer::runCacheThread() {
std::unique_lock<std::mutex> 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; i<chans; i++) {
memset(buf[i],0,size*sizeof(float));
}
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();
}
for (unsigned int i=0; i<size; i++) {
// acknowledge pending events
if (pendingPosOffset==i) {
pendingPosOffset=UINT_MAX;
playPos=pendingPos;
rateAccum=0;
}
if (pendingPlayOffset==i) {
playing=true;
}
if (pendingStopOffset==i) {
playing=false;
}
ssize_t blockIndex=playPos>>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<chans; j++) {
buf[j][i]=s;
}
} else for (int j=0; j<chans; j++) {
if (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<chans; j++) {
buf[j][i]=0.0f;
}
}
}
}
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) {
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);
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() {
if (blocks==NULL) return 0;
size_t ret=0;
for (size_t i=0; i<numBlocks; i++) {
if (blocks[i]) ret+=DIV_FPCACHE_BLOCK_SIZE*si.channels*sizeof(float);
}
return ret;
}
bool DivFilePlayer::isBlockPresent(ssize_t pos) {
if (blocks==NULL) return false;
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() {
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<numBlocks; i++) {
if (blocks[i]) {
delete[] blocks[i];
blocks[i]=NULL;
}
}
numBlocks=0;
delete[] blocks;
blocks=NULL;
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];
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<DIV_FPCACHE_BLOCKS_FROM_FILL; i++) {
if (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<numBlocks; i++) {
blocks[i]=new float[DIV_FPCACHE_BLOCK_SIZE*si.channels];
}
for (size_t i=0; i<numBlocks; i++) {
sf_count_t totalRead=sf_readf_float(sf,blocks[i],DIV_FPCACHE_BLOCK_SIZE);
if (totalRead<DIV_FPCACHE_BLOCK_SIZE) {
// we've reached end of file
break;
}
}
} else {
logV("file is seekable");
// read the first couple blocks
fillBlocksNear(0);
}
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;
}
String DivFilePlayer::getLastError() {
return lastError;
}
const SF_INFO& DivFilePlayer::getFileInfo() {
return si;
}
void DivFilePlayer::setOutputRate(int rate) {
if (rate<1) return;
outRate=rate;
}
float DivFilePlayer::getVolume() {
return volume;
}
void DivFilePlayer::setVolume(float vol) {
volume=vol;
}
bool DivFilePlayer::getActive() {
return isActive;
}
void DivFilePlayer::setActive(bool active) {
isActive=active;
}
DivFilePlayer::DivFilePlayer():
discardBuf(NULL),
blocks(NULL),
priorityBlock(NULL),
numBlocks(0),
sf(NULL),
playPos(0),
lastWantBlock(DIV_NO_BLOCK),
wantBlock(DIV_NO_BLOCK),
outRate(44100),
rateAccum(0),
volume(0.0f),
playing(false),
fileError(false),
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();
}
DivFilePlayer::~DivFilePlayer() {
closeFile();
}

106
src/engine/filePlayer.h Normal file
View file

@ -0,0 +1,106 @@
/**
* 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 _FILEPLAYER_H
#define _FILEPLAYER_H
#include "../ta-utils.h"
#include <thread>
#include <mutex>
#include <condition_variable>
#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

View file

@ -128,4 +128,4 @@ float* DivFilterTables::getSincIntegralSmallTable() {
}
}
return sincIntegralSmallTable;
}
}

View file

@ -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 (filePlayerBufLen<size) {
for (int i=0; i<DIV_MAX_OUTPUTS; i++) {
if (filePlayerBuf[i]!=NULL) delete[] filePlayerBuf[i];
filePlayerBuf[i]=new float[size];
}
filePlayerBufLen=size;
}
if (curFilePlayer!=NULL && !exporting) {
curFilePlayer->mix(filePlayerBuf,outChans,size);
} else {
for (int i=0; i<DIV_MAX_OUTPUTS; i++) {
memset(filePlayerBuf[i],0,size*sizeof(float));
}
}
// process metronome
// resize the metronome's audio buffer if necessary
if (metroBufLen<size || metroBuf==NULL) {
@ -3274,6 +3311,19 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi
}
}
// calculate volume of reference file player (so we can attenuate the rest according to the mix slider)
// -1 to 0: player volume goes from 0% to 100%
// 0 to +1: tracker volume goes from 100% to 0%
float refPlayerVol=1.0f;
if (curFilePlayer!=NULL) {
// 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;
}
}
// 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 (srcPortSet<song.systemLen && playing && !halted) {
if (srcSubPort<disCont[srcPortSet].dispatch->getOutputCount()) {
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<size; j++) {
out[destSubPort][j]+=filePlayerBuf[srcSubPort][j];
}
} else if (srcPortSet==0xffd) {
// sample preview
for (size_t j=0; j<size; j++) {

View file

@ -19,254 +19,414 @@
#include "song.h"
#include "../ta-log.h"
#include <chrono>
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<DIV_MAX_PATTERNS; i++) {
if (orders[i]) {
delete[] orders[i];
orders[i]=NULL;
}
}
}
void DivSubSong::calcTimestamps(int chans, std::vector<DivGroovePattern>& 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; i<DIV_MAX_PATTERNS; i++) {
if (ts.orders[i]) {
delete[] ts.orders[i];
ts.orders[i]=NULL;
}
}
// walking state
unsigned char wsWalked[8192];
memset(wsWalked,0,8192);
if (firstPat>0) {
memset(wsWalked,255,32*firstPat);
}
for (int i=firstPat; i<ordersLen; i++) {
for (int j=0; j<chans; j++) {
subPat[j]=pat[j].getPattern(orders.ord[j][i],false);
}
if (i>lastSuspectedLoopEnd) {
lastSuspectedLoopEnd=i;
}
for (int j=nextRow; j<patLen; j++) {
nextRow=0;
bool changingOrder=false;
bool jumpingOrder=false;
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) {
loopOrder=i;
loopRow=j;
loopEnd=lastSuspectedLoopEnd;
return true;
}
for (int k=0; k<chans; k++) {
for (int l=0; l<pat[k].effectCols; l++) {
effectVal=subPat[k]->newData[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; j<pat[i].effectCols; j++) {
short effect=p->newData[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 ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
// - 2: DefleMask (jump to next order unless it is the last one and ignoreJumpAtEnd is on)
if ((curOrder<(ordersLen-1) || !ignoreJumpAtEnd)) {
// changeOrd -2 means increase order by 1
// it overrides a previous 0Bxx effect
changeOrd=-2;
changePos=effectVal;
}
} else if (jumpTreatment==1) {
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
// - 1: old Furnace (same as 2 but ignored if 0Bxx is present)
if (changeOrd<0 && (curOrder<(ordersLen-1) || !ignoreJumpAtEnd)) {
changeOrd=-2;
changePos=effectVal;
}
} else {
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
if (!changingOrder) {
nextOrder=i+1;
// - 0: normal
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;
}
jumpingOrder=true;
nextRow=effectVal;
changePos=effectVal;
}
}
} else if (subPat[k]->newData[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; j<pat[i].effectCols; j++) {
short effect=p->newData[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<chans; i++) {
tinyProcessRow(i,false);
}
// mark this row as "walked" over
wsWalked[((curOrder<<5)+(curRow>>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; i<chans; i++) {
if (rowDelay[i]>0) {
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<MIN(16,speeds.len); i++) {
speedSum+=speeds.val[i];
}
speedSum/=MAX(1,speeds.len);
if (timeBase<1.0) timeBase=1.0;
if (speedSum<1.0) speedSum=1.0;
if (vD<1) vD=1;
return 1.0/((60.0*hz/(timeBase*hl*speedSum))*(double)vN/(double)vD/60.0);
}
void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders_vec, std::vector<DivGroovePattern>& 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; i<ordersLen; i++) {
bool jumped=false;
for (int j=0; j<chans; j++) {
subPat[j]=pat[j].getPattern(orders.ord[j][i],false);
if (shallStop) {
// FFxx found - the song doesn't loop
ts.isLoopable=false;
ts.isLoopDefined=false;
break;
}
if (i>lastSuspectedLoopEnd) {
lastSuspectedLoopEnd=i;
}
for (int j=nextRow; j<patLen; j++) {
nextRow=0;
bool changingOrder=false;
bool jumpingOrder=false;
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) {
return;
}
for (int k=0; k<chans; k++) {
for (int l=0; l<pat[k].effectCols; l++) {
effectVal=subPat[k]->newData[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 ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
}
} else if (jumpTreatment==1) {
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
}
} else {
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
if (!changingOrder) {
nextOrder=i+1;
}
jumpingOrder=true;
nextRow=effectVal;
}
}
} else if (subPat[k]->newData[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; i<DIV_MAX_ROWS; i++) {
ts.orders[prevOrder][i].seconds=-1;
}
}
ts.orders[prevOrder][prevRow]=DivSongTimestamps::Timestamp(ts.totalSeconds,ts.totalMicros);
rowChanged=false;
}
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++;
}
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]<curRow) ts.maxRow[curOrder]=curRow;
}
ts.totalRows--;
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<std::chrono::microseconds>(timeEnd-timeStart).count());
}
void DivSubSong::clearData() {
@ -400,24 +560,23 @@ void DivSong::findSubSongs(int chans) {
for (DivSubSong* i: subsong) {
std::vector<int> subSongStart;
std::vector<int> subSongEnd;
int loopOrder=0;
int loopRow=0;
int loopEnd=-1;
int curStart=-1;
// find possible subsongs
logD("finding subsongs...");
while (++curStart<i->ordersLen) {
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

View file

@ -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<int>& orders, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);
void calcTimestamps(int chans, std::vector<DivGroovePattern>& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat=0);
void clearData();
void removeUnusedPatterns();

View file

@ -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="";

View file

@ -381,6 +381,7 @@ void FurnaceGUI::drawPalette() {
showError("cannot add chip! ("+e->getLastError()+")");
} else {
MARK_MODIFIED;
recalcTimestamps=true;
}
ImGui::CloseCurrentPopup();
if (e->song.autoSystem) {

View file

@ -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; i<e->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();
}
if (ImGui::TreeNode("Sample Debug")) {
for (int i=0; i<e->song.sampleLen; i++) {
DivSample* sample=e->getSample(i);

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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<unsigned short,DivPattern*> 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) {

View file

@ -459,6 +459,7 @@ void FurnaceGUI::doReplace() {
if (!curQueryResults.empty()) {
MARK_MODIFIED;
}
recalcTimestamps=true;
if (!us.pat.empty()) {
undoHist.push_back(us);

View file

@ -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; i<loopOrder && i<(int)songOrdersLengths.size(); i++) {
songLoopedSectionLength-=songOrdersLengths[i];
}
songLoopedSectionLength-=loopRow;
// TODO: fix!
songFadeoutSectionLength=0;
songHasSongEndCommand=!e->curSubSong->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; i<MIN(curOrder,(int)songOrdersLengths.size()); i++) *curPosInRowsLambda+=songOrdersLengths[i];
if (!songHasSongEndCommand) {
e->getLoopsLeft(*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);

View file

@ -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<int> 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<bool*,64> 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);

View file

@ -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),

View file

@ -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) {

View file

@ -290,6 +290,7 @@ void FurnaceGUI::drawNewSong() {
samplePos=0;
updateSampleTex=true;
notifySampleChange=true;
e->calcSongTimestamps();
selStart=SelectionPoint();
selEnd=SelectionPoint();
cursor=SelectionPoint();

View file

@ -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;

View file

@ -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() {

272
src/gui/refPlayer.cpp Normal file
View file

@ -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 <fmt/printf.h>
#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);
}
}

View file

@ -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();

View file

@ -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;

View file

@ -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();

View file

@ -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));
}