From 706b71d7fd8228c100f6fb848c2cf945b90ada1e Mon Sep 17 00:00:00 2001 From: bnnm Date: Thu, 15 Aug 2019 15:12:13 +0200 Subject: [PATCH] Add Pivotal .sch [The Great Escape, Conflict: Desert Storm] --- src/formats.c | 1 + src/meta/meta.h | 1 + src/meta/psf.c | 475 +++++++++++++++++++++++++++++++++++++++++++----- src/vgmstream.c | 1 + 4 files changed, 432 insertions(+), 46 deletions(-) diff --git a/src/formats.c b/src/formats.c index 81278130..2a017e03 100644 --- a/src/formats.c +++ b/src/formats.c @@ -388,6 +388,7 @@ static const char* extension_list[] = { "sbin", "sc", "scd", + "sch", "sd9", "sdf", "sdt", diff --git a/src/meta/meta.h b/src/meta/meta.h index 73af3cbf..86b34de8 100644 --- a/src/meta/meta.h +++ b/src/meta/meta.h @@ -856,5 +856,6 @@ VGMSTREAM * init_vgmstream_xavs(STREAMFILE * streamFile); VGMSTREAM * init_vgmstream_psf_single(STREAMFILE * streamFile); VGMSTREAM * init_vgmstream_psf_segmented(STREAMFILE * streamFile); +VGMSTREAM * init_vgmstream_sch(STREAMFILE * streamFile); #endif /*_META_H*/ diff --git a/src/meta/psf.c b/src/meta/psf.c index a7f79ef4..940d87ac 100644 --- a/src/meta/psf.c +++ b/src/meta/psf.c @@ -25,32 +25,36 @@ VGMSTREAM * init_vgmstream_psf_single(STREAMFILE *streamFile) { flags = read_8bit(0x03,streamFile); switch(flags) { case 0xC0: /* [The Great Escape (PS2), Conflict: Desert Storm (PS2)] */ + case 0x40: /* [The Great Escape (PS2)] */ case 0xA1: /* [Conflict: Desert Storm 2 (PS2)] */ case 0x21: /* [Conflict: Desert Storm 2 (PS2), Conflict: Global Storm (PS2)] */ //case 0x22: /* [Conflict: Vietman (PS2)] */ //todo weird size value, stereo, only one found - channel_count = 2; - if (flags == 0x21) - channel_count = 1; - interleave = 0x10; codec = coding_PSX; + interleave = 0x10; + + channel_count = 2; + if (flags == 0x21 || flags == 0x40) + channel_count = 1; start_offset = 0x08; break; case 0x80: /* [The Great Escape (PC/Xbox), Conflict: Desert Storm (Xbox/GC), Conflict: Desert Storm 2 (Xbox)] */ case 0x81: /* [Conflict: Desert Storm 2 (Xbox), Conflict: Vietnam (Xbox)] */ case 0x01: /* [Conflict: Global Storm (Xbox)] */ + codec = coding_PSX_pivotal; + interleave = 0x10; + channel_count = 2; if (flags == 0x01) channel_count = 1; - interleave = 0x10; - codec = coding_PSX_pivotal; start_offset = 0x08; break; case 0xD1: /* [Conflict: Desert Storm 2 (GC)] */ - channel_count = 2; - interleave = 0x08; codec = coding_NGC_DSP; + interleave = 0x08; + + channel_count = 2; start_offset = 0x08 + 0x60 * channel_count; break; @@ -65,16 +69,14 @@ VGMSTREAM * init_vgmstream_psf_single(STREAMFILE *streamFile) { /* pitch/cents? */ rate_value = (psf_config >> 20) & 0xFFF; switch(rate_value) { - //case 0xEB5: - //case 0xEB4: - case 0xEB3: sample_rate = 44100; break; - case 0x555: sample_rate = 16000; break; - case 0x355: sample_rate = 11050; break; - case 0x1d5: sample_rate = 6000; break; /* ? */ - case 0x1cc: sample_rate = 5000; break; + case 3763: sample_rate = 44100; break; + case 1365: sample_rate = 16000; break; + case 940: sample_rate = 11050; break; + case 460: sample_rate = 5000; break; default: VGM_LOG("PSF: unknown rate value %x\n", rate_value); - goto fail; + sample_rate = rate_value * 11.72; /* not exact but works well enough */ + break; } data_size = (psf_config & 0xFFFFF) * (interleave * channel_count); /* in blocks */ @@ -119,6 +121,8 @@ VGMSTREAM * init_vgmstream_psf_single(STREAMFILE *streamFile) { goto fail; } + vgmstream->stream_size = data_size; + if (!vgmstream_open_stream(vgmstream,streamFile,start_offset)) goto fail; return vgmstream; @@ -144,7 +148,7 @@ VGMSTREAM * init_vgmstream_psf_segmented(STREAMFILE *streamFile) { goto fail; if (read_32bitBE(0x00,streamFile) != 0x50534660 && /* "PSF\60" [The Great Escape (PC/Xbox/PS2), Conflict: Desert Storm (Xbox/GC)] */ - read_32bitBE(0x00,streamFile) != 0x50534631) /* "PSF\31" [Conflict: Desert Storm 2 (Xbox/GC/PS2)] */ + read_32bitBE(0x00,streamFile) != 0x50534631) /* "PSF\31" [Conflict: Desert Storm 2 (Xbox/GC/PS2), Conflict: Global Terror (Xbox)] */ goto fail; segment_count = read_32bitLE(0x04, streamFile); @@ -161,10 +165,10 @@ VGMSTREAM * init_vgmstream_psf_segmented(STREAMFILE *streamFile) { for (i = 0; i < segment_count; i++) { off_t psf_offset; size_t psf_size; - uint32_t psf_id; /* mini table */ psf_offset = read_32bitLE(offset + 0x00, streamFile); + psf_size = get_streamfile_size(streamFile) - psf_offset; /* not ok but meh */ /* 0x04-0c: 0x02*4 transition segments (possibly to 4 song variations) */ /* use last section transition as loop */ @@ -175,15 +179,7 @@ VGMSTREAM * init_vgmstream_psf_segmented(STREAMFILE *streamFile) { } /* multiple segment can point to the same PSF offset (for repeated song sections) */ - //todo reuse repeated VGMSTREAMs to improve memory a bit - - psf_id = read_32bitBE(psf_offset + 0x00, streamFile); - psf_size = read_32bitLE(psf_offset + 0x04, streamFile); - if (psf_id == 0x505346D1) //todo improve - psf_size = (psf_size & 0xFFFFF) * 0x10; - else - psf_size = (psf_size & 0xFFFFF) * 0x20; - //;VGM_LOG("PSF: offset=%lx, size=%x\n", psf_offset, psf_size); + //todo reuse repeated VGMSTREAMs to improve memory and bitrate calcs a bit temp_streamFile = setup_subfile_streamfile(streamFile, psf_offset, psf_size, "psf"); if (!temp_streamFile) goto fail; @@ -200,8 +196,6 @@ VGMSTREAM * init_vgmstream_psf_segmented(STREAMFILE *streamFile) { vgmstream = allocate_segmented_vgmstream(data,loop_flag, loop_start, loop_end); if (!vgmstream) goto fail; - vgmstream->stream_size = get_streamfile_size(streamFile); - return vgmstream; fail: if (!vgmstream) free_layout_segmented(data); @@ -210,33 +204,422 @@ fail: return NULL; } -#if 0 -VGMSTREAM * init_vgmstream_sch(STREAMFILE *streamFile) { +/* ***************************************************** */ + +static VGMSTREAM * init_vgmstream_psf_pfsm(STREAMFILE *streamFile) { VGMSTREAM * vgmstream = NULL; off_t start_offset; - int loop_flag, channel_count; + int loop_flag, channel_count, sample_rate = 0, rate_value = 0, interleave, big_endian; + size_t data_size; + coding_t codec; + int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL; + int16_t (*read_16bit)(off_t,STREAMFILE*) = NULL; + + /* standard: + * 0x00: -1/number (lang?) + * 0x04: config/size? + * 0x08: channel size? only ok for PSX-pivotal + * 0x0c: sample rate or rate_value + * 0x0e: 0x4=PSX-pivotal or 0xFF=PSX + * 0x0f: name size (0xCC/FF=null) + * 0x10: data + * + * GC is similar with 0x20-align between some fields + */ + + /* checks */ + //if (!check_extensions(streamFile, "psf")) + // goto fail; + if (read_32bitBE(0x00,streamFile) != 0x5046534D && /* "PFSM" */ + read_32bitLE(0x00,streamFile) != 0x5046534D) /* "PFSM" (BE) */ + goto fail; + + big_endian = (read_32bitLE(0x00,streamFile) == 0x5046534D); + if (big_endian) { + read_32bit = read_32bitBE; + read_16bit = read_16bitBE; + } + else { + read_32bit = read_32bitLE; + read_16bit = read_16bitLE; + } + + loop_flag = 0; + + + if (big_endian && read_32bit(0x50, streamFile) != 0) { /* GC */ + codec = coding_NGC_DSP; + interleave = 0x08; + channel_count = 1; + rate_value = (uint16_t)read_16bit(0x48, streamFile); + + start_offset = 0x60 + 0x60 * channel_count; + } + else if (big_endian) { /* GC */ + codec = coding_PCM16BE; + interleave = 0x02; + channel_count = 1; + rate_value = (uint16_t)read_16bit(0x48, streamFile); + + start_offset = 0x60; + } + else if ((uint8_t)read_8bit(0x16, streamFile) == 0xFF) { /* PS2 */ + codec = coding_PSX; + interleave = 0x10; + rate_value = (uint16_t)read_16bit(0x14, streamFile); + channel_count = 1; + + start_offset = 0x18; + } + else { /* PC/Xbox, some PS2/GC */ + codec = coding_PSX_pivotal; + interleave = 0x10; + sample_rate = (uint16_t)read_16bit(0x14, streamFile); + channel_count = 1; + + start_offset = 0x18; + } + + data_size = get_streamfile_size(streamFile) - start_offset; + + /* pitch/cents? */ + if (sample_rate == 0) { + /* pitch/cents? */ + switch(rate_value) { + case 3763: sample_rate = 44100; break; + case 1365: sample_rate = 16000; break; + case 940: sample_rate = 11050; break; + case 460: sample_rate = 5000; break; + default: + VGM_LOG("PSF: unknown rate value %x\n", rate_value); + sample_rate = rate_value * 11.72; /* not exact but works well enough */ + break; + } + } + + + /* build the VGMSTREAM */ + vgmstream = allocate_vgmstream(channel_count, loop_flag); + if (!vgmstream) goto fail; + + vgmstream->meta_type = meta_PSF; + vgmstream->sample_rate = sample_rate; + + switch(codec) { + case coding_PCM16BE: + vgmstream->coding_type = coding_PCM16BE; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = interleave; + + vgmstream->num_samples = pcm_bytes_to_samples(data_size, channel_count, 16); + break; + + case coding_PSX: + vgmstream->coding_type = coding_PSX; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = interleave; + + vgmstream->num_samples = ps_bytes_to_samples(data_size, channel_count); + break; + + case coding_PSX_pivotal: + vgmstream->coding_type = coding_PSX_pivotal; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = interleave; + + vgmstream->num_samples = ps_cfg_bytes_to_samples(data_size, 0x10, channel_count); + break; + + case coding_NGC_DSP: + vgmstream->coding_type = coding_NGC_DSP; + vgmstream->layout_type = layout_interleave; + vgmstream->interleave_block_size = interleave; + /* has standard DSP headers at 0x08 */ + dsp_read_coefs_be(vgmstream,streamFile,0x60+0x1c,0x60); + dsp_read_hist_be (vgmstream,streamFile,0x60+0x40,0x60); + + vgmstream->num_samples = read_32bitBE(0x60, streamFile);//dsp_bytes_to_samples(data_size, channel_count); + break; + + default: + goto fail; + } + + if (!vgmstream_open_stream(vgmstream,streamFile,start_offset)) + goto fail; + return vgmstream; + +fail: + close_vgmstream(vgmstream); + return NULL; +} + + + +typedef enum { UNKNOWN, IMUS, PFST, PFSM } sch_type; + + + +/* SCH - Pivotal games multi-audio container [The Great Escape, Conflict series] */ +VGMSTREAM * init_vgmstream_sch(STREAMFILE *streamFile) { + VGMSTREAM *vgmstream = NULL; + STREAMFILE *external_streamFile = NULL; + STREAMFILE *temp_streamFile = 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 = streamFile->stream_index; + int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL; + sch_type target_type = UNKNOWN; + char stream_name[STREAM_NAME_SIZE] ={0}; /* checks */ if (!check_extensions(streamFile, "sch")) goto fail; - /* chunked format (id+size, GC pads to 0x20 and uses BE/inverted ids): - * - SCH\0: start - * - IMUS: points to a external .psf + segment table (same as in .psf, TGE only?) - * - BANK: volume/etc info? points to something? - * - PFSM: single .psf-like file (larger header) - * - PFST: points to single PSF offset (.psf in TGE, or STREAMS.SWD); may be chained to next PFST? - * - * no other info so total subsongs would be count of usable chunks - * in later games, segmented .psf seems to be removed and PFST is used instead - */ + if (read_32bitBE(0x00,streamFile) == 0x48445253) /* "HDRSND" (found on later games) */ + skip = 0x0E; + if (read_32bitBE(skip + 0x00,streamFile) != 0x53434800 && /* "SCH\0" */ + read_32bitLE(skip + 0x00,streamFile) != 0x53434800) /* "SCH\0" (BE consoles) */ + goto fail; + + + /* 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 = (read_32bitLE(skip + 0x00,streamFile) == 0x53434800); + if (big_endian) { + read_32bit = read_32bitBE; + chunk_padding = 0x18; + } + else { + read_32bit = read_32bitLE; + chunk_padding = 0; + } + + file_size = get_streamfile_size(streamFile); + if (read_32bit(skip + 0x04,streamFile) + skip + 0x08 + chunk_padding < file_size) /* sometimes padded */ + goto fail; + + if (target_subsong == 0) target_subsong = 1; + + chunk_offset = skip + 0x08 + chunk_padding; + + /* get all files*/ + while (chunk_offset < file_size) { + uint32_t chunk_id = read_32bitBE(chunk_offset + 0x00,streamFile); + uint32_t chunk_size = read_32bit(chunk_offset + 0x04,streamFile); + sch_type current_type = UNKNOWN; + + switch(chunk_id) { + case 0x494D5553: /* "IMUS" (TGE PC/Xbox only) */ + current_type = IMUS; + break; + + case 0x54534650: + case 0x50465354: /* "PFST" */ + current_type = PFST; + break; + + case 0x4D534650: + case 0x5046534D: /* "PFSM" */ + current_type = PFSM; + break; + + case 0x4B4E4142: + case 0x42414E4B: /* "BANK" */ + /* unknown format (variable size), maybe config for entry numbers */ + break; + case 0x424C4F4B: /* "BLOK" [Conflict: Desert Storm (Xbox)] */ + /* some ids or something? */ + break; + + default: + VGM_LOG("SCH: unknown chunk at %lx\n", chunk_offset); + goto fail; + } + + if (current_type != UNKNOWN) + total_subsongs++; + + if (total_subsongs == target_subsong && target_type == UNKNOWN) { + target_type = current_type; + target_offset = chunk_offset; + target_size = 0x08 + chunk_padding + chunk_size; + } + + chunk_offset += 0x08 + chunk_padding + chunk_size; + } + + if (target_subsong < 0 || target_subsong > total_subsongs || total_subsongs < 1) goto fail; + if (target_size == 0) goto fail; + + header_offset = target_offset + 0x08 + chunk_padding; + + //;VGM_LOG("SCH: offset=%lx, size=%x\n",target_offset, target_size); + + switch(target_type) { + case IMUS: { /* external segmented track */ + STREAMFILE *psf_streamFile; + uint8_t name_size; + char name[255]; + + /* 0x00: config/size? + * 0x04: name size + * 0x05: segments + * 0x06: ? + * 0x08: relative path to .psf + * 0xNN: segment table (same as .psf) + */ + + name_size = read_8bit(header_offset + 0x04, streamFile); + read_string(name,name_size, header_offset + 0x08, streamFile); + + /* later games have name but actually use bigfile [Conflict: Global Storm (Xbox)] */ + if ((uint8_t)read_8bit(header_offset + 0x07, streamFile) == 0xCC) { + external_streamFile = open_streamfile_by_filename(streamFile, "Stream.swd"); + if (!external_streamFile) goto fail; + + subfile_offset = read_32bit(header_offset + 0x08 + name_size, streamFile); + subfile_size = get_streamfile_size(external_streamFile) - subfile_offset; /* not ok but meh */ + + temp_streamFile = setup_subfile_streamfile(external_streamFile, subfile_offset,subfile_size, "psf"); + if (!temp_streamFile) goto fail; + + psf_streamFile = temp_streamFile; + } + else { + external_streamFile = open_streamfile_by_filename(streamFile, name); + if (!external_streamFile) goto fail; + + psf_streamFile = external_streamFile; + } + + vgmstream = init_vgmstream_psf_segmented(psf_streamFile); + if (!vgmstream) { + vgmstream = init_vgmstream_psf_single(psf_streamFile); + if (!vgmstream) goto fail; + } + + snprintf(stream_name,sizeof(stream_name), "%s-%s" , "IMUS", name); + break; + } + + case PFST: { /* external track */ + STREAMFILE *psf_streamFile; + uint8_t name_size; + char name[255]; + + if (chunk_padding == 0 && target_size > 0x08 + 0x0c) { /* TGE PC/Xbox version */ + /* 0x00: -1/0 + * 0x04: config/size? + * 0x08: channel size + * 0x0c: sample rate? (differs vs PSF) + * 0x0e: 4? + * 0x0f: name size + * 0x10: name + */ + + /* later games have name but actually use bigfile [Conflict: Global Storm (Xbox)] */ + if ((read_32bitBE(header_offset + 0x14, streamFile) & 0x0000FFFF) == 0xCCCC) { + name_size = read_8bit(header_offset + 0x13, streamFile); + read_string(name,name_size, header_offset + 0x18, streamFile); + + external_streamFile = open_streamfile_by_filename(streamFile, "Stream.swd"); + if (!external_streamFile) goto fail; + + subfile_offset = read_32bit(header_offset + 0x0c, streamFile); + subfile_size = get_streamfile_size(external_streamFile) - subfile_offset; /* not ok but meh */ + + temp_streamFile = setup_subfile_streamfile(external_streamFile, subfile_offset,subfile_size, "psf"); + if (!temp_streamFile) goto fail; + + psf_streamFile = temp_streamFile; + } + else { + name_size = read_8bit(header_offset + 0x0f, streamFile); + read_string(name,name_size, header_offset + 0x10, streamFile); + + external_streamFile = open_streamfile_by_filename(streamFile, name); + if (!external_streamFile) goto fail; + + psf_streamFile = external_streamFile; + } + } + else if (chunk_padding) { + strcpy(name, "STREAM.SWD"); /* fixed */ + + /* 0x00: -1 + * 0x04: config/size? + * 0x08: .swd offset + */ + external_streamFile = open_streamfile_by_filename(streamFile, name); + if (!external_streamFile) goto fail; + + subfile_offset = read_32bit(header_offset + 0x24, streamFile); + subfile_size = get_streamfile_size(external_streamFile) - subfile_offset; /* not ok but meh */ + + temp_streamFile = setup_subfile_streamfile(external_streamFile, subfile_offset,subfile_size, "psf"); + if (!temp_streamFile) goto fail; + + psf_streamFile = temp_streamFile; + } + else { /* others */ + strcpy(name, "STREAM.SWD"); /* fixed */ + + /* 0x00: -1 + * 0x04: config/size? + * 0x08: .swd offset + */ + external_streamFile = open_streamfile_by_filename(streamFile, name); + if (!external_streamFile) goto fail; + + subfile_offset = read_32bit(header_offset + 0x08, streamFile); + subfile_size = get_streamfile_size(external_streamFile) - subfile_offset; /* not ok but meh */ + + temp_streamFile = setup_subfile_streamfile(external_streamFile, subfile_offset,subfile_size, "psf"); + if (!temp_streamFile) goto fail; + + psf_streamFile = temp_streamFile; + } + + vgmstream = init_vgmstream_psf_segmented(psf_streamFile); + if (!vgmstream) { + vgmstream = init_vgmstream_psf_single(psf_streamFile); + if (!vgmstream) goto fail; + } + + snprintf(stream_name,sizeof(stream_name), "%s-%s" , "PFST", name); + break; + } + + case PFSM: + /* internal sound */ + + temp_streamFile = setup_subfile_streamfile(streamFile, target_offset,target_size, NULL); + if (!temp_streamFile) goto fail; + + vgmstream = init_vgmstream_psf_pfsm(temp_streamFile); + if (!vgmstream) goto fail; + + snprintf(stream_name,sizeof(stream_name), "%s" , "PFSM"); + break; + + default: /* target not found */ + goto fail; + } + + vgmstream->num_streams = total_subsongs; + strcpy(vgmstream->stream_name, stream_name); - return vgmstream; -fail: - if (!vgmstream) free_layout_layered(data); close_streamfile(temp_streamFile); + close_streamfile(external_streamFile); + return vgmstream; + +fail: + close_streamfile(temp_streamFile); + close_streamfile(external_streamFile); close_vgmstream(vgmstream); return NULL; } -#endif diff --git a/src/vgmstream.c b/src/vgmstream.c index 94ac6cba..e521a4ff 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -471,6 +471,7 @@ VGMSTREAM * (*init_vgmstream_functions[])(STREAMFILE *streamFile) = { init_vgmstream_psf_single, init_vgmstream_psf_segmented, init_vgmstream_dsp_itl, + init_vgmstream_sch, /* lowest priority metas (should go after all metas, and TXTH should go before raw formats) */ init_vgmstream_txth, /* proper parsers should supersede TXTH, once added */