#include "meta.h"
#include "../layout/layout.h"
#include "../coding/coding.h"
#include "ubi_sb_streamfile.h"


#define SB_MAX_LAYER_COUNT 16  /* arbitrary max */
#define SB_MAX_CHAIN_COUNT 256 /* +150 exist in Tonic Trouble */

typedef enum { UBI_IMA, UBI_ADPCM, RAW_PCM, RAW_PSX, RAW_DSP, RAW_XBOX, FMT_VAG, FMT_AT3, RAW_AT3, FMT_XMA1, RAW_XMA1, FMT_OGG, FMT_CWAV, FMT_APM, FMT_MPDX } ubi_sb_codec;
typedef enum { UBI_PC, UBI_PS2, UBI_XBOX, UBI_GC, UBI_X360, UBI_PSP, UBI_PS3, UBI_WII, UBI_3DS } ubi_sb_platform;
typedef enum { UBI_NONE = 0, UBI_AUDIO, UBI_LAYER, UBI_SEQUENCE, UBI_SILENCE } ubi_sb_type;

typedef struct {
    int map_version;
    size_t map_entry_size;
    size_t section1_entry_size;
    size_t section2_entry_size;
    size_t section3_entry_size;
    size_t resource_name_size;

    off_t audio_extra_offset;
    off_t audio_stream_size;
    off_t audio_stream_offset;
    off_t audio_stream_type;
    off_t audio_group_id;
    off_t audio_external_flag;
    off_t audio_loop_flag;
    off_t audio_num_samples;
    off_t audio_num_samples2;
    off_t audio_sample_rate;
    off_t audio_channels;
    off_t audio_stream_name;
    off_t audio_extra_name;
    off_t audio_xma_offset;
    int audio_external_and;
    int audio_loop_and;
    int audio_group_and;
    int num_codec_flags;
    int audio_has_internal_names;
    size_t audio_interleave;
    int audio_fix_psx_samples;

    off_t sequence_extra_offset;
    off_t sequence_sequence_loop;
    off_t sequence_sequence_single;
    off_t sequence_sequence_count;
    off_t sequence_entry_number;
    size_t sequence_entry_size;

    off_t layer_extra_offset;
    off_t layer_layer_count;
    off_t layer_stream_size;
    off_t layer_stream_offset;
    off_t layer_stream_name;
    off_t layer_extra_name;
    off_t layer_sample_rate;
    off_t layer_channels;
    off_t layer_stream_type;
    off_t layer_num_samples;
    size_t layer_entry_size;
    size_t layer_hijack;

    off_t silence_duration_int;
    off_t silence_duration_float;

    off_t random_extra_offset;
    off_t random_sequence_count;
    size_t random_entry_size;
    int random_percent_int;

    int is_padded_section1_offset;
    int is_padded_section2_offset;
    int is_padded_section3_offset;
    int is_padded_sectionX_offset;
    int is_padded_sounds_offset;
    int ignore_layer_error;
    int default_codec_for_group0;
} ubi_sb_config;

typedef struct {
    ubi_sb_platform platform;
    int is_psp_old;
    int big_endian;
    int total_subsongs;
    int bank_subsongs;

    /* SB config */
    /* header varies slightly per game/version but not enough parse case by case,
     * instead we configure sizes and offsets to where each variable is */
    ubi_sb_config cfg;

    /* map base header info */
    off_t map_start;
    off_t map_num;

    uint32_t map_type;
    uint32_t map_zero;
    off_t map_offset;
    off_t map_size;
    char map_name[0x28];
    uint32_t map_unknown;

    /* SB info (some values are derived depending if it's standard sbX or map sbX) */
    int is_bank;
    int is_map;
    int is_bnm;
    uint32_t version;           /* 16b+16b major+minor version */
    uint32_t version_empty;     /* map sbX versions are empty */
    /* events (often share header_id/type with some descriptors,
     * but may exist without headers or header exist without them) */
    size_t section1_num;
    size_t section1_offset;
    /* descriptors, audio header or other config types */
    size_t section2_num;
    size_t section2_offset;
    /* internal streams table, referenced by each header */
    size_t section3_num;
    size_t section3_offset;
    /* section with sounds in some map versions */
    size_t section4_num;
    size_t section4_offset;
    /* extra table, config for certain types (DSP coefs, external resources, layer headers, etc) */
    size_t sectionX_size;
    size_t sectionX_offset;
    /* unknown, usually -1 but can be others (0/1/2/etc) */
    int flag1;
    int flag2;

    /* header/stream info */
    ubi_sb_type type;           /* unified type */
    ubi_sb_codec codec;         /* unified codec */
    int header_index;           /* entry number within section2 */
    off_t header_offset;        /* entry offset within section2 */
    uint32_t header_id;         /* 16b+16b group+sound identifier (unique within a sbX, but not smX), may start from 0 */
    uint32_t header_type;       /* parsed type (we only need audio types) */
    off_t extra_offset;         /* offset within sectionX to extra data */
    off_t stream_offset;        /* offset within the data section (internal) or absolute (external) to the audio */
    size_t stream_size;         /* size of the audio data */
    uint32_t stream_type;       /* rough codec value */
    uint32_t group_id;          /* internal id to reference in section3 */

    int loop_flag;              /* stream loops (normally internal sfx, but also external music) */
    int loop_start;             /* usually 0 */
    int num_samples;            /* should match manually calculated samples */
    int sample_rate;
    int channels;
    off_t xma_header_offset;    /* some XMA have extra header stuff */

    int layer_count;            /* number of layers in a layer type */
    int layer_channels[SB_MAX_LAYER_COUNT];
    int sequence_count;         /* number of segments in a sequence type */
    int sequence_chain[SB_MAX_CHAIN_COUNT]; /* sequence of entry numbers */
    int sequence_banks[SB_MAX_CHAIN_COUNT]; /* sequence of bnk bank numbers */
    int sequence_multibank;     /* info flag */
    int sequence_loop;          /* chain index to loop */
    int sequence_single;        /* if que sequence plays once (loops by default) */

    float duration;             /* silence duration */

    int is_external;            /* stream is in a external file */
    char resource_name[0x28];   /* filename to the external stream, or internal stream info for some games */

    char readable_name[255];    /* final subsong name */
    int types[16];              /* counts each header types, for debugging */
    int allowed_types[16];
} ubi_sb_header;

static int parse_bnm_header(ubi_sb_header * sb, STREAMFILE *streamFile);
static int parse_header(ubi_sb_header * sb, STREAMFILE *streamFile, off_t offset, int index);
static int parse_sb(ubi_sb_header * sb, STREAMFILE *streamFile, int target_subsong);
static VGMSTREAM * init_vgmstream_ubi_sb_header(ubi_sb_header *sb, STREAMFILE* streamTest, STREAMFILE *streamFile);
static int config_sb_platform(ubi_sb_header * sb, STREAMFILE *streamFile);
static int config_sb_version(ubi_sb_header * sb, STREAMFILE *streamFile);


/* .SBx - banks from Ubisoft's DARE (Digital Audio Rendering Engine) engine games in ~2000-2008+ */
VGMSTREAM * init_vgmstream_ubi_sb(STREAMFILE *streamFile) {
    VGMSTREAM* vgmstream = NULL;
    STREAMFILE *streamTest = NULL;
    int32_t(*read_32bit)(off_t, STREAMFILE*) = NULL;
    ubi_sb_header sb = {0};
    int target_subsong = streamFile->stream_index;


    /* checks (number represents the platform, see later) */
    if (!check_extensions(streamFile, "sb0,sb1,sb2,sb3,sb4,sb5,sb6,sb7"))
        goto fail;

    /* .sbX (sound bank) is a small multisong format (loaded in memory?) that contains SFX data
     * but can also reference .ss0/ls0 (sound stream) external files for longer streams.
     * A companion .sp0 (sound project) describes files and if it uses BANKs (.sbX) or MAPs (.smX). */


    /* PLATFORM DETECTION */
    if (!config_sb_platform(&sb, streamFile))
        goto fail;
    read_32bit = sb.big_endian ? read_32bitBE : read_32bitLE;

    if (target_subsong <= 0) target_subsong = 1;

    /* use smaller header buffer for performance */
    streamTest = reopen_streamfile(streamFile, 0x100);
    if (!streamTest) goto fail;


    /* SB HEADER */
    /* SBx layout: header, section1, section2, extra section, section3, data (all except header can be null) */
    sb.is_bank = 1;
    sb.version       = read_32bit(0x00, streamFile);

    if (!config_sb_version(&sb, streamFile))
        goto fail;

    if (sb.version <= 0x0000000D) {
        sb.section1_num = read_32bit(0x04, streamFile);
        sb.section2_num = read_32bit(0x0c, streamFile);
        sb.section3_num = read_32bit(0x14, streamFile);
        sb.sectionX_size = read_32bit(0x1c, streamFile);

        sb.section1_offset = 0x20;
    } else if (sb.version <= 0x000A0000) {
        sb.section1_num = read_32bit(0x04, streamFile);
        sb.section2_num = read_32bit(0x0c, streamFile);
        sb.section3_num = read_32bit(0x14, streamFile);
        sb.sectionX_size = read_32bit(0x1c, streamFile);
        sb.flag1 = read_32bit(0x14, streamFile);

        sb.section1_offset = 0x18;
    } else {
        sb.section1_num = read_32bit(0x04, streamFile);
        sb.section2_num = read_32bit(0x0c, streamFile);
        sb.section3_num = read_32bit(0x14, streamFile);
        sb.sectionX_size = read_32bit(0x1c, streamFile);
        sb.flag1 = read_32bit(0x14, streamFile);
        sb.flag2 = read_32bit(0x18, streamFile);

        sb.section1_offset = 0x1c;
    }

    if (sb.cfg.is_padded_section1_offset)
        sb.section1_offset = align_size_to_block(sb.section1_offset, 0x10);

    sb.section2_offset = sb.section1_offset + sb.cfg.section1_entry_size * sb.section1_num;
    if (sb.cfg.is_padded_section2_offset)
        sb.section2_offset = align_size_to_block(sb.section2_offset, 0x10);

    sb.sectionX_offset = sb.section2_offset + sb.cfg.section2_entry_size * sb.section2_num;
    if (sb.cfg.is_padded_sectionX_offset)
        sb.sectionX_offset = align_size_to_block(sb.sectionX_offset, 0x10);

    sb.section3_offset = sb.sectionX_offset + sb.sectionX_size;
    if (sb.cfg.is_padded_section3_offset)
        sb.section3_offset = align_size_to_block(sb.section3_offset, 0x10);

    if (!parse_sb(&sb, streamTest, target_subsong))
        goto fail;

    /* CREATE VGMSTREAM */
    vgmstream = init_vgmstream_ubi_sb_header(&sb, streamTest, streamFile);
    close_streamfile(streamTest);
    return vgmstream;

fail:
    close_streamfile(streamTest);
    return NULL;
}

/* .SMx - maps (sets of custom SBx files) also from Ubisoft's sound engine games in ~2000-2008+ */
VGMSTREAM * init_vgmstream_ubi_sm(STREAMFILE *streamFile) {
    VGMSTREAM* vgmstream = NULL;
    STREAMFILE *streamTest = NULL;
    int32_t(*read_32bit)(off_t, STREAMFILE*) = NULL;
    ubi_sb_header sb = {0}, target_sb = {0};
    int target_subsong = streamFile->stream_index;
    int i;


    /* checks (number represents platform, lmX are localized variations) */
    if (!check_extensions(streamFile, "sm0,sm1,sm2,sm3,sm4,sm5,sm6,sm7,lm0,lm1,lm2,lm3,lm4,lm5,lm6,lm7"))
        goto fail;

    /* .smX (sound map) is a set of slightly different sbX files, compiled into one "map" file.
     * Map has a sbX (called "submap") per named area (example: menu, level1, boss1, level2...).
     * This counts subsongs from all sbX, so totals can be massive, but there are splitters into mini-smX. */


    /* PLATFORM DETECTION */
    if (!config_sb_platform(&sb, streamFile))
        goto fail;
    read_32bit = sb.big_endian ? read_32bitBE : read_32bitLE;

    if (target_subsong <= 0) target_subsong = 1;

    /* use smaller header buffer for performance */
    streamTest = reopen_streamfile(streamFile, 0x100);
    if (!streamTest) goto fail;


    /* SM BASE HEADER */
    /* SMx layout: header with N map area offset/sizes + custom SBx with relative offsets */
    sb.is_map = 1;
    sb.version   = read_32bit(0x00, streamFile);
    sb.map_start = read_32bit(0x04, streamFile);
    sb.map_num   = read_32bit(0x08, streamFile);

    if (!config_sb_version(&sb, streamFile))
        goto fail;


    for (i = 0; i < sb.map_num; i++) {
        off_t offset = sb.map_start + i * sb.cfg.map_entry_size;

        /* SUBMAP HEADER */
        sb.map_type     = read_32bit(offset + 0x00, streamFile); /* usually 0/1=first, 0=rest */
        sb.map_zero     = read_32bit(offset + 0x04, streamFile);
        sb.map_offset   = read_32bit(offset + 0x08, streamFile);
        sb.map_size     = read_32bit(offset + 0x0c, streamFile); /* includes sbX header, but not internal streams */
        read_string(sb.map_name, sizeof(sb.map_name), offset + 0x10, streamFile); /* null-terminated and may contain garbage after null */
        if (sb.cfg.map_version >= 3)
            sb.map_unknown  = read_32bit(offset + 0x30, streamFile); /* uncommon, id/config? longer name? mem garbage? */

        /* SB HEADER */
        /* SBx layout: base header, section1, section2, section4, extra section, section3, data (all except header can be null?) */
        sb.version_empty    = read_32bit(sb.map_offset + 0x00, streamFile); /* sbX in maps don't set version */
        sb.section1_offset  = read_32bit(sb.map_offset + 0x04, streamFile) + sb.map_offset;
        sb.section1_num     = read_32bit(sb.map_offset + 0x08, streamFile);
        sb.section2_offset  = read_32bit(sb.map_offset + 0x0c, streamFile) + sb.map_offset;
        sb.section2_num     = read_32bit(sb.map_offset + 0x10, streamFile);

        if (sb.cfg.map_version < 3) {
            sb.section3_offset  = read_32bit(sb.map_offset + 0x14, streamFile) + sb.map_offset;
            sb.section3_num     = read_32bit(sb.map_offset + 0x18, streamFile);
            sb.sectionX_offset  = read_32bit(sb.map_offset + 0x1c, streamFile) + sb.map_offset;
            sb.sectionX_size    = read_32bit(sb.map_offset + 0x20, streamFile);
        } else {
            sb.section4_offset  = read_32bit(sb.map_offset + 0x14, streamFile);
            sb.section4_num     = read_32bit(sb.map_offset + 0x18, streamFile);
            sb.section3_offset  = read_32bit(sb.map_offset + 0x1c, streamFile) + sb.map_offset;
            sb.section3_num     = read_32bit(sb.map_offset + 0x20, streamFile);
            sb.sectionX_offset  = read_32bit(sb.map_offset + 0x24, streamFile) + sb.map_offset;
            sb.sectionX_size    = read_32bit(sb.map_offset + 0x28, streamFile);

            /* latest map format has another section with sounds after section 2 */
            sb.section2_num    += sb.section4_num;    /* let's just merge it with section 2 */
            sb.sectionX_offset += sb.section4_offset; /* for some reason, this is relative to section 4 here */
        }

        VGM_ASSERT(sb.map_type != 0 && sb.map_type != 1, "UBI SM: unknown map_type at %x\n", (uint32_t)offset);
        VGM_ASSERT(sb.map_zero != 0, "UBI SM: unknown map_zero at %x\n", (uint32_t)offset);
        //;VGM_ASSERT(sb.map_unknown != 0, "UBI SM: unknown map_unknown at %x\n", (uint32_t)offset);
        VGM_ASSERT(sb.version_empty != 0, "UBI SM: unknown version_empty at %x\n", (uint32_t)offset);

        if (!parse_sb(&sb, streamTest, target_subsong))
            goto fail;

        /* snapshot of current sb if subsong was found
         * (it gets rewritten and we need exact values for sequences and stuff) */
        if (sb.type != UBI_NONE) {
            target_sb = sb; /* memcpy */
            sb.type = UBI_NONE; /* reset parsed flag */
        }
    }

    target_sb.total_subsongs = sb.total_subsongs;

    /* CREATE VGMSTREAM */
    vgmstream = init_vgmstream_ubi_sb_header(&target_sb, streamTest, streamFile);
    close_streamfile(streamTest);
    return vgmstream;

fail:
    close_streamfile(streamTest);
    return NULL;
}


