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.
This commit is contained in:
bnnm 2019-09-15 15:47:41 +02:00
parent 9571e5b11d
commit 7858cec330
7 changed files with 133 additions and 37 deletions

View File

@ -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

View File

@ -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<file> 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<file> m_file,const char * const filename, size_t buffersize, abort_callback * p_abort);
static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t<file> 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<file> m_file,const char * const filename, size_t buffersize, abort_callback * p_abort) {
static STREAMFILE * open_foo_streamfile_buffer_by_file(service_ptr_t<file> 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<file> 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<file> 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<file> 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;

View File

@ -73,20 +73,28 @@ input_vgmstream::~input_vgmstream() {
}
// called first when a new file is opened
void input_vgmstream::open(service_ptr_t<file> p_filehint,const char * p_path,t_input_open_reason p_reason,abort_callback & p_abort) {
void input_vgmstream::open(service_ptr_t<file> 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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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 */
/* -------------------------------------------------------------------------*/

View File

@ -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;