diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e3caa0b8..78a68aa83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -469,6 +469,7 @@ src/engine/blip_buf.c src/engine/brrUtils.c src/engine/safeReader.cpp src/engine/safeWriter.cpp +src/engine/cmdStream.cpp src/engine/config.cpp src/engine/configEngine.cpp src/engine/dispatchContainer.cpp diff --git a/src/engine/cmdStream.cpp b/src/engine/cmdStream.cpp new file mode 100644 index 000000000..4ef2c5e2b --- /dev/null +++ b/src/engine/cmdStream.cpp @@ -0,0 +1,301 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "cmdStream.h" +#include "engine.h" +#include "../ta-log.h" + +void DivCSPlayer::cleanup() { + delete b; +} + +bool DivCSPlayer::tick() { + bool ticked=false; + for (int i=0; igetTotalChannelCount(); i++) { + bool sendVolume=false; + if (chan[i].readPos==0) continue; + + ticked=true; + + chan[i].waitTicks--; + while (chan[i].waitTicks<=0) { + stream.seek(chan[i].readPos,SEEK_SET); + unsigned char next=stream.readC(); + unsigned char command=0; + + if (next<0xb3) { // note + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,next-60)); + } else if (next>=0xd0 && next<=0xdf) { + command=fastCmds[next&15]; + } else if (next>=0xe0 && next<=0xef) { // preset delay + chan[i].waitTicks=fastDelays[next&15]; + } else switch (next) { + case 0xb4: // note on null + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); + break; + case 0xb5: // note off + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF,i)); + break; + case 0xb6: // note off env + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF_ENV,i)); + break; + case 0xb7: // env release + e->dispatchCmd(DivCommand(DIV_CMD_ENV_RELEASE,i)); + break; + case 0xb8: case 0xbe: case 0xc0: case 0xc2: + case 0xc3: case 0xc4: case 0xc5: case 0xc6: + case 0xc7: case 0xc8: case 0xc9: case 0xca: + command=next-0xb4; + break; + case 0xf7: + command=stream.readC(); + break; + case 0xf8: + logE("TODO: CALL"); + break; + case 0xf9: + logE("TODO: RET"); + break; + case 0xfa: + logE("TODO: JMP"); + break; + case 0xfb: + logE("TODO: RATE"); + break; + case 0xfc: + chan[i].waitTicks=(unsigned short)stream.readS(); + break; + case 0xfd: + chan[i].waitTicks=(unsigned char)stream.readC(); + break; + case 0xfe: + chan[i].waitTicks=1; + break; + case 0xff: + chan[i].readPos=0; + break; + } + + if (chan[i].readPos==0) break; + + if (command) { + int arg0=0; + int arg1=0; + switch (command) { + case DIV_CMD_INSTRUMENT: + case DIV_CMD_HINT_VIBRATO_RANGE: + case DIV_CMD_HINT_VIBRATO_SHAPE: + case DIV_CMD_HINT_PITCH: + case DIV_CMD_HINT_VOLUME: + arg0=(unsigned char)stream.readC(); + break; + case DIV_CMD_PANNING: + case DIV_CMD_HINT_VIBRATO: + case DIV_CMD_HINT_ARPEGGIO: + case DIV_CMD_HINT_PORTA: + arg0=(unsigned char)stream.readC(); + arg1=(unsigned char)stream.readC(); + break; + case DIV_CMD_PRE_PORTA: + arg0=(unsigned char)stream.readC(); + arg1=(arg0&0x40)?1:0; + arg0=(arg0&0x80)?1:0; + break; + case DIV_CMD_HINT_VOL_SLIDE: + arg0=(short)stream.readS(); + break; + case DIV_CMD_SAMPLE_MODE: + case DIV_CMD_SAMPLE_FREQ: + case DIV_CMD_SAMPLE_BANK: + case DIV_CMD_SAMPLE_POS: + case DIV_CMD_SAMPLE_DIR: + case DIV_CMD_FM_HARD_RESET: + case DIV_CMD_FM_LFO: + case DIV_CMD_FM_LFO_WAVE: + case DIV_CMD_FM_FB: + case DIV_CMD_FM_EXTCH: + case DIV_CMD_FM_AM_DEPTH: + case DIV_CMD_FM_PM_DEPTH: + case DIV_CMD_STD_NOISE_FREQ: + case DIV_CMD_STD_NOISE_MODE: + case DIV_CMD_WAVE: + case DIV_CMD_GB_SWEEP_TIME: + case DIV_CMD_GB_SWEEP_DIR: + case DIV_CMD_PCE_LFO_MODE: + case DIV_CMD_PCE_LFO_SPEED: + case DIV_CMD_NES_DMC: + case DIV_CMD_C64_CUTOFF: + case DIV_CMD_C64_RESONANCE: + case DIV_CMD_C64_FILTER_MODE: + case DIV_CMD_C64_RESET_TIME: + case DIV_CMD_C64_RESET_MASK: + case DIV_CMD_C64_FILTER_RESET: + case DIV_CMD_C64_DUTY_RESET: + case DIV_CMD_C64_EXTENDED: + case DIV_CMD_AY_ENVELOPE_SET: + case DIV_CMD_AY_ENVELOPE_LOW: + case DIV_CMD_AY_ENVELOPE_HIGH: + case DIV_CMD_AY_ENVELOPE_SLIDE: + case DIV_CMD_AY_NOISE_MASK_AND: + case DIV_CMD_AY_NOISE_MASK_OR: + case DIV_CMD_AY_AUTO_ENVELOPE: + case DIV_CMD_FDS_MOD_DEPTH: + case DIV_CMD_FDS_MOD_HIGH: + case DIV_CMD_FDS_MOD_LOW: + case DIV_CMD_FDS_MOD_POS: + case DIV_CMD_FDS_MOD_WAVE: + case DIV_CMD_SAA_ENVELOPE: + case DIV_CMD_AMIGA_FILTER: + case DIV_CMD_AMIGA_AM: + case DIV_CMD_AMIGA_PM: + case DIV_CMD_MACRO_OFF: + case DIV_CMD_MACRO_ON: + arg0=(unsigned char)stream.readC(); + break; + case DIV_CMD_FM_TL: + case DIV_CMD_FM_AM: + case DIV_CMD_FM_AR: + case DIV_CMD_FM_DR: + case DIV_CMD_FM_SL: + case DIV_CMD_FM_D2R: + case DIV_CMD_FM_RR: + case DIV_CMD_FM_DT: + case DIV_CMD_FM_DT2: + case DIV_CMD_FM_RS: + case DIV_CMD_FM_KSR: + case DIV_CMD_FM_VIB: + case DIV_CMD_FM_SUS: + case DIV_CMD_FM_WS: + case DIV_CMD_FM_SSG: + case DIV_CMD_FM_REV: + case DIV_CMD_FM_EG_SHIFT: + case DIV_CMD_FM_MULT: + case DIV_CMD_FM_FINE: + case DIV_CMD_AY_IO_WRITE: + case DIV_CMD_AY_AUTO_PWM: + case DIV_CMD_SURROUND_PANNING: + arg0=(unsigned char)stream.readC(); + arg1=(unsigned char)stream.readC(); + break; + case DIV_CMD_C64_FINE_DUTY: + case DIV_CMD_C64_FINE_CUTOFF: + case DIV_CMD_LYNX_LFSR_LOAD: + arg0=(unsigned short)stream.readS(); + break; + case DIV_CMD_FM_FIXFREQ: + arg0=(unsigned short)stream.readS(); + arg1=arg0&0x7ff; + arg0>>=12; + break; + case DIV_CMD_NES_SWEEP: + arg0=(unsigned char)stream.readC(); + arg1=arg0&0x77; + arg0=(arg0&8)?1:0; + break; + } + + switch (command) { + case DIV_CMD_HINT_VOLUME: + chan[i].volume=arg0<<8; + sendVolume=true; + break; + case DIV_CMD_HINT_VOL_SLIDE: + chan[i].volSpeed=arg0; + break; + default: // dispatch it + e->dispatchCmd(DivCommand((DivDispatchCmds)command,i,arg0,arg1)); + break; + } + } + + chan[i].readPos=stream.tell(); + } + + if (sendVolume || chan[i].volSpeed!=0) { + chan[i].volume+=chan[i].volSpeed; + if (chan[i].volume<0) { + chan[i].volume=0; + } + if (chan[i].volume>chan[i].volMax) { + chan[i].volume=chan[i].volMax; + } + + e->dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + } + } + + return ticked; +} + +bool DivCSPlayer::init() { + unsigned char magic[4]; + stream.seek(0,SEEK_SET); + stream.read(magic,4); + + if (memcmp(magic,"FCS",4)!=0) return false; + + unsigned int chans=stream.readI(); + + for (unsigned int i=0; i=DIV_MAX_CHANS) { + stream.readI(); + continue; + } + if ((int)i>=e->getTotalChannelCount()) { + stream.readI(); + continue; + } + chan[i].readPos=stream.readI(); + } + + stream.read(fastDelays,16); + stream.read(fastCmds,16); + + // initialize state + for (int i=0; igetTotalChannelCount(); i++) { + chan[i].volMax=(e->getDispatch(e->dispatchOfChan[i])->dispatch(DivCommand(DIV_CMD_GET_VOLMAX,e->dispatchChanOfChan[i]))<<8)|0xff; + chan[i].volume=chan[i].volMax; + } + + return true; +} + +// DivEngine + +bool DivEngine::playStream(unsigned char* f, size_t length) { + BUSY_BEGIN; + cmdStreamInt=new DivCSPlayer(this,f,length); + if (!cmdStreamInt->init()) { + logE("not a command stream!"); + lastError="not a command stream"; + delete[] f; + delete cmdStreamInt; + cmdStreamInt=NULL; + BUSY_END; + return false; + } + + if (!playing) { + reset(); + freelance=true; + playing=true; + } + BUSY_END; + return true; +} diff --git a/src/engine/cmdStream.h b/src/engine/cmdStream.h new file mode 100644 index 000000000..e12b0599e --- /dev/null +++ b/src/engine/cmdStream.h @@ -0,0 +1,59 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 _CMD_STREAM_H +#define _CMD_STREAM_H + +#include "defines.h" +#include "safeReader.h" + +class DivEngine; + +struct DivCSChannelState { + unsigned int readPos; + int waitTicks; + + int volume, volMax, volSpeed; + + DivCSChannelState(): + readPos(0), + waitTicks(0), + volume(0x7f00), + volMax(0), + volSpeed(0) {} +}; + +class DivCSPlayer { + DivEngine* e; + unsigned char* b; + SafeReader stream; + DivCSChannelState chan[DIV_MAX_CHANS]; + unsigned char fastDelays[16]; + unsigned char fastCmds[16]; + public: + void cleanup(); + bool tick(); + bool init(); + DivCSPlayer(DivEngine* en, unsigned char* buf, size_t len): + e(en), + b(buf), + stream(buf,len) {} +}; + +#endif diff --git a/src/engine/engine.h b/src/engine/engine.h index 7fc2a110c..0f5c40901 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -26,6 +26,7 @@ #include "export.h" #include "dataErrors.h" #include "safeWriter.h" +#include "cmdStream.h" #include "../audio/taAudio.h" #include "blip_buf.h" #include @@ -405,6 +406,8 @@ class DivEngine { static DivSystem sysFileMapFur[DIV_MAX_CHIP_DEFS]; static DivSystem sysFileMapDMF[DIV_MAX_CHIP_DEFS]; + DivCSPlayer* cmdStreamInt; + struct SamplePreview { double rate; int sample; @@ -449,7 +452,6 @@ class DivEngine { // MIDI stuff std::function midiCallback=[](const TAMidiMessage&) -> int {return -2;}; - int dispatchCmd(DivCommand c); void processRow(int i, bool afterDelay); void nextOrder(); void nextRow(); @@ -537,6 +539,8 @@ class DivEngine { void createNewFromDefaults(); // load a file. bool load(unsigned char* f, size_t length); + // play a binary command stream. + bool playStream(unsigned char* f, size_t length); // save as .dmf. SafeWriter* saveDMF(unsigned char version); // save as .fur. @@ -567,6 +571,9 @@ class DivEngine { // notify wavetable change void notifyWaveChange(int wave); + // dispatch a command + int dispatchCmd(DivCommand c); + // get system IDs static DivSystem systemFromFileFur(unsigned char val); static unsigned char systemToFileFur(DivSystem val); @@ -1152,6 +1159,7 @@ class DivEngine { audioEngine(DIV_AUDIO_NULL), exportMode(DIV_EXPORT_MODE_ONE), exportFadeOut(0.0), + cmdStreamInt(NULL), midiBaseChan(0), midiPoly(true), midiAgeCounter(0), diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index c58763b40..c77afaee5 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -1385,6 +1385,15 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { } } + if (subticks==tickMult && cmdStreamInt) { + if (!cmdStreamInt->tick()) { + cmdStreamInt->cleanup(); + delete cmdStreamInt; + cmdStreamInt=NULL; + } + } + + firstTick=false; if (shallStop) { diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 09610bdf8..ae1a1dfce 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -58,6 +58,8 @@ void FurnaceGUI::drawDebug() { ImGui::SameLine(); if (ImGui::Button("Pattern Advance")) e->haltWhen(DIV_HALT_PATTERN); + if (ImGui::Button("Play Command Stream")) openFileDialog(GUI_FILE_CMDSTREAM_OPEN); + if (ImGui::Button("Panic")) e->syncReset(); ImGui::SameLine(); if (ImGui::Button("Abort")) { diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index 8de859afd..98babf574 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -526,7 +526,21 @@ void FurnaceGUI::drawMobileControls() { ImGui::Separator(); - drawSongInfo(true); + if (ImGui::BeginTabBar("MobileSong")) { + if (ImGui::BeginTabItem("Song Info")) { + drawSongInfo(true); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Subsongs")) { + drawSubSongs(true); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Speed")) { + drawSpeed(true); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } break; } case GUI_SCENE_CHANNELS: diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 9258c4d97..7c23114ad 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1849,6 +1849,17 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { dpiScale ); break; + case GUI_FILE_CMDSTREAM_OPEN: + if (!dirExists(workingDirROM)) workingDirROM=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Play Command Stream", + {"command stream", "*.bin", + "all files", "*"}, + "command stream{.bin},.*", + workingDirROM, + dpiScale + ); + break; case GUI_FILE_TEST_OPEN: if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); hasOpened=fileDialog->openLoad( @@ -2103,6 +2114,64 @@ void FurnaceGUI::pushRecentFile(String path) { } } +int FurnaceGUI::loadStream(String path) { + if (!path.empty()) { + logI("loading stream..."); + FILE* f=ps_fopen(path.c_str(),"rb"); + if (f==NULL) { + perror("error"); + lastError=strerror(errno); + return 1; + } + if (fseek(f,0,SEEK_END)<0) { + perror("size error"); + lastError=fmt::sprintf("on seek: %s",strerror(errno)); + fclose(f); + return 1; + } + ssize_t len=ftell(f); + if (len==(SIZE_MAX>>1)) { + perror("could not get file length"); + lastError=fmt::sprintf("on pre tell: %s",strerror(errno)); + fclose(f); + return 1; + } + if (len<1) { + if (len==0) { + logE("that file is empty!"); + lastError="file is empty"; + } else { + perror("tell error"); + lastError=fmt::sprintf("on tell: %s",strerror(errno)); + } + fclose(f); + return 1; + } + if (fseek(f,0,SEEK_SET)<0) { + perror("size error"); + lastError=fmt::sprintf("on get size: %s",strerror(errno)); + fclose(f); + return 1; + } + unsigned char* file=new unsigned char[len]; + if (fread(file,1,(size_t)len,f)!=(size_t)len) { + perror("read error"); + lastError=fmt::sprintf("on read: %s",strerror(errno)); + fclose(f); + delete[] file; + return 1; + } + fclose(f); + if (!e->playStream(file,(size_t)len)) { + lastError=e->getLastError(); + logE("could not open file!"); + return 1; + } + } + return 0; +} + + void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->saveAudio(path.c_str(),exportLoops+1,mode,exportFadeOut); displayExporting=true; @@ -4241,6 +4310,9 @@ bool FurnaceGUI::loop() { case GUI_FILE_MU5_ROM_OPEN: workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; break; + case GUI_FILE_CMDSTREAM_OPEN: + workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; case GUI_FILE_TEST_OPEN: case GUI_FILE_TEST_OPEN_MULTI: case GUI_FILE_TEST_SAVE: @@ -4706,6 +4778,11 @@ bool FurnaceGUI::loop() { case GUI_FILE_MU5_ROM_OPEN: settings.mu5Path=copyOfName; break; + case GUI_FILE_CMDSTREAM_OPEN: + if (loadStream(copyOfName)>0) { + showError(fmt::sprintf("Error while loading file! (%s)",lastError)); + } + break; case GUI_FILE_TEST_OPEN: showWarning(fmt::sprintf("You opened: %s",copyOfName),GUI_WARN_GENERIC); break; diff --git a/src/gui/gui.h b/src/gui/gui.h index cb2e047b4..7dddb53e7 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -383,6 +383,7 @@ enum FurnaceGUIFileDialogs { GUI_FILE_YRW801_ROM_OPEN, GUI_FILE_TG100_ROM_OPEN, GUI_FILE_MU5_ROM_OPEN, + GUI_FILE_CMDSTREAM_OPEN, GUI_FILE_TEST_OPEN, GUI_FILE_TEST_OPEN_MULTI, @@ -1982,7 +1983,7 @@ class FurnaceGUI { void drawNewSong(); void drawLog(); void drawEffectList(); - void drawSubSongs(); + void drawSubSongs(bool asChild=false); void drawFindReplace(); void drawSpoiler(); void drawClock(); @@ -2074,6 +2075,7 @@ class FurnaceGUI { void openFileDialog(FurnaceGUIFileDialogs type); int save(String path, int dmfVersion); int load(String path); + int loadStream(String path); void pushRecentFile(String path); void exportAudio(String path, DivAudioExportModes mode); diff --git a/src/gui/subSongs.cpp b/src/gui/subSongs.cpp index b6e49e5b2..c0056b723 100644 --- a/src/gui/subSongs.cpp +++ b/src/gui/subSongs.cpp @@ -4,15 +4,18 @@ #include "misc/cpp/imgui_stdlib.h" #include "intConst.h" -void FurnaceGUI::drawSubSongs() { +void FurnaceGUI::drawSubSongs(bool asChild) { if (nextWindow==GUI_WINDOW_SUBSONGS) { subSongsOpen=true; ImGui::SetNextWindowFocus(); nextWindow=GUI_WINDOW_NOTHING; } - if (!subSongsOpen) return; - ImGui::SetNextWindowSizeConstraints(ImVec2(64.0f*dpiScale,32.0f*dpiScale),ImVec2(canvasW,canvasH)); - if (ImGui::Begin("Subsongs",&subSongsOpen,globalWinFlags)) { + if (!subSongsOpen && !asChild) return; + if (!asChild) { + ImGui::SetNextWindowSizeConstraints(ImVec2(64.0f*dpiScale,32.0f*dpiScale),ImVec2(canvasW,canvasH)); + } + bool began=asChild?ImGui::BeginChild("Subsongs"):ImGui::Begin("Subsongs",&subSongsOpen,globalWinFlags); + if (began) { char id[1024]; ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::GetFrameHeightWithSpacing()*2.0f-ImGui::GetStyle().ItemSpacing.x); if (e->curSubSong->name.empty()) { @@ -107,12 +110,16 @@ void FurnaceGUI::drawSubSongs() { MARK_MODIFIED; } - if (ImGui::GetContentRegionAvail().y>(10.0f*dpiScale)) { + if (!asChild && ImGui::GetContentRegionAvail().y>(10.0f*dpiScale)) { if (ImGui::InputTextMultiline("##SubSongNotes",&e->curSubSong->notes,ImGui::GetContentRegionAvail(),ImGuiInputTextFlags_UndoRedo)) { MARK_MODIFIED; } } } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SUBSONGS; - ImGui::End(); + if (!asChild && ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SUBSONGS; + if (asChild) { + ImGui::EndChild(); + } else { + ImGui::End(); + } }