Merge branch 'master' into spectrum

This commit is contained in:
Eknous-P 2025-11-01 00:13:38 +04:00
commit a78116ba02
64 changed files with 2700 additions and 640 deletions

View file

@ -104,10 +104,9 @@ void FurnaceGUI::drawClock() {
}
}
if (clockShowTime) {
int totalTicks=e->getTotalTicks();
int totalSeconds=e->getTotalSeconds();
String timeFormatted=e->getCurTime().toString(2,TA_TIME_FORMAT_MS_ZERO);
ImGui::PushFont(bigFont);
ImGui::Text("%.2d:%.2d.%.2d",(totalSeconds/60),totalSeconds%60,totalTicks/10000);
ImGui::TextUnformatted(timeFormatted.c_str());
ImGui::PopFont();
}
}

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

@ -25,6 +25,7 @@
#include <fmt/printf.h>
#include "imgui.h"
#include "imgui_internal.h"
#include "misc/cpp/imgui_stdlib.h"
PendingDrawOsc _debugDo;
static float oscDebugData[2048];
@ -199,6 +200,41 @@ 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;
String timeFormatted=ts.totalTime.toString(-1,TA_TIME_FORMAT_AUTO);
ImGui::Text("song duration: %s (%d ticks; %d rows)",timeFormatted.c_str(),ts.totalTicks,ts.totalRows);
if (ts.isLoopDefined) {
ImGui::Text("loop region is defined");
} else {
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);
timeFormatted=ts.loopStartTime.toString(-1,TA_TIME_FORMAT_AUTO);
ImGui::Text("loop start time: %s",timeFormatted.c_str());
if (ImGui::TreeNode("Maximum rows")) {
for (int i=0; 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);
@ -334,6 +370,25 @@ void FurnaceGUI::drawDebug() {
ImGui::Unindent();
ImGui::TreePop();
}
if (ImGui::TreeNode("TimeMicros Test")) {
static TimeMicros testTS;
static String testTSIn;
String testTSFormatted=testTS.toString();
ImGui::Text("Current Value: %s",testTSFormatted.c_str());
if (ImGui::InputText("fromString",&testTSIn)) {
try {
testTS=TimeMicros::fromString(testTSIn);
} catch (std::invalid_argument& e) {
ImGui::Text("COULD NOT! (%s)",e.what());
}
}
ImGui::InputInt("seconds",&testTS.seconds);
ImGui::InputInt("micros",&testTS.micros);
ImGui::TreePop();
}
if (ImGui::TreeNode("New File Picker Test")) {
static bool check0, check1, check2, check3, check4, check5;

View file

@ -363,6 +363,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;
@ -471,6 +474,9 @@ void FurnaceGUI::doAction(int what) {
case GUI_WINDOW_USER_PRESETS:
userPresetsOpen=false;
break;
case GUI_WINDOW_REF_PLAYER:
refPlayerOpen=false;
break;
case GUI_WINDOW_TUNER:
tunerOpen=false;
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);
}
@ -755,8 +759,18 @@ void FurnaceGUI::drawEditControls() {
ImGui::SameLine();
pushToggleColors(noteInputPoly);
if (ImGui::Button(noteInputPoly?(_("Poly##PolyInput")):(_("Mono##PolyInput")))) {
noteInputPoly=!noteInputPoly;
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
if (noteInputPoly) {
if (noteInputChord) {
noteInputPoly=false;
noteInputChord=false;
} else {
noteInputChord=true;
}
} else {
noteInputPoly=true;
noteInputChord=false;
}
e->setAutoNotePoly(noteInputPoly);
}
if (ImGui::IsItemHovered()) {
@ -885,8 +899,18 @@ void FurnaceGUI::drawEditControls() {
ImGui::SameLine();
pushToggleColors(noteInputPoly);
if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) {
noteInputPoly=!noteInputPoly;
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
if (noteInputPoly) {
if (noteInputChord) {
noteInputPoly=false;
noteInputChord=false;
} else {
noteInputChord=true;
}
} else {
noteInputPoly=true;
noteInputChord=false;
}
e->setAutoNotePoly(noteInputPoly);
}
if (ImGui::IsItemHovered()) {
@ -1023,8 +1047,18 @@ void FurnaceGUI::drawEditControls() {
popToggleColors();
pushToggleColors(noteInputPoly);
if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) {
noteInputPoly=!noteInputPoly;
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
if (noteInputPoly) {
if (noteInputChord) {
noteInputPoly=false;
noteInputChord=false;
} else {
noteInputChord=true;
}
} else {
noteInputPoly=true;
noteInputChord=false;
}
e->setAutoNotePoly(noteInputPoly);
}
if (ImGui::IsItemHovered()) {
@ -1123,8 +1157,18 @@ void FurnaceGUI::drawEditControls() {
ImGui::SameLine();
pushToggleColors(noteInputPoly);
if (ImGui::Button(noteInputPoly?_("Poly##PolyInput"):_("Mono##PolyInput"))) {
noteInputPoly=!noteInputPoly;
if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) {
if (noteInputPoly) {
if (noteInputChord) {
noteInputPoly=false;
noteInputChord=false;
} else {
noteInputChord=true;
}
} else {
noteInputPoly=true;
noteInputChord=false;
}
e->setAutoNotePoly(noteInputPoly);
}
if (ImGui::IsItemHovered()) {

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

@ -1283,7 +1283,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;
@ -1302,6 +1301,7 @@ void FurnaceGUI::play(int row) {
}
curNibble=false;
orderNibble=false;
chordInputOffset=0;
activeNotes.clear();
}
@ -1314,10 +1314,10 @@ 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;
chordInputOffset=0;
if (followPattern && wasPlaying) {
nextScroll=-1.0f;
nextAddScroll=0.0f;
@ -1360,13 +1360,24 @@ void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode, bool autoNote) {
}
}
void FurnaceGUI::noteInput(int num, int key, int vol) {
void FurnaceGUI::noteInput(int num, int key, int vol, int chanOff) {
int ch=cursor.xCoarse;
int ord=curOrder;
int y=cursor.y;
int tick=0;
int speed=0;
if (chanOff>0 && noteInputChord) {
ch=e->getViableChannel(ch,chanOff,curIns);
if ((!e->isPlaying() || !followPattern)) {
y-=editStep;
while (y<0) {
if (--ord<0) ord=0;
y+=e->curSubSong->patLen;
}
}
}
if (e->isPlaying() && !e->isStepping() && followPattern) {
e->getPlayPosTick(ord,y,tick,speed);
if (tick<=(speed/2)) { // round
@ -1380,12 +1391,12 @@ 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, offset %d, %d:%d %d/%d",ch,chanOff,ord,y,tick,speed);
DivPattern* pat=e->curPat[ch].getPattern(e->curOrders->ord[ch][ord],true);
bool removeIns=false;
prepareUndo(GUI_UNDO_PATTERN_EDIT);
prepareUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y));
if (key==GUI_NOTE_OFF) { // note off
pat->newData[y][DIV_PAT_NOTE]=DIV_NOTE_OFF;
@ -1423,8 +1434,10 @@ void FurnaceGUI::noteInput(int num, int key, int vol) {
pat->newData[y][DIV_PAT_VOL]=-1;
}
}
editAdvance();
makeUndo(GUI_UNDO_PATTERN_EDIT);
if ((!e->isPlaying() || !followPattern) && (chanOff<1 || !noteInputChord)) {
editAdvance();
}
makeUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y));
curNibble=false;
}
@ -1433,6 +1446,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);
}
@ -1543,7 +1558,6 @@ void FurnaceGUI::orderInput(int num) {
}
}
}
e->walkSong(loopOrder,loopRow,loopEnd);
makeUndo(GUI_UNDO_CHANGE_ORDER);
}
}
@ -1742,8 +1756,9 @@ void FurnaceGUI::keyDown(SDL_Event& ev) {
if (num>119) num=119; // B-9
if (edit) {
noteInput(num,key);
noteInput(num,key,-1,chordInputOffset);
}
chordInputOffset++;
}
} else if (edit) { // value
auto it=valueKeys.find(ev.key.keysym.sym);
@ -1839,7 +1854,10 @@ void FurnaceGUI::keyDown(SDL_Event& ev) {
}
void FurnaceGUI::keyUp(SDL_Event& ev) {
// nothing for now
// this is very, very lazy...
if (--chordInputOffset<0) {
chordInputOffset=0;
}
}
bool dirExists(String s) {
@ -2316,6 +2334,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(
@ -2553,7 +2582,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)) {
@ -2711,20 +2740,11 @@ int FurnaceGUI::loadStream(String path) {
void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
songOrdersLengths.clear();
e->calcSongTimestamps();
DivSongTimestamps& ts=e->curSubSong->ts;
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;
songLength=ts.totalTime.toDouble();
double loopLength=songLength-(ts.loopStartTime.seconds+(double)ts.loopStartTime.micros/1000000.0);
e->saveAudio(path.c_str(),audioExportOptions);
@ -2732,19 +2752,15 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
e->getTotalAudioFiles(totalFiles);
int totalLoops=0;
lengthOfOneFile=songLength;
if (!songHasSongEndCommand) {
if (ts.isLoopable) {
e->getTotalLoops(totalLoops);
lengthOfOneFile+=songLoopedSectionLength*totalLoops;
lengthOfOneFile+=songFadeoutSectionLength; // account for fadeout
songLength+=loopLength*totalLoops;
songLength+=audioExportOptions.fadeOut;
}
totalLength=lengthOfOneFile*totalFiles;
totalLength=songLength*totalFiles;
curProgress=0.0f;
displayExporting=true;
}
@ -3848,6 +3864,7 @@ bool FurnaceGUI::loop() {
DECLARE_METRIC(log)
DECLARE_METRIC(effectList)
DECLARE_METRIC(userPresets)
DECLARE_METRIC(refPlayer)
DECLARE_METRIC(popup)
#ifdef IS_MOBILE
@ -3869,6 +3886,7 @@ bool FurnaceGUI::loop() {
while (!quit) {
SDL_Event ev;
SelectionPoint prevCursor=cursor;
if (e->isPlaying()) {
WAKE_UP;
}
@ -4003,6 +4021,9 @@ bool FurnaceGUI::loop() {
break;
case SDL_KEYUP:
// for now
if (!ImGui::GetIO().WantCaptureKeyboard || (newFilePicker->isOpened() && !ImGui::GetIO().WantTextInput)) {
keyUp(ev);
}
insEditMayBeDirty=true;
if (introPos<11.0 && introSkip<0.5 && !shortIntro) {
introSkipDo=false;
@ -4186,14 +4207,19 @@ bool FurnaceGUI::loop() {
if (action!=0) {
doAction(action);
} else switch (msg.type&0xf0) {
case TA_MIDI_NOTE_OFF:
if (--chordInputOffset<0) chordInputOffset=0;
break;
case TA_MIDI_NOTE_ON:
if (midiMap.valueInputStyle==0 || midiMap.valueInputStyle>3 || cursor.xFine==0) {
if (midiMap.noteInput && edit && msg.data[1]!=0) {
noteInput(
msg.data[0]-12,
0,
midiMap.volInput?msg.data[1]:-1
midiMap.volInput?msg.data[1]:-1,
chordInputOffset
);
chordInputOffset++;
}
} else {
if (edit && msg.data[1]!=0) {
@ -4462,6 +4488,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) {
@ -4648,6 +4675,7 @@ bool FurnaceGUI::loop() {
showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError()));
} else {
MARK_MODIFIED;
recalcTimestamps=true;
}
ImGui::CloseCurrentPopup();
if (e->song.autoSystem) {
@ -4677,6 +4705,7 @@ bool FurnaceGUI::loop() {
if (picked!=DIV_SYSTEM_NULL) {
if (e->changeSystem(i,picked,preserveChanPos)) {
MARK_MODIFIED;
recalcTimestamps=true;
if (e->song.autoSystem) {
autoDetectSystem();
}
@ -4701,6 +4730,7 @@ bool FurnaceGUI::loop() {
showError(fmt::sprintf(_("cannot remove chip! (%s)"),e->getLastError()));
} else {
MARK_MODIFIED;
recalcTimestamps=true;
}
if (e->song.autoSystem) {
autoDetectSystem();
@ -4832,6 +4862,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();
@ -4850,8 +4881,7 @@ bool FurnaceGUI::loop() {
}
ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_PLAYBACK_STAT]);
if (e->isPlaying() && settings.playbackTime) {
int totalTicks=e->getTotalTicks();
int totalSeconds=e->getTotalSeconds();
TimeMicros totalTime=e->getCurTime();
String info;
@ -4880,32 +4910,10 @@ bool FurnaceGUI::loop() {
info+=_("| ");
if (totalSeconds==0x7fffffff) {
if (totalTime.seconds==0x7fffffff) {
info+=_("Don't you have anything better to do?");
} else {
if (totalSeconds>=86400) {
int totalDays=totalSeconds/86400;
int totalYears=totalDays/365;
totalDays%=365;
int totalMonths=totalDays/30;
totalDays%=30;
#ifdef HAVE_LOCALE
info+=fmt::sprintf(ngettext("%d year ","%d years ",totalYears),totalYears);
info+=fmt::sprintf(ngettext("%d month ","%d months ",totalMonths),totalMonths);
info+=fmt::sprintf(ngettext("%d day ","%d days ",totalDays),totalDays);
#else
info+=fmt::sprintf(_GN("%d year ","%d years ",totalYears),totalYears);
info+=fmt::sprintf(_GN("%d month ","%d months ",totalMonths),totalMonths);
info+=fmt::sprintf(_GN("%d day ","%d days ",totalDays),totalDays);
#endif
}
if (totalSeconds>=3600) {
info+=fmt::sprintf("%.2d:",(totalSeconds/3600)%24);
}
info+=fmt::sprintf("%.2d:%.2d.%.2d",(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000);
info+=totalTime.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO);
}
ImGui::TextUnformatted(info.c_str());
@ -5054,6 +5062,7 @@ bool FurnaceGUI::loop() {
MEASURE(memory,drawMemory());
MEASURE(effectList,drawEffectList());
MEASURE(userPresets,drawUserPresets());
MEASURE(refPlayer,drawRefPlayer());
MEASURE(patManager,drawPatManager());
} else {
@ -5101,6 +5110,7 @@ bool FurnaceGUI::loop() {
MEASURE(log,drawLog());
MEASURE(effectList,drawEffectList());
MEASURE(userPresets,drawUserPresets());
MEASURE(refPlayer,drawRefPlayer());
}
@ -5252,6 +5262,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:
@ -5890,6 +5903,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;
@ -6025,46 +6049,21 @@ bool FurnaceGUI::loop() {
if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_CHAN) {
ImGui::Text(_("Please wait..."));
}
float* progressLambda=&curProgress;
int curPosInRows=0;
int* curPosInRowsLambda=&curPosInRows;
int 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, 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;
}
curProgress=(e->getCurTime().toDouble()+(songLength*(*curFileLambda)))/totalLength;
}
);
}
ImGui::Text(_("Row %d of %d"),curPosInRows+((totalLoops)-(loopsLeft))*songLength,lengthOfOneFile);
if (curProgress<0.0f) curProgress=0.0f;
if (curProgress>1.0f) curProgress=1.0f;
if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles);
ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0),fmt::sprintf("%.2f%%",curProgress*100.0f).c_str());
@ -6515,6 +6514,7 @@ bool FurnaceGUI::loop() {
selStart.order=0;
selEnd.order=0;
MARK_MODIFIED;
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Current subsong"))) {
@ -6528,6 +6528,7 @@ bool FurnaceGUI::loop() {
selStart.order=0;
selEnd.order=0;
MARK_MODIFIED;
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Orders"))) {
@ -6542,6 +6543,7 @@ bool FurnaceGUI::loop() {
selStart.order=0;
selEnd.order=0;
MARK_MODIFIED;
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Pattern"))) {
@ -6553,6 +6555,7 @@ bool FurnaceGUI::loop() {
}
});
MARK_MODIFIED;
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Instruments"))) {
@ -6596,6 +6599,7 @@ bool FurnaceGUI::loop() {
e->curSubSong->rearrangePatterns();
});
MARK_MODIFIED;
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Remove unused patterns"))) {
@ -6604,6 +6608,7 @@ bool FurnaceGUI::loop() {
e->curSubSong->removeUnusedPatterns();
});
MARK_MODIFIED;
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
if (ImGui::Button(_("Remove unused instruments"))) {
@ -6657,6 +6662,7 @@ bool FurnaceGUI::loop() {
selStart=cursor;
selEnd=cursor;
curOrder=0;
recalcTimestamps=true;
MARK_MODIFIED;
}
ImGui::CloseCurrentPopup();
@ -6675,6 +6681,7 @@ bool FurnaceGUI::loop() {
MARK_MODIFIED;
}
updateROMExportAvail();
recalcTimestamps=true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
@ -7387,6 +7394,26 @@ 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()) {
TimeMicros rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y);
if (rowTS.seconds!=-1) {
TimeMicros cueTime=e->getFilePlayerCue();
fp->setPosSeconds(cueTime+rowTS);
}
}
}
}
sampleMapWaitingInput=(curWindow==GUI_WINDOW_INS_EDIT && sampleMapFocused);
curWindowThreadSafe=curWindow;
@ -7436,6 +7463,9 @@ bool FurnaceGUI::loop() {
break;
}
}
// reset chord count just in case
chordInputOffset=0;
}
if (!settings.renderClearPos || renderBackend==GUI_BACKEND_METAL) {
@ -8183,11 +8213,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);
@ -8231,6 +8263,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);
@ -8266,6 +8299,8 @@ void FurnaceGUI::syncState() {
followOrders=e->getConfBool("followOrders",true);
followPattern=e->getConfBool("followPattern",true);
noteInputPoly=e->getConfBool("noteInputPoly",true);
noteInputChord=e->getConfBool("noteInputChord",false);
filePlayerSync=e->getConfBool("filePlayerSync",true);
audioExportOptions.loops=e->getConfInt("exportLoops",0);
if (audioExportOptions.loops<0) audioExportOptions.loops=0;
audioExportOptions.fadeOut=e->getConfDouble("exportFadeOut",0.0);
@ -8354,11 +8389,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
@ -8399,6 +8436,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);
@ -8430,6 +8468,8 @@ void FurnaceGUI::commitState(DivConfig& conf) {
conf.set("followPattern",followPattern);
conf.set("orderEditMode",orderEditMode);
conf.set("noteInputPoly",noteInputPoly);
conf.set("noteInputChord",noteInputChord);
conf.set("filePlayerSync",filePlayerSync);
if (settings.persistFadeOut) {
conf.set("exportLoops",audioExportOptions.loops);
conf.set("exportFadeOut",audioExportOptions.fadeOut);
@ -8630,8 +8670,10 @@ FurnaceGUI::FurnaceGUI():
sysDupCloneChannels(true),
sysDupEnd(false),
noteInputPoly(true),
noteInputChord(false),
notifyWaveChange(false),
notifySampleChange(false),
recalcTimestamps(true),
wantScrollListIns(false),
wantScrollListWave(false),
wantScrollListSample(false),
@ -8653,15 +8695,18 @@ FurnaceGUI::FurnaceGUI():
safeMode(false),
midiWakeUp(true),
makeDrumkitMode(false),
filePlayerSync(true),
audioEngineChanged(false),
settingsChanged(false),
debugFFT(false),
debugRowTimestamps(false),
vgmExportVersion(0x171),
vgmExportTrailingTicks(-1),
vgmExportCorrectedRate(44100),
drawHalt(10),
macroPointSize(16),
waveEditStyle(0),
chordInputOffset(0),
displayInsTypeListMakeInsSample(-1),
makeDrumkitOctave(3),
mobileEditPage(0),
@ -8726,12 +8771,8 @@ FurnaceGUI::FurnaceGUI():
patFont(NULL),
bigFont(NULL),
headFont(NULL),
songLength(0),
songLoopedSectionLength(0),
songFadeoutSectionLength(0),
songHasSongEndCommand(false),
lengthOfOneFile(0),
totalLength(0),
songLength(0.0),
totalLength(0.0),
curProgress(0.0f),
totalFiles(0),
localeRequiresJapanese(false),
@ -8757,9 +8798,6 @@ FurnaceGUI::FurnaceGUI():
soloChan(-1),
orderEditMode(0),
orderCursor(-1),
loopOrder(-1),
loopRow(-1),
loopEnd(-1),
isClipping(0),
newSongCategory(0),
latchTarget(0),
@ -8830,6 +8868,7 @@ FurnaceGUI::FurnaceGUI():
csPlayerOpen(false),
cvOpen(false),
userPresetsOpen(false),
refPlayerOpen(false),
cvNotSerious(false),
shortIntro(false),
insListDir(false),
@ -9031,6 +9070,7 @@ FurnaceGUI::FurnaceGUI():
resampleTarget(32000),
resampleStrat(5),
amplifyVol(100.0),
amplifyOff(0.0),
sampleSelStart(-1),
sampleSelEnd(-1),
sampleInfo(true),
@ -9115,6 +9155,9 @@ FurnaceGUI::FurnaceGUI():
xyOscIntensity(2.0f),
xyOscThickness(2.0f),
tunerPlan(NULL),
fpCueInput(""),
fpCueInputFailed(false),
fpCueInputFailReason(""),
followLog(true),
#ifdef IS_MOBILE
pianoOctaves(7),
@ -9294,8 +9337,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

@ -573,6 +573,7 @@ enum FurnaceGUIWindows {
GUI_WINDOW_MEMORY,
GUI_WINDOW_CS_PLAYER,
GUI_WINDOW_USER_PRESETS,
GUI_WINDOW_REF_PLAYER,
GUI_WINDOW_SPOILER
};
@ -653,6 +654,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,
@ -779,6 +781,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,
@ -1696,7 +1699,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;
@ -1718,7 +1721,9 @@ class FurnaceGUI {
bool vgmExportDirectStream, displayInsTypeList, displayWaveSizeList;
bool portrait, injectBackUp, mobileMenuOpen, warnColorPushed;
bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu;
bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, notifyWaveChange, notifySampleChange;
bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, noteInputChord;
bool notifyWaveChange, notifySampleChange;
bool recalcTimestamps;
bool wantScrollListIns, wantScrollListWave, wantScrollListSample;
bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString;
bool displayPendingSamples, replacePendingSample;
@ -1731,7 +1736,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;
@ -1740,6 +1746,7 @@ class FurnaceGUI {
int drawHalt;
int macroPointSize;
int waveEditStyle;
int chordInputOffset;
int displayInsTypeListMakeInsSample;
int makeDrumkitOctave;
int mobileEditPage;
@ -1826,13 +1833,8 @@ 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
bool songHasSongEndCommand; // song has "Song end" command (FFxx)
int lengthOfOneFile; // length of one rendering pass. song length times num of loops + fadeout
int totalLength; // total length of render (lengthOfOneFile times num of files for per-channel export)
double songLength; // length of the song in seconds
double totalLength; // total length of render (songLength times num of files for per-channel export)
float curProgress;
int totalFiles;
@ -2385,7 +2387,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;
@ -2402,7 +2404,7 @@ class FurnaceGUI {
bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen;
bool pianoOpen, notesOpen, tunerOpen, spectrumOpen, 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;
@ -2631,7 +2633,7 @@ class FurnaceGUI {
int resizeSize, silenceSize;
double resampleTarget;
int resampleStrat;
float amplifyVol;
float amplifyVol, amplifyOff;
int sampleSelStart, sampleSelEnd;
bool sampleInfo, sampleCompatRate;
bool sampleDragActive, sampleDragMode, sampleDrag16, sampleZoomAuto;
@ -2776,6 +2778,11 @@ class FurnaceGUI {
float keyHit1[DIV_MAX_CHANS];
int lastIns[DIV_MAX_CHANS];
// file player temp variables
String fpCueInput;
bool fpCueInputFailed;
String fpCueInputFailReason;
// log window
bool followLog;
@ -3036,6 +3043,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);
@ -3118,7 +3126,7 @@ class FurnaceGUI {
void doDrag(bool copy=false);
void editOptions(bool topMenu);
DivSystem systemPicker(bool fullWidth);
void noteInput(int num, int key, int vol=-1);
void noteInput(int num, int key, int vol=-1, int chanOff=0);
void valueInput(int num, bool direct=false, int target=-1);
void orderInput(int num);

View file

@ -674,6 +674,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

@ -362,8 +362,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

@ -90,30 +90,11 @@ void FurnaceGUI::drawMobileOrderSel() {
// time
if (e->isPlaying() && settings.playbackTime) {
int totalTicks=e->getTotalTicks();
int totalSeconds=e->getTotalSeconds();
String info="";
TimeMicros totalTime=e->getCurTime();
String info=totalTime.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO);
if (totalSeconds==0x7fffffff) {
if (totalTime.seconds==0x7fffffff) {
info="";
} else {
if (totalSeconds>=86400) {
int totalDays=totalSeconds/86400;
int totalYears=totalDays/365;
totalDays%=365;
int totalMonths=totalDays/30;
totalDays%=30;
info+=fmt::sprintf("%dy",totalYears);
info+=fmt::sprintf("%dm",totalMonths);
info+=fmt::sprintf("%dd",totalDays);
}
if (totalSeconds>=3600) {
info+=fmt::sprintf("%.2d:",(totalSeconds/3600)%24);
}
info+=fmt::sprintf("%.2d:%.2d.%.2d",(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000);
}
ImVec2 textSize=ImGui::CalcTextSize(info.c_str());
@ -344,7 +325,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 +373,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 +380,6 @@ void FurnaceGUI::drawOrders() {
}
} else {
setOrder(i);
e->walkSong(loopOrder,loopRow,loopEnd);
if (orderEditMode!=0) {
orderCursor=j;
curNibble=false;
@ -441,7 +420,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 +427,6 @@ void FurnaceGUI::drawOrders() {
}
} else {
setOrder(i);
e->walkSong(loopOrder,loopRow,loopEnd);
if (orderEditMode!=0) {
orderCursor=j;
curNibble=false;

View file

@ -406,6 +406,16 @@ 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) {
TimeMicros rowTS=e->curSubSong->ts.getTimes(ord,i);
if (rowTS.seconds==-1) {
ImGui::Text("---");
} else {
String timeFormatted=rowTS.toString(2,TA_TIME_FORMAT_AUTO_MS_ZERO);
ImGui::TextUnformatted(timeFormatted.c_str());
}
}
}
void FurnaceGUI::drawPattern() {

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

@ -0,0 +1,243 @@
/**
* 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"
#include "misc/cpp/imgui_stdlib.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)) {
fp->stop();
fp->setPosSeconds(e->getFilePlayerCue());
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) {
fp->stop();
fp->setPos(0);
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
fp->setPosSeconds(e->getFilePlayerCue());
}
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
TimeMicros curPos=fp->getPosSeconds();
TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0);
if (rowTS.seconds==-1) {
showError(_("the first row of this order isn't going to play."));
} else {
e->setFilePlayerCue(curPos-rowTS);
}
}
if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) {
fp->setPos(0);
}
if (ImGui::BeginPopupContextItem("Edit Cue Position",ImGuiPopupFlags_MouseButtonRight)) {
ImGui::TextUnformatted(_("Set cue position at first order:"));
TimeMicros cueTime=e->getFilePlayerCue();
bool altered=false;
fpCueInput=cueTime.toString(-1,TA_TIME_FORMAT_AUTO);
pushWarningColor(false,fpCueInputFailed);
if (ImGui::InputText("##CuePos",&fpCueInput)) {
try {
cueTime=TimeMicros::fromString(fpCueInput);
altered=true;
fpCueInputFailed=false;
} catch (std::invalid_argument& e) {
fpCueInputFailed=true;
fpCueInputFailReason=e.what();
}
}
if (!ImGui::IsItemActive()) {
fpCueInputFailed=false;
}
if (ImGui::IsItemHovered() && fpCueInputFailed) {
ImGui::SetTooltip("%s",fpCueInputFailReason.c_str());
}
popWarningColor();
if (altered) {
e->setFilePlayerCue(cueTime);
}
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
TimeMicros curPos=fp->getPosSeconds();
TimeMicros rowTS=e->curSubSong->ts.getTimes(curOrder,0);
if (rowTS.seconds==-1) {
showError(_("the first row of this order isn't going to play."));
} else {
e->setFilePlayerCue(curPos-rowTS);
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)) {
TimeMicros rowTS;
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
rowTS=e->curSubSong->ts.getTimes(cursor.order,cursor.y);
} else {
rowTS=e->curSubSong->ts.getTimes(curOrder,0);
}
TimeMicros cueTime=e->getFilePlayerCue();
if (rowTS.seconds==-1) {
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
showError(_("the row that the pattern cursor is at isn't going to play. try moving the cursor."));
} else {
showError(_("the first row of this order isn't going to play. try another order."));
}
} else {
fp->setPosSeconds(rowTS+cueTime);
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

@ -1212,7 +1212,7 @@ void FurnaceGUI::drawSampleEdit() {
sameLineMaybe();
ImGui::Button(ICON_FA_VOLUME_UP "##SAmplify");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(_("Amplify"));
ImGui::SetTooltip(_("Amplify/Offset"));
}
if (openSampleAmplifyOpt) {
openSampleAmplifyOpt=false;
@ -1226,6 +1226,11 @@ void FurnaceGUI::drawSampleEdit() {
}
ImGui::SameLine();
ImGui::Text("(%.1fdB)",20.0*log10(amplifyVol/100.0f));
ImGui::Text(_("DC offset"));
if (ImGui::InputFloat("##Offset",&amplifyOff,-100.0,100.0,"%g%%")) {
if (amplifyOff<-100) amplifyOff=-100;
if (amplifyOff>100) amplifyOff=100;
}
if (ImGui::Button(_("Apply"))) {
sample->prepareUndo(true);
e->lockEngine([this,sample]() {
@ -1233,15 +1238,17 @@ void FurnaceGUI::drawSampleEdit() {
float vol=amplifyVol/100.0f;
if (sample->depth==DIV_SAMPLE_DEPTH_16BIT) {
float off=32767.0f*(amplifyOff/100.0f);
for (unsigned int i=start; i<end; i++) {
float val=sample->data16[i]*vol;
float val=off+sample->data16[i]*vol;
if (val<-32768) val=-32768;
if (val>32767) val=32767;
sample->data16[i]=val;
}
} else if (sample->depth==DIV_SAMPLE_DEPTH_8BIT) {
float off=127.0f*(amplifyOff/100.0f);
for (unsigned int i=start; i<end; i++) {
float val=sample->data8[i]*vol;
float val=off+sample->data8[i]*vol;
if (val<-128) val=-128;
if (val>127) val=127;
sample->data8[i]=val;

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

@ -1082,7 +1082,7 @@ void FurnaceGUI::drawTutorial() {
oneQuarter=(oneQuarter*e->curSubSong->virtualTempoN)/e->curSubSong->virtualTempoD;
oneQuarter/=e->curSubSong->hz;
oneQuarter/=4;
if (cv->playSongs && e->getTotalSeconds()>=oneQuarter) {
if (cv->playSongs && e->getCurTime().seconds>=oneQuarter) {
if (loadRandomDemoSong()) {
cv->loadInstruments();
e->changeSongP(0);