#include <math.h>
#include "meta.h"
#include "../layout/layout.h"
#include "../coding/coding.h"
#include "ea_eaac_streamfile.h"

/* EAAudioCore (aka SND10) formats, EA's current audio middleware */

#define EAAC_VERSION_V0                 0x00 /* SNR/SNS */
#define EAAC_VERSION_V1                 0x01 /* SPS */

#define EAAC_CODEC_NONE                 0x00
#define EAAC_CODEC_RESERVED             0x01 /* EALAYER3 V1a? MP30/P6L0/P2B0/P2L0/P8S0/P8U0/PFN0? */
#define EAAC_CODEC_PCM16BE              0x02
#define EAAC_CODEC_EAXMA                0x03
#define EAAC_CODEC_XAS1                 0x04
#define EAAC_CODEC_EALAYER3_V1          0x05
#define EAAC_CODEC_EALAYER3_V2_PCM      0x06
#define EAAC_CODEC_EALAYER3_V2_SPIKE    0x07
#define EAAC_CODEC_GCADPCM              0x08
#define EAAC_CODEC_EASPEEX              0x09
#define EAAC_CODEC_EATRAX               0x0a
#define EAAC_CODEC_EAMP3                0x0b
#define EAAC_CODEC_EAOPUS               0x0c
#define EAAC_CODEC_EAATRAC9             0x0d
#define EAAC_CODEC_EAOPUSM              0x0e
#define EAAC_CODEC_EAOPUSMU             0x0f

#define EAAC_TYPE_RAM                   0x00
#define EAAC_TYPE_STREAM                0x01
#define EAAC_TYPE_GIGASAMPLE            0x02

#define EAAC_BLOCKID0_DATA              0x00
#define EAAC_BLOCKID0_END               0x80 /* maybe meant to be a bitflag? */

#define EAAC_BLOCKID1_HEADER            0x48 /* 'H' */
#define EAAC_BLOCKID1_DATA              0x44 /* 'D' */
#define EAAC_BLOCKID1_END               0x45 /* 'E' */

static VGMSTREAM* init_vgmstream_eaaudiocore_header(STREAMFILE* sf_head, STREAMFILE* sf_data, off_t header_offset, off_t start_offset, meta_t meta_type, int standalone);
static VGMSTREAM* parse_s10a_header(STREAMFILE* sf, off_t offset, uint16_t target_index, off_t ast_offset);


/* .SNR+SNS - from EA latest games (~2005-2010), v0 header */
VGMSTREAM* init_vgmstream_ea_snr_sns(STREAMFILE* sf) {
    /* check extension, case insensitive */
    if (!check_extensions(sf,"snr"))
        goto fail;

    return init_vgmstream_eaaudiocore_header(sf, NULL, 0x00, 0x00, meta_EA_SNR_SNS, 1);

fail:
    return NULL;
}

