diff --git a/CMakeLists.txt b/CMakeLists.txt index 63f17d5aa9..6589216bb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -481,6 +481,7 @@ src/subprocess.cpp src/baseutils.cpp src/fileutils.cpp src/utfutils.cpp +src/stringutils.cpp extern/itcompress/compression.c diff --git a/src/engine/engine.h b/src/engine/engine.h index c64268dd85..167079aa13 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -499,6 +499,7 @@ class DivEngine { DivAudioEngines audioEngine; DivAudioExportModes exportMode; DivAudioExportFormats exportFormat; + String exportFileExtNoDot; double exportFadeOut; bool isFadingOut; int exportOutputs; @@ -623,8 +624,8 @@ class DivEngine { void loadFF(SafeReader& reader, std::vector& ret, String& stripPath); void loadWOPL(SafeReader& reader, std::vector& ret, String& stripPath); void loadWOPN(SafeReader& reader, std::vector& ret, String& stripPath); - - //sample banks + + // sample banks void loadP(SafeReader& reader, std::vector& ret, String& stripPath); void loadPPC(SafeReader& reader, std::vector& ret, String& stripPath); void loadPPS(SafeReader& reader, std::vector& ret, String& stripPath); @@ -633,8 +634,6 @@ class DivEngine { void loadPZI(SafeReader& reader, std::vector& ret, String& stripPath); void loadP86(SafeReader& reader, std::vector& ret, String& stripPath); - - int loadSampleROM(String path, ssize_t expectedSize, unsigned char*& ret); bool initAudioBackend(); @@ -734,7 +733,7 @@ class DivEngine { // export to text SafeWriter* saveText(bool separatePatterns=true); // export to an audio file - bool saveAudio(const char* path, DivAudioExportOptions options); + bool saveAudio(const char* path, DivAudioExportOptions options, const char *fileExt); // wait for audio export to finish void waitAudioFile(); // stop audio file export diff --git a/src/engine/sfWrapper.cpp b/src/engine/sfWrapper.cpp index 18a1c0c344..dbd10d2209 100644 --- a/src/engine/sfWrapper.cpp +++ b/src/engine/sfWrapper.cpp @@ -75,12 +75,16 @@ int SFWrapper::doClose() { return ret; } -SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) { +void SFWrapper::initVio() { vio.get_filelen=_vioGetSize; vio.read=_vioRead; vio.seek=_vioSeek; vio.tell=_vioTell; vio.write=_vioWrite; +} + +SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) { + initVio(); logV("SFWrapper: opening %s",path); const char* modeC="rb"; @@ -103,7 +107,7 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) { f=NULL; return NULL; } - + len=ftell(f); if (len==(SIZE_MAX>>1)) { logE("SFWrapper: failed to tell (%s)",strerror(errno)); @@ -128,3 +132,21 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) { } return sf; } + +SNDFILE* SFWrapper::doOpenFromWriteFd(int writeFd, SF_INFO* sfinfo) { + f=fdopen(writeFd,"w"); + if (f==NULL) { + logE("SFWrapper: failed to open file descriptor %d (pipe) as file: %s",writeFd,strerror(errno)); + return NULL; + } + + initVio(); + len=0; // I am hoping this is not used when writing + fileMode=SFM_WRITE; + + sf=sf_open_virtual(&vio,SFM_WRITE,sfinfo,this); + if (sf==NULL) { + logE("SFWrapper: WHY IS IT NULL?!"); + } + return sf; +} diff --git a/src/engine/sfWrapper.h b/src/engine/sfWrapper.h index c32c8ba194..50abab6b0d 100644 --- a/src/engine/sfWrapper.h +++ b/src/engine/sfWrapper.h @@ -44,8 +44,10 @@ class SFWrapper { sf_count_t ioWrite(const void* ptr, sf_count_t count); sf_count_t ioTell(); + void initVio(); int doClose(); SNDFILE* doOpen(const char* path, int mode, SF_INFO* sfinfo); + SNDFILE* doOpenFromWriteFd(int fd, SF_INFO* sfinfo); SFWrapper(): f(NULL), len(0), diff --git a/src/engine/wavOps.cpp b/src/engine/wavOps.cpp index 0b8893f9bf..d5645222f8 100644 --- a/src/engine/wavOps.cpp +++ b/src/engine/wavOps.cpp @@ -20,6 +20,7 @@ #include "engine.h" #include "../ta-log.h" #include "../subprocess.h" +#include "../stringutils.h" #ifdef HAVE_SNDFILE #include "sfWrapper.h" #endif @@ -120,92 +121,106 @@ void DivEngine::runExportThread() { si.format=SF_FORMAT_WAV|SF_FORMAT_FLOAT; } - // TODO: receive format! and check if we really need to pipe it out - std::vector args={"echo", "hello world!"}; - Subprocess proc(args); - if (!proc.start()) { - logE("failed to start subprocess!\n"); - // int writeFd=proc.pipeStdin(); // TODO - } else { - proc.wait(); - } - - sf=sfWrap.doOpen(exportPath.c_str(),SFM_WRITE,&si); - if (sf==NULL) { - logE("could not open file for writing! (%s)",sf_strerror(NULL)); - exporting=false; - return; - } + const auto doExport=[&](){ + if (sf==NULL) { + logE("could not initialize export"); + exporting=false; + return; + } - float* outBuf[DIV_MAX_OUTPUTS]; - float* outBufFinal; - for (int i=0; iEXPORT_BUFSIZE) { - logE("error: total processed is bigger than export bufsize! %d>%d",totalProcessed,EXPORT_BUFSIZE); - totalProcessed=EXPORT_BUFSIZE; - } - int fi=0; - for (int i=0; i<(int)totalProcessed; i++) { - total++; - if (isFadingOut) { - double mul=(1.0-((double)curFadeOutSample/(double)fadeOutSamples)); - for (int j=0; j=fadeOutSamples) { - playing=false; - break; - } - } else { - for (int j=0; j-1 && i>=lastLoopPos && totalLoops>=exportLoopCount) { - logD("start fading out..."); - isFadingOut=true; - if (fadeOutSamples==0) break; + while (playing) { + size_t total=0; + nextBuf(NULL,outBuf,0,exportOutputs,EXPORT_BUFSIZE); + if (totalProcessed>EXPORT_BUFSIZE) { + logE("error: total processed is bigger than export bufsize! %d>%d",totalProcessed,EXPORT_BUFSIZE); + totalProcessed=EXPORT_BUFSIZE; + } + int fi=0; + for (int i=0; i<(int)totalProcessed; i++) { + total++; + if (isFadingOut) { + double mul=(1.0-((double)curFadeOutSample/(double)fadeOutSamples)); + for (int j=0; j=fadeOutSamples) { + playing=false; + break; + } + } else { + for (int j=0; j-1 && i>=lastLoopPos && totalLoops>=exportLoopCount) { + logD("start fading out..."); + isFadingOut=true; + if (fadeOutSamples==0) break; + } } } - } - if (sf_writef_float(sf,outBufFinal,total)!=(int)total) { - logE("error: failed to write entire buffer!"); - break; + if (sf_writef_float(sf,outBufFinal,total)!=(int)total) { + logE("error: failed to write entire buffer!"); + break; + } } - } - delete[] outBufFinal; - for (int i=0; isetRun(true)) { + logE("error while activating audio!"); + } } - if (!output->setRun(true)) { - logE("error while activating audio!"); + }; + + if (exportFileExtNoDot=="wav") { + sf=sfWrap.doOpen(exportPath.c_str(),SFM_WRITE,&si); + doExport(); + } else { + std::vector args={"tee", "test.txt"}; // TODO: build args + Subprocess proc(args); + int writeFd=proc.pipeStdin(); + if (writeFd==-1) { + logE("failed to create stdin pipe for subprocess"); + } if (proc.start()) { + sf=sfWrap.doOpenFromWriteFd(writeFd,&si); + doExport(); + logI("waiting for ffmpeg to finish..."); + int code = proc.wait(); + if (code!=0) { + logE("ffmpeg failed to export successfully"); // TODO: actually show this in the UI? (it might be a good idea to read the output from ffmpeg, in that case) + } + } else { + logE("failed to start subprocess"); } } - logI("done!"); + + logI("exporting done!"); exporting=false; break; } @@ -481,7 +496,7 @@ bool DivEngine::shallSwitchCores() { return true; } -bool DivEngine::saveAudio(const char* path, DivAudioExportOptions options) { +bool DivEngine::saveAudio(const char* path, DivAudioExportOptions options, const char *fileExt) { #ifndef HAVE_SNDFILE logE("Furnace was not compiled with libsndfile. cannot export!"); return false; @@ -490,17 +505,15 @@ bool DivEngine::saveAudio(const char* path, DivAudioExportOptions options) { exportMode=options.mode; exportFormat=options.format; exportFadeOut=options.fadeOut; + if (fileExt[0]=='.') { + exportFileExtNoDot=&fileExt[1]; + } else { + exportFileExtNoDot=fileExt; + } + memcpy(exportChannelMask,options.channelMask,DIV_MAX_CHANS*sizeof(bool)); if (exportMode!=DIV_EXPORT_MODE_ONE) { - // remove extension - String lowerCase=exportPath; - for (char& i: lowerCase) { - if (i>='A' && i<='Z') i+='a'-'A'; - } - size_t extPos=lowerCase.rfind(".wav"); - if (extPos!=String::npos) { - exportPath=exportPath.substr(0,extPos); - } + removeFileExt(exportPath,exportFileExtNoDot.c_str()); } exporting=true; stopExport=false; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 5e3461fcf0..d24448c975 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -2583,9 +2583,6 @@ int FurnaceGUI::loadStream(String path) { void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { songOrdersLengths.clear(); - const FurnaceGUIExportFormat *ef=&exportFormats[curAudioExportFormat]; - const char *extNoDot=&ef->fileExt[1]; - int loopOrder=0; int loopRow=0; int loopEnd=0; @@ -2599,7 +2596,8 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { } songLoopedSectionLength-=loopRow; - e->saveAudio(path.c_str(),audioExportOptions); + const FurnaceGUIExportFormat& ef=exportFormats[curAudioExportFormat]; + e->saveAudio(path.c_str(),audioExportOptions,ef.fileExt); totalFiles=0; e->getTotalAudioFiles(totalFiles); diff --git a/src/main.cpp b/src/main.cpp index 64c8880521..b1990d38cd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -913,7 +913,7 @@ int main(int argc, char** argv) { } if (outName!="") { e.setConsoleMode(true); - e.saveAudio(outName.c_str(),exportOptions); + e.saveAudio(outName.c_str(),exportOptions,"wav"); e.waitAudioFile(); } if (romOutName!="") { diff --git a/src/stringutils.cpp b/src/stringutils.cpp new file mode 100644 index 0000000000..f9df9e55fb --- /dev/null +++ b/src/stringutils.cpp @@ -0,0 +1,45 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 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 "stringutils.h" + +String lowerCaseCopy(const char* str) { + String lowerCase=str; + for (char& i: lowerCase) { + if (i>='A' && i<='Z') i+='a'-'A'; + } + return lowerCase; +} + +void removeFileExt(String& str, const char* ext) { + String lowerCase=lowerCaseCopy(str.c_str()); + + String extLowerCase; + if (ext[0]=='.') { + extLowerCase=lowerCaseCopy(ext); + } else { + extLowerCase="."; + extLowerCase+=lowerCaseCopy(ext); + } + + size_t extPos=lowerCase.rfind(extLowerCase); + if (extPos!=String::npos) { + str=str.substr(0,extPos); + } +} diff --git a/src/stringutils.h b/src/stringutils.h new file mode 100644 index 0000000000..a4ac5ac6a2 --- /dev/null +++ b/src/stringutils.h @@ -0,0 +1,31 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 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 _STRINGUTILS_H +#define _STRINGUTILS_H +#include "ta-utils.h" + +// returns a lowercase copy of an ASCII string +String lowerCaseCopy(const char* str); + +// removes an extension off the end of a string +// the leading dot in `ext` is optional (e.g., both `.wav` and `wav` are accepted) +void removeFileExt(String& str, const char* ext); + +#endif diff --git a/src/subprocess.cpp b/src/subprocess.cpp index 893dfc7e98..29c9daa81d 100644 --- a/src/subprocess.cpp +++ b/src/subprocess.cpp @@ -19,6 +19,7 @@ #include #include "subprocess.h" +#include "ta-log.h" #define tryMakePipe(_arrVar) \ int _arrVar[2]; \ @@ -27,21 +28,27 @@ return -1; \ } -Subprocess::Pipe::~Pipe() { - // FIXME: should we close both ends of the pipe? - if (writeFd!=-1) - close(writeFd); - if (readFd!=-1) - close(readFd); -} - Subprocess::Subprocess(std::vector args): args(args) {} +Subprocess::~Subprocess() { + logD("closing pipes!"); + const auto closePipe = [](Subprocess::Pipe& p){ + if (p.readFd!=-1) + close(p.readFd); + if (p.writeFd!=-1) + close(p.writeFd); + }; + closePipe(stdinPipe); + closePipe(stdoutPipe); + closePipe(stderrPipe); +} + int Subprocess::pipeStdin() { tryMakePipe(arr); stdinPipe=Subprocess::Pipe(arr); + logD("made pipe, fds (%d,%d) ~ (%d,%d)\n",arr[0],arr[1],stdinPipe.writeFd,stdinPipe.readFd); return stdinPipe.writeFd; } diff --git a/src/subprocess.h b/src/subprocess.h index 56c2cb8e0a..c2850ef0ab 100644 --- a/src/subprocess.h +++ b/src/subprocess.h @@ -29,7 +29,6 @@ class Subprocess { int writeFd; Pipe(): readFd(-1), writeFd(-1) {} Pipe(int pipeArr[2]): readFd(pipeArr[0]), writeFd(pipeArr[1]) {} - ~Pipe(); }; private: @@ -40,6 +39,7 @@ class Subprocess { public: Subprocess(std::vector args); + ~Subprocess(); // enables stdin piping, and returns a file descriptor to the writable end // should only be called before start()