add experimental command stream player

for verification

after that I am going to write optimization code
This commit is contained in:
tildearrow 2023-03-26 18:48:16 -05:00
parent 24c39c7819
commit c4510e16e0
10 changed files with 490 additions and 10 deletions

View file

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

301
src/engine/cmdStream.cpp Normal file
View file

@ -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; i<e->getTotalChannelCount(); 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<chans; i++) {
if (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; i<e->getTotalChannelCount(); 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;
}

59
src/engine/cmdStream.h Normal file
View file

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

View file

@ -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 <atomic>
@ -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<int(const TAMidiMessage&)> 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),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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