/* .SPS - from EA latest games (~2010~present), v1 header */
VGMSTREAM* init_vgmstream_ea_sps(STREAMFILE* sf) {
    /* check extension, case insensitive */
    if (!check_extensions(sf,"sps"))
        goto fail;

    return init_vgmstream_eaaudiocore_header(sf, NULL, 0x00, 0x00, meta_EA_SPS, 1);

fail:
    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* sf) {
    VGMSTREAM* vgmstream = NULL;
    off_t start_offset, header_offset;
    int32_t (*read_32bit)(off_t,STREAMFILE*) = NULL;

    /* check extension, case insensitive */
    if (!check_extensions(sf,"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,sf)) {
        read_32bit = read_32bitBE;
    } else {
        read_32bit = read_32bitLE;
    }

    header_offset = 0x10; /* SNR header */
    start_offset = read_32bit(0x08,sf); /* SNS blocks */

    vgmstream = init_vgmstream_eaaudiocore_header(sf, sf, header_offset, start_offset, meta_EA_SNU, 0);
    if (!vgmstream) goto fail;

    return vgmstream;

fail:
    close_vgmstream(vgmstream);
    return NULL;
}

/* EA ABK - ABK header seems to be same as in the old games but the sound table is different and it contains SNR/SNS sounds instead */
VGMSTREAM* init_vgmstream_ea_abk_eaac(STREAMFILE* sf) {
    int is_dupe, total_sounds = 0, target_stream = sf->stream_index;
    off_t bnk_offset, modules_table, module_data, player_offset, samples_table, entry_offset, ast_offset;
    off_t cfg_num_players_off, cfg_module_data_off, cfg_module_entry_size, cfg_samples_table_off;
    uint32_t i, j, k, num_sounds, num_sample_tables;
    uint16_t num_modules, bnk_index, bnk_target_index;
    uint8_t num_players;
    off_t sample_tables[0x400];
    VGMSTREAM* vgmstream;
    int32_t(*read_32bit)(off_t, STREAMFILE*);
    int16_t(*read_16bit)(off_t, STREAMFILE*);

    /* check extension */
    if (!check_extensions(sf, "abk"))
        goto fail;

    if (read_32bitBE(0x00, sf) != 0x41424B43) /* "ABKC" */
        goto fail;

    /* use table offset to check endianness */
    if (guess_endianness32bit(0x1C, sf)) {
        read_32bit = read_32bitBE;
        read_16bit = read_16bitBE;
    } else {
        read_32bit = read_32bitLE;
        read_16bit = read_16bitLE;
    }

    if (target_stream == 0) target_stream = 1;
    if (target_stream < 0)
        goto fail;

    num_modules = read_16bit(0x0A, sf);
    modules_table = read_32bit(0x1C, sf);
    bnk_offset = read_32bit(0x20, sf);
    num_sample_tables = 0;
    bnk_target_index = 0xFFFF;
    ast_offset = 0;

    if (!bnk_offset || read_32bitBE(bnk_offset, sf) != 0x53313041) /* "S10A" */
        goto fail;

    /* set up some common values */
    if (modules_table == 0x5C) {
        /* the usual variant */
        cfg_num_players_off = 0x24;
        cfg_module_data_off = 0x2C;
        cfg_module_entry_size = 0x3C;
        cfg_samples_table_off = 0x04;
    } else if (modules_table == 0x78) {
        /* FIFA 08 has a bunch of extra zeroes all over the place, don't know what's up with that */
        cfg_num_players_off = 0x40;
        cfg_module_data_off = 0x54;
        cfg_module_entry_size = 0x68;
        cfg_samples_table_off = 0x0C;
    } else {
        goto fail;
    }

    for (i = 0; i < num_modules; i++) {
        num_players = read_8bit(modules_table + cfg_num_players_off, sf);
        module_data = read_32bit(modules_table + cfg_module_data_off, sf);
        if (num_players == 0xff) goto fail; /* EOF read */

        for (j = 0; j < num_players; j++) {
            player_offset = read_32bit(modules_table + cfg_module_entry_size + 0x04 * j, sf);
            samples_table = read_32bit(module_data + player_offset + cfg_samples_table_off, sf);

            /* multiple players may point at the same sound table */
            is_dupe = 0;
            for (k = 0; k < num_sample_tables; k++) {
                if (samples_table == sample_tables[k]) {
                    is_dupe = 1;
                    break;
                }
            }

            if (is_dupe)
                continue;

            sample_tables[num_sample_tables++] = samples_table;
            num_sounds = read_32bit(samples_table, sf);
            if (num_sounds == 0xffffffff) goto fail; /* EOF read */

            for (k = 0; k < num_sounds; k++) {
                /* 0x00: sound index */
                /* 0x02: priority */
                /* 0x03: azimuth */
                /* 0x08: streamed data offset */
                entry_offset = samples_table + 0x04 + 0x0C * k;
                bnk_index = read_16bit(entry_offset + 0x00, sf);

                /* some of these are dummies */
                if (bnk_index == 0xFFFF)
                    continue;

                total_sounds++;
                if (target_stream == total_sounds) {
                    bnk_target_index = bnk_index;
                    ast_offset = read_32bit(entry_offset + 0x08, sf);
                }
            }
        }

        /* skip class controllers */
        num_players += read_8bit(modules_table + cfg_num_players_off + 0x03, sf);
        modules_table += cfg_module_entry_size + num_players * 0x04;
    }

    if (bnk_target_index == 0xFFFF || ast_offset == 0)
        goto fail;

    vgmstream = parse_s10a_header(sf, bnk_offset, bnk_target_index, ast_offset);
    if (!vgmstream)
        goto fail;

    vgmstream->num_streams = total_sounds;
    return vgmstream;

fail:
    return NULL;
}

/* EA S10A header - seen inside new ABK files. Putting it here in case it's encountered stand-alone. */
static VGMSTREAM* parse_s10a_header(STREAMFILE* sf, off_t offset, uint16_t target_index, off_t sns_offset) {
    uint32_t num_sounds;
    off_t snr_offset;
    STREAMFILE *astFile = NULL;
    VGMSTREAM* vgmstream;

    /* header is always big endian */
    /* 0x00: header magic */
    /* 0x04: version */
    /* 0x05: padding */
    /* 0x06: serial number */
    /* 0x08: number of files */
    /* 0x0C: offsets table */
    if (read_32bitBE(offset + 0x00, sf) != 0x53313041) /* "S10A" */
        goto fail;

    num_sounds = read_32bitBE(offset + 0x08, sf);
    if (num_sounds == 0 || target_index >= num_sounds)
        goto fail;

    snr_offset = offset + read_32bitBE(offset + 0x0C + 0x04 * target_index, sf);

    if (sns_offset == 0xFFFFFFFF) {
        /* RAM asset */
        //;VGM_LOG("EA S10A: RAM at snr=%lx", snr_offset);
        vgmstream = init_vgmstream_eaaudiocore_header(sf, NULL, snr_offset, 0x00, meta_EA_SNR_SNS, 0);
        if (!vgmstream)
            goto fail;
    } else {
        /* streamed asset */
        astFile = open_streamfile_by_ext(sf, "ast");
        if (!astFile)
            goto fail;

        if (read_32bitBE(0x00, astFile) != 0x53313053) /* "S10S" */
            goto fail;

        //;VGM_LOG("EA S10A: stream at snr=%lx, sns=%lx\n", snr_offset, sns_offset);
        vgmstream = init_vgmstream_eaaudiocore_header(sf, astFile, snr_offset, sns_offset, meta_EA_SNR_SNS, 0);
        if (!vgmstream)
            goto fail;

        close_streamfile(astFile);
    }

    return vgmstream;

fail:
    close_streamfile(astFile);
    return NULL;
}

/* EA SBR/SBS - used in older 7th gen games for storing SFX */
VGMSTREAM* init_vgmstream_ea_sbr(STREAMFILE* sf) {
    uint32_t i, num_sounds, type_desc;
    uint16_t num_metas, meta_type;
    off_t table_offset, types_offset, entry_offset, metas_offset, data_offset, snr_offset, sns_offset;
    STREAMFILE *sbsFile = NULL;
    VGMSTREAM* vgmstream = NULL;
    int target_stream = sf->stream_index;

    if (!check_extensions(sf, "sbr"))
        goto fail;

    if (read_32bitBE(0x00, sf) != 0x53424B52) /* "SBKR" */
        goto fail;

    /* SBR files are always big endian */
    num_sounds = read_32bitBE(0x1c, sf);
    table_offset = read_32bitBE(0x24, sf);
    types_offset = read_32bitBE(0x28, sf);

    if (target_stream == 0) target_stream = 1;
    if (target_stream < 0 || num_sounds == 0 || target_stream > num_sounds)
        goto fail;

    entry_offset = table_offset + 0x0a * (target_stream - 1);
    num_metas = read_16bitBE(entry_offset + 0x04, sf);
    metas_offset = read_32bitBE(entry_offset + 0x06, sf);

    snr_offset = 0;
    sns_offset = 0;

    for (i = 0; i < num_metas; i++) {
        entry_offset = metas_offset + 0x06 * i;
        meta_type = read_16bitBE(entry_offset + 0x00, sf);
        data_offset = read_32bitBE(entry_offset + 0x02, sf);

        type_desc = read_32bitBE(types_offset + 0x06 * meta_type, sf);

        switch (type_desc) {
            case 0x534E5231: /* "SNR1" */
                snr_offset = data_offset;
                break;
            case 0x534E5331: /* "SNS1" */
                sns_offset = read_32bitBE(data_offset, sf);
                break;
            default:
                break;
        }
    }

    if (snr_offset == 0 && sns_offset == 0)
        goto fail;

    if (snr_offset == 0) {
        /* SPS file */
        sbsFile = open_streamfile_by_ext(sf, "sbs");
        if (!sbsFile)
            goto fail;

        if (read_32bitBE(0x00, sbsFile) != 0x53424B53) /* "SBKS" */
            goto fail;

        vgmstream = init_vgmstream_eaaudiocore_header(sbsFile, NULL, sns_offset, 0x00, meta_EA_SPS, 0);
        if (!vgmstream)
            goto fail;
    } else if (sns_offset == 0) {
        /* RAM asset */
        vgmstream = init_vgmstream_eaaudiocore_header(sf, NULL, snr_offset, 0x00, meta_EA_SNR_SNS, 0);
        if (!vgmstream)
            goto fail;
    } else {
        /* streamed asset */
        sbsFile = open_streamfile_by_ext(sf, "sbs");
        if (!sbsFile)
            goto fail;

        if (read_32bitBE(0x00, sbsFile) != 0x53424B53) /* "SBKS" */
            goto fail;

        vgmstream = init_vgmstream_eaaudiocore_header(sf, sbsFile, snr_offset, sns_offset, meta_EA_SNR_SNS, 0);
        if (!vgmstream)
            goto fail;
    }

    vgmstream->num_streams = num_sounds;
    close_streamfile(sbsFile);
    return vgmstream;

fail:
    close_streamfile(sbsFile);
    return NULL;
}

/* EA HDR/STH/DAT - seen in older 7th gen games, used for storing speech */
VGMSTREAM* init_vgmstream_ea_hdr_sth_dat(STREAMFILE* sf) {
    int target_stream = sf->stream_index;
    uint32_t snr_offset, sns_offset, block_size;
    uint16_t sth_offset, sth_offset2;
    uint8_t userdata_size, total_sounds, block_id;
    size_t dat_size;
    STREAMFILE *sf_dat = NULL, *sf_sth = NULL;
    VGMSTREAM* vgmstream;
    uint32_t(*read_u32)(off_t, STREAMFILE*);

    /* 0x00: ID */
    /* 0x02: parameters (userdata size, ...) */
    /* 0x03: number of files */
    /* 0x04: sub-ID (used for different police voices in NFS games) */
    /* 0x08: sample repeat (alt number of files?) */
    /* 0x09: block size (always zero?) */
    /* 0x0a: number of blocks (related to size?) */
    /* 0x0c: number of sub-banks (always zero?) */
    /* 0x0e: padding */
    /* 0x10: table start */

    if (!check_extensions(sf, "hdr"))
        goto fail;

    if (read_u8(0x09, sf) != 0)
        goto fail;

    if (read_u32be(0x0c, sf) != 0)
        goto fail;

    /* first offset is always zero */
    if (read_u16be(0x10, sf) != 0)
        goto fail;

    sf_sth = open_streamfile_by_ext(sf, "sth");
    if (!sf_sth)
        goto fail;

    sf_dat = open_streamfile_by_ext(sf, "dat");
    if (!sf_dat)
        goto fail;

    /* STH always starts with the first offset of zero */
    sns_offset = read_u32be(0x00, sf_sth);
    if (sns_offset != 0)
        goto fail;

    /* check if DAT starts with a correct SNS block */
    block_id = read_u8(0x00, sf_dat);
    if (block_id != EAAC_BLOCKID0_DATA && block_id != EAAC_BLOCKID0_END)
        goto fail;

    userdata_size = read_u8(0x02, sf) & 0x0F;
    total_sounds = read_u8(0x03, sf);

    if (read_u8(0x08, sf) > 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 */
    sth_offset = read_u16be(0x10 + (0x02 + userdata_size) * (target_stream - 1), sf);

#if 0
    snr_offset = sth_offset + 0x04;
    sns_offset = read_u32(sth_offset + 0x00, sf_sth);
#else
    /* overly intricate way to detect byte endianness because of the simplicity of HDR format */
    dat_size = get_streamfile_size(sf_dat);
    snr_offset = 0;
    sns_offset = 0;

    if (total_sounds == 1) {
        /* always 0 */
        snr_offset = sth_offset + 0x04;
        sns_offset = 0x00;
    } else {
        /* find the first sound size and match it up with the second sound offset to detect endianness */
        while (1) {
            if (sns_offset >= dat_size)
                goto fail;

            block_id = read_u8(sns_offset, sf_dat);
            block_size = read_u32be(sns_offset, sf_dat) & 0x00FFFFFF;
            if (block_size == 0)
                goto fail;

            if (block_id != EAAC_BLOCKID0_DATA && block_id != EAAC_BLOCKID0_END)
                goto fail;

            sns_offset += block_size;

            if (block_id == EAAC_BLOCKID0_END)
                break;
        }

        sns_offset = align_size_to_block(sns_offset, 0x40);
        sth_offset2 = read_u16be(0x10 + (0x02 + userdata_size) * 1, sf);
        if (sns_offset == read_u32be(sth_offset2, sf_sth)) {
            read_u32 = read_u32be;
        } else if (sns_offset == read_u32le(sth_offset2, sf_sth)) {
            read_u32 = read_u32le;
        } else {
            goto fail;
        }

        snr_offset = sth_offset + 0x04;
        sns_offset = read_u32(sth_offset + 0x00, sf_sth);
    }
#endif

    block_id = read_u8(sns_offset, sf_dat);
    if (block_id != EAAC_BLOCKID0_DATA && block_id != EAAC_BLOCKID0_END)
        goto fail;

    vgmstream = init_vgmstream_eaaudiocore_header(sf_sth, sf_dat, snr_offset, sns_offset, meta_EA_SNR_SNS, 0);
    if (!vgmstream)
        goto fail;

    vgmstream->num_streams = total_sounds;
    close_streamfile(sf_sth);
    close_streamfile(sf_dat);
    return vgmstream;

fail:
    close_streamfile(sf_sth);
    close_streamfile(sf_dat);
    return NULL;
}

/* open map/mpf+mus pairs that aren't exact pairs, since EA's games can load any combo */
static STREAMFILE *open_mapfile_pair(STREAMFILE* sf, int track /*, int num_tracks*/) {
    static const char *const mapfile_pairs[][2] = {
        /* standard cases, replace map part with mus part (from the end to preserve prefixes) */
        {"game.mpf",        "Game_Stream.mus"}, /* Skate 1/2/3 */
        {"ipod.mpf",        "Ipod_Stream.mus"},
        {"world.mpf",       "World_Stream.mus"},
        {"FreSkate.mpf",    "track.mus,ram.mus"}, /* Skate It */
        {"nsf_sing.mpf",    "track_main.mus"}, /* Need for Speed: Nitro */
        {"nsf_wii.mpf",     "Track.mus"},
        {"ssx_fe.mpf",      "stream_1.mus,stream_2.mus"}, /* SSX 2012 */
        {"ssxdd.mpf",       "main_trk.mus,"
                            "trick_alaska0.mus,"
                            "trick_rockies0.mus,"
                            "trick_pata0.mus,"
                            "trick_ant0.mus,"
                            "trick_killi0.mus,"
                            "trick_cyb0.mus,"
                            "trick_hima0.mus,"
                            "trick_nz0.mus,"
                            "trick_alps0.mus,"
                            "trick_lhotse0.mus"}
    };
    STREAMFILE *sf_mus = NULL;
    char file_name[PATH_LIMIT];
    int pair_count = (sizeof(mapfile_pairs) / sizeof(mapfile_pairs[0]));
    int i, j;
    size_t file_len, map_len;

    /* try parsing TXTM if present */
    sf_mus = read_filemap_file(sf, track);
    if (sf_mus) return sf_mus;

    /* if loading the first track, try opening MUS with the same name first (most common scenario) */
    if (track == 0) {
        sf_mus = open_streamfile_by_ext(sf, "mus");
        if (sf_mus) return sf_mus;
    }

    get_streamfile_filename(sf, file_name, PATH_LIMIT);
    file_len = strlen(file_name);

    for (i = 0; i < pair_count; i++) {
        const char *map_name = mapfile_pairs[i][0];
        const char *mus_name = mapfile_pairs[i][1];
        char buf[PATH_LIMIT] = { 0 };
        char *pch;
        int use_mask = 0;
        map_len = strlen(map_name);

        /* replace map_name with expected mus_name */
        if (file_len < map_len)
            continue;

        if (map_name[0] == '*') {
            use_mask = 1;
            map_name++;
            map_len--;

            if (strncmp(file_name + (file_len - map_len), map_name, map_len) != 0)
                continue;
        } else {
            if (strcmp(file_name, map_name) != 0)
                continue;
        }

        strncpy(buf, mus_name, PATH_LIMIT - 1);
        pch = strtok(buf, ","); //TODO: not thread safe in std C
        for (j = 0; j < track && pch; j++) {
            pch = strtok(NULL, ",");
        }
        if (!pch) continue; /* invalid track */

        if (use_mask) {
            file_name[file_len - map_len] = '\0';
            strncat(file_name, pch + 1, PATH_LIMIT - 1);
        } else {
            strncpy(file_name, pch, PATH_LIMIT - 1);
        }

        sf_mus = open_streamfile_by_filename(sf, file_name);
        if (sf_mus) return sf_mus;

        get_streamfile_filename(sf, file_name, PATH_LIMIT); /* reset for next loop */
    }

    /* hack when when multiple maps point to the same mus, uses name before "+"
     * ex. ZZZTR00A.TRJ+ZTR00PGR.MAP or ZZZTR00A.TRJ+ZTR00R0A.MAP both point to ZZZTR00A.TRJ */
    {
        char *mod_name = strchr(file_name, '+');
        if (mod_name) {
            mod_name[0] = '\0';
            sf_mus = open_streamfile_by_filename(sf, file_name);
            if (sf_mus) return sf_mus;
        }
    }

    VGM_LOG("No MPF/MUS pair specified for %s.\n", file_name);
    return NULL;
}

/* EA MPF/MUS combo - used in older 7th gen games for storing interactive music */
VGMSTREAM* init_vgmstream_ea_mpf_mus_eaac(STREAMFILE* sf) {
    uint32_t num_tracks, track_start, track_checksum = 0, mus_sounds, mus_stream = 0;
    uint32_t tracks_table, samples_table, eof_offset, table_offset, entry_offset, snr_offset, sns_offset;
    uint16_t num_subbanks;
    uint8_t version, sub_version;
    STREAMFILE *musFile = NULL;
    VGMSTREAM* vgmstream = NULL;
    int i;
    int target_stream = sf->stream_index, total_streams, is_ram = 0;
    uint32_t(*read_u32)(off_t, STREAMFILE *);
    uint16_t(*read_u16)(off_t, STREAMFILE *);

    /* check extension */
    if (!check_extensions(sf, "mpf"))
        goto fail;

    /* detect endianness */
    if (read_u32be(0x00, sf) == 0x50464478) { /* "PFDx" */
        read_u32 = read_u32be;
        read_u16 = read_u16be;
    } else if (read_u32le(0x00, sf) == 0x50464478) { /* "xDFP" */
        read_u32 = read_u32le;
        read_u16 = read_u16le;
    } else {
        goto fail;
    }

    version = read_u8(0x04, sf);
    sub_version = read_u8(0x05, sf);
    if (version != 5 || sub_version < 2 || sub_version > 3) goto fail;

    num_tracks = read_u8(0x0d, sf);

    tracks_table = read_u32(0x2c, sf);
    samples_table = read_u32(0x34, sf);
    eof_offset = read_u32(0x38, sf);
    total_streams = (eof_offset - samples_table) / 0x08;

    if (target_stream == 0) target_stream = 1;
    if (target_stream < 0 || total_streams == 0 || target_stream > total_streams)
        goto fail;

    for (i = num_tracks - 1; i >= 0; i--) {
        entry_offset = read_u32(tracks_table + i * 0x04, sf) * 0x04;
        track_start = read_u32(entry_offset + 0x00, sf);

        if (track_start == 0 && i != 0)
            continue; /* empty track */

        if (track_start <= target_stream - 1) {
            num_subbanks = read_u16(entry_offset + 0x04, sf);
            track_checksum = read_u32be(entry_offset + 0x08, sf);
            is_ram = (num_subbanks != 0);

            if (num_subbanks > 1) {
                VGM_LOG("EA MPF: Found EAAC MPF with more than 1 RAM sub-bank.\n");
                goto fail;
            }

            /* checks to distinguish it from older versions */
            if (is_ram) {
                if (read_u32(entry_offset + 0x0c, sf) != 0x00)
                    goto fail;

                track_checksum = read_u32be(entry_offset + 0x14, sf);
            } else {
                if (read_u32(entry_offset + 0x0c, sf) == 0x00)
                    goto fail;
            }

            mus_stream = target_stream - 1 - track_start;
            break;
        }
    }

    /* open MUS file that matches this track */
    musFile = open_mapfile_pair(sf, i);//, num_tracks
    if (!musFile)
        goto fail;

    if (read_u32be(0x00, musFile) != track_checksum)
        goto fail;

    /* sample offsets table is still there but it just holds SNS offsets, it's of little use to us */
    /* MUS file has a header, however */
    if (sub_version == 2) {
        if (read_u32(0x04, musFile) != 0x00)
            goto fail;

        /*
         * 0x00: flags? index?
         * 0x04: SNR offset
         * 0x08: SNS offset (contains garbage for RAM sounds)
         */
        table_offset = 0x08;
        entry_offset = table_offset + mus_stream * 0x0c;
        snr_offset = read_u32(entry_offset + 0x04, musFile);
        sns_offset = read_u32(entry_offset + 0x08, musFile);
    } else if (sub_version == 3) {
        /* number of samples is always little endian */
        mus_sounds = read_u32le(0x04, musFile);
        if (mus_stream >= mus_sounds)
            goto fail;

        if (is_ram) {
            /* not seen so far */
            VGM_LOG("Found RAM SNR in MPF v5.3.\n");
            goto fail;
        }

        /*
         * 0x00: checksum
         * 0x04: index
         * 0x06: sub-index
         * 0x08: SNR offset
         * 0x0c: SNS offset
         * 0x10: SNR size
         * 0x14: SNS size
         * 0x18: zero
         */
        table_offset = 0x28;
        entry_offset = table_offset + mus_stream * 0x1c;
        snr_offset = read_u32(entry_offset + 0x08, musFile) * 0x10;
        sns_offset = read_u32(entry_offset + 0x0c, musFile) * 0x80;
    } else {
        goto fail;
    }

    vgmstream = init_vgmstream_eaaudiocore_header(musFile, musFile, snr_offset, sns_offset, meta_EA_SNR_SNS, 0);
    if (!vgmstream)
        goto fail;

    vgmstream->num_streams = total_streams;
    get_streamfile_filename(musFile, vgmstream->stream_name, STREAM_NAME_SIZE);
    close_streamfile(musFile);
    return vgmstream;

fail:
    close_streamfile(musFile);
    return NULL;
}

/* EA TMX - used for engine sounds in NFS games (2007-2011) */
VGMSTREAM* init_vgmstream_ea_tmx(STREAMFILE* sf) {
    uint32_t num_sounds, sound_type, table_offset, data_offset, entry_offset, sound_offset;
    VGMSTREAM* vgmstream = NULL;
    STREAMFILE* temp_sf = NULL;
    int target_stream = sf->stream_index;
    uint32_t(*read_u32)(off_t, STREAMFILE *);

    if (!check_extensions(sf, "tmx"))
        goto fail;

    if (read_u32be(0x0c, sf) == 0x30303031) { /* "0001" */
        read_u32 = read_u32be;
    } else if (read_u32le(0x0c, sf) == 0x30303031) { /* "1000" */
        read_u32 = read_u32le;
    } else {
        goto fail;
    }

    num_sounds = read_u32(0x20, sf);
    table_offset = read_u32(0x58, sf);
    data_offset = read_u32(0x5c, sf);

    if (target_stream == 0) target_stream = 1;
    if (target_stream < 0 || num_sounds == 0 || target_stream > num_sounds)
        goto fail;

    entry_offset = table_offset + (target_stream - 1) * 0x24;
    sound_type = read_u32(entry_offset + 0x00, sf);
    sound_offset = read_u32(entry_offset + 0x08, sf) + data_offset;

    switch (sound_type) {
        case 0x47494E20: /* "GIN " */
            temp_sf = setup_subfile_streamfile(sf, sound_offset, get_streamfile_size(sf) - sound_offset, "gin");
            if (!temp_sf) goto fail;

            vgmstream = init_vgmstream_gin(temp_sf);
            if (!vgmstream) goto fail;
            close_streamfile(temp_sf);
            break;
        case 0x534E5220: /* "SNR " */
            vgmstream = init_vgmstream_eaaudiocore_header(sf, NULL, sound_offset, 0x00, meta_EA_SNR_SNS, 0);
            if (!vgmstream) goto fail;
            break;
        default:
            goto fail;
    }

    vgmstream->num_streams = num_sounds;
    return vgmstream;

fail:
    close_streamfile(temp_sf);
    return NULL;
}

/* EA Harmony Sample Bank - used in 8th gen EA Sports games */
VGMSTREAM* init_vgmstream_ea_sbr_harmony(STREAMFILE* sf) {
    uint64_t base_offset, sound_offset, offset, prev_offset;
    uint32_t chunk_id, data_offset, table_offset, dset_offset, set_values, set_sounds, sound_table_offset;
    int32_t flag;
    uint16_t num_dsets;
    uint8_t set_type, offset_size;
    uint32_t i, j;
    char sound_name[STREAM_NAME_SIZE];
    STREAMFILE *sf_sbs = NULL, *sf_data = NULL;
    VGMSTREAM* vgmstream = NULL;
    int target_stream = sf->stream_index, total_sounds, local_target, is_streamed = 0;
    uint64_t(*read_u64)(off_t, STREAMFILE *);
    uint32_t(*read_u32)(off_t, STREAMFILE*);
    uint16_t(*read_u16)(off_t, STREAMFILE*);

    if (!check_extensions(sf, "sbr"))
        goto fail;

    /* check header */
    if (is_id32be(0x00, sf, "SBle")) {
        read_u64 = read_u64le;
        read_u32 = read_u32le;
        read_u16 = read_u16le;
    } else if (is_id32be(0x00, sf, "SBbe")) {
        read_u64 = read_u64be;
        read_u32 = read_u32be;
        read_u16 = read_u16be;
    } else {
        goto fail;
    }

    num_dsets = read_u16(0x0a, sf);
    table_offset = read_u32(0x18, sf);
    data_offset = read_u32(0x20, sf);

    if (target_stream == 0) target_stream = 1;
    if (target_stream < 0)
        goto fail;

    total_sounds = 0;
    sound_offset = 0;

    /* the bank is split into DSET sections each of which references one or multiple sounds */
    /* each set can contain RAM sounds (stored in SBR in data section) or streamed sounds (stored separately in SBS file) */
    for (i = 0; i < num_dsets; i++) {
        dset_offset = read_u32(table_offset + 0x08 * i, sf);
        if (read_u32(dset_offset, sf) != 0x44534554) /* "DSET" */
            goto fail;

        set_values = read_u32(dset_offset + 0x38, sf);
        local_target = target_stream - total_sounds - 1;
        dset_offset += 0x48;

        /* Find RAM or OFF chunk */
        while(1) {
            chunk_id = read_u32(dset_offset, sf);
            if (chunk_id == 0x2E52414D) { /* ".RAM" */
                break;
            } else if (chunk_id == 0x2E4F4646) { /* ".OFF" */
                break;
            } else if (chunk_id == 0x2E4C4452 || /* ".LDR" */
                chunk_id == 0x2E4F424A || /* ".OBJ" */
                chunk_id == 0x2E445552 || /* ".DUR" */
                (chunk_id & 0xFF00FFFF) == 0x2E00534C) { /* ".?SL */
                dset_offset += 0x18;
            } else {
                goto fail;
            }
        }

        /* different set types store offsets differently */
        set_type = read_u8(dset_offset + 0x05, sf);

        /* data sets often contain duplicate offets, need to filter them out however we can */
        /* offsets are stored in ascending order which makes things easier */
        if (set_type == 0x00) {
            set_sounds = 1;
            total_sounds += set_sounds;
            if (local_target < 0 || local_target > 0)
                continue;

            sound_offset = read_u64(dset_offset + 0x08, sf);
        } else if (set_type == 0x01) {
            flag = (int16_t)read_u16(dset_offset + 0x06, sf);
            base_offset = read_u64(dset_offset + 0x08, sf);

            set_sounds = set_values;
            total_sounds += set_sounds;
            if (local_target < 0 || local_target >= set_sounds)
                continue;

            sound_offset = base_offset + flag * local_target;
        } else if (set_type == 0x02) {
            flag = (read_u16(dset_offset + 0x06, sf) >> 0) & 0xFF;
            offset_size = (read_u16(dset_offset + 0x06, sf) >> 8) & 0xFF;
            base_offset = read_u64(dset_offset + 0x08, sf);
            sound_table_offset = read_u32(dset_offset + 0x10, sf);

            set_sounds = 0;
            prev_offset = UINT64_MAX;
            for (j = 0; j < set_values; j++) {
                if (offset_size == 0x01) {
                    offset = read_u8(sound_table_offset + 0x01 * j, sf);
                } else if (offset_size == 0x02) {
                    offset = read_u16(sound_table_offset + 0x02 * j, sf);
                } else if (offset_size == 0x04) {
                    offset = read_u32(sound_table_offset + 0x04 * j, sf);
                } else {
                    goto fail;
                }
                offset <<= flag;
                offset += base_offset;

                if (offset != prev_offset) {
                    if (set_sounds == local_target)
                        sound_offset = offset;
                    set_sounds++;
                }
                prev_offset = offset;
            }

            total_sounds += set_sounds;
            if (local_target < 0 || local_target >= set_sounds)
                continue;
        } else if (set_type == 0x03) {
            offset_size = (read_u16(dset_offset + 0x06, sf) >> 8) & 0xFF;
            set_sounds = read_u64(dset_offset + 0x08, sf);
            sound_table_offset = read_u32(dset_offset + 0x10, sf);

            total_sounds += set_sounds;
            if (local_target < 0 || local_target >= set_sounds)
                continue;

            if (offset_size == 0x01) {
                sound_offset = read_u8(sound_table_offset + 0x01 * local_target, sf);
            } else if (offset_size == 0x02) {
                sound_offset = read_u16(sound_table_offset + 0x02 * local_target, sf);
            } else if (offset_size == 0x04) {
                sound_offset = read_u32(sound_table_offset + 0x04 * local_target, sf);
            } else {
                goto fail;
            }
        } else if (set_type == 0x04) {
            sound_table_offset = read_u32(dset_offset + 0x10, sf);

            set_sounds = 0;
            prev_offset = UINT64_MAX;
            for (j = 0; j < set_values; j++) {
                offset = read_u64(sound_table_offset + 0x08 * j, sf);

                if (sound_offset != prev_offset) {
                    if (set_sounds == local_target)
                        sound_offset = offset;
                    set_sounds++;
                }
                prev_offset = offset;
            }

            total_sounds += set_sounds;
            if (local_target < 0 || local_target >= set_sounds)
                continue;
        } else {
            goto fail;
        }

        snprintf(sound_name, STREAM_NAME_SIZE, "DSET %02d/%04d", i, local_target);

        if (chunk_id == 0x2E52414D) { /* ".RAM" */
            is_streamed = 0;
        } else if (chunk_id == 0x2E4F4646) { /* ".OFF" */
            is_streamed = 1;
        }
    }

    if (sound_offset == 0)
        goto fail;

    if (!is_streamed) {
        /* RAM asset */
        if (!is_id32be(data_offset, sf, "data") &&
            !is_id32be(data_offset, sf, "DATA"))
            goto fail;

        sf_data = sf;
        sound_offset += data_offset;
    } else {
        /* streamed asset */
        sf_sbs = open_streamfile_by_ext(sf, "sbs");
        if (!sf_sbs)
            goto fail;

        if (!is_id32be(0x00, sf_sbs, "data") &&
            !is_id32be(0x00, sf_sbs, "DATA"))
            goto fail;

        sf_data = sf_sbs;

        if (is_id32be(sound_offset, sf_data, "slot")) {
            /* skip "slot" section */
            sound_offset += 0x30;
        }
    }

    vgmstream = init_vgmstream_eaaudiocore_header(sf_data, NULL, sound_offset, 0x00, meta_EA_SPS, 0);
    if (!vgmstream)
        goto fail;

    vgmstream->num_streams = total_sounds;
    strncpy(vgmstream->stream_name, sound_name, STREAM_NAME_SIZE);
    close_streamfile(sf_sbs);
    return vgmstream;

fail:
    close_streamfile(sf_sbs);
    return NULL;
}

/* ************************************************************************* */

typedef struct {
    int version;
    int codec;
    int channel_config;
    int sample_rate;
    int type;

    int streamed;
    int channels;

    int num_samples;
    int loop_start;
    int loop_end;
    int loop_flag;
    int prefetch_samples;

    off_t stream_offset;
    off_t loop_offset;
    off_t prefetch_offset;
} eaac_header;

static segmented_layout_data* build_segmented_eaaudiocore_looping(STREAMFILE *sf_data, eaac_header *eaac);
static layered_layout_data* build_layered_eaaudiocore(STREAMFILE* sf, eaac_header *eaac, off_t start_offset);
static STREAMFILE *setup_eaac_streamfile(eaac_header *ea, STREAMFILE* sf_head, STREAMFILE* sf_data);
static size_t calculate_eaac_size(STREAMFILE* sf, eaac_header *ea, uint32_t num_samples, off_t start_offset, int is_ram);

/* 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* sf_head, STREAMFILE* sf_data, off_t header_offset, off_t start_offset, meta_t meta_type, int standalone) {
    VGMSTREAM* vgmstream = NULL;
    STREAMFILE *temp_sf = NULL, *sf = NULL, *snsFile = NULL;
    uint32_t header1, header2, header_block_size = 0, header_size;
    uint8_t header_block_id;
    eaac_header eaac = {0};

    if (meta_type == meta_EA_SPS) {
        header_block_id = read_8bit(header_offset, sf_head);
        header_block_size = read_32bitBE(header_offset, sf_head) & 0x00FFFFFF;
        if (header_block_id != EAAC_BLOCKID1_HEADER)
            goto fail;

        header_offset += 0x04;
    }

    /* EA SNR/SPH header */
    header1 = (uint32_t)read_32bitBE(header_offset + 0x00, sf_head);
    header2 = (uint32_t)read_32bitBE(header_offset + 0x04, sf_head);
    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 >>  0) & 0x03FFFF; /* 18 bits */
    eaac.type           = (header2 >> 30) & 0x03; /* 2 bits */
    eaac.loop_flag      = (header2 >> 29) & 0x01; /* 1 bit */
    eaac.num_samples    = (header2 >>  0) & 0x1FFFFFFF; /* 29 bits */
    /* rest is optional, depends on used flags and codec (handled below) */

    /* common channel configs are mono/stereo/quad/5.1/7.1 (from debug strings), while others are quite rare
     * [Battlefield 4 (X360)-EAXMA: 3/5/7ch, Army of Two: The Devil's Cartel (PS3)-EALayer3v2P: 11ch] */
    eaac.channels = eaac.channel_config + 1;
    /* EA 6ch channel mapping is L C R BL BR LFE, but may use stereo layers for dynamic music
     * instead, so we can't re-map automatically (use TXTP) */

    /* V0: SNR+SNS, V1: SPH+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;
    }

    /* accepted max (some Dead Space 2 (PC) do use 96000) */
    if (eaac.sample_rate > 200000) {
        VGM_LOG("EA EAAC: unknown sample rate\n");
        goto fail;
    }

    /* catch unknown values */
    if (eaac.type != EAAC_TYPE_RAM && eaac.type != EAAC_TYPE_STREAM && eaac.type != EAAC_TYPE_GIGASAMPLE) {
        VGM_LOG("EA EAAC: unknown type 0x%02x\n", eaac.type);
        goto fail;
    }

    if (eaac.version == EAAC_VERSION_V1 && eaac.type != EAAC_TYPE_STREAM) {
        /* should never happen */
        VGM_LOG("EA EAAC: bad stream type for version %x\n", eaac.version);
        goto fail;
    }

    /* Non-streamed sounds are stored as a single block (may not set block end flags) */
    eaac.streamed = (eaac.type != EAAC_TYPE_RAM);

    /* get loops (fairly involved due to the multiple layouts and mutant streamfiles)
     * full loops aren't too uncommon [Dead Space (PC) stream sfx/ambiance, FIFA 08 (PS3) RAM sfx],
     * while actual looping is very rare [Need for Speed: World (PC)-EAL3, The Simpsons Game (X360)-EAXMA] */

    /* get optional header values */
    header_size = 0x08;
    if (eaac.loop_flag) {
        header_size += 0x04;
        eaac.loop_start = read_32bitBE(header_offset + 0x08, sf_head);
        eaac.loop_end = eaac.num_samples;

        /* 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 ||
            eaac.codec == EAAC_CODEC_EAXMA ||
            eaac.codec == EAAC_CODEC_XAS1)) {
            VGM_LOG("EA EAAC: unknown actual looping for codec %x\n", eaac.codec);
            goto fail;
        }
    }

    switch (eaac.type) {
        case EAAC_TYPE_RAM:
            break;
        case EAAC_TYPE_STREAM:
            if (eaac.loop_flag) {
                header_size += 0x04;
                eaac.loop_offset = read_32bitBE(header_offset + 0x0c, sf_head);
            }
            break;
        case EAAC_TYPE_GIGASAMPLE: /* rarely seen [Def Jam Icon (X360)] */
            header_size += 0x04;
            eaac.prefetch_samples = read_32bitBE(header_offset + eaac.loop_flag ? 0x0c : 0x08, sf_head);

            if (eaac.loop_flag && eaac.loop_start >= eaac.prefetch_samples) {
                header_size += 0x04;
                eaac.loop_offset = read_32bitBE(header_offset + 0x10, sf_head);
            }
            break;
        default:
            goto fail;
    }

    /* get data offsets */
    if (eaac.version == EAAC_VERSION_V0) {
        switch (eaac.type) {
            case EAAC_TYPE_RAM:
                eaac.stream_offset = header_offset + header_size;
                break;
            case EAAC_TYPE_STREAM:
                eaac.stream_offset = start_offset;
                break;
            case EAAC_TYPE_GIGASAMPLE:
                eaac.prefetch_offset = header_offset + header_size;
                eaac.stream_offset = start_offset;
                break;
            default:
                goto fail;
        }
    } else {
        eaac.stream_offset = header_offset - 0x04 + header_block_size;
    }

    /* correct loop offsets */
    if (eaac.loop_flag) {
        if (eaac.streamed) {
            /* SNR+SNS are separate so offsets are relative to the data start
             * (first .SNS block, or extra data before the .SNS block in case of .SNU)
             * SPS have headers+data together so offsets are relative to the file start [ex. FIFA 18 (PC)] */
            if (eaac.version == EAAC_VERSION_V0) {
                if (eaac.prefetch_samples != 0) {
                    if (eaac.loop_start == 0) {
                        /* loop from the beginning */
                        eaac.loop_offset = 0x00;
                    } else if (eaac.loop_start < eaac.prefetch_samples) {
                        /* loop from the second RAM block */
                        eaac.loop_offset = read_32bitBE(eaac.prefetch_offset, sf_head) & 0x00FFFFFF;
                    } else {
                        /* loop from offset within SNS */
                        eaac.loop_offset += read_32bitBE(eaac.prefetch_offset, sf_head) & 0x00FFFFFF;
                    }
                }
            } else {
                eaac.loop_offset -= header_block_size;
            }
        } else if (eaac.loop_start > 0) {
            /* RAM assets have two blocks in case of actual loops */
            /* find the second block by getting the first block size */
            eaac.loop_offset = read_32bitBE(eaac.stream_offset, sf_head) & 0x00FFFFFF;
        } else {
            /* RAM assets have only one block in case of full loops */
            eaac.loop_offset = 0x00; /* implicit */
        }
    }

    if (eaac.version == EAAC_VERSION_V0 && eaac.streamed) {
        /* open SNS file if needed */
        if (standalone) {
            snsFile = open_streamfile_by_ext(sf_head, "sns");
            sf_data = snsFile;
        }
        if (!sf_data) goto fail;
    }

    /* build streamfile with audio data */
    sf = setup_eaac_streamfile(&eaac, sf_head, sf_data);
    if (!sf) goto fail;

    /* build the VGMSTREAM */
    vgmstream = allocate_vgmstream(eaac.channels,eaac.loop_flag);
    if (!vgmstream) goto fail;

    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;
    vgmstream->stream_size = get_streamfile_size(sf);

    /* EA decoder list and known internal FourCCs */
    switch(eaac.codec) {

        case EAAC_CODEC_PCM16BE: /* "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)] */

            /* special (if hacky) loop handling, see comments */
            if (eaac.loop_start > 0) {
                segmented_layout_data *data = build_segmented_eaaudiocore_looping(sf, &eaac);
                if (!data) goto fail;
                vgmstream->layout_data = data;
                vgmstream->coding_type = data->segments[0]->coding_type;
                vgmstream->layout_type = layout_segmented;
            }
            else {
                vgmstream->layout_data = build_layered_eaaudiocore(sf, &eaac, 0x00);
                if (!vgmstream->layout_data) goto fail;
                vgmstream->coding_type = coding_FFmpeg;
                vgmstream->layout_type = layout_layered;
            }

            break;
        }
#endif

        case EAAC_CODEC_XAS1: /* "Xas1": EA-XAS v1 [Dead Space (PC/PS3)] */

            /* special (if hacky) loop handling, see comments */
            if (eaac.loop_start > 0) {
                segmented_layout_data *data = build_segmented_eaaudiocore_looping(sf, &eaac);
                if (!data) goto fail;
                vgmstream->layout_data = data;
                vgmstream->coding_type = data->segments[0]->coding_type;
                vgmstream->layout_type = layout_segmented;
            } else {
                vgmstream->coding_type = coding_EA_XAS_V1;
                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)] */
            mpeg_custom_config cfg = {0};
            mpeg_custom_t type = (eaac.codec == 0x05 ? MPEG_EAL31b : (eaac.codec == 0x06) ? MPEG_EAL32P : MPEG_EAL32S);

            /* 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) */

            /* special (if hacky) loop handling, see comments */
            if (eaac.loop_start > 0) {
                segmented_layout_data *data = build_segmented_eaaudiocore_looping(sf, &eaac);
                if (!data) goto fail;
                vgmstream->layout_data = data;
                vgmstream->coding_type = data->segments[0]->coding_type;
                vgmstream->layout_type = layout_segmented;
            }
            else {
                temp_sf = setup_eaac_audio_streamfile(sf, eaac.version, eaac.codec, eaac.streamed,0,0, 0x00);
                if (!temp_sf) goto fail;

                vgmstream->codec_data = init_mpeg_custom(temp_sf, 0x00, &vgmstream->coding_type, vgmstream->channels, type, &cfg);
                if (!vgmstream->codec_data) goto fail;
                vgmstream->layout_type = layout_none;
            }

            break;
        }
