Add raw interleaved XA subsong handling

This commit is contained in:
bnnm 2019-10-27 23:50:48 +01:00
parent 246222dd52
commit abd281e7c8
4 changed files with 262 additions and 169 deletions

View File

@ -93,7 +93,7 @@ void decode_hevag(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspaci
/* xa_decoder */
void decode_xa(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel);
size_t xa_bytes_to_samples(size_t bytes, int channels, int is_blocked);
size_t xa_bytes_to_samples(size_t bytes, int channels, int is_blocked, int is_form2);
/* ea_xa_decoder */
void decode_ea_xa(VGMSTREAMCHANNEL * stream, sample * outbuf, int channelspacing, int32_t first_sample, int32_t samples_to_do, int channel);

View File

@ -152,9 +152,9 @@ void decode_xa(VGMSTREAMCHANNEL * stream, sample_t * outbuf, int channelspacing,
stream->adpcm_history2_32 = hist2;
}
size_t xa_bytes_to_samples(size_t bytes, int channels, int is_blocked) {
size_t xa_bytes_to_samples(size_t bytes, int channels, int is_blocked, int is_form2) {
if (is_blocked) {
return (bytes / 0x930) * (28*8/ channels) * 18;
return (bytes / 0x930) * (28*8/ channels) * (is_form2 ? 18 : 16);
}
else {
return (bytes / 0x80) * (28*8 / channels);

View File

@ -7,7 +7,8 @@ void block_update_xa(off_t block_offset, VGMSTREAM * vgmstream) {
STREAMFILE* streamFile = vgmstream->ch[0].streamfile;
int i, is_audio;
size_t block_samples;
uint8_t xa_target, xa_submode, config_target;
uint16_t xa_config, target_config;
uint8_t xa_submode;
/* XA mode2/form2 sector, size 0x930
@ -21,37 +22,42 @@ void block_update_xa(off_t block_offset, VGMSTREAM * vgmstream) {
* 0x930: end
* Sectors with no data may exist near other with data
*/
xa_target = (uint8_t)read_16bitBE(block_offset + 0x10, streamFile);
config_target = vgmstream->codec_config;
xa_config = read_u16be(block_offset + 0x10, streamFile);
target_config = vgmstream->codec_config;
/* Sector subheader's file+channel markers are used to interleave streams (music/sfx/voices)
* by reading one target file+channel while ignoring the rest. This is needed to adjust
* CD drive spinning <> decoding speed (data is read faster otherwise, so can't have 2
* sectors of the same channel), Normally N channels = N streams (usually 8/16/32 depending
* on sample rate/stereo), though channels can be empty or contain video (like 7 or 15 video
* sectors + 1 audio frame). Normally interleaved channels use with the same file ID, but some
* games change ID too. Extractors deinterleave and split .xa using file + channel + EOF flags. */
* sectors of the same channel).
*
* Normally N channels = N streams (usually 8/16/32 depending on sample rate/stereo or even higher),
* though channels can be empty or contain video (like 7 or 15 video sectors + 1 audio frame).
* Usually nterleaved channels use with the same file ID, but some games change ID too. Channels
* don't always follow an order (so 00,01,02,08,06,00 is possible though uncommon, ex. Spyro 2)
*
* Extractors deinterleave and split .xa using file + channel + EOF flags.
* 'Channel' here doesn't mean "audio channel", just a fancy name for substreams (mono or stereo).
* Files can go up to 255, normally file 0=sequential, 1+=interleaved */
/* submode flag bits (typical audio value = 0x64 01100100)
* - 7 (0x80 10000000): end of file (usually at data end, not per subheader's file)
* - 6 (0x40 01000000): real time mode
* - 7 (0x80 10000000): end of file (usually at last sector of a channel or at data end)
* - 6 (0x40 01000000): real time sector (special control flag)
* - 5 (0x20 00100000): sector form (0=form1, 1=form2)
* - 4 (0x10 00010000): trigger (for application)
* - 4 (0x10 00010000): trigger (generates interrupt for the application)
* - 3 (0x08 00001000): data sector
* - 2 (0x04 00000100): audio sector
* - 1 (0x02 00000010): video sector
* - 0 (0x01 00000001): end of audio
* - 0 (0x01 00000001): end of audio (optional for non-real time XAs)
* Empty sectors with no flags may exist interleaved with other with audio/data.
*/
xa_submode = (uint8_t)read_8bit(block_offset + 0x12,streamFile);
xa_submode = read_u8(block_offset + 0x12,streamFile);
/* audio sector must set/not set certain flags, as per spec */
/* audio sector must set/not set certain flags, as per spec (in theory form2 only too) */
is_audio = !(xa_submode & 0x08) && (xa_submode & 0x04) && !(xa_submode & 0x02);
if (xa_target != config_target) {
//;VGM_LOG("XA block: ignored block at %x\n", (uint32_t)block_offset);
if (xa_config != target_config) {
block_samples = 0; /* not a target sector */
}
else if (is_audio) {
@ -65,8 +71,6 @@ void block_update_xa(off_t block_offset, VGMSTREAM * vgmstream) {
}
}
else {
;VGM_ASSERT_ONCE(block_offset < get_streamfile_size(streamFile),
"XA block: non audio block found at %x\n", (uint32_t)block_offset);
block_samples = 0; /* not an audio sector */
}

View File

@ -2,164 +2,72 @@
#include "../layout/layout.h"
#include "../coding/coding.h"
/* CD-XA - from Sony PS1 and Philips CD-i CD audio, also Saturn streams */
static int xa_read_subsongs(STREAMFILE *sf, int target_subsong, off_t start, uint16_t *p_stream_config, off_t *p_stream_offset, size_t *p_stream_size, int *p_form2);
static int xa_check_format(STREAMFILE *sf, off_t offset, int is_blocked);
/* XA - from Sony PS1 and Philips CD-i CD audio, also Saturn streams */
VGMSTREAM * init_vgmstream_xa(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL;
off_t start_offset;
int loop_flag = 0, channel_count, sample_rate;
int is_riff = 0, is_blocked = 0;
size_t file_size, stream_size;
int total_subsongs, /*target_subsong = streamFile->stream_index,*/ target_config;
int is_riff = 0, is_blocked = 0, is_form2 = 0;
size_t stream_size = 0;
int total_subsongs = 0, target_subsong = streamFile->stream_index;
uint16_t target_config = 0;
/* checks
* .xa: common
* .str: often (but not always) videos
* .adp: Phantasy Star Collection (SAT) raw XA */
if ( !check_extensions(streamFile,"xa,str,adp") )
/* checks */
/* .xa: common
* .str: often videos and sometimes speech/music
* .adp: Phantasy Star Collection (SAT) raw XA
* (extensionless): bigfiles [Castlevania: Symphony of the Night (PS1)] */
if (!check_extensions(streamFile,"xa,str,adp,"))
goto fail;
file_size = get_streamfile_size(streamFile);
/* Proper XA comes in raw (BIN 2352 mode2/form2) CD sectors, that contain XA subheaders.
* This also has minimal support for headerless (ISO 2048 mode1/data) mode. */
* Also has minimal support for headerless (ISO 2048 mode1/data) mode. */
/* check RIFF header = raw (optional, added when ripping and not part of the CD data) */
if (read_32bitBE(0x00,streamFile) == 0x52494646 && /* "RIFF" */
read_32bitBE(0x08,streamFile) == 0x43445841 && /* "CDXA" */
read_32bitBE(0x0C,streamFile) == 0x666D7420) { /* "fmt " */
if (read_u32be(0x00,streamFile) == 0x52494646 && /* "RIFF" */
read_u32be(0x08,streamFile) == 0x43445841 && /* "CDXA" */
read_u32be(0x0C,streamFile) == 0x666D7420) { /* "fmt " */
is_blocked = 1;
is_riff = 1;
start_offset = 0x2c; /* after "data" (chunk size tends to be a bit off) */
start_offset = 0x2c; /* after "data", ignore RIFF values as often are wrong */
}
else {
/* sector sync word = raw */
if (read_32bitBE(0x00,streamFile) == 0x00FFFFFF &&
read_32bitBE(0x04,streamFile) == 0xFFFFFFFF &&
read_32bitBE(0x08,streamFile) == 0xFFFFFF00) {
if (read_u32be(0x00,streamFile) == 0x00FFFFFF &&
read_u32be(0x04,streamFile) == 0xFFFFFFFF &&
read_u32be(0x08,streamFile) == 0xFFFFFF00) {
is_blocked = 1;
start_offset = 0x00;
}
else { /* headerless and possibly incorrectly ripped */
else {
/* headerless or possibly incorrectly ripped */
start_offset = 0x00;
}
}
/* test some blocks (except when RIFF) since other .XA/STR may start blank */
if (!is_riff) {
int i, j, block = 0, miss = 0;
off_t test_offset = start_offset;
const size_t sector_size = (is_blocked ? 0x900 : 0x800);
const size_t block_size = 0x80;
const int block_max = 3;
const int miss_max = 25;
while (block < block_max) {
uint8_t xa_submode = read_u8(test_offset + 0x12, streamFile);
int is_audio = !(xa_submode & 0x08) && (xa_submode & 0x04) && !(xa_submode & 0x02);
if (is_blocked && !is_audio) {
miss++;
if (block == 0 && miss > miss_max) /* no a single audio block found */
goto fail;
test_offset += sector_size + (is_blocked ? 0x18 + 0x18 : 0);
continue;
}
test_offset += (is_blocked ? 0x18 : 0x00); /* header */
for (i = 0; i < (sector_size / block_size); i++) {
/* XA headers checks: filter indexes should be 0..3, and shifts 0..C */
for (j = 0; j < 16; j++) {
uint8_t header = (uint8_t)read_8bit(test_offset + j, streamFile);
if (((header >> 4) & 0xF) > 0x03)
goto fail;
if (((header >> 0) & 0xF) > 0x0c)
goto fail;
}
/* XA headers pairs are repeated */
if (read_32bitBE(test_offset+0x00,streamFile) != read_32bitBE(test_offset+0x04,streamFile) ||
read_32bitBE(test_offset+0x08,streamFile) != read_32bitBE(test_offset+0x0c,streamFile))
goto fail;
/* blank frames should always use 0x0c0c0c0c (due to how shift works) */
if (read_32bitBE(test_offset+0x00,streamFile) == 0 &&
read_32bitBE(test_offset+0x04,streamFile) == 0 &&
read_32bitBE(test_offset+0x08,streamFile) == 0 &&
read_32bitBE(test_offset+0x0c,streamFile) == 0)
goto fail;
test_offset += 0x80;
}
test_offset += (is_blocked ? 0x18 : 0x00); /* footer */
block++;
}
}
/* test for XA data, since format is raw-ish (with RIFF it's assumed to be ok) */
if (!is_riff && !xa_check_format(streamFile, start_offset, is_blocked))
goto fail;
/* find subsongs as XA can interleave sectors using 'file' and 'channel' makers (see blocked_xa.c) */
if (is_blocked) {
off_t offset;
STREAMFILE *sf_test = NULL;
/* mini buffer to speed up by reading headers only (not sure if O.S. has buffer though */
sf_test = reopen_streamfile(streamFile, 0x10);
if (!sf_test) goto fail;
//TODO add subsongs (optimized for giant xa)
// - only do if first sector and next sector have different channels?
// N sectors of the same channel then N sectors of another should't happen, but
// we need to read all sectors to count samples anyway.
// - read all sectors
// - skip non-audio sectors
// - find first actual stream start
// - detect file+channel change + register new subsong (or detect file end flags too)
// - save total sectors subsong_sectors[subsong] = N for quick sample calcs + stream size
// (block_update is much slower since it buffers all data)
total_subsongs = 0;
target_config = -1;
offset = start_offset;
while (offset < file_size) {
uint16_t xa_subheader = read_u16be(offset + 0x10, sf_test);
uint8_t xa_submode = read_u8 (offset + 0x12, sf_test);
int is_audio = !(xa_submode & 0x08) && (xa_submode & 0x04) && !(xa_submode & 0x02);
if (!is_audio) {
offset += 0x900 + 0x18 + 0x18;
continue;
}
//if target_subsong ..
//total_subsongs++
//...
target_config = xa_subheader;
start_offset = offset; //stream_offset
break;
}
close_streamfile(sf_test);
stream_size = file_size;
//stream_size = ...;
//if (target_subsong == 0) target_subsong = 1;
//if (target_subsong < 0 || target_subsong > total_subsongs || total_subsongs < 1) goto fail;
/* file has no audio */
if (target_config < 0) {
VGM_LOG("XA: no audio found");
goto fail;
}
if (/*!is_riff &&*/ is_blocked) {
total_subsongs = xa_read_subsongs(streamFile, target_subsong, start_offset, &target_config, &start_offset, &stream_size, &is_form2);
if (total_subsongs <= 0) goto fail;
}
else {
stream_size = get_streamfile_size(streamFile) - start_offset;
}
/* data is ok: parse header */
if (is_blocked) {
/* parse 0x18 sector header (also see blocked_xa.c) */
uint8_t xa_header = (uint8_t)read_8bit(start_offset + 0x13,streamFile);
uint8_t xa_header = read_u8(start_offset + 0x13,streamFile);
switch((xa_header >> 0) & 3) { /* 0..1: mono/stereo */
case 0: channel_count = 1; break;
@ -181,13 +89,13 @@ VGMSTREAM * init_vgmstream_xa(STREAMFILE *streamFile) {
case 0: break;
default: /* shouldn't be used by games */
VGM_LOG("XA: unknown emphasis found\n");
break;
break;
}
switch((xa_header >> 7) & 1) { /* 7: reserved */
case 0: break;
default:
VGM_LOG("XA: unknown reserved bit found\n");
break;
break;
}
}
else {
@ -221,39 +129,220 @@ VGMSTREAM * init_vgmstream_xa(STREAMFILE *streamFile) {
vgmstream = allocate_vgmstream(channel_count,loop_flag);
if (!vgmstream) goto fail;
vgmstream->sample_rate = sample_rate;
vgmstream->meta_type = meta_XA;
vgmstream->sample_rate = sample_rate;
vgmstream->coding_type = coding_XA;
vgmstream->layout_type = is_blocked ? layout_blocked_xa : layout_none;
if (is_blocked) {
vgmstream->codec_config = target_config;
vgmstream->num_streams = total_subsongs;
vgmstream->stream_size = stream_size;
if (total_subsongs > 1) {
/* useful at times if game uses many file+channel */
snprintf(vgmstream->stream_name, STREAM_NAME_SIZE, "%04x", target_config);
}
}
vgmstream->num_samples = xa_bytes_to_samples(stream_size, channel_count, is_blocked, is_form2);
if ( !vgmstream_open_stream(vgmstream, streamFile, start_offset) )
goto fail;
if (is_blocked) {
/* calc num_samples as blocks may be empty or smaller than usual depending on flags */
vgmstream->next_block_offset = start_offset;
do {
block_update(vgmstream->next_block_offset,vgmstream);
vgmstream->num_samples += vgmstream->current_block_samples;
}
while (vgmstream->next_block_offset < file_size);
block_update(start_offset,vgmstream);
}
else {
vgmstream->num_samples = xa_bytes_to_samples(file_size - start_offset, channel_count, is_blocked);
}
return vgmstream;
fail:
if (vgmstream) close_vgmstream(vgmstream);
close_vgmstream(vgmstream);
return NULL;
}
static int xa_check_format(STREAMFILE *sf, off_t offset, int is_blocked) {
int i, j, sector = 0, skip = 0;
off_t test_offset = offset;
const size_t sector_size = (is_blocked ? 0x900 : 0x800);
const size_t extra_size = (is_blocked ? 0x18 : 0x00);
const size_t frame_size = 0x80;
const int sector_max = 3;
const int skip_max = 32; /* videos interleave 7 or 15 sectors + 1 audio sector, maybe 31 too */
/* test frames inside CD sectors */
while (sector < sector_max) {
uint8_t xa_submode = read_u8(test_offset + 0x12, sf);
int is_audio = !(xa_submode & 0x08) && (xa_submode & 0x04) && !(xa_submode & 0x02);
if (is_blocked && !is_audio) {
skip++;
if (sector == 0 && skip > skip_max) /* no a single audio sector found */
goto fail;
test_offset += sector_size + extra_size + extra_size;
continue;
}
test_offset += extra_size; /* header */
for (i = 0; i < (sector_size / frame_size); i++) {
/* XA frame checks: filter indexes should be 0..3, and shifts 0..C */
for (j = 0; j < 16; j++) {
uint8_t header = read_u8(test_offset + j, sf);
if (((header >> 4) & 0xF) > 0x03)
goto fail;
if (((header >> 0) & 0xF) > 0x0c)
goto fail;
}
/* XA headers pairs are repeated */
if (read_u32be(test_offset+0x00, sf) != read_u32be(test_offset+0x04, sf) ||
read_u32be(test_offset+0x08, sf) != read_u32be(test_offset+0x0c, sf))
goto fail;
/* blank frames should always use 0x0c0c0c0c (due to how shift works) */
if (read_u32be(test_offset+0x00, sf) == 0 &&
read_u32be(test_offset+0x04, sf) == 0 &&
read_u32be(test_offset+0x08, sf) == 0 &&
read_u32be(test_offset+0x0c, sf) == 0)
goto fail;
test_offset += 0x80;
}
test_offset += extra_size; /* footer */
sector++;
}
return 1;
fail:
return 0;
}
#define XA_SUBSONG_MAX 1024 /* +500 found in bigfiles like Castlevania SOTN */
typedef struct xa_subsong_t {
uint16_t config;
off_t start;
int form2;
int sectors;
int end_flag;
} xa_subsong_t;
/* Get subsong info, as real XA data interleaves N sectors/subsongs (often 8/16). Extractors deinterleave
* but we parse interleaved too for completeness. Even if we have a single deint'd XA this is useful to get
* usable sectors for bytes-to-samples.
*
* Bigfiles that paste tons of XA together are slow to parse since we need to read every sector to
* count totals, but XA subsong handling is mainly for educational purposes. */
static int xa_read_subsongs(STREAMFILE *sf, int target_subsong, off_t start, uint16_t *p_stream_config, off_t *p_stream_offset, size_t *p_stream_size, int *p_form2) {
xa_subsong_t *cur_subsong = NULL;
xa_subsong_t subsongs[XA_SUBSONG_MAX] = {0};
const size_t sector_size = 0x930;
uint16_t prev_config;
int i, subsongs_count = 0;
size_t file_size;
off_t offset;
STREAMFILE *sf_test = NULL;
uint8_t header[4];
/* buffer to speed up header reading; bigger (+0x8000) is actually faster than very small (~0x10),
* even though we only need sector headers and will end up reading the whole file that way */
sf_test = reopen_streamfile(sf, 0x10000);
if (!sf_test) goto fail;
prev_config = 0xFFFFu;
file_size = get_streamfile_size(sf);
offset = start;
/* read XA sectors */
while (offset < file_size) {
uint16_t xa_config;
uint8_t xa_submode;
int is_audio, is_eof;
read_streamfile(header, offset + 0x10, sizeof(header), sf_test);
xa_config = get_u16be(header + 0x00); /* file+channel markers */
xa_submode = get_u8 (header + 0x02); /* flags */
is_audio = !(xa_submode & 0x08) && (xa_submode & 0x04) && !(xa_submode & 0x02);
is_eof = (xa_submode & 0x80);
VGM_ASSERT((xa_submode & 0x01), "XA: end of audio at %lx\n", offset); /* used? */
//;VGM_ASSERT(is_eof, "XA: eof at %lx\n", offset);
//;VGM_ASSERT(!is_audio, "XA: not audio at %lx\n", offset);
if (!is_audio) {
offset += sector_size;
continue;
}
/* detect file+channel change = sector of new subsong or existing subsong
* (happens on every sector for interleaved XAs but only once for deint'd or video XAs) */
if (xa_config != prev_config)
{
/* find if this sector/config belongs to known subsong that hasn't ended
* (unsure if repeated configs+end flag is possible but probably in giant XAs) */
cur_subsong = NULL;
for (i = 0; i < subsongs_count; i++) { /* search algo could be improved, meh */
if (subsongs[i].config == xa_config && !subsongs[i].end_flag) {
cur_subsong = &subsongs[i];
break;
}
}
/* old subsong not found = add new to list */
if (cur_subsong == NULL) {
uint8_t xa_channel = get_u8(header + 0x01);
/* when file+channel changes mark prev subsong of the same channel as finished
* (this ensures reused ids in bigfiles are handled properly, ex.: 0101... 0201... 0101...) */
for (i = subsongs_count - 1; i >= 0; i--) {
uint16_t subsong_config = subsongs[i].config;
uint8_t subsong_channel = subsong_config & 0xFF;
if (xa_config != subsong_config && xa_channel == subsong_channel) {
subsongs[i].end_flag = 1;
break;
}
}
cur_subsong = &subsongs[subsongs_count];
subsongs_count++;
if (subsongs_count >= XA_SUBSONG_MAX) goto fail;
cur_subsong->config = xa_config;
cur_subsong->form2 = (xa_submode & 0x20);
cur_subsong->start = offset;
//cur_subsong->sectors = 0;
//cur_subsong->end_flag = 0;
//;VGM_LOG("XA: new subsong %i with config %04x at %lx\n", subsongs_count, xa_config, offset);
}
prev_config = xa_config;
}
else {
if (cur_subsong == NULL) goto fail;
}
cur_subsong->sectors++;
if (is_eof)
cur_subsong->end_flag = 1;
offset += sector_size;
}
VGM_ASSERT(subsongs_count < 1, "XA: no audio found\n");
if (target_subsong == 0) target_subsong = 1;
if (target_subsong < 0 || target_subsong > subsongs_count || subsongs_count < 1) goto fail;
cur_subsong = &subsongs[target_subsong - 1];
*p_stream_config = cur_subsong->config;
*p_stream_offset = cur_subsong->start;
*p_stream_size = cur_subsong->sectors * sector_size;
*p_form2 = cur_subsong->form2;
//;VGM_LOG("XA: subsong config=%x, offset=%lx, size=%x, form2=%i\n", *p_stream_config, *p_stream_offset, *p_stream_size, *p_form2);
close_streamfile(sf_test);
return subsongs_count;
fail:
close_streamfile(sf_test);
return 0;
}