/* SXD - Sony/SCE's SNDX lib format (cousin of SGXD) [Gravity Rush, Freedom Wars, Soul Sacrifice PSV] */ VGMSTREAM * init_vgmstream_sxd(STREAMFILE *streamFile) { VGMSTREAM * vgmstream = NULL; STREAMFILE * streamHeader = NULL, *streamData = NULL; off_t start_offset, chunk_offset, first_offset = 0x60, name_offset = 0; size_t chunk_size, stream_size = 0; int is_dual = 0, is_external = 0; int loop_flag, channels, codec, flags; int sample_rate, num_samples, loop_start_sample, loop_end_sample; uint32_t at9_config_data = 0; int total_subsongs, target_subsong = streamFile->stream_index; /* check extension, case insensitive */ /* .sxd: header+data (SXDF), .sxd1: header (SXDF) + .sxd2 = data (SXDS) */ if (!check_extensions(streamFile,"sxd,sxd2")) goto fail; is_dual = !check_extensions(streamFile,"sxd"); /* sxd1+sxd2: use sxd1 as header; otherwise use the current file as header */ if (is_dual) { if (read_32bitBE(0x00,streamFile) != 0x53584453) /* "SXDS" */ goto fail; streamHeader = open_streamfile_by_ext(streamFile, "sxd1"); if (!streamHeader) goto fail; } else { streamHeader = streamFile; } if (read_32bitBE(0x00,streamHeader) != 0x53584446) /* "SXDF" */ goto fail; /* typical chunks: NAME, WAVE and many control chunks (SXDs don't need to contain any sound data) */ if (!find_chunk_le(streamHeader, 0x57415645,first_offset,0, &chunk_offset,&chunk_size)) /* "WAVE" */ goto fail; /* check multi-streams (usually only in SFX containers) */ total_subsongs = read_32bitLE(chunk_offset+0x04,streamHeader); if (target_subsong == 0) target_subsong = 1; if (target_subsong < 0 || target_subsong > total_subsongs || total_subsongs < 1) goto fail; // todo rarely a WAVE subsong may point to a repeated data offset, with different tags only /* read stream header */ { off_t table_offset, header_offset, stream_offset; /* get target offset using table of relative offsets within WAVE */ table_offset = chunk_offset + 0x08 + 4*(target_subsong-1); header_offset = table_offset + read_32bitLE(table_offset,streamHeader); flags = read_32bitLE(header_offset+0x00,streamHeader); codec = read_8bit (header_offset+0x04,streamHeader); channels = read_8bit (header_offset+0x05,streamHeader); /* 0x06(2): unknown, rarely 0xFF */ sample_rate = read_32bitLE(header_offset+0x08,streamHeader); /* 0x0c(4): unknown size? (0x4000/0x3999/0x3333/etc, not related to extra data) */ /* 0x10(4): ? + volume? + pan? (can be 0 for music) */ num_samples = read_32bitLE(header_offset+0x14,streamHeader); loop_start_sample = read_32bitLE(header_offset+0x18,streamHeader); loop_end_sample = read_32bitLE(header_offset+0x1c,streamHeader); stream_size = read_32bitLE(header_offset+0x20,streamHeader); stream_offset = read_32bitLE(header_offset+0x24,streamHeader); loop_flag = loop_start_sample != -1 && loop_end_sample != -1; /* known flag combos: * 0x00: Chaos Rings 2 sfx (RAM + no tags) * 0x01: common (RAM + tags) * 0x02: Chaos Rings 3 sfx (stream + no tags) * 0x03: common (stream + tags) * 0x05: Gravity Rush 2 sfx (RAM + tags) */ //has_tags = flags & 1; is_external = flags & 2; //unknown = flags & 4; /* no apparent differences with/without it? */ /* flag 1 signals TLV-like extra data. Format appears to be 0x00(1)=tag?, 0x01(1)=extra size*32b?, 0x02(2)=config? * but not always (ex. size=0x07 but actually=0), perhaps only some bits are used or varies with tag, or are subflags. * A tag may appear with or without extra data (even 0x0a), 0x01/03/04/06 are common at the beginnig (imply number of tags?), * 0x64/7F are common at the end (but not always), 0x0A=ATRAC9 config, 0x0B/0C appear with RAM preloading data * (most variable in Soul Sacrifice; total TLVs size isn't plainly found in the SXD header AFAIK). */ /* manually try to find ATRAC9 tag */ if (codec == 0x42) { off_t extra_offset = header_offset+0x28; off_t max_offset = chunk_offset + chunk_size; if (!(flags & 1)) goto fail; while (extra_offset < max_offset) { uint32_t tag = read_32bitBE(extra_offset, streamHeader); if (tag == 0x0A010000 || tag == 0x0A010600) { at9_config_data = read_32bitLE(extra_offset+0x04,streamHeader); /* yes, LE */ break; } extra_offset += 0x04; } if (!at9_config_data) goto fail; } /* usually .sxd=header+data and .sxd1=header + .sxd2=data, but rarely sxd1 may contain data [The Last Guardian (PS4)] */ if (is_external) { start_offset = stream_offset; /* absolute if external */ } else { start_offset = header_offset+0x24 + stream_offset; /* from current entry offset if internal */ } } /* get stream name (NAME is tied to REQD/cues, and SFX cues repeat WAVEs, but should work ok for streams) */ if (is_dual && find_chunk_le(streamHeader, 0x4E414D45,first_offset,0, &chunk_offset,NULL)) { /* "NAME" */ /* table: relative offset (32b) + hash? (32b) + cue index (32b) */ int i; int num_entries = read_16bitLE(chunk_offset+0x04,streamHeader); /* can be bigger than streams */ for (i = 0; i < num_entries; i++) { uint32_t index = (uint32_t)read_32bitLE(chunk_offset+0x08 + 0x08 + i*0x0c,streamHeader); if (index+1 == target_subsong) { name_offset = chunk_offset+0x08 + 0x00 + i*0x0c + read_32bitLE(chunk_offset+0x08 + 0x00 + i*0x0c,streamHeader); break; } } } if (is_external && !is_dual) { VGM_LOG("SXD: found single sxd with external data\n"); goto fail; } if (is_external) { streamData = streamFile; } else { streamData = streamHeader; } if (start_offset > get_streamfile_size(streamData)) { VGM_LOG("SXD: wrong location?\n"); goto fail; } /* build the VGMSTREAM */ vgmstream = allocate_vgmstream(channels,loop_flag); if (!vgmstream) goto fail; vgmstream->sample_rate = sample_rate; vgmstream->num_samples = num_samples; vgmstream->loop_start_sample = loop_start_sample; vgmstream->loop_end_sample = loop_end_sample; vgmstream->num_streams = total_subsongs; vgmstream->stream_size = stream_size; vgmstream->meta_type = meta_SXD; if (name_offset) read_string(vgmstream->stream_name,STREAM_NAME_SIZE, name_offset,streamHeader); switch (codec) { case 0x20: /* PS-ADPCM [Hot Shots Golf: World Invitational (Vita) sfx] */ vgmstream->coding_type = coding_PSX; vgmstream->layout_type = layout_interleave; vgmstream->interleave_block_size = 0x10; break; case 0x21: /* HEVAG [Gravity Rush (Vita) sfx] */ vgmstream->coding_type = coding_HEVAG; vgmstream->layout_type = layout_interleave; vgmstream->interleave_block_size = 0x10; break; #ifdef VGM_USE_ATRAC9 case 0x42: { /* ATRAC9 [Soul Sacrifice (Vita), Freedom Wars (Vita), Gravity Rush 2 (PS4)] */ atrac9_config cfg = {0}; cfg.channels = vgmstream->channels; cfg.config_data = at9_config_data; vgmstream->codec_data = init_atrac9(&cfg); if (!vgmstream->codec_data) goto fail; vgmstream->coding_type = coding_ATRAC9; vgmstream->layout_type = layout_none; break; } #endif //case 0x28: /* dummy codec? (found with 0 samples) [Hot Shots Golf: World Invitational (Vita) sfx] */ default: VGM_LOG("SXD: unknown codec 0x%x\n", codec); goto fail; } /* open the file for reading */ if (!vgmstream_open_stream(vgmstream,streamData,start_offset)) goto fail; if (is_dual) close_streamfile(streamHeader); return vgmstream; fail: if (is_dual) close_streamfile(streamHeader); close_vgmstream(vgmstream); return NULL; }
/* SGXD - Sony/SCEI's format (SGB+SGH / SGD / SGX) */ VGMSTREAM * init_vgmstream_sgxd(STREAMFILE *streamFile) { VGMSTREAM * vgmstream = NULL; STREAMFILE * streamHeader = NULL; off_t start_offset, data_offset, chunk_offset, name_offset = 0; size_t stream_size; int is_sgx, is_sgb = 0; int loop_flag, channels, type; int sample_rate, num_samples, loop_start_sample, loop_end_sample; int total_subsongs, target_subsong = streamFile->stream_index; /* check extension, case insensitive */ /* .sgx: header+data (Genji), .sgd: header+data, .sgh/sgd: header/data */ if (!check_extensions(streamFile,"sgx,sgd,sgb")) goto fail; is_sgx = check_extensions(streamFile,"sgx"); is_sgb = check_extensions(streamFile,"sgb"); /* SGB+SGH: use SGH as header; otherwise use the current file as header */ if (is_sgb) { streamHeader = open_stream_ext(streamFile, "sgh"); if (!streamHeader) goto fail; } else { streamHeader = streamFile; } /* SGXD base (size 0x10) */ if (read_32bitBE(0x00,streamHeader) != 0x53475844) /* "SGXD" */ goto fail; /* 0x04 SGX: full header_size; SGD/SGH: unknown header_size (counting from 0x0/0x8/0x10, varies) */ /* 0x08 SGX: first chunk offset? (0x10); SGD/SGH: full header_size */ /* 0x0c SGX/SGH: full data size with padding; SGD: full data size + 0x80000000 with padding */ if (is_sgb) { data_offset = 0x00; } else if ( is_sgx ) { data_offset = read_32bitLE(0x04,streamHeader); } else { data_offset = read_32bitLE(0x08,streamHeader); } /* typical chunks: WAVE, RGND, NAME (strings for WAVE or RGND), SEQD (related to SFX), WSUR, WMKR, BUSS */ /* WAVE chunk (size 0x10 + files * 0x38 + optional padding) */ if (is_sgx) { /* position after chunk+size */ if (read_32bitBE(0x10,streamHeader) != 0x57415645) goto fail; /* "WAVE" */ chunk_offset = 0x18; } else { if (!find_chunk_le(streamHeader, 0x57415645,0x10,0, &chunk_offset,NULL)) goto fail; /* "WAVE" */ } /* 0x04 SGX: unknown; SGD/SGH: chunk length, 0x08 null */ /* check multi-streams (usually only SE containers; Puppeteer) */ total_subsongs = read_32bitLE(chunk_offset+0x04,streamHeader); if (target_subsong == 0) target_subsong = 1; if (target_subsong < 0 || target_subsong > total_subsongs || total_subsongs < 1) goto fail; /* read stream header */ { off_t stream_offset; chunk_offset += 0x08 + 0x38 * (target_subsong-1); /* position in target header*/ /* 0x00 ? (00/01/02) */ if (!is_sgx) /* meaning unknown in .sgx; offset 0 = not a stream (a RGND sample) */ name_offset = read_32bitLE(chunk_offset+0x04,streamHeader); type = read_8bit(chunk_offset+0x08,streamHeader); channels = read_8bit(chunk_offset+0x09,streamHeader); /* 0x0a null */ sample_rate = read_32bitLE(chunk_offset+0x0c,streamHeader); /* 0x10 info_type: meaning of the next value * (00=null, 30/40=data size without padding (ADPCM, ATRAC3plus), 80/A0=block size (AC3) */ /* 0x14 info_value (see above) */ /* 0x18 unknown (ex. 0x0008/0010/3307/CC02/etc)x2 */ /* 0x1c null */ num_samples = read_32bitLE(chunk_offset+0x20,streamHeader); loop_start_sample = read_32bitLE(chunk_offset+0x24,streamHeader); loop_end_sample = read_32bitLE(chunk_offset+0x28,streamHeader); stream_size = read_32bitLE(chunk_offset+0x2c,streamHeader); /* stream size (without padding) / interleave (for type3) */ if (is_sgx) { stream_offset = 0x0; } else{ stream_offset = read_32bitLE(chunk_offset+0x30,streamHeader); } /* 0x34 SGX: unknown; SGD/SGH: stream size (with padding) / interleave */ loop_flag = loop_start_sample!=0xffffffff && loop_end_sample!=0xffffffff; start_offset = data_offset + stream_offset; } /* build the VGMSTREAM */ vgmstream = allocate_vgmstream(channels,loop_flag); if (!vgmstream) goto fail; vgmstream->sample_rate = sample_rate; vgmstream->num_samples = num_samples; vgmstream->loop_start_sample = loop_start_sample; vgmstream->loop_end_sample = loop_end_sample; vgmstream->num_streams = total_subsongs; vgmstream->stream_size = stream_size; vgmstream->meta_type = meta_SGXD; if (name_offset) read_string(vgmstream->stream_name,STREAM_NAME_SIZE, name_offset,streamHeader); /* needs -1 to match RIFF AT3's loop chunk * (maybe SGXD = "loop before this sample" rather than "loop after this sample" as expected by vgmstream) */ if (vgmstream->loop_end_sample > 0) vgmstream->loop_end_sample -= 1; switch (type) { case 0x03: /* PS-ADPCM [Genji (PS3), Ape Escape Move (PS3)]*/ vgmstream->coding_type = coding_PSX; vgmstream->layout_type = layout_interleave; if (is_sgx || is_sgb) { vgmstream->interleave_block_size = 0x10; } else { /* this only seems to happen with SFX */ vgmstream->interleave_block_size = stream_size; } break; #ifdef VGM_USE_FFMPEG case 0x04: { /* ATRAC3plus [Kurohyo 1/2 (PSP), BraveStory (PSP)] */ ffmpeg_codec_data *ffmpeg_data; /* internally has a RIFF header; but the SGXD header / sample rate has priority over it (may not match) */ ffmpeg_data = init_ffmpeg_offset(streamFile, start_offset, stream_size); if ( !ffmpeg_data ) goto fail; vgmstream->codec_data = ffmpeg_data; vgmstream->coding_type = coding_FFmpeg; vgmstream->layout_type = layout_none; /* manually read skip_samples if FFmpeg didn't do it */ if (ffmpeg_data->skipSamples <= 0) { off_t chunk_offset; size_t chunk_size, fact_skip_samples = 0; if (!find_chunk_le(streamFile, 0x66616374,start_offset+0xc,0, &chunk_offset,&chunk_size)) /* find "fact" */ goto fail; if (chunk_size == 0x8) { fact_skip_samples = read_32bitLE(chunk_offset+0x4, streamFile); } else if (chunk_size == 0xc) { fact_skip_samples = read_32bitLE(chunk_offset+0x8, streamFile); } ffmpeg_set_skip_samples(ffmpeg_data, fact_skip_samples); } /* SGXD loop/sample values are relative (without skip samples) vs RIFF (with skip samples), no need to adjust */ break; } #endif case 0x05: /* Short PS-ADPCM [Afrika (PS3)] */ vgmstream->coding_type = coding_PSX_cfg; vgmstream->layout_type = layout_interleave; vgmstream->interleave_block_size = 0x4; break; #ifdef VGM_USE_FFMPEG case 0x06: { /* AC3 [Tokyo Jungle (PS3), Afrika (PS3)] */ ffmpeg_codec_data *ffmpeg_data; ffmpeg_data = init_ffmpeg_offset(streamFile, start_offset, stream_size); if ( !ffmpeg_data ) goto fail; vgmstream->codec_data = ffmpeg_data; vgmstream->coding_type = coding_FFmpeg; vgmstream->layout_type = layout_none; /* manually set skip_samples if FFmpeg didn't do it */ if (ffmpeg_data->skipSamples <= 0) { /* PS3 AC3 consistently has 256 encoder delay samples, and there are ~1000-2000 samples after num_samples. * Skipping them marginally improves full loops in some Tokyo Jungle tracks (ex. a_1.sgd). */ ffmpeg_set_skip_samples(ffmpeg_data, 256); } /* SGXD loop/sample values are relative (without skip samples), no need to adjust */ break; } #endif default: goto fail; } /* open the file for reading */ if (!vgmstream_open_stream(vgmstream,streamFile,start_offset)) goto fail; if (is_sgb && streamHeader) close_streamfile(streamHeader); return vgmstream; fail: if (is_sgb && streamHeader) close_streamfile(streamHeader); close_vgmstream(vgmstream); return NULL; }