#endif

        case EAAC_CODEC_GCADPCM: /* "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_SPEEX
        case EAAC_CODEC_EASPEEX: { /* "Esp0": EASpeex (libspeex variant, base versions vary: 1.0.5, 1.2beta3) [FIFA 14 (PS4), FIFA 2020 (Switch)] */
            /* EASpeex looks normal but simplify with custom IO to avoid worrying about blocks.
             * First block samples count frames' samples subtracting encoder delay. */

            vgmstream->codec_data = init_speex_ea(eaac.channels);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_SPEEX;
            vgmstream->layout_type = layout_none;

            temp_sf = setup_eaac_audio_streamfile(sf, eaac.version, eaac.codec, eaac.streamed,0,0, 0x00);
            if (!temp_sf) goto fail;

            break;
        }
#endif

#ifdef VGM_USE_ATRAC9
        case EAAC_CODEC_EATRAX: { /* EATrax (unknown FourCC) [Need for Speed: Most Wanted (Vita)] */
            atrac9_config cfg = {0};

            /* EATrax is "buffered" ATRAC9, uses custom IO since it's kind of complex to add to the decoder */

            cfg.channels = eaac.channels;
            /* sub-header after normal header */
            cfg.config_data = read_32bitBE(header_offset + header_size + 0x00,sf_head);
            /* 0x04: data size without blocks, LE b/c why make sense (but don't use it in case of truncated files) */
            /* 0x08: 16b frame size (same as config data) */

            vgmstream->codec_data = init_atrac9(&cfg);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_ATRAC9;
            vgmstream->layout_type = layout_none;

            temp_sf = setup_eaac_audio_streamfile(sf, eaac.version, eaac.codec, eaac.streamed,0,0, 0x00);
            if (!temp_sf) goto fail;

            break;
        }
