diff --git a/doc/FORMATS.md b/doc/FORMATS.md index a17a8334..73a5a2e6 100644 --- a/doc/FORMATS.md +++ b/doc/FORMATS.md @@ -241,7 +241,7 @@ different internally (encrypted, different versions, etc) and not always can be - **aifc.c** - Apple AIFF-C header [*AIFC*] - Apple AIFF header [*AIFF*] - - *aifc*: `.aif .laif .wav .lwav .(extensionless) .aifc .laifc .afc .cbd2 .bgm .fda .n64 .xa .aiff .laiff .acm .adp .ai .pcm` + - *aifc*: `.aif .laif .wav .lwav .(extensionless) .aifc .laifc .afc .cbd2 .bgm .fda .n64 .xa .caf .aiff .laiff .acm .adp .ai .pcm` - Codecs: SDX2 CBD2 DVI_IMA_int APPLE_IMA4 RELIC VADPCM PCM8 PCM16BE XA - **str_snds.c** - 3DO SNDS header [*STR_SNDS*] @@ -271,7 +271,7 @@ different internally (encrypted, different versions, etc) and not always can be - RIFF WAVE header (ctrl looping) [*RIFF_WAVE_MWV*] - RIFX WAVE header [*RIFX_WAVE*] - RIFX WAVE header (smpl looping) [*RIFX_WAVE_smpl*] - - *riff*: `.wav .lwav .xwav .mwv .da .dax .cd .med .snd .adx .adp .xss .xsew .adpcm .adw .wd .(extensionless) .sbv .wvx .str .at3 .rws .aud .at9 .ckd .saf .ima .nsa .pcm .xvag .ogg .logg .p1d .xms .mus .dat .ldat .wma .lwma` + - *riff*: `.wav .lwav .xwav .mwv .da .dax .cd .med .snd .adx .adp .xss .xsew .adpcm .adw .wd .(extensionless) .sbv .wvx .str .at3 .rws .aud .at9 .ckd .saf .ima .nsa .pcm .xvag .ogg .logg .p1d .xms .mus .dat .ldat .wma .lwma .caf` - *rifx*: `.wav .lwav` - Codecs: AICA_int PCM32LE PCM24LE PCM16BE PCM16LE PCM8_U MSADPCM IMA PCMFLOAT MS_IMA AICA MPEG_custom XBOX_IMA MS_IMA_3BIT DVI_IMA L5_555 OGG_VORBIS ATRAC9 ATRAC3 MPEG MSADPCM_int - **nwa.c** diff --git a/src/base/seek.c b/src/base/seek.c index 24e076fe..5c79c6f9 100644 --- a/src/base/seek.c +++ b/src/base/seek.c @@ -168,7 +168,7 @@ void seek_vgmstream(VGMSTREAM* vgmstream, int32_t seek_sample) { if (!vgmstream->hit_loop) { int32_t skip_samples; - if (vgmstream->current_sample >= vgmstream->loop_start_sample) { + if (vgmstream->current_sample > vgmstream->loop_start_sample) { /* may be 0 */ VGM_LOG("SEEK: bad current sample %i vs %i\n", vgmstream->current_sample, vgmstream->loop_start_sample); reset_vgmstream(vgmstream); } diff --git a/src/coding/libs/utkdec.c b/src/coding/libs/utkdec.c index b08ffb45..bf6061d8 100644 --- a/src/coding/libs/utkdec.c +++ b/src/coding/libs/utkdec.c @@ -3,7 +3,7 @@ #include #include "utkdec.h" - +// AKA 'UTALKSTATE' struct utk_context_t { /* config */ utk_type_t type; @@ -35,12 +35,12 @@ struct utk_context_t { }; -/* bit mask; (1 << count) - 1 is probably faster now but OG code uses a table */ +/* AKA 'bitmask'; (1 << count) - 1 is probably faster now but OG code uses a table */ static const uint8_t mask_table[8] = { 0x01,0x03,0x07,0x0F,0x1F,0x3F,0x7F,0xFF }; -/* reflection coefficients, rounded that correspond to hex values in exes (actual float is longer) +/* AKA 'coeff_table', reflection coefficients (rounded) that correspond to hex values in exes (actual float is longer) * note this table is mirrored: for (i = 1 .. 32) t[64 - i] = -t[i]) */ static const float utk_rc_table[64] = { /* 6b index start */ @@ -63,6 +63,7 @@ static const float utk_rc_table[64] = { +0.977431f, +0.983879f, +0.990327f, +0.996776f, }; +// AKA 'index_table' static const uint8_t utk_codebooks[2][256] = { /* normal model */ { @@ -109,6 +110,7 @@ enum { MDL_LARGEPULSE = 1 }; +// AKA 'decode_table' static const struct { int next_model; int code_size; @@ -202,7 +204,7 @@ static uint8_t peek_bits(struct bitreader_t* br, int count) { return br->bits_value & mask; } -/* assumes count <= 8, which is always true since sizes are known and don't depend on the bitstream. */ +/* aka 'getbits', LSB style and assumes count <= 8, which is always true since sizes are known and don't depend on the bitstream. */ static uint8_t read_bits(struct bitreader_t* br, int count) { uint8_t mask = mask_table[count - 1]; uint8_t ret = br->bits_value & mask; @@ -218,7 +220,7 @@ static uint8_t read_bits(struct bitreader_t* br, int count) { return ret; } -/* for clarity, as found in OG code (no return) */ +/* AKA 'discardbits', as found in OG code (no return) */ static void consume_bits(struct bitreader_t* br, int count) { read_bits(br, count); } @@ -307,19 +309,19 @@ static void decode_excitation(utk_context_t* ctx, bool use_multipulse, float* ou int bits = 0; float val = 0.0f; - /* peek + partial consume code (odd to use 2 codes for 0.0 but seen in multiple exes) */ + /* peek + partial consume code (bitreader is LSB so this is equivalent to reading bit by bit, but OG handles it like this) */ int huffman_code = peek_bits(&ctx->br, 2); /* variable-length, may consume less */ switch (huffman_code) { - case 0: //code: 0 - case 2: //code: 1 (maybe meant to be -0.0?) + case 0: //value 00 = h.code: 0 + case 2: //value 10 = h.code: 0 val = 0.0f; bits = 1; break; - case 1: //code: 01 + case 1: //value 01 = h.code: 10 val = -2.0f; bits = 2; break; - case 3: //code: 11 + case 3: //value 11 = h.code: 11 val = 2.0f; bits = 2; break; @@ -334,6 +336,7 @@ static void decode_excitation(utk_context_t* ctx, bool use_multipulse, float* ou } } +// AKA ref_to_lpc static void rc_to_lpc(const float* rc_data, float* lpc) { int j; float tmp1[12]; @@ -364,6 +367,7 @@ static void rc_to_lpc(const float* rc_data, float* lpc) { } } +// AKA 'filter' static void lp_synthesis_filter(utk_context_t* ctx, int offset, int blocks) { int i, j, k; float lpc[12]; @@ -393,7 +397,7 @@ static void lp_synthesis_filter(utk_context_t* ctx, int offset, int blocks) { } } -/* OG sometimes inlines this (sx3, not B&B/CBX) */ +// AKA 'interpolate', OG sometimes inlines this (sx3, not B&B/CBX) */ static void interpolate_rest(float* excitation) { for (int i = 0; i < 108; i += 2) { float tmp1 = (excitation[i - 5] + excitation[i + 5]) * 0.01803268f; @@ -403,6 +407,7 @@ static void interpolate_rest(float* excitation) { } } +// AKA 'decodemut' static void decode_frame_main(utk_context_t* ctx) { bool use_multipulse = false; float excitation[5 + 108 + 5]; /* extra +5*2 for interpolation */ @@ -436,7 +441,8 @@ static void decode_frame_main(utk_context_t* ctx) { rc_delta[i] = (utk_rc_table[idx] - ctx->rc_data[i]) * 0.25f; } - /* decode four subframes */ + + /* decode four subframes (AKA 'readsamples' but inline'd) */ for (int i = 0; i < 4; i++) { int pitch_lag = read_bits(&ctx->br, 8); int pitch_value = read_bits(&ctx->br, 4); diff --git a/src/coding/libs/utkdec.h b/src/coding/libs/utkdec.h index fac7e1ac..4e5f8b0a 100644 --- a/src/coding/libs/utkdec.h +++ b/src/coding/libs/utkdec.h @@ -10,6 +10,7 @@ * EA classifies MT as MT10:1 (smaller frames) and MT5:1 (bigger frames), but both are the same * with different encoding parameters. Later revisions may have PCM blocks (rare). This codec was * also reused by Traveller Tales in CBX (same devs?) with minor modifications. + * Internally it's sometimes called "UTalk" too. * * TODO: * - lazy/avoid peeking/overreading when no bits left (OG code does it though, shouldn't matter) @@ -25,9 +26,10 @@ typedef enum { /* opaque struct */ typedef struct utk_context_t utk_context_t; -/* inits UTK (must be externally created + init here) */ +/* inits UTK */ utk_context_t* utk_init(utk_type_t type); +/* frees UTK */ void utk_free(utk_context_t*); /* reset/flush */ diff --git a/src/meta/aifc.c b/src/meta/aifc.c index a3693a30..3dde4523 100644 --- a/src/meta/aifc.c +++ b/src/meta/aifc.c @@ -82,35 +82,39 @@ VGMSTREAM* init_vgmstream_aifc(STREAMFILE* sf) { /* checks */ if (!is_id32be(0x00,sf, "FORM")) - goto fail; + return NULL; - /* .aif: common (AIFF or AIFC), .aiff: common AIFF, .aifc: common AIFC - * .laif/laiff/laifc: for plugins + /* .aif: common (AIFF or AIFC) + * .wav: SimCity 3000 (Mac) (both AIFF and AIFC) + * (extensionless): Doom (3DO) + * + * .aifc: renamed AIFC? + * .afc: ? * .cbd2: M2 games * .bgm: Super Street Fighter II Turbo (3DO) + * .fda: Homeworld 2 (PC) + * .n64: Turok (N64) src + * .xa: SimCity 3000 (Mac) + * .caf: Topple (iOS) + * + * .aiff: renamed AIFF? * .acm: Crusader - No Remorse (SAT) * .adp: Sonic Jam (SAT) * .ai: Dragon Force (SAT) - * (extensionless: Doom (3DO) - * .fda: Homeworld 2 (PC) - * .n64: Turok (N64) src * .pcm: Road Rash (SAT) - * .wav: SimCity 3000 (Mac) (both AIFC and AIFF) - * .lwav: for media players that may confuse this format with the usual RIFF WAVE file. - * .xa: SimCity 3000 (Mac) */ if (check_extensions(sf, "aif,laif,wav,lwav,")) { is_aifc_ext = 1; is_aiff_ext = 1; } - else if (check_extensions(sf, "aifc,laifc,afc,cbd2,bgm,fda,n64,xa")) { + else if (check_extensions(sf, "aifc,laifc,afc,cbd2,bgm,fda,n64,xa,caf")) { is_aifc_ext = 1; } else if (check_extensions(sf, "aiff,laiff,acm,adp,ai,pcm")) { is_aiff_ext = 1; } else { - goto fail; + return NULL; } file_size = get_streamfile_size(sf); diff --git a/src/meta/ogg_vorbis.c b/src/meta/ogg_vorbis.c index 31a7ae6c..50d3f79f 100644 --- a/src/meta/ogg_vorbis.c +++ b/src/meta/ogg_vorbis.c @@ -507,7 +507,7 @@ static int _init_vgmstream_ogg_vorbis_tests(STREAMFILE* sf, ogg_vorbis_io_config } } - /* .um3: Ultramarine / Bruns Engine files */ + /* .um3: Ultramarine / Bruns Engine files */ if (check_extensions(sf,"um3")) { if (!is_id32be(0x00,sf, "OggS")) { ovmi->decryption_callback = um3_ogg_decryption_callback; @@ -644,69 +644,68 @@ static VGMSTREAM* _init_vgmstream_ogg_vorbis_config(STREAMFILE* sf, off_t start, while (ogg_vorbis_get_comment(data, &comment)) { ;VGM_LOG("OGG: user_comment=%s\n", comment); - if (strstr(comment,"loop_start=") == comment || /* Phantasy Star Online: Blue Burst (PC) (no loop_end pair) */ - strstr(comment,"LOOP_START=") == comment || /* Phantasy Star Online: Blue Burst (PC), common */ - strstr(comment,"LOOPPOINT=") == comment || /* Sonic Robo Blast 2 */ - strstr(comment,"COMMENT=LOOPPOINT=") == comment || - strstr(comment,"LOOPSTART=") == comment || - strstr(comment,"um3.stream.looppoint.start=") == comment || - strstr(comment,"LOOP_BEGIN=") == comment || /* Hatsune Miku: Project Diva F (PS3) */ - strstr(comment,"LoopStart=") == comment || /* Capcom games [Devil May Cry 4 (PC)] */ - strstr(comment,"LOOP=") == comment || /* Duke Nukem 3D: 20th Anniversary World Tour */ - strstr(comment,"XIPH_CUE_LOOPSTART=") == comment) { /* DeNa games [Super Mario Run (Android), FF Record Keeper (Android)] */ + if ( strstr(comment,"loop_start=") == comment /* Phantasy Star Online: Blue Burst (PC) (no loop_end pair) */ + || strstr(comment,"LOOP_START=") == comment /* Phantasy Star Online: Blue Burst (PC), common */ + || strstr(comment,"LOOPPOINT=") == comment /* Sonic Robo Blast 2 (PC) */ + || strstr(comment,"COMMENT=LOOPPOINT=") == comment + || strstr(comment,"LOOPSTART=") == comment /* common? */ + || strstr(comment,"um3.stream.looppoint.start=") == comment /* Ultramarine / Bruns Engine files */ + || strstr(comment,"LOOP_BEGIN=") == comment /* Hatsune Miku: Project Diva F (PS3) */ + || strstr(comment,"LoopStart=") == comment /* Capcom games [Devil May Cry 4 (PC)] */ + || strstr(comment,"LOOP=") == comment /* Duke Nukem 3D: 20th Anniversary World Tour */ + || strstr(comment,"XIPH_CUE_LOOPSTART=") == comment /* DeNa games [Super Mario Run (Android), FF Record Keeper (Android)] */ + || strstr(comment,"LOOPS=") == comment /* The Rumble Fish + (Switch) */ + ) { loop_start = atol(strrchr(comment,'=')+1); loop_flag = (loop_start >= 0); } - else if (strstr(comment,"LOOPLENGTH=") == comment) {/* (LOOPSTART pair) */ + else if (strstr(comment,"LOOPLENGTH=") == comment) { /* (LOOPSTART pair) */ loop_length = atol(strrchr(comment,'=')+1); loop_length_found = 1; } - else if (strstr(comment,"title=-lps") == comment) { /* KID [Memories Off #5 (PC), Remember11 (PC)] */ - loop_start = atol(comment+10); - loop_flag = (loop_start >= 0); - } - else if (strstr(comment,"album=-lpe") == comment) { /* (title=-lps pair) */ - loop_end = atol(comment+10); + else if ( strstr(comment,"LoopEnd=") == comment /* (LoopStart pair) */ + || strstr(comment,"LOOP_END=") == comment /* (LOOP_START/LOOP_BEGIN pair) */ + || strstr(comment, "XIPH_CUE_LOOPEND=") == comment /* (XIPH_CUE_LOOPSTART pair) */ + || strstr(comment, "LOOPE=") == comment /* (LOOPS pair) */ + ) { + loop_end = atol(strrchr(comment, '=') + 1); loop_end_found = 1; loop_flag = 1; } - else if (strstr(comment,"LoopEnd=") == comment) { /* (LoopStart pair) */ - loop_end = atol(strrchr(comment,'=')+1); - loop_end_found = 1; + else if (strstr(comment,"title=-lps") == comment) { /* KID [Memories Off #5 (PC), Remember11 (PC)] */ + loop_start = atol(comment+10); + loop_flag = (loop_start >= 0); } - else if (strstr(comment,"LOOP_END=") == comment) { /* (LOOP_START/LOOP_BEGIN pair) */ - loop_end = atol(strrchr(comment,'=')+1); + else if (strstr(comment,"album=-lpe") == comment) { /* (title=-lps pair) */ + loop_end = atol(comment+10); loop_end_found = 1; + loop_flag = 1; } else if (strstr(comment,"lp=") == comment) { sscanf(strrchr(comment,'=')+1,"%d,%d", &loop_start,&loop_end); loop_end_found = 1; loop_flag = 1; } - else if (strstr(comment,"LOOPDEFS=") == comment) { /* Fairy Fencer F: Advent Dark Force */ + else if (strstr(comment,"LOOPDEFS=") == comment) { /* Fairy Fencer F: Advent Dark Force */ sscanf(strrchr(comment,'=')+1,"%d,%d", &loop_start,&loop_end); - loop_flag = 1; loop_end_found = 1; + loop_flag = 1; } - else if (strstr(comment,"COMMENT=loop(") == comment) { /* Zero Time Dilemma (PC) */ + else if (strstr(comment,"COMMENT=loop(") == comment) { /* Zero Time Dilemma (PC) */ sscanf(strrchr(comment,'(')+1,"%d,%d", &loop_start,&loop_end); + loop_end_found = 1; loop_flag = 1; - loop_end_found = 1; } - else if (strstr(comment, "XIPH_CUE_LOOPEND=") == comment) { /* (XIPH_CUE_LOOPSTART pair) */ - loop_end = atol(strrchr(comment, '=') + 1); - loop_end_found = 1; - } - else if (strstr(comment, "omment=") == comment) { /* Air (Android) */ + else if (strstr(comment, "omment=") == comment) { /* Air (Android) */ sscanf(strstr(comment, "=LOOPSTART=") + 11, "%d,LOOPEND=%d", &loop_start, &loop_end); - loop_flag = 1; loop_end_found = 1; + loop_flag = 1; } - else if (strstr(comment,"MarkerNum=0002") == comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) flag */ + else if (strstr(comment,"MarkerNum=0002") == comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) flag */ /* uses LoopStart=-1 LoopEnd=-1, then 3 secuential comments: "MarkerNum" + "M=7F(start)" + "M=7F(end)" */ loop_flag = 1; } - else if (strstr(comment,"M=7F") == comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) start/end */ + else if (strstr(comment,"M=7F") == comment) { /* Megaman X Legacy Collection: MMX1/2/3 (PC) start/end */ if (loop_flag && loop_start < 0) { /* LoopStart should set as -1 before */ sscanf(comment,"M=7F%x", &loop_start); } @@ -715,7 +714,7 @@ static VGMSTREAM* _init_vgmstream_ogg_vorbis_config(STREAMFILE* sf, off_t start, loop_end_found = 1; } } - else if (strstr(comment,"LOOPMS=") == comment) { /* Sonic Robo Blast 2 (PC) */ + else if (strstr(comment,"LOOPMS=") == comment) { /* Sonic Robo Blast 2 (PC) */ loop_start = atol(strrchr(comment,'=')+1) * sample_rate / 1000; /* ms to samples */ loop_flag = (loop_start >= 0); } @@ -730,8 +729,7 @@ static VGMSTREAM* _init_vgmstream_ogg_vorbis_config(STREAMFILE* sf, off_t start, * loopTime nor have wrong granules though) */ force_seek = 1; } - - else if (strstr(comment,"COMMENT=*loopsample,") == comment) { /* Tsuki ni Yorisou Otome no Sahou (PC) */ + else if (strstr(comment,"COMMENT=*loopsample,") == comment) { /* Tsuki ni Yorisou Otome no Sahou (PC) */ int unk0; // always 0 (delay?) int unk1; // always -1 (loop flag? but non-looped files have no comment) int m = sscanf(comment,"COMMENT=*loopsample,%d,%d,%d,%d", &unk0, &loop_start, &loop_end, &unk1); diff --git a/src/meta/riff.c b/src/meta/riff.c index e574530b..0f1bb0e3 100644 --- a/src/meta/riff.c +++ b/src/meta/riff.c @@ -354,12 +354,12 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { /* checks*/ if (!is_id32be(0x00,sf,"RIFF")) - goto fail; + return NULL; riff_size = read_u32le(0x04,sf); if (!is_id32be(0x08,sf, "WAVE")) - goto fail; + return NULL; file_size = get_streamfile_size(sf); @@ -398,9 +398,10 @@ VGMSTREAM* init_vgmstream_riff(STREAMFILE* sf) { * .mus: Burnout Legends/Dominator (PSP) * .dat/ldat: RollerCoaster Tycoon 1/2 (PC) * .wma/lwma: SRS: Street Racing Syndicate (Xbox), Fast and the Furious (Xbox) + * .caf: Topple (iOS) */ - if (!check_extensions(sf, "wav,lwav,xwav,mwv,da,dax,cd,med,snd,adx,adp,xss,xsew,adpcm,adw,wd,,sbv,wvx,str,at3,rws,aud,at9,ckd,saf,ima,nsa,pcm,xvag,ogg,logg,p1d,xms,mus,dat,ldat,wma,lwma")) { - goto fail; + if (!check_extensions(sf, "wav,lwav,xwav,mwv,da,dax,cd,med,snd,adx,adp,xss,xsew,adpcm,adw,wd,,sbv,wvx,str,at3,rws,aud,at9,ckd,saf,ima,nsa,pcm,xvag,ogg,logg,p1d,xms,mus,dat,ldat,wma,lwma,caf")) { + return NULL; } /* some games have wonky sizes, selectively fix to catch bad rips and new mutations */ diff --git a/src/vgmstream.c b/src/vgmstream.c index 1f47c240..b8eeac44 100644 --- a/src/vgmstream.c +++ b/src/vgmstream.c @@ -951,6 +951,7 @@ static void try_dual_file_stereo(VGMSTREAM* opened_vgmstream, STREAMFILE* sf, in VGMSTREAM* new_vgmstream = NULL; STREAMFILE* dual_sf = NULL; int i,j, dfs_pair_count, extension_len, filename_len; + int sample_variance, loop_variance; if (opened_vgmstream->channels != 1) return; @@ -1021,14 +1022,11 @@ static void try_dual_file_stereo(VGMSTREAM* opened_vgmstream, STREAMFILE* sf, in new_vgmstream = init_vgmstream_function(dual_sf); /* use the init function that just worked */ close_streamfile(dual_sf); + if (!new_vgmstream) + goto fail; /* see if we were able to open the file, and if everything matched nicely */ - if (!(new_vgmstream && - new_vgmstream->channels == 1 && - /* we have seen legitimate pairs where these are off by one... - * but leaving it commented out until I can find those and recheck */ - /* abs(new_vgmstream->num_samples-opened_vgmstream->num_samples <= 1) && */ - new_vgmstream->num_samples == opened_vgmstream->num_samples && + if (!(new_vgmstream->channels == 1 && new_vgmstream->sample_rate == opened_vgmstream->sample_rate && new_vgmstream->meta_type == opened_vgmstream->meta_type && new_vgmstream->coding_type == opened_vgmstream->coding_type && @@ -1040,13 +1038,39 @@ static void try_dual_file_stereo(VGMSTREAM* opened_vgmstream, STREAMFILE* sf, in goto fail; } - /* check these even if there is no loop, because they should then be zero in both - * (Homura PS2 right channel doesn't have loop points so this check is ignored) */ - if (new_vgmstream->meta_type != meta_SMPL && - !(new_vgmstream->loop_flag == opened_vgmstream->loop_flag && - new_vgmstream->loop_start_sample== opened_vgmstream->loop_start_sample && - new_vgmstream->loop_end_sample == opened_vgmstream->loop_end_sample)) { - goto fail; + /* samples/loops should match even when there is no loop, except in special cases + * in the odd cases where values diverge, will use either L's loops or R's loops depending on which file is opened */ + if (new_vgmstream->meta_type == meta_SMPL) { + loop_variance = -1; /* right channel doesn't have loop points so this check is ignored [Homura (PS2)] */ + sample_variance = 0; + } + else if (new_vgmstream->meta_type == meta_DSP_STD && new_vgmstream->sample_rate <= 24000) { + loop_variance = 170000; /* rarely loop points are a bit apart, though usually only a few samples [Harvest Moon: Tree of Tranquility (Wii)] */ + sample_variance = opened_vgmstream->loop_flag ? 1600 : 700; /* less common but loops don't reach end */ + } + else { + loop_variance = 0; /* otherwise should match exactly */ + sample_variance = 0; + } + + { + int ns_variance = new_vgmstream->num_samples - opened_vgmstream->num_samples; + + /* either channel may be bigger */ + if (abs(ns_variance) > sample_variance) + goto fail; + } + + if (loop_variance >= 0) { + int ls_variance = new_vgmstream->loop_start_sample - opened_vgmstream->loop_start_sample; + int le_variance = new_vgmstream->loop_end_sample - opened_vgmstream->loop_end_sample; + + if (new_vgmstream->loop_flag != opened_vgmstream->loop_flag) + goto fail; + + /* either channel may be bigger */ + if (abs(ls_variance) > loop_variance || abs(le_variance) > loop_variance) + goto fail; } /* We seem to have a usable, matching file. Merge in the second channel. */