Fix rare EALayer3 with proper loops [Need for Speed: World (PC)]

This commit is contained in:
bnnm 2018-08-12 14:03:04 +02:00
parent 7f424bb7e9
commit c7f9a47fcd

View File

@ -8,7 +8,7 @@
#define EAAC_VERSION_V0 0x00 /* SNR/SNS */
#define EAAC_VERSION_V1 0x01 /* SPS */
#define EAAC_CODEC_NONE 0x00 /* internal 'codec not set' flag */
#define EAAC_CODEC_NONE 0x00 /* internal 'codec not set' */
#define EAAC_CODEC_RESERVED 0x01 /* not used/reserved? /MP30/P6L0/P2B0/P2L0/P8S0/P8U0/PFN0? */
#define EAAC_CODEC_PCM 0x02
#define EAAC_CODEC_EAXMA 0x03
@ -26,7 +26,7 @@
#define EAAC_FLAG_STREAMED 0x04
#define EAAC_BLOCKID0_DATA 0x00
#define EAAC_BLOCKID0_END 0x80
#define EAAC_BLOCKID0_END 0x80 /* maybe meant to be a bitflag? */
#define EAAC_BLOCKID1_HEADER 0x48 /* 'H' */
#define EAAC_BLOCKID1_DATA 0x44 /* 'D' */
@ -36,6 +36,8 @@ static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, ST
static size_t get_snr_size(STREAMFILE *streamFile, off_t offset);
static VGMSTREAM *parse_s10a_header(STREAMFILE *streamFile, off_t offset, uint16_t target_index, off_t ast_offset);
/* .SNR+SNS - from EA latest games (~2008-2013), v0 header */
VGMSTREAM * init_vgmstream_ea_snr_sns(STREAMFILE * streamFile) {
VGMSTREAM * vgmstream = NULL;
@ -131,7 +133,7 @@ fail:
}
/* .SPS - from Frostbite engine games, v1 header */
VGMSTREAM * init_vgmstream_ea_sps_fb(STREAMFILE *streamFile) {
VGMSTREAM * init_vgmstream_ea_sps_fb(STREAMFILE *streamFile) { //todo remove in the future, use better extractors
VGMSTREAM * vgmstream = NULL;
off_t start_offset = 0, header_offset = 0, sps_offset, max_offset;
@ -459,6 +461,30 @@ fail:
return NULL;
}
/* ************************************************************************* */
typedef struct {
int version;
int codec;
int channel_config;
int sample_rate;
int flags;
int streamed;
int channels;
int num_samples;
int loop_start;
int loop_end;
int loop_flag;
off_t stream_offset;
off_t loop_offset;
} eaac_header;
static segmented_layout_data* build_segmented_eaaudiocore_looping(STREAMFILE *streamData, eaac_header *eaac);
/* EA newest header from RwAudioCore (RenderWare?) / EAAudioCore library (still generated by sx.exe).
* Audio "assets" come in separate RAM headers (.SNR/SPH) and raw blocked streams (.SNS/SPS),
* or together in pseudoformats (.SNU, .SBR+.SBS banks, .AEMS, .MUS, etc).
@ -466,91 +492,108 @@ fail:
static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, STREAMFILE * streamData, off_t header_offset, off_t start_offset, meta_t meta_type) {
VGMSTREAM * vgmstream = NULL;
STREAMFILE* temp_streamFile = NULL;
int channel_count, loop_flag = 0, streamed, version, codec, channel_config, flags;
int32_t header1, header2, sample_rate, num_samples, loop_start = 0, loop_end = 0;
uint32_t header1, header2;
eaac_header eaac = {0};
/* EA SNR/SPH header */
/* 4 bits: version */
/* 4 bits: codec */
/* 6 bits: channel config */
/* 18 bits: sample rate */
/* 4 bits: flags */
/* 28 bits: number of samples */
header1 = read_32bitBE(header_offset + 0x00, streamHead);
header2 = read_32bitBE(header_offset + 0x04, streamHead);
version = (header1 >> 28) & 0x0F;
codec = (header1 >> 24) & 0x0F;
channel_config = (header1 >> 18) & 0x3F;
sample_rate = (header1 & 0x03FFFF); /* some Dead Space 2 (PC) uses 96000 */
flags = (header2 >> 28) & 0x0F; /* TODO: maybe even 3 bits and not 4? */
num_samples = (header2 & 0x0FFFFFFF);
/* rest is optional, depends on flags header used (ex. SNU and SPS may have bigger headers): */
/* 0x02: loop start sample, 0x00/04: nothing, 0x06: loop start sample and loop start block offset */
header1 = (uint32_t)read_32bitBE(header_offset + 0x00, streamHead);
header2 = (uint32_t)read_32bitBE(header_offset + 0x04, streamHead);
eaac.version = (header1 >> 28) & 0x0F; /* 4 bits */
eaac.codec = (header1 >> 24) & 0x0F; /* 4 bits */
eaac.channel_config = (header1 >> 18) & 0x3F; /* 6 bits */
eaac.sample_rate = (header1 & 0x03FFFF); /* 18 bits (some Dead Space 2 (PC) do use 96000) */
eaac.flags = (header2 >> 28) & 0x0F; /* 4 bits *//* TODO: maybe even 3 bits and not 4? */
eaac.num_samples = (header2 & 0x0FFFFFFF); /* 28 bits */
/* rest is optional, depends on used flags and codec (handled below) */
eaac.stream_offset = start_offset;
/* V0: SNR+SNS, V1: SPR+SPS (no apparent differences, other than the block flags used) */
if (version != EAAC_VERSION_V0 && version != EAAC_VERSION_V1) {
VGM_LOG("EA SNS/SPS: unknown version\n");
/* V0: SNR+SNS, V1: SPR+SPS (no apparent differences, other than block flags) */
if (eaac.version != EAAC_VERSION_V0 && eaac.version != EAAC_VERSION_V1) {
VGM_LOG("EA EAAC: unknown version\n");
goto fail;
}
if (flags != EAAC_FLAG_NONE &&
!(flags & (EAAC_FLAG_LOOPED | EAAC_FLAG_STREAMED))) {
VGM_LOG("EA SNS/SPS: unknown flags 0x%02x\n", flags);
/* catch unknown/garbage values just in case */
if (eaac.flags != EAAC_FLAG_NONE && !(eaac.flags & (EAAC_FLAG_LOOPED | EAAC_FLAG_STREAMED))) {
VGM_LOG("EA EAAC: unknown flags 0x%02x\n", eaac.flags);
goto fail;
}
/* TODO: Properly implement looping, needed for Need for Speed: World (PC) */
if (flags & EAAC_FLAG_LOOPED) {
loop_flag = 1;
loop_start = 0;
loop_end = num_samples;
}
/* Non-streamed sounds are stored as a single block */
streamed = (flags & EAAC_FLAG_STREAMED) != 0;
/* get loops (fairly involved due to the multiple layouts and mutant streamfiles)
* full loops aren't too uncommon [Dead Space (PC) sfx/ambiance],
* while actual looping is very rare [Need for Speed: World (PC)] */
if (eaac.flags & EAAC_FLAG_LOOPED) {
eaac.loop_flag = 1;
/* accepted channel configs only seem to be mono/stereo/quad/5.1/7.1 */
/* fail with unknown values just in case */
switch(channel_config) {
case 0x00: channel_count = 1; break;
case 0x01: channel_count = 2; break;
case 0x03: channel_count = 4; break;
case 0x05: channel_count = 6; break;
case 0x07: channel_count = 8; break;
default:
VGM_LOG("EA SNS/SPS: unknown channel config 0x%02x\n", channel_config);
eaac.loop_start = read_32bitBE(header_offset+0x08, streamHead);
eaac.loop_end = eaac.num_samples;
//todo header size always depends on flags or is implicit by loop values? see get_snr_size
if (eaac.loop_start > 0)
eaac.loop_offset = read_32bitBE(header_offset+0x0c, streamHead); /* normal loop */
else
eaac.loop_offset = eaac.stream_offset; /* full loop */
//todo EATrax has extra values in header, which would coexist with loop values
if (eaac.codec == EAAC_CODEC_EATRAX) {
VGM_LOG("EA EAAC: unknown loop header for EATrax\n");
goto fail;
}
//todo need more cases to test how layout/streamfiles react
if (eaac.loop_start > 0 && !(eaac.codec == EAAC_CODEC_EALAYER3_V1 ||
eaac.codec == EAAC_CODEC_EALAYER3_V2_PCM || eaac.codec == EAAC_CODEC_EALAYER3_V2_SPIKE)) {
VGM_LOG("EA EAAC: unknown actual looping for non-EALayer3\n");
goto fail;
}
}
/* Non-streamed sounds are stored as a single block (may not set block end flags) */
eaac.streamed = (eaac.flags & EAAC_FLAG_STREAMED) != 0;
/* accepted channel configs only seem to be mono/stereo/quad/5.1/7.1, from debug strings */
switch(eaac.channel_config) {
case 0x00: eaac.channels = 1; break;
case 0x01: eaac.channels = 2; break;
case 0x03: eaac.channels = 4; break;
case 0x05: eaac.channels = 6; break;
case 0x07: eaac.channels = 8; break;
default:
VGM_LOG("EA EAAC: unknown channel config 0x%02x\n", eaac.channel_config);
goto fail; /* fail with unknown values just in case */
}
/* build the VGMSTREAM */
vgmstream = allocate_vgmstream(channel_count,loop_flag);
vgmstream = allocate_vgmstream(eaac.channels,eaac.loop_flag);
if (!vgmstream) goto fail;
vgmstream->sample_rate = sample_rate;
vgmstream->num_samples = num_samples;
vgmstream->loop_start_sample = loop_start;
vgmstream->loop_end_sample = loop_end;
vgmstream->sample_rate = eaac.sample_rate;
vgmstream->num_samples = eaac.num_samples;
vgmstream->loop_start_sample = eaac.loop_start;
vgmstream->loop_end_sample = eaac.loop_end;
vgmstream->meta_type = meta_type;
/* EA decoder list and known internal FourCCs */
switch(codec) {
switch(eaac.codec) {
case EAAC_CODEC_PCM: /* "P6B0": PCM16BE [NBA Jam (Wii)] */
case EAAC_CODEC_PCM: /* "P6B0": PCM16BE [NBA Jam (Wii)] */
vgmstream->coding_type = coding_PCM16_int;
vgmstream->codec_endian = 1;
vgmstream->layout_type = layout_blocked_ea_sns;
break;
#ifdef VGM_USE_FFMPEG
case EAAC_CODEC_EAXMA: { /* "EXm0": EA-XMA [Dante's Inferno (X360)] */
case EAAC_CODEC_EAXMA: { /* "EXm0": EA-XMA [Dante's Inferno (X360)] */
uint8_t buf[0x100];
int bytes, block_size, block_count;
size_t stream_size, virtual_size;
ffmpeg_custom_config cfg = {0};
stream_size = get_streamfile_size(streamData) - start_offset;
virtual_size = ffmpeg_get_eaxma_virtual_size(vgmstream->channels, streamed, start_offset,stream_size, streamData);
stream_size = get_streamfile_size(streamData) - eaac.stream_offset; //todo not correct for banks
virtual_size = ffmpeg_get_eaxma_virtual_size(vgmstream->channels, eaac.streamed, eaac.stream_offset,stream_size, streamData);
block_size = 0x10000; /* todo unused and not correctly done by the parser */
block_count = stream_size / block_size + (stream_size % block_size ? 1 : 0);
@ -561,7 +604,7 @@ static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, ST
cfg.virtual_size = virtual_size;
cfg.channels = vgmstream->channels;
vgmstream->codec_data = init_ffmpeg_config(streamData, buf,bytes, start_offset,stream_size, &cfg);
vgmstream->codec_data = init_ffmpeg_config(streamData, buf,bytes, eaac.stream_offset,stream_size, &cfg);
if (!vgmstream->codec_data) goto fail;
vgmstream->coding_type = coding_FFmpeg;
@ -570,46 +613,60 @@ static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, ST
}
#endif
case EAAC_CODEC_XAS: /* "Xas1": EA-XAS [Dead Space (PC/PS3)] */
case EAAC_CODEC_XAS: /* "Xas1": EA-XAS [Dead Space (PC/PS3)] */
vgmstream->coding_type = coding_EA_XAS;
vgmstream->layout_type = layout_blocked_ea_sns;
break;
#ifdef VGM_USE_MPEG
case EAAC_CODEC_EALAYER3_V1: /* "EL31": EALayer3 v1 [Need for Speed: Hot Pursuit (PS3)] */
case EAAC_CODEC_EALAYER3_V2_PCM: /* "L32P": EALayer3 v2 "PCM" [Battlefield 1943 (PS3)] */
case EAAC_CODEC_EALAYER3_V2_SPIKE: { /* "L32S": EALayer3 v2 "Spike" [Dante's Inferno (PS3)] */
case EAAC_CODEC_EALAYER3_V1: /* "EL31": EALayer3 v1 [Need for Speed: Hot Pursuit (PS3)] */
case EAAC_CODEC_EALAYER3_V2_PCM: /* "L32P": EALayer3 v2 "PCM" [Battlefield 1943 (PS3)] */
case EAAC_CODEC_EALAYER3_V2_SPIKE: { /* "L32S": EALayer3 v2 "Spike" [Dante's Inferno (PS3)] */
mpeg_custom_config cfg = {0};
mpeg_custom_t type = (codec == 0x05 ? MPEG_EAL31b : (codec == 0x06) ? MPEG_EAL32P : MPEG_EAL32S);
mpeg_custom_t type = (eaac.codec == 0x05 ? MPEG_EAL31b : (eaac.codec == 0x06) ? MPEG_EAL32P : MPEG_EAL32S);
/* remove blocks on reads for some edge cases in L32P and to properly apply discard modes
* (otherwise, and removing discards, it'd work with layout_blocked_ea_sns) */
temp_streamFile = setup_eaac_streamfile(streamData, version, codec, streamed, start_offset, 0);
if (!temp_streamFile) goto fail;
/* EALayer3 needs custom IO that removes blocks on reads to fix some edge cases in L32P
* and to properly apply discard modes (see ealayer3 decoder)
* (otherwise, and after removing discard code, it'd work with layout_blocked_ea_sns) */
start_offset = 0x00; /* must point to the custom streamfile's beginning */
/* layout is still blocks, but should work fine with the custom mpeg decoder */
vgmstream->codec_data = init_mpeg_custom(temp_streamFile, start_offset, &vgmstream->coding_type, vgmstream->channels, type, &cfg);
if (!vgmstream->codec_data) goto fail;
vgmstream->layout_type = layout_none;
if (eaac.loop_start > 0) { /* special (if hacky) loop handling, see comments */
segmented_layout_data *data = build_segmented_eaaudiocore_looping(streamData, &eaac);
if (!data) goto fail;
vgmstream->layout_data = data;
vgmstream->coding_type = data->segments[0]->coding_type;
vgmstream->layout_type = layout_segmented;
}
else {
temp_streamFile = setup_eaac_streamfile(streamData, eaac.version, eaac.codec, eaac.streamed, eaac.stream_offset,0);
if (!temp_streamFile) goto fail;
vgmstream->codec_data = init_mpeg_custom(temp_streamFile, start_offset, &vgmstream->coding_type, vgmstream->channels, type, &cfg);
if (!vgmstream->codec_data) goto fail;
vgmstream->layout_type = layout_none;
}
break;
}
#endif
case EAAC_CODEC_DSP: /* "Gca0"?: DSP [Need for Speed: Nitro sfx (Wii)] */
case EAAC_CODEC_DSP: /* "Gca0"?: DSP [Need for Speed: Nitro (Wii) sfx] */
vgmstream->coding_type = coding_NGC_DSP;
vgmstream->layout_type = layout_blocked_ea_sns;
/* DSP coefs are read in the blocks */
break;
#ifdef VGM_USE_ATRAC9
case EAAC_CODEC_EATRAX: { /* EATrax */
case EAAC_CODEC_EATRAX: { /* EATrax (unknown FourCC) [Need for Speed: Most Wanted (Vita)] */
atrac9_config cfg = {0};
size_t total_size;
cfg.channels = vgmstream->channels;
/* EATrax is "buffered" ATRAC9, uses custom IO since it's kind of complex to add to the decoder */
start_offset = 0x00; /* must point to the custom streamfile's beginning */
cfg.channels = eaac.channels;
cfg.config_data = read_32bitBE(header_offset + 0x08,streamHead);
/* 0x10: frame size? (same as config data?) */
total_size = read_32bitLE(header_offset + 0x0c,streamHead); /* actual data size without blocks, LE b/c why make sense */
@ -619,11 +676,9 @@ static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, ST
vgmstream->coding_type = coding_ATRAC9;
vgmstream->layout_type = layout_none;
/* EATrax is "buffered" ATRAC9, uses custom IO since it's kind of complex to add to the decoder */
temp_streamFile = setup_eaac_streamfile(streamData, version, codec, streamed, start_offset, total_size);
temp_streamFile = setup_eaac_streamfile(streamData, eaac.version, eaac.codec, eaac.streamed, eaac.stream_offset, total_size);
if (!temp_streamFile) goto fail;
start_offset = 0x00; /* must point to the custom streamfile's beginning */
break;
}
#endif
@ -633,7 +688,7 @@ static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, ST
case EAAC_CODEC_EAOPUS: /* EAOpus (inside each SNS/SPS block is 16b frame size + standard? Opus packet) */
/* TODO */
default:
VGM_LOG("EA SNS/SPS: unknown codec 0x%02x\n", codec);
VGM_LOG("EA EAAC: unknown codec 0x%02x\n", eaac.codec);
goto fail;
}
@ -660,3 +715,73 @@ static size_t get_snr_size(STREAMFILE *streamFile, off_t offset) {
default: return 0x08;
}
}
/* Actual looping uses 2 block sections, separated by a block end flag *and* padded.
*
* We use the segmented layout, since the eaac_streamfile doesn't handle padding properly ATM
* (getting EALayer3 frame sizes + skip sizes can be fairly involved), plus seems likely
* that after a block end the decoder needs to be reset (not possible from a streamfile).
*
* Or could fix the blocked_layout+L32P bug, though that involves a lot of rewrites.
* So this is the simplest, surest way ATM (if very ugly). */
// todo consider better ways to handle this once more looped files for other codecs are found
static segmented_layout_data* build_segmented_eaaudiocore_looping(STREAMFILE *streamData, eaac_header *eaac) {
segmented_layout_data *data = NULL;
STREAMFILE* temp_streamFile[2] = {0};
off_t offsets[2] = { eaac->stream_offset, eaac->loop_offset };
off_t sizes[2] = { eaac->stream_offset + eaac->loop_offset, 0}; /* 0 = let streamfile guess */
int num_samples[2] = { eaac->loop_start, eaac->num_samples - eaac->loop_start};
int segment_count = 2; /* intro/loop */
int i;
/* init layout */
data = init_layout_segmented(segment_count);
if (!data) goto fail;
for (i = 0; i < segment_count; i++) {
temp_streamFile[i] = setup_eaac_streamfile(streamData, eaac->version,eaac->codec,eaac->streamed, offsets[i], sizes[i]);
if (!temp_streamFile[i]) goto fail;
data->segments[i] = allocate_vgmstream(eaac->channels, 0);
if (!data->segments[i]) goto fail;
data->segments[i]->sample_rate = eaac->sample_rate;
data->segments[i]->num_samples = num_samples[i];
//data->segments[i]->meta_type = eaac->meta_type; /* bleh */
switch(eaac->codec) {
#ifdef VGM_USE_MPEG
case EAAC_CODEC_EALAYER3_V1:
case EAAC_CODEC_EALAYER3_V2_PCM:
case EAAC_CODEC_EALAYER3_V2_SPIKE: {
mpeg_custom_config cfg = {0};
mpeg_custom_t type = (eaac->codec == 0x05 ? MPEG_EAL31b : (eaac->codec == 0x06) ? MPEG_EAL32P : MPEG_EAL32S);
data->segments[i]->codec_data = init_mpeg_custom(temp_streamFile[i], 0x00, &data->segments[i]->coding_type, eaac->channels, type, &cfg);
if (!data->segments[i]->codec_data) goto fail;
data->segments[i]->layout_type = layout_none;
break;
}
#endif
default:
goto fail;
}
if (!vgmstream_open_stream(data->segments[i],temp_streamFile[i],0x00))
goto fail;
}
/* setup segmented VGMSTREAMs */
if (!setup_layout_segmented(data))
goto fail;
data->loop_segment = 1;
return data;
fail:
for (i = 0; i < segment_count; i++)
close_streamfile(temp_streamFile[i]);
free_layout_segmented(data);
return NULL;
}