#endif


#ifdef VGM_USE_MPEG
        case EAAC_CODEC_EAMP3: { /* "EM30": EA-MP3 [Need for Speed 2015 (PS4), FIFA 2021 (PC)] */
            mpeg_custom_config cfg = {0};

            temp_sf = setup_eaac_audio_streamfile(sf, eaac.version, eaac.codec, eaac.streamed,0,0, 0x00);
            if (!temp_sf) goto fail;

            vgmstream->codec_data = init_mpeg_custom(temp_sf, 0x00, &vgmstream->coding_type, vgmstream->channels, MPEG_EAMP3, &cfg);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->layout_type = layout_none;

            break;
        }
#endif

#ifdef VGM_USE_FFMPEG
        case EAAC_CODEC_EAOPUS: { /* "Eop0": EAOpus [FIFA 17 (PC), FIFA 19 (Switch)]*/
            vgmstream->layout_data = build_layered_eaaudiocore(sf, &eaac, 0x00);
            if (!vgmstream->layout_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_layered;
            break;
        }
#endif
#ifdef VGM_USE_FFMPEG
      //case EAAC_CODEC_EAOPUSMU:  /* "MSU0": Multi-Stream Opus Uncoupled (not seen) */
        case EAAC_CODEC_EAOPUSM: { /* "MSO0": Multi-Stream Opus */
            off_t offset = 0x00; // eaac.stream_offset;
            off_t data_size = get_streamfile_size(sf);
            opus_config cfg = {0};

            cfg.channels = eaac.channels;
            {
                uint32_t block_size = read_u32be(offset + 0x00, sf) & 0x00FFFFFF;
                uint32_t curr_samples = read_u32be(offset + 0x04, sf);
                uint32_t next_samples = read_u32be(offset + block_size + 0x04, sf);

                cfg.skip = next_samples - curr_samples;
                /* maybe should check if next block exists, but files of single packet? */
            }

            /* find coupled OPUS streams (internal streams using 2ch) */
            if (eaac.codec == EAAC_CODEC_EAOPUSMU) {
                cfg.coupled_count = 0;
            }
            else {
                switch(eaac.channels) {
                  //case 8:  cfg.coupled_count = 3; break;   /* 2ch+2ch+2ch+1ch+1ch, 5 streams */
                  //case 6:                                  /* 2ch+2ch+1ch+1ch, 4 streams */
                    case 4:  cfg.coupled_count = 2; break;   /* 2ch+2ch, 2 streams */
                  //case 3:                                  /* 2ch+1ch, 2 streams */
                    case 2:  cfg.coupled_count = 1; break;   /* 2ch, 1 stream */
                  //case 1:  cfg.coupled_count = 0; break;   /* 1ch, 1 stream */
                    default: goto fail;
                }
            }

            /* total number internal OPUS streams (should be >0) */
            cfg.stream_count = cfg.channels - cfg.coupled_count;

            /* We *don't* remove EA blocks b/c in Multi Opus 1 block = 1 Opus packet
             * Regular EAOPUS uses layers to fake multichannel, this is normal multichannel Opus.
             * This can be used for stereo too, so probably replaces EAOPUS. */
            //temp_sf = setup_eaac_audio_streamfile(sf_data, eaac->version, eaac->codec, eaac->streamed,0,0, 0x00);
            //if (!temp_sf) goto fail;

            vgmstream->codec_data = init_ffmpeg_ea_opusm(sf, offset, data_size, &cfg);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;
            break;
        }
#endif

        case EAAC_CODEC_EAATRAC9: /* "AT90" (possibly ATRAC9 with a saner layout than EATRAX) */
        default:
            VGM_LOG("EA EAAC: unknown codec 0x%02x\n", eaac.codec);
            goto fail;
    }

    if (!vgmstream_open_stream(vgmstream, temp_sf ? temp_sf : sf, 0x00))
        goto fail;

    close_streamfile(sf);
    close_streamfile(snsFile);
    close_streamfile(temp_sf);
    return vgmstream;

