Merge pull request #242 from bnnm/str-mus-exts

STR, MUS, exts
This commit is contained in:
Christopher Snowhill 2018-06-30 14:03:42 -07:00 committed by GitHub
commit 869ca7127a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 169 additions and 88 deletions

View File

@ -21,6 +21,7 @@ extern "C" {
#ifndef VERSION #ifndef VERSION
#include "../version.h" #include "../version.h"
#endif #endif
#ifndef VERSION #ifndef VERSION
#define PLUGIN_VERSION __DATE__ #define PLUGIN_VERSION __DATE__
#else #else
@ -145,6 +146,8 @@ void input_vgmstream::get_info(t_uint32 p_subsong, file_info & p_info, abort_cal
p_info.meta_set("TITLE",temp); p_info.meta_set("TITLE",temp);
} }
p_info.info_set("vgmstream version",PLUGIN_VERSION);
p_info.info_set_int("samplerate", samplerate); p_info.info_set_int("samplerate", samplerate);
p_info.info_set_int("channels", channels); p_info.info_set_int("channels", channels);
p_info.info_set_int("bitspersample",16); p_info.info_set_int("bitspersample",16);

View File

@ -1,13 +1,16 @@
#include "vgmstream.h" #include "vgmstream.h"
//#define VGM_REGISTER_TYPE(extension) ...
//#define VGM_REGISTER_TYPE_COMMON(extension) ... /* for common extensions like aiff */
/* defines the list of accepted extensions. vgmstream doesn't use it internally so it's here
* to inform plugins that need it. Common extensions are commented out to avoid stealing them. */
/* some extensions could be #ifdef but no really needed */ /* some extensions require external libraries and could be #ifdef, no really needed */
/* some formats marked as "not parsed" mean they'll go through FFmpeg, the header/extension is not parsed */ /* some formats marked as "not parsed" mean they'll go through FFmpeg, the header/extension is not parsed */
static const char* extension_list[] = { static const char* extension_list[] = {
//"", /* vgmstream can plays extensionless files too, but plugins must accept them manually */
"04sw", "04sw",
"2dx9", "2dx9",
"2pfs", "2pfs",
@ -38,6 +41,7 @@ static const char* extension_list[] = {
"al2", "al2",
"amts", //fake extension/header id for .stm (to be removed) "amts", //fake extension/header id for .stm (to be removed)
"ao", //txth/reserved [Cloudphobia (PC)] "ao", //txth/reserved [Cloudphobia (PC)]
"apc", //txth/reserved [MegaRace 3 (PC)]
"as4", "as4",
"asd", "asd",
"asf", "asf",
@ -367,6 +371,7 @@ static const char* extension_list[] = {
"v0", "v0",
//"v1", //dual channel with v0 //"v1", //dual channel with v0
"vag", "vag",
"vai", //txth/reserved [Ratatouille (GC)]
"vas", "vas",
"vawx", "vawx",
"vb", "vb",
@ -435,7 +440,7 @@ static const char* extension_list[] = {
"zsd", "zsd",
"zwdsp", "zwdsp",
"vgmstream" "vgmstream" /* fake extension, catch-all for FFmpeg/txth/etc */
//, NULL //end mark //, NULL //end mark
}; };
@ -907,7 +912,7 @@ static const meta_info meta_info_list[] = {
{meta_X360_TRA, "Terminal Reality .TRA raw header"}, {meta_X360_TRA, "Terminal Reality .TRA raw header"},
{meta_PS2_VGS, "Princess Soft VGS header"}, {meta_PS2_VGS, "Princess Soft VGS header"},
{meta_PS2_IAB, "Runtime .IAB header"}, {meta_PS2_IAB, "Runtime .IAB header"},
{meta_PS2_STRLR, "STR L/R header"}, {meta_PS2_STRLR, "The Bouncer STR header"},
{meta_LSF_N1NJ4N, ".lsf !n1nj4n header"}, {meta_LSF_N1NJ4N, ".lsf !n1nj4n header"},
{meta_VAWX, "feelplus VAWX header"}, {meta_VAWX, "feelplus VAWX header"},
{meta_PC_SNDS, "assumed Heavy Iron IMA by .snds extension"}, {meta_PC_SNDS, "assumed Heavy Iron IMA by .snds extension"},
@ -1023,6 +1028,7 @@ static const meta_info meta_info_list[] = {
{meta_OGG_GWM, "Ogg Vorbis (GWM header)"}, {meta_OGG_GWM, "Ogg Vorbis (GWM header)"},
{meta_DSP_SADF, "Procyon Studio SADF header"}, {meta_DSP_SADF, "Procyon Studio SADF header"},
{meta_H4M, "Hudson HVQM4 header"}, {meta_H4M, "Hudson HVQM4 header"},
{meta_OGG_MUS, "Ogg Vorbis (MUS header)"},
#ifdef VGM_USE_FFMPEG #ifdef VGM_USE_FFMPEG
{meta_FFmpeg, "FFmpeg supported file format"}, {meta_FFmpeg, "FFmpeg supported file format"},

View File

@ -1,19 +1,17 @@
#include "layout.h" #include "layout.h"
#include "../vgmstream.h" #include "../vgmstream.h"
/* set up for the block at the given offset */ /* The Bouncer STRx blocks, one block per channel when stereo */
void block_update_ps2_strlr(off_t block_offset, VGMSTREAM * vgmstream) { void block_update_ps2_strlr(off_t block_offset, VGMSTREAM * vgmstream) {
STREAMFILE* streamFile = vgmstream->ch[0].streamfile;
int i; int i;
vgmstream->current_block_offset = block_offset; vgmstream->current_block_offset = block_offset;
vgmstream->current_block_size = read_32bitLE( vgmstream->current_block_size = read_32bitLE(block_offset+0x04,streamFile); /* can be smaller than 0x800 */
vgmstream->current_block_offset+0x4, vgmstream->next_block_offset = block_offset + 0x800*vgmstream->channels;
vgmstream->ch[0].streamfile)*2; /* 0x08: number of remaning blocks, 0x10: some id/size? (shared in all blocks) */
vgmstream->next_block_offset = vgmstream->current_block_offset+vgmstream->current_block_size+0x40;
//vgmstream->current_block_size/=vgmstream->channels;
for (i=0;i<vgmstream->channels;i++) {
vgmstream->ch[i].offset = vgmstream->current_block_offset+0x20+(0x800*i);
for (i = 0; i < vgmstream->channels; i++) {
vgmstream->ch[i].offset = block_offset + 0x20 + 0x800*i;
} }
} }

View File

@ -240,6 +240,12 @@ static const hcakey_info hcakey_list[] = {
// PriPara: All Idol Perfect Stage (Takara Tomy) [Switch] // PriPara: All Idol Perfect Stage (Takara Tomy) [Switch]
{217735759}, // 000000000CFA624F {217735759}, // 000000000CFA624F
// Space Invaders Extreme (Taito Corporation, Backbone Entertainment) [PC]
{91380310056}, // 0000001546B0E028
// CR Another God Hades Advent (Universal Entertainment Corporation) [iOS/Android]
{64813795}, // 0000000003DCFAE3
}; };
#endif/*_HCA_KEYS_H_*/ #endif/*_HCA_KEYS_H_*/

View File

@ -226,6 +226,28 @@ static void gwm_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, vo
} }
} }
static void mus_ogg_decryption_callback(void *ptr, size_t size, size_t nmemb, void *datasource) {
static const uint8_t key[16] = {
0x21,0x4D,0x6F,0x01,0x20,0x4C,0x6E,0x02,0x1F,0x4B,0x6D,0x03,0x20,0x4C,0x6E,0x02
};
size_t bytes_read = size*nmemb;
ogg_vorbis_streamfile * const ov_streamfile = datasource;
int i;
char *header_id = "OggS";
/* bytes are xor'd with key, first "OggS" is changed */
for (i = 0; i < bytes_read; i++) {
if (ov_streamfile->offset+i < 0x04) { /* if decrypted gives "Mus " */
((uint8_t*)ptr)[i] = (uint8_t)header_id[(ov_streamfile->offset + i) % 4];
}
else {
((uint8_t*)ptr)[i] ^= key[(ov_streamfile->offset + i) % sizeof(key)];
}
}
}
/* Ogg Vorbis, by way of libvorbisfile; may contain loop comments */ /* Ogg Vorbis, by way of libvorbisfile; may contain loop comments */
VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) { VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
ogg_vorbis_meta_info_t ovmi = {0}; ogg_vorbis_meta_info_t ovmi = {0};
@ -239,6 +261,7 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
int is_rpgmvo = 0; int is_rpgmvo = 0;
int is_eno = 0; int is_eno = 0;
int is_gwm = 0; int is_gwm = 0;
int is_mus = 0;
/* check extension */ /* check extension */
@ -262,6 +285,8 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
is_eno = 1; is_eno = 1;
} else if (check_extensions(streamFile,"gwm")) { /* .gwm: Adagio: Cloudburst (PC) */ } else if (check_extensions(streamFile,"gwm")) { /* .gwm: Adagio: Cloudburst (PC) */
is_gwm = 1; is_gwm = 1;
} else if (check_extensions(streamFile,"mus")) { /* .mus: Redux - Dark Matters (PC) */
is_mus = 1;
} else { } else {
goto fail; goto fail;
} }
@ -355,7 +380,6 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
start_offset = 0x01; start_offset = 0x01;
} }
/* check GWM [Adagio: Cloudburst (PC)], encrypted */ /* check GWM [Adagio: Cloudburst (PC)], encrypted */
if (is_gwm) { if (is_gwm) {
ovmi.xor_value = 0x5D; ovmi.xor_value = 0x5D;
@ -363,6 +387,12 @@ VGMSTREAM * init_vgmstream_ogg_vorbis(STREAMFILE *streamFile) {
ovmi.meta_type = meta_OGG_GWM; ovmi.meta_type = meta_OGG_GWM;
} }
/* check .mus [Redux - Dark Matters (PC)], encrypted */
if (is_mus) {
ovmi.decryption_callback = mus_ogg_decryption_callback;
ovmi.meta_type = meta_OGG_MUS;
}
return init_vgmstream_ogg_vorbis_callbacks(streamFile, NULL, start_offset, &ovmi); return init_vgmstream_ogg_vorbis_callbacks(streamFile, NULL, start_offset, &ovmi);

View File

@ -5,17 +5,14 @@
VGMSTREAM * init_vgmstream_ps2_joe(STREAMFILE *streamFile) { VGMSTREAM * init_vgmstream_ps2_joe(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL; VGMSTREAM * vgmstream = NULL;
off_t start_offset; off_t start_offset;
int channel_count, loop_flag, sample_rate, num_samples;
size_t file_size, data_size, unknown1, unknown2, interleave; size_t file_size, data_size, unknown1, unknown2, interleave;
int loop_flag;
int channel_count;
/* checks */ /* checks */
if (!check_extensions(streamFile, "joe")) if (!check_extensions(streamFile, "joe"))
goto fail; goto fail;
loop_flag = 1;
channel_count = 2;
file_size = get_streamfile_size(streamFile); file_size = get_streamfile_size(streamFile);
data_size = read_32bitLE(0x04,streamFile); data_size = read_32bitLE(0x04,streamFile);
unknown1 = read_32bitLE(0x08,streamFile); unknown1 = read_32bitLE(0x08,streamFile);
@ -48,14 +45,20 @@ VGMSTREAM * init_vgmstream_ps2_joe(STREAMFILE *streamFile) {
start_offset = file_size - data_size; start_offset = file_size - data_size;
channel_count = 2;
sample_rate = read_32bitLE(0x00,streamFile);
num_samples = ps_bytes_to_samples(data_size, channel_count);
/* most songs simply repeat except a few jingles (PS-ADPCM flags are always set) */
loop_flag = (num_samples > 20*sample_rate); /* in seconds */
/* build the VGMSTREAM */ /* build the VGMSTREAM */
vgmstream = allocate_vgmstream(channel_count,loop_flag); vgmstream = allocate_vgmstream(channel_count,loop_flag);
if (!vgmstream) goto fail; if (!vgmstream) goto fail;
vgmstream->sample_rate = read_32bitLE(0x00,streamFile); vgmstream->sample_rate = sample_rate;
vgmstream->coding_type = coding_PSX; vgmstream->num_samples = num_samples;
vgmstream->num_samples = ps_bytes_to_samples(data_size, channel_count);
//todo improve, not working 100% with early .joe //todo improve, not working 100% with early .joe
{ {
@ -103,6 +106,7 @@ VGMSTREAM * init_vgmstream_ps2_joe(STREAMFILE *streamFile) {
} }
} }
vgmstream->coding_type = coding_PSX;
vgmstream->layout_type = layout_interleave; vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave; vgmstream->interleave_block_size = interleave;
vgmstream->meta_type = meta_PS2_JOE; vgmstream->meta_type = meta_PS2_JOE;

View File

@ -1,80 +1,57 @@
#include "meta.h" #include "meta.h"
#include "../layout/layout.h" #include "../layout/layout.h"
#include "../util.h" #include "../coding/coding.h"
/* STR: The Bouncer (PS2) */ /* STR - The Bouncer (PS2) */
VGMSTREAM * init_vgmstream_ps2_strlr(STREAMFILE *streamFile) { VGMSTREAM * init_vgmstream_ps2_strlr(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL; VGMSTREAM * vgmstream = NULL;
char filename[PATH_LIMIT]; int channel_count, loop_flag;
int loop_flag = 0;
int channel_count;
int i;
off_t start_offset; off_t start_offset;
/* check extension, case insensitive */
streamFile->get_name(streamFile,filename,sizeof(filename));
if (strcasecmp("str",filename_extension(filename))) goto fail;
#if 0 /* checks */
/* .vs: real extension (from .nam container) , .str: partial header id */
if (!check_extensions(streamFile, "vs,str"))
goto fail;
/* check header */ /* check header */
if (read_32bitBE(0x00,streamFile) != 0x5354524C) /* "STRL" */ if (!(read_32bitBE(0x000,streamFile) == 0x5354524C && /* "STRL" */
read_32bitBE(0x800,streamFile) == 0x53545252) && /* "STRR" */
read_32bitBE(0x00,streamFile) != 0x5354524D) /* "STRM" */
goto fail; goto fail;
if (read_32bitBE(0x00,streamFile) != 0x53545252) /* "STRR" */
goto fail;
#endif
/* don't hijack Sonic & Sega All Stars Racing X360 (xma) */
if (read_32bitBE(0x00,streamFile) == 0x52494646)
goto fail; /* "RIFF"*/
/* don't hijack Mad Dash Racing (Xbox) */
if (read_32bitLE(0x0c,streamFile) == 1
&& read_32bitLE(0x010,streamFile) == 0
&& read_32bitLE(0x400,streamFile) == 0
&& read_32bitLE(0x7f0,streamFile) == 0)
goto fail;
loop_flag = 0; loop_flag = 0;
channel_count = 2; channel_count = (read_32bitBE(0x00,streamFile) == 0x5354524D) ? 1 : 2; /* "STRM"=mono (voices) */
start_offset = 0x00;
/* build the VGMSTREAM */ /* build the VGMSTREAM */
vgmstream = allocate_vgmstream(channel_count,loop_flag); vgmstream = allocate_vgmstream(channel_count,loop_flag);
if (!vgmstream) goto fail; if (!vgmstream) goto fail;
/* fill in the vital statistics */ vgmstream->sample_rate = 44100;
start_offset = 0x0;
vgmstream->channels = channel_count;
vgmstream->sample_rate = 48000;
vgmstream->coding_type = coding_PSX; vgmstream->coding_type = coding_PSX;
vgmstream->layout_type = layout_blocked_ps2_strlr; vgmstream->layout_type = layout_blocked_ps2_strlr;
//vgmstream->interleave_block_size = read_32bitLE(0xC, streamFile);
vgmstream->meta_type = meta_PS2_STRLR; vgmstream->meta_type = meta_PS2_STRLR;
/* open the file for reading by each channel */ if (!vgmstream_open_stream(vgmstream,streamFile,start_offset))
goto fail;
/* calc num_samples */
{ {
for (i=0;i<channel_count;i++) vgmstream->next_block_offset = start_offset;
{ do {
vgmstream->ch[i].streamfile = streamFile->open(streamFile, filename, 0x8000); block_update_ps2_strlr(vgmstream->next_block_offset,vgmstream);
if (!vgmstream->ch[i].streamfile) goto fail; vgmstream->num_samples += ps_bytes_to_samples(vgmstream->current_block_size, 1);
} }
while (vgmstream->next_block_offset < get_streamfile_size(streamFile));
} }
/* Calc num_samples */
block_update_ps2_strlr(start_offset, vgmstream); block_update_ps2_strlr(start_offset, vgmstream);
vgmstream->num_samples=0;
do
{
vgmstream->num_samples += vgmstream->current_block_size * 14 / 16;
block_update_ps2_strlr(vgmstream->next_block_offset, vgmstream);
} while (vgmstream->next_block_offset < get_streamfile_size(streamFile));
block_update_ps2_strlr(start_offset, vgmstream);
return vgmstream; return vgmstream;
/* clean up anything we may have opened */
fail: fail:
if (vgmstream) close_vgmstream(vgmstream); close_vgmstream(vgmstream);
return NULL; return NULL;
} }

View File

@ -313,6 +313,7 @@ static int parse_sb_header(ubi_sb_header * sb, STREAMFILE *streamFile) {
/* ignore non-audio entry (other types seem to have config data) */ /* ignore non-audio entry (other types seem to have config data) */
if (read_32bit(offset + 0x04, streamFile) != 0x01) if (read_32bit(offset + 0x04, streamFile) != 0x01)
continue; continue;
//;VGM_LOG("SB at %lx\n", offset);
/* weird case when there is no internal substream ID and just seem to rotate every time type changes, joy */ /* weird case when there is no internal substream ID and just seem to rotate every time type changes, joy */
if (sb->has_rotating_ids) { /* assumes certain configs can't happen in this case */ if (sb->has_rotating_ids) { /* assumes certain configs can't happen in this case */
@ -779,6 +780,23 @@ static int config_sb_header_version(ubi_sb_header * sb, STREAMFILE *streamFile)
return 1; return 1;
} }
#if 0
/* Far cry: Instincts - Evolution (2006)(Xbox) */
if (sb->version == 0x00170000 && is_sb2) {
sb->section1_entry_size = 0x48;
sb->section2_entry_size = 0x6c;
sb->external_flag_offset = 0;
sb->num_samples_offset = 0x28;
sb->stream_id_offset = 0;
sb->sample_rate_offset = 0x3c;
sb->channels_offset = 0x44;
sb->stream_type_offset = 0x48;
sb->extra_name_offset = 0x58;
return 1;
}
#endif
/* Prince of Persia: Rival Swords (2007)(PSP) */ /* Prince of Persia: Rival Swords (2007)(PSP) */
if (sb->version == 0x00180005 && is_sb5) { if (sb->version == 0x00180005 && is_sb5) {

View File

@ -2,16 +2,28 @@
#include "util.h" #include "util.h"
#include "streamtypes.h" #include "streamtypes.h"
const char * filename_extension(const char * filename) { const char * filename_extension(const char * pathname) {
const char * ext; const char * filename;
const char * extension;
/* You know what would be nice? strrchrnul(). /* get basename + extension */
* Instead I have to do it myself. */ filename = pathname;
ext = strrchr(filename,'.'); #if 0
if (ext==NULL) ext=filename+strlen(filename); /* point to null, i.e. an empty string for the extension */ //must detect empty extensions in folders with . in the name; not too important and DIR_SEPARATOR could improved
else ext=ext+1; /* skip the dot */ filename = strrchr(pathname, DIR_SEPARATOR);
if (filename == NULL)
filename = pathname; /* pathname has no separators (single filename) */
else
filename++; /* skip the separator */
#endif
return ext; extension = strrchr(filename,'.');
if (extension==NULL)
extension = filename+strlen(filename); /* point to null, i.e. an empty string for the extension */
else
extension++; /* skip the dot */
return extension;
} }
/* unused */ /* unused */

View File

@ -679,6 +679,7 @@ typedef enum {
meta_TA_AAC_VITA, /* tri-Ace AAC (Judas Code) */ meta_TA_AAC_VITA, /* tri-Ace AAC (Judas Code) */
meta_OGG_GWM, /* Ogg Vorbis with encryption [Metronomicon (PC)] */ meta_OGG_GWM, /* Ogg Vorbis with encryption [Metronomicon (PC)] */
meta_H4M, /* Hudson HVQM4 video [Resident Evil 0 (GC), Tales of Symphonia (GC)] */ meta_H4M, /* Hudson HVQM4 video [Resident Evil 0 (GC), Tales of Symphonia (GC)] */
meta_OGG_MUS, /* Ogg Vorbis with encryption [Redux - Dark Matters (PC)] */
#ifdef VGM_USE_FFMPEG #ifdef VGM_USE_FFMPEG
meta_FFmpeg, meta_FFmpeg,

View File

@ -43,7 +43,7 @@
In_Module input_module; In_Module input_module;
DWORD WINAPI __stdcall decode(void *arg); DWORD WINAPI __stdcall decode(void *arg);
/* Winamp Play extension list, needed to accept/play and associate extensions in Windows */ /* Winamp Play extension list, to accept and associate extensions in Windows */
#define EXTENSION_LIST_SIZE (0x2000 * 6) #define EXTENSION_LIST_SIZE (0x2000 * 6)
#define EXT_BUFFER_SIZE 200 #define EXT_BUFFER_SIZE 200
char working_extension_list[EXTENSION_LIST_SIZE] = {0}; char working_extension_list[EXTENSION_LIST_SIZE] = {0};
@ -88,6 +88,7 @@ in_char lastfn[PATH_LIMIT] = {0}; /* name of the currently playing file */
#define wa_strchr wcschr #define wa_strchr wcschr
#define wa_sscanf swscanf #define wa_sscanf swscanf
#define wa_snprintf _snwprintf #define wa_snprintf _snwprintf
#define wa_strrchr wcsrchr
#define wa_fileinfo fileinfoW #define wa_fileinfo fileinfoW
#define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAMEW #define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAMEW
#define wa_L(x) L ##x #define wa_L(x) L ##x
@ -99,6 +100,7 @@ in_char lastfn[PATH_LIMIT] = {0}; /* name of the currently playing file */
#define wa_strchr strchr #define wa_strchr strchr
#define wa_sscanf sscanf #define wa_sscanf sscanf
#define wa_snprintf snprintf #define wa_snprintf snprintf
#define wa_strrchr strrchr
#define wa_fileinfo fileinfo #define wa_fileinfo fileinfo
#define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAME #define wa_IPC_PE_INSERTFILENAME IPC_PE_INSERTFILENAME
#define wa_L(x) x #define wa_L(x) x
@ -777,7 +779,9 @@ static void add_extension(int length, char * dst, const char * ext) {
} }
/* Creates Winamp's extension list, a single string that ends with \0\0. /* Creates Winamp's extension list, a single string that ends with \0\0.
* Each extension must be in this format: "extension\0Description\0" */ * Each extension must be in this format: "extension\0Description\0"
* The list is used to accept extensions by default when IsOurFile returns 0, and to register file types.
* It could be ignored/empty and just detect in IsOurFile instead. */
static void build_extension_list() { static void build_extension_list() {
const char ** ext_list; const char ** ext_list;
size_t ext_list_len; size_t ext_list_len;
@ -871,7 +875,29 @@ void winamp_Quit() {
/* called before extension checks, to allow detection of mms://, etc */ /* called before extension checks, to allow detection of mms://, etc */
int winamp_IsOurFile(const in_char *fn) { int winamp_IsOurFile(const in_char *fn) {
return 0; /* we don't recognize protocols */ const in_char *filename;
const in_char *extension;
/* get basename + extension */
filename = fn;
#if 0
//must detect empty extensions in folders with . in the name; doesn't work ok?
filename = wa_strrchr(fn, wa_L('\\'));
if (filename == NULL)
filename = fn;
else
filename++;
#endif
extension = wa_strrchr(filename, wa_L('.'));
if (extension == NULL)
return 1; /* extensionless, try to play it */
else
extension++;
/* returning 0 here means it only accepts the extensions in working_extension_list */
/* it's possible to ignore the list and manually accept extensions, like foobar's g_is_our_path */
return 0;
} }
/* request to start playing a file */ /* request to start playing a file */