diff --git a/CMakeLists.txt b/CMakeLists.txt index f64e92b0b..eff3bd2f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -791,6 +791,7 @@ src/engine/export.cpp src/engine/exportDef.cpp src/engine/fileOpsIns.cpp src/engine/fileOpsSample.cpp +src/engine/filePlayer.cpp src/engine/filter.cpp src/engine/instrument.cpp src/engine/macroInt.cpp @@ -994,6 +995,7 @@ src/gui/patManager.cpp src/gui/pattern.cpp src/gui/piano.cpp src/gui/presets.cpp +src/gui/refPlayer.cpp src/gui/regView.cpp src/gui/sampleEdit.cpp src/gui/scaling.cpp diff --git a/papers/format.md b/papers/format.md index 196f8078c..c92c7da3f 100644 --- a/papers/format.md +++ b/papers/format.md @@ -432,6 +432,7 @@ reserved input portsets: reserved output portsets: - `000` through `01F`: chip outputs +- `FFC`: reference file/music player (>=238) - `FFD`: wave/sample preview - `FFE`: metronome - `FFF`: "null" portset diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 9503106f1..190d9918d 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1648,6 +1648,16 @@ void DivEngine::getCommandStream(std::vector& where) { BUSY_END; } +DivFilePlayer* DivEngine::getFilePlayer() { + if (curFilePlayer==NULL) { + BUSY_BEGIN_SOFT; + curFilePlayer=new DivFilePlayer; + curFilePlayer->setOutputRate(got.rate); + BUSY_END; + } + return curFilePlayer; +} + void DivEngine::playSub(bool preserveDrift, int goalRow) { logV("playSub() called"); std::chrono::high_resolution_clock::time_point timeStart=std::chrono::high_resolution_clock::now(); @@ -3419,6 +3429,12 @@ void DivEngine::autoPatchbay() { } } + // file player + song.patchbay.reserve(DIV_MAX_OUTPUTS); + for (unsigned int j=0; j @@ -54,8 +55,8 @@ class DivWorkPool; #define DIV_UNSTABLE -#define DIV_VERSION "dev237" -#define DIV_ENGINE_VERSION 237 +#define DIV_VERSION "dev238" +#define DIV_ENGINE_VERSION 238 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 @@ -587,6 +588,7 @@ class DivEngine { int samp_temp, samp_prevSample; short* samp_bbIn; short* samp_bbOut; + unsigned char* metroTick; size_t metroTickLen; float* metroBuf; @@ -596,6 +598,10 @@ class DivEngine { float metroVol; float previewVol; + float* filePlayerBuf[DIV_MAX_OUTPUTS]; + size_t filePlayerBufLen; + DivFilePlayer* curFilePlayer; + size_t totalProcessed; unsigned int renderPoolThreads; @@ -738,12 +744,17 @@ class DivEngine { void createNewFromDefaults(); // load a file. bool load(unsigned char* f, size_t length, const char* nameHint=NULL); + // play a binary command stream. bool playStream(unsigned char* f, size_t length); // get the playing stream. DivCSPlayer* getStreamPlayer(); // destroy command stream player. bool killStream(); + + // get the audio file player. + DivFilePlayer* getFilePlayer(); + // save as .dmf. SafeWriter* saveDMF(unsigned char version); // save as .fur. @@ -1552,6 +1563,8 @@ class DivEngine { metroAmp(0.0f), metroVol(1.0f), previewVol(1.0f), + filePlayerBufLen(0), + curFilePlayer(NULL), totalProcessed(0), renderPoolThreads(0), renderPool(NULL), @@ -1584,6 +1597,7 @@ class DivEngine { memset(oscBuf,0,DIV_MAX_OUTPUTS*(sizeof(float*))); memset(exportChannelMask,1,DIV_MAX_CHANS*sizeof(bool)); memset(chipPeak,0,DIV_MAX_CHIPS*DIV_MAX_OUTPUTS*sizeof(float)); + memset(filePlayerBuf,0,DIV_MAX_OUTPUTS*sizeof(float)); for (int i=0; i #define DIV_FPCACHE_BLOCK_SHIFT 16 #define DIV_FPCACHE_BLOCK_SIZE (1U<>DIV_FPCACHE_BLOCK_SHIFT)!=firstBlock) { + // we need to seek + logV("- seeking"); + // we seek to a previous position in order to compensate for possible decoding differences when seeking + // (usually in lossy codecs) + sf_count_t seekWhere=firstBlock<>DIV_FPCACHE_BLOCK_SHIFT; + if (blockIndex!=lastBlock) { + fillBlocksNear(playPos); + lastBlock=blockIndex; + } + if (blockIndex>=numBlocks) { + // stop here + for (int j=0; j=si.channels) { + buf[j][i]=0.0f; + } else { + buf[j][i]=block[posInBlock++]*volume; + } + } + } + + // advance + rateAccum+=si.samplerate; + while (rateAccum>=outRate) { + rateAccum-=outRate; + playPos++; + if (playPos>=(size_t)si.frames) { + playPos=0; + } + } + } else { + for (int j=0; j>DIV_FPCACHE_BLOCK_SHIFT]!=NULL); +} + +bool DivFilePlayer::isLoaded() { + return (sf!=NULL); +} + +bool DivFilePlayer::isPlaying() { + return playing; +} + +void DivFilePlayer::play(unsigned int offset) { + logV("DivFilePlayer: playing"); playing=true; } -void DivFilePlayer::stop(unsigned int offset=0) { +void DivFilePlayer::stop(unsigned int offset) { + logV("DivFilePlayer: stopping"); playing=false; } @@ -116,24 +215,36 @@ bool DivFilePlayer::closeFile() { delete[] discardBuf; discardBuf=NULL; + + return true; } bool DivFilePlayer::loadFile(const char* path) { + if (sf!=NULL) closeFile(); + + logD("DivFilePlayer: opening file..."); sf=sfw.doOpen(path,SFM_READ,&si); if (sf==NULL) { + logE("could not open file!"); return false; } + logV("- samples: %d",si.frames); + logV("- channels: %d",si.channels); + logV("- rate: %d",si.samplerate); + numBlocks=(DIV_FPCACHE_BLOCK_MASK+si.frames)>>DIV_FPCACHE_BLOCK_SHIFT; blocks=new float*[numBlocks]; memset(blocks,0,numBlocks*sizeof(void*)); playPos=0; + lastBlock=SIZE_MAX; rateAccum=0; fileError=false; // read the entire file if not seekable if (!si.seekable) { + logV("file not seekable - reading..."); for (size_t i=0; iwait(); } + // process file player + // resize file player audio buffer if necessary + if (filePlayerBufLenmix(filePlayerBuf,outChans,size); + } + // process metronome // resize the metronome's audio buffer if necessary if (metroBufLenopenLoad( + _("Open Audio File"), + audioLoadFormats, + workingDirMusic, + dpiScale, + NULL, + false + ); + break; case GUI_FILE_TEST_OPEN: if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); hasOpened=fileDialog->openLoad( @@ -3841,6 +3852,7 @@ bool FurnaceGUI::loop() { DECLARE_METRIC(log) DECLARE_METRIC(effectList) DECLARE_METRIC(userPresets) + DECLARE_METRIC(refPlayer) DECLARE_METRIC(popup) #ifdef IS_MOBILE @@ -4453,6 +4465,7 @@ bool FurnaceGUI::loop() { IMPORT_CLOSE(memoryOpen); IMPORT_CLOSE(csPlayerOpen); IMPORT_CLOSE(userPresetsOpen); + IMPORT_CLOSE(refPlayerOpen); } else if (pendingLayoutImportStep==1) { // let the UI settle } else if (pendingLayoutImportStep==2) { @@ -4821,6 +4834,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem(_("effect list"),BIND_FOR(GUI_ACTION_WINDOW_EFFECT_LIST),effectListOpen)) effectListOpen=!effectListOpen; if (ImGui::MenuItem(_("play/edit controls"),BIND_FOR(GUI_ACTION_WINDOW_EDIT_CONTROLS),editControlsOpen)) editControlsOpen=!editControlsOpen; if (ImGui::MenuItem(_("piano/input pad"),BIND_FOR(GUI_ACTION_WINDOW_PIANO),pianoOpen)) pianoOpen=!pianoOpen; + if (ImGui::MenuItem(_("reference music player"),BIND_FOR(GUI_ACTION_WINDOW_REF_PLAYER),refPlayerOpen)) refPlayerOpen=!refPlayerOpen; if (spoilerOpen) if (ImGui::MenuItem(_("spoiler"),NULL,spoilerOpen)) spoilerOpen=!spoilerOpen; ImGui::EndMenu(); @@ -5043,6 +5057,7 @@ bool FurnaceGUI::loop() { MEASURE(memory,drawMemory()); MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); + MEASURE(refPlayer,drawRefPlayer()); MEASURE(patManager,drawPatManager()); } else { @@ -5088,6 +5103,7 @@ bool FurnaceGUI::loop() { MEASURE(log,drawLog()); MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); + MEASURE(refPlayer,drawRefPlayer()); } @@ -5239,6 +5255,9 @@ bool FurnaceGUI::loop() { case GUI_FILE_CMDSTREAM_OPEN: workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; break; + case GUI_FILE_MUSIC_OPEN: + workingDirMusic=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; case GUI_FILE_TEST_OPEN: case GUI_FILE_TEST_OPEN_MULTI: case GUI_FILE_TEST_SAVE: @@ -5877,6 +5896,11 @@ bool FurnaceGUI::loop() { showError(fmt::sprintf(_("Error while loading file! (%s)"),lastError)); } break; + case GUI_FILE_MUSIC_OPEN: + if (!e->getFilePlayer()->loadFile(copyOfName.c_str())) { + showError(fmt::sprintf(_("Error while loading file!"))); + } + break; case GUI_FILE_TEST_OPEN: showWarning(fmt::sprintf(_("You opened: %s"),copyOfName),GUI_WARN_GENERIC); break; @@ -8170,11 +8194,13 @@ void FurnaceGUI::syncState() { workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir); workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir); workingDirROMExport=e->getConfString("lastDirROMExport",workingDir); + workingDirROM=e->getConfString("lastDirROM",workingDir); workingDirFont=e->getConfString("lastDirFont",workingDir); workingDirColors=e->getConfString("lastDirColors",workingDir); workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir); workingDirLayout=e->getConfString("lastDirLayout",workingDir); workingDirConfig=e->getConfString("lastDirConfig",workingDir); + workingDirMusic=e->getConfString("lastDirMusic",workingDir); workingDirTest=e->getConfString("lastDirTest",workingDir); editControlsOpen=e->getConfBool("editControlsOpen",true); @@ -8216,6 +8242,7 @@ void FurnaceGUI::syncState() { findOpen=e->getConfBool("findOpen",false); spoilerOpen=e->getConfBool("spoilerOpen",false); userPresetsOpen=e->getConfBool("userPresetsOpen",false); + refPlayerOpen=e->getConfBool("refPlayerOpen",false); insListDir=e->getConfBool("insListDir",false); waveListDir=e->getConfBool("waveListDir",false); @@ -8333,11 +8360,13 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("lastDirAudioExport",workingDirAudioExport); conf.set("lastDirVGMExport",workingDirVGMExport); conf.set("lastDirROMExport",workingDirROMExport); + conf.set("lastDirROM",workingDirROM); conf.set("lastDirFont",workingDirFont); conf.set("lastDirColors",workingDirColors); conf.set("lastDirKeybinds",workingDirKeybinds); conf.set("lastDirLayout",workingDirLayout); conf.set("lastDirConfig",workingDirConfig); + conf.set("lastDirMusic",workingDirMusic); conf.set("lastDirTest",workingDirTest); // commit last open windows @@ -8376,6 +8405,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("findOpen",findOpen); conf.set("spoilerOpen",spoilerOpen); conf.set("userPresetsOpen",userPresetsOpen); + conf.set("refPlayerOpen",refPlayerOpen); // commit dir state conf.set("insListDir",insListDir); @@ -8779,6 +8809,7 @@ FurnaceGUI::FurnaceGUI(): csPlayerOpen(false), cvOpen(false), userPresetsOpen(false), + refPlayerOpen(false), cvNotSerious(false), shortIntro(false), insListDir(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 6aaaf741e..1d96a7047 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -567,6 +567,7 @@ enum FurnaceGUIWindows { GUI_WINDOW_MEMORY, GUI_WINDOW_CS_PLAYER, GUI_WINDOW_USER_PRESETS, + GUI_WINDOW_REF_PLAYER, GUI_WINDOW_SPOILER }; @@ -647,6 +648,7 @@ enum FurnaceGUIFileDialogs { GUI_FILE_TG100_ROM_OPEN, GUI_FILE_MU5_ROM_OPEN, GUI_FILE_CMDSTREAM_OPEN, + GUI_FILE_MUSIC_OPEN, GUI_FILE_TEST_OPEN, GUI_FILE_TEST_OPEN_MULTI, @@ -771,6 +773,7 @@ enum FurnaceGUIActions { GUI_ACTION_WINDOW_MEMORY, GUI_ACTION_WINDOW_CS_PLAYER, GUI_ACTION_WINDOW_USER_PRESETS, + GUI_ACTION_WINDOW_REF_PLAYER, GUI_ACTION_COLLAPSE_WINDOW, GUI_ACTION_CLOSE_WINDOW, @@ -1688,7 +1691,7 @@ class FurnaceGUI { String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport; String workingDirVGMExport, workingDirROMExport; String workingDirFont, workingDirColors, workingDirKeybinds; - String workingDirLayout, workingDirROM, workingDirTest; + String workingDirLayout, workingDirROM, workingDirMusic, workingDirTest; String workingDirConfig; String mmlString[32]; String mmlStringW, grooveString, grooveListString, mmlStringModTable; @@ -2392,7 +2395,7 @@ class FurnaceGUI { bool mixerOpen, debugOpen, inspectorOpen, oscOpen, volMeterOpen, statsOpen, compatFlagsOpen; bool pianoOpen, notesOpen, channelsOpen, regViewOpen, logOpen, effectListOpen, chanOscOpen; bool subSongsOpen, findOpen, spoilerOpen, patManagerOpen, sysManagerOpen, clockOpen, speedOpen; - bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen; + bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen, refPlayerOpen; bool cvNotSerious; @@ -2992,6 +2995,7 @@ class FurnaceGUI { void drawTutorial(); void drawXYOsc(); void drawUserPresets(); + void drawRefPlayer(); float drawSystemChannelInfo(const DivSysDef* whichDef, int keyHitOffset=-1, float width=-1.0f); void drawSystemChannelInfoText(const DivSysDef* whichDef); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 8d1af66dc..f34292887 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -656,6 +656,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WINDOW_MEMORY", _N("Memory Composition"), 0), D("WINDOW_CS_PLAYER", _N("Command Stream Player"), 0), D("WINDOW_USER_PRESETS", _N("User Presets"), 0), + D("WINDOW_REF_PLAYER", _N("Reference Music Player"), 0), D("COLLAPSE_WINDOW", _N("Collapse/expand current window"), 0), D("CLOSE_WINDOW", _N("Close current window"), FURKMOD_SHIFT|SDLK_ESCAPE), diff --git a/src/gui/mixer.cpp b/src/gui/mixer.cpp index 4cb561723..cfc33a33a 100644 --- a/src/gui/mixer.cpp +++ b/src/gui/mixer.cpp @@ -333,8 +333,21 @@ void FurnaceGUI::drawMixer() { } } - // metronome/sample preview + // file player/metronome/sample preview if (displayInternalPorts) { + if (portSet(_("Music Player"),0xffc,0,16,0,16,selectedSubPort,portPos)) { + selectedPortSet=0xffc; + if (selectedSubPort>=0) { + portDragActive=true; + ImGui::InhibitInertialScroll(); + auto subPortI=portPos.find((selectedPortSet<<4)|selectedSubPort); + if (subPortI!=portPos.cend()) { + subPortPos=subPortI->second; + } else { + portDragActive=false; + } + } + } if (portSet(_("Sample Preview"),0xffd,0,1,0,1,selectedSubPort,portPos)) { selectedPortSet=0xffd; if (selectedSubPort>=0) { diff --git a/src/gui/refPlayer.cpp b/src/gui/refPlayer.cpp new file mode 100644 index 000000000..3ef5a65a0 --- /dev/null +++ b/src/gui/refPlayer.cpp @@ -0,0 +1,82 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "gui.h" +#include +#include "imgui.h" +#include "IconsFontAwesome4.h" + +void FurnaceGUI::drawRefPlayer() { + if (nextWindow==GUI_WINDOW_REF_PLAYER) { + refPlayerOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!refPlayerOpen) return; + if (ImGui::Begin("Music Player",&refPlayerOpen,globalWinFlags,_("Music Player"))) { + DivFilePlayer* fp=e->getFilePlayer(); + + size_t playPos=fp->getPos(); + size_t minPos=0; + size_t maxPos=fp->getFileInfo().frames; + int fileRate=fp->getFileInfo().samplerate; + if (fileRate<1) fileRate=1; + int posHours=(playPos/fileRate)/3600; + int posMinutes=((playPos/fileRate)/60)%60; + int posSeconds=(playPos/fileRate)%60; + int posMillis=(1000*(playPos%fileRate))/fileRate; + ImGui::Text("%d:%02d:%02d.%03d",posHours,posMinutes,posSeconds,posMillis); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::SliderScalar("##Position",ImGuiDataType_U64,&playPos,&minPos,&maxPos,"")) { + fp->setPos(playPos); + } + + if (ImGui::Button("Open")) { + openFileDialog(GUI_FILE_MUSIC_OPEN); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_FAST_BACKWARD)) { + fp->stop(); + fp->setPos(0); + } + ImGui::SameLine(); + if (fp->isPlaying()) { + pushToggleColors(true); + if (ImGui::Button(ICON_FA_PAUSE "##Pause")) { + fp->stop(); + } + popToggleColors(); + } else { + if (ImGui::Button(ICON_FA_PLAY "##Play")) { + fp->play(); + } + } + ImGui::SameLine(); + + float vol=fp->getVolume(); + if (ImGui::SliderFloat("Volume",&vol,0.0f,1.0f)) { + if (vol<0.0f) vol=0.0f; + if (vol>1.0f) vol=1.0f; + fp->setVolume(vol); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_REF_PLAYER; + ImGui::End(); +}