fail:
    close_streamfile(sf);
    close_streamfile(snsFile);
    close_streamfile(temp_sf);
    close_vgmstream(vgmstream);
    return NULL;
}

static size_t calculate_eaac_size(STREAMFILE *sf, eaac_header *ea, uint32_t num_samples, off_t start_offset, int is_ram) {
    uint32_t block_size;
    uint8_t block_id;
    size_t stream_size, file_size;
    off_t block_offset;
    int looped;

    file_size = get_streamfile_size(sf);
    block_offset = start_offset;
    stream_size = 0;
    looped = 0;

    while (block_offset < file_size) {
        block_id = read_8bit(block_offset, sf);
        block_size = read_32bitBE(block_offset, sf) & 0x00FFFFFF;

        /* stop when we reach the end marker */
        if (ea->version == EAAC_VERSION_V0) {
            if (block_id != EAAC_BLOCKID0_DATA && block_id != EAAC_BLOCKID0_END)
                goto fail;
        } else {
            if (block_id == EAAC_BLOCKID1_END)
                break;

            if (block_id != EAAC_BLOCKID1_DATA)
                goto fail;
        }

        stream_size += block_size;
        block_offset += block_size;

        if (ea->version == EAAC_VERSION_V0) {
            if (is_ram) {
                /* RAM data only consists of one block (two for looped sounds) */
                if (ea->loop_start > 0 && ea->loop_start < num_samples && !looped) looped = 1;
                else break;
            } else if (block_id == EAAC_BLOCKID0_END) {
                if (ea->loop_offset > 0 && ea->loop_start >= ea->prefetch_samples && !looped) looped = 1;
                else break;
            }
        }
    }

    return stream_size;

fail:
    return 0;
}