/* .BNM - proto-sbX with map style format [Rayman 2 (PC), Donald Duck: Goin' Quackers (PC), Tonic Trouble (PC)] */
VGMSTREAM * init_vgmstream_ubi_bnm(STREAMFILE *streamFile) {
    VGMSTREAM* vgmstream = NULL;
    STREAMFILE *streamTest = NULL;
    ubi_sb_header sb = {0};
    int target_subsong = streamFile->stream_index;

    if (target_subsong <= 0) target_subsong = 1;


    /* checks */
    if (!check_extensions(streamFile, "bnm"))
        goto fail;

    /* v0, header is somewhat like a map-style bank (offsets + sizes) but sectionX/3 fields are
     * fixed/reserved (unused?). Header entry sizes and config works the same, and type numbers are
     * slightly different, but otherwise pretty much the same engine (not named DARE yet). Curiously
     * it may stream RIFF .wav (stream_offset pointing to "data"), and also .raw (PCM) or .apm IMA. */

    /* use smaller header buffer for performance */
    streamTest = reopen_streamfile(streamFile, 0x100);
    if (!streamTest) goto fail;

    if (!parse_bnm_header(&sb, streamTest))
        goto fail;

    if (!parse_sb(&sb, streamTest, target_subsong))
        goto fail;

    /* CREATE VGMSTREAM */
    vgmstream = init_vgmstream_ubi_sb_header(&sb, streamTest, streamFile);
    close_streamfile(streamTest);
    return vgmstream;

fail:
    close_streamfile(streamTest);
    return NULL;
}

static int parse_bnm_header(ubi_sb_header * sb, STREAMFILE *streamFile) {
    int32_t(*read_32bit)(off_t, STREAMFILE*) = NULL;

    /* PLATFORM DETECTION */
    sb->platform = UBI_PC;
    read_32bit = sb->big_endian ? read_32bitBE : read_32bitLE;

    /* SB HEADER */
    /* SBx layout: header, section1, section2, extra section, section3, data (all except header can be null) */
    sb->is_bnm = 1;
    sb->version          = read_32bit(0x00, streamFile);
    sb->section1_offset  = read_32bit(0x04, streamFile);
    sb->section1_num     = read_32bit(0x08, streamFile);
    sb->section2_offset  = read_32bit(0x0c, streamFile);
    sb->section2_num     = read_32bit(0x10, streamFile);
    /* next are data start offset x3 + data size offset x3 */
    sb->section3_offset  = read_32bit(0x14, streamFile);
    sb->section3_num     = 0;

    if (!config_sb_version(sb, streamFile))
        goto fail;

    sb->sectionX_offset  = sb->section2_offset + sb->section2_num * sb->cfg.section2_entry_size;
    sb->sectionX_size    = sb->section3_offset - sb->sectionX_offset;

    return 1;
fail:
    return 0;
}

static int is_bnm_other_bank(STREAMFILE *streamFile, int bank_number) {
    char current_name[PATH_LIMIT];
    char bank_name[255];

    get_streamfile_filename(streamFile, current_name, PATH_LIMIT);
    sprintf(bank_name, "Bnk_%i.bnm", bank_number);

    return strcmp(current_name, bank_name) != 0;
}

#if 0
/* .BLK - maps in separate .blk chunks [Donald Duck: Goin' Quackers (PS2), The Jungle Book Rhythm N'Groove (PS2)] */
VGMSTREAM * init_vgmstream_ubi_blk(STREAMFILE *streamFile) {

    /* Somewhat equivalent to a v0x00000003 map:
     * - HEADER.BLK: base map header (slightly different?) + submaps headers
     * - EVT.BLK: section1 from all submaps
     * - RES.BLK: section2 + sectionX from all submaps
     * - MAPS.BLK, MAPLANG.BLK: section3 variation?
     * - STREAMED.BLK, STRLANG.BLK: audio data
     *
     * Parsing may be be simplified with multifile_streamfiles?
     */
    return NULL;
}
#endif

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

static VGMSTREAM * init_vgmstream_ubi_sb_base(ubi_sb_header *sb, STREAMFILE *streamHead, STREAMFILE *streamData, off_t start_offset) {
    VGMSTREAM * vgmstream = NULL;


    /* build the VGMSTREAM */
    vgmstream = allocate_vgmstream(sb->channels, sb->loop_flag);
    if (!vgmstream) goto fail;

    vgmstream->meta_type = meta_UBI_SB;
    vgmstream->sample_rate = sb->sample_rate;
    vgmstream->num_streams = sb->total_subsongs;
    vgmstream->stream_size = sb->stream_size;

    vgmstream->num_samples = sb->num_samples;
    vgmstream->loop_start_sample = sb->loop_start;
    vgmstream->loop_end_sample = sb->num_samples;

    switch(sb->codec) {
        case UBI_IMA:
            vgmstream->coding_type = coding_UBI_IMA;
            vgmstream->layout_type = layout_none;
            break;

        case UBI_ADPCM:
            /* custom Ubi 4/6-bit ADPCM used in early games:
             * - Splinter Cell (PC): 4-bit w/ 2ch (music), 6-bit w/ 1ch (sfx)
             * - Batman: Vengeance (PC): 4-bit w/ 2ch (music), 6-bit w/ 1ch (sfx)
             * - Myst IV (PC/Xbox): 4bit-1ch (amb), some files only (ex. sfx_si_puzzle_stream.sb2)
             * - possibly others */

            /* skip extra header (some kind of id?) found in Myst IV */
            if (read_32bitBE(start_offset + 0x00, streamData) != 0x08000000 &&
                read_32bitBE(start_offset + 0x08, streamData) == 0x08000000) {
                start_offset += 0x08;
                sb->stream_size -= 0x08;
            }

            vgmstream->codec_data = init_ubi_adpcm(streamData, start_offset, vgmstream->channels);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_UBI_ADPCM;
            vgmstream->layout_type = layout_none;
            break;

        case RAW_PCM:
            vgmstream->coding_type = coding_PCM16LE; /* always LE */
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x02;

            if (vgmstream->num_samples == 0) { /* happens in .bnm */
                //todo with external wav streams stream_size may be off?
                vgmstream->num_samples       = pcm_bytes_to_samples(sb->stream_size, sb->channels, 16);
                vgmstream->loop_end_sample   = vgmstream->num_samples;
            }
            break;

        case RAW_PSX:
            vgmstream->coding_type = coding_PSX;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size =  (sb->cfg.audio_interleave) ?
                            sb->cfg.audio_interleave :
                            sb->stream_size / sb->channels;

            if (vgmstream->num_samples == 0) { /* early PS2 games may not set it for internal streams */
                vgmstream->num_samples = ps_bytes_to_samples(sb->stream_size, sb->channels);
                vgmstream->loop_end_sample = vgmstream->num_samples;
            }

            /* late PS3 SBs have double sample count here for who knows why
             * (loops or not, PS-ADPCM only, possibly only when using codec 0x02 for RAW_PSX) */
            if (sb->cfg.audio_fix_psx_samples) {
                vgmstream->num_samples /= sb->channels;
                vgmstream->loop_start_sample /= sb->channels;
                vgmstream->loop_end_sample /= sb->channels;
            }

            break;

        case RAW_XBOX:
            vgmstream->coding_type = coding_XBOX_IMA;
            vgmstream->layout_type = layout_none;
            break;

        case RAW_DSP:
            vgmstream->coding_type = coding_NGC_DSP;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = align_size_to_block(sb->stream_size / sb->channels, 0x08); /* frame-aligned */

            /* mini DSP header (first 0x10 seem to contain DSP header fields like nibbles and format) */
            dsp_read_coefs_be(vgmstream, streamHead, sb->extra_offset + 0x10, 0x40);
            dsp_read_hist_be (vgmstream, streamHead, sb->extra_offset + 0x34, 0x40); /* after gain/initial ps */
            break;

        case FMT_VAG:
            /* skip VAG header (some sb4 use VAG and others raw PSX) */
            if (read_32bitBE(start_offset, streamData) == 0x56414770) { /* "VAGp" */
                start_offset += 0x30;
                sb->stream_size  -= 0x30;
            }

            vgmstream->coding_type = coding_PSX;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = sb->stream_size / sb->channels;
            break;

#ifdef VGM_USE_FFMPEG
        case FMT_AT3: {
            /* skip weird value (3 or 4) in Brothers in Arms: D-Day (PSP) */
            if (read_32bitBE(start_offset+0x04,streamData) == 0x52494646) {
                VGM_LOG("UBI SB: skipping unknown value 0x%x before RIFF\n", read_32bitBE(start_offset+0x00,streamData));
                start_offset += 0x04;
                sb->stream_size -= 0x04;
            }

            vgmstream->codec_data = init_ffmpeg_atrac3_riff(streamData, start_offset, NULL);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;
            break;
        }

        case RAW_AT3: {
            int block_align, encoder_delay;

            block_align = 0x98 * sb->channels;
            encoder_delay = 1024 + 69*2; /* approximate */

            vgmstream->codec_data = init_ffmpeg_atrac3_raw(streamData, start_offset,sb->stream_size, sb->num_samples,sb->channels,sb->sample_rate, block_align, encoder_delay);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;
            break;
        }

        //todo: some XMA1 decode a bit strangely at certain positions (FFmpeg bug?)
        case FMT_XMA1: {
            ffmpeg_codec_data *ffmpeg_data;
            uint8_t buf[0x100];
            uint32_t sec1_num, sec2_num, sec3_num, bits_per_frame;
            uint8_t flag;
            size_t bytes, chunk_size, header_size, data_size;
            off_t header_offset;

            chunk_size = 0x20;

            /* formatted XMA sounds have a strange custom header */
            header_offset = start_offset; /* XMA fmt chunk at the start */
            flag = read_8bit(header_offset + 0x20, streamData);
            sec2_num = read_32bitBE(header_offset + 0x24, streamData); /* number of XMA frames */
            sec1_num = read_32bitBE(header_offset + 0x28, streamData);
            sec3_num = read_32bitBE(header_offset + 0x2c, streamData);

            bits_per_frame = 4;
            if (flag == 0x02 || flag == 0x04)
                bits_per_frame = 2;
            else if (flag == 0x08)
                bits_per_frame = 1;

            header_size = 0x30;
            header_size += sec1_num * 0x04;
            header_size += align_size_to_block(sec2_num * bits_per_frame, 32) / 8; /* bitstream seek table? */
            header_size += sec3_num * 0x08;
            start_offset += header_size;
            data_size = sec2_num * 0x800;

            bytes = ffmpeg_make_riff_xma_from_fmt_chunk(buf, 0x100, header_offset, chunk_size, data_size, streamData, 1);

            ffmpeg_data = init_ffmpeg_header_offset(streamData, buf, bytes, start_offset, data_size);
            if (!ffmpeg_data) goto fail;
            vgmstream->codec_data = ffmpeg_data;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;

            xma_fix_raw_samples_ch(vgmstream, streamData, start_offset, data_size, sb->channels, 0, 0);
            break;
        }

        case RAW_XMA1: {
            ffmpeg_codec_data *ffmpeg_data;
            uint8_t buf[0x100];
            size_t bytes, chunk_size;
            off_t header_offset;

            VGM_ASSERT(sb->is_external, "Ubi SB: Raw XMA used for external sound\n");

            /* get XMA header from extra section */
            chunk_size = 0x20;
            header_offset = sb->xma_header_offset;
            if (header_offset == 0)
                header_offset = sb->extra_offset;

            bytes = ffmpeg_make_riff_xma_from_fmt_chunk(buf, 0x100, header_offset, chunk_size, sb->stream_size, streamHead, 1);

            ffmpeg_data = init_ffmpeg_header_offset(streamData, buf, bytes, start_offset, sb->stream_size);
            if (!ffmpeg_data) goto fail;
            vgmstream->codec_data = ffmpeg_data;
            vgmstream->coding_type = coding_FFmpeg;
            vgmstream->layout_type = layout_none;

            xma_fix_raw_samples_ch(vgmstream, streamData, start_offset, sb->stream_size, sb->channels, 0, 0);
            break;
        }
#endif
#ifdef VGM_USE_VORBIS
        case FMT_OGG: {
            vgmstream->codec_data = init_ogg_vorbis(streamData, start_offset, sb->stream_size, NULL);
            if (!vgmstream->codec_data) goto fail;
            vgmstream->coding_type = coding_OGG_VORBIS;
            vgmstream->layout_type = layout_none;
            break;
        }
#endif
        case FMT_CWAV:
            if (sb->channels > 1) goto fail; /* unknown layout */
            vgmstream->coding_type = coding_NGC_DSP;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x08;

            dsp_read_coefs_le(vgmstream,streamData,start_offset + 0x7c, 0x40);
            start_offset += 0xe0; /* skip CWAV header */
            break;

        case FMT_APM:
            /* APM is a full format though most fields are repeated from .bnm
             * (info from https://github.com/Synthesis/ray2get)
             * 0x00(2): format tag (0x2000 for Ubisoft ADPCM)
             * 0x02(2): channels
             * 0x04(4): sample rate
             * 0x08(4): byte rate? PCM samples?
             * 0x0C(2): block align
             * 0x0E(2): bits per sample
             * 0x10(4): header size
             * 0x14(4): "vs12"
             * 0x18(4): file size
             * 0x1C(4): nibble size
             * 0x20(4): -1?
             * 0x24(4): 0?
             * 0x28(4): high/low nibble flag (when loaded in memory)
             * 0x2C(N): ADPCM info per channel, last to first
             * - 0x00(4): ADPCM hist
             * - 0x04(4): ADPCM step index
             * - 0x08(4): copy of ADPCM data (after interleave, ex. R from data + 0x01)
             * 0x60(4): "DATA"
             * 0x64(N): ADPCM data
             */

            vgmstream->coding_type = coding_DVI_IMA_int;
            vgmstream->layout_type = layout_interleave;
            vgmstream->interleave_block_size = 0x01;

            /* read initial hist (last to first) */
            {
                int i;
                for (i = 0; i < sb->channels; i++) {
                    vgmstream->ch[i].adpcm_history1_32 = read_32bitLE(start_offset + 0x2c + 0x0c*(sb->channels - 1 - i) + 0x00, streamData);
                    vgmstream->ch[i].adpcm_step_index  = read_32bitLE(start_offset + 0x2c + 0x0c*(sb->channels - 1 - i) + 0x04, streamData);
                }
            }
            //todo supposedly APM IMA removes lower 3b after assigning step, but wave looks a bit off (Rayman 2 only?):
            // ...; step = adpcm_table[step_index]; delta = (step >> 3); step &= (~7); ...

            start_offset += 0x64; /* skip APM header (may be internal or external) */

            if (vgmstream->num_samples == 0) {
                vgmstream->num_samples       = ima_bytes_to_samples(sb->stream_size - 0x64, sb->channels);
                vgmstream->loop_end_sample   = vgmstream->num_samples;
            }
            break;

        case FMT_MPDX:
            /* a custom, chunked MPEG format (sigh)
             * 0x00: samples? (related to size)
             * 0x04: "2RUS" (apparently "1RUS" for mono files)
             * Rest is a MPEG-like sync but not an actual MPEG header? (DLLs do refer it as MPEG)
             * Files may have multiple "2RUS" or just a big one
             * A companion .csb has some not-too-useful info */
            goto fail;

        default:
            VGM_LOG("UBI SB: unknown codec\n");
            goto fail;
    }

    /* open the actual for decoding (streamData can be an internal or external stream) */
    if ( !vgmstream_open_stream(vgmstream, streamData, start_offset) )
        goto fail;
    return vgmstream;

fail:
    close_vgmstream(vgmstream);
    return NULL;
}

