Merge pull request #1658 from bnnm/musx-misc

- Fix some .sfx MUSX sample rate [Ice Ace 3 (PC), G-Force (PS3)]
- Fix some .sch [Conflict: Desert Storm (PC)]
- Add ADX key
- Add extra .wave test codecs
This commit is contained in:
bnnm 2025-01-08 18:20:01 +01:00 committed by GitHub
commit 30cad540c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 235 additions and 173 deletions

View File

@ -83,23 +83,37 @@ class App(object):
if line.startswith('#'):
continue
if not line.endswith('.txtp'):
if self.args.force:
line += '.txtp'
else:
continue
line.replace('\\', '/')
line = line.replace('\\n', '\n') #may conflict with paths but...
line = line.replace('\\', '/')
subdir = self.args.subdir
if ':' in line:
index = line.find(':') #internal txtp : txtp name
# 'commands : name.txtp' format
index = line.find(':')
text = line[0:index].strip()
name = line[index+1:].strip()
# detect and allow 'name.txtp : commands' too
if text.endswith('.txtp') and not name.endswith('.txtp'):
temp = text
text = name
name = temp
if not name.endswith('.txtp'):
if self.args.force:
name += '.txtp'
else:
continue
elif self.args.maxitxtp or subdir:
if not line.endswith('.txtp'):
if self.args.force:
line += '.txtp'
else:
continue
index = line.find('.') #first extension
if line[index:].startswith('.txtp'): #???
@ -114,6 +128,12 @@ class App(object):
subdir = subdir + '/'
text = subdir + text
else:
if not line.endswith('.txtp'):
if self.args.force:
line += '.txtp'
else:
continue
# should be a mini-txtp, but if name isn't "file.ext.txtp" and just "file.txtp",
# probably means proper txtp exists and should't be created (when generating from !tags.m3u)
name = line
@ -128,6 +148,9 @@ class App(object):
outpath = os.path.join(path, name)
dir_path = os.path.dirname(outpath)
os.makedirs(dir_path, exist_ok=True)
with open(outpath, 'w') as fo:
if text:
fo.write(text)

View File