static STREAMFILE *setup_eaac_streamfile(eaac_header *ea, STREAMFILE* sf_head, STREAMFILE* sf_data) {
    size_t data_size;
    STREAMFILE *new_sf = NULL;
    STREAMFILE *temp_sf = NULL;
    STREAMFILE *sf_segments[2] = { 0 };

    if (ea->version == EAAC_VERSION_V0) {
        switch (ea->type) {
            case EAAC_TYPE_RAM:
                /* both header and data in SNR */
                data_size = calculate_eaac_size(sf_head, ea, ea->num_samples, ea->stream_offset, 1);
                if (data_size == 0) goto fail;

                new_sf = open_wrap_streamfile(sf_head);
                if (!new_sf) goto fail;
                temp_sf = new_sf;

                new_sf = open_clamp_streamfile(temp_sf, ea->stream_offset, data_size);
                if (!new_sf) goto fail;
                temp_sf = new_sf;
                break;
            case EAAC_TYPE_STREAM:
                /* header in SNR, data in SNS */
                data_size = calculate_eaac_size(sf_data, ea, ea->num_samples, ea->stream_offset, 0);
                if (data_size == 0) goto fail;

                new_sf = open_wrap_streamfile(sf_data);
                if (!new_sf) goto fail;
                temp_sf = new_sf;

                new_sf = open_clamp_streamfile(temp_sf, ea->stream_offset, data_size);
                if (!new_sf) goto fail;
                temp_sf = new_sf;
                break;
            case EAAC_TYPE_GIGASAMPLE:
                /* header and prefetched data in SNR, rest of data in SNS */
                /* open prefetched data */
                data_size = calculate_eaac_size(sf_head, ea, ea->prefetch_samples, ea->prefetch_offset, 1);
                if (data_size == 0) goto fail;

                new_sf = open_wrap_streamfile(sf_head);
                if (!new_sf) goto fail;
                sf_segments[0] = new_sf;

                new_sf = open_clamp_streamfile(sf_segments[0], ea->prefetch_offset, data_size);
                if (!new_sf) goto fail;
                sf_segments[0] = new_sf;

                /* open main data */
                data_size = calculate_eaac_size(sf_data, ea, ea->num_samples - ea->prefetch_samples, ea->stream_offset, 0);
                if (data_size == 0) goto fail;

                new_sf = open_wrap_streamfile(sf_data);
                if (!new_sf) goto fail;
                sf_segments[1] = new_sf;

                new_sf = open_clamp_streamfile(sf_segments[1], ea->stream_offset, data_size);
                if (!new_sf) goto fail;
                sf_segments[1] = new_sf;

                new_sf = open_multifile_streamfile(sf_segments, 2);
                if (!new_sf) goto fail;
                temp_sf = new_sf;
                sf_segments[0] = NULL;
                sf_segments[1] = NULL;
                break;
            default:
                goto fail;
        }
    } else {
        data_size = calculate_eaac_size(sf_head, ea, ea->num_samples, ea->stream_offset, 0);
        if (data_size == 0) goto fail;

        new_sf = open_wrap_streamfile(sf_head);
        if (!new_sf) goto fail;
        temp_sf = new_sf;

        new_sf = open_clamp_streamfile(temp_sf, ea->stream_offset, data_size);
        if (!new_sf) goto fail;
        temp_sf = new_sf;
    }

    return temp_sf;

fail:
    close_streamfile(sf_segments[0]);
    close_streamfile(sf_segments[1]);
    close_streamfile(temp_sf);

    return NULL;
}