static VGMSTREAM * init_vgmstream_ubi_sb_audio(ubi_sb_header *sb, STREAMFILE *streamTest, STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    STREAMFILE *streamData = NULL;

    /* open external stream if needed */
    if (sb->is_external) {
        streamData = open_streamfile_by_filename(streamFile,sb->resource_name);
        if (streamData == NULL) {
            VGM_LOG("UBI SB: external stream '%s' not found\n", sb->resource_name);
            goto fail;
        }
    }
    else {
        streamData = streamFile;
    }


    /* init actual VGMSTREAM */
    vgmstream = init_vgmstream_ubi_sb_base(sb, streamTest, streamData, sb->stream_offset);
    if (!vgmstream) goto fail;


    if (sb->is_external && streamData) close_streamfile(streamData);
    return vgmstream;

fail:
    if (sb->is_external && streamData) close_streamfile(streamData);
    close_vgmstream(vgmstream);
    return NULL;
}

static VGMSTREAM * init_vgmstream_ubi_sb_layer(ubi_sb_header *sb, STREAMFILE *streamTest, STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    layered_layout_data* data = NULL;
    STREAMFILE* temp_streamFile = NULL;
    STREAMFILE *streamData = NULL;
    size_t full_stream_size = sb->stream_size;
    int i, total_channels = 0;

    /* open external stream if needed */
    if (sb->is_external) {
        streamData = open_streamfile_by_filename(streamFile,sb->resource_name);
        if (streamData == NULL) {
            VGM_LOG("UBI SB: external stream '%s' not found\n", sb->resource_name);
            goto fail;
        }
    }
    else {
        streamData = streamFile;
    }

    /* init layout */
    data = init_layout_layered(sb->layer_count);
    if (!data) goto fail;

    /* open all layers and mix */
    for (i = 0; i < sb->layer_count; i++) {
        /* prepare streamfile from a single layer section */
        temp_streamFile = setup_ubi_sb_streamfile(streamData, sb->stream_offset, full_stream_size, i, sb->layer_count, sb->big_endian, sb->cfg.layer_hijack);
        if (!temp_streamFile) goto fail;

        sb->stream_size = get_streamfile_size(temp_streamFile);
        sb->channels = sb->layer_channels[i];
        total_channels += sb->layer_channels[i];

        /* build the layer VGMSTREAM (standard sb with custom streamfile) */
        data->layers[i] = init_vgmstream_ubi_sb_base(sb, streamTest, temp_streamFile, 0x00);
        if (!data->layers[i]) goto fail;

        close_streamfile(temp_streamFile);
        temp_streamFile = NULL;
    }

    if (!setup_layout_layered(data))
        goto fail;


    /* build the base VGMSTREAM */
    vgmstream = allocate_vgmstream(total_channels, sb->loop_flag);
    if (!vgmstream) goto fail;

    vgmstream->meta_type = meta_UBI_SB;
    vgmstream->sample_rate = sb->sample_rate;
    vgmstream->num_streams = sb->total_subsongs;
    vgmstream->stream_size = full_stream_size;

    vgmstream->num_samples = sb->num_samples;
    vgmstream->loop_start_sample = sb->loop_start;
    vgmstream->loop_end_sample = sb->num_samples;

    vgmstream->coding_type = data->layers[0]->coding_type;
    vgmstream->layout_type = layout_layered;
    vgmstream->layout_data = data;

    if (sb->is_external && streamData) close_streamfile(streamData);

    return vgmstream;
fail:
    close_streamfile(temp_streamFile);
    if (sb->is_external && streamData) close_streamfile(streamData);
    if (vgmstream)
        close_vgmstream(vgmstream);
    else
        free_layout_layered(data);
    return NULL;
}

static VGMSTREAM * init_vgmstream_ubi_sb_sequence(ubi_sb_header *sb, STREAMFILE *streamTest, STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    segmented_layout_data* data = NULL;
    int i;
    STREAMFILE *streamBank = streamTest;


    //todo optimization: open streamData once / only if new name (doesn't change 99% of the time)

    /* init layout */
    data = init_layout_segmented(sb->sequence_count);
    if (!data) goto fail;

    sb->channels = 0;
    sb->num_samples = 0;

    /* open all segments and mix */
    for (i = 0; i < sb->sequence_count; i++) {
        ubi_sb_header temp_sb = {0};
        off_t entry_offset;
        int entry_index = sb->sequence_chain[i];


        /* bnm sequences may use to entries from other banks, do some voodoo */
        if (sb->is_bnm) {
            /* see if *current* bank has changed (may use a different bank N times) */
            if (is_bnm_other_bank(streamBank, sb->sequence_banks[i])) {
                char bank_name[255];
                sprintf(bank_name, "Bnk_%i.bnm", sb->sequence_banks[i]);

                if (streamBank != streamTest)
                    close_streamfile(streamBank);

                streamBank = open_streamfile_by_filename(streamFile, bank_name);
                if (!streamBank) goto fail;
            }

            /* re-parse the thing */
            if (!parse_bnm_header(&temp_sb, streamBank))
                goto fail;
            temp_sb.total_subsongs = 1; /* eh... just to keep parse_header happy */
        }
        else {
            temp_sb = *sb;  /* memcpy'ed */
        }

        /* parse expected entry */
        entry_offset = temp_sb.section2_offset + temp_sb.cfg.section2_entry_size * entry_index;
        if (!parse_header(&temp_sb, streamBank, entry_offset, entry_index))
            goto fail;

        if (temp_sb.type == UBI_NONE || temp_sb.type == UBI_SEQUENCE) {
            VGM_LOG("UBI SB: unexpected sequence %i entry type at %x\n", i, (uint32_t)entry_offset);
            goto fail; /* not seen, technically ok but too much recursiveness? */
        }

        /* build the layer VGMSTREAM (current sb entry config) */
        data->segments[i] = init_vgmstream_ubi_sb_header(&temp_sb, streamBank, streamFile);
        if (!data->segments[i]) goto fail;

        if (i == sb->sequence_loop)
            sb->loop_start = sb->num_samples;
        sb->num_samples += data->segments[i]->num_samples;

        /* save current (silences don't have values, so this ensures they know later, when memcpy'ed) */
        sb->channels = temp_sb.channels;
        sb->sample_rate = temp_sb.sample_rate;
    }

    if (streamBank != streamTest)
        close_streamfile(streamBank);

    if (!setup_layout_segmented(data))
        goto fail;

    /* build the base VGMSTREAM */
    vgmstream = allocate_vgmstream(data->segments[0]->channels, !sb->sequence_single);
    if (!vgmstream) goto fail;

    vgmstream->meta_type = meta_UBI_SB;
    vgmstream->sample_rate = data->segments[0]->sample_rate;
    vgmstream->num_streams = sb->total_subsongs;
    //vgmstream->stream_size = sb->stream_size; /* auto when getting avg br */

    vgmstream->num_samples = sb->num_samples;
    vgmstream->loop_start_sample = sb->loop_start;
    vgmstream->loop_end_sample = sb->num_samples;

    vgmstream->coding_type = data->segments[0]->coding_type;
    vgmstream->layout_type = layout_segmented;
    vgmstream->layout_data = data;

    return vgmstream;
fail:
    if (vgmstream)
        close_vgmstream(vgmstream);
    else
        free_layout_segmented(data);
    if (streamBank != streamTest)
        close_streamfile(streamBank);
    return NULL;
}

static size_t silence_io_read(STREAMFILE *streamfile, uint8_t *dest, off_t offset, size_t length, void* data) {
    int i;
    for (i = 0; i < length; i++) {
        dest[i] = 0;
    }
    return length; /* pretend we read zeroes */
}
static size_t silence_io_size(STREAMFILE *streamfile, void* data) {
    return 0x7FFFFFF; /* whatevs */
}
static STREAMFILE* setup_silence_streamfile(STREAMFILE *streamFile) {
    STREAMFILE *temp_streamFile = NULL, *new_streamFile = NULL;

    /* setup custom streamfile */
    new_streamFile = open_wrap_streamfile(streamFile);
    if (!new_streamFile) goto fail;
    temp_streamFile = new_streamFile;

    new_streamFile = open_io_streamfile(temp_streamFile, NULL,0, silence_io_read,silence_io_size);
    if (!new_streamFile) goto fail;
    temp_streamFile = new_streamFile;

    return temp_streamFile;

fail:
    close_streamfile(temp_streamFile);
    return NULL;
}

static VGMSTREAM * init_vgmstream_ubi_sb_silence(ubi_sb_header *sb, STREAMFILE *streamTest, STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;
    STREAMFILE *temp_streamFile = NULL;
    int channel_count, sample_rate;

    channel_count = sb->channels;
    sample_rate = sb->sample_rate;

    /* by default silences don't have settings so let's pretend */
    if (channel_count == 0)
        channel_count = 2;
    if (sample_rate == 0)
        sample_rate = 48000;


    /* build the VGMSTREAM */
    vgmstream = allocate_vgmstream(channel_count, 0);
    if (!vgmstream) goto fail;

    vgmstream->meta_type = meta_UBI_SB;
    vgmstream->sample_rate = sample_rate;

    vgmstream->num_samples = (int32_t)(sb->duration * (float)sample_rate);
    vgmstream->num_streams = sb->total_subsongs;
    vgmstream->stream_size = vgmstream->num_samples * channel_count * 0x02; /* PCM size */

    vgmstream->coding_type = coding_PCM16LE;
    vgmstream->layout_type = layout_interleave;
    vgmstream->interleave_block_size = 0x02;

    temp_streamFile = setup_silence_streamfile(streamFile);
    if ( !vgmstream_open_stream(vgmstream, temp_streamFile, 0x00) )
        goto fail;
    close_streamfile(temp_streamFile);
    return vgmstream;

fail:
    close_vgmstream(vgmstream);
    close_streamfile(temp_streamFile);
    return vgmstream;
}


static VGMSTREAM * init_vgmstream_ubi_sb_header(ubi_sb_header *sb, STREAMFILE* streamTest, STREAMFILE *streamFile) {
    VGMSTREAM * vgmstream = NULL;

    if (sb->total_subsongs == 0) {
        VGM_LOG("UBI SB: no subsongs\n");
        goto fail;
    }

    ;VGM_LOG("UBI SB: target at %x + %x, extra=%x, name=%s, g=%i, t=%i\n",
        (uint32_t)sb->header_offset, sb->cfg.section2_entry_size, (uint32_t)sb->extra_offset, sb->resource_name, sb->group_id, sb->stream_type);
    ;VGM_LOG("UBI SB: stream offset=%x, size=%x, name=%s\n", (uint32_t)sb->stream_offset, sb->stream_size, sb->is_external ? sb->resource_name : "internal" );

    switch(sb->type) {
        case UBI_AUDIO:
            vgmstream = init_vgmstream_ubi_sb_audio(sb, streamTest, streamFile);
            break;

        case UBI_LAYER:
            vgmstream = init_vgmstream_ubi_sb_layer(sb, streamTest, streamFile);
            break;

        case UBI_SEQUENCE:
            vgmstream = init_vgmstream_ubi_sb_sequence(sb, streamTest, streamFile);
            break;

        case UBI_SILENCE:
            vgmstream = init_vgmstream_ubi_sb_silence(sb, streamTest, streamFile);
            break;

        case UBI_NONE:
        default:
            VGM_LOG("UBI SB: subsong not found/parsed\n");
            goto fail;
    }

    if (!vgmstream) goto fail;

    strcpy(vgmstream->stream_name, sb->readable_name);
    return vgmstream;
fail:
    close_vgmstream(vgmstream);
    return NULL;
}

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

static void build_readable_name(char * buf, size_t buf_size, ubi_sb_header * sb) {
    const char *grp_name;
    const char *res_name;
    uint32_t id;
    uint32_t type;
    int index;

    /* config */
    if (sb->is_map) {
        grp_name = sb->map_name;
    }
    else if (sb->is_bnm) {
        if (sb->sequence_multibank)
            grp_name = "bnm-multi";
        else
            grp_name = "bnm";
    }
    else {
        grp_name = "bank";
    }
    id = sb->header_id;
    type = sb->header_type;
    if (sb->is_map)
        index = sb->header_index; //sb->bank_subsongs;
    else
        index = sb->header_index; //-1

    if (sb->type == UBI_SEQUENCE) {
        if (sb->sequence_single) {
            if (sb->sequence_count == 1)
                res_name = "single";
            else
                res_name = "multi";
        }
        else {
            if (sb->sequence_count == 1)
                res_name = "single-loop";
            else
                res_name = (sb->sequence_loop == 0) ? "multi-loop" : "intro-loop";
        }
    }
    else {
        if (sb->is_external || sb->cfg.audio_has_internal_names)
            res_name = sb->resource_name;
        else
            res_name = NULL;
    }

    /* maps can contain +10000 subsongs, we need something helpful
     * (best done right after subsong detection, since some sequence re-parse types) */
    if (res_name && res_name[0]) {
        if (index >= 0)
            snprintf(buf,buf_size, "%s/%04d/%02x-%08x/%s", grp_name, index, type, id, res_name);
        else
            snprintf(buf,buf_size, "%s/%02x-%08x/%s", grp_name, type, id, res_name);
    }
    else {
        if (index >= 0)
            snprintf(buf,buf_size, "%s/%04d/%02x-%08x", grp_name, index, type, id);
        else
            snprintf(buf,buf_size, "%s/%02x-%08x", grp_name, type, id);
    }
}

