Skip to content

Commit

Permalink
ffmpeg export: improved export command system
Browse files Browse the repository at this point in the history
Each ffmpeg export command is now specified by a template string, and
this opens up a window for future proper custom commands.
  • Loading branch information
yohannd1 committed Jan 17, 2025
1 parent 9ed92a6 commit ba6629f
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 62 deletions.
42 changes: 37 additions & 5 deletions src/engine/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,26 @@ enum DivAudioExportFormats {
DIV_EXPORT_FORMAT_F32
};

enum DivAudioExportWriters {
DIV_EXPORT_WRITER_SNDFILE=0,
DIV_EXPORT_WRITER_COMMAND
};

struct DivAudioCommandExportDef {
String name, fileExt, commandTemplate;
DivAudioCommandExportDef(String n, String fe, String ct):
name(n),
fileExt(fe),
commandTemplate(ct) {}
};

struct DivAudioExportOptions {
DivAudioExportModes mode;
DivAudioExportFormats format;
String fileExt;
String ffmpegFlags;
String extraFlags;
DivAudioExportWriters curWriter;
std::vector<DivAudioCommandExportDef> commandExportWriterDefs;
int curCommandWriterIndex;
int sampleRate;
int chans;
int loops;
Expand All @@ -120,8 +135,9 @@ struct DivAudioExportOptions {
DivAudioExportOptions():
mode(DIV_EXPORT_MODE_ONE),
format(DIV_EXPORT_FORMAT_S16),
fileExt("wav"),
ffmpegFlags(""),
extraFlags(""),
curWriter(DIV_EXPORT_WRITER_SNDFILE),
curCommandWriterIndex(0),
sampleRate(44100),
chans(2),
loops(0),
Expand All @@ -131,6 +147,16 @@ struct DivAudioExportOptions {
for (int i=0; i<DIV_MAX_CHANS; i++) {
channelMask[i]=true;
}

const char *ffmpegTemplate="%ffmpeg% -y -v verbose -f %input_format% -ar %sample_rate% -ac %channel_count% -i pipe:0 %output_file%"; // TODO: %extra_flags% (or maybe don't use that)

commandExportWriterDefs={
DivAudioCommandExportDef("FLAC file (.flac) (ffmpeg)","flac",ffmpegTemplate),
DivAudioCommandExportDef("OGG file (.ogg) (ffmpeg)","ogg",ffmpegTemplate),
DivAudioCommandExportDef("MP3 file (.mp3) (ffmpeg)","mp3",ffmpegTemplate),
DivAudioCommandExportDef("M4A file (.m4a) (ffmpeg)","m4a",ffmpegTemplate),
DivAudioCommandExportDef("OPUS file (.opus) (ffmpeg)","opus",ffmpegTemplate)
};
}
};

Expand Down Expand Up @@ -503,8 +529,10 @@ class DivEngine {
DivAudioEngines audioEngine;
DivAudioExportModes exportMode;
DivAudioExportFormats exportFormat;
DivAudioExportWriters exportWriter;
String exportCommand;
String exportFileExtNoDot;
String exportFfmpegFlags;
String exportExtraFlags;
double exportFadeOut;
bool isFadingOut;
int exportOutputs;
Expand Down Expand Up @@ -1476,6 +1504,10 @@ class DivEngine {
audioEngine(DIV_AUDIO_NULL),
exportMode(DIV_EXPORT_MODE_ONE),
exportFormat(DIV_EXPORT_FORMAT_S16),
exportWriter(DIV_EXPORT_WRITER_SNDFILE),
exportCommand(""),
exportFileExtNoDot("wav"),
exportExtraFlags(""),
exportFadeOut(0.0),
isFadingOut(false),
exportOutputs(2),
Expand Down
100 changes: 74 additions & 26 deletions src/engine/wavOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,44 @@ class ProcWriter {
};
#endif

#include <iomanip>

std::vector<String> substituteArgs(const std::vector<String>& args, const std::map<String, String>& defs) {
std::vector<String> result;
for (const String& arg : args) {
String finalArg;
size_t start=0;
while (start<arg.size()) {
size_t end=arg.find('%',start);
if (end==start) {
// we found a % right where we are

size_t secondEnd=arg.find('%',start+1);
if (secondEnd==end+1) {
// we got another % right after - it's an escaped %
finalArg.push_back('%');
} else if (secondEnd==String::npos) {
// we would error but let's just not substitute
finalArg.append(arg,start);
start=end;
} else {
String key=arg.substr(end+1,secondEnd-(end+1));
if (defs.find(key)!=defs.end()) {
finalArg.append(defs.at(key));
}
}
start=(secondEnd==String::npos)?(String::npos):(secondEnd+1);
} else {
// we found a % more ahead (or got to the end of the string)
finalArg.append(arg,start,end-start);
start=end;
}
}
result.push_back(finalArg);
}
return result;
}

#ifdef HAVE_SNDFILE
void DivEngine::runExportThread() {
size_t fadeOutSamples=got.rate*exportFadeOut;
Expand Down Expand Up @@ -298,42 +336,50 @@ void DivEngine::runExportThread() {
}
};

if (exportFileExtNoDot=="wav") {
if (exportWriter==DIV_EXPORT_WRITER_SNDFILE) {
SndfileWavWriter wr;
if (!wr.open(makeSfInfo(),exportPath.c_str())) {
logE("could not initialize export writer");
exporting=false;
return;
}
doExport(&wr);
} else {
} else if (exportWriter==DIV_EXPORT_WRITER_COMMAND) {
#ifdef _WIN32
logE("ffmpeg export is not yet supported");
#else
String inputFormatArg=(exportFormat==DIV_EXPORT_FORMAT_S16)?"s16le":"f32le";

// build command vector
std::vector<String> command={
"ffmpeg","-y",
"-v","verbose",
"-f",inputFormatArg,
"-ar",fmt::sprintf("%ld",(long int)got.rate), // sample rate
"-ac",fmt::sprintf("%d",exportOutputs), // channel amount
// "-guess_layout_max","0",
"-i","pipe:0",
// "-f",exportFileExtNoDot
std::map<String, String> defMap;
defMap["ffmpeg"]="ffmpeg";
defMap["input_format"]=inputFormatArg;
defMap["sample_rate"]=fmt::sprintf("%ld",(long int)got.rate);
defMap["channel_count"]=fmt::sprintf("%d",exportOutputs);
defMap["extra_flags"]=exportExtraFlags; // FIXME: the flags won't be split!!! whoops
defMap["output_file"]=exportPath;

const auto vec2str=[](const std::vector<String>& vec) {
String output;
output+="[";
for (size_t i=0; i<vec.size(); i++) {
output+='"';
output+=vec[i];
output+='"';
if (i<vec.size()-1)
output+=", ";
}
output+="]";
return output;
};
splitString(exportFfmpegFlags,' ',command);
command.push_back(exportPath);

String totalCommand;
for (const String& s : command) {
totalCommand+=s;
totalCommand+=" ";
}
logD("command: %s",totalCommand);
std::vector<String> args;
splitString(exportCommand,' ',args);

logD("Before replacing: %s",vec2str(args));
args=substituteArgs(args,defMap);
logD("After replacing: %s",vec2str(args));

Subprocess proc(command);
Subprocess proc(args);
int writeFd=proc.pipeStdin();
if (writeFd==-1) {
logE("failed to create stdin pipe for subprocess");
Expand Down Expand Up @@ -645,13 +691,15 @@ bool DivEngine::saveAudio(const char* path, DivAudioExportOptions options) {
exportMode=options.mode;
exportFormat=options.format;
exportFadeOut=options.fadeOut;
exportFfmpegFlags=options.ffmpegFlags;
exportWriter=options.curWriter;
exportExtraFlags=options.extraFlags;

const char* fileExt=options.fileExt.c_str();
if (fileExt[0]=='.') {
exportFileExtNoDot=&fileExt[1];
if (exportWriter==DIV_EXPORT_WRITER_SNDFILE) {
exportFileExtNoDot="wav";
} else {
exportFileExtNoDot=fileExt;
DivAudioCommandExportDef& def=options.commandExportWriterDefs[options.curCommandWriterIndex];
exportCommand=def.commandTemplate;
exportFileExtNoDot=def.fileExt;
}

memcpy(exportChannelMask,options.channelMask,DIV_MAX_CHANS*sizeof(bool));
Expand Down
56 changes: 37 additions & 19 deletions src/gui/exportOptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,58 @@
#include "misc/cpp/imgui_stdlib.h"
#include <imgui.h>

// Enforces `_val` being an index on a container of size `_size`.
#define CLAMP_TO_SIZE(_val,_size) \
if (_size==0||_val<0) _val=0; \
else if (_val>=(int)_size) _val=(int)_size-1;

void FurnaceGUI::drawExportAudio(bool onWindow) {
exitDisabledTimer=1;
CLAMP_TO_SIZE(audioExportOptions.curCommandWriterIndex,audioExportOptions.commandExportWriterDefs.size());

#ifdef _WIN32
const bool allowNonWav=false;
const bool isWin32=true;
#else
const bool allowNonWav=true;
const bool isWin32=false;
#endif

const auto formatEF = [](const FurnaceGUIExportFormat& ef) {
return fmt::sprintf("%s (%s)",_(ef.name),ef.fileExt);
};
const bool allowCommandWriter=!isWin32;
const char* sndfileDesc="Wave file (.wav) (libsndfile)";

const FurnaceGUIExportFormat& currentFormat=exportFormats[curAudioExportFormat];
if (ImGui::BeginCombo(_("file format"),formatEF(currentFormat).c_str())) {
for (int i=0; i<EXPORT_FORMAT_COUNT; i++) {
const FurnaceGUIExportFormat& ef=exportFormats[i];
const auto getCurrentFormatDesc=[this,sndfileDesc]() -> String {
switch (audioExportOptions.curWriter) {
case DIV_EXPORT_WRITER_SNDFILE:
return sndfileDesc;
case DIV_EXPORT_WRITER_COMMAND:
return audioExportOptions.commandExportWriterDefs[audioExportOptions.curCommandWriterIndex].name.c_str();
default:
return "???";
}
};

if (!allowNonWav && i>0) ImGui::BeginDisabled();
if (ImGui::Selectable(formatEF(ef).c_str())) {
curAudioExportFormat=i;
audioExportOptions.fileExt=ef.fileExt;
if (ImGui::BeginCombo(_("file format"),getCurrentFormatDesc().c_str())) {
if (ImGui::Selectable(sndfileDesc)) {
audioExportOptions.curWriter=DIV_EXPORT_WRITER_SNDFILE;
audioExportOptions.curCommandWriterIndex=0;
}
for (size_t i=0; i<audioExportOptions.commandExportWriterDefs.size(); i++) {
if (!allowCommandWriter) ImGui::BeginDisabled();
if (ImGui::Selectable(audioExportOptions.commandExportWriterDefs[i].name.c_str())) {
audioExportOptions.curWriter=DIV_EXPORT_WRITER_COMMAND;
audioExportOptions.curCommandWriterIndex=i;
}
if (!allowNonWav && i>0) ImGui::EndDisabled();
if (!allowCommandWriter) ImGui::EndDisabled();
}
ImGui::EndCombo();
}
#ifdef _WIN32
ImGui::Text("Note: non-wav file formats are not yet supported on windows. Sorry!");
#endif

if (curAudioExportFormat>0) {
ImGui::InputText(_("extra ffmpeg flags"),&audioExportOptions.ffmpegFlags,ImGuiInputTextFlags_UndoRedo);
#ifdef _WIN32
ImGui::Text("Note: custom-command exports (including via ffmpeg) are not yet supported on Windows. Sorry!");
#else
if (audioExportOptions.curWriter==DIV_EXPORT_WRITER_COMMAND) {
ImGui::InputText(_("extra flags"),&audioExportOptions.extraFlags,ImGuiInputTextFlags_UndoRedo);
}
#endif

ImGui::Text(_("Export type:"));

Expand Down
29 changes: 19 additions & 10 deletions src/gui/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,15 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
}
}

const auto getAudioExportGlob=[this]() -> std::vector<String> {
if (audioExportOptions.curWriter==DIV_EXPORT_WRITER_SNDFILE) {
return {_("Wave file"), "*.wav"};
} else {
const DivAudioCommandExportDef& ed=audioExportOptions.commandExportWriterDefs[audioExportOptions.curCommandWriterIndex];
return {_(ed.name.c_str()), fmt::sprintf("*.%s",ed.fileExt)};
}
};

switch (type) {
case GUI_FILE_OPEN:
if (!dirExists(workingDirSong)) workingDirSong=getHomeDir();
Expand Down Expand Up @@ -1965,10 +1974,9 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
break;
case GUI_FILE_EXPORT_AUDIO_ONE: {
if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
const FurnaceGUIExportFormat *ef=&exportFormats[curAudioExportFormat];
hasOpened=fileDialog->openSave(
_("Export Audio"),
{_(ef->name), ef->globPattern},
getAudioExportGlob(),
workingDirAudioExport,
dpiScale,
(settings.autoFillSave)?shortName:""
Expand All @@ -1977,10 +1985,9 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
}
case GUI_FILE_EXPORT_AUDIO_PER_SYS: {
if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
const FurnaceGUIExportFormat *ef=&exportFormats[curAudioExportFormat];
hasOpened=fileDialog->openSave(
_("Export Audio"),
{_(ef->name), ef->globPattern},
getAudioExportGlob(),
workingDirAudioExport,
dpiScale,
(settings.autoFillSave)?shortName:""
Expand All @@ -1989,10 +1996,9 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
}
case GUI_FILE_EXPORT_AUDIO_PER_CHANNEL: {
if (!dirExists(workingDirAudioExport)) workingDirAudioExport=getHomeDir();
const FurnaceGUIExportFormat *ef=&exportFormats[curAudioExportFormat];
hasOpened=fileDialog->openSave(
_("Export Audio"),
{_(ef->name), ef->globPattern},
getAudioExportGlob(),
workingDirAudioExport,
dpiScale,
(settings.autoFillSave)?shortName:""
Expand Down Expand Up @@ -5113,8 +5119,12 @@ bool FurnaceGUI::loop() {
if (curFileDialog==GUI_FILE_EXPORT_AUDIO_ONE ||
curFileDialog==GUI_FILE_EXPORT_AUDIO_PER_SYS ||
curFileDialog==GUI_FILE_EXPORT_AUDIO_PER_CHANNEL) {
const FurnaceGUIExportFormat *ef=&exportFormats[curAudioExportFormat];
checkExtension(ef->fileExt);
if (audioExportOptions.curWriter==DIV_EXPORT_WRITER_SNDFILE) {
checkExtension(".wav");
} else {
String ext=fmt::sprintf(".%s",audioExportOptions.commandExportWriterDefs[audioExportOptions.curCommandWriterIndex].fileExt);
checkExtension(ext.c_str());
}
}
if (curFileDialog==GUI_FILE_INS_SAVE) {
checkExtension(".fui");
Expand Down Expand Up @@ -8206,7 +8216,7 @@ void FurnaceGUI::commitState(DivConfig& conf) {
conf.set("xyOscIntensity",xyOscIntensity);
conf.set("xyOscThickness",xyOscThickness);

conf.set("audioExportFfmpegFlags",audioExportOptions.ffmpegFlags);
conf.set("audioExportExtraFlags",audioExportOptions.extraFlags);

// commit recent files
for (int i=0; i<30; i++) {
Expand Down Expand Up @@ -8283,7 +8293,6 @@ FurnaceGUI::FurnaceGUI():
sampleTexW(0),
sampleTexH(0),
updateSampleTex(true),
curAudioExportFormat(0),
quit(false),
warnQuit(false),
willCommit(false),
Expand Down
1 change: 0 additions & 1 deletion src/gui/gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -1629,7 +1629,6 @@ class FurnaceGUI {
String mmlStringW, grooveString, grooveListString, mmlStringModTable;
String mmlStringSNES[DIV_MAX_CHIPS];
String folderString;
int curAudioExportFormat;

struct PaletteSearchResult { int id; std::vector<int> highlightChars; };
std::vector<DivSystem> sysSearchResults;
Expand Down
2 changes: 1 addition & 1 deletion src/gui/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5407,7 +5407,7 @@ void FurnaceGUI::readConfig(DivConfig& conf, FurnaceGUISettingGroups groups) {
if (settings.exportLoops<0.0) settings.exportLoops=0.0;
if (settings.exportFadeOut<0.0) settings.exportFadeOut=0.0;

audioExportOptions.ffmpegFlags=conf.getString("audioExportFfmpegFlags","");
audioExportOptions.extraFlags=conf.getString("audioExportExtraFlags","");
}

void FurnaceGUI::writeConfig(DivConfig& conf, FurnaceGUISettingGroups groups) {
Expand Down

0 comments on commit ba6629f

Please sign in to comment.