/* 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,
 * and the segments seem fully separate (so even skipping would probably decode wrong). */
// todo reorganize code for more standard init
static segmented_layout_data* build_segmented_eaaudiocore_looping(STREAMFILE *sf_data, eaac_header *eaac) {
    segmented_layout_data *data = NULL;
    STREAMFILE* temp_sf = NULL;
    off_t offsets[2] = { 0x00, eaac->loop_offset };
    off_t start_offset;
    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++) {
        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_FFMPEG
            case EAAC_CODEC_EAXMA: {
                eaac_header temp_eaac = *eaac; /* equivalent to memcpy... I think */
                temp_eaac.loop_flag = 0;
                temp_eaac.num_samples = num_samples[i];

                start_offset = 0x00; /* must point to the custom streamfile's beginning */

                /* layers inside segments, how trippy */
                data->segments[i]->layout_data = build_layered_eaaudiocore(sf_data, &temp_eaac, offsets[i]);
                if (!data->segments[i]->layout_data) goto fail;
                data->segments[i]->coding_type = coding_FFmpeg;
                data->segments[i]->layout_type = layout_layered;
                break;
            }
#endif

            case EAAC_CODEC_XAS1: {
                start_offset = offsets[i];

                data->segments[i]->coding_type = coding_EA_XAS_V1;
                data->segments[i]->layout_type = layout_blocked_ea_sns;
                break;
            }

#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);

                start_offset = 0x00; /* must point to the custom streamfile's beginning */

                temp_sf = setup_eaac_audio_streamfile(sf_data, eaac->version,eaac->codec,eaac->streamed,0,0, offsets[i]);
                if (!temp_sf) goto fail;

                data->segments[i]->codec_data = init_mpeg_custom(temp_sf, 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_sf == NULL ? sf_data : temp_sf, start_offset))
            goto fail;

        close_streamfile(temp_sf);
        temp_sf = NULL;
    }

    if (!setup_layout_segmented(data))
        goto fail;
    return data;