static int parse_type_audio(ubi_sb_header * sb, off_t offset, STREAMFILE* streamFile) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;
    int16_t (*read_16bit)(off_t,STREAMFILE*) = sb->big_endian ? read_16bitBE : read_16bitLE;

    /* audio header */
    sb->type = UBI_AUDIO;

    sb->extra_offset    = read_32bit(offset + sb->cfg.audio_extra_offset, streamFile) + sb->sectionX_offset;
    sb->stream_size     = read_32bit(offset + sb->cfg.audio_stream_size, streamFile);
    sb->stream_offset   = read_32bit(offset + sb->cfg.audio_stream_offset, streamFile);
    sb->channels        = (sb->cfg.audio_channels % 4) ? /* non-aligned offset is always 16b */
                (uint16_t)read_16bit(offset + sb->cfg.audio_channels, streamFile) :
                (uint32_t)read_32bit(offset + sb->cfg.audio_channels, streamFile);
    sb->sample_rate     = read_32bit(offset + sb->cfg.audio_sample_rate, streamFile);
    sb->stream_type     = read_32bit(offset + sb->cfg.audio_stream_type, streamFile);

    if (sb->stream_size == 0) {
        VGM_LOG("UBI SB: bad stream size\n");
        goto fail;
    }

    if (sb->cfg.audio_external_flag && sb->cfg.audio_external_and) {
        sb->is_external = (read_32bit(offset + sb->cfg.audio_external_flag, streamFile) & sb->cfg.audio_external_and);
    }

    if (sb->cfg.audio_group_id && sb->cfg.audio_group_and) {
        /* probably means "SW decoded" */
        sb->group_id = read_32bit(offset + sb->cfg.audio_group_id, streamFile);
        if (sb->cfg.audio_group_and) sb->group_id &= sb->cfg.audio_group_and;

        /* normalize bitflag, known groups are only id 0/1 (if needed could calculate
         * shift-right value here, based on cfg.audio_group_and first 1-bit) */
        if (sb->group_id > 1)
            sb->group_id = 1;
    } else {
        /* old group flag (HW decoded?) */
        sb->group_id = (int)!(sb->stream_type & 0x01);
    }

    if (sb->cfg.audio_loop_flag && sb->cfg.audio_loop_and) {
        sb->loop_flag = (read_32bit(offset + sb->cfg.audio_loop_flag, streamFile) & sb->cfg.audio_loop_and);
    }

    if (sb->loop_flag) {
        sb->loop_start  = read_32bit(offset + sb->cfg.audio_num_samples, streamFile);
        sb->num_samples = read_32bit(offset + sb->cfg.audio_num_samples2, streamFile) + sb->loop_start;

        if (sb->cfg.audio_num_samples == sb->cfg.audio_num_samples2) { /* early games just repeat and don't set loop start */
            sb->num_samples = sb->loop_start;
            sb->loop_start = 0;
        }
        /* Loop starts that aren't 0 do exist but are very rare (ex. Splinter Cell PC, Beowulf PSP sb 82, index 575).
         * Also rare are looping external streams, since it's normally done through sequences (ex. Surf's Up).
         * Loop end may be +1? (ex. Splinter Cell: Double Agent PS3 #14331). */
    } else {
        sb->num_samples = read_32bit(offset + sb->cfg.audio_num_samples, streamFile);
    }

    if (sb->cfg.resource_name_size > sizeof(sb->resource_name)) {
        goto fail;
    }

    /* external stream name can be found in the header (first versions) or the sectionX table (later versions) */
    if (sb->cfg.audio_stream_name) {
        read_string(sb->resource_name, sb->cfg.resource_name_size, offset + sb->cfg.audio_stream_name, streamFile);
    }
    else {
        sb->cfg.audio_stream_name = read_32bit(offset + sb->cfg.audio_extra_name, streamFile);
        if (sb->cfg.layer_stream_name != 0xFFFFFFFF)
            read_string(sb->resource_name, sb->cfg.resource_name_size, sb->sectionX_offset + sb->cfg.audio_stream_name, streamFile);
    }

    /* points at XMA1 header in the extra section (only for RAW_XMA1, ignored garbage otherwise) */
    if (sb->cfg.audio_xma_offset) {
        sb->xma_header_offset = read_32bit(offset + sb->cfg.audio_xma_offset, streamFile) + sb->sectionX_offset;
    }

    return 1;
fail:
    return 0;
}

static int parse_type_sequence(ubi_sb_header * sb, off_t offset, STREAMFILE* streamFile) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;
    off_t table_offset;
    int i;

    /* sequence chain */
    sb->type = UBI_SEQUENCE;
    if (sb->cfg.sequence_entry_size == 0) {
        VGM_LOG("Ubi SB: sequence entry size not configured at %x\n", (uint32_t)offset);
        goto fail;
    }

    sb->extra_offset    = read_32bit(offset + sb->cfg.sequence_extra_offset, streamFile) + sb->sectionX_offset;
    sb->sequence_loop   = read_32bit(offset + sb->cfg.sequence_sequence_loop, streamFile);
    sb->sequence_single = read_32bit(offset + sb->cfg.sequence_sequence_single, streamFile);
    sb->sequence_count  = read_32bit(offset + sb->cfg.sequence_sequence_count, streamFile);

    if (sb->sequence_count > SB_MAX_CHAIN_COUNT) {
        VGM_LOG("Ubi SB: incorrect sequence count %i vs %i\n", sb->sequence_count, SB_MAX_CHAIN_COUNT);
        goto fail;
    }

    /* get chain in extra table */
    table_offset = sb->extra_offset;
    for (i = 0; i < sb->sequence_count; i++) {
        uint32_t entry_number = (uint32_t)read_32bit(table_offset+sb->cfg.sequence_entry_number, streamFile);

        /* bnm sequences may refer to entries from different banks, whee */
        if (sb->is_bnm) {
            int16_t bank_number = (entry_number >> 16) & 0xFFFF;
            entry_number        = (entry_number >> 00) & 0xFFFF;

            //;VGM_LOG("UBI SB: bnm sequence entry=%i, bank=%i\n", entry_number, bank_number);
            sb->sequence_banks[i] = bank_number;

            /* info flag, does bank number point to another file? */
            if (!sb->sequence_multibank) {
                sb->sequence_multibank = is_bnm_other_bank(streamFile, bank_number);
            }
        }
        else {
            entry_number = entry_number & 0x3FFFFFFF;
            if (entry_number > sb->section2_num) {
                VGM_LOG("UBI SB: chain with wrong entry %i vs %i at %x\n", entry_number, sb->section2_num, (uint32_t)sb->extra_offset);
                goto fail;
            }
        }

        /* some sequences have an upper bit (2 bits in Donald Duck voices) for some reason */
        //;VGM_ASSERT_ONCE(entry_number & 0xC0000000, "UBI SB: sequence bit entry found at %x\n", (uint32_t)sb->extra_offset);

        sb->sequence_chain[i] = entry_number;

        table_offset += sb->cfg.sequence_entry_size;
    }

    return 1;
fail:
    return 0;
}

static int parse_type_layer(ubi_sb_header * sb, off_t offset, STREAMFILE* streamFile) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;
    int16_t (*read_16bit)(off_t,STREAMFILE*) = sb->big_endian ? read_16bitBE : read_16bitLE;
    off_t table_offset;
    int i;

    /* layer header */
    sb->type = UBI_LAYER;
    if (sb->cfg.layer_entry_size == 0) {
        VGM_LOG("Ubi SB: layer entry size not configured at %x\n", (uint32_t)offset);
        goto fail;
    }

    sb->extra_offset    = read_32bit(offset + sb->cfg.layer_extra_offset, streamFile) + sb->sectionX_offset;
    sb->layer_count     = read_32bit(offset + sb->cfg.layer_layer_count, streamFile);
    sb->stream_size     = read_32bit(offset + sb->cfg.layer_stream_size, streamFile);
    sb->stream_offset   = read_32bit(offset + sb->cfg.layer_stream_offset, streamFile);

    if (sb->stream_size == 0) {
        VGM_LOG("UBI SB: bad stream size\n");
        goto fail;
    }

    if (sb->layer_count > SB_MAX_LAYER_COUNT) {
        VGM_LOG("Ubi SB: incorrect layer count\n");
        goto fail;
    }

    /* get 1st layer header in extra table and validate all headers match */
    table_offset = sb->extra_offset;
  //sb->channels        = (sb->cfg.layer_channels % 4) ? /* non-aligned offset is always 16b */
  //            (uint16_t)read_16bit(table_offset + sb->cfg.layer_channels, streamFile) :
  //            (uint32_t)read_32bit(table_offset + sb->cfg.layer_channels, streamFile);
    sb->sample_rate     = read_32bit(table_offset + sb->cfg.layer_sample_rate, streamFile);
    sb->stream_type     = read_32bit(table_offset + sb->cfg.layer_stream_type, streamFile);
    sb->num_samples     = read_32bit(table_offset + sb->cfg.layer_num_samples, streamFile);

    for (i = 0; i < sb->layer_count; i++) {
        int channels    = (sb->cfg.layer_channels % 4) ? /* non-aligned offset is always 16b */
                (uint16_t)read_16bit(table_offset + sb->cfg.layer_channels, streamFile) :
                (uint32_t)read_32bit(table_offset + sb->cfg.layer_channels, streamFile);
        int sample_rate = read_32bit(table_offset + sb->cfg.layer_sample_rate, streamFile);
        int stream_type = read_32bit(table_offset + sb->cfg.layer_stream_type, streamFile);
        int num_samples = read_32bit(table_offset + sb->cfg.layer_num_samples, streamFile);

        if (sb->sample_rate != sample_rate || sb->stream_type != stream_type) {
            VGM_LOG("Ubi SB: %i layer headers don't match at %x > %x\n", sb->layer_count, (uint32_t)offset, (uint32_t)table_offset);
            if (!sb->cfg.ignore_layer_error) /* layers of different rates happens sometimes */
                goto fail;
        }

        /* uncommonly channels may vary per layer [Brothers in Arms 2 (PS2) ex. MP_B01_NL.SB1] */
        sb->layer_channels[i] = channels;

        /* can be +-1 */
        if (sb->num_samples != num_samples && sb->num_samples + 1 == num_samples) {
            sb->num_samples -= 1;
        }

        table_offset += sb->cfg.layer_entry_size;
    }

    /* all layers seem external */
    sb->is_external = 1;

    /* external stream name can be found in the header (first versions) or the sectionX table (later versions) */
    if (sb->cfg.layer_stream_name) {
        read_string(sb->resource_name, sb->cfg.resource_name_size, offset + sb->cfg.layer_stream_name, streamFile);
    } else {
        sb->cfg.layer_stream_name = read_32bit(offset + sb->cfg.layer_extra_name, streamFile);
        if (sb->cfg.layer_stream_name != 0xFFFFFFFF)
            read_string(sb->resource_name, sb->cfg.resource_name_size, sb->sectionX_offset + sb->cfg.layer_stream_name, streamFile);
    }

    /* layers seem to include XMA header */

    return 1;
fail:
    return 0;
}

static int parse_type_silence(ubi_sb_header * sb, off_t offset, STREAMFILE* streamFile) {
    float (*read_f32)(off_t,STREAMFILE*) = sb->big_endian ? read_f32be : read_f32le;
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;

    /* silence header */
    sb->type = UBI_SILENCE;
    if (sb->cfg.silence_duration_int == 0 && sb->cfg.silence_duration_float == 0) {
        VGM_LOG("Ubi SB: silence duration not configured at %x\n", (uint32_t)offset);
        goto fail;
    }

    if (sb->cfg.silence_duration_int) {
        uint32_t duration_int = (uint32_t)read_32bit(offset + sb->cfg.silence_duration_int, streamFile);
        sb->duration = (float)duration_int / 65536.0f; /* 65536.0 is common so probably means 1.0 */
    }
    else if (sb->cfg.silence_duration_float) {
        sb->duration = read_f32(offset + sb->cfg.silence_duration_float, streamFile);
    }

    return 1;
fail:
    return 0;
}

// todo improve, only used in bnm sequences as sequence end (and may point to another bnm)
static int parse_type_random(ubi_sb_header * sb, off_t offset, STREAMFILE* streamFile) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;

    off_t sb_extra_offset, table_offset;
    int i, sb_sequence_count;

    /* sequence chain */
    if (sb->cfg.random_entry_size == 0) {
        VGM_LOG("Ubi SB: random entry size not configured at %x\n", (uint32_t)offset);
        goto fail;
    }

    sb_extra_offset    = read_32bit(offset + sb->cfg.random_extra_offset, streamFile) + sb->sectionX_offset;
    sb_sequence_count  = read_32bit(offset + sb->cfg.random_sequence_count, streamFile);


    /* get chain in extra table */
    table_offset = sb_extra_offset;
    for (i = 0; i < sb_sequence_count; i++) {
        uint32_t entry_number = (uint32_t)read_32bit(table_offset+0x00, streamFile);
        //uint32_t entry_chance = (uint32_t)read_32bit(table_offset+0x04, streamFile);

        if (sb->is_bnm) {
            int16_t bank_number = (entry_number >> 16) & 0xFFFF;
            entry_number        = (entry_number >> 00) & 0xFFFF;

            ;VGM_LOG("UBI SB: bnm sequence entry=%i, bank=%i\n", entry_number, bank_number);
            //sb->sequence_banks[i] = bank_number;

            /* not seen */
            if (is_bnm_other_bank(streamFile, bank_number)) {
                VGM_LOG("UBI SB: random in other bank\n");
                goto fail;
            }
        }

        //todo make rand or stuff (old chance: int from 0 to 0x10000, new: float from 0.0 to 1.0)
        { //if (entry_chance == ...)
            off_t entry_offset = sb->section2_offset + sb->cfg.section2_entry_size * entry_number;
            return parse_type_audio(sb, entry_offset, streamFile);
        }

        table_offset += sb->cfg.random_entry_size;
    }

    return 1;
fail:
    return 0;
}


/* find actual codec from type (as different games' stream_type can overlap) */
static int parse_stream_codec(ubi_sb_header * sb) {

    if (sb->type == UBI_SEQUENCE)
        return 1;

    /* in early versions, this is a bitfield with either 1 or 2 rightmost bits being flags */
    sb->stream_type >>= sb->cfg.num_codec_flags;

    if (sb->cfg.default_codec_for_group0 && sb->type == UBI_AUDIO && sb->group_id == 0) {
        /* early Xbox games contain garbage in stream_type field in this case, it seems that 0x00 is assumed */
        sb->stream_type = 0x00;
    }

    /* guess codec */
    if (sb->stream_type == 0x00) {
        switch (sb->platform) {
            case UBI_PC:
                sb->codec = RAW_PCM;
                break;
            case UBI_PS2:
                sb->codec = RAW_PSX;
                break;
            case UBI_PSP:
                if (sb->is_psp_old)
                    sb->codec = FMT_VAG;
                else
                    sb->codec = RAW_PSX;
                break;
            case UBI_XBOX:
                sb->codec = RAW_XBOX;
                break;
            case UBI_GC:
            case UBI_WII:
                sb->codec = RAW_DSP;
                break;
            case UBI_X360:
                sb->codec = RAW_XMA1;
                break;
#if 0
            case UBI_PS3: /* assumed, but no games seem to use it */
                sb->codec = RAW_AT3;
                break;
#endif
            case UBI_3DS:
                sb->codec = FMT_CWAV;
                break;
            default:
                VGM_LOG("UBI SB: unknown internal format\n");
                goto fail;
        }
    } else if (sb->version == 0x00000000) {
        /* really old version predating SBx and SMx formats */
        /* Rayman 2: The Great Escape */
        /* Tonic Trouble */
        /* Donald Duck: Goin' Quackers */
        /* Disney's Dinosaur */

        switch (sb->stream_type) {
            case 0x01:
                sb->codec = FMT_MPDX;
                break;
                
            case 0x02:
                sb->codec = FMT_APM;
                break;

            default:
                VGM_LOG("Unknown stream_type %02x for version %08x\n", sb->stream_type, sb->version);
                goto fail;
        }
    } else if (sb->version < 0x000A0000) {
        switch (sb->stream_type) {
            case 0x01:
                sb->codec = UBI_ADPCM;
                break;

            case 0x02:
                sb->codec = UBI_IMA; /* Ubi IMA v2/v3 */
                break;

            default:
                VGM_LOG("Unknown stream_type %02x for version %08x\n", sb->stream_type, sb->version);
                goto fail;
        }
    } else { 
        switch (sb->stream_type) {
            case 0x01:
                sb->codec = RAW_PCM; /* uncommon, ex. Wii/PSP/3DS */
                break;

            case 0x02:
                switch (sb->platform) {
                    case UBI_PS3:
                        sb->codec = RAW_PSX; /* PS3 */
                        break;
                    case UBI_PSP:
                        /* TODO: IMA using Ubisoft ADPCM frame layout [Splinter Cell: Essentials (PSP)] */
                        VGM_LOG("Unimplemented custom IMA codec.\n");
                        goto fail;
                    default:
                        sb->codec = UBI_ADPCM;
                        break;
                }
                break;

            case 0x03:
                sb->codec = UBI_IMA; /* Ubi IMA v3+ (versions handled in decoder) */
                break;

            case 0x04:
                sb->codec = FMT_OGG; /* later PC games */
                break;

            case 0x05:
                switch (sb->platform) {
                    case UBI_X360:
                        sb->codec = FMT_XMA1;
                        break;
                    case UBI_PS3:
                    case UBI_PSP:
                        sb->codec = FMT_AT3;
                        break;
                    default:
                        VGM_LOG("UBI SB: unknown codec for stream_type %x\n", sb->stream_type);
                        goto fail;
                }
                break;

            case 0x06:
                sb->codec = RAW_PSX; /* later PSP and PS3(?) games */
                break;

            case 0x07:
                sb->codec = RAW_AT3; /* PS3 */
                break;

            case 0x08:
                sb->codec = FMT_AT3;
                break;

            default:
                VGM_LOG("Unknown stream_type %02x\n", sb->stream_type);
                goto fail;
        }
    }

    return 1;
fail:
    return 0;
}

