improve audio export options - UNTESTED/UNFINISHED

This commit is contained in:
tildearrow 2024-05-10 20:01:12 -05:00
parent e5026e43aa
commit 008fe4b6b8
8 changed files with 112 additions and 60 deletions

View file

@ -1,5 +1,6 @@
# to-do for 0.6.4 # to-do for 0.6.4
- revamp audio export dialog
- fix possible issues when moving selection - fix possible issues when moving selection
- fix Metal intro crash - fix Metal intro crash
@ -7,3 +8,5 @@
- finish auto-clone - finish auto-clone
- new pattern renderer - performance improvements - new pattern renderer - performance improvements
- new info header
- unlimited channels and chips

View file

@ -98,6 +98,28 @@ enum DivMIDIModes {
DIV_MIDI_MODE_LIGHT_SHOW DIV_MIDI_MODE_LIGHT_SHOW
}; };
struct DivAudioExportOptions {
DivAudioExportModes mode;
int sampleRate;
int chans;
int loops;
double fadeOut;
int orderBegin, orderEnd;
bool channelMask[DIV_MAX_CHANS];
DivAudioExportOptions():
mode(DIV_EXPORT_MODE_ONE),
sampleRate(44100),
chans(2),
loops(0),
fadeOut(0.0),
orderBegin(-1),
orderEnd(-1) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
channelMask[i]=true;
}
}
};
struct DivChannelState { struct DivChannelState {
std::vector<DivDelayedCommand> delayed; std::vector<DivDelayedCommand> delayed;
int note, oldNote, lastIns, pitch, portaSpeed, portaNote; int note, oldNote, lastIns, pitch, portaSpeed, portaNote;
@ -457,6 +479,7 @@ class DivEngine {
DivAudioEngines audioEngine; DivAudioEngines audioEngine;
DivAudioExportModes exportMode; DivAudioExportModes exportMode;
double exportFadeOut; double exportFadeOut;
int exportOutputs;
DivConfig conf; DivConfig conf;
FixedQueue<DivNoteEvent,8192> pendingNotes; FixedQueue<DivNoteEvent,8192> pendingNotes;
// bitfield // bitfield
@ -670,7 +693,7 @@ class DivEngine {
// export to text // export to text
SafeWriter* saveText(bool separatePatterns=true); SafeWriter* saveText(bool separatePatterns=true);
// export to an audio file // export to an audio file
bool saveAudio(const char* path, int loops, DivAudioExportModes mode, double fadeOutTime=0.0); bool saveAudio(const char* path, DivAudioExportOptions options);
// wait for audio export to finish // wait for audio export to finish
void waitAudioFile(); void waitAudioFile();
// stop audio file export // stop audio file export
@ -1360,6 +1383,7 @@ class DivEngine {
audioEngine(DIV_AUDIO_NULL), audioEngine(DIV_AUDIO_NULL),
exportMode(DIV_EXPORT_MODE_ONE), exportMode(DIV_EXPORT_MODE_ONE),
exportFadeOut(0.0), exportFadeOut(0.0),
exportOutputs(2),
cmdStreamInt(NULL), cmdStreamInt(NULL),
midiBaseChan(0), midiBaseChan(0),
midiPoly(true), midiPoly(true),

View file

@ -45,7 +45,7 @@ void DivEngine::runExportThread() {
SF_INFO si; SF_INFO si;
SFWrapper sfWrap; SFWrapper sfWrap;
si.samplerate=got.rate; si.samplerate=got.rate;
si.channels=2; si.channels=exportOutputs;
si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16; si.format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
sf=sfWrap.doOpen(exportPath.c_str(),SFM_WRITE,&si); sf=sfWrap.doOpen(exportPath.c_str(),SFM_WRITE,&si);
@ -55,10 +55,12 @@ void DivEngine::runExportThread() {
return; return;
} }
float* outBuf[3]; float* outBuf[DIV_MAX_OUTPUTS];
outBuf[0]=new float[EXPORT_BUFSIZE]; float* outBufFinal;
outBuf[1]=new float[EXPORT_BUFSIZE]; for (int i=0; i<exportOutputs; i++) {
outBuf[2]=new float[EXPORT_BUFSIZE*2]; outBuf[i]=new float[EXPORT_BUFSIZE];
}
outBufFinal=new float[EXPORT_BUFSIZE*exportOutputs];
// take control of audio output // take control of audio output
deinitAudioBackend(); deinitAudioBackend();
@ -68,24 +70,27 @@ void DivEngine::runExportThread() {
while (playing) { while (playing) {
size_t total=0; size_t total=0;
nextBuf(NULL,outBuf,0,2,EXPORT_BUFSIZE); nextBuf(NULL,outBuf,0,exportOutputs,EXPORT_BUFSIZE);
if (totalProcessed>EXPORT_BUFSIZE) { if (totalProcessed>EXPORT_BUFSIZE) {
logE("error: total processed is bigger than export bufsize! %d>%d",totalProcessed,EXPORT_BUFSIZE); logE("error: total processed is bigger than export bufsize! %d>%d",totalProcessed,EXPORT_BUFSIZE);
totalProcessed=EXPORT_BUFSIZE; totalProcessed=EXPORT_BUFSIZE;
} }
int fi=0;
for (int i=0; i<(int)totalProcessed; i++) { for (int i=0; i<(int)totalProcessed; i++) {
total++; total++;
if (isFadingOut) { if (isFadingOut) {
double mul=(1.0-((double)curFadeOutSample/(double)fadeOutSamples)); double mul=(1.0-((double)curFadeOutSample/(double)fadeOutSamples));
outBuf[2][i<<1]=MAX(-1.0f,MIN(1.0f,outBuf[0][i]))*mul; for (int j=0; j<exportOutputs; j++) {
outBuf[2][1+(i<<1)]=MAX(-1.0f,MIN(1.0f,outBuf[1][i]))*mul; outBufFinal[fi++]=MAX(-1.0f,MIN(1.0f,outBuf[j][i]))*mul;
}
if (++curFadeOutSample>=fadeOutSamples) { if (++curFadeOutSample>=fadeOutSamples) {
playing=false; playing=false;
break; break;
} }
} else { } else {
outBuf[2][i<<1]=MAX(-1.0f,MIN(1.0f,outBuf[0][i])); for (int j=0; j<exportOutputs; j++) {
outBuf[2][1+(i<<1)]=MAX(-1.0f,MIN(1.0f,outBuf[1][i])); outBufFinal[fi++]=MAX(-1.0f,MIN(1.0f,outBuf[j][i]));
}
if (lastLoopPos>-1 && i>=lastLoopPos && totalLoops>=exportLoopCount) { if (lastLoopPos>-1 && i>=lastLoopPos && totalLoops>=exportLoopCount) {
logD("start fading out..."); logD("start fading out...");
isFadingOut=true; isFadingOut=true;
@ -94,15 +99,16 @@ void DivEngine::runExportThread() {
} }
} }
if (sf_writef_float(sf,outBuf[2],total)!=(int)total) { if (sf_writef_float(sf,outBufFinal,total)!=(int)total) {
logE("error: failed to write entire buffer!"); logE("error: failed to write entire buffer!");
break; break;
} }
} }
delete[] outBuf[0]; delete[] outBufFinal;
delete[] outBuf[1]; for (int i=0; i<exportOutputs; i++) {
delete[] outBuf[2]; delete[] outBuf[i];
}
if (sfWrap.doClose()!=0) { if (sfWrap.doClose()!=0) {
logE("could not close audio file!"); logE("could not close audio file!");
@ -369,18 +375,17 @@ void DivEngine::runExportThread() {
#endif #endif
bool DivEngine::shallSwitchCores() { bool DivEngine::shallSwitchCores() {
// TODO: detect whether we should
return true; return true;
} }
bool DivEngine::saveAudio(const char* path, int loops, DivAudioExportModes mode, double fadeOutTime) { bool DivEngine::saveAudio(const char* path, DivAudioExportOptions options) {
#ifndef HAVE_SNDFILE #ifndef HAVE_SNDFILE
logE("Furnace was not compiled with libsndfile. cannot export!"); logE("Furnace was not compiled with libsndfile. cannot export!");
return false; return false;
#else #else
exportPath=path; exportPath=path;
exportMode=mode; exportMode=options.mode;
exportFadeOut=fadeOutTime; exportFadeOut=options.fadeOut;
if (exportMode!=DIV_EXPORT_MODE_ONE) { if (exportMode!=DIV_EXPORT_MODE_ONE) {
// remove extension // remove extension
String lowerCase=exportPath; String lowerCase=exportPath;
@ -412,7 +417,12 @@ bool DivEngine::saveAudio(const char* path, int loops, DivAudioExportModes mode,
} }
} }
exportLoopCount=loops; got.rate=options.sampleRate;
exportOutputs=options.chans;
if (exportOutputs<1) exportOutputs=1;
if (exportOutputs>DIV_MAX_OUTPUTS) exportOutputs=DIV_MAX_OUTPUTS;
exportLoopCount=options.loops+1;
exportThread=new std::thread(_runExportThread,this); exportThread=new std::thread(_runExportThread,this);
return true; return true;
#endif #endif

View file

@ -26,14 +26,35 @@
void FurnaceGUI::drawExportAudio(bool onWindow) { void FurnaceGUI::drawExportAudio(bool onWindow) {
exitDisabledTimer=1; exitDisabledTimer=1;
ImGui::RadioButton("one file",&audioExportType,0); ImGui::Text("Export type:");
ImGui::RadioButton("multiple files (one per chip)",&audioExportType,1);
ImGui::RadioButton("multiple files (one per channel)",&audioExportType,2); ImGui::Indent();
if (ImGui::InputInt("Loops",&exportLoops,1,2)) { if (ImGui::RadioButton("one file",audioExportOptions.mode==DIV_EXPORT_MODE_ONE)) {
if (exportLoops<0) exportLoops=0; audioExportOptions.mode=DIV_EXPORT_MODE_ONE;
} }
if (ImGui::InputDouble("Fade out (seconds)",&exportFadeOut,1.0,2.0,"%.1f")) { if (ImGui::RadioButton("multiple files (one per chip)",audioExportOptions.mode==DIV_EXPORT_MODE_MANY_SYS)) {
if (exportFadeOut<0.0) exportFadeOut=0.0; audioExportOptions.mode=DIV_EXPORT_MODE_MANY_SYS;
}
if (ImGui::RadioButton("multiple files (one per channel)",audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN)) {
audioExportOptions.mode=DIV_EXPORT_MODE_MANY_CHAN;
}
ImGui::Unindent();
if (ImGui::InputInt("Sample rate",&audioExportOptions.sampleRate,100,10000)) {
if (audioExportOptions.sampleRate<8000) audioExportOptions.sampleRate=8000;
if (audioExportOptions.sampleRate>384000) audioExportOptions.sampleRate=384000;
}
if (ImGui::InputInt("Channels in file",&audioExportOptions.chans,1,1)) {
if (audioExportOptions.chans<1) audioExportOptions.chans=1;
if (audioExportOptions.chans>16) audioExportOptions.chans=16;
}
if (ImGui::InputInt("Loops",&audioExportOptions.loops,1,2)) {
if (audioExportOptions.loops<0) audioExportOptions.loops=0;
}
if (ImGui::InputDouble("Fade out (seconds)",&audioExportOptions.fadeOut,1.0,2.0,"%.1f")) {
if (audioExportOptions.fadeOut<0.0) audioExportOptions.fadeOut=0.0;
} }
if (onWindow) { if (onWindow) {
@ -43,14 +64,14 @@ void FurnaceGUI::drawExportAudio(bool onWindow) {
} }
if (ImGui::Button("Export",ImVec2(200.0f*dpiScale,0))) { if (ImGui::Button("Export",ImVec2(200.0f*dpiScale,0))) {
switch (audioExportType) { switch (audioExportOptions.mode) {
case 0: case DIV_EXPORT_MODE_ONE:
openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE); openFileDialog(GUI_FILE_EXPORT_AUDIO_ONE);
break; break;
case 1: case DIV_EXPORT_MODE_MANY_SYS:
openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_SYS); openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_SYS);
break; break;
case 2: case DIV_EXPORT_MODE_MANY_CHAN:
openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_CHANNEL); openFileDialog(GUI_FILE_EXPORT_AUDIO_PER_CHANNEL);
break; break;
} }

View file

@ -2436,7 +2436,7 @@ int FurnaceGUI::loadStream(String path) {
void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
e->saveAudio(path.c_str(),exportLoops+1,mode,exportFadeOut); e->saveAudio(path.c_str(),audioExportOptions);
displayExporting=true; displayExporting=true;
} }
@ -6761,10 +6761,10 @@ bool FurnaceGUI::init() {
followOrders=e->getConfBool("followOrders",true); followOrders=e->getConfBool("followOrders",true);
followPattern=e->getConfBool("followPattern",true); followPattern=e->getConfBool("followPattern",true);
noteInputPoly=e->getConfBool("noteInputPoly",true); noteInputPoly=e->getConfBool("noteInputPoly",true);
exportLoops=e->getConfInt("exportLoops",0); audioExportOptions.loops=e->getConfInt("exportLoops",0);
if (exportLoops<0) exportLoops=0; if (audioExportOptions.loops<0) audioExportOptions.loops=0;
exportFadeOut=e->getConfDouble("exportFadeOut",0.0); audioExportOptions.fadeOut=e->getConfDouble("exportFadeOut",0.0);
if (exportFadeOut<0.0) exportFadeOut=0.0; if (audioExportOptions.fadeOut<0.0) audioExportOptions.fadeOut=0.0;
orderEditMode=e->getConfInt("orderEditMode",0); orderEditMode=e->getConfInt("orderEditMode",0);
if (orderEditMode<0) orderEditMode=0; if (orderEditMode<0) orderEditMode=0;
if (orderEditMode>3) orderEditMode=3; if (orderEditMode>3) orderEditMode=3;
@ -6826,8 +6826,8 @@ bool FurnaceGUI::init() {
syncTutorial(); syncTutorial();
if (!settings.persistFadeOut) { if (!settings.persistFadeOut) {
exportLoops=settings.exportLoops; audioExportOptions.loops=settings.exportLoops;
exportFadeOut=settings.exportFadeOut; audioExportOptions.fadeOut=settings.exportFadeOut;
} }
for (int i=0; i<settings.maxRecentFile; i++) { for (int i=0; i<settings.maxRecentFile; i++) {
@ -7320,8 +7320,8 @@ void FurnaceGUI::commitState() {
e->setConf("orderEditMode",orderEditMode); e->setConf("orderEditMode",orderEditMode);
e->setConf("noteInputPoly",noteInputPoly); e->setConf("noteInputPoly",noteInputPoly);
if (settings.persistFadeOut) { if (settings.persistFadeOut) {
e->setConf("exportLoops",exportLoops); e->setConf("exportLoops",audioExportOptions.loops);
e->setConf("exportFadeOut",exportFadeOut); e->setConf("exportFadeOut",audioExportOptions.fadeOut);
} }
// commit oscilloscope state // commit oscilloscope state
@ -7581,7 +7581,6 @@ FurnaceGUI::FurnaceGUI():
oldRow(0), oldRow(0),
editStep(1), editStep(1),
editStepCoarse(16), editStepCoarse(16),
exportLoops(0),
soloChan(-1), soloChan(-1),
orderEditMode(0), orderEditMode(0),
orderCursor(-1), orderCursor(-1),
@ -7604,7 +7603,6 @@ FurnaceGUI::FurnaceGUI():
curPaletteChoice(0), curPaletteChoice(0),
curPaletteType(0), curPaletteType(0),
soloTimeout(0.0f), soloTimeout(0.0f),
exportFadeOut(5.0),
patExtraButtons(false), patExtraButtons(false),
patChannelNames(false), patChannelNames(false),
patChannelPairs(true), patChannelPairs(true),
@ -7955,7 +7953,6 @@ FurnaceGUI::FurnaceGUI():
introStopped(false), introStopped(false),
curTutorial(-1), curTutorial(-1),
curTutorialStep(0), curTutorialStep(0),
audioExportType(0),
dmfExportVersion(0), dmfExportVersion(0),
curExportType(GUI_EXPORT_NONE) { curExportType(GUI_EXPORT_NONE) {
// value keys // value keys

View file

@ -2194,15 +2194,13 @@ class FurnaceGUI {
int pendingLayoutImportStep; int pendingLayoutImportStep;
FixedQueue<bool*,64> pendingLayoutImportReopen; FixedQueue<bool*,64> pendingLayoutImportReopen;
int curIns, curWave, curSample, curOctave, curOrder, playOrder, prevIns, oldRow, editStep, editStepCoarse, exportLoops, soloChan, orderEditMode, orderCursor; int curIns, curWave, curSample, curOctave, curOrder, playOrder, prevIns, oldRow, editStep, editStepCoarse, soloChan, orderEditMode, orderCursor;
int loopOrder, loopRow, loopEnd, isClipping, newSongCategory, latchTarget; int loopOrder, loopRow, loopEnd, isClipping, newSongCategory, latchTarget;
int wheelX, wheelY, dragSourceX, dragSourceXFine, dragSourceY, dragDestinationX, dragDestinationXFine, dragDestinationY, oldBeat, oldBar; int wheelX, wheelY, dragSourceX, dragSourceXFine, dragSourceY, dragDestinationX, dragDestinationXFine, dragDestinationY, oldBeat, oldBar;
int curGroove, exitDisabledTimer; int curGroove, exitDisabledTimer;
int curPaletteChoice, curPaletteType; int curPaletteChoice, curPaletteType;
float soloTimeout; float soloTimeout;
double exportFadeOut;
bool patExtraButtons, patChannelNames, patChannelPairs; bool patExtraButtons, patChannelNames, patChannelPairs;
unsigned char patChannelHints; unsigned char patChannelHints;
@ -2603,7 +2601,7 @@ class FurnaceGUI {
ImGuiListClipper csClipper; ImGuiListClipper csClipper;
// export options // export options
int audioExportType; DivAudioExportOptions audioExportOptions;
int dmfExportVersion; int dmfExportVersion;
FurnaceGUIExportTypes curExportType; FurnaceGUIExportTypes curExportType;

View file

@ -705,13 +705,13 @@ void FurnaceGUI::drawSettings() {
ImGui::BeginDisabled(settings.persistFadeOut); ImGui::BeginDisabled(settings.persistFadeOut);
ImGui::Indent(); ImGui::Indent();
if (ImGui::InputInt("Loops",&settings.exportLoops,1,2)) { if (ImGui::InputInt("Loops",&settings.exportLoops,1,2)) {
if (exportLoops<0) exportLoops=0; if (settings.exportLoops<0) settings.exportLoops=0;
exportLoops=settings.exportLoops; audioExportOptions.loops=settings.exportLoops;
settingsChanged=true; settingsChanged=true;
} }
if (ImGui::InputDouble("Fade out (seconds)",&settings.exportFadeOut,1.0,2.0,"%.1f")) { if (ImGui::InputDouble("Fade out (seconds)",&settings.exportFadeOut,1.0,2.0,"%.1f")) {
if (exportFadeOut<0.0) exportFadeOut=0.0; if (settings.exportFadeOut<0.0) settings.exportFadeOut=0.0;
exportFadeOut=settings.exportFadeOut; audioExportOptions.fadeOut=settings.exportFadeOut;
settingsChanged=true; settingsChanged=true;
} }
ImGui::Unindent(); ImGui::Unindent();

View file

@ -66,10 +66,9 @@ String outName;
String vgmOutName; String vgmOutName;
String zsmOutName; String zsmOutName;
String cmdOutName; String cmdOutName;
int loops=1;
int benchMode=0; int benchMode=0;
int subsong=-1; int subsong=-1;
DivAudioExportModes outMode=DIV_EXPORT_MODE_ONE; DivAudioExportOptions exportOptions;
#ifdef HAVE_GUI #ifdef HAVE_GUI
bool consoleMode=false; bool consoleMode=false;
@ -299,9 +298,9 @@ TAParamResult pLoops(String val) {
try { try {
int count=std::stoi(val); int count=std::stoi(val);
if (count<0) { if (count<0) {
loops=0; exportOptions.loops=0;
} else { } else {
loops=count+1; exportOptions.loops=count;
} }
} catch (std::exception& e) { } catch (std::exception& e) {
logE("loop count shall be a number."); logE("loop count shall be a number.");
@ -327,11 +326,11 @@ TAParamResult pSubSong(String val) {
TAParamResult pOutMode(String val) { TAParamResult pOutMode(String val) {
if (val=="one") { if (val=="one") {
outMode=DIV_EXPORT_MODE_ONE; exportOptions.mode=DIV_EXPORT_MODE_ONE;
} else if (val=="persys") { } else if (val=="persys") {
outMode=DIV_EXPORT_MODE_MANY_SYS; exportOptions.mode=DIV_EXPORT_MODE_MANY_SYS;
} else if (val=="perchan") { } else if (val=="perchan") {
outMode=DIV_EXPORT_MODE_MANY_CHAN; exportOptions.mode=DIV_EXPORT_MODE_MANY_CHAN;
} else { } else {
logE("invalid value for outmode! valid values are: one, persys and perchan."); logE("invalid value for outmode! valid values are: one, persys and perchan.");
return TA_PARAM_ERROR; return TA_PARAM_ERROR;
@ -401,7 +400,7 @@ void initParams() {
params.push_back(TAParam("n","nostatus",false,pNoStatus,"","disable playback status in console mode")); params.push_back(TAParam("n","nostatus",false,pNoStatus,"","disable playback status in console mode"));
params.push_back(TAParam("N","nocontrols",false,pNoControls,"","disable standard input controls in console mode")); params.push_back(TAParam("N","nocontrols",false,pNoControls,"","disable standard input controls in console mode"));
params.push_back(TAParam("l","loops",true,pLoops,"<count>","set number of loops (-1 means loop forever)")); params.push_back(TAParam("l","loops",true,pLoops,"<count>","set number of loops"));
params.push_back(TAParam("s","subsong",true,pSubSong,"<number>","set sub-song")); params.push_back(TAParam("s","subsong",true,pSubSong,"<number>","set sub-song"));
params.push_back(TAParam("o","outmode",true,pOutMode,"one|persys|perchan","set file output mode")); params.push_back(TAParam("o","outmode",true,pOutMode,"one|persys|perchan","set file output mode"));
params.push_back(TAParam("S","safemode",false,pSafeMode,"","enable safe mode (software rendering and no audio)")); params.push_back(TAParam("S","safemode",false,pSafeMode,"","enable safe mode (software rendering and no audio)"));
@ -716,7 +715,7 @@ int main(int argc, char** argv) {
} }
if (outName!="") { if (outName!="") {
e.setConsoleMode(true); e.setConsoleMode(true);
e.saveAudio(outName.c_str(),loops,outMode); e.saveAudio(outName.c_str(),exportOptions);
e.waitAudioFile(); e.waitAudioFile();
} }
finishLogFile(); finishLogFile();