fail:
    close_streamfile(temp_sf);
    free_layout_segmented(data);
    return NULL;
}

static layered_layout_data* build_layered_eaaudiocore(STREAMFILE *sf_data, eaac_header *eaac, off_t start_offset) {
    layered_layout_data* data = NULL;
    STREAMFILE* temp_sf = NULL;
    int i, layers = (eaac->channels+1) / 2;


    /* init layout */
    data = init_layout_layered(layers);
    if (!data) goto fail;

    /* open each layer subfile (1/2ch streams: 2ch+2ch..+1ch or 2ch+2ch..+2ch). */
    for (i = 0; i < layers; i++) {
        int layer_channels = (i+1 == layers && eaac->channels % 2 == 1) ? 1 : 2; /* last layer can be 1/2ch */

        /* build the layer VGMSTREAM */
        data->layers[i] = allocate_vgmstream(layer_channels, eaac->loop_flag);
        if (!data->layers[i]) goto fail;

        data->layers[i]->sample_rate = eaac->sample_rate;
        data->layers[i]->num_samples = eaac->num_samples;
        data->layers[i]->loop_start_sample = eaac->loop_start;
        data->layers[i]->loop_end_sample = eaac->loop_end;

        switch(eaac->codec) {
#ifdef VGM_USE_FFMPEG
            /* EA-XMA uses completely separate 1/2ch streams, unlike standard XMA that interleaves 1/2ch
             * streams with a skip counter to reinterleave (so EA-XMA streams don't have skips set) */
            case EAAC_CODEC_EAXMA: {
                uint8_t buf[0x100];
                int bytes, block_size, block_count;
                size_t stream_size;
                int is_xma1;

                temp_sf = setup_eaac_audio_streamfile(sf_data, eaac->version, eaac->codec, eaac->streamed,i,layers, start_offset);
                if (!temp_sf) goto fail;

                stream_size = get_streamfile_size(temp_sf);
                block_size = 0x10000; /* unused */
                block_count = stream_size / block_size + (stream_size % block_size ? 1 : 0);

                /* EA adopted XMA2 when it appeared around 2006, but detection isn't so easy
                 * (SNS with XMA2 do exist). Decoder should work when playing XMA1 as XMA2, but
                 * the other way around can cause issues, so it's safer to just use XMA2. */
                is_xma1 = 0; //eaac->version == EAAC_VERSION_V0; /* approximate */
                if (is_xma1)
                    bytes = ffmpeg_make_riff_xma1(buf, 0x100, data->layers[i]->num_samples, stream_size, data->layers[i]->channels, data->layers[i]->sample_rate, 0);
                else
                    bytes = ffmpeg_make_riff_xma2(buf, 0x100, data->layers[i]->num_samples, stream_size, data->layers[i]->channels, data->layers[i]->sample_rate, block_count, block_size);
                data->layers[i]->codec_data = init_ffmpeg_header_offset(temp_sf, buf,bytes, 0x00, stream_size);
                if (!data->layers[i]->codec_data) goto fail;

                data->layers[i]->coding_type = coding_FFmpeg;
                data->layers[i]->layout_type = layout_none;
                data->layers[i]->stream_size = get_streamfile_size(temp_sf);

                xma_fix_raw_samples(data->layers[i], temp_sf, 0x00,stream_size, 0, 0,0); /* samples are ok? */
                break;
            }

            /* Opus can do multichannel just fine, but that wasn't weird enough for EA */
            case EAAC_CODEC_EAOPUS: {
                int skip;
                size_t data_size;

                /* We'll remove EA blocks and pass raw data to FFmpeg Opus decoder */
                temp_sf = setup_eaac_audio_streamfile(sf_data, eaac->version, eaac->codec, eaac->streamed,i,layers, start_offset);
                if (!temp_sf) goto fail;

                skip = ea_opus_get_encoder_delay(0x00, temp_sf);
                data_size = get_streamfile_size(temp_sf);

                data->layers[i]->codec_data = init_ffmpeg_ea_opus(temp_sf, 0x00,data_size, layer_channels, skip, eaac->sample_rate);
                if (!data->layers[i]->codec_data) goto fail;
                data->layers[i]->coding_type = coding_FFmpeg;
                data->layers[i]->layout_type = layout_none;
                break;
            }
#endif
            default:
                goto fail;
        }

        if (!vgmstream_open_stream(data->layers[i], temp_sf, 0x00)) {
            goto fail;
        }

        close_streamfile(temp_sf);
        temp_sf = NULL;
    }

    if (!setup_layout_layered(data))
        goto fail;
    return data;

fail:
    close_streamfile(temp_sf);
    free_layout_layered(data);
    return NULL;
}