/* find actual stream offset in section3 */
static int parse_offsets(ubi_sb_header * sb, STREAMFILE *streamFile) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;
    int i, j, k;

    if (sb->type == UBI_SEQUENCE)
        return 1;

    VGM_ASSERT(!sb->is_map && sb->section3_num > 2, "UBI SB: section3 > 2 found\n");

    if (sb->is_external)
        return 1;

    /* Internal sounds are split into codec groups, with their offsets being relative to group start.
     * A table contains sizes of each group, so we adjust offsets based on the group ID of our sound.
     * Headers normally only use 0 or 1, and section3 may only define id1 (which the internal sound would use).
     * May exist even for external streams only, and they often use id 1 too. */

    if (sb->is_map) {
        /* maps store internal sounds offsets in a separate subtable, find the matching entry
         * each sec3 entry consists of the header and two tables
         * 0x00: some ID? (always -1 for the first entry)
         * 0x04: table 1 offset
         * 0x08: table 1 entries
         * 0x0c: table 2 offset
         * 0x10: table 2 entries
         * table 1 - for each entry:
         * 0x00: sec2 entry index
         * 0x04: sound offset
         * table 2 - for each entry:
         * 0x00 - group ID
         * 0x04 - size with padding included
         * 0x08 - size without padding
         * 0x0c - absolute group offset */

        for (i = 0; i < sb->section3_num; i++) {
            off_t offset = sb->section3_offset + 0x14 * i;
            off_t table_offset  = read_32bit(offset + 0x04, streamFile) + sb->section3_offset;
            uint32_t table_num  = read_32bit(offset + 0x08, streamFile);
            off_t table2_offset = read_32bit(offset + 0x0c, streamFile) + sb->section3_offset;
            uint32_t table2_num = read_32bit(offset + 0x10, streamFile);

            for (j = 0; j < table_num; j++) {
                int index = read_32bit(table_offset + 0x08 * j + 0x00, streamFile) & 0x0000FFFF;

                if (index == sb->header_index) {
                    sb->stream_offset = read_32bit(table_offset + 0x08 * j + 0x04, streamFile);
                    for (k = 0; k < table2_num; k++) {
                        uint32_t id = read_32bit(table2_offset + 0x10 * k + 0x00, streamFile);

                        if (id == sb->group_id) {
                            sb->stream_offset += read_32bit(table2_offset + 0x10 * k + 0x0c, streamFile);
                            break;
                        }
                    }
                    break;
                }
            }

            if (sb->stream_offset)
                break;
        }
    }
    else {
        /* banks store internal sounds after all headers and adjusted by the group table, find the matching entry */

        off_t sounds_offset = sb->section3_offset + sb->cfg.section3_entry_size*sb->section3_num;
        if (sb->cfg.is_padded_sounds_offset)
            sounds_offset = align_size_to_block(sounds_offset, 0x10);
        sb->stream_offset = sounds_offset + sb->stream_offset;

        if (sb->section3_num > 1) { /* maybe should always test this? */
            for (i = 0; i < sb->section3_num; i++) {
                off_t offset = sb->section3_offset + sb->cfg.section3_entry_size * i;

                /* table has unordered ids+size, so if our id doesn't match current data offset must be beyond */
                if (read_32bit(offset + 0x00, streamFile) == sb->group_id)
                    break;
                sb->stream_offset += read_32bit(offset + 0x04, streamFile);
            }
        }
    }

    return 1;
//fail:
//    return 0;
}

/* parse a single known header resource at offset (see config_sb for info) */
static int parse_header(ubi_sb_header * sb, STREAMFILE *streamFile, off_t offset, int index) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;

    sb->header_index    = index;
    sb->header_offset   = offset;

    sb->header_id       = read_32bit(offset + 0x00, streamFile);
    sb->header_type     = read_32bit(offset + 0x04, streamFile);

    switch(sb->header_type) {
        case 0x01:
            if (!parse_type_audio(sb, offset, streamFile))
                goto fail;
            break;
        case 0x05:
        case 0x0b:
        case 0x0c:
            if (!parse_type_sequence(sb, offset, streamFile))
                goto fail;
            break;
        case 0x06:
        case 0x0d:
            if (!parse_type_layer(sb, offset, streamFile))
                goto fail;
            break;
        case 0x08:
        case 0x0f:
            if (!parse_type_silence(sb, offset, streamFile))
                goto fail;
            break;
        case 0x0a:
            if (!parse_type_random(sb, offset, streamFile))
                goto fail;
            break;
        default:
            VGM_LOG("UBI SB: unknown header type %x at %x\n", sb->header_type, (uint32_t)offset);
            goto fail;
    }

    if (!parse_stream_codec(sb))
        goto fail;

    if (!parse_offsets(sb, streamFile))
        goto fail;

    return 1;
fail:
    return 0;
}

/* parse a bank and its possible audio headers */
static int parse_sb(ubi_sb_header * sb, STREAMFILE *streamFile, int target_subsong) {
    int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;
    int i;

    //;VGM_LOG("UBI SB: s1=%x (%x*%x), s2=%x (%x*%x), sX=%x (%x), s3=%x (%x*%x)\n",
    //        sb->section1_offset,sb->cfg.section1_entry_size,sb->section1_num,sb->section2_offset,sb->cfg.section2_entry_size,sb->section2_num,
    //        sb->sectionX_offset,sb->sectionX_size,sb->section3_offset,sb->cfg.section3_entry_size,sb->section3_num);

    /* find target subsong info in section2 and keeps counting */
    sb->bank_subsongs = 0;
    for (i = 0; i < sb->section2_num; i++) {
        off_t offset = sb->section2_offset + sb->cfg.section2_entry_size*i;
        uint32_t header_type;

      /*header_id =*/ read_32bit(offset + 0x00, streamFile); /* forces buffer read */
        header_type = read_32bit(offset + 0x04, streamFile);

        if (header_type <= 0x00 || header_type >= 0x10) {
            VGM_LOG("UBI SB: unknown type %x at %x\n", header_type, (uint32_t)offset);
            goto fail;
        }

        sb->types[header_type]++;
        if (!sb->allowed_types[header_type])
            continue;

        sb->bank_subsongs++;
        sb->total_subsongs++;
        if (sb->total_subsongs != target_subsong)
            continue;

        if (!parse_header(sb, streamFile, offset, i))
            goto fail;

        build_readable_name(sb->readable_name, sizeof(sb->readable_name), sb);
    }

    /* either found target subsong or it's in another bank (in case of maps), both handled externally */

    //;VGM_LOG("UBI SB: types "); {int i; for (i=0;i<16;i++){ VGM_ASSERT(sb->types[i],"%02x=%i ",i,sb->types[i]); }} VGM_LOG("\n");

    return 1;
fail:
    return 0;
}

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

static int config_sb_platform(ubi_sb_header * sb, STREAMFILE *streamFile) {
    char filename[PATH_LIMIT];
    int filename_len;
    char platform_char;
    uint32_t version;

    /* to find out hijacking (LE) platforms */
    version = read_32bitLE(0x00, streamFile);

    /* get X from .sbX/smX/lmX */
    get_streamfile_name(streamFile,filename,sizeof(filename));
    filename_len = strlen(filename);
    platform_char = filename[filename_len - 1];

    switch(platform_char) {
        case '0':
            sb->platform = UBI_PC;
            break;
        case '1':
            sb->platform = UBI_PS2;
            break;
        case '2':
            sb->platform = UBI_XBOX;
            break;
        case '3':
            sb->platform = UBI_GC;
            break;
        case '4':
            switch(version) { /* early PSP clashes with X360 */
                case 0x0012000C: /* multiple games use this ID and all are sb4/sm4 */
                    sb->platform = UBI_PSP;
                    sb->is_psp_old = 1;
                    break;
                default:
                    sb->platform = UBI_X360;
                    break;
            }
            break;
        case '5':
            switch(version) { /* 3DS could be sb8/sm8 but somehow hijacks extension */
                case 0x00130001: /* Splinter Cell 3DS (2011) */
                    sb->platform = UBI_3DS;
                    break;
                default:
                    sb->platform = UBI_PSP;
                    break;
            }
            break;
        case '6':
            sb->platform = UBI_PS3;
            break;
        case '7':
            sb->platform = UBI_WII;
            break;
        default:
            goto fail;
    }

    sb->big_endian =
            sb->platform == UBI_GC ||
            sb->platform == UBI_PS3 ||
            sb->platform == UBI_X360 ||
            sb->platform == UBI_WII;

    return 1;
fail:
    return 0;
}


static void config_sb_entry(ubi_sb_header * sb, size_t section1_size_entry, size_t section2_size_entry) {
    sb->cfg.section1_entry_size     = section1_size_entry;
    sb->cfg.section2_entry_size     = section2_size_entry;
    sb->cfg.section3_entry_size     = 0x08;
}
static void config_sb_audio_fs(ubi_sb_header * sb, off_t external_flag, off_t group_id, off_t loop_flag) {
    /* audio header with standard flags */
    sb->cfg.audio_external_flag     = external_flag;
    sb->cfg.audio_group_id          = group_id;
    sb->cfg.audio_loop_flag         = loop_flag;
    sb->cfg.audio_external_and      = 1;
    sb->cfg.audio_group_and         = 1;
    sb->cfg.audio_loop_and          = 1;
}
static void config_sb_audio_fb(ubi_sb_header * sb, off_t flag_bits, int external_and, int group_and, int loop_and) {
    /* audio header with bit flags */
    sb->cfg.audio_external_flag     = flag_bits;
    sb->cfg.audio_group_id          = flag_bits;
    sb->cfg.audio_loop_flag         = flag_bits;
    sb->cfg.audio_external_and      = external_and;
    sb->cfg.audio_group_and         = group_and;
    sb->cfg.audio_loop_and          = loop_and;
}
static void config_sb_audio_hs(ubi_sb_header * sb, off_t channels, off_t sample_rate, off_t num_samples, off_t num_samples2, off_t stream_name, off_t stream_type) {
    /* audio header with stream name */
    sb->cfg.audio_channels          = channels;
    sb->cfg.audio_sample_rate       = sample_rate;
    sb->cfg.audio_num_samples       = num_samples;
    sb->cfg.audio_num_samples2      = num_samples2;
    sb->cfg.audio_stream_name       = stream_name;
    sb->cfg.audio_stream_type       = stream_type;
}
static void config_sb_audio_he(ubi_sb_header * sb, off_t channels, off_t sample_rate, off_t num_samples, off_t num_samples2, off_t extra_name, off_t stream_type) {
    /* audio header with extra name */
    sb->cfg.audio_channels          = channels;
    sb->cfg.audio_sample_rate       = sample_rate;
    sb->cfg.audio_num_samples       = num_samples;
    sb->cfg.audio_num_samples2      = num_samples2;
    sb->cfg.audio_extra_name        = extra_name;
    sb->cfg.audio_stream_type       = stream_type;
}
static void config_sb_sequence(ubi_sb_header * sb, off_t sequence_count, off_t entry_size) {
    /* sequence header and chain table */
    sb->cfg.sequence_sequence_loop  = sequence_count - 0x10;
    sb->cfg.sequence_sequence_single= sequence_count - 0x0c;
    sb->cfg.sequence_sequence_count = sequence_count;
    sb->cfg.sequence_entry_size     = entry_size;
    sb->cfg.sequence_entry_number   = 0x00;
    if (sb->is_bnm) {
        sb->cfg.sequence_sequence_loop  = sequence_count - 0x0c;
        sb->cfg.sequence_sequence_single= sequence_count - 0x08;
    }
}
static void config_sb_layer_hs(ubi_sb_header * sb, off_t layer_count, off_t stream_size, off_t stream_offset, off_t stream_name) {
    /* layer headers with stream name */
    sb->cfg.layer_layer_count       = layer_count;
    sb->cfg.layer_stream_size       = stream_size;
    sb->cfg.layer_stream_offset     = stream_offset;
    sb->cfg.layer_stream_name       = stream_name;
}
static void config_sb_layer_he(ubi_sb_header * sb, off_t layer_count, off_t stream_size, off_t stream_offset, off_t extra_name) {
    /* layer headers with extra name */
    sb->cfg.layer_layer_count       = layer_count;
    sb->cfg.layer_stream_size       = stream_size;
    sb->cfg.layer_stream_offset     = stream_offset;
    sb->cfg.layer_extra_name        = extra_name;
}
static void config_sb_layer_sh(ubi_sb_header * sb, off_t entry_size, off_t sample_rate, off_t channels, off_t stream_type, off_t num_samples) {
    /* layer sub-headers in extra table */
    sb->cfg.layer_entry_size        = entry_size;
    sb->cfg.layer_sample_rate       = sample_rate;
    sb->cfg.layer_channels          = channels;
    sb->cfg.layer_stream_type       = stream_type;
    sb->cfg.layer_num_samples       = num_samples;
}
static void config_sb_silence_i(ubi_sb_header * sb, off_t duration) {
    /* silence headers in int value */
    sb->cfg.silence_duration_int    = duration;
}
static void config_sb_silence_f(ubi_sb_header * sb, off_t duration) {
    /* silence headers in float value */
    sb->cfg.silence_duration_float  = duration;
}

static void config_sb_random_old(ubi_sb_header * sb, off_t sequence_count, off_t entry_size) {
    sb->cfg.random_sequence_count = sequence_count;
    sb->cfg.random_entry_size = entry_size;
    sb->cfg.random_percent_int = 1;
}

