From 7858cec33054766751d946310852b3cea0312ab4 Mon Sep 17 00:00:00 2001 From: bnnm Date: Sun, 15 Sep 2019 15:47:41 +0200 Subject: [PATCH] Allow virtual .txtp in playlists (foobar/winamp/cli) for config/tagging Makes plugins' STREAMFILEs not throw an error on non-existing .txtp (normally written inside .m3u) treating it like a command/url-ish name. This lets the TXTP parser to try to opening the virtual file with commands, as 0-size TXTP with a formatted name is valid, like "file.adx#l 1.0.txtp" ("play file.adx with 1 loop"), to allow quick per-file config. --- README.md | 40 ++++++++++++++++++++++++++++++++++++++ fb2k/foo_streamfile.cpp | 43 +++++++++++++++++++++++++++-------------- fb2k/foo_vgmstream.cpp | 22 ++++++++++++++------- src/streamfile.c | 26 +++++++++++++++++-------- src/vgmstream.c | 17 ++++++++++++++++ src/vgmstream.h | 3 +++ winamp/in_vgmstream.c | 19 +++++++++++------- 7 files changed, 133 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 007f6356..45a5cba7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Help and newest builds can be found here: https://www.hcs64.com/ Latest development is usually here: https://github.com/losnoco/vgmstream/ +You can find further info about other details in https://github.com/losnoco/vgmstream/tree/master/doc + ## Needed extra files (for Windows) Support for some codecs (Ogg Vorbis, MPEG audio, etc) is done with external libraries, so you will need to have certain DLL files. @@ -307,6 +309,44 @@ named and that filenames inside match the song filename. For Winamp you need to make sure options > titles > advanced title formatting checkbox is set and the format defined. +## Virtual TXTP files +Some of vgmstream's plugins allow you to use virtual .txtp files, that combined +with playlists let you make quick song configs. + +Normally you can create a physical .txtp file that points to another file with +config, and .txtp have a "mini-txtp" mode that configures files with only the +filename. + +Instead of manually creating .txtp files you can put non-existing virtual .txtp +in a `.m3u` playlist: +``` +# playlist that opens subsongs directly without having to create .txtp +# notice the full filename, then #(config), then ".txtp" (spaces are optional) +bank_bgm_full.nub #s1 .txtp +bank_bgm_full.nub #s10 .txtp +``` + +Combine with tagging (see above) for extra fun OST-like config. +``` +# @ALBUM GOD HAND + +# play 1 loop, delay and do a longer fade +# %TITLE Too Hot !! +circus_a_mix_ver2.adx #l 1.0 #d 5.0 #f 15.0 .txtp + +# play 1 loop instead of the default 2 then fade with the song's internal fading +# %TITLE Yet... Oh see mind +boss2_3ningumi_ver6.adx #l 1.0 #F .txtp + +... +``` + +You can also use it in CLI for quick access to some txtp-exclusive functions: +``` +# force change sample rate to 22050 +test.exe btl_koopa1_44k_lp.brstm #h22050.txtp -o btl_koopa1_44k_lp.wav +``` + ## Supported codec types Quick list of codecs vgmstream supports, including many obscure ones that diff --git a/fb2k/foo_streamfile.cpp b/fb2k/foo_streamfile.cpp index 3235cdb0..969e15f3 100644 --- a/fb2k/foo_streamfile.cpp +++ b/fb2k/foo_streamfile.cpp @@ -20,6 +20,7 @@ extern "C" { typedef struct { STREAMFILE sf; /* callbacks */ + bool m_file_opened; /* if foobar IO service opened the file */ service_ptr_t m_file; /* foobar IO service */ abort_callback * p_abort; /* foobar error stuff */ char * name; /* IO filename */ @@ -32,12 +33,12 @@ typedef struct { } FOO_STREAMFILE; static STREAMFILE * open_foo_streamfile_buffer(const char * const filename, size_t buffersize, abort_callback * p_abort, t_filestats * stats); -static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t m_file,const char * const filename, size_t buffersize, abort_callback * p_abort); +static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t m_file, bool m_file_opened, const char * const filename, size_t buffersize, abort_callback * p_abort); static size_t read_foo(FOO_STREAMFILE *streamfile, uint8_t * dest, off_t offset, size_t length) { size_t length_read_total = 0; - if (!streamfile || !dest || length <= 0 || offset < 0) + if (!streamfile || !streamfile->m_file_opened || !dest || length <= 0 || offset < 0) return 0; /* is the part of the requested length in the buffer? */ @@ -124,7 +125,8 @@ static void get_name_foo(FOO_STREAMFILE *streamfile,char *buffer,size_t length) } } static void close_foo(FOO_STREAMFILE * streamfile) { - streamfile->m_file.release(); + if (streamfile->m_file_opened) + streamfile->m_file.release(); free(streamfile->name); free(streamfile->buffer); free(streamfile); @@ -139,22 +141,22 @@ static STREAMFILE *open_foo(FOO_STREAMFILE *streamFile,const char * const filena return NULL; // if same name, duplicate the file pointer we already have open - if (!strcmp(streamFile->name,filename)) { - m_file = streamFile->m_file; + if (streamFile->m_file_opened && !strcmp(streamFile->name,filename)) { + m_file = streamFile->m_file; //copy? { - newstreamFile = open_foo_streamfile_buffer_by_file(m_file,filename,buffersize,streamFile->p_abort); + newstreamFile = open_foo_streamfile_buffer_by_file(m_file, streamFile->m_file_opened, filename, buffersize, streamFile->p_abort); if (newstreamFile) { return newstreamFile; } // failure, close it and try the default path (which will probably fail a second time) } } - // a normal open, open a new file + // a normal open, open a new file return open_foo_streamfile_buffer(filename,buffersize,streamFile->p_abort,NULL); } -static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t m_file,const char * const filename, size_t buffersize, abort_callback * p_abort) { +static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t m_file, bool m_file_opened, const char * const filename, size_t buffersize, abort_callback * p_abort) { uint8_t * buffer; FOO_STREAMFILE * streamfile; @@ -171,6 +173,7 @@ static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t m_fil streamfile->sf.open = (_STREAMFILE *(__cdecl *)(_STREAMFILE *,const char *const ,size_t)) open_foo; streamfile->sf.close = (void (__cdecl *)(_STREAMFILE *)) close_foo; + streamfile->m_file_opened = m_file_opened; streamfile->m_file = m_file; streamfile->p_abort = p_abort; streamfile->buffersize = buffersize; @@ -180,7 +183,10 @@ static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t m_fil if (!streamfile->name) goto fail; /* cache filesize */ - streamfile->filesize = streamfile->m_file->get_size(*streamfile->p_abort); + if (streamfile->m_file_opened) + streamfile->filesize = streamfile->m_file->get_size(*streamfile->p_abort); + else + streamfile->filesize = 0; return &streamfile->sf; @@ -193,16 +199,23 @@ fail: static STREAMFILE * open_foo_streamfile_buffer(const char * const filename, size_t buffersize, abort_callback * p_abort, t_filestats * stats) { STREAMFILE *streamFile; service_ptr_t infile; + bool infile_exists; - if(!(filesystem::g_exists(filename, *p_abort))) - return NULL; + infile_exists = filesystem::g_exists(filename, *p_abort); + if(!infile_exists) { + /* allow non-existing files in some cases */ + if (!vgmstream_is_virtual_filename(filename)) + return NULL; + } - filesystem::g_open_read(infile,filename,*p_abort); - if(stats) *stats = infile->get_stats(*p_abort); + if (infile_exists) { + filesystem::g_open_read(infile,filename,*p_abort); + if(stats) *stats = infile->get_stats(*p_abort); + } - streamFile = open_foo_streamfile_buffer_by_file(infile,filename,buffersize,p_abort); + streamFile = open_foo_streamfile_buffer_by_file(infile, infile_exists, filename, buffersize, p_abort); if (!streamFile) { - // fclose(infile); + //m_file.release(); //todo not needed after g_open_read? } return streamFile; diff --git a/fb2k/foo_vgmstream.cpp b/fb2k/foo_vgmstream.cpp index 92130473..f21a1dfc 100644 --- a/fb2k/foo_vgmstream.cpp +++ b/fb2k/foo_vgmstream.cpp @@ -73,20 +73,28 @@ input_vgmstream::~input_vgmstream() { } // called first when a new file is opened -void input_vgmstream::open(service_ptr_t p_filehint,const char * p_path,t_input_open_reason p_reason,abort_callback & p_abort) { +void input_vgmstream::open(service_ptr_t p_filehint, const char * p_path,t_input_open_reason p_reason,abort_callback & p_abort) { if (!p_path) { // shouldn't be possible throw exception_io_data(); - return; + return; //??? } filename = p_path; + // allow non-existing files in some cases + bool infile_virtual = !filesystem::g_exists(p_path, p_abort) + && vgmstream_is_virtual_filename(filename) == 1; - // keep file stats around (timestamp, filesize) - if ( p_filehint.is_empty() ) - input_open_file_helper( p_filehint, filename, p_reason, p_abort ); - stats = p_filehint->get_stats( p_abort ); + // don't try to open virtual files as it'll fail + // (doesn't seem to have any adverse effect, except maybe no stats) + // setup_vgmstream also makes further checks before file is finally opened + if (!infile_virtual) { + // keep file stats around (timestamp, filesize) + if ( p_filehint.is_empty() ) + input_open_file_helper( p_filehint, filename, p_reason, p_abort ); + stats = p_filehint->get_stats( p_abort ); + } switch(p_reason) { case input_open_decode: // prepare to retrieve info and decode @@ -169,7 +177,7 @@ void input_vgmstream::get_info(t_uint32 p_subsong, file_info & p_info, abort_cal strcpy(tagfile_path,tagfile_name); } - STREAMFILE *tagFile = open_foo_streamfile(tagfile_path, &p_abort, &stats); + STREAMFILE *tagFile = open_foo_streamfile(tagfile_path, &p_abort, NULL); if (tagFile != NULL) { VGMSTREAM_TAGS *tags; const char *tag_key, *tag_val; diff --git a/src/streamfile.c b/src/streamfile.c index 4c997f48..46d2c789 100644 --- a/src/streamfile.c +++ b/src/streamfile.c @@ -26,7 +26,7 @@ static STREAMFILE * open_stdio_streamfile_buffer_by_file(FILE *infile,const char static size_t read_stdio(STDIOSTREAMFILE *streamfile,uint8_t * dest, off_t offset, size_t length) { size_t length_read_total = 0; - if (!streamfile || !dest || length <= 0 || offset < 0) + if (!streamfile || !streamfile->infile || !dest || length <= 0 || offset < 0) return 0; /* is the part of the requested length in the buffer? */ @@ -110,7 +110,8 @@ static void get_name_stdio(STDIOSTREAMFILE *streamfile,char *buffer,size_t lengt buffer[length-1]='\0'; } static void close_stdio(STDIOSTREAMFILE * streamfile) { - fclose(streamfile->infile); + if (streamfile->infile) + fclose(streamfile->infile); free(streamfile->buffer); free(streamfile); } @@ -124,7 +125,7 @@ static STREAMFILE *open_stdio(STDIOSTREAMFILE *streamFile,const char * const fil return NULL; #if !defined (__ANDROID__) // if same name, duplicate the file pointer we already have open - if (!strcmp(streamFile->name,filename)) { + if (streamFile->infile && !strcmp(streamFile->name,filename)) { if (((newfd = dup(fileno(streamFile->infile))) >= 0) && (newfile = fdopen( newfd, "rb" ))) { @@ -141,7 +142,7 @@ static STREAMFILE *open_stdio(STDIOSTREAMFILE *streamFile,const char * const fil return open_stdio_streamfile_buffer(filename,buffersize); } -static STREAMFILE * open_stdio_streamfile_buffer_by_file(FILE *infile,const char * const filename, size_t buffersize) { +static STREAMFILE * open_stdio_streamfile_buffer_by_file(FILE *infile, const char * const filename, size_t buffersize) { uint8_t * buffer = NULL; STDIOSTREAMFILE * streamfile = NULL; @@ -166,8 +167,13 @@ static STREAMFILE * open_stdio_streamfile_buffer_by_file(FILE *infile,const char streamfile->name[sizeof(streamfile->name)-1] = '\0'; /* cache filesize */ - fseeko(streamfile->infile,0,SEEK_END); - streamfile->filesize = ftello(streamfile->infile); + if (infile) { + fseeko(streamfile->infile,0,SEEK_END); + streamfile->filesize = ftello(streamfile->infile); + } + else { + streamfile->filesize = 0; /* allow virtual, non-existing files */ + } /* Typically fseek(o)/ftell(o) may only handle up to ~2.14GB, signed 32b = 0x7FFFFFFF * (happens in banks like FSB, though rarely). Can be remedied with the @@ -190,11 +196,15 @@ static STREAMFILE * open_stdio_streamfile_buffer(const char * const filename, si STREAMFILE *streamFile; infile = fopen(filename,"rb"); - if (!infile) return NULL; + if (!infile) { + /* allow non-existing files in some cases */ + if (!vgmstream_is_virtual_filename(filename)) + return NULL; + } streamFile = open_stdio_streamfile_buffer_by_file(infile,filename,buffersize); if (!streamFile) { - fclose(infile); + if (infile) fclose(infile); } return streamFile; diff --git a/src/vgmstream.c b/src/vgmstream.c index 6b728908..a299afd5 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -2882,3 +2882,20 @@ fail: /* open streams will be closed in close_vgmstream(), hopefully called by the meta */ return 0; } + +int vgmstream_is_virtual_filename(const char* filename) { + int len = strlen(filename); + if (len < 6) + return 0; + + /* vgmstream can play .txtp files that have size 0 but point to another file with config + * based only in the filename (ex. "file.fsb #2.txtp" plays 2nd subsong of file.fsb). + * + * Also, .m3u playlist can include files that don't exist, and players often allow filenames + * pointing to nothing (since could be some protocol/url). + * + * Plugins can use both quirks to allow "virtual files" (.txtp) in .m3u that don't need + * to exist but allow config. Plugins with this function if the filename is virtual, + * and their STREAMFILEs should be modified as to ignore null FILEs and report size 0. */ + return strcmp(&filename[len-5], ".txtp") == 0; +} diff --git a/src/vgmstream.h b/src/vgmstream.h index 7b79d3ed..f095102a 100644 --- a/src/vgmstream.h +++ b/src/vgmstream.h @@ -1336,6 +1336,9 @@ void vgmstream_force_loop(VGMSTREAM* vgmstream, int loop_flag, int loop_start_sa /* Set number of max loops to do, then play up to stream end (for songs with proper endings) */ void vgmstream_set_loop_target(VGMSTREAM* vgmstream, int loop_target); +/* Return 1 if vgmstream detects from the filename that said file can be used even if doesn't physically exist */ +int vgmstream_is_virtual_filename(const char* filename); + /* -------------------------------------------------------------------------*/ /* vgmstream "private" API */ /* -------------------------------------------------------------------------*/ diff --git a/winamp/in_vgmstream.c b/winamp/in_vgmstream.c index 964cf3fb..aa060aa3 100644 --- a/winamp/in_vgmstream.c +++ b/winamp/in_vgmstream.c @@ -210,7 +210,7 @@ static FILE* wa_fdopen(int fd) { typedef struct { STREAMFILE sf; STREAMFILE *stdiosf; - FILE *infile_ref; /* pointer to the infile in stdiosf */ + FILE *infile_ref; /* pointer to the infile in stdiosf (partially handled by stdiosf) */ } WINAMP_STREAMFILE; static STREAMFILE *open_winamp_streamfile_by_file(FILE *infile, const char * path); @@ -244,7 +244,7 @@ static STREAMFILE *wasf_open(WINAMP_STREAMFILE *streamFile, const char *const fi /* if same name, duplicate the file pointer we already have open */ //unsure if all this is needed streamFile->stdiosf->get_name(streamFile->stdiosf, name, PATH_LIMIT); - if (!strcmp(name,filename)) { + if (streamFile->infile_ref && !strcmp(name,filename)) { if (((newfd = dup(fileno(streamFile->infile_ref))) >= 0) && (newfile = wa_fdopen(newfd))) { @@ -275,7 +275,7 @@ static STREAMFILE *open_winamp_streamfile_by_file(FILE *infile, const char * pat this_sf = calloc(1,sizeof(WINAMP_STREAMFILE)); if (!this_sf) goto fail; - stdiosf = open_stdio_streamfile_by_file(infile,path); + stdiosf = open_stdio_streamfile_by_file(infile, path); if (!stdiosf) goto fail; this_sf->sf.read = (void*)wasf_read; @@ -302,16 +302,21 @@ static STREAMFILE *open_winamp_streamfile_by_ipath(const in_char *wpath) { STREAMFILE *streamFile; char path[PATH_LIMIT]; - /* open a FILE from a Winamp (possibly UTF-16) path */ - infile = wa_fopen(wpath); - if (!infile) return NULL; /* convert to UTF-8 if needed for internal use */ wa_ichar_to_char(path,PATH_LIMIT, wpath); + /* open a FILE from a Winamp (possibly UTF-16) path */ + infile = wa_fopen(wpath); + if (!infile) { + /* allow non-existing files in some cases */ + if (!vgmstream_is_virtual_filename(path)) + return NULL; + } + streamFile = open_winamp_streamfile_by_file(infile,path); if (!streamFile) { - fclose(infile); + if (infile) fclose(infile); } return streamFile;