diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 1d64ec500..6d0f09dee 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -2122,6 +2122,90 @@ SafeWriter* DivEngine::saveVGM() { return NULL; } +void _runExportThread(DivEngine* caller) { + caller->runExportThread(); +} + +bool DivEngine::isExporting() { + return exporting; +} + +#define EXPORT_BUFSIZE 2048 + +void DivEngine::runExportThread() { + SNDFILE* sf; + SF_INFO si; + si.samplerate=got.rate; + si.channels=2; + si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16; + + sf=sf_open(exportPath.c_str(),SFM_WRITE,&si); + if (sf==NULL) { + logE("could not open file for writing!\n"); + return; + } + + float* outBuf[3]; + outBuf[0]=new float[EXPORT_BUFSIZE]; + outBuf[1]=new float[EXPORT_BUFSIZE]; + outBuf[2]=new float[EXPORT_BUFSIZE*2]; + + // take control of audio output + deinitAudioBackend(); + playSub(false); + + while (playing) { + nextBuf(NULL,outBuf,0,2,EXPORT_BUFSIZE); + for (int i=0; iEXPORT_BUFSIZE) { + logE("error: total processed is bigger than export bufsize! %d>%d\n",totalProcessed,EXPORT_BUFSIZE); + } + if (sf_writef_float(sf,outBuf[2],totalProcessed)!=(int)totalProcessed) { + logE("error: failed to write entire buffer!\n"); + break; + } + } + + if (sf_close(sf)!=0) { + logE("could not close audio file!\n"); + } + exporting=false; + + if (initAudioBackend()) { + for (int i=0; isetRun(true)) { + logE("error while activating audio!\n"); + } + } +} + +bool DivEngine::saveAudio(const char* path, int loops, DivAudioExportModes mode) { + exportPath=path; + exporting=true; + stop(); + setOrder(0); + remainingLoops=loops; + exportThread=new std::thread(_runExportThread,this); + return true; +} + +void DivEngine::waitAudioFile() { + if (exportThread!=NULL) { + exportThread->join(); + } +} + +bool DivEngine::haltAudioFile() { + stop(); + return true; +} + #ifdef _WIN32 #define CONFIG_FILE "\\furnace.cfg" #else @@ -2577,6 +2661,7 @@ void DivEngine::stop() { freelance=false; playing=false; extValuePresent=false; + remainingLoops=-1; isBusy.unlock(); } diff --git a/src/engine/engine.h b/src/engine/engine.h index c676684ea..ff9f428ff 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -5,6 +5,7 @@ #include "safeWriter.h" #include "../audio/taAudio.h" #include "blip_buf.h" +#include #include #include #include @@ -23,6 +24,12 @@ enum DivAudioEngines { DIV_AUDIO_SDL=1 }; +enum DivAudioExportModes { + DIV_EXPORT_MODE_ONE=0, + DIV_EXPORT_MODE_MANY_SYS, + DIV_EXPORT_MODE_MANY_CHAN +}; + struct DivChannelState { std::vector delayed; int note, oldNote, pitch, portaSpeed, portaNote; @@ -110,6 +117,8 @@ class DivEngine { DivDispatchContainer disCont[32]; TAAudio* output; TAAudioDesc want, got; + String exportPath; + std::thread* exportThread; int chans; bool active; bool lowQuality; @@ -121,6 +130,7 @@ class DivEngine { bool extValuePresent; bool repeatPattern; bool metronome; + bool exporting; int ticks, curRow, curOrder, remainingLoops, nextSpeed, divider; int cycles, clockDrift; int changeOrd, changePos, totalSeconds, totalTicks, totalTicksR, totalCmds, lastCmds, cmdsPerSecond, globalPitch; @@ -129,6 +139,7 @@ class DivEngine { DivStatusView view; DivChannelState chan[DIV_MAX_CHANS]; DivAudioEngines audioEngine; + DivAudioExportModes exportMode; std::map conf; std::queue pendingNotes; bool isMuted[DIV_MAX_CHANS]; @@ -185,6 +196,7 @@ class DivEngine { public: DivSong song; + void runExportThread(); void nextBuf(float** in, float** out, int inChans, int outChans, unsigned int size); DivInstrument* getIns(int index); DivWavetable* getWave(int index); @@ -202,7 +214,7 @@ class DivEngine { // dump to VGM (TODO). SafeWriter* saveVGM(); // export to an audio file - bool saveAudio(const char* path); + bool saveAudio(const char* path, int loops, DivAudioExportModes mode); // wait for audio export to finish void waitAudioFile(); // stop audio file export @@ -338,6 +350,9 @@ class DivEngine { // is playing bool isPlaying(); + // is exporting + bool isExporting(); + // add instrument int addInstrument(int refChan=0); @@ -448,6 +463,7 @@ class DivEngine { DivEngine(): output(NULL), + exportThread(NULL), chans(0), active(false), lowQuality(false), @@ -459,6 +475,7 @@ class DivEngine { extValuePresent(false), repeatPattern(false), metronome(false), + exporting(false), ticks(0), curRow(0), curOrder(0), diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index b89e60b3f..47f1df382 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -962,7 +962,14 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi if (nextTick()) { if (remainingLoops>0) { remainingLoops--; - if (!remainingLoops) logI("end of song!\n"); + if (!remainingLoops) { + logI("end of song!\n"); + remainingLoops=-1; + playing=false; + freelance=false; + extValuePresent=false; + break; + } } } } else { @@ -993,7 +1000,7 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi playing=false; extValuePresent=false; } - totalProcessed=(1+runPos[0])*got.rate/disCont[0].dispatch->rate; + totalProcessed=size-(runLeftG>>MASTER_CLOCK_PREC); for (int i=0; iOpenModal("FileDialog","Save Sample","Wave file{.wav}",workingDir); break; + case GUI_FILE_EXPORT_AUDIO_ONE: + ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDir); + break; + case GUI_FILE_EXPORT_AUDIO_PER_SYS: + ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDir); + break; + case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: + ImGuiFileDialog::Instance()->OpenModal("FileDialog","Export Audio","Wave file{.wav}",workingDir); + break; + case GUI_FILE_EXPORT_VGM: case GUI_FILE_EXPORT_ROM: + showError("Coming soon!"); + break; } curFileDialog=type; } @@ -2949,6 +2961,11 @@ int FurnaceGUI::load(String path) { return 0; } +void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { + e->saveAudio(path.c_str(),exportLoops+1,mode); + displayExporting=true; +} + void FurnaceGUI::showWarning(String what, FurnaceGUIWarnings type) { warnString=what; warnAction=type; @@ -3118,6 +3135,22 @@ bool FurnaceGUI::loop() { openFileDialog(GUI_FILE_SAVE); } ImGui::Separator(); + if (ImGui::BeginMenu("export audio...")) { + if (ImGui::MenuItem("one file")) { + openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE); + } + if (ImGui::MenuItem("multiple files (one per system)")) { + openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_SYS); + } + if (ImGui::MenuItem("multiple files (one per channel)")) { + openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_CHANNEL); + } + if (ImGui::InputInt("Loops",&exportLoops,1,2)) { + if (exportLoops<0) exportLoops=0; + } + ImGui::EndMenu(); + } + ImGui::Separator(); if (ImGui::BeginMenu("add system...")) { sysAddOption(DIV_SYSTEM_GENESIS); sysAddOption(DIV_SYSTEM_GENESIS_EXT); @@ -3302,6 +3335,19 @@ bool FurnaceGUI::loop() { e->song.sample[curSample]->save(copyOfName.c_str()); } break; + case GUI_FILE_EXPORT_AUDIO_ONE: + exportAudio(copyOfName,DIV_EXPORT_MODE_ONE); + break; + case GUI_FILE_EXPORT_AUDIO_PER_SYS: + exportAudio(copyOfName,DIV_EXPORT_MODE_MANY_SYS); + break; + case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: + exportAudio(copyOfName,DIV_EXPORT_MODE_MANY_CHAN); + break; + case GUI_FILE_EXPORT_VGM: + case GUI_FILE_EXPORT_ROM: + showError("Coming soon!"); + break; } curFileDialog=GUI_FILE_OPEN; } @@ -3325,8 +3371,26 @@ bool FurnaceGUI::loop() { ImGui::OpenPopup("Error"); } + if (displayExporting) { + displayExporting=false; + ImGui::OpenPopup("Rendering..."); + } + if (aboutOpen) drawAbout(); + if (ImGui::BeginPopupModal("Rendering...",NULL,ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Please wait...\n"); + if (ImGui::Button("Abort")) { + if (e->haltAudioFile()) { + ImGui::CloseCurrentPopup(); + } + } + if (!e->isExporting()) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + if (ImGui::BeginPopupModal("Error",NULL,ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("%s",errorString.c_str()); if (ImGui::Button("OK")) { @@ -3544,6 +3608,7 @@ FurnaceGUI::FurnaceGUI(): edit(false), modified(false), displayError(false), + displayExporting(false), curFileDialog(GUI_FILE_OPEN), warnAction(GUI_WARN_OPEN), scrW(1280), @@ -3560,6 +3625,7 @@ FurnaceGUI::FurnaceGUI(): oldOrder(0), oldOrder1(0), editStep(1), + exportLoops(0), editControlsOpen(true), ordersOpen(true), insListOpen(true), diff --git a/src/gui/gui.h b/src/gui/gui.h index b33bb9da0..107fb2d6d 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -80,7 +80,12 @@ enum FurnaceGUIFileDialogs { GUI_FILE_OPEN, GUI_FILE_SAVE, GUI_FILE_SAMPLE_OPEN, - GUI_FILE_SAMPLE_SAVE + GUI_FILE_SAMPLE_SAVE, + GUI_FILE_EXPORT_AUDIO_ONE, + GUI_FILE_EXPORT_AUDIO_PER_SYS, + GUI_FILE_EXPORT_AUDIO_PER_CHANNEL, + GUI_FILE_EXPORT_VGM, + GUI_FILE_EXPORT_ROM }; enum FurnaceGUIWarnings { @@ -147,7 +152,7 @@ class FurnaceGUI { String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName; - bool quit, warnQuit, willCommit, edit, modified, displayError; + bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting; FurnaceGUIFileDialogs curFileDialog; FurnaceGUIWarnings warnAction; @@ -197,7 +202,7 @@ class FurnaceGUI { char finalLayoutPath[4096]; - int curIns, curWave, curSample, curOctave, oldRow, oldOrder, oldOrder1, editStep; + int curIns, curWave, curSample, curOctave, oldRow, oldOrder, oldOrder1, editStep, exportLoops; bool editControlsOpen, ordersOpen, insListOpen, songInfoOpen, patternOpen, insEditOpen; bool waveListOpen, waveEditOpen, sampleListOpen, sampleEditOpen, aboutOpen, settingsOpen; bool mixerOpen; @@ -291,6 +296,7 @@ class FurnaceGUI { void openFileDialog(FurnaceGUIFileDialogs type); int save(String path); int load(String path); + void exportAudio(String path, DivAudioExportModes mode); void showWarning(String what, FurnaceGUIWarnings type); void showError(String what);