static int config_sb_version(ubi_sb_header * sb, STREAMFILE *streamFile) {
    int is_dino_pc = 0;
    int is_ttse_pc = 0;
    int is_bia_ps2 = 0, is_biadd_psp = 0;
    int is_sc2_ps2_gc = 0;
    int is_sc4_pc_online = 0;

    /* Most of the format varies with almost every game + platform (struct serialization?).
     * Support is configured case-by-case as offsets/order/fields only change slightly,
     * and later games may remove fields. We only configure those actually needed. 
     *
     * Various type use "chains" of entry numbers (defined in the extra table).
     * Its format also depends on type.
     */

    /* Header types found in section2 (possibly called "resource headers"):
     * 
     * Type 01 (old/new):
     * Single audio header, external or internal, part of a chain or single. Format:
     * - fixed part (id, type, stream size, extra offset, stream offset)
     * - flags (as bitflags or in separate fields, around ~6 observed flags)
     * - samples+samples (loop+total) + size+size (roughly equal to stream size)
     * - bitrate (total sample rate)
     * - base info (sample rate, pcm bits?, channels, codec)
     * - external filename or internal filename on some platforms (earlier versions)
     * - external filename offset in the extra table (later versions)
     * - end flags?
     * A few games (Splinter Cell, Rainbow Six) have wrong voice sample rates,
     * maybe there is some pitch value too.
     *
     * Type 02 (old?/new):
     * Chain, possibly to play with config (ex: type 08 (float 0.3) + 01)
     *
     * Type 03 (new), 09? (old): 
     * Chain, other way to play things? (ex: type 03 + 04)
     *
     * Type 04 (old?/new), 0a (old):
     * Table of N types + chance % (sums to 65536), to play one as random. Usually N
     * voice/sfx variations like death screams, or sequence alts.
     *
     * Type 05 (new), 0c (old): sequences
     * N audio segment, normally with lead-in but not lead-outs.  Sequences can reuse
     * segments (internal loops), or can be single entries (full song or leads).
     * Sequences seem to include only music or cutscenes, so even single entries can be
     * useful to parse, since the readable name would make them stand out. Format:
     * - extra offset to chain
     * - loop segment
     * - non-looping flag
     * - sequence count
     * - ID-like fields in the header and sequence table may point to other chains?
     *
     * Type 06 (new), 0d (old):
     * Layer header, stream divided into N equal parts in a blocked format. Format:
     * - fixed part (id, type)
     * - extra offset to layer info (sample rate, pcm bits?, channels, codec, samples)
     * - layer count
     * - sometimes total channels, bitrate, etc
     * - flags?
     * - stream size + stream offset
     * - external filename, or filename offset in the extra table
     * Layer blocks are handled separatedly as the format doesn't depend on sb's version/platform.
     * Some values may be flags/config as multiple 0x06 can point to the same layer, with different 'flags'?
     *
     * Type 07 (new), 0e (old):
     * Another chain of something (single entry?), rare.
     *
     * Type 08 (new), 0f (old):
     * Silence, with a value representing duration (no sample rate/channels/extra table/etc given).
     * Typically used in chains to extend play time of another audio.
     * For older games 08 is used for something else (maybe equivalent to 02?)
     */

    /* debug strings reference:
     * - TYPE_SAMPLE: should be 0x01 (also "sound resource")
     * - TYPE_MULTITRACK: should be 0x06/0x0d/0x0b (also "multilayer resource")
     * - TYPE_SILENCE: should be 0x08
     * sequences may be "theme resource"
     * "class descriptor" is referenced too.
     *
     * Type names from .bnm (.sb's predecessor):
     * 0: TYPE_INVALID
     * 1: TYPE_SAMPLE
     * 2: TYPE_MIDI
     * 3: TYPE_CDAUDIO
     * 4: TYPE_SEQUENCE (sfx chain?)
     * 5: TYPE_SWITCH_OLD
     * 6: TYPE_SPLIT
     * 7: TYPE_THEME_OLD
     * 8: TYPE_SWITCH
     * 9: TYPE_THEME_OLD2
     * A: TYPE_RANDOM
     * B: TYPE_THEME0 (sequence)
     * Only 1, 2, 4, 9, A and B are known.
     * 2 is used rarely in Donald Duck's demo and point to a .mdx (midi?)
     * 9 is used in Tonic Trouble Special Edition
     * Others are common.
     */

    /* All types may contain memory garbage, making it harder to identify fields (platforms
     * and games are affected differently by this). Often types contain memory from the previous
     * type header unless overwritten, random memory, or default initialization garbage.
     * So if some non-audio type looks like audio it's probably repeating old data.
     * This even happens for common fields (ex. type 6 at 0x08 has prev garbage, not stream size). */

    /* games <= 0x00100000 seem to use old types, rest new types */


    /* maybe 0x20/0x24 for some but ok enough (null terminated) */
    sb->cfg.resource_name_size          = 0x28; /* min for Brother in Arms 2 (PS2) */

    /* represents map style (1=first, 2=mid, 3=latest) */
    if (sb->version <= 0x00000007)
        sb->cfg.map_version             = 1;
    else if (sb->version < 0x00150000)
        sb->cfg.map_version             = 2;
    else
        sb->cfg.map_version             = 3;

    sb->cfg.map_entry_size = (sb->cfg.map_version < 2) ? 0x30 : 0x34;

    if (sb->version == 0x00000000 || sb->version == 0x00000200) {
        sb->cfg.audio_stream_size       = 0x0c;
        sb->cfg.audio_stream_offset     = 0x10;
      //sb->cfg.audio_extra_offset      = 0x10;
      //sb->cfg.audio_extra_size        = 0x0c;

        sb->cfg.sequence_extra_offset   = 0x10;
      //sb->cfg.sequence_extra_size     = 0x0c;

        //sb->cfg.layer_extra_offset    = 0x10;
        //sb->cfg.layer_extra_size      = 0x0c;

        sb->cfg.random_extra_offset     = 0x10;
      //sb->cfg.random_extra_size       = 0x0c;
    } else if (sb->version <= 0x00000007) {
        sb->cfg.audio_stream_size       = 0x0c;
        sb->cfg.audio_extra_offset      = 0x10;
        sb->cfg.audio_stream_offset     = 0x14;

        sb->cfg.sequence_extra_offset   = 0x10;

        sb->cfg.layer_extra_offset      = 0x10;
    } else {
        sb->cfg.audio_stream_size       = 0x08;
        sb->cfg.audio_extra_offset      = 0x0c;
        sb->cfg.audio_stream_offset     = 0x10;

        sb->cfg.sequence_extra_offset   = 0x0c;

        sb->cfg.layer_extra_offset      = 0x0c;
    }

    if (sb->version == 0x00000000 || sb->version == 0x00000200) {
        sb->cfg.num_codec_flags = 1;
    } else if (sb->version <= 0x00000004) {
        sb->cfg.num_codec_flags = 2;
    } else if (sb->version < 0x000A0000) {
        sb->cfg.num_codec_flags = 1;
    }

    sb->allowed_types[0x01] = 1;
    sb->allowed_types[0x05] = 1;
    sb->allowed_types[0x0c] = 1;
    sb->allowed_types[0x06] = 1;
    sb->allowed_types[0x0d] = 1;
  //sb->allowed_types[0x08] = 1; /* only needed inside sequences */
  //sb->allowed_types[0x0f] = 1;
    if (sb->is_bnm) {
      //sb->allowed_types[0x0a] = 1; /* only needed inside sequences */
        sb->allowed_types[0x0b] = 1;
        sb->allowed_types[0x09] = 1;
    }

#if 0
    {
        STREAMFILE * streamTest;
        streamTest= open_streamfile_by_filename(streamFile, ".no_audio.sbx");
        if (streamTest) { sb->allowed_types[0x01] = 0; close_streamfile(streamTest); }

        streamTest= open_streamfile_by_filename(streamFile, ".no_sequence.sbx");
        if (streamTest) { sb->allowed_types[0x05] = sb->allowed_types[0x0c] = 0; close_streamfile(streamTest); }

        streamTest= open_streamfile_by_filename(streamFile, ".no_layer.sbx");
        if (streamTest) { sb->allowed_types[0x06] = sb->allowed_types[0x0d] = 0; close_streamfile(streamTest); }
    }
#endif

    /* two configs with same id; use SND file as identifier */
    if (sb->version == 0x00000000 && sb->platform == UBI_PC) {
        STREAMFILE * streamTest = open_streamfile_by_filename(streamFile, "Dino.lcb");
        if (streamTest) {
            is_dino_pc = 1;
            close_streamfile(streamTest);
        }
    }

    /* some files in Dinosaur */
    if (sb->version == 0x00000200 && sb->platform == UBI_PC) {
        sb->version = 0x00000000;
        is_dino_pc = 1;
    }

    /* Tonic Touble beta has garbage instead of version */
    if (sb->is_bnm && sb->version > 0x00000000 && sb->platform == UBI_PC) {
        STREAMFILE * streamTest = open_streamfile_by_filename(streamFile, "ED_MAIN.LCB");
        if (streamTest) {
            is_ttse_pc = 1;
            sb->version = 0x00000000;
            close_streamfile(streamTest);
        }
    }


    /* Tonic Trouble Special Edition (1999)(PC)-bnm */
    if (sb->version == 0x00000000 && sb->platform == UBI_PC && is_ttse_pc) {
        config_sb_entry(sb, 0x20, 0x5c);

        config_sb_audio_fs(sb, 0x2c, 0x00, 0x30);
        config_sb_audio_hs(sb, 0x42, 0x3c, 0x38, 0x38, 0x48, 0x44);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x24, 0x18);

        //config_sb_random_old(sb, 0x18, 0x0c);

        /* no layers */
        //todo type 9 needed
        //todo MPX don't set stream size?
        return 1;
    }


    /* Rayman 2: The Great Escape (1999)(PC)-bnm */
    /* Tonic Trouble (1999)(PC)-bnm */
    /* Donald Duck: Goin' Quackers (2000)(PC)-bnm */
    /* Disney's Dinosaur (2000)(PC)-bnm */
    if (sb->version == 0x00000000 && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x20, 0x5c);

        config_sb_audio_fs(sb, 0x2c, 0x00, 0x30);
        config_sb_audio_hs(sb, 0x42, 0x3c, 0x34, 0x34, 0x48, 0x44);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x24, 0x18);

        config_sb_random_old(sb, 0x18, 0x0c); /* Rayman 2 needs it for rare sequence ends (ex. Bnk_31.bnm) */

        /* no layers */

        if (is_dino_pc)
            config_sb_entry(sb, 0x20, 0x60);
        return 1;
    }

    /* Batman: Vengeance (2001)(PC)-map */
    /* Batman: Vengeance (2001)(Xbox)-map */
    if ((sb->version == 0x00000003 && sb->platform == UBI_PC) ||
        (sb->version == 0x00000003 && sb->platform == UBI_XBOX)) {
        config_sb_entry(sb, 0x40, 0x68);

        config_sb_audio_fs(sb, 0x30, 0x00, 0x34);
        config_sb_audio_hs(sb, 0x52, 0x4c, 0x38, 0x40, 0x58, 0x54);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x2c, 0x1c);

        config_sb_layer_hs(sb, 0x20, 0x4c, 0x44, 0x34);
        config_sb_layer_sh(sb, 0x1c, 0x04, 0x0a, 0x0c, 0x18);
        return 1;
    }

    /* Disney's Tarzan: Untamed (2001)(GC)-map */
    /* Batman: Vengeance (2001)(GC)-map */
    /* Donald Duck: Goin' Quackers (2002)(GC)-map */
    if (sb->version == 0x00000003 && sb->platform == UBI_GC) {
        config_sb_entry(sb, 0x40, 0x6c);

        config_sb_audio_fs(sb, 0x30, 0x00, 0x34);
        config_sb_audio_hs(sb, 0x56, 0x50, 0x48, 0x48, 0x5c, 0x58); /* 0x38 may be num samples too? */

        config_sb_sequence(sb, 0x2c, 0x1c);

        config_sb_layer_hs(sb, 0x20, 0x4c, 0x44, 0x34);
        config_sb_layer_sh(sb, 0x1c, 0x04, 0x0a, 0x0c, 0x18);
        return 1;
    }

#if 0
    //todo too weird
    /* Batman: Vengeance (2001)(PS2)-map */
    /* Disney's Tarzan: Untamed (2001)(PS2)-map */
    if (sb->version == 0x00000003 && sb->platform == UBI_PS2) {
        config_sb_entry(sb, 0x30, 0x3c);

        config_sb_audio_fb(sb, 0x1c, (1 << 2), (1 << 3), (1 << 4)); /* not ok */
        config_sb_audio_hs(sb, 0x00, 0x24, 0x28, 0x28, 0x00, 0x00);
        /* channels: 0? maybe 2=external, 1=internal? */
        /* stream type: always PS-ADPCM (interleave unknown) */
        /* sb->cfg.audio_stream_string = "STRM.SM1"; */ /* fixed */

        config_sb_sequence(sb, 0x2c, 0x10); /* this is normal enough */

        /* layers have a weird format too */
        return 1;
    }
#endif

#if 0
    //todo group flags and maybe num_samples for sfx are off
    /* Myst III: Exile (2001)(PS2)-map */
    if (sb->version == 0x00000004 && sb->platform == UBI_PS2) {
        config_sb_entry(sb, 0x34, 0x70);

        config_sb_audio_fb(sb, 0x1c, (1 << 3), (1 << 6), (1 << 4)); //???
        config_sb_audio_hs(sb, 0x24, 0x28, 0x2c, 0x34, 0x44, 0x6c);
        sb->cfg.audio_external_flag = 0x6c; /* no external flag? use codec as flag */

        config_sb_sequence(sb, 0x2c, 0x24);
        return 1;
    }
#endif

    /* Splinter Cell (2002)(PC)-map */
    /* Splinter Cell: Pandora Tomorrow (2004)(PC)-map */
    if (sb->version == 0x00000007 && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x58, 0x80);

        config_sb_audio_fs(sb, 0x28, 0x00, 0x2c);
        config_sb_audio_hs(sb, 0x4a, 0x44, 0x30, 0x38, 0x50, 0x4c);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x2c, 0x34);

        config_sb_layer_hs(sb, 0x24, 0x64, 0x5c, 0x34);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x06, 0x08, 0x14);
        return 1;
    }

    /* Splinter Cell (2002)(Xbox)-map */
    /* Splinter Cell: Pandora Tomorrow (2004)(Xbox)-map */
    if (sb->version == 0x00000007 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x58, 0x78);

        config_sb_audio_fs(sb, 0x28, 0x00, 0x2c);
        config_sb_audio_hs(sb, 0x4a, 0x44, 0x30, 0x38, 0x50, 0x4c);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x2c, 0x34);

        config_sb_layer_hs(sb, 0x24, 0x64, 0x5c, 0x34);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x06, 0x08, 0x14);
        return 1;
    }

    /* SC:PT PS2/GC has some quirks, noooo (lame autodetection but this stuff is too hard) */
    if ((sb->version == 0x00000007 && sb->platform == UBI_PS2) ||
        (sb->version == 0x00000007 && sb->platform == UBI_GC) ) {
        int32_t (*read_32bit)(off_t,STREAMFILE*) = sb->big_endian ? read_32bitBE : read_32bitLE;

        /* both SC:PT's LMx and SMx have 33 maps, SC1 doesn't */
        is_sc2_ps2_gc = read_32bit(0x08, streamFile) == 0x21;

        /* could also load ECHELON.SP1/Echelon.SP3 and test BE 0x04 == 0x00ACBF77,
         * but it's worse for localization subdirs without it */
    }

    /* Splinter Cell (2002)(PS2)-map */
    /* Splinter Cell: Pandora Tomorrow (2006)(PS2)-map 0x00000007 */
    if (sb->version == 0x00000007 && sb->platform == UBI_PS2) {
        config_sb_entry(sb, 0x40, 0x70);

        config_sb_audio_fb(sb, 0x1c, (1 << 2), 0, (1 << 4));
        config_sb_audio_hs(sb, 0x24, 0x28, 0x34, 0x3c, 0x44, 0x6c); /* num_samples may be null */

        config_sb_sequence(sb, 0x2c, 0x30);

        config_sb_layer_hs(sb, 0x24, 0x64, 0x5c, 0x34);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x06, 0x08, 0x14);

        if (is_sc2_ps2_gc) {
            sb->cfg.map_entry_size = 0x38;
            /* some amb .ss2 have bad sizes with mixed random data, bad extraction/unused crap? */
            /* Pandora Tomorrow voices have bad offsets too */
        }
        return 1;
    }

    /* Splinter Cell (2002)(GC)-map */
    /* Splinter Cell: Pandora Tomorrow (2004)(GC)-map */
    if (sb->version == 0x00000007 && sb->platform == UBI_GC) {
        config_sb_entry(sb, 0x58, 0x78);

        config_sb_audio_fs(sb, 0x24, 0x00, 0x28);
        config_sb_audio_hs(sb, 0x4a, 0x44, 0x2c, 0x34, 0x50, 0x4c);

        config_sb_sequence(sb, 0x2c, 0x34);

        config_sb_layer_hs(sb, 0x24, 0x64, 0x5c, 0x34);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x06, 0x08, 0x14);

        if (is_sc2_ps2_gc) {
            sb->cfg.map_entry_size = 0x38;
            sb->cfg.audio_external_and = 0x01000000; /* did somebody forget about BE? */
        }
        return 1;
    }

    /* Rainbow Six 3 (2003)(PC)-bank 0x0000000B */
    if (sb->version == 0x0000000B && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x5c, 0x7c);

        config_sb_audio_fs(sb, 0x24, 0x00, 0x28);
        config_sb_audio_hs(sb, 0x46, 0x40, 0x2c, 0x34, 0x4c, 0x48);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x28, 0x34);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);
        return 1;
    }

    /* Prince of Persia: The Sands of Time Demo (2003)(Xbox)-bank 0x0000000D */
    if (sb->version == 0x0000000D && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x5c, 0x74);

        config_sb_audio_fs(sb, 0x24, 0x00, 0x28);
        config_sb_audio_hs(sb, 0x46, 0x40, 0x2c, 0x34, 0x4c, 0x48);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x28, 0x34);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);
        return 1;
    }

    /* Prince of Persia: The Sands of Time Demo (2003)(Xbox)-bank 0x000A0000 */
    if (sb->version == 0x000A0000 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x64, 0x78);

        config_sb_audio_fs(sb, 0x24, 0x28, 0x2c);
        config_sb_audio_hs(sb, 0x4a, 0x44, 0x30, 0x38, 0x50, 0x4c);

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);
        return 1;
    }

    /* Prince of Persia: Sands of Time (2003)(PC)-bank 0x000A0004 / 0x000A0002 (just in case) */
    if ((sb->version == 0x000A0002 && sb->platform == UBI_PC) ||
        (sb->version == 0x000A0004 && sb->platform == UBI_PC)) {
        config_sb_entry(sb, 0x64, 0x80);

        config_sb_audio_fs(sb, 0x24, 0x2c, 0x28);
        config_sb_audio_hs(sb, 0x4a, 0x44, 0x30, 0x38, 0x50, 0x4c);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);
        return 1;
    }

    /* two configs with same id; use project file as identifier */
    if (sb->version == 0x000A0007 && sb->platform == UBI_PS2) {
        STREAMFILE * streamTest = open_streamfile_by_filename(streamFile, "BIAAUDIO.SP1");
        if (!streamTest) /* try again for localized subfolders */
            streamTest = open_streamfile_by_filename(streamFile, "../BIAAUDIO.SP1");
        if (streamTest) {
            is_bia_ps2 = 1;
            close_streamfile(streamTest);
        }
    }

    /* Prince of Persia: The Sands of Time (2003)(PS2)-bank 0x000A0004 / 0x000A0002 (POP1 port/Demo) */
    /* Tom Clancy's Rainbow Six 3 (2003)(PS2)-bank 0x000A0007 */
    /* Tom Clancy's Ghost Recon 2 (2004)(PS2)-bank 0x000A0007 */
    /* Splinter Cell: Pandora Tomorrow (2006)(PS2)-bank 0x000A0008 (separate banks from main map) */
    /* Prince of Persia: Warrior Within Demo (2004)(PS2)-bank 0x00100000 */
    /* Prince of Persia: Warrior Within (2004)(PS2)-bank 0x00120009 */
    if ((sb->version == 0x000A0002 && sb->platform == UBI_PS2) ||
        (sb->version == 0x000A0004 && sb->platform == UBI_PS2) ||
        (sb->version == 0x000A0007 && sb->platform == UBI_PS2 && !is_bia_ps2) ||
        (sb->version == 0x000A0008 && sb->platform == UBI_PS2) ||
        (sb->version == 0x00100000 && sb->platform == UBI_PS2) ||
        (sb->version == 0x00120009 && sb->platform == UBI_PS2)) {
        config_sb_entry(sb, 0x48, 0x6c);

        config_sb_audio_fb(sb, 0x18, (1 << 2), (1 << 3), (1 << 4));
        config_sb_audio_hs(sb, 0x20, 0x24, 0x30, 0x38, 0x40, 0x68); /* num_samples may be null */

        config_sb_sequence(sb, 0x28, 0x10);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);

        config_sb_silence_i(sb, 0x18);

        return 1;
    }

    /* Brothers in Arms: Road to Hill 30 (2005)[PS2] */
    /* Brothers in Arms: Earned in Blood (2005)[PS2] */
    if (sb->version == 0x000A0007 && sb->platform == UBI_PS2 && is_bia_ps2) {
        config_sb_entry(sb, 0x5c, 0x14c);

        config_sb_audio_fb(sb, 0x18, (1 << 2), (1 << 3), (1 << 4));
        config_sb_audio_hs(sb, 0x20, 0x24, 0x30, 0x38, 0x40, 0x148); /* num_samples may be null */

        config_sb_sequence(sb, 0x28, 0x10);

        config_sb_layer_hs(sb, 0x20, 0x140, 0x138, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);

        sb->cfg.is_padded_section1_offset = 1;
        sb->cfg.is_padded_section2_offset = 1;
        sb->cfg.is_padded_section3_offset = 1;
        sb->cfg.is_padded_sectionX_offset = 1;
        sb->cfg.is_padded_sounds_offset = 1;
        return 1;
    }

    /* Batman: Rise of Sin Tzu (2003)(Xbox)-map 0x000A0003 */
    if (sb->version == 0x000A0003 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x64, 0x80);

        config_sb_audio_fs(sb, 0x24, 0x28, 0x34);
        config_sb_audio_hs(sb, 0x52, 0x4c, 0x38, 0x40, 0x58, 0x54);
        sb->cfg.audio_has_internal_names = 1;
        sb->cfg.default_codec_for_group0 = 1;

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);
        //todo some sequences mix 1ch and 2ch (voices?)
        return 1;
    }

    /* Prince of Persia: The Sands of Time (2003)(Xbox)-bank 0x000A0004 / 0x000A0002 (POP1 port/Demo) */
    if ((sb->version == 0x000A0002 && sb->platform == UBI_XBOX) ||
        (sb->version == 0x000A0004 && sb->platform == UBI_XBOX)) {
        config_sb_entry(sb, 0x64, 0x78);

        config_sb_audio_fs(sb, 0x24, 0x28, 0x2c);
        config_sb_audio_hs(sb, 0x4a, 0x44, 0x30, 0x38, 0x50, 0x4c);
        sb->cfg.audio_has_internal_names = 1;
        sb->cfg.default_codec_for_group0 = 1;

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);
        return 1;
    }

    /* Batman: Rise of Sin Tzu (2003)(GC)-map 0x000A0002 */
    /* Prince of Persia: The Sands of Time (2003)(GC)-bank 0x000A0004 / 0x000A0002 (POP1 port) */
    /* Tom Clancy's Rainbow Six 3 (2003)(Xbox)-bank 0x000A0007 */
    if ((sb->version == 0x000A0002 && sb->platform == UBI_GC) ||
        (sb->version == 0x000A0004 && sb->platform == UBI_GC) ||
        (sb->version == 0x000A0007 && sb->platform == UBI_GC)) {
        config_sb_entry(sb, 0x64, 0x74);

        config_sb_audio_fs(sb, 0x20, 0x24, 0x28);
        config_sb_audio_hs(sb, 0x46, 0x40, 0x2c, 0x34, 0x4c, 0x48);

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);

        config_sb_silence_i(sb, 0x18);
        return 1;
    }

    /* Tom Clancy's Rainbow Six 3 (2003)(Xbox)-bank 0x000A0007 */
    if (sb->version == 0x000A0007 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x64, 0x8c);

        config_sb_audio_fs(sb, 0x24, 0x28, 0x40);
        config_sb_audio_hs(sb, 0x5e, 0x58, 0x44, 0x4c, 0x64, 0x60);
        sb->cfg.audio_has_internal_names = 1;
        sb->cfg.default_codec_for_group0 = 1;

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x20, 0x60, 0x58, 0x30);
        config_sb_layer_sh(sb, 0x14, 0x00, 0x06, 0x08, 0x10);

        config_sb_silence_i(sb, 0x18);
        return 1;
    }

    /* Myst IV Demo (2004)(PC)-bank */
    if (sb->version == 0x00100000 && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x68, 0xa4);

        config_sb_audio_fs(sb, 0x24, 0x2c, 0x28);
        config_sb_audio_hs(sb, 0x4c, 0x44, 0x30, 0x38, 0x54, 0x50);
        sb->cfg.audio_has_internal_names = 1;
        return 1;
    }

    /* Prince of Persia: Warrior Within Demo (2004)(PC)-bank 0x00120006 */
    /* Prince of Persia: Warrior Within (2004)(PC)-bank 0x00120009 */
    if ((sb->version == 0x00120006 && sb->platform == UBI_PC) ||
        (sb->version == 0x00120009 && sb->platform == UBI_PC)) {
        config_sb_entry(sb, 0x6c, 0x84);

        config_sb_audio_fs(sb, 0x24, 0x2c, 0x28);
        config_sb_audio_hs(sb, 0x4c, 0x44, 0x30, 0x38, 0x54, 0x50);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x28, 0x14);
        return 1;
    }

    /* Prince of Persia: Warrior Within (2004)(Xbox)-bank */
    if (sb->version == 0x00120009 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x6c, 0x90);

        config_sb_audio_fs(sb, 0x24, 0x28, 0x40);
        config_sb_audio_hs(sb, 0x60, 0x58, 0x44, 0x4c, 0x68, 0x64);
        sb->cfg.audio_has_internal_names = 1;
        sb->cfg.default_codec_for_group0 = 1;

        config_sb_sequence(sb, 0x28, 0x14);
        return 1;
    }

    /* Prince of Persia: Warrior Within (2004)(GC)-bank */
    if (sb->version == 0x00120009 && sb->platform == UBI_GC) {
        config_sb_entry(sb, 0x6c, 0x78);

        config_sb_audio_fs(sb, 0x20, 0x24, 0x28);
        config_sb_audio_hs(sb, 0x48, 0x40, 0x2c, 0x34, 0x50, 0x4c);

        config_sb_sequence(sb, 0x28, 0x14);
        return 1;
    }

    /* two configs with same id and both sb4/sm4; use project file as identifier */
    if (sb->version == 0x0012000C && sb->platform == UBI_PSP) {
        STREAMFILE * streamTest = open_streamfile_by_filename(streamFile, "BIAAUDIO.SP4");
        if (streamTest) {
            is_biadd_psp = 1;
            close_streamfile(streamTest);
        }
    }

    /* Prince of Persia: Revelations (2005)(PSP)-bank */
    /* Splinter Cell: Essentials (2006)(PSP)-map */
    /* Beowulf: The Game (2007)(PSP)-map */
    if (sb->version == 0x0012000C && sb->platform == UBI_PSP && !is_biadd_psp) {
        config_sb_entry(sb, 0x68, 0x84);

        config_sb_audio_fs(sb, 0x24, 0x2c, 0x28);
        config_sb_audio_hs(sb, 0x4c, 0x44, 0x30, 0x38, 0x54, 0x50);
        sb->cfg.audio_has_internal_names = 1;

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_hs(sb, 0x1c, 0x60, 0x64, 0x30);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    //todo some .sbX have bad external stream offsets, but not all (ex. offset 0xE3641 but should be 0x0A26)
    /* Brothers in Arms: D-Day (2006)(PSP)-bank */
    if (sb->version == 0x0012000C && sb->platform == UBI_PSP && is_biadd_psp) {
        config_sb_entry(sb, 0x80, 0x94);

        config_sb_audio_fs(sb, 0x24, 0x2c, 0x28);
        config_sb_audio_hs(sb, 0x4c, 0x44, 0x30, 0x38, 0x54, 0x50);
        sb->cfg.audio_has_internal_names = 1;
        return 1;
    }

    /* Splinter Cell: Chaos Theory (2005)(PC)-map */
    if (sb->version == 0x00120012 && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x68, 0x60);

        config_sb_audio_fs(sb, 0x24, 0x2c, 0x28);
        config_sb_audio_he(sb, 0x4c, 0x44, 0x30, 0x38, 0x54, 0x50);

        config_sb_sequence(sb, 0x28, 0x14);
        return 1;
    }

    /* Myst IV: Revelation (2005)(PC)-bank */
    /* Splinter Cell: Chaos Theory (2005)(Xbox)-map */
    if (sb->version == 0x00120012 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x48, 0x4c);

        config_sb_audio_fb(sb, 0x18, (1 << 3), (1 << 4), (1 << 10));
        config_sb_audio_he(sb, 0x38, 0x30, 0x1c, 0x24, 0x40, 0x3c);

        config_sb_sequence(sb, 0x28, 0x10);
        return 1;
    }

    /* Splinter Cell: Chaos Theory (2005)(GC)-map */
    if (sb->version == 0x00130001 && sb->platform == UBI_GC) {
        config_sb_entry(sb, 0x68, 0x54);

        config_sb_audio_fs(sb, 0x20, 0x24, 0x28);
        config_sb_audio_he(sb, 0x48, 0x40, 0x2c, 0x34, 0x50, 0x4c);

        config_sb_sequence(sb, 0x28, 0x14);

        config_sb_layer_he(sb, 0x1c, 0x34, 0x3c, 0x40);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Splinter Cell 3D (2011)(3DS)-map */
    if (sb->version == 0x00130001 && sb->platform == UBI_3DS) {
        config_sb_entry(sb, 0x48, 0x4c);

        config_sb_audio_fb(sb, 0x18, (1 << 2), (1 << 3), (1 << 4));
        config_sb_audio_he(sb, 0x38, 0x30, 0x1c, 0x24, 0x40, 0x3c);

        config_sb_sequence(sb, 0x28, 0x10);

        config_sb_layer_he(sb, 0x1c, 0x28, 0x30, 0x34);
        config_sb_layer_sh(sb, 0x18, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Tom Clancy's Ghost Recon Advanced Warfighter (2006)(PS2)-bank */
    if (sb->version == 0x00130004 && sb->platform == UBI_PS2) {
        config_sb_entry(sb, 0x48, 0x50);

        config_sb_audio_fb(sb, 0x18, (1 << 2), (1 << 3), (1 << 4));
        config_sb_audio_he(sb, 0x20, 0x24, 0x30, 0x38, 0x40, 0x4c);
        sb->cfg.audio_interleave = 0x8000;

        sb->cfg.is_padded_section1_offset = 1;
        sb->cfg.is_padded_sounds_offset = 1;
        return 1;
    }

    /* Prince of Persia: The Two Thrones (2005)(PC)-bank */
    if (sb->version == 0x00150000 && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x68, 0x78);

        config_sb_audio_fs(sb, 0x2c, 0x34, 0x30);
        config_sb_audio_he(sb, 0x5c, 0x54, 0x40, 0x48, 0x64, 0x60);

        config_sb_sequence(sb, 0x2c, 0x14);
        return 1;
    }

    /* Prince of Persia: The Two Thrones (2005)(PS2)-bank */
    if (sb->version == 0x00150000 && sb->platform == UBI_PS2) {
        config_sb_entry(sb, 0x48, 0x5c);

        config_sb_audio_fb(sb, 0x20, (1 << 2), (1 << 3), (1 << 4));
        config_sb_audio_he(sb, 0x2c, 0x30, 0x3c, 0x44, 0x4c, 0x50);

        config_sb_sequence(sb, 0x2c, 0x10);
        return 1;
    }

    /* Prince of Persia: The Two Thrones (2005)(Xbox)-bank 0x00150000 */
    /* Far Cry Instincts (2005)(Xbox)-bank 0x00150000 */
    /* Splinter Cell: Double Agent (2006)(Xbox)-map 0x00160002 */
    /* Far Cry Instincts: Evolution (2006)(Xbox)-bank 0x00170000 */
    if ((sb->version == 0x00150000 && sb->platform == UBI_XBOX) ||
        (sb->version == 0x00160002 && sb->platform == UBI_XBOX) ||
        (sb->version == 0x00170000 && sb->platform == UBI_XBOX)) {
        config_sb_entry(sb, 0x48, 0x58);

        config_sb_audio_fb(sb, 0x20, (1 << 3), (1 << 4), (1 << 10));
        config_sb_audio_he(sb, 0x44, 0x3c, 0x28, 0x30, 0x4c, 0x48);

        config_sb_sequence(sb, 0x2c, 0x10);

        config_sb_layer_he(sb, 0x20, 0x2c, 0x34, 0x3c);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Prince of Persia: The Two Thrones (2005)(GC)-bank 0x00150000 */
    /* Splinter Cell: Double Agent (2006)(GC)-map 0x00160002 */
    if ((sb->version == 0x00150000 && sb->platform == UBI_GC) || 
        (sb->version == 0x00160002 && sb->platform == UBI_GC)) {
        config_sb_entry(sb, 0x68, 0x6c);

        config_sb_audio_fs(sb, 0x28, 0x2c, 0x30);
        config_sb_audio_he(sb, 0x58, 0x50, 0x3c, 0x44, 0x60, 0x5c);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x38, 0x40, 0x48);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Splinter Cell: Double Agent (2006)(PS2)-map 0x00160002 */
    /* Open Season (2006)(PS2)-map 0x00180003 */
    /* Open Season (2006)(PSP)-map 0x00180003 */
    /* Shaun White Snowboarding (2008)(PS2)-map 0x00180003 */
    /* Prince of Persia: Rival Swords (2007)(PSP)-bank 0x00180005 */
    /* Rainbow Six Vegas (2007)(PSP)-bank 0x00180006 */
    /* Star Wars: Lethal Alliance (2006)(PSP)-map 0x00180007 */
    if ((sb->version == 0x00160002 && sb->platform == UBI_PS2) ||
        (sb->version == 0x00180003 && sb->platform == UBI_PS2) ||
        (sb->version == 0x00180003 && sb->platform == UBI_PSP) ||
        (sb->version == 0x00180005 && sb->platform == UBI_PSP) ||
        (sb->version == 0x00180006 && sb->platform == UBI_PSP) ||
        (sb->version == 0x00180007 && sb->platform == UBI_PSP)) {
        config_sb_entry(sb, 0x48, 0x54);

        config_sb_audio_fb(sb, 0x20, (1 << 2), (1 << 3), (1 << 4));
        config_sb_audio_he(sb, 0x28, 0x2c, 0x34, 0x3c, 0x44, 0x48);

        config_sb_sequence(sb, 0x2c, 0x10);

        config_sb_layer_he(sb, 0x20, 0x2c, 0x30, 0x38);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);

        config_sb_silence_f(sb, 0x1c);

        /* Rainbow Six Vegas (PSP) has 2 layers with different sample rates, but 2nd layer is silent and can be ignored */
        if (sb->version == 0x00180006 && sb->platform == UBI_PSP)
            sb->cfg.ignore_layer_error = 1;
        return 1;
    }

    /* Tom Clancy's Ghost Recon Advanced Warfighter (2006)(X360)-bank */
    if (sb->version == 0x00170001 && sb->platform == UBI_X360) {
        config_sb_entry(sb, 0x68, 0x70);

        config_sb_audio_fs(sb, 0x2c, 0x30, 0x34);
        config_sb_audio_he(sb, 0x5c, 0x54, 0x40, 0x48, 0x64, 0x60);
        sb->cfg.audio_xma_offset = 0; /* header is in the extra table */

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x38, 0x40, 0x48);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x08, 0x0c, 0x14);
        sb->cfg.layer_hijack = 1; /* WTF!!! layer format different from other layers using same id!!! */
        return 1;
    }

    /* Open Season (2006)(PC)-map 0x00180003 */
    if (sb->version == 0x00180003 && sb->platform == UBI_PC) {
        config_sb_entry(sb, 0x68, 0x78);

        config_sb_audio_fs(sb, 0x2c, 0x34, 0x30);
        config_sb_audio_he(sb, 0x5c, 0x54, 0x40, 0x48, 0x64, 0x60);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x38, 0x3c, 0x44);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);

        config_sb_silence_f(sb, 0x1c);
        return 1;
    }

    /* Open Season (2006)(Xbox)-map 0x00180003 */
    if (sb->version == 0x00180003 && sb->platform == UBI_XBOX) {
        config_sb_entry(sb, 0x48, 0x58);

        config_sb_audio_fb(sb, 0x20, (1 << 3), (1 << 4), (1 << 10));
        config_sb_audio_he(sb, 0x44, 0x3c, 0x28, 0x30, 0x4c, 0x48);

        config_sb_sequence(sb, 0x2c, 0x10);

        config_sb_layer_he(sb, 0x20, 0x2c, 0x30, 0x38);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);

        config_sb_silence_f(sb, 0x1c);
        return 1;
    }

    /* Open Season (2006)(GC)-map 0x00180003 */
    if (sb->version == 0x00180003 && sb->platform == UBI_GC) {
        config_sb_entry(sb, 0x68, 0x6c);

        config_sb_audio_fs(sb, 0x28, 0x2c, 0x30);
        config_sb_audio_he(sb, 0x58, 0x50, 0x3c, 0x44, 0x60, 0x5c);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x38, 0x3c, 0x44);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);

        config_sb_silence_f(sb, 0x1c);
        return 1;
    }


    /* two configs with same id; use project file as identifier */
    if (sb->version == 0x00180006 && sb->platform == UBI_PC) {
        STREAMFILE * streamTest = open_streamfile_by_filename(streamFile, "Sc4_online_SoundProject.SP0");
        if (streamTest) {
            is_sc4_pc_online = 1;
            close_streamfile(streamTest);
        }
    }

    /* Splinter Cell: Double Agent (2006)(PC)-map (offline) */
    if (sb->version == 0x00180006 && sb->platform == UBI_PC && !is_sc4_pc_online) {
        config_sb_entry(sb, 0x68, 0x7c);

        config_sb_audio_fs(sb, 0x2c, 0x34, 0x30);
        config_sb_audio_he(sb, 0x5c, 0x54, 0x40, 0x48, 0x64, 0x60);

        config_sb_layer_he(sb, 0x20, 0x38, 0x3c, 0x44);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Splinter Cell: Double Agent (2006)(PC)-map (online) */
    if (sb->version == 0x00180006 && sb->platform == UBI_PC && is_sc4_pc_online) {
        config_sb_entry(sb, 0x68, 0x78);

        config_sb_audio_fs(sb, 0x2c, 0x34, 0x30);
        config_sb_audio_he(sb, 0x5c, 0x54, 0x40, 0x48, 0x64, 0x60);

        config_sb_layer_he(sb, 0x20, 0x38, 0x3c, 0x44);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Splinter Cell: Double Agent (2006)(X360)-map */
    if (sb->version == 0x00180006 && sb->platform == UBI_X360) {
        config_sb_entry(sb, 0x68, 0x78);

        config_sb_audio_fs(sb, 0x2c, 0x30, 0x34);
        config_sb_audio_he(sb, 0x5c, 0x54, 0x40, 0x48, 0x64, 0x60);
        sb->cfg.audio_xma_offset = 0x70;

        config_sb_layer_he(sb, 0x20, 0x38, 0x3c, 0x44);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* Red Steel (2006)(Wii)-bank */
    if (sb->version == 0x00180006 && sb->platform == UBI_WII) {
        config_sb_entry(sb, 0x68, 0x6c);

        config_sb_audio_fs(sb, 0x28, 0x2c, 0x30);
        config_sb_audio_he(sb, 0x58, 0x50, 0x3c, 0x44, 0x60, 0x5c);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x38, 0x3c, 0x44);
        config_sb_layer_sh(sb, 0x34, 0x00, 0x08, 0x0c, 0x14);
        return 1;
    }

    /* TMNT (2007)(PSP)-map 0x00190001 */
    /* Surf's Up (2007)(PSP)-map 0x00190005 */
    if ((sb->version == 0x00190001 && sb->platform == UBI_PSP) ||
        (sb->version == 0x00190005 && sb->platform == UBI_PSP)) {
        config_sb_entry(sb, 0x48, 0x58);

        config_sb_audio_fb(sb, 0x20, (1 << 2), (1 << 3), (1 << 4)); /* assumed group_flag */
        config_sb_audio_he(sb, 0x28, 0x2c, 0x34, 0x3c, 0x44, 0x48);

        config_sb_sequence(sb, 0x2c, 0x10);

        config_sb_layer_he(sb, 0x20, 0x2c, 0x30, 0x38);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x04, 0x08, 0x10);
        return 1;
    }

    /* TMNT (2007)(GC)-bank */
    if (sb->version == 0x00190002 && sb->platform == UBI_GC) {
        config_sb_entry(sb, 0x68, 0x6c);

        config_sb_audio_fs(sb, 0x28, 0x2c, 0x30); /* assumed groud_id */
        config_sb_audio_he(sb, 0x3c, 0x40, 0x48, 0x50, 0x58, 0x5c);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x34, 0x38, 0x40);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x04, 0x08, 0x10);

        config_sb_silence_f(sb, 0x1c);
        return 1;
    }

    /* TMNT (2007)(PS2)-bank */
    if (sb->version == 0x00190002 && sb->platform == UBI_PS2) {
        config_sb_entry(sb, 0x48, 0x5c);

        config_sb_audio_fb(sb, 0x20, (1 << 2), (1 << 3), (1 << 4)); /* assumed group_flag */
        config_sb_audio_he(sb, 0x28, 0x2c, 0x34, 0x3c, 0x44, 0x48);

        config_sb_sequence(sb, 0x2c, 0x10);

        config_sb_layer_he(sb, 0x20, 0x2c, 0x30, 0x38);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x04, 0x08, 0x10);

        config_sb_silence_f(sb, 0x1c);
        return 1;
    }

    /* TMNT (2007)(X360)-bank 0x00190002 */
    /* Prince of Persia: Rival Swords (2007)(Wii)-bank 0x00190003 */
    /* Rainbow Six Vegas (2007)(PS3)-bank 0x00190005 */
    /* Surf's Up (2007)(PS3)-bank 0x00190005 */
    /* Surf's Up (2007)(X360)-bank 0x00190005 */
    /* Splinter Cell: Double Agent (2007)(PS3)-map 0x00190005 */
    if ((sb->version == 0x00190002 && sb->platform == UBI_X360) ||
        (sb->version == 0x00190003 && sb->platform == UBI_WII) ||
        (sb->version == 0x00190005 && sb->platform == UBI_PS3) ||
        (sb->version == 0x00190005 && sb->platform == UBI_X360)) {
        config_sb_entry(sb, 0x68, 0x70);
        sb->cfg.audio_fix_psx_samples = 1; /* ex. RSV PS3: 3n#10, SC DA PS3 */

        config_sb_audio_fs(sb, 0x28, 0x2c, 0x30);
        config_sb_audio_he(sb, 0x3c, 0x40, 0x48, 0x50, 0x58, 0x5c);
        sb->cfg.audio_xma_offset = 0x6c;
        sb->cfg.audio_interleave = 0x10;

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x34, 0x38, 0x40);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x04, 0x08, 0x10);

        return 1;
    }

    /* TMNT (2007)(PC)-bank 0x00190002 */
    /* Surf's Up (2007)(PC)-bank 0x00190005 */
    if ((sb->version == 0x00190002 && sb->platform == UBI_PC) ||
        (sb->version == 0x00190005 && sb->platform == UBI_PC)) {
        config_sb_entry(sb, 0x68, 0x74);

        config_sb_audio_fs(sb, 0x28, 0x2c, 0x30);
        config_sb_audio_he(sb, 0x3c, 0x40, 0x48, 0x50, 0x58, 0x5c);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x34, 0x38, 0x40);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x04, 0x08, 0x10);

        config_sb_silence_f(sb, 0x1c);
        return 1;
    }

    /* Rainbow Six Vegas 2 (2008)(PS3)-bank */
    /* Rainbow Six Vegas 2 (2008)(X360)-bank */
    if ((sb->version == 0x001C0000 && sb->platform == UBI_PS3) ||
        (sb->version == 0x001C0000 && sb->platform == UBI_X360)) {
        config_sb_entry(sb, 0x64, 0x7c);

        config_sb_audio_fs(sb, 0x28, 0x30, 0x34);
        config_sb_audio_he(sb, 0x44, 0x48, 0x50, 0x58, 0x60, 0x64);
        sb->cfg.audio_xma_offset = 0x78;
        sb->cfg.audio_interleave = 0x10;
        sb->cfg.audio_fix_psx_samples = 1;

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x44, 0x48, 0x54);
        config_sb_layer_sh(sb, 0x30, 0x00, 0x04, 0x08, 0x10);
        return 1;
    }

    /* Michael Jackson: The Experience (2010)(PSP)-map */
    if (sb->version == 0x001D0000 && sb->platform == UBI_PSP) {
        config_sb_entry(sb, 0x40, 0x60);

        config_sb_audio_fb(sb, 0x20, (1 << 2), (1 << 3), (1 << 5)); /* assumed group_flag */
        config_sb_audio_he(sb, 0x28, 0x30, 0x38, 0x40, 0x48, 0x4c);
        return 1;
    }

    /* Splinter Cell Classic Trilogy HD (2011)(PS3)-map */
    if (sb->version == 0x001D0000 && sb->platform == UBI_PS3) {
        config_sb_entry(sb, 0x5c, 0x80);
        sb->cfg.audio_interleave = 0x10;
        sb->cfg.audio_fix_psx_samples = 1;

        config_sb_audio_fs(sb, 0x28, 0x30, 0x34);
        config_sb_audio_he(sb, 0x44, 0x4c, 0x54, 0x5c, 0x64, 0x68);

        config_sb_sequence(sb, 0x2c, 0x14);

        config_sb_layer_he(sb, 0x20, 0x44, 0x48, 0x54);
        config_sb_layer_sh(sb, 0x38, 0x00, 0x04, 0x08, 0x10);
        return 1;
    }

    VGM_LOG("UBI SB: unknown SB/SM version+platform %08x\n", sb->version);
    return 0;
}