vgmstream/src/meta/ea_eaac.c

464 lines
18 KiB
C
Raw Normal View History

#include "meta.h"
#include "../layout/layout.h"
#include "../coding/coding.h"
#include "ea_eaac_streamfile.h"
/* EAAudioCore formats, EA's current audio middleware */
static VGMSTREAM * init_vgmstream_eaaudiocore_header(STREAMFILE * streamHead, STREAMFILE * streamData, off_t header_offset, off_t start_offset, meta_t meta_type);
2018-07-21 22:09:44 +02:00
static size_t get_snr_size(STREAMFILE *streamFile, off_t offset);
/* .SNR+SNS - from EA latest games (~2008-2013), v0 header */
VGMSTREAM * init_vgmstream_ea_snr_sns(STREAMFILE * streamFile) {
VGMSTREAM * vgmstream = NULL;
STREAMFILE * streamData = NULL;
/* check extension, case insensitive */
if (!check_extensions(streamFile,"snr"))
goto fail;
/* SNR headers normally need an external SNS file, but some have data [Burnout Paradise, NFL2013 (iOS)] */
2017-12-02 02:56:37 +01:00
if (get_streamfile_size(streamFile) > 0x10) {
2018-07-21 22:09:44 +02:00
off_t start_offset = get_snr_size(streamFile, 0x00);
2017-12-02 02:56:37 +01:00
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, 0x00, start_offset, meta_EA_SNR_SNS);
if (!vgmstream) goto fail;
}
else {
streamData = open_streamfile_by_ext(streamFile,"sns");
if (!streamData) goto fail;
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamData, 0x00, 0x00, meta_EA_SNR_SNS);
if (!vgmstream) goto fail;
}
close_streamfile(streamData);
return vgmstream;
fail:
close_streamfile(streamData);
return NULL;
}
/* .SPS - from EA latest games (~2014), v1 header */
VGMSTREAM * init_vgmstream_ea_sps(STREAMFILE * streamFile) {
VGMSTREAM * vgmstream = NULL;
off_t start_offset;
/* check extension, case insensitive */
if (!check_extensions(streamFile,"sps"))
goto fail;
2017-12-17 17:38:54 +01:00
/* SPS block start: 0x00(1): block flag (header=0x48); 0x01(3): block size (usually 0x0c-0x14) */
if (read_8bit(0x00, streamFile) != 0x48)
goto fail;
2017-12-17 17:38:54 +01:00
start_offset = read_32bitBE(0x00, streamFile) & 0x00FFFFFF;
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, 0x04, start_offset, meta_EA_SPS);
if (!vgmstream) goto fail;
return vgmstream;
fail:
close_vgmstream(vgmstream);
return NULL;
}
/* .SNU - from EA Redwood Shores/Visceral games (Dead Space, Dante's Inferno, The Godfather 2), v0 header */
VGMSTREAM * init_vgmstream_ea_snu(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL;
off_t start_offset, header_offset;
int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL;
/* check extension, case insensitive */
if (!check_extensions(streamFile,"snu"))
goto fail;
/* EA SNU header (BE/LE depending on platform) */
/* 0x00(1): related to sample rate? (03=48000)
* 0x01(1): flags/count? (when set has extra block data before start_offset)
* 0x02(1): always 0?
* 0x03(1): channels? (usually matches but rarely may be 0)
* 0x04(4): some size, maybe >>2 ~= number of frames
* 0x08(4): start offset
* 0x0c(4): some sub-offset? (0x20, found when @0x01 is set) */
/* use start_offset as endianness flag */
if (guess_endianness32bit(0x08,streamFile)) {
read_32bit = read_32bitBE;
} else {
read_32bit = read_32bitLE;
}
header_offset = 0x10; /* SNR header */
start_offset = read_32bit(0x08,streamFile); /* SPS blocks */
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, header_offset, start_offset, meta_EA_SNU);
if (!vgmstream) goto fail;
return vgmstream;
fail:
close_vgmstream(vgmstream);
return NULL;
}
/* .SPS - from Frostbite engine games, v1 header */
VGMSTREAM * init_vgmstream_ea_sps_fb(STREAMFILE *streamFile) {
VGMSTREAM * vgmstream = NULL;
off_t start_offset = 0, header_offset = 0, sps_offset, max_offset;
/* checks */
/* should be .sps once extracted (filenames are hashed) */
if (!check_extensions(streamFile,"sps"))
goto fail;
if (read_32bitBE(0x00,streamFile) != 0x011006C0 && /* Need for Speed: The Run (PS3), Need for Speed: Rivals (PS4) */
read_32bitBE(0x00,streamFile) != 0x01100180 && /* Need for Speed: The Run (X360) */
read_32bitBE(0x00,streamFile) != 0x01100000) /* Need for Speed: The Run (PC) */
goto fail;
/* file has a Frostbite descriptor (SoundWaveAsset segments) data before actual .sps, exact size unknown.
* 0x00: segments/flags/sizes? 0x04: SegmentLength?, 0x08: SeekTableOffset?, 0x0c: mini SPS header
* rest: unknown fields? may be padded? (ex. 0x22 > 0x24, 0x1d > 0x20 */
/* actual offsets are probably somewhere but for now just manually search. */
sps_offset = read_32bitBE(0x08, streamFile); /* seek table, number of entries unknown */
max_offset = sps_offset + 0x3000;
if (max_offset > get_streamfile_size(streamFile))
max_offset = get_streamfile_size(streamFile);
/* find .sps start block */
while (sps_offset < max_offset) {
if ((read_32bitBE(sps_offset, streamFile) & 0xFFFFFF00) == 0x48000000) {
header_offset = sps_offset + 0x04;
start_offset = sps_offset + (read_32bitBE(sps_offset, streamFile) & 0x00FFFFFF);
break;
}
sps_offset += 0x04;
}
if (!start_offset)
goto fail; /* not found */
vgmstream = init_vgmstream_eaaudiocore_header(streamFile, streamFile, header_offset, start_offset, meta_EA_SPS);
if (!vgmstream) goto fail;
return vgmstream;
fail:
close_vgmstream(vgmstream);
return NULL;
}
2018-07-21 20:58:01 +02:00
/* EA HDR/STH/DAT - used for storing speech in some newer games */
VGMSTREAM * init_vgmstream_ea_hdr_sth_dat(STREAMFILE *streamFile) {
int target_stream = streamFile->stream_index;
uint8_t userdata_size, total_sounds, block_id, flags;
uint8_t i;
off_t snr_offset, sns_offset;
size_t file_size, block_size;
STREAMFILE *datFile = NULL, *sthFile = NULL;
VGMSTREAM *vgmstream;
/* 0x00: ID */
/* 0x02: userdata size */
/* 0x03: number of files */
/* 0x04: sub-ID (used for different police voices in NFS games) */
/* 0x08: alt number of files? */
/* 0x09: zero */
/* 0x0A: ??? */
/* 0x0C: zero */
/* 0x10: table start */
sthFile = open_streamfile_by_ext(streamFile, "sth");
if (!sthFile)
goto fail;
datFile = open_streamfile_by_ext(streamFile, "dat");
if (!datFile)
goto fail;
/* STH always starts with the first offset of zero */
sns_offset = read_32bitLE(0x00, sthFile);
if (sns_offset != 0)
goto fail;
/* check if DAT starts with a correct SNS block */
block_id = read_8bit(0x00, datFile);
if (block_id != 0x00 && block_id != 0x80)
goto fail;
userdata_size = read_8bit(0x02, streamFile);
total_sounds = read_8bit(0x03, streamFile);
if (read_8bit(0x08, streamFile) > total_sounds)
goto fail;
if (target_stream == 0) target_stream = 1;
if (target_stream < 0 || total_sounds == 0 || target_stream > total_sounds)
goto fail;
/* offsets in HDR are always big endian */
//snr_offset = (off_t)read_16bitBE(0x10 + (0x02+userdata_size) * (target_stream-1), streamFile) + 0x04;
//sns_offset = read_32bit(snr_offset, sthFile);
/* we can't reliably detect byte endianness so we're going to find the sound the hacky way */
/* go through blocks until we reach the goal sound */
file_size = get_streamfile_size(datFile);
snr_offset = 0;
sns_offset = 0;
for (i = 0; i < total_sounds; i++) {
snr_offset = (off_t)read_16bitBE(0x10 + (0x02+userdata_size) * i, streamFile) + 0x04;
2018-07-21 22:09:44 +02:00
flags = (read_8bit(snr_offset + 0x04, sthFile) >> 4) & 0x0F;
2018-07-21 20:58:01 +02:00
if (i == target_stream - 1)
break;
while (true) {
if (sns_offset >= file_size)
goto fail;
block_id = read_8bit(sns_offset, datFile);
block_size = read_32bitBE(sns_offset, datFile) & 0x00FFFFFF;
if (block_size == 0)
goto fail;
if (block_id != 0x00 && block_id != 0x80)
goto fail;
sns_offset += block_size;
2018-07-21 22:09:44 +02:00
if (flags & 4) {
2018-07-21 20:58:01 +02:00
if (block_id == 0x80)
break;
}
else {
break;
}
}
}
block_id = read_8bit(sns_offset, datFile);
if (block_id != 0x00 && block_id != 0x80)
goto fail;
vgmstream = init_vgmstream_eaaudiocore_header(sthFile, datFile, snr_offset, sns_offset, meta_EA_SNR_SNS);
if (!vgmstream)
goto fail;
vgmstream->num_streams = total_sounds;
close_streamfile(sthFile);
close_streamfile(datFile);
return vgmstream;
fail:
close_streamfile(sthFile);
close_streamfile(datFile);
return NULL;
}
/* 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).
* Some .SNR include stream data, while .SPS have headers so .SPH is optional. */
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;
2018-03-31 20:10:57 +02:00
STREAMFILE* temp_streamFile = NULL;
2018-07-21 22:09:44 +02:00
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;
/* EA SNR/SPH header */
2018-07-21 22:09:44 +02:00
/* 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 */
/* V0: SNR+SNS, V1: SPR+SPS (no apparent differences, other than the block flags used) */
if (version != 0 && version != 1) {
VGM_LOG("EA SNS/SPS: unknown version\n");
goto fail;
}
2018-07-21 22:09:44 +02:00
/* 0x04: stream asset, 0x02: full loop, 0x00: default/RAM asset */
if (flags != 0x06 && flags != 0x04 && flags != 0x02 && flags != 0x00) {
VGM_LOG("EA SNS/SPS: unknown flag 0x%02x\n", flags);
goto fail;
}
2018-07-21 22:09:44 +02:00
/* TODO: Properly implement looping, needed for Need for Speed: World (PC) */
if (flags & 0x02) {
loop_flag = 1;
loop_start = 0;
loop_end = num_samples;
}
2018-07-21 22:09:44 +02:00
/* Non-streamed sounds are stored as a single block */
streamed = (flags & 0x04) != 0;
/* accepted channel configs only seem to be mono/stereo/quad/5.1/7.1 */
2018-07-21 22:09:44 +02:00
/* fail with unknown values just in case */
switch(channel_config) {
case 0x00: channel_count = 1; break;
2018-07-21 22:09:44 +02:00
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);
goto fail;
}
/* build the VGMSTREAM */
vgmstream = allocate_vgmstream(channel_count,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->meta_type = meta_type;
/* EA decoder list and known internal FourCCs */
switch(codec) {
case 0x02: /* "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 0x03: { /* "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;
2018-07-21 22:09:44 +02:00
virtual_size = ffmpeg_get_eaxma_virtual_size(vgmstream->channels, streamed, start_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);
bytes = ffmpeg_make_riff_xma2(buf, 0x100, vgmstream->num_samples, virtual_size, vgmstream->channels, vgmstream->sample_rate, block_count, block_size);
if (bytes <= 0) goto fail;
cfg.type = FFMPEG_EA_XMA;
cfg.virtual_size = virtual_size;
cfg.channels = vgmstream->channels;
vgmstream->codec_data = init_ffmpeg_config(streamData, buf,bytes, start_offset,stream_size, &cfg);
if (!vgmstream->codec_data) goto fail;
vgmstream->coding_type = coding_FFmpeg;
vgmstream->layout_type = layout_none;
break;
}
#endif
case 0x04: /* "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 0x05: /* "EL31": EALayer3 v1 [Need for Speed: Hot Pursuit (PS3)] */
case 0x06: /* "L32P": EALayer3 v2 "PCM" [Battlefield 1943 (PS3)] */
case 0x07: { /* "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);
/* 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) */
2018-07-21 22:09:44 +02:00
temp_streamFile = setup_eaac_streamfile(streamData, version, codec, streamed, start_offset, 0);
if (!temp_streamFile) goto fail;
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;
break;
}
#endif
case 0x08: /* "Gca0"?: DSP [Need for Speed: Nitro sfx (Wii)] */
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 0x0a: { /* EATrax */
atrac9_config cfg = {0};
2018-03-31 20:10:57 +02:00
size_t total_size;
cfg.channels = vgmstream->channels;
cfg.config_data = read_32bitBE(header_offset + 0x08,streamHead);
2018-03-31 20:10:57 +02:00
/* 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 */
vgmstream->codec_data = init_atrac9(&cfg);
if (!vgmstream->codec_data) goto fail;
vgmstream->coding_type = coding_ATRAC9;
2018-03-31 20:10:57 +02:00
vgmstream->layout_type = layout_none;
/* EATrax is "buffered" ATRAC9, uses custom IO since it's kind of complex to add to the decoder */
2018-07-21 22:09:44 +02:00
temp_streamFile = setup_eaac_streamfile(streamData, version, codec, streamed, start_offset, total_size);
2018-03-31 20:10:57 +02:00
if (!temp_streamFile) goto fail;
start_offset = 0x00; /* must point to the custom streamfile's beginning */
break;
}
#endif
case 0x00: /* "NONE" (internal 'codec not set' flag) */
case 0x01: /* not used/reserved? /MP30/P6L0/P2B0/P2L0/P8S0/P8U0/PFN0? */
case 0x09: /* EASpeex (libspeex variant, base versions vary: 1.0.5, 1.2beta3) */
case 0x0b: /* ? */
case 0x0c: /* EAOpus (inside each SNS/SPS block is 16b frame size + standard? Opus packet) */
case 0x0d: /* ? */
case 0x0e: /* ? */
case 0x0f: /* ? */
default:
VGM_LOG("EA SNS/SPS: unknown codec 0x%02x\n", codec);
goto fail;
}
2018-03-31 20:10:57 +02:00
if (!vgmstream_open_stream(vgmstream,temp_streamFile ? temp_streamFile : streamData,start_offset))
goto fail;
if (vgmstream->layout_type == layout_blocked_ea_sns)
block_update_ea_sns(start_offset, vgmstream);
close_streamfile(temp_streamFile);
return vgmstream;
fail:
2018-03-31 20:10:57 +02:00
close_streamfile(temp_streamFile);
close_vgmstream(vgmstream);
return NULL;
}
2018-07-21 22:09:44 +02:00
static size_t get_snr_size(STREAMFILE *streamFile, off_t offset) {
switch (read_8bit(offset + 0x04, streamFile) >> 4 & 0x0F) { /* flags */
case 0x06: return 0x10;
case 0x02: return 0x0C;
default: return 0x08;
}
}