@ -973,3 +973,23 @@ with `.txtp` as well.
This may even happen with formats that do have loops in other games (for example
relatively common with `.fsb` and mobile games, that may define loops in a .json file).
## Modding game audio and encoding wav files to video game formats
vgmstream cannot *encode* (convert *from* `.wav` *to* a game format), it only *decodes*
(plays game audio). It also can't repack/mod game files (like `.wem`) into other game
formats (like `.bnk`).
One may think it's easy to do, since vgmstream reads game audio might as well write audio
too, but *encoding* and *decoding* are very different.
To *decode* vgmstream just reads a few existing values from the file's *header*,
to setup and play the file's *body* data, decompressing the game's audio codec.
To *encode* the program would need to make the *header* from scratch (having to include
lots of values the game needs but aren't needed for vgmstream to play audio), and take
PCM audio (.wav) and compress it (*very* different than decompressing) to make a *body*.
In other words you need a dedicated tool that can *encode* to your particular format.
Since *encoding* is lot harder than *decoding* it's not very common to find public tools,
and may need to program one yourself.

View File

@ -279,6 +279,9 @@ static const adxkey_info adxkey9_list[] = {
/* ARGONAVIS -Kimi ga Mita Stage e- (Android) */
{0x0000,0x0000,0x0000, NULL,301179795002661}, // 000111EBE2B1D525 (+ AWB subkeys)
// Bungo Stray Dogs: Mayoi Inu Kaikitan (iOS/Android)
{0x0000,0x0000,0x0000, NULL,1655728931134731873}, // 16FA54B0C09F7661
};
static const int adxkey8_list_count = sizeof(adxkey8_list) / sizeof(adxkey8_list[0]);

View File

@ -25,6 +25,7 @@ typedef struct {
int channels;
int sample_rate;
int loop_flag;
uint32_t flags;
int32_t loop_start;
int32_t loop_end;
int32_t num_samples;
@ -173,7 +174,6 @@ fail:
static int parse_musx_stream(STREAMFILE* sf, musx_header* musx) {
uint32_t (*read_u32)(off_t,STREAMFILE*) = NULL;
int default_channels, default_sample_rate;
if (musx->big_endian) {
read_u32 = read_u32be;
@ -210,7 +210,85 @@ static int parse_musx_stream(STREAMFILE* sf, musx_header* musx) {
}
/* parse loops and other info */
if (musx->tables_offset && musx->loops_offset) {
/* cue/stream position table thing */
/* 0x00: cues1 entries (entry size 0x34 or 0x18)
* 0x04: cues2 entries (entry size 0x20 or 0x14)
* 0x08: header size (always 0x14)
* 0x0c: cues2 start
* 0x10: volume? (usually <= 100) */
/* find loops (cues1 also seems to have this info but this looks ok) */
int cues2_count = read_u32(musx->loops_offset+0x04, sf);
off_t cues2_offset = musx->loops_offset + read_u32(musx->loops_offset+0x0c, sf);
for (int i = 0; i < cues2_count; i++) {
uint32_t type, offset1, offset2;
if (musx->is_old) {
offset1 = read_u32(cues2_offset + i*0x20 + 0x04, sf);
type = read_u32(cues2_offset + i*0x20 + 0x08, sf);
offset2 = read_u32(cues2_offset + i*0x20 + 0x14, sf);
} else {
offset1 = read_u32(cues2_offset + i*0x14 + 0x04, sf);
type = read_u32(cues2_offset + i*0x14 + 0x08, sf);
offset2 = read_u32(cues2_offset + i*0x14 + 0x0c, sf);
}
/* other types (0x0a, 0x09) look like section/end markers, 0x06/07 only seems to exist once */
if (type == 0x06 || type == 0x07) { /* loop / goto */
musx->loop_start = offset2;
musx->loop_end = offset1;
musx->loop_flag = 1;
break;
}
}
}
else if (musx->loops_offset && read_u32be(musx->loops_offset, sf) != 0xABABABAB) {
/* parse loop table (loop starts are -1 if non-looping)
* 0x00: version? (always 1)
* 0x04: flags (&1=loops, &2=alt?)
* 0x08: loop start offset?
* 0x0c: loop end offset?
* 0x10: loop end sample
* 0x14: loop start sample
* 0x18: loop end offset
* 0x1c: loop start offset */
musx->flags = read_u32le(musx->loops_offset+0x04, sf);
musx->loop_end_sample = read_s32le(musx->loops_offset+0x10, sf);
musx->loop_start_sample = read_s32le(musx->loops_offset+0x14, sf);
musx->loop_end = read_s32le(musx->loops_offset+0x18, sf);
musx->loop_start = read_s32le(musx->loops_offset+0x1c, sf);
musx->num_samples = musx->loop_end_sample; /* preferable even if not looping as some files have padding */
musx->loop_flag = (musx->loop_start_sample >= 0);
}
/* fix some v10 platform (like PSP) sizes */
if (musx->stream_size == 0) {
musx->stream_size = musx->file_size - musx->stream_offset;
/* always padded to nearest 0x800 sector */
if (musx->stream_size > 0x800) {
uint8_t buf[0x800];
int pos;
off_t offset = musx->stream_offset + musx->stream_size - 0x800;
if (read_streamfile(buf, offset, sizeof(buf), sf) != 0x800)
goto fail;
pos = 0x800 - 0x04;
while (pos > 0) {
if (get_u32be(buf + pos) != 0xABABABAB)
break;
musx->stream_size -= 0x04;
pos -= 0x04;
}
}
}
/* defaults */
int default_channels, default_sample_rate;
switch(musx->platform) {
case 0x5053325F: /* "PS2_" */
@ -254,27 +332,27 @@ static int parse_musx_stream(STREAMFILE* sf, musx_header* musx) {
break;
case 0x5749495F: /* "WII_" */
default_channels = 2;
default_sample_rate = 32000;
musx->codec = DAT;
break;
case 0x5053335F: /* "PS3_" */
default_channels = 2;
default_sample_rate = 44100;
musx->codec = DAT;
break;
case 0x58455F5F: /* "XE__" */
default_channels = 2;
default_sample_rate = 32000;
musx->codec = DAT;
break;
case 0x5053335F: /* "PS3_" */
case 0x50435F5F: /* "PC__" */
default_channels = 2;
default_sample_rate = 44100;
musx->codec = DAT;
// some v10 versions use 44100 and others 32000, the latter seem to have loop info table (even without loops) and a flag
// - 44100: Robots (PC)-v10 (no loop table), Pirates of the Caribbean: At World's End (PC)-v10 (no loop table), Beijing 2008 (loop table)
// - 32000: G-Force (PS3)-v10, Ice Age 3 (PC)-v10 (loop table with flag 2)
// The flag also exists in files with similar loop tables in the DAT* chunk
if (musx->version == 10 && musx->flags && musx->flags & 0x02) {
default_sample_rate = 32000;
}
//TO-DO: some files use 22050 but don't seem to set any flag [Beijing 2008 (PS3)]
break;
case 0x50433032: /* "PC02" */
@ -293,85 +371,6 @@ static int parse_musx_stream(STREAMFILE* sf, musx_header* musx) {
if (musx->sample_rate == 0)
musx->sample_rate = default_sample_rate;
/* parse loops and other info */
if (musx->tables_offset && musx->loops_offset) {
int i, cues2_count;
off_t cues2_offset;
/* cue/stream position table thing */
/* 0x00: cues1 entries (entry size 0x34 or 0x18)
* 0x04: cues2 entries (entry size 0x20 or 0x14)
* 0x08: header size (always 0x14)
* 0x0c: cues2 start
* 0x10: volume? (usually <= 100) */
/* find loops (cues1 also seems to have this info but this looks ok) */
cues2_count = read_u32(musx->loops_offset+0x04, sf);
cues2_offset = musx->loops_offset + read_u32(musx->loops_offset+0x0c, sf);
for (i = 0; i < cues2_count; i++) {
uint32_t type, offset1, offset2;
if (musx->is_old) {
offset1 = read_u32(cues2_offset + i*0x20 + 0x04, sf);
type = read_u32(cues2_offset + i*0x20 + 0x08, sf);
offset2 = read_u32(cues2_offset + i*0x20 + 0x14, sf);
} else {
offset1 = read_u32(cues2_offset + i*0x14 + 0x04, sf);
type = read_u32(cues2_offset + i*0x14 + 0x08, sf);
offset2 = read_u32(cues2_offset + i*0x14 + 0x0c, sf);
}
/* other types (0x0a, 0x09) look like section/end markers, 0x06/07 only seems to exist once */
if (type == 0x06 || type == 0x07) { /* loop / goto */
musx->loop_start = offset2;
musx->loop_end = offset1;
musx->loop_flag = 1;
break;
}
}
}
else if (musx->loops_offset && read_u32be(musx->loops_offset, sf) != 0xABABABAB) {
/* parse loop table (loop starts are -1 if non-looping)
* 0x00: version?
* 0x04: flags? (&1=loops)
* 0x08: loop start offset?
* 0x0c: loop end offset?
* 0x10: loop end sample
* 0x14: loop start sample
* 0x18: loop end offset
* 0x1c: loop start offset */
musx->loop_end_sample = read_s32le(musx->loops_offset+0x10, sf);
musx->loop_start_sample = read_s32le(musx->loops_offset+0x14, sf);
musx->loop_end = read_s32le(musx->loops_offset+0x18, sf);
musx->loop_start = read_s32le(musx->loops_offset+0x1c, sf);
musx->num_samples = musx->loop_end_sample; /* preferable even if not looping as some files have padding */
musx->loop_flag = (musx->loop_start_sample >= 0);
}
/* fix some v10 platform (like PSP) sizes */
if (musx->stream_size == 0) {
musx->stream_size = musx->file_size - musx->stream_offset;
/* always padded to nearest 0x800 sector */
if (musx->stream_size > 0x800) {
uint8_t buf[0x800];
int pos;
off_t offset = musx->stream_offset + musx->stream_size - 0x800;
if (read_streamfile(buf, offset, sizeof(buf), sf) != 0x800)
goto fail;
pos = 0x800 - 0x04;
while (pos > 0) {
if (get_u32be(buf + pos) != 0xABABABAB)
break;
musx->stream_size -= 0x04;
pos -= 0x04;
}
}
}
return 1;
fail:
return 0;

View File

@ -10,20 +10,19 @@ VGMSTREAM* init_vgmstream_psf_single(STREAMFILE* sf) {
off_t start_offset;
int loop_flag, channel_count, sample_rate, rate_value, interleave;
uint32_t psf_config;
uint8_t flags;
size_t data_size;
coding_t codec;
/* checks */
if ((read_u32be(0x00,sf) & 0xFFFFFF00) != get_id32be("PSF\0"))
goto fail;
return NULL;
/* .psf: actual extension
* .swd: bigfile extension */
if (!check_extensions(sf, "psf,swd"))
goto fail;
return NULL;
flags = read_8bit(0x03,sf);
uint8_t flags = read_8bit(0x03,sf);
switch(flags) {
case 0xC0: /* [The Great Escape (PS2), Conflict: Desert Storm (PS2)] */
case 0x40: /* [The Great Escape (PS2)] */
@ -495,6 +494,8 @@ fail:
typedef enum { UNKNOWN, IMUS, PFST, PFSM } sch_type;
#define SCH_STREAM "Stream.swd"
#define SCH_STREAM_PS2 "STREAM.SWD"
/* SCH - Pivotal games multi-audio container [The Great Escape, Conflict series] */
@ -504,7 +505,6 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
STREAMFILE* temp_sf = NULL;
off_t skip = 0, chunk_offset, target_offset = 0, header_offset, subfile_offset = 0;
size_t file_size, chunk_padding, target_size = 0, subfile_size = 0;
int big_endian;
int total_subsongs = 0, target_subsong = sf->stream_index;
int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL;
sch_type target_type = UNKNOWN;
@ -516,23 +516,23 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
skip = 0x0E;
if (!is_id32be(skip + 0x00,sf, "SCH\0") &&
!is_id32le(skip + 0x00,sf, "SCH\0")) /* (BE consoles) */
goto fail;
return NULL;
if (!check_extensions(sf, "sch"))
goto fail;
return NULL;
/* chunked format (id+size, GC pads to 0x20 and uses BE/inverted ids):
* no other info so total subsongs would be count of usable chunks
* (offsets are probably in level .dat files) */
big_endian = (is_id32le(skip + 0x00,sf, "SCH\0"));
int big_endian = (is_id32le(skip + 0x00,sf, "SCH\0"));
if (big_endian) {
read_32bit = read_32bitBE;
chunk_padding = 0x18;
}
else {
read_32bit = read_32bitLE;
chunk_padding = 0;
chunk_padding = 0x00;
}
file_size = get_streamfile_size(sf);
@ -598,9 +598,9 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
switch(target_type) {
case IMUS: { /* external segmented track */
STREAMFILE *psf_sf;
STREAMFILE* psf_sf = NULL;
uint8_t name_size;
char name[255];
char name[256];
/* 0x00: config/size?
* 0x04: name size
@ -611,32 +611,33 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
*/
name_size = read_u8(header_offset + 0x04, sf);
read_string(name,name_size, header_offset + 0x08, sf);
read_string(name, name_size, header_offset + 0x08, sf);
/* later games have name but actually use bigfile [Conflict: Global Storm (Xbox)] */
/* later Xbox games have name but actually use bigfile [Conflict: Global Storm (Xbox)] */
if (read_u8(header_offset + 0x07, sf) == 0xCC) {
external_sf = open_streamfile_by_filename(sf, SCH_STREAM);
if (!external_sf) {
vgm_logi("SCH: external file '%s' not found (put together)\n", SCH_STREAM);
goto fail;
if (external_sf) {
subfile_offset = read_32bit(header_offset + 0x08 + name_size, sf);
subfile_size = get_streamfile_size(external_sf) - subfile_offset; /* not ok but meh */
temp_sf = setup_subfile_streamfile(external_sf, subfile_offset,subfile_size, "psf");
if (!temp_sf) goto fail;
psf_sf = temp_sf;
}
subfile_offset = read_32bit(header_offset + 0x08 + name_size, sf);
subfile_size = get_streamfile_size(external_sf) - subfile_offset; /* not ok but meh */
temp_sf = setup_subfile_streamfile(external_sf, subfile_offset,subfile_size, "psf");
if (!temp_sf) goto fail;
psf_sf = temp_sf;
}
else {
external_sf = open_streamfile_by_filename(sf, name);
if (!external_sf) {
vgm_logi("SCH: external file '%s' not found (put together)\n", name);
goto fail;
}
psf_sf = external_sf;
/* PC games still use name + 0xCC at header, no diffs vs Xbox? [Conflict: Global Storm (PC)] */
if (!psf_sf) {
external_sf = open_streamfile_by_filename(sf, name);
if (external_sf) {
psf_sf = external_sf;
}
}
if (!psf_sf) {
vgm_logi("SCH: external file '%s' or '%s' not found (put together)\n", SCH_STREAM, name);
goto fail;
}
vgmstream = init_vgmstream_psf_segmented(psf_sf);
@ -652,7 +653,7 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
case PFST: { /* external track */
STREAMFILE *psf_sf;
uint8_t name_size;
char name[255];
char name[256];
if (chunk_padding == 0 && target_size > 0x08 + 0x0c) { /* TGE PC/Xbox version */
/* 0x00: -1/0
@ -697,7 +698,7 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
}
}
else if (chunk_padding) {
strcpy(name, "STREAM.SWD"); /* fixed */
strcpy(name, SCH_STREAM_PS2); /* fixed */
/* 0x00: -1
* 0x04: config/size?
@ -715,7 +716,7 @@ VGMSTREAM* init_vgmstream_sch(STREAMFILE* sf) {
psf_sf = temp_sf;
}
else { /* others */
strcpy(name, "STREAM.SWD"); /* fixed */
strcpy(name, SCH_STREAM_PS2); /* fixed */
/* 0x00: -1
* 0x04: config/size?

View File

@ -854,7 +854,7 @@ static int add_entry(txtp_header_t* txtp, char* filename, int is_default) {
txtp_entry_t entry = {0};
;VGM_LOG("TXTP: input filename=%s\n", filename);
//;VGM_LOG("TXTP: input filename=%s\n", filename);
/* parse filename: file.ext#(commands) */
{
@ -896,11 +896,11 @@ static int add_entry(txtp_header_t* txtp, char* filename, int is_default) {
params = NULL;
}
;VGM_LOG("TXTP: params=%s\n", params);
//;VGM_LOG("TXTP: params=%s\n", params);
parse_params(&entry, params);
}
;VGM_LOG("TXTP: output filename=%s\n", filename);
//;VGM_LOG("TXTP: output filename=%s\n", filename);
clean_filename(filename);
//;VGM_LOG("TXTP: clean filename='%s'\n", filename);

View File

@ -8,7 +8,6 @@ VGMSTREAM* init_vgmstream_wave(STREAMFILE* sf) {
uint32_t start_offset, extradata_offset, interleave;
int channels, loop_flag, sample_rate, codec, version;
int32_t num_samples, loop_start, loop_end;
int big_endian;
read_u32_t read_u32;
read_s32_t read_s32;
read_f32_t read_f32;
@ -25,7 +24,7 @@ VGMSTREAM* init_vgmstream_wave(STREAMFILE* sf) {
if (!check_extensions(sf, "wave"))
return NULL;
big_endian = read_u32be(0x00,sf) == 0xE5B7ECFE || is_id32be(0x00,sf, "WWAV");
bool big_endian = read_u32be(0x00,sf) == 0xE5B7ECFE || is_id32be(0x00,sf, "WWAV");
if (big_endian) {
read_u32 = read_u32be;
read_s32 = read_s32be;
@ -46,7 +45,7 @@ VGMSTREAM* init_vgmstream_wave(STREAMFILE* sf) {
loop_end = read_s32(0x18, sf);
codec = read_u8(0x1c, sf);
channels = read_u8(0x1d, sf);
channels = read_u8(0x1d, sf); // DS can only do mono
if (read_u8(0x1e, sf) != 0x00) goto fail; /* unknown */
if (read_u8(0x1f, sf) != 0x00) goto fail; /* unknown */
@ -60,14 +59,19 @@ VGMSTREAM* init_vgmstream_wave(STREAMFILE* sf) {
if(!loop_flag
&& loop_start == 0 && loop_end == num_samples /* full loop */
&& (channels > 1 || (channels == 1 && start_offset <= 0x40))
&& num_samples > 30*sample_rate) { /* in seconds */
&& num_samples > 30 * sample_rate) { /* in seconds */
loop_flag = 1;
}
/* normalize codec: WWAV uses codec 0x00 for DSP */
/* normalize codec (files generated by DsBuildWave/3dsBuildWave) */
if (codec == 0x00 && version == 0x00050000 && start_offset > 0x40) {
/* WWAV uses codec 0x00 for DSP (only one?) */
codec = 0x02;
}
else if (codec == 0x02 && start_offset <= 0x40) {
/* DS games use IMA, no apparent flag (could also test ID) */
codec = 0x03;
}
/* build the VGMSTREAM */
@ -81,39 +85,51 @@ VGMSTREAM* init_vgmstream_wave(STREAMFILE* sf) {
vgmstream->meta_type = meta_WAVE;
/* not sure if there are other codecs but anyway (based on wave-segmented) */
/* some codecs aren't used by known games but can be created by DsBuildWave/3dsBuildWave */
switch(codec) {
case 0x02:
/* DS games use IMA, no apparent flag (could also test ID) */
if (start_offset <= 0x40) {
vgmstream->coding_type = coding_IMA_int;
vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave;
/* extradata:
* 0x00: base hist? (only seen 0)
* 0x02: base step? (only seen 0)
* 0x04: loop hist?
* 0x06: loop step?
*/
}
else {
vgmstream->coding_type = coding_NGC_DSP;
vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave;
/* ADPCM setup: 0x20 coefs + 0x06 initial ps/hist1/hist2 + 0x06 loop ps/hist1/hist2 + ?, per channel */
int head_spacing = 0x2c;
int hist_spacing = 0x22;
if (version == 0x00050000) { /* has an extra empty 16b after coefs */
head_spacing = 0x2e;
hist_spacing = 0x24;
}
dsp_read_coefs(vgmstream, sf, extradata_offset + 0x00, head_spacing, big_endian);
dsp_read_hist(vgmstream, sf, extradata_offset + hist_spacing, head_spacing, big_endian);
}
case 0x00: // PCM8 (not seen)
vgmstream->coding_type = coding_PCM8;
vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave;
break;
case 0x01: // PCM16 (not seen)
vgmstream->coding_type = coding_PCM16BE;
vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave;
break;
case 0x02: { // DSP (3DS only, common)
vgmstream->coding_type = coding_NGC_DSP;
vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave;
/* ADPCM setup: 0x20 coefs + 0x06 initial ps/hist1/hist2 + 0x06 loop ps/hist1/hist2 + ?, per channel */
int head_spacing = 0x2c;
int hist_spacing = 0x22;
if (version == 0x00050000) { /* has an extra empty 16b after coefs */
head_spacing = 0x2e;
hist_spacing = 0x24;
}
dsp_read_coefs(vgmstream, sf, extradata_offset + 0x00, head_spacing, big_endian);
dsp_read_hist(vgmstream, sf, extradata_offset + hist_spacing, head_spacing, big_endian);
break;
}
case 0x03: //IMA (DS uses codec 02 for IMA, common; 3DS: uses 03 but not seen)
vgmstream->coding_type = coding_IMA_int;
vgmstream->layout_type = layout_interleave;
vgmstream->interleave_block_size = interleave;
/* extradata:
* 0x00: base hist? (only seen 0)
* 0x02: base step? (only seen 0)
* 0x04: loop hist?
* 0x06: loop step?
*/
break;
default:
goto fail;
}