WordPress/WordPress

View on GitHub
wp-includes/ID3/module.tag.id3v2.php

Summary

Maintainability
F
1 mo
Test Coverage
<?php

/////////////////////////////////////////////////////////////////
/// getID3() by James Heinrich <info@getid3.org>               //
//  available at https://github.com/JamesHeinrich/getID3       //
//            or https://www.getid3.org                        //
//            or http://getid3.sourceforge.net                 //
//  see readme.txt for more details                            //
/////////////////////////////////////////////////////////////////
///                                                            //
// module.tag.id3v2.php                                        //
// module for analyzing ID3v2 tags                             //
// dependencies: module.tag.id3v1.php                          //
//                                                            ///
/////////////////////////////////////////////////////////////////

if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
    exit;
}
getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v1.php', __FILE__, true);

class getid3_id3v2 extends getid3_handler
{
    public $StartingOffset = 0;

    /**
     * @return bool
     */
    public function Analyze() {
        $info = &$this->getid3->info;

        //    Overall tag structure:
        //        +-----------------------------+
        //        |      Header (10 bytes)      |
        //        +-----------------------------+
        //        |       Extended Header       |
        //        | (variable length, OPTIONAL) |
        //        +-----------------------------+
        //        |   Frames (variable length)  |
        //        +-----------------------------+
        //        |           Padding           |
        //        | (variable length, OPTIONAL) |
        //        +-----------------------------+
        //        | Footer (10 bytes, OPTIONAL) |
        //        +-----------------------------+

        //    Header
        //        ID3v2/file identifier      "ID3"
        //        ID3v2 version              $04 00
        //        ID3v2 flags                (%ab000000 in v2.2, %abc00000 in v2.3, %abcd0000 in v2.4.x)
        //        ID3v2 size             4 * %0xxxxxxx


        // shortcuts
        $info['id3v2']['header'] = true;
        $thisfile_id3v2                  = &$info['id3v2'];
        $thisfile_id3v2['flags']         =  array();
        $thisfile_id3v2_flags            = &$thisfile_id3v2['flags'];


        $this->fseek($this->StartingOffset);
        $header = $this->fread(10);
        if (substr($header, 0, 3) == 'ID3'  &&  strlen($header) == 10) {

            $thisfile_id3v2['majorversion'] = ord($header[3]);
            $thisfile_id3v2['minorversion'] = ord($header[4]);

            // shortcut
            $id3v2_majorversion = &$thisfile_id3v2['majorversion'];

        } else {

            unset($info['id3v2']);
            return false;

        }

        if ($id3v2_majorversion > 4) { // this script probably won't correctly parse ID3v2.5.x and above (if it ever exists)

            $this->error('this script only parses up to ID3v2.4.x - this tag is ID3v2.'.$id3v2_majorversion.'.'.$thisfile_id3v2['minorversion']);
            return false;

        }

        $id3_flags = ord($header[5]);
        switch ($id3v2_majorversion) {
            case 2:
                // %ab000000 in v2.2
                $thisfile_id3v2_flags['unsynch']     = (bool) ($id3_flags & 0x80); // a - Unsynchronisation
                $thisfile_id3v2_flags['compression'] = (bool) ($id3_flags & 0x40); // b - Compression
                break;

            case 3:
                // %abc00000 in v2.3
                $thisfile_id3v2_flags['unsynch']     = (bool) ($id3_flags & 0x80); // a - Unsynchronisation
                $thisfile_id3v2_flags['exthead']     = (bool) ($id3_flags & 0x40); // b - Extended header
                $thisfile_id3v2_flags['experim']     = (bool) ($id3_flags & 0x20); // c - Experimental indicator
                break;

            case 4:
                // %abcd0000 in v2.4
                $thisfile_id3v2_flags['unsynch']     = (bool) ($id3_flags & 0x80); // a - Unsynchronisation
                $thisfile_id3v2_flags['exthead']     = (bool) ($id3_flags & 0x40); // b - Extended header
                $thisfile_id3v2_flags['experim']     = (bool) ($id3_flags & 0x20); // c - Experimental indicator
                $thisfile_id3v2_flags['isfooter']    = (bool) ($id3_flags & 0x10); // d - Footer present
                break;
        }

        $thisfile_id3v2['headerlength'] = getid3_lib::BigEndian2Int(substr($header, 6, 4), 1) + 10; // length of ID3v2 tag in 10-byte header doesn't include 10-byte header length

        $thisfile_id3v2['tag_offset_start'] = $this->StartingOffset;
        $thisfile_id3v2['tag_offset_end']   = $thisfile_id3v2['tag_offset_start'] + $thisfile_id3v2['headerlength'];



        // create 'encoding' key - used by getid3::HandleAllTags()
        // in ID3v2 every field can have it's own encoding type
        // so force everything to UTF-8 so it can be handled consistantly
        $thisfile_id3v2['encoding'] = 'UTF-8';


    //    Frames

    //        All ID3v2 frames consists of one frame header followed by one or more
    //        fields containing the actual information. The header is always 10
    //        bytes and laid out as follows:
    //
    //        Frame ID      $xx xx xx xx  (four characters)
    //        Size      4 * %0xxxxxxx
    //        Flags         $xx xx

        $sizeofframes = $thisfile_id3v2['headerlength'] - 10; // not including 10-byte initial header
        if (!empty($thisfile_id3v2['exthead']['length'])) {
            $sizeofframes -= ($thisfile_id3v2['exthead']['length'] + 4);
        }
        if (!empty($thisfile_id3v2_flags['isfooter'])) {
            $sizeofframes -= 10; // footer takes last 10 bytes of ID3v2 header, after frame data, before audio
        }
        if ($sizeofframes > 0) {

            $framedata = $this->fread($sizeofframes); // read all frames from file into $framedata variable

            //    if entire frame data is unsynched, de-unsynch it now (ID3v2.3.x)
            if (!empty($thisfile_id3v2_flags['unsynch']) && ($id3v2_majorversion <= 3)) {
                $framedata = $this->DeUnsynchronise($framedata);
            }
            //        [in ID3v2.4.0] Unsynchronisation [S:6.1] is done on frame level, instead
            //        of on tag level, making it easier to skip frames, increasing the streamability
            //        of the tag. The unsynchronisation flag in the header [S:3.1] indicates that
            //        there exists an unsynchronised frame, while the new unsynchronisation flag in
            //        the frame header [S:4.1.2] indicates unsynchronisation.


            //$framedataoffset = 10 + ($thisfile_id3v2['exthead']['length'] ? $thisfile_id3v2['exthead']['length'] + 4 : 0); // how many bytes into the stream - start from after the 10-byte header (and extended header length+4, if present)
            $framedataoffset = 10; // how many bytes into the stream - start from after the 10-byte header


            //    Extended Header
            if (!empty($thisfile_id3v2_flags['exthead'])) {
                $extended_header_offset = 0;

                if ($id3v2_majorversion == 3) {

                    // v2.3 definition:
                    //Extended header size  $xx xx xx xx   // 32-bit integer
                    //Extended Flags        $xx xx
                    //     %x0000000 %00000000 // v2.3
                    //     x - CRC data present
                    //Size of padding       $xx xx xx xx

                    $thisfile_id3v2['exthead']['length'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4), 0);
                    $extended_header_offset += 4;

                    $thisfile_id3v2['exthead']['flag_bytes'] = 2;
                    $thisfile_id3v2['exthead']['flag_raw'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, $thisfile_id3v2['exthead']['flag_bytes']));
                    $extended_header_offset += $thisfile_id3v2['exthead']['flag_bytes'];

                    $thisfile_id3v2['exthead']['flags']['crc'] = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x8000);

                    $thisfile_id3v2['exthead']['padding_size'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4));
                    $extended_header_offset += 4;

                    if ($thisfile_id3v2['exthead']['flags']['crc']) {
                        $thisfile_id3v2['exthead']['flag_data']['crc'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4));
                        $extended_header_offset += 4;
                    }
                    $extended_header_offset += $thisfile_id3v2['exthead']['padding_size'];

                } elseif ($id3v2_majorversion == 4) {

                    // v2.4 definition:
                    //Extended header size   4 * %0xxxxxxx // 28-bit synchsafe integer
                    //Number of flag bytes       $01
                    //Extended Flags             $xx
                    //     %0bcd0000 // v2.4
                    //     b - Tag is an update
                    //         Flag data length       $00
                    //     c - CRC data present
                    //         Flag data length       $05
                    //         Total frame CRC    5 * %0xxxxxxx
                    //     d - Tag restrictions
                    //         Flag data length       $01

                    $thisfile_id3v2['exthead']['length'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4), true);
                    $extended_header_offset += 4;

                    $thisfile_id3v2['exthead']['flag_bytes'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should always be 1
                    $extended_header_offset += 1;

                    $thisfile_id3v2['exthead']['flag_raw'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, $thisfile_id3v2['exthead']['flag_bytes']));
                    $extended_header_offset += $thisfile_id3v2['exthead']['flag_bytes'];

                    $thisfile_id3v2['exthead']['flags']['update']       = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x40);
                    $thisfile_id3v2['exthead']['flags']['crc']          = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x20);
                    $thisfile_id3v2['exthead']['flags']['restrictions'] = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x10);

                    if ($thisfile_id3v2['exthead']['flags']['update']) {
                        $ext_header_chunk_length = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should be 0
                        $extended_header_offset += 1;
                    }

                    if ($thisfile_id3v2['exthead']['flags']['crc']) {
                        $ext_header_chunk_length = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should be 5
                        $extended_header_offset += 1;
                        $thisfile_id3v2['exthead']['flag_data']['crc'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, $ext_header_chunk_length), true, false);
                        $extended_header_offset += $ext_header_chunk_length;
                    }

                    if ($thisfile_id3v2['exthead']['flags']['restrictions']) {
                        $ext_header_chunk_length = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should be 1
                        $extended_header_offset += 1;

                        // %ppqrrstt
                        $restrictions_raw = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1));
                        $extended_header_offset += 1;
                        $thisfile_id3v2['exthead']['flags']['restrictions']['tagsize']  = ($restrictions_raw & 0xC0) >> 6; // p - Tag size restrictions
                        $thisfile_id3v2['exthead']['flags']['restrictions']['textenc']  = ($restrictions_raw & 0x20) >> 5; // q - Text encoding restrictions
                        $thisfile_id3v2['exthead']['flags']['restrictions']['textsize'] = ($restrictions_raw & 0x18) >> 3; // r - Text fields size restrictions
                        $thisfile_id3v2['exthead']['flags']['restrictions']['imgenc']   = ($restrictions_raw & 0x04) >> 2; // s - Image encoding restrictions
                        $thisfile_id3v2['exthead']['flags']['restrictions']['imgsize']  = ($restrictions_raw & 0x03) >> 0; // t - Image size restrictions

                        $thisfile_id3v2['exthead']['flags']['restrictions_text']['tagsize']  = $this->LookupExtendedHeaderRestrictionsTagSizeLimits($thisfile_id3v2['exthead']['flags']['restrictions']['tagsize']);
                        $thisfile_id3v2['exthead']['flags']['restrictions_text']['textenc']  = $this->LookupExtendedHeaderRestrictionsTextEncodings($thisfile_id3v2['exthead']['flags']['restrictions']['textenc']);
                        $thisfile_id3v2['exthead']['flags']['restrictions_text']['textsize'] = $this->LookupExtendedHeaderRestrictionsTextFieldSize($thisfile_id3v2['exthead']['flags']['restrictions']['textsize']);
                        $thisfile_id3v2['exthead']['flags']['restrictions_text']['imgenc']   = $this->LookupExtendedHeaderRestrictionsImageEncoding($thisfile_id3v2['exthead']['flags']['restrictions']['imgenc']);
                        $thisfile_id3v2['exthead']['flags']['restrictions_text']['imgsize']  = $this->LookupExtendedHeaderRestrictionsImageSizeSize($thisfile_id3v2['exthead']['flags']['restrictions']['imgsize']);
                    }

                    if ($thisfile_id3v2['exthead']['length'] != $extended_header_offset) {
                        $this->warning('ID3v2.4 extended header length mismatch (expecting '.intval($thisfile_id3v2['exthead']['length']).', found '.intval($extended_header_offset).')');
                    }
                }

                $framedataoffset += $extended_header_offset;
                $framedata = substr($framedata, $extended_header_offset);
            } // end extended header


            while (isset($framedata) && (strlen($framedata) > 0)) { // cycle through until no more frame data is left to parse
                if (strlen($framedata) <= $this->ID3v2HeaderLength($id3v2_majorversion)) {
                    // insufficient room left in ID3v2 header for actual data - must be padding
                    $thisfile_id3v2['padding']['start']  = $framedataoffset;
                    $thisfile_id3v2['padding']['length'] = strlen($framedata);
                    $thisfile_id3v2['padding']['valid']  = true;
                    for ($i = 0; $i < $thisfile_id3v2['padding']['length']; $i++) {
                        if ($framedata[$i] != "\x00") {
                            $thisfile_id3v2['padding']['valid'] = false;
                            $thisfile_id3v2['padding']['errorpos'] = $thisfile_id3v2['padding']['start'] + $i;
                            $this->warning('Invalid ID3v2 padding found at offset '.$thisfile_id3v2['padding']['errorpos'].' (the remaining '.($thisfile_id3v2['padding']['length'] - $i).' bytes are considered invalid)');
                            break;
                        }
                    }
                    break; // skip rest of ID3v2 header
                }
                $frame_header = null;
                $frame_name   = null;
                $frame_size   = null;
                $frame_flags  = null;
                if ($id3v2_majorversion == 2) {
                    // Frame ID  $xx xx xx (three characters)
                    // Size      $xx xx xx (24-bit integer)
                    // Flags     $xx xx

                    $frame_header = substr($framedata, 0, 6); // take next 6 bytes for header
                    $framedata    = substr($framedata, 6);    // and leave the rest in $framedata
                    $frame_name   = substr($frame_header, 0, 3);
                    $frame_size   = getid3_lib::BigEndian2Int(substr($frame_header, 3, 3), 0);
                    $frame_flags  = 0; // not used for anything in ID3v2.2, just set to avoid E_NOTICEs

                } elseif ($id3v2_majorversion > 2) {

                    // Frame ID  $xx xx xx xx (four characters)
                    // Size      $xx xx xx xx (32-bit integer in v2.3, 28-bit synchsafe in v2.4+)
                    // Flags     $xx xx

                    $frame_header = substr($framedata, 0, 10); // take next 10 bytes for header
                    $framedata    = substr($framedata, 10);    // and leave the rest in $framedata

                    $frame_name = substr($frame_header, 0, 4);
                    if ($id3v2_majorversion == 3) {
                        $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 0); // 32-bit integer
                    } else { // ID3v2.4+
                        $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 1); // 32-bit synchsafe integer (28-bit value)
                    }

                    if ($frame_size < (strlen($framedata) + 4)) {
                        $nextFrameID = substr($framedata, $frame_size, 4);
                        if ($this->IsValidID3v2FrameName($nextFrameID, $id3v2_majorversion)) {
                            // next frame is OK
                        } elseif (($frame_name == "\x00".'MP3') || ($frame_name == "\x00\x00".'MP') || ($frame_name == ' MP3') || ($frame_name == 'MP3e')) {
                            // MP3ext known broken frames - "ok" for the purposes of this test
                        } elseif (($id3v2_majorversion == 4) && ($this->IsValidID3v2FrameName(substr($framedata, getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 0), 4), 3))) {
                            $this->warning('ID3v2 tag written as ID3v2.4, but with non-synchsafe integers (ID3v2.3 style). Older versions of (Helium2; iTunes) are known culprits of this. Tag has been parsed as ID3v2.3');
                            $id3v2_majorversion = 3;
                            $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 0); // 32-bit integer
                        }
                    }


                    $frame_flags = getid3_lib::BigEndian2Int(substr($frame_header, 8, 2));
                }

                if ((($id3v2_majorversion == 2) && ($frame_name == "\x00\x00\x00")) || ($frame_name == "\x00\x00\x00\x00")) {
                    // padding encountered

                    $thisfile_id3v2['padding']['start']  = $framedataoffset;
                    $thisfile_id3v2['padding']['length'] = strlen($frame_header) + strlen($framedata);
                    $thisfile_id3v2['padding']['valid']  = true;

                    $len = strlen($framedata);
                    for ($i = 0; $i < $len; $i++) {
                        if ($framedata[$i] != "\x00") {
                            $thisfile_id3v2['padding']['valid'] = false;
                            $thisfile_id3v2['padding']['errorpos'] = $thisfile_id3v2['padding']['start'] + $i;
                            $this->warning('Invalid ID3v2 padding found at offset '.$thisfile_id3v2['padding']['errorpos'].' (the remaining '.($thisfile_id3v2['padding']['length'] - $i).' bytes are considered invalid)');
                            break;
                        }
                    }
                    break; // skip rest of ID3v2 header
                }

                if ($iTunesBrokenFrameNameFixed = self::ID3v22iTunesBrokenFrameName($frame_name)) {
                    $this->warning('error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: IsValidID3v2FrameName("'.str_replace("\x00", ' ', $frame_name).'", '.$id3v2_majorversion.'))). [Note: this particular error has been known to happen with tags edited by iTunes (versions "X v2.0.3", "v3.0.1", "v7.0.0.70" are known-guilty, probably others too)]. Translated frame name from "'.str_replace("\x00", ' ', $frame_name).'" to "'.$iTunesBrokenFrameNameFixed.'" for parsing.');
                    $frame_name = $iTunesBrokenFrameNameFixed;
                }
                if (($frame_size <= strlen($framedata)) && ($this->IsValidID3v2FrameName($frame_name, $id3v2_majorversion))) {

                    $parsedFrame                    = array();
                    $parsedFrame['frame_name']      = $frame_name;
                    $parsedFrame['frame_flags_raw'] = $frame_flags;
                    $parsedFrame['data']            = substr($framedata, 0, $frame_size);
                    $parsedFrame['datalength']      = getid3_lib::CastAsInt($frame_size);
                    $parsedFrame['dataoffset']      = $framedataoffset;

                    $this->ParseID3v2Frame($parsedFrame);
                    $thisfile_id3v2[$frame_name][] = $parsedFrame;

                    $framedata = substr($framedata, $frame_size);

                } else { // invalid frame length or FrameID

                    if ($frame_size <= strlen($framedata)) {

                        if ($this->IsValidID3v2FrameName(substr($framedata, $frame_size, 4), $id3v2_majorversion)) {

                            // next frame is valid, just skip the current frame
                            $framedata = substr($framedata, $frame_size);
                            $this->warning('Next ID3v2 frame is valid, skipping current frame.');

                        } else {

                            // next frame is invalid too, abort processing
                            //unset($framedata);
                            $framedata = null;
                            $this->error('Next ID3v2 frame is also invalid, aborting processing.');

                        }

                    } elseif ($frame_size == strlen($framedata)) {

                        // this is the last frame, just skip
                        $this->warning('This was the last ID3v2 frame.');

                    } else {

                        // next frame is invalid too, abort processing
                        //unset($framedata);
                        $framedata = null;
                        $this->warning('Invalid ID3v2 frame size, aborting.');

                    }
                    if (!$this->IsValidID3v2FrameName($frame_name, $id3v2_majorversion)) {

                        switch ($frame_name) {
                            case "\x00\x00".'MP':
                            case "\x00".'MP3':
                            case ' MP3':
                            case 'MP3e':
                            case "\x00".'MP':
                            case ' MP':
                            case 'MP3':
                                $this->warning('error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace("\x00", ' ', $frame_name).'", '.$id3v2_majorversion.'))). [Note: this particular error has been known to happen with tags edited by "MP3ext (www.mutschler.de/mp3ext/)"]');
                                break;

                            default:
                                $this->warning('error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace("\x00", ' ', $frame_name).'", '.$id3v2_majorversion.'))).');
                                break;
                        }

                    } elseif (!isset($framedata) || ($frame_size > strlen($framedata))) {

                        $this->error('error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: $frame_size ('.$frame_size.') > strlen($framedata) ('.(isset($framedata) ? strlen($framedata) : 'null').')).');

                    } else {

                        $this->error('error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag).');

                    }

                }
                $framedataoffset += ($frame_size + $this->ID3v2HeaderLength($id3v2_majorversion));

            }

        }


    //    Footer

    //    The footer is a copy of the header, but with a different identifier.
    //        ID3v2 identifier           "3DI"
    //        ID3v2 version              $04 00
    //        ID3v2 flags                %abcd0000
    //        ID3v2 size             4 * %0xxxxxxx

        if (isset($thisfile_id3v2_flags['isfooter']) && $thisfile_id3v2_flags['isfooter']) {
            $footer = $this->fread(10);
            if (substr($footer, 0, 3) == '3DI') {
                $thisfile_id3v2['footer'] = true;
                $thisfile_id3v2['majorversion_footer'] = ord($footer[3]);
                $thisfile_id3v2['minorversion_footer'] = ord($footer[4]);
            }
            if ($thisfile_id3v2['majorversion_footer'] <= 4) {
                $id3_flags = ord($footer[5]);
                $thisfile_id3v2_flags['unsynch_footer']  = (bool) ($id3_flags & 0x80);
                $thisfile_id3v2_flags['extfoot_footer']  = (bool) ($id3_flags & 0x40);
                $thisfile_id3v2_flags['experim_footer']  = (bool) ($id3_flags & 0x20);
                $thisfile_id3v2_flags['isfooter_footer'] = (bool) ($id3_flags & 0x10);

                $thisfile_id3v2['footerlength'] = getid3_lib::BigEndian2Int(substr($footer, 6, 4), 1);
            }
        } // end footer

        if (isset($thisfile_id3v2['comments']['genre'])) {
            $genres = array();
            foreach ($thisfile_id3v2['comments']['genre'] as $key => $value) {
                foreach ($this->ParseID3v2GenreString($value) as $genre) {
                    $genres[] = $genre;
                }
            }
            $thisfile_id3v2['comments']['genre'] = array_unique($genres);
            unset($key, $value, $genres, $genre);
        }

        if (isset($thisfile_id3v2['comments']['track_number'])) {
            foreach ($thisfile_id3v2['comments']['track_number'] as $key => $value) {
                if (strstr($value, '/')) {
                    list($thisfile_id3v2['comments']['track_number'][$key], $thisfile_id3v2['comments']['totaltracks'][$key]) = explode('/', $thisfile_id3v2['comments']['track_number'][$key]);
                }
            }
        }

        if (!isset($thisfile_id3v2['comments']['year']) && !empty($thisfile_id3v2['comments']['recording_time'][0]) && preg_match('#^([0-9]{4})#', trim($thisfile_id3v2['comments']['recording_time'][0]), $matches)) {
            $thisfile_id3v2['comments']['year'] = array($matches[1]);
        }


        if (!empty($thisfile_id3v2['TXXX'])) {
            // MediaMonkey does this, maybe others: write a blank RGAD frame, but put replay-gain adjustment values in TXXX frames
            foreach ($thisfile_id3v2['TXXX'] as $txxx_array) {
                switch ($txxx_array['description']) {
                    case 'replaygain_track_gain':
                        if (empty($info['replay_gain']['track']['adjustment']) && !empty($txxx_array['data'])) {
                            $info['replay_gain']['track']['adjustment'] = floatval(trim(str_replace('dB', '', $txxx_array['data'])));
                        }
                        break;
                    case 'replaygain_track_peak':
                        if (empty($info['replay_gain']['track']['peak']) && !empty($txxx_array['data'])) {
                            $info['replay_gain']['track']['peak'] = floatval($txxx_array['data']);
                        }
                        break;
                    case 'replaygain_album_gain':
                        if (empty($info['replay_gain']['album']['adjustment']) && !empty($txxx_array['data'])) {
                            $info['replay_gain']['album']['adjustment'] = floatval(trim(str_replace('dB', '', $txxx_array['data'])));
                        }
                        break;
                }
            }
        }


        // Set avdataoffset
        $info['avdataoffset'] = $thisfile_id3v2['headerlength'];
        if (isset($thisfile_id3v2['footer'])) {
            $info['avdataoffset'] += 10;
        }

        return true;
    }

    /**
     * @param string $genrestring
     *
     * @return array
     */
    public function ParseID3v2GenreString($genrestring) {
        // Parse genres into arrays of genreName and genreID
        // ID3v2.2.x, ID3v2.3.x: '(21)' or '(4)Eurodisco' or '(51)(39)' or '(55)((I think...)'
        // ID3v2.4.x: '21' $00 'Eurodisco' $00
        $clean_genres = array();

        // hack-fixes for some badly-written ID3v2.3 taggers, while trying not to break correctly-written tags
        if (($this->getid3->info['id3v2']['majorversion'] == 3) && !preg_match('#[\x00]#', $genrestring)) {
            // note: MusicBrainz Picard incorrectly stores plaintext genres separated by "/" when writing in ID3v2.3 mode, hack-fix here:
            // replace / with NULL, then replace back the two ID3v1 genres that legitimately have "/" as part of the single genre name
            if (strpos($genrestring, '/') !== false) {
                $LegitimateSlashedGenreList = array(  // https://github.com/JamesHeinrich/getID3/issues/223
                    'Pop/Funk',    // ID3v1 genre #62 - https://en.wikipedia.org/wiki/ID3#standard
                    'Cut-up/DJ',   // Discogs - https://www.discogs.com/style/cut-up/dj
                    'RnB/Swing',   // Discogs - https://www.discogs.com/style/rnb/swing
                    'Funk / Soul', // Discogs (note spaces) - https://www.discogs.com/genre/funk+%2F+soul
                );
                $genrestring = str_replace('/', "\x00", $genrestring);
                foreach ($LegitimateSlashedGenreList as $SlashedGenre) {
                    $genrestring = str_ireplace(str_replace('/', "\x00", $SlashedGenre), $SlashedGenre, $genrestring);
                }
            }

            // some other taggers separate multiple genres with semicolon, e.g. "Heavy Metal;Thrash Metal;Metal"
            if (strpos($genrestring, ';') !== false) {
                $genrestring = str_replace(';', "\x00", $genrestring);
            }
        }


        if (strpos($genrestring, "\x00") === false) {
            $genrestring = preg_replace('#\(([0-9]{1,3})\)#', '$1'."\x00", $genrestring);
        }

        $genre_elements = explode("\x00", $genrestring);
        foreach ($genre_elements as $element) {
            $element = trim($element);
            if ($element) {
                if (preg_match('#^[0-9]{1,3}$#', $element)) {
                    $clean_genres[] = getid3_id3v1::LookupGenreName($element);
                } else {
                    $clean_genres[] = str_replace('((', '(', $element);
                }
            }
        }
        return $clean_genres;
    }

    /**
     * @param array $parsedFrame
     *
     * @return bool
     */
    public function ParseID3v2Frame(&$parsedFrame) {

        // shortcuts
        $info = &$this->getid3->info;
        $id3v2_majorversion = $info['id3v2']['majorversion'];

        $parsedFrame['framenamelong']  = $this->FrameNameLongLookup($parsedFrame['frame_name']);
        if (empty($parsedFrame['framenamelong'])) {
            unset($parsedFrame['framenamelong']);
        }
        $parsedFrame['framenameshort'] = $this->FrameNameShortLookup($parsedFrame['frame_name']);
        if (empty($parsedFrame['framenameshort'])) {
            unset($parsedFrame['framenameshort']);
        }

        if ($id3v2_majorversion >= 3) { // frame flags are not part of the ID3v2.2 standard
            if ($id3v2_majorversion == 3) {
                //    Frame Header Flags
                //    %abc00000 %ijk00000
                $parsedFrame['flags']['TagAlterPreservation']  = (bool) ($parsedFrame['frame_flags_raw'] & 0x8000); // a - Tag alter preservation
                $parsedFrame['flags']['FileAlterPreservation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x4000); // b - File alter preservation
                $parsedFrame['flags']['ReadOnly']              = (bool) ($parsedFrame['frame_flags_raw'] & 0x2000); // c - Read only
                $parsedFrame['flags']['compression']           = (bool) ($parsedFrame['frame_flags_raw'] & 0x0080); // i - Compression
                $parsedFrame['flags']['Encryption']            = (bool) ($parsedFrame['frame_flags_raw'] & 0x0040); // j - Encryption
                $parsedFrame['flags']['GroupingIdentity']      = (bool) ($parsedFrame['frame_flags_raw'] & 0x0020); // k - Grouping identity

            } elseif ($id3v2_majorversion == 4) {
                //    Frame Header Flags
                //    %0abc0000 %0h00kmnp
                $parsedFrame['flags']['TagAlterPreservation']  = (bool) ($parsedFrame['frame_flags_raw'] & 0x4000); // a - Tag alter preservation
                $parsedFrame['flags']['FileAlterPreservation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x2000); // b - File alter preservation
                $parsedFrame['flags']['ReadOnly']              = (bool) ($parsedFrame['frame_flags_raw'] & 0x1000); // c - Read only
                $parsedFrame['flags']['GroupingIdentity']      = (bool) ($parsedFrame['frame_flags_raw'] & 0x0040); // h - Grouping identity
                $parsedFrame['flags']['compression']           = (bool) ($parsedFrame['frame_flags_raw'] & 0x0008); // k - Compression
                $parsedFrame['flags']['Encryption']            = (bool) ($parsedFrame['frame_flags_raw'] & 0x0004); // m - Encryption
                $parsedFrame['flags']['Unsynchronisation']     = (bool) ($parsedFrame['frame_flags_raw'] & 0x0002); // n - Unsynchronisation
                $parsedFrame['flags']['DataLengthIndicator']   = (bool) ($parsedFrame['frame_flags_raw'] & 0x0001); // p - Data length indicator

                // Frame-level de-unsynchronisation - ID3v2.4
                if ($parsedFrame['flags']['Unsynchronisation']) {
                    $parsedFrame['data'] = $this->DeUnsynchronise($parsedFrame['data']);
                }

                if ($parsedFrame['flags']['DataLengthIndicator']) {
                    $parsedFrame['data_length_indicator'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 0, 4), 1);
                    $parsedFrame['data']                  =                           substr($parsedFrame['data'], 4);
                }
            }

            //    Frame-level de-compression
            if ($parsedFrame['flags']['compression']) {
                $parsedFrame['decompressed_size'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 0, 4));
                if (!function_exists('gzuncompress')) {
                    $this->warning('gzuncompress() support required to decompress ID3v2 frame "'.$parsedFrame['frame_name'].'"');
                } else {
                    if ($decompresseddata = @gzuncompress(substr($parsedFrame['data'], 4))) {
                    //if ($decompresseddata = @gzuncompress($parsedFrame['data'])) {
                        $parsedFrame['data'] = $decompresseddata;
                        unset($decompresseddata);
                    } else {
                        $this->warning('gzuncompress() failed on compressed contents of ID3v2 frame "'.$parsedFrame['frame_name'].'"');
                    }
                }
            }
        }

        if (!empty($parsedFrame['flags']['DataLengthIndicator'])) {
            if ($parsedFrame['data_length_indicator'] != strlen($parsedFrame['data'])) {
                $this->warning('ID3v2 frame "'.$parsedFrame['frame_name'].'" should be '.$parsedFrame['data_length_indicator'].' bytes long according to DataLengthIndicator, but found '.strlen($parsedFrame['data']).' bytes of data');
            }
        }

        if (isset($parsedFrame['datalength']) && ($parsedFrame['datalength'] == 0)) {

            $warning = 'Frame "'.$parsedFrame['frame_name'].'" at offset '.$parsedFrame['dataoffset'].' has no data portion';
            switch ($parsedFrame['frame_name']) {
                case 'WCOM':
                    $warning .= ' (this is known to happen with files tagged by RioPort)';
                    break;

                default:
                    break;
            }
            $this->warning($warning);

        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'UFID')) || // 4.1   UFID Unique file identifier
            (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'UFI'))) {  // 4.1   UFI  Unique file identifier
            //   There may be more than one 'UFID' frame in a tag,
            //   but only one with the same 'Owner identifier'.
            // <Header for 'Unique file identifier', ID: 'UFID'>
            // Owner identifier        <text string> $00
            // Identifier              <up to 64 bytes binary data>
            $exploded = explode("\x00", $parsedFrame['data'], 2);
            $parsedFrame['ownerid'] = (isset($exploded[0]) ? $exploded[0] : '');
            $parsedFrame['data']    = (isset($exploded[1]) ? $exploded[1] : '');

        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'TXXX')) || // 4.2.2 TXXX User defined text information frame
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'TXX'))) {    // 4.2.2 TXX  User defined text information frame
            //   There may be more than one 'TXXX' frame in each tag,
            //   but only one with the same description.
            // <Header for 'User defined text information frame', ID: 'TXXX'>
            // Text encoding     $xx
            // Description       <text string according to encoding> $00 (00)
            // Value             <text string according to encoding>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }
            $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
            if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
            }
            $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
            $parsedFrame['encodingid']  = $frame_textencoding;
            $parsedFrame['encoding']    = $this->TextEncodingNameLookup($frame_textencoding);

            $parsedFrame['description'] = trim(getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['description']));
            $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator));
            $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $frame_textencoding_terminator);
            if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                $commentkey = ($parsedFrame['description'] ? $parsedFrame['description'] : (isset($info['id3v2']['comments'][$parsedFrame['framenameshort']]) ? count($info['id3v2']['comments'][$parsedFrame['framenameshort']]) : 0));
                if (!isset($info['id3v2']['comments'][$parsedFrame['framenameshort']]) || !array_key_exists($commentkey, $info['id3v2']['comments'][$parsedFrame['framenameshort']])) {
                    $info['id3v2']['comments'][$parsedFrame['framenameshort']][$commentkey] = trim(getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']));
                } else {
                    $info['id3v2']['comments'][$parsedFrame['framenameshort']][]            = trim(getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']));
                }
            }
            //unset($parsedFrame['data']); do not unset, may be needed elsewhere, e.g. for replaygain


        } elseif ($parsedFrame['frame_name'][0] == 'T') { // 4.2. T??[?] Text information frame
            //   There may only be one text information frame of its kind in an tag.
            // <Header for 'Text information frame', ID: 'T000' - 'TZZZ',
            // excluding 'TXXX' described in 4.2.6.>
            // Text encoding                $xx
            // Information                  <text string(s) according to encoding>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
            }

            $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset);
            $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding));

            $parsedFrame['encodingid'] = $frame_textencoding;
            $parsedFrame['encoding']   = $this->TextEncodingNameLookup($frame_textencoding);
            if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                // ID3v2.3 specs say that TPE1 (and others) can contain multiple artist values separated with /
                // This of course breaks when an artist name contains slash character, e.g. "AC/DC"
                // MP3tag (maybe others) implement alternative system where multiple artists are null-separated, which makes more sense
                // getID3 will split null-separated artists into multiple artists and leave slash-separated ones to the user
                switch ($parsedFrame['encoding']) {
                    case 'UTF-16':
                    case 'UTF-16BE':
                    case 'UTF-16LE':
                        $wordsize = 2;
                        break;
                    case 'ISO-8859-1':
                    case 'UTF-8':
                    default:
                        $wordsize = 1;
                        break;
                }
                $Txxx_elements = array();
                $Txxx_elements_start_offset = 0;
                for ($i = 0; $i < strlen($parsedFrame['data']); $i += $wordsize) {
                    if (substr($parsedFrame['data'], $i, $wordsize) == str_repeat("\x00", $wordsize)) {
                        $Txxx_elements[] = substr($parsedFrame['data'], $Txxx_elements_start_offset, $i - $Txxx_elements_start_offset);
                        $Txxx_elements_start_offset = $i + $wordsize;
                    }
                }
                $Txxx_elements[] = substr($parsedFrame['data'], $Txxx_elements_start_offset, $i - $Txxx_elements_start_offset);
                foreach ($Txxx_elements as $Txxx_element) {
                    $string = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $Txxx_element);
                    if (!empty($string)) {
                        $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $string;
                    }
                }
                unset($string, $wordsize, $i, $Txxx_elements, $Txxx_element, $Txxx_elements_start_offset);
            }

        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'WXXX')) || // 4.3.2 WXXX User defined URL link frame
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'WXX'))) {    // 4.3.2 WXX  User defined URL link frame
            //   There may be more than one 'WXXX' frame in each tag,
            //   but only one with the same description
            // <Header for 'User defined URL link frame', ID: 'WXXX'>
            // Text encoding     $xx
            // Description       <text string according to encoding> $00 (00)
            // URL               <text string>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }
            $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
            if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
            }
            $parsedFrame['encodingid']  = $frame_textencoding;
            $parsedFrame['encoding']    = $this->TextEncodingNameLookup($frame_textencoding);
            $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);           // according to the frame text encoding
            $parsedFrame['url']         = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); // always ISO-8859-1
            $parsedFrame['description'] = $this->RemoveStringTerminator($parsedFrame['description'], $frame_textencoding_terminator);
            $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);

            if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) {
                $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback('ISO-8859-1', $info['id3v2']['encoding'], $parsedFrame['url']);
            }
            unset($parsedFrame['data']);


        } elseif ($parsedFrame['frame_name'][0] == 'W') { // 4.3. W??? URL link frames
            //   There may only be one URL link frame of its kind in a tag,
            //   except when stated otherwise in the frame description
            // <Header for 'URL link frame', ID: 'W000' - 'WZZZ', excluding 'WXXX'
            // described in 4.3.2.>
            // URL              <text string>

            $parsedFrame['url'] = trim($parsedFrame['data']); // always ISO-8859-1
            if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) {
                $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback('ISO-8859-1', $info['id3v2']['encoding'], $parsedFrame['url']);
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'IPLS')) || // 4.4  IPLS Involved people list (ID3v2.3 only)
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'IPL'))) {     // 4.4  IPL  Involved people list (ID3v2.2 only)
            // http://id3.org/id3v2.3.0#sec4.4
            //   There may only be one 'IPL' frame in each tag
            // <Header for 'User defined URL link frame', ID: 'IPL'>
            // Text encoding     $xx
            // People list strings    <textstrings>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
            }
            $parsedFrame['encodingid'] = $frame_textencoding;
            $parsedFrame['encoding']   = $this->TextEncodingNameLookup($parsedFrame['encodingid']);
            $parsedFrame['data_raw']   = (string) substr($parsedFrame['data'], $frame_offset);

            // https://www.getid3.org/phpBB3/viewtopic.php?t=1369
            // "this tag typically contains null terminated strings, which are associated in pairs"
            // "there are users that use the tag incorrectly"
            $IPLS_parts = array();
            if (strpos($parsedFrame['data_raw'], "\x00") !== false) {
                $IPLS_parts_unsorted = array();
                if (((strlen($parsedFrame['data_raw']) % 2) == 0) && ((substr($parsedFrame['data_raw'], 0, 2) == "\xFF\xFE") || (substr($parsedFrame['data_raw'], 0, 2) == "\xFE\xFF"))) {
                    // UTF-16, be careful looking for null bytes since most 2-byte characters may contain one; you need to find twin null bytes, and on even padding
                    $thisILPS  = '';
                    for ($i = 0; $i < strlen($parsedFrame['data_raw']); $i += 2) {
                        $twobytes = substr($parsedFrame['data_raw'], $i, 2);
                        if ($twobytes === "\x00\x00") {
                            $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS);
                            $thisILPS  = '';
                        } else {
                            $thisILPS .= $twobytes;
                        }
                    }
                    if (strlen($thisILPS) > 2) { // 2-byte BOM
                        $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS);
                    }
                } else {
                    // ISO-8859-1 or UTF-8 or other single-byte-null character set
                    $IPLS_parts_unsorted = explode("\x00", $parsedFrame['data_raw']);
                }
                if (count($IPLS_parts_unsorted) == 1) {
                    // just a list of names, e.g. "Dino Baptiste, Jimmy Copley, John Gordon, Bernie Marsden, Sharon Watson"
                    foreach ($IPLS_parts_unsorted as $key => $value) {
                        $IPLS_parts_sorted = preg_split('#[;,\\r\\n\\t]#', $value);
                        $position = '';
                        foreach ($IPLS_parts_sorted as $person) {
                            $IPLS_parts[] = array('position'=>$position, 'person'=>$person);
                        }
                    }
                } elseif ((count($IPLS_parts_unsorted) % 2) == 0) {
                    $position = '';
                    $person   = '';
                    foreach ($IPLS_parts_unsorted as $key => $value) {
                        if (($key % 2) == 0) {
                            $position = $value;
                        } else {
                            $person   = $value;
                            $IPLS_parts[] = array('position'=>$position, 'person'=>$person);
                            $position = '';
                            $person   = '';
                        }
                    }
                } else {
                    foreach ($IPLS_parts_unsorted as $key => $value) {
                        $IPLS_parts[] = array($value);
                    }
                }

            } else {
                $IPLS_parts = preg_split('#[;,\\r\\n\\t]#', $parsedFrame['data_raw']);
            }
            $parsedFrame['data'] = $IPLS_parts;

            if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['data'];
            }


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'MCDI')) || // 4.4   MCDI Music CD identifier
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'MCI'))) {     // 4.5   MCI  Music CD identifier
            //   There may only be one 'MCDI' frame in each tag
            // <Header for 'Music CD identifier', ID: 'MCDI'>
            // CD TOC                <binary data>

            if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['data'];
            }


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'ETCO')) || // 4.5   ETCO Event timing codes
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'ETC'))) {     // 4.6   ETC  Event timing codes
            //   There may only be one 'ETCO' frame in each tag
            // <Header for 'Event timing codes', ID: 'ETCO'>
            // Time stamp format    $xx
            //   Where time stamp format is:
            // $01  (32-bit value) MPEG frames from beginning of file
            // $02  (32-bit value) milliseconds from beginning of file
            //   Followed by a list of key events in the following format:
            // Type of event   $xx
            // Time stamp      $xx (xx ...)
            //   The 'Time stamp' is set to zero if directly at the beginning of the sound
            //   or after the previous event. All events MUST be sorted in chronological order.

            $frame_offset = 0;
            $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));

            while ($frame_offset < strlen($parsedFrame['data'])) {
                $parsedFrame['typeid']    = substr($parsedFrame['data'], $frame_offset++, 1);
                $parsedFrame['type']      = $this->ETCOEventLookup($parsedFrame['typeid']);
                $parsedFrame['timestamp'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
                $frame_offset += 4;
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'MLLT')) || // 4.6   MLLT MPEG location lookup table
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'MLL'))) {     // 4.7   MLL MPEG location lookup table
            //   There may only be one 'MLLT' frame in each tag
            // <Header for 'Location lookup table', ID: 'MLLT'>
            // MPEG frames between reference  $xx xx
            // Bytes between reference        $xx xx xx
            // Milliseconds between reference $xx xx xx
            // Bits for bytes deviation       $xx
            // Bits for milliseconds dev.     $xx
            //   Then for every reference the following data is included;
            // Deviation in bytes         %xxx....
            // Deviation in milliseconds  %xxx....

            $frame_offset = 0;
            $parsedFrame['framesbetweenreferences'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 0, 2));
            $parsedFrame['bytesbetweenreferences']  = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 2, 3));
            $parsedFrame['msbetweenreferences']     = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 5, 3));
            $parsedFrame['bitsforbytesdeviation']   = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 8, 1));
            $parsedFrame['bitsformsdeviation']      = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 9, 1));
            $parsedFrame['data'] = substr($parsedFrame['data'], 10);
            $deviationbitstream = '';
            while ($frame_offset < strlen($parsedFrame['data'])) {
                $deviationbitstream .= getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1));
            }
            $reference_counter = 0;
            while (strlen($deviationbitstream) > 0) {
                $parsedFrame[$reference_counter]['bytedeviation'] = bindec(substr($deviationbitstream, 0, $parsedFrame['bitsforbytesdeviation']));
                $parsedFrame[$reference_counter]['msdeviation']   = bindec(substr($deviationbitstream, $parsedFrame['bitsforbytesdeviation'], $parsedFrame['bitsformsdeviation']));
                $deviationbitstream = substr($deviationbitstream, $parsedFrame['bitsforbytesdeviation'] + $parsedFrame['bitsformsdeviation']);
                $reference_counter++;
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'SYTC')) || // 4.7   SYTC Synchronised tempo codes
                  (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'STC'))) {  // 4.8   STC  Synchronised tempo codes
            //   There may only be one 'SYTC' frame in each tag
            // <Header for 'Synchronised tempo codes', ID: 'SYTC'>
            // Time stamp format   $xx
            // Tempo data          <binary data>
            //   Where time stamp format is:
            // $01  (32-bit value) MPEG frames from beginning of file
            // $02  (32-bit value) milliseconds from beginning of file

            $frame_offset = 0;
            $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $timestamp_counter = 0;
            while ($frame_offset < strlen($parsedFrame['data'])) {
                $parsedFrame[$timestamp_counter]['tempo'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
                if ($parsedFrame[$timestamp_counter]['tempo'] == 255) {
                    $parsedFrame[$timestamp_counter]['tempo'] += ord(substr($parsedFrame['data'], $frame_offset++, 1));
                }
                $parsedFrame[$timestamp_counter]['timestamp'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
                $frame_offset += 4;
                $timestamp_counter++;
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'USLT')) || // 4.8   USLT Unsynchronised lyric/text transcription
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'ULT'))) {    // 4.9   ULT  Unsynchronised lyric/text transcription
            //   There may be more than one 'Unsynchronised lyrics/text transcription' frame
            //   in each tag, but only one with the same language and content descriptor.
            // <Header for 'Unsynchronised lyrics/text transcription', ID: 'USLT'>
            // Text encoding        $xx
            // Language             $xx xx xx
            // Content descriptor   <text string according to encoding> $00 (00)
            // Lyrics/text          <full text string according to encoding>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }
            if (strlen($parsedFrame['data']) >= (4 + strlen($frame_textencoding_terminator))) {  // shouldn't be an issue but badly-written files have been spotted in the wild with not only no contents but also missing the required language field, see https://github.com/JamesHeinrich/getID3/issues/315
                $frame_language = substr($parsedFrame['data'], $frame_offset, 3);
                $frame_offset += 3;
                $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
                if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                    $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
                }
                $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
                $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
                $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator));
                $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $frame_textencoding_terminator);

                $parsedFrame['encodingid']   = $frame_textencoding;
                $parsedFrame['encoding']     = $this->TextEncodingNameLookup($frame_textencoding);

                $parsedFrame['language']     = $frame_language;
                $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false);
                if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                    $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']);
                }
            } else {
                $this->warning('Invalid data in frame "'.$parsedFrame['frame_name'].'" at offset '.$parsedFrame['dataoffset']);
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'SYLT')) || // 4.9   SYLT Synchronised lyric/text
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'SLT'))) {     // 4.10  SLT  Synchronised lyric/text
            //   There may be more than one 'SYLT' frame in each tag,
            //   but only one with the same language and content descriptor.
            // <Header for 'Synchronised lyrics/text', ID: 'SYLT'>
            // Text encoding        $xx
            // Language             $xx xx xx
            // Time stamp format    $xx
            //   $01  (32-bit value) MPEG frames from beginning of file
            //   $02  (32-bit value) milliseconds from beginning of file
            // Content type         $xx
            // Content descriptor   <text string according to encoding> $00 (00)
            //   Terminated text to be synced (typically a syllable)
            //   Sync identifier (terminator to above string)   $00 (00)
            //   Time stamp                                     $xx (xx ...)

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }
            $frame_language = substr($parsedFrame['data'], $frame_offset, 3);
            $frame_offset += 3;
            $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['contenttypeid']   = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['contenttype']     = $this->SYTLContentTypeLookup($parsedFrame['contenttypeid']);
            $parsedFrame['encodingid']      = $frame_textencoding;
            $parsedFrame['encoding']        = $this->TextEncodingNameLookup($frame_textencoding);

            $parsedFrame['language']        = $frame_language;
            $parsedFrame['languagename']    = $this->LanguageLookup($frame_language, false);

            $timestampindex = 0;
            $frame_remainingdata = substr($parsedFrame['data'], $frame_offset);
            while (strlen($frame_remainingdata)) {
                $frame_offset = 0;
                $frame_terminatorpos = strpos($frame_remainingdata, $frame_textencoding_terminator);
                if ($frame_terminatorpos === false) {
                    $frame_remainingdata = '';
                } else {
                    if (ord(substr($frame_remainingdata, $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                        $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
                    }
                    $parsedFrame['lyrics'][$timestampindex]['data'] = substr($frame_remainingdata, $frame_offset, $frame_terminatorpos - $frame_offset);

                    $frame_remainingdata = substr($frame_remainingdata, $frame_terminatorpos + strlen($frame_textencoding_terminator));
                    if (($timestampindex == 0) && (ord($frame_remainingdata[0]) != 0)) {
                        // timestamp probably omitted for first data item
                    } else {
                        $parsedFrame['lyrics'][$timestampindex]['timestamp'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 0, 4));
                        $frame_remainingdata = substr($frame_remainingdata, 4);
                    }
                    $timestampindex++;
                }
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'COMM')) || // 4.10  COMM Comments
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'COM'))) {     // 4.11  COM  Comments
            //   There may be more than one comment frame in each tag,
            //   but only one with the same language and content descriptor.
            // <Header for 'Comment', ID: 'COMM'>
            // Text encoding          $xx
            // Language               $xx xx xx
            // Short content descrip. <text string according to encoding> $00 (00)
            // The actual text        <full text string according to encoding>

            if (strlen($parsedFrame['data']) < 5) {

                $this->warning('Invalid data (too short) for "'.$parsedFrame['frame_name'].'" frame at offset '.$parsedFrame['dataoffset']);

            } else {

                $frame_offset = 0;
                $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
                $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
                if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                    $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                    $frame_textencoding_terminator = "\x00";
                }
                $frame_language = substr($parsedFrame['data'], $frame_offset, 3);
                $frame_offset += 3;
                $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
                if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                    $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
                }
                $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
                $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
                $frame_text = (string) substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator));
                $frame_text = $this->RemoveStringTerminator($frame_text, $frame_textencoding_terminator);

                $parsedFrame['encodingid']   = $frame_textencoding;
                $parsedFrame['encoding']     = $this->TextEncodingNameLookup($frame_textencoding);

                $parsedFrame['language']     = $frame_language;
                $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false);
                $parsedFrame['data']         = $frame_text;
                if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                    $commentkey = ($parsedFrame['description'] ? $parsedFrame['description'] : (!empty($info['id3v2']['comments'][$parsedFrame['framenameshort']]) ? count($info['id3v2']['comments'][$parsedFrame['framenameshort']]) : 0));
                    if (!isset($info['id3v2']['comments'][$parsedFrame['framenameshort']]) || !array_key_exists($commentkey, $info['id3v2']['comments'][$parsedFrame['framenameshort']])) {
                        $info['id3v2']['comments'][$parsedFrame['framenameshort']][$commentkey] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']);
                    } else {
                        $info['id3v2']['comments'][$parsedFrame['framenameshort']][]            = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']);
                    }
                }

            }

        } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'RVA2')) { // 4.11  RVA2 Relative volume adjustment (2) (ID3v2.4+ only)
            //   There may be more than one 'RVA2' frame in each tag,
            //   but only one with the same identification string
            // <Header for 'Relative volume adjustment (2)', ID: 'RVA2'>
            // Identification          <text string> $00
            //   The 'identification' string is used to identify the situation and/or
            //   device where this adjustment should apply. The following is then
            //   repeated for every channel:
            // Type of channel         $xx
            // Volume adjustment       $xx xx
            // Bits representing peak  $xx
            // Peak volume             $xx (xx ...)

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00");
            $frame_idstring = substr($parsedFrame['data'], 0, $frame_terminatorpos);
            if (ord($frame_idstring) === 0) {
                $frame_idstring = '';
            }
            $frame_remainingdata = substr($parsedFrame['data'], $frame_terminatorpos + strlen("\x00"));
            $parsedFrame['description'] = $frame_idstring;
            $RVA2channelcounter = 0;
            while (strlen($frame_remainingdata) >= 5) {
                $frame_offset = 0;
                $frame_channeltypeid = ord(substr($frame_remainingdata, $frame_offset++, 1));
                $parsedFrame[$RVA2channelcounter]['channeltypeid']  = $frame_channeltypeid;
                $parsedFrame[$RVA2channelcounter]['channeltype']    = $this->RVA2ChannelTypeLookup($frame_channeltypeid);
                $parsedFrame[$RVA2channelcounter]['volumeadjust']   = getid3_lib::BigEndian2Int(substr($frame_remainingdata, $frame_offset, 2), false, true); // 16-bit signed
                $frame_offset += 2;
                $parsedFrame[$RVA2channelcounter]['bitspeakvolume'] = ord(substr($frame_remainingdata, $frame_offset++, 1));
                if (($parsedFrame[$RVA2channelcounter]['bitspeakvolume'] < 1) || ($parsedFrame[$RVA2channelcounter]['bitspeakvolume'] > 4)) {
                    $this->warning('ID3v2::RVA2 frame['.$RVA2channelcounter.'] contains invalid '.$parsedFrame[$RVA2channelcounter]['bitspeakvolume'].'-byte bits-representing-peak value');
                    break;
                }
                $frame_bytespeakvolume = ceil($parsedFrame[$RVA2channelcounter]['bitspeakvolume'] / 8);
                $parsedFrame[$RVA2channelcounter]['peakvolume']     = getid3_lib::BigEndian2Int(substr($frame_remainingdata, $frame_offset, $frame_bytespeakvolume));
                $frame_remainingdata = substr($frame_remainingdata, $frame_offset + $frame_bytespeakvolume);
                $RVA2channelcounter++;
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'RVAD')) || // 4.12  RVAD Relative volume adjustment (ID3v2.3 only)
                  (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'RVA'))) {  // 4.12  RVA  Relative volume adjustment (ID3v2.2 only)
            //   There may only be one 'RVA' frame in each tag
            // <Header for 'Relative volume adjustment', ID: 'RVA'>
            // ID3v2.2 => Increment/decrement     %000000ba
            // ID3v2.3 => Increment/decrement     %00fedcba
            // Bits used for volume descr.        $xx
            // Relative volume change, right      $xx xx (xx ...) // a
            // Relative volume change, left       $xx xx (xx ...) // b
            // Peak volume right                  $xx xx (xx ...)
            // Peak volume left                   $xx xx (xx ...)
            //   ID3v2.3 only, optional (not present in ID3v2.2):
            // Relative volume change, right back $xx xx (xx ...) // c
            // Relative volume change, left back  $xx xx (xx ...) // d
            // Peak volume right back             $xx xx (xx ...)
            // Peak volume left back              $xx xx (xx ...)
            //   ID3v2.3 only, optional (not present in ID3v2.2):
            // Relative volume change, center     $xx xx (xx ...) // e
            // Peak volume center                 $xx xx (xx ...)
            //   ID3v2.3 only, optional (not present in ID3v2.2):
            // Relative volume change, bass       $xx xx (xx ...) // f
            // Peak volume bass                   $xx xx (xx ...)

            $frame_offset = 0;
            $frame_incrdecrflags = getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['incdec']['right'] = (bool) substr($frame_incrdecrflags, 6, 1);
            $parsedFrame['incdec']['left']  = (bool) substr($frame_incrdecrflags, 7, 1);
            $parsedFrame['bitsvolume'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_bytesvolume = ceil($parsedFrame['bitsvolume'] / 8);
            $parsedFrame['volumechange']['right'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
            if ($parsedFrame['incdec']['right'] === false) {
                $parsedFrame['volumechange']['right'] *= -1;
            }
            $frame_offset += $frame_bytesvolume;
            $parsedFrame['volumechange']['left'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
            if ($parsedFrame['incdec']['left'] === false) {
                $parsedFrame['volumechange']['left'] *= -1;
            }
            $frame_offset += $frame_bytesvolume;
            $parsedFrame['peakvolume']['right'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
            $frame_offset += $frame_bytesvolume;
            $parsedFrame['peakvolume']['left']  = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
            $frame_offset += $frame_bytesvolume;
            if ($id3v2_majorversion == 3) {
                $parsedFrame['data'] = substr($parsedFrame['data'], $frame_offset);
                if (strlen($parsedFrame['data']) > 0) {
                    $parsedFrame['incdec']['rightrear'] = (bool) substr($frame_incrdecrflags, 4, 1);
                    $parsedFrame['incdec']['leftrear']  = (bool) substr($frame_incrdecrflags, 5, 1);
                    $parsedFrame['volumechange']['rightrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    if ($parsedFrame['incdec']['rightrear'] === false) {
                        $parsedFrame['volumechange']['rightrear'] *= -1;
                    }
                    $frame_offset += $frame_bytesvolume;
                    $parsedFrame['volumechange']['leftrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    if ($parsedFrame['incdec']['leftrear'] === false) {
                        $parsedFrame['volumechange']['leftrear'] *= -1;
                    }
                    $frame_offset += $frame_bytesvolume;
                    $parsedFrame['peakvolume']['rightrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    $frame_offset += $frame_bytesvolume;
                    $parsedFrame['peakvolume']['leftrear']  = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    $frame_offset += $frame_bytesvolume;
                }
                $parsedFrame['data'] = substr($parsedFrame['data'], $frame_offset);
                if (strlen($parsedFrame['data']) > 0) {
                    $parsedFrame['incdec']['center'] = (bool) substr($frame_incrdecrflags, 3, 1);
                    $parsedFrame['volumechange']['center'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    if ($parsedFrame['incdec']['center'] === false) {
                        $parsedFrame['volumechange']['center'] *= -1;
                    }
                    $frame_offset += $frame_bytesvolume;
                    $parsedFrame['peakvolume']['center'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    $frame_offset += $frame_bytesvolume;
                }
                $parsedFrame['data'] = substr($parsedFrame['data'], $frame_offset);
                if (strlen($parsedFrame['data']) > 0) {
                    $parsedFrame['incdec']['bass'] = (bool) substr($frame_incrdecrflags, 2, 1);
                    $parsedFrame['volumechange']['bass'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    if ($parsedFrame['incdec']['bass'] === false) {
                        $parsedFrame['volumechange']['bass'] *= -1;
                    }
                    $frame_offset += $frame_bytesvolume;
                    $parsedFrame['peakvolume']['bass'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume));
                    $frame_offset += $frame_bytesvolume;
                }
            }
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'EQU2')) { // 4.12  EQU2 Equalisation (2) (ID3v2.4+ only)
            //   There may be more than one 'EQU2' frame in each tag,
            //   but only one with the same identification string
            // <Header of 'Equalisation (2)', ID: 'EQU2'>
            // Interpolation method  $xx
            //   $00  Band
            //   $01  Linear
            // Identification        <text string> $00
            //   The following is then repeated for every adjustment point
            // Frequency          $xx xx
            // Volume adjustment  $xx xx

            $frame_offset = 0;
            $frame_interpolationmethod = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_idstring = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_idstring) === 0) {
                $frame_idstring = '';
            }
            $parsedFrame['description'] = $frame_idstring;
            $frame_remainingdata = substr($parsedFrame['data'], $frame_terminatorpos + strlen("\x00"));
            while (strlen($frame_remainingdata)) {
                $frame_frequency = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 0, 2)) / 2;
                $parsedFrame['data'][$frame_frequency] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 2, 2), false, true);
                $frame_remainingdata = substr($frame_remainingdata, 4);
            }
            $parsedFrame['interpolationmethod'] = $frame_interpolationmethod;
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'EQUA')) || // 4.12  EQUA Equalisation (ID3v2.3 only)
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'EQU'))) {     // 4.13  EQU  Equalisation (ID3v2.2 only)
            //   There may only be one 'EQUA' frame in each tag
            // <Header for 'Relative volume adjustment', ID: 'EQU'>
            // Adjustment bits    $xx
            //   This is followed by 2 bytes + ('adjustment bits' rounded up to the
            //   nearest byte) for every equalisation band in the following format,
            //   giving a frequency range of 0 - 32767Hz:
            // Increment/decrement   %x (MSB of the Frequency)
            // Frequency             (lower 15 bits)
            // Adjustment            $xx (xx ...)

            $frame_offset = 0;
            $parsedFrame['adjustmentbits'] = substr($parsedFrame['data'], $frame_offset++, 1);
            $frame_adjustmentbytes = ceil($parsedFrame['adjustmentbits'] / 8);

            $frame_remainingdata = (string) substr($parsedFrame['data'], $frame_offset);
            while (strlen($frame_remainingdata) > 0) {
                $frame_frequencystr = getid3_lib::BigEndian2Bin(substr($frame_remainingdata, 0, 2));
                $frame_incdec    = (bool) substr($frame_frequencystr, 0, 1);
                $frame_frequency = bindec(substr($frame_frequencystr, 1, 15));
                $parsedFrame[$frame_frequency]['incdec'] = $frame_incdec;
                $parsedFrame[$frame_frequency]['adjustment'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 2, $frame_adjustmentbytes));
                if ($parsedFrame[$frame_frequency]['incdec'] === false) {
                    $parsedFrame[$frame_frequency]['adjustment'] *= -1;
                }
                $frame_remainingdata = substr($frame_remainingdata, 2 + $frame_adjustmentbytes);
            }
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'RVRB')) || // 4.13  RVRB Reverb
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'REV'))) {     // 4.14  REV  Reverb
            //   There may only be one 'RVRB' frame in each tag.
            // <Header for 'Reverb', ID: 'RVRB'>
            // Reverb left (ms)                 $xx xx
            // Reverb right (ms)                $xx xx
            // Reverb bounces, left             $xx
            // Reverb bounces, right            $xx
            // Reverb feedback, left to left    $xx
            // Reverb feedback, left to right   $xx
            // Reverb feedback, right to right  $xx
            // Reverb feedback, right to left   $xx
            // Premix left to right             $xx
            // Premix right to left             $xx

            $frame_offset = 0;
            $parsedFrame['left']  = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
            $frame_offset += 2;
            $parsedFrame['right'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
            $frame_offset += 2;
            $parsedFrame['bouncesL']      = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['bouncesR']      = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['feedbackLL']    = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['feedbackLR']    = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['feedbackRR']    = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['feedbackRL']    = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['premixLR']      = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['premixRL']      = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'APIC')) || // 4.14  APIC Attached picture
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'PIC'))) {     // 4.15  PIC  Attached picture
            //   There may be several pictures attached to one file,
            //   each in their individual 'APIC' frame, but only one
            //   with the same content descriptor
            // <Header for 'Attached picture', ID: 'APIC'>
            // Text encoding      $xx
            // ID3v2.3+ => MIME type          <text string> $00
            // ID3v2.2  => Image format       $xx xx xx
            // Picture type       $xx
            // Description        <text string according to encoding> $00 (00)
            // Picture data       <binary data>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }

            $frame_imagetype = null;
            $frame_mimetype = null;
            if ($id3v2_majorversion == 2 && strlen($parsedFrame['data']) > $frame_offset) {
                $frame_imagetype = substr($parsedFrame['data'], $frame_offset, 3);
                if (strtolower($frame_imagetype) == 'ima') {
                    // complete hack for mp3Rage (www.chaoticsoftware.com) that puts ID3v2.3-formatted
                    // MIME type instead of 3-char ID3v2.2-format image type  (thanks xbhoffØpacbell*net)
                    $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
                    $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
                    if (ord($frame_mimetype) === 0) {
                        $frame_mimetype = '';
                    }
                    $frame_imagetype = strtoupper(str_replace('image/', '', strtolower($frame_mimetype)));
                    if ($frame_imagetype == 'JPEG') {
                        $frame_imagetype = 'JPG';
                    }
                    $frame_offset = $frame_terminatorpos + strlen("\x00");
                } else {
                    $frame_offset += 3;
                }
            }
            if ($id3v2_majorversion > 2 && strlen($parsedFrame['data']) > $frame_offset) {
                $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
                $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
                if (ord($frame_mimetype) === 0) {
                    $frame_mimetype = '';
                }
                $frame_offset = $frame_terminatorpos + strlen("\x00");
            }

            $frame_picturetype = ord(substr($parsedFrame['data'], $frame_offset++, 1));

            if ($frame_offset >= $parsedFrame['datalength']) {
                $this->warning('data portion of APIC frame is missing at offset '.($parsedFrame['dataoffset'] + 8 + $frame_offset));
            } else {
                $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
                if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                    $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
                }
                $parsedFrame['description']   = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
                $parsedFrame['description']   = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
                $parsedFrame['encodingid']    = $frame_textencoding;
                $parsedFrame['encoding']      = $this->TextEncodingNameLookup($frame_textencoding);

                if ($id3v2_majorversion == 2) {
                    $parsedFrame['imagetype'] = isset($frame_imagetype) ? $frame_imagetype : null;
                } else {
                    $parsedFrame['mime']      = isset($frame_mimetype) ? $frame_mimetype : null;
                }
                $parsedFrame['picturetypeid'] = $frame_picturetype;
                $parsedFrame['picturetype']   = $this->APICPictureTypeLookup($frame_picturetype);
                $parsedFrame['data']          = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator));
                $parsedFrame['datalength']    = strlen($parsedFrame['data']);

                $parsedFrame['image_mime']    = '';
                $imageinfo = array();
                if ($imagechunkcheck = getid3_lib::GetDataImageSize($parsedFrame['data'], $imageinfo)) {
                    if (($imagechunkcheck[2] >= 1) && ($imagechunkcheck[2] <= 3)) {
                        $parsedFrame['image_mime']       = image_type_to_mime_type($imagechunkcheck[2]);
                        if ($imagechunkcheck[0]) {
                            $parsedFrame['image_width']  = $imagechunkcheck[0];
                        }
                        if ($imagechunkcheck[1]) {
                            $parsedFrame['image_height'] = $imagechunkcheck[1];
                        }
                    }
                }

                do {
                    if ($this->getid3->option_save_attachments === false) {
                        // skip entirely
                        unset($parsedFrame['data']);
                        break;
                    }
                    $dir = '';
                    if ($this->getid3->option_save_attachments === true) {
                        // great
/*
                    } elseif (is_int($this->getid3->option_save_attachments)) {
                        if ($this->getid3->option_save_attachments < $parsedFrame['data_length']) {
                            // too big, skip
                            $this->warning('attachment at '.$frame_offset.' is too large to process inline ('.number_format($parsedFrame['data_length']).' bytes)');
                            unset($parsedFrame['data']);
                            break;
                        }
*/
                    } elseif (is_string($this->getid3->option_save_attachments)) {
                        $dir = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->getid3->option_save_attachments), DIRECTORY_SEPARATOR);
                        if (!is_dir($dir) || !getID3::is_writable($dir)) {
                            // cannot write, skip
                            $this->warning('attachment at '.$frame_offset.' cannot be saved to "'.$dir.'" (not writable)');
                            unset($parsedFrame['data']);
                            break;
                        }
                    }
                    // if we get this far, must be OK
                    if (is_string($this->getid3->option_save_attachments)) {
                        $destination_filename = $dir.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$frame_offset;
                        if (!file_exists($destination_filename) || getID3::is_writable($destination_filename)) {
                            file_put_contents($destination_filename, $parsedFrame['data']);
                        } else {
                            $this->warning('attachment at '.$frame_offset.' cannot be saved to "'.$destination_filename.'" (not writable)');
                        }
                        $parsedFrame['data_filename'] = $destination_filename;
                        unset($parsedFrame['data']);
                    } else {
                        if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                            if (!isset($info['id3v2']['comments']['picture'])) {
                                $info['id3v2']['comments']['picture'] = array();
                            }
                            $comments_picture_data = array();
                            foreach (array('data', 'image_mime', 'image_width', 'image_height', 'imagetype', 'picturetype', 'description', 'datalength') as $picture_key) {
                                if (isset($parsedFrame[$picture_key])) {
                                    $comments_picture_data[$picture_key] = $parsedFrame[$picture_key];
                                }
                            }
                            $info['id3v2']['comments']['picture'][] = $comments_picture_data;
                            unset($comments_picture_data);
                        }
                    }
                } while (false); // @phpstan-ignore-line
            }

        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'GEOB')) || // 4.15  GEOB General encapsulated object
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'GEO'))) {     // 4.16  GEO  General encapsulated object
            //   There may be more than one 'GEOB' frame in each tag,
            //   but only one with the same content descriptor
            // <Header for 'General encapsulated object', ID: 'GEOB'>
            // Text encoding          $xx
            // MIME type              <text string> $00
            // Filename               <text string according to encoding> $00 (00)
            // Content description    <text string according to encoding> $00 (00)
            // Encapsulated object    <binary data>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_mimetype) === 0) {
                $frame_mimetype = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
            if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
            }
            $frame_filename = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_filename) === 0) {
                $frame_filename = '';
            }
            $frame_offset = $frame_terminatorpos + strlen($frame_textencoding_terminator);

            $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
            if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
            }
            $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
            $frame_offset = $frame_terminatorpos + strlen($frame_textencoding_terminator);

            $parsedFrame['objectdata']  = (string) substr($parsedFrame['data'], $frame_offset);
            $parsedFrame['encodingid']  = $frame_textencoding;
            $parsedFrame['encoding']    = $this->TextEncodingNameLookup($frame_textencoding);

            $parsedFrame['mime']        = $frame_mimetype;
            $parsedFrame['filename']    = $frame_filename;
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'PCNT')) || // 4.16  PCNT Play counter
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'CNT'))) {     // 4.17  CNT  Play counter
            //   There may only be one 'PCNT' frame in each tag.
            //   When the counter reaches all one's, one byte is inserted in
            //   front of the counter thus making the counter eight bits bigger
            // <Header for 'Play counter', ID: 'PCNT'>
            // Counter        $xx xx xx xx (xx ...)

            $parsedFrame['data']          = getid3_lib::BigEndian2Int($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'POPM')) || // 4.17  POPM Popularimeter
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'POP'))) {    // 4.18  POP  Popularimeter
            //   There may be more than one 'POPM' frame in each tag,
            //   but only one with the same email address
            // <Header for 'Popularimeter', ID: 'POPM'>
            // Email to user   <text string> $00
            // Rating          $xx
            // Counter         $xx xx xx xx (xx ...)

            $frame_offset = 0;
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_emailaddress = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_emailaddress) === 0) {
                $frame_emailaddress = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");
            $frame_rating = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['counter'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset));
            $parsedFrame['email']   = $frame_emailaddress;
            $parsedFrame['rating']  = $frame_rating;
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'RBUF')) || // 4.18  RBUF Recommended buffer size
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'BUF'))) {     // 4.19  BUF  Recommended buffer size
            //   There may only be one 'RBUF' frame in each tag
            // <Header for 'Recommended buffer size', ID: 'RBUF'>
            // Buffer size               $xx xx xx
            // Embedded info flag        %0000000x
            // Offset to next tag        $xx xx xx xx

            $frame_offset = 0;
            $parsedFrame['buffersize'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 3));
            $frame_offset += 3;

            $frame_embeddedinfoflags = getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['flags']['embededinfo'] = (bool) substr($frame_embeddedinfoflags, 7, 1);
            $parsedFrame['nexttagoffset'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'CRM')) { // 4.20  Encrypted meta frame (ID3v2.2 only)
            //   There may be more than one 'CRM' frame in a tag,
            //   but only one with the same 'owner identifier'
            // <Header for 'Encrypted meta frame', ID: 'CRM'>
            // Owner identifier      <textstring> $00 (00)
            // Content/explanation   <textstring> $00 (00)
            // Encrypted datablock   <binary data>

            $frame_offset = 0;
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $parsedFrame['ownerid']     = $frame_ownerid;
            $parsedFrame['data']        = (string) substr($parsedFrame['data'], $frame_offset);
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'AENC')) || // 4.19  AENC Audio encryption
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'CRA'))) {     // 4.21  CRA  Audio encryption
            //   There may be more than one 'AENC' frames in a tag,
            //   but only one with the same 'Owner identifier'
            // <Header for 'Audio encryption', ID: 'AENC'>
            // Owner identifier   <text string> $00
            // Preview start      $xx xx
            // Preview length     $xx xx
            // Encryption info    <binary data>

            $frame_offset = 0;
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_ownerid) === 0) {
                $frame_ownerid = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");
            $parsedFrame['ownerid'] = $frame_ownerid;
            $parsedFrame['previewstart'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
            $frame_offset += 2;
            $parsedFrame['previewlength'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
            $frame_offset += 2;
            $parsedFrame['encryptioninfo'] = (string) substr($parsedFrame['data'], $frame_offset);
            unset($parsedFrame['data']);


        } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'LINK')) || // 4.20  LINK Linked information
                (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'LNK'))) {    // 4.22  LNK  Linked information
            //   There may be more than one 'LINK' frame in a tag,
            //   but only one with the same contents
            // <Header for 'Linked information', ID: 'LINK'>
            // ID3v2.3+ => Frame identifier   $xx xx xx xx
            // ID3v2.2  => Frame identifier   $xx xx xx
            // URL                            <text string> $00
            // ID and additional data         <text string(s)>

            $frame_offset = 0;
            if ($id3v2_majorversion == 2) {
                $parsedFrame['frameid'] = substr($parsedFrame['data'], $frame_offset, 3);
                $frame_offset += 3;
            } else {
                $parsedFrame['frameid'] = substr($parsedFrame['data'], $frame_offset, 4);
                $frame_offset += 4;
            }

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_url = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_url) === 0) {
                $frame_url = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");
            $parsedFrame['url'] = $frame_url;

            $parsedFrame['additionaldata'] = (string) substr($parsedFrame['data'], $frame_offset);
            if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) {
                $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback_iso88591_utf8($parsedFrame['url']);
            }
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'POSS')) { // 4.21  POSS Position synchronisation frame (ID3v2.3+ only)
            //   There may only be one 'POSS' frame in each tag
            // <Head for 'Position synchronisation', ID: 'POSS'>
            // Time stamp format         $xx
            // Position                  $xx (xx ...)

            $frame_offset = 0;
            $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['position']        = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset));
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'USER')) { // 4.22  USER Terms of use (ID3v2.3+ only)
            //   There may be more than one 'Terms of use' frame in a tag,
            //   but only one with the same 'Language'
            // <Header for 'Terms of use frame', ID: 'USER'>
            // Text encoding        $xx
            // Language             $xx xx xx
            // The actual text      <text string according to encoding>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
            }
            $frame_language = substr($parsedFrame['data'], $frame_offset, 3);
            $frame_offset += 3;
            $parsedFrame['language']     = $frame_language;
            $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false);
            $parsedFrame['encodingid']   = $frame_textencoding;
            $parsedFrame['encoding']     = $this->TextEncodingNameLookup($frame_textencoding);

            $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset);
            $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding));
            if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) {
                $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']);
            }
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'OWNE')) { // 4.23  OWNE Ownership frame (ID3v2.3+ only)
            //   There may only be one 'OWNE' frame in a tag
            // <Header for 'Ownership frame', ID: 'OWNE'>
            // Text encoding     $xx
            // Price paid        <text string> $00
            // Date of purch.    <text string>
            // Seller            <text string according to encoding>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
            }
            $parsedFrame['encodingid'] = $frame_textencoding;
            $parsedFrame['encoding']   = $this->TextEncodingNameLookup($frame_textencoding);

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_pricepaid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $parsedFrame['pricepaid']['currencyid'] = substr($frame_pricepaid, 0, 3);
            $parsedFrame['pricepaid']['currency']   = $this->LookupCurrencyUnits($parsedFrame['pricepaid']['currencyid']);
            $parsedFrame['pricepaid']['value']      = substr($frame_pricepaid, 3);

            $parsedFrame['purchasedate'] = substr($parsedFrame['data'], $frame_offset, 8);
            if ($this->IsValidDateStampString($parsedFrame['purchasedate'])) {
                $parsedFrame['purchasedateunix'] = mktime (0, 0, 0, substr($parsedFrame['purchasedate'], 4, 2), substr($parsedFrame['purchasedate'], 6, 2), substr($parsedFrame['purchasedate'], 0, 4));
            }
            $frame_offset += 8;

            $parsedFrame['seller'] = (string) substr($parsedFrame['data'], $frame_offset);
            $parsedFrame['seller'] = $this->RemoveStringTerminator($parsedFrame['seller'], $this->TextEncodingTerminatorLookup($frame_textencoding));
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'COMR')) { // 4.24  COMR Commercial frame (ID3v2.3+ only)
            //   There may be more than one 'commercial frame' in a tag,
            //   but no two may be identical
            // <Header for 'Commercial frame', ID: 'COMR'>
            // Text encoding      $xx
            // Price string       <text string> $00
            // Valid until        <text string>
            // Contact URL        <text string> $00
            // Received as        $xx
            // Name of seller     <text string according to encoding> $00 (00)
            // Description        <text string according to encoding> $00 (00)
            // Picture MIME type  <string> $00
            // Seller logo        <binary data>

            $frame_offset = 0;
            $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_textencoding_terminator = $this->TextEncodingTerminatorLookup($frame_textencoding);
            if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) {
                $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding');
                $frame_textencoding_terminator = "\x00";
            }

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_pricestring = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $frame_offset = $frame_terminatorpos + strlen("\x00");
            $frame_rawpricearray = explode('/', $frame_pricestring);
            foreach ($frame_rawpricearray as $key => $val) {
                $frame_currencyid = substr($val, 0, 3);
                $parsedFrame['price'][$frame_currencyid]['currency'] = $this->LookupCurrencyUnits($frame_currencyid);
                $parsedFrame['price'][$frame_currencyid]['value']    = substr($val, 3);
            }

            $frame_datestring = substr($parsedFrame['data'], $frame_offset, 8);
            $frame_offset += 8;

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_contacturl = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $frame_receivedasid = ord(substr($parsedFrame['data'], $frame_offset++, 1));

            $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
            if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
            }
            $frame_sellername = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_sellername) === 0) {
                $frame_sellername = '';
            }
            $frame_offset = $frame_terminatorpos + strlen($frame_textencoding_terminator);

            $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset);
            if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) {
                $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00
            }
            $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']);
            $frame_offset = $frame_terminatorpos + strlen($frame_textencoding_terminator);

            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $frame_sellerlogo = substr($parsedFrame['data'], $frame_offset);

            $parsedFrame['encodingid']        = $frame_textencoding;
            $parsedFrame['encoding']          = $this->TextEncodingNameLookup($frame_textencoding);

            $parsedFrame['pricevaliduntil']   = $frame_datestring;
            $parsedFrame['contacturl']        = $frame_contacturl;
            $parsedFrame['receivedasid']      = $frame_receivedasid;
            $parsedFrame['receivedas']        = $this->COMRReceivedAsLookup($frame_receivedasid);
            $parsedFrame['sellername']        = $frame_sellername;
            $parsedFrame['mime']              = $frame_mimetype;
            $parsedFrame['logo']              = $frame_sellerlogo;
            unset($parsedFrame['data']);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'ENCR')) { // 4.25  ENCR Encryption method registration (ID3v2.3+ only)
            //   There may be several 'ENCR' frames in a tag,
            //   but only one containing the same symbol
            //   and only one containing the same owner identifier
            // <Header for 'Encryption method registration', ID: 'ENCR'>
            // Owner identifier    <text string> $00
            // Method symbol       $xx
            // Encryption data     <binary data>

            $frame_offset = 0;
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_ownerid) === 0) {
                $frame_ownerid = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $parsedFrame['ownerid']      = $frame_ownerid;
            $parsedFrame['methodsymbol'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['data']         = (string) substr($parsedFrame['data'], $frame_offset);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'GRID')) { // 4.26  GRID Group identification registration (ID3v2.3+ only)

            //   There may be several 'GRID' frames in a tag,
            //   but only one containing the same symbol
            //   and only one containing the same owner identifier
            // <Header for 'Group ID registration', ID: 'GRID'>
            // Owner identifier      <text string> $00
            // Group symbol          $xx
            // Group dependent data  <binary data>

            $frame_offset = 0;
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_ownerid) === 0) {
                $frame_ownerid = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $parsedFrame['ownerid']       = $frame_ownerid;
            $parsedFrame['groupsymbol']   = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['data']          = (string) substr($parsedFrame['data'], $frame_offset);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'PRIV')) { // 4.27  PRIV Private frame (ID3v2.3+ only)
            //   The tag may contain more than one 'PRIV' frame
            //   but only with different contents
            // <Header for 'Private frame', ID: 'PRIV'>
            // Owner identifier      <text string> $00
            // The private data      <binary data>

            $frame_offset = 0;
            $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset);
            $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset);
            if (ord($frame_ownerid) === 0) {
                $frame_ownerid = '';
            }
            $frame_offset = $frame_terminatorpos + strlen("\x00");

            $parsedFrame['ownerid'] = $frame_ownerid;
            $parsedFrame['data']    = (string) substr($parsedFrame['data'], $frame_offset);


        } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'SIGN')) { // 4.28  SIGN Signature frame (ID3v2.4+ only)
            //   There may be more than one 'signature frame' in a tag,
            //   but no two may be identical
            // <Header for 'Signature frame', ID: 'SIGN'>
            // Group symbol      $xx
            // Signature         <binary data>

            $frame_offset = 0;
            $parsedFrame['groupsymbol'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $parsedFrame['data']        = (string) substr($parsedFrame['data'], $frame_offset);


        } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'SEEK')) { // 4.29  SEEK Seek frame (ID3v2.4+ only)
            //   There may only be one 'seek frame' in a tag
            // <Header for 'Seek frame', ID: 'SEEK'>
            // Minimum offset to next tag       $xx xx xx xx

            $frame_offset = 0;
            $parsedFrame['data']          = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));


        } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'ASPI')) { // 4.30  ASPI Audio seek point index (ID3v2.4+ only)
            //   There may only be one 'audio seek point index' frame in a tag
            // <Header for 'Seek Point Index', ID: 'ASPI'>
            // Indexed data start (S)         $xx xx xx xx
            // Indexed data length (L)        $xx xx xx xx
            // Number of index points (N)     $xx xx
            // Bits per index point (b)       $xx
            //   Then for every index point the following data is included:
            // Fraction at index (Fi)          $xx (xx)

            $frame_offset = 0;
            $parsedFrame['datastart'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            $frame_offset += 4;
            $parsedFrame['indexeddatalength'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            $frame_offset += 4;
            $parsedFrame['indexpoints'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
            $frame_offset += 2;
            $parsedFrame['bitsperpoint'] = ord(substr($parsedFrame['data'], $frame_offset++, 1));
            $frame_bytesperpoint = ceil($parsedFrame['bitsperpoint'] / 8);
            for ($i = 0; $i < $parsedFrame['indexpoints']; $i++) {
                $parsedFrame['indexes'][$i] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesperpoint));
                $frame_offset += $frame_bytesperpoint;
            }
            unset($parsedFrame['data']);

        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'RGAD')) { // Replay Gain Adjustment
            // http://privatewww.essex.ac.uk/~djmrob/replaygain/file_format_id3v2.html
            //   There may only be one 'RGAD' frame in a tag
            // <Header for 'Replay Gain Adjustment', ID: 'RGAD'>
            // Peak Amplitude                      $xx $xx $xx $xx
            // Radio Replay Gain Adjustment        %aaabbbcd %dddddddd
            // Audiophile Replay Gain Adjustment   %aaabbbcd %dddddddd
            //   a - name code
            //   b - originator code
            //   c - sign bit
            //   d - replay gain adjustment

            $frame_offset = 0;
            $parsedFrame['peakamplitude'] = getid3_lib::BigEndian2Float(substr($parsedFrame['data'], $frame_offset, 4));
            $frame_offset += 4;
            foreach (array('track','album') as $rgad_entry_type) {
                $rg_adjustment_word = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
                $frame_offset += 2;
                $parsedFrame['raw'][$rgad_entry_type]['name']       = ($rg_adjustment_word & 0xE000) >> 13;
                $parsedFrame['raw'][$rgad_entry_type]['originator'] = ($rg_adjustment_word & 0x1C00) >> 10;
                $parsedFrame['raw'][$rgad_entry_type]['signbit']    = ($rg_adjustment_word & 0x0200) >>  9;
                $parsedFrame['raw'][$rgad_entry_type]['adjustment'] = ($rg_adjustment_word & 0x0100);
            }
            $parsedFrame['track']['name']       = getid3_lib::RGADnameLookup($parsedFrame['raw']['track']['name']);
            $parsedFrame['track']['originator'] = getid3_lib::RGADoriginatorLookup($parsedFrame['raw']['track']['originator']);
            $parsedFrame['track']['adjustment'] = getid3_lib::RGADadjustmentLookup($parsedFrame['raw']['track']['adjustment'], $parsedFrame['raw']['track']['signbit']);
            $parsedFrame['album']['name']       = getid3_lib::RGADnameLookup($parsedFrame['raw']['album']['name']);
            $parsedFrame['album']['originator'] = getid3_lib::RGADoriginatorLookup($parsedFrame['raw']['album']['originator']);
            $parsedFrame['album']['adjustment'] = getid3_lib::RGADadjustmentLookup($parsedFrame['raw']['album']['adjustment'], $parsedFrame['raw']['album']['signbit']);

            $info['replay_gain']['track']['peak']       = $parsedFrame['peakamplitude'];
            $info['replay_gain']['track']['originator'] = $parsedFrame['track']['originator'];
            $info['replay_gain']['track']['adjustment'] = $parsedFrame['track']['adjustment'];
            $info['replay_gain']['album']['originator'] = $parsedFrame['album']['originator'];
            $info['replay_gain']['album']['adjustment'] = $parsedFrame['album']['adjustment'];

            unset($parsedFrame['data']);

        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'CHAP')) { // CHAP Chapters frame (ID3v2.3+ only)
            // http://id3.org/id3v2-chapters-1.0
            // <ID3v2.3 or ID3v2.4 frame header, ID: "CHAP">           (10 bytes)
            // Element ID      <text string> $00
            // Start time      $xx xx xx xx
            // End time        $xx xx xx xx
            // Start offset    $xx xx xx xx
            // End offset      $xx xx xx xx
            // <Optional embedded sub-frames>

            $frame_offset = 0;
            @list($parsedFrame['element_id']) = explode("\x00", $parsedFrame['data'], 2);
            $frame_offset += strlen($parsedFrame['element_id']."\x00");
            $parsedFrame['time_begin'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            $frame_offset += 4;
            $parsedFrame['time_end']   = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            $frame_offset += 4;
            if (substr($parsedFrame['data'], $frame_offset, 4) != "\xFF\xFF\xFF\xFF") {
                // "If these bytes are all set to 0xFF then the value should be ignored and the start time value should be utilized."
                $parsedFrame['offset_begin'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            }
            $frame_offset += 4;
            if (substr($parsedFrame['data'], $frame_offset, 4) != "\xFF\xFF\xFF\xFF") {
                // "If these bytes are all set to 0xFF then the value should be ignored and the start time value should be utilized."
                $parsedFrame['offset_end']   = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
            }
            $frame_offset += 4;

            if ($frame_offset < strlen($parsedFrame['data'])) {
                $parsedFrame['subframes'] = array();
                while ($frame_offset < strlen($parsedFrame['data'])) {
                    // <Optional embedded sub-frames>
                    $subframe = array();
                    $subframe['name']      =                           substr($parsedFrame['data'], $frame_offset, 4);
                    $frame_offset += 4;
                    $subframe['size']      = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
                    $frame_offset += 4;
                    $subframe['flags_raw'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
                    $frame_offset += 2;
                    if ($subframe['size'] > (strlen($parsedFrame['data']) - $frame_offset)) {
                        $this->warning('CHAP subframe "'.$subframe['name'].'" at frame offset '.$frame_offset.' claims to be "'.$subframe['size'].'" bytes, which is more than the available data ('.(strlen($parsedFrame['data']) - $frame_offset).' bytes)');
                        break;
                    }
                    $subframe_rawdata = substr($parsedFrame['data'], $frame_offset, $subframe['size']);
                    $frame_offset += $subframe['size'];

                    $subframe['encodingid'] = ord(substr($subframe_rawdata, 0, 1));
                    $subframe['text']       =     substr($subframe_rawdata, 1);
                    $subframe['encoding']   = $this->TextEncodingNameLookup($subframe['encodingid']);
                    $encoding_converted_text = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe['text']));
                    switch (substr($encoding_converted_text, 0, 2)) {
                        case "\xFF\xFE":
                        case "\xFE\xFF":
                            switch (strtoupper($info['id3v2']['encoding'])) {
                                case 'ISO-8859-1':
                                case 'UTF-8':
                                    $encoding_converted_text = substr($encoding_converted_text, 2);
                                    // remove unwanted byte-order-marks
                                    break;
                                default:
                                    // ignore
                                    break;
                            }
                            break;
                        default:
                            // do not remove BOM
                            break;
                    }

                    switch ($subframe['name']) {
                        case 'TIT2':
                            $parsedFrame['chapter_name']        = $encoding_converted_text;
                            $parsedFrame['subframes'][] = $subframe;
                            break;
                        case 'TIT3':
                            $parsedFrame['chapter_description'] = $encoding_converted_text;
                            $parsedFrame['subframes'][] = $subframe;
                            break;
                        case 'WXXX':
                            @list($subframe['chapter_url_description'], $subframe['chapter_url']) = explode("\x00", $encoding_converted_text, 2);
                            $parsedFrame['chapter_url'][$subframe['chapter_url_description']] = $subframe['chapter_url'];
                            $parsedFrame['subframes'][] = $subframe;
                            break;
                        case 'APIC':
                            if (preg_match('#^([^\\x00]+)*\\x00(.)([^\\x00]+)*\\x00(.+)$#s', $subframe['text'], $matches)) {
                                list($dummy, $subframe_apic_mime, $subframe_apic_picturetype, $subframe_apic_description, $subframe_apic_picturedata) = $matches;
                                $subframe['image_mime']   = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe_apic_mime));
                                $subframe['picture_type'] = $this->APICPictureTypeLookup($subframe_apic_picturetype);
                                $subframe['description']  = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe_apic_description));
                                if (strlen($this->TextEncodingTerminatorLookup($subframe['encoding'])) == 2) {
                                    // the null terminator between "description" and "picture data" could be either 1 byte (ISO-8859-1, UTF-8) or two bytes (UTF-16)
                                    // the above regex assumes one byte, if it's actually two then strip the second one here
                                    $subframe_apic_picturedata = substr($subframe_apic_picturedata, 1);
                                }
                                $subframe['data'] = $subframe_apic_picturedata;
                                unset($dummy, $subframe_apic_mime, $subframe_apic_picturetype, $subframe_apic_description, $subframe_apic_picturedata);
                                unset($subframe['text'], $parsedFrame['text']);
                                $parsedFrame['subframes'][] = $subframe;
                                $parsedFrame['picture_present'] = true;
                            } else {
                                $this->warning('ID3v2.CHAP subframe #'.(count($parsedFrame['subframes']) + 1).' "'.$subframe['name'].'" not in expected format');
                            }
                            break;
                        default:
                            $this->warning('ID3v2.CHAP subframe "'.$subframe['name'].'" not handled (supported: TIT2, TIT3, WXXX, APIC)');
                            break;
                    }
                }
                unset($subframe_rawdata, $subframe, $encoding_converted_text);
                unset($parsedFrame['data']); // debatable whether this this be here, without it the returned structure may contain a large amount of duplicate data if chapters contain APIC
            }

            $id3v2_chapter_entry = array();
            foreach (array('id', 'time_begin', 'time_end', 'offset_begin', 'offset_end', 'chapter_name', 'chapter_description', 'chapter_url', 'picture_present') as $id3v2_chapter_key) {
                if (isset($parsedFrame[$id3v2_chapter_key])) {
                    $id3v2_chapter_entry[$id3v2_chapter_key] = $parsedFrame[$id3v2_chapter_key];
                }
            }
            if (!isset($info['id3v2']['chapters'])) {
                $info['id3v2']['chapters'] = array();
            }
            $info['id3v2']['chapters'][] = $id3v2_chapter_entry;
            unset($id3v2_chapter_entry, $id3v2_chapter_key);


        } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'CTOC')) { // CTOC Chapters Table Of Contents frame (ID3v2.3+ only)
            // http://id3.org/id3v2-chapters-1.0
            // <ID3v2.3 or ID3v2.4 frame header, ID: "CTOC">           (10 bytes)
            // Element ID      <text string> $00
            // CTOC flags        %xx
            // Entry count       $xx
            // Child Element ID  <string>$00   /* zero or more child CHAP or CTOC entries */
            // <Optional embedded sub-frames>

            $frame_offset = 0;
            @list($parsedFrame['element_id']) = explode("\x00", $parsedFrame['data'], 2);
            $frame_offset += strlen($parsedFrame['element_id']."\x00");
            $ctoc_flags_raw = ord(substr($parsedFrame['data'], $frame_offset, 1));
            $frame_offset += 1;
            $parsedFrame['entry_count'] = ord(substr($parsedFrame['data'], $frame_offset, 1));
            $frame_offset += 1;

            $terminator_position = null;
            for ($i = 0; $i < $parsedFrame['entry_count']; $i++) {
                $terminator_position = strpos($parsedFrame['data'], "\x00", $frame_offset);
                $parsedFrame['child_element_ids'][$i] = substr($parsedFrame['data'], $frame_offset, $terminator_position - $frame_offset);
                $frame_offset = $terminator_position + 1;
            }

            $parsedFrame['ctoc_flags']['ordered']   = (bool) ($ctoc_flags_raw & 0x01);
            $parsedFrame['ctoc_flags']['top_level'] = (bool) ($ctoc_flags_raw & 0x03);

            unset($ctoc_flags_raw, $terminator_position);

            if ($frame_offset < strlen($parsedFrame['data'])) {
                $parsedFrame['subframes'] = array();
                while ($frame_offset < strlen($parsedFrame['data'])) {
                    // <Optional embedded sub-frames>
                    $subframe = array();
                    $subframe['name']      =                           substr($parsedFrame['data'], $frame_offset, 4);
                    $frame_offset += 4;
                    $subframe['size']      = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4));
                    $frame_offset += 4;
                    $subframe['flags_raw'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2));
                    $frame_offset += 2;
                    if ($subframe['size'] > (strlen($parsedFrame['data']) - $frame_offset)) {
                        $this->warning('CTOS subframe "'.$subframe['name'].'" at frame offset '.$frame_offset.' claims to be "'.$subframe['size'].'" bytes, which is more than the available data ('.(strlen($parsedFrame['data']) - $frame_offset).' bytes)');
                        break;
                    }
                    $subframe_rawdata = substr($parsedFrame['data'], $frame_offset, $subframe['size']);
                    $frame_offset += $subframe['size'];

                    $subframe['encodingid'] = ord(substr($subframe_rawdata, 0, 1));
                    $subframe['text']       =     substr($subframe_rawdata, 1);
                    $subframe['encoding']   = $this->TextEncodingNameLookup($subframe['encodingid']);
                    $encoding_converted_text = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe['text']));;
                    switch (substr($encoding_converted_text, 0, 2)) {
                        case "\xFF\xFE":
                        case "\xFE\xFF":
                            switch (strtoupper($info['id3v2']['encoding'])) {
                                case 'ISO-8859-1':
                                case 'UTF-8':
                                    $encoding_converted_text = substr($encoding_converted_text, 2);
                                    // remove unwanted byte-order-marks
                                    break;
                                default:
                                    // ignore
                                    break;
                            }
                            break;
                        default:
                            // do not remove BOM
                            break;
                    }

                    if (($subframe['name'] == 'TIT2') || ($subframe['name'] == 'TIT3')) {
                        if ($subframe['name'] == 'TIT2') {
                            $parsedFrame['toc_name']        = $encoding_converted_text;
                        } elseif ($subframe['name'] == 'TIT3') {
                            $parsedFrame['toc_description'] = $encoding_converted_text;
                        }
                        $parsedFrame['subframes'][] = $subframe;
                    } else {
                        $this->warning('ID3v2.CTOC subframe "'.$subframe['name'].'" not handled (only TIT2 and TIT3)');
                    }
                }
                unset($subframe_rawdata, $subframe, $encoding_converted_text);
            }

        }

        return true;
    }

    /**
     * @param string $data
     *
     * @return string
     */
    public function DeUnsynchronise($data) {
        return str_replace("\xFF\x00", "\xFF", $data);
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public function LookupExtendedHeaderRestrictionsTagSizeLimits($index) {
        static $LookupExtendedHeaderRestrictionsTagSizeLimits = array(
            0x00 => 'No more than 128 frames and 1 MB total tag size',
            0x01 => 'No more than 64 frames and 128 KB total tag size',
            0x02 => 'No more than 32 frames and 40 KB total tag size',
            0x03 => 'No more than 32 frames and 4 KB total tag size',
        );
        return (isset($LookupExtendedHeaderRestrictionsTagSizeLimits[$index]) ? $LookupExtendedHeaderRestrictionsTagSizeLimits[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public function LookupExtendedHeaderRestrictionsTextEncodings($index) {
        static $LookupExtendedHeaderRestrictionsTextEncodings = array(
            0x00 => 'No restrictions',
            0x01 => 'Strings are only encoded with ISO-8859-1 or UTF-8',
        );
        return (isset($LookupExtendedHeaderRestrictionsTextEncodings[$index]) ? $LookupExtendedHeaderRestrictionsTextEncodings[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public function LookupExtendedHeaderRestrictionsTextFieldSize($index) {
        static $LookupExtendedHeaderRestrictionsTextFieldSize = array(
            0x00 => 'No restrictions',
            0x01 => 'No string is longer than 1024 characters',
            0x02 => 'No string is longer than 128 characters',
            0x03 => 'No string is longer than 30 characters',
        );
        return (isset($LookupExtendedHeaderRestrictionsTextFieldSize[$index]) ? $LookupExtendedHeaderRestrictionsTextFieldSize[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public function LookupExtendedHeaderRestrictionsImageEncoding($index) {
        static $LookupExtendedHeaderRestrictionsImageEncoding = array(
            0x00 => 'No restrictions',
            0x01 => 'Images are encoded only with PNG or JPEG',
        );
        return (isset($LookupExtendedHeaderRestrictionsImageEncoding[$index]) ? $LookupExtendedHeaderRestrictionsImageEncoding[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public function LookupExtendedHeaderRestrictionsImageSizeSize($index) {
        static $LookupExtendedHeaderRestrictionsImageSizeSize = array(
            0x00 => 'No restrictions',
            0x01 => 'All images are 256x256 pixels or smaller',
            0x02 => 'All images are 64x64 pixels or smaller',
            0x03 => 'All images are exactly 64x64 pixels, unless required otherwise',
        );
        return (isset($LookupExtendedHeaderRestrictionsImageSizeSize[$index]) ? $LookupExtendedHeaderRestrictionsImageSizeSize[$index] : '');
    }

    /**
     * @param string $currencyid
     *
     * @return string
     */
    public function LookupCurrencyUnits($currencyid) {

        $begin = __LINE__;

        /** This is not a comment!


            AED    Dirhams
            AFA    Afghanis
            ALL    Leke
            AMD    Drams
            ANG    Guilders
            AOA    Kwanza
            ARS    Pesos
            ATS    Schillings
            AUD    Dollars
            AWG    Guilders
            AZM    Manats
            BAM    Convertible Marka
            BBD    Dollars
            BDT    Taka
            BEF    Francs
            BGL    Leva
            BHD    Dinars
            BIF    Francs
            BMD    Dollars
            BND    Dollars
            BOB    Bolivianos
            BRL    Brazil Real
            BSD    Dollars
            BTN    Ngultrum
            BWP    Pulas
            BYR    Rubles
            BZD    Dollars
            CAD    Dollars
            CDF    Congolese Francs
            CHF    Francs
            CLP    Pesos
            CNY    Yuan Renminbi
            COP    Pesos
            CRC    Colones
            CUP    Pesos
            CVE    Escudos
            CYP    Pounds
            CZK    Koruny
            DEM    Deutsche Marks
            DJF    Francs
            DKK    Kroner
            DOP    Pesos
            DZD    Algeria Dinars
            EEK    Krooni
            EGP    Pounds
            ERN    Nakfa
            ESP    Pesetas
            ETB    Birr
            EUR    Euro
            FIM    Markkaa
            FJD    Dollars
            FKP    Pounds
            FRF    Francs
            GBP    Pounds
            GEL    Lari
            GGP    Pounds
            GHC    Cedis
            GIP    Pounds
            GMD    Dalasi
            GNF    Francs
            GRD    Drachmae
            GTQ    Quetzales
            GYD    Dollars
            HKD    Dollars
            HNL    Lempiras
            HRK    Kuna
            HTG    Gourdes
            HUF    Forints
            IDR    Rupiahs
            IEP    Pounds
            ILS    New Shekels
            IMP    Pounds
            INR    Rupees
            IQD    Dinars
            IRR    Rials
            ISK    Kronur
            ITL    Lire
            JEP    Pounds
            JMD    Dollars
            JOD    Dinars
            JPY    Yen
            KES    Shillings
            KGS    Soms
            KHR    Riels
            KMF    Francs
            KPW    Won
            KWD    Dinars
            KYD    Dollars
            KZT    Tenge
            LAK    Kips
            LBP    Pounds
            LKR    Rupees
            LRD    Dollars
            LSL    Maloti
            LTL    Litai
            LUF    Francs
            LVL    Lati
            LYD    Dinars
            MAD    Dirhams
            MDL    Lei
            MGF    Malagasy Francs
            MKD    Denars
            MMK    Kyats
            MNT    Tugriks
            MOP    Patacas
            MRO    Ouguiyas
            MTL    Liri
            MUR    Rupees
            MVR    Rufiyaa
            MWK    Kwachas
            MXN    Pesos
            MYR    Ringgits
            MZM    Meticais
            NAD    Dollars
            NGN    Nairas
            NIO    Gold Cordobas
            NLG    Guilders
            NOK    Krone
            NPR    Nepal Rupees
            NZD    Dollars
            OMR    Rials
            PAB    Balboa
            PEN    Nuevos Soles
            PGK    Kina
            PHP    Pesos
            PKR    Rupees
            PLN    Zlotych
            PTE    Escudos
            PYG    Guarani
            QAR    Rials
            ROL    Lei
            RUR    Rubles
            RWF    Rwanda Francs
            SAR    Riyals
            SBD    Dollars
            SCR    Rupees
            SDD    Dinars
            SEK    Kronor
            SGD    Dollars
            SHP    Pounds
            SIT    Tolars
            SKK    Koruny
            SLL    Leones
            SOS    Shillings
            SPL    Luigini
            SRG    Guilders
            STD    Dobras
            SVC    Colones
            SYP    Pounds
            SZL    Emalangeni
            THB    Baht
            TJR    Rubles
            TMM    Manats
            TND    Dinars
            TOP    Pa'anga
            TRL    Liras (old)
            TRY    Liras
            TTD    Dollars
            TVD    Tuvalu Dollars
            TWD    New Dollars
            TZS    Shillings
            UAH    Hryvnia
            UGX    Shillings
            USD    Dollars
            UYU    Pesos
            UZS    Sums
            VAL    Lire
            VEB    Bolivares
            VND    Dong
            VUV    Vatu
            WST    Tala
            XAF    Francs
            XAG    Ounces
            XAU    Ounces
            XCD    Dollars
            XDR    Special Drawing Rights
            XPD    Ounces
            XPF    Francs
            XPT    Ounces
            YER    Rials
            YUM    New Dinars
            ZAR    Rand
            ZMK    Kwacha
            ZWD    Zimbabwe Dollars

        */

        return getid3_lib::EmbeddedLookup($currencyid, $begin, __LINE__, __FILE__, 'id3v2-currency-units');
    }

    /**
     * @param string $currencyid
     *
     * @return string
     */
    public function LookupCurrencyCountry($currencyid) {

        $begin = __LINE__;

        /** This is not a comment!

            AED    United Arab Emirates
            AFA    Afghanistan
            ALL    Albania
            AMD    Armenia
            ANG    Netherlands Antilles
            AOA    Angola
            ARS    Argentina
            ATS    Austria
            AUD    Australia
            AWG    Aruba
            AZM    Azerbaijan
            BAM    Bosnia and Herzegovina
            BBD    Barbados
            BDT    Bangladesh
            BEF    Belgium
            BGL    Bulgaria
            BHD    Bahrain
            BIF    Burundi
            BMD    Bermuda
            BND    Brunei Darussalam
            BOB    Bolivia
            BRL    Brazil
            BSD    Bahamas
            BTN    Bhutan
            BWP    Botswana
            BYR    Belarus
            BZD    Belize
            CAD    Canada
            CDF    Congo/Kinshasa
            CHF    Switzerland
            CLP    Chile
            CNY    China
            COP    Colombia
            CRC    Costa Rica
            CUP    Cuba
            CVE    Cape Verde
            CYP    Cyprus
            CZK    Czech Republic
            DEM    Germany
            DJF    Djibouti
            DKK    Denmark
            DOP    Dominican Republic
            DZD    Algeria
            EEK    Estonia
            EGP    Egypt
            ERN    Eritrea
            ESP    Spain
            ETB    Ethiopia
            EUR    Euro Member Countries
            FIM    Finland
            FJD    Fiji
            FKP    Falkland Islands (Malvinas)
            FRF    France
            GBP    United Kingdom
            GEL    Georgia
            GGP    Guernsey
            GHC    Ghana
            GIP    Gibraltar
            GMD    Gambia
            GNF    Guinea
            GRD    Greece
            GTQ    Guatemala
            GYD    Guyana
            HKD    Hong Kong
            HNL    Honduras
            HRK    Croatia
            HTG    Haiti
            HUF    Hungary
            IDR    Indonesia
            IEP    Ireland (Eire)
            ILS    Israel
            IMP    Isle of Man
            INR    India
            IQD    Iraq
            IRR    Iran
            ISK    Iceland
            ITL    Italy
            JEP    Jersey
            JMD    Jamaica
            JOD    Jordan
            JPY    Japan
            KES    Kenya
            KGS    Kyrgyzstan
            KHR    Cambodia
            KMF    Comoros
            KPW    Korea
            KWD    Kuwait
            KYD    Cayman Islands
            KZT    Kazakstan
            LAK    Laos
            LBP    Lebanon
            LKR    Sri Lanka
            LRD    Liberia
            LSL    Lesotho
            LTL    Lithuania
            LUF    Luxembourg
            LVL    Latvia
            LYD    Libya
            MAD    Morocco
            MDL    Moldova
            MGF    Madagascar
            MKD    Macedonia
            MMK    Myanmar (Burma)
            MNT    Mongolia
            MOP    Macau
            MRO    Mauritania
            MTL    Malta
            MUR    Mauritius
            MVR    Maldives (Maldive Islands)
            MWK    Malawi
            MXN    Mexico
            MYR    Malaysia
            MZM    Mozambique
            NAD    Namibia
            NGN    Nigeria
            NIO    Nicaragua
            NLG    Netherlands (Holland)
            NOK    Norway
            NPR    Nepal
            NZD    New Zealand
            OMR    Oman
            PAB    Panama
            PEN    Peru
            PGK    Papua New Guinea
            PHP    Philippines
            PKR    Pakistan
            PLN    Poland
            PTE    Portugal
            PYG    Paraguay
            QAR    Qatar
            ROL    Romania
            RUR    Russia
            RWF    Rwanda
            SAR    Saudi Arabia
            SBD    Solomon Islands
            SCR    Seychelles
            SDD    Sudan
            SEK    Sweden
            SGD    Singapore
            SHP    Saint Helena
            SIT    Slovenia
            SKK    Slovakia
            SLL    Sierra Leone
            SOS    Somalia
            SPL    Seborga
            SRG    Suriname
            STD    São Tome and Principe
            SVC    El Salvador
            SYP    Syria
            SZL    Swaziland
            THB    Thailand
            TJR    Tajikistan
            TMM    Turkmenistan
            TND    Tunisia
            TOP    Tonga
            TRL    Turkey
            TRY    Turkey
            TTD    Trinidad and Tobago
            TVD    Tuvalu
            TWD    Taiwan
            TZS    Tanzania
            UAH    Ukraine
            UGX    Uganda
            USD    United States of America
            UYU    Uruguay
            UZS    Uzbekistan
            VAL    Vatican City
            VEB    Venezuela
            VND    Viet Nam
            VUV    Vanuatu
            WST    Samoa
            XAF    Communauté Financière Africaine
            XAG    Silver
            XAU    Gold
            XCD    East Caribbean
            XDR    International Monetary Fund
            XPD    Palladium
            XPF    Comptoirs Français du Pacifique
            XPT    Platinum
            YER    Yemen
            YUM    Yugoslavia
            ZAR    South Africa
            ZMK    Zambia
            ZWD    Zimbabwe

        */

        return getid3_lib::EmbeddedLookup($currencyid, $begin, __LINE__, __FILE__, 'id3v2-currency-country');
    }

    /**
     * @param string $languagecode
     * @param bool   $casesensitive
     *
     * @return string
     */
    public static function LanguageLookup($languagecode, $casesensitive=false) {

        if (!$casesensitive) {
            $languagecode = strtolower($languagecode);
        }

        // http://www.id3.org/id3v2.4.0-structure.txt
        // [4.   ID3v2 frame overview]
        // The three byte language field, present in several frames, is used to
        // describe the language of the frame's content, according to ISO-639-2
        // [ISO-639-2]. The language should be represented in lower case. If the
        // language is not known the string "XXX" should be used.


        // ISO 639-2 - http://www.id3.org/iso639-2.html

        $begin = __LINE__;

        /** This is not a comment!

            XXX    unknown
            xxx    unknown
            aar    Afar
            abk    Abkhazian
            ace    Achinese
            ach    Acoli
            ada    Adangme
            afa    Afro-Asiatic (Other)
            afh    Afrihili
            afr    Afrikaans
            aka    Akan
            akk    Akkadian
            alb    Albanian
            ale    Aleut
            alg    Algonquian Languages
            amh    Amharic
            ang    English, Old (ca. 450-1100)
            apa    Apache Languages
            ara    Arabic
            arc    Aramaic
            arm    Armenian
            arn    Araucanian
            arp    Arapaho
            art    Artificial (Other)
            arw    Arawak
            asm    Assamese
            ath    Athapascan Languages
            ava    Avaric
            ave    Avestan
            awa    Awadhi
            aym    Aymara
            aze    Azerbaijani
            bad    Banda
            bai    Bamileke Languages
            bak    Bashkir
            bal    Baluchi
            bam    Bambara
            ban    Balinese
            baq    Basque
            bas    Basa
            bat    Baltic (Other)
            bej    Beja
            bel    Byelorussian
            bem    Bemba
            ben    Bengali
            ber    Berber (Other)
            bho    Bhojpuri
            bih    Bihari
            bik    Bikol
            bin    Bini
            bis    Bislama
            bla    Siksika
            bnt    Bantu (Other)
            bod    Tibetan
            bra    Braj
            bre    Breton
            bua    Buriat
            bug    Buginese
            bul    Bulgarian
            bur    Burmese
            cad    Caddo
            cai    Central American Indian (Other)
            car    Carib
            cat    Catalan
            cau    Caucasian (Other)
            ceb    Cebuano
            cel    Celtic (Other)
            ces    Czech
            cha    Chamorro
            chb    Chibcha
            che    Chechen
            chg    Chagatai
            chi    Chinese
            chm    Mari
            chn    Chinook jargon
            cho    Choctaw
            chr    Cherokee
            chu    Church Slavic
            chv    Chuvash
            chy    Cheyenne
            cop    Coptic
            cor    Cornish
            cos    Corsican
            cpe    Creoles and Pidgins, English-based (Other)
            cpf    Creoles and Pidgins, French-based (Other)
            cpp    Creoles and Pidgins, Portuguese-based (Other)
            cre    Cree
            crp    Creoles and Pidgins (Other)
            cus    Cushitic (Other)
            cym    Welsh
            cze    Czech
            dak    Dakota
            dan    Danish
            del    Delaware
            deu    German
            din    Dinka
            div    Divehi
            doi    Dogri
            dra    Dravidian (Other)
            dua    Duala
            dum    Dutch, Middle (ca. 1050-1350)
            dut    Dutch
            dyu    Dyula
            dzo    Dzongkha
            efi    Efik
            egy    Egyptian (Ancient)
            eka    Ekajuk
            ell    Greek, Modern (1453-)
            elx    Elamite
            eng    English
            enm    English, Middle (ca. 1100-1500)
            epo    Esperanto
            esk    Eskimo (Other)
            esl    Spanish
            est    Estonian
            eus    Basque
            ewe    Ewe
            ewo    Ewondo
            fan    Fang
            fao    Faroese
            fas    Persian
            fat    Fanti
            fij    Fijian
            fin    Finnish
            fiu    Finno-Ugrian (Other)
            fon    Fon
            fra    French
            fre    French
            frm    French, Middle (ca. 1400-1600)
            fro    French, Old (842- ca. 1400)
            fry    Frisian
            ful    Fulah
            gaa    Ga
            gae    Gaelic (Scots)
            gai    Irish
            gay    Gayo
            gdh    Gaelic (Scots)
            gem    Germanic (Other)
            geo    Georgian
            ger    German
            gez    Geez
            gil    Gilbertese
            glg    Gallegan
            gmh    German, Middle High (ca. 1050-1500)
            goh    German, Old High (ca. 750-1050)
            gon    Gondi
            got    Gothic
            grb    Grebo
            grc    Greek, Ancient (to 1453)
            gre    Greek, Modern (1453-)
            grn    Guarani
            guj    Gujarati
            hai    Haida
            hau    Hausa
            haw    Hawaiian
            heb    Hebrew
            her    Herero
            hil    Hiligaynon
            him    Himachali
            hin    Hindi
            hmo    Hiri Motu
            hun    Hungarian
            hup    Hupa
            hye    Armenian
            iba    Iban
            ibo    Igbo
            ice    Icelandic
            ijo    Ijo
            iku    Inuktitut
            ilo    Iloko
            ina    Interlingua (International Auxiliary language Association)
            inc    Indic (Other)
            ind    Indonesian
            ine    Indo-European (Other)
            ine    Interlingue
            ipk    Inupiak
            ira    Iranian (Other)
            iri    Irish
            iro    Iroquoian uages
            isl    Icelandic
            ita    Italian
            jav    Javanese
            jaw    Javanese
            jpn    Japanese
            jpr    Judeo-Persian
            jrb    Judeo-Arabic
            kaa    Kara-Kalpak
            kab    Kabyle
            kac    Kachin
            kal    Greenlandic
            kam    Kamba
            kan    Kannada
            kar    Karen
            kas    Kashmiri
            kat    Georgian
            kau    Kanuri
            kaw    Kawi
            kaz    Kazakh
            kha    Khasi
            khi    Khoisan (Other)
            khm    Khmer
            kho    Khotanese
            kik    Kikuyu
            kin    Kinyarwanda
            kir    Kirghiz
            kok    Konkani
            kom    Komi
            kon    Kongo
            kor    Korean
            kpe    Kpelle
            kro    Kru
            kru    Kurukh
            kua    Kuanyama
            kum    Kumyk
            kur    Kurdish
            kus    Kusaie
            kut    Kutenai
            lad    Ladino
            lah    Lahnda
            lam    Lamba
            lao    Lao
            lat    Latin
            lav    Latvian
            lez    Lezghian
            lin    Lingala
            lit    Lithuanian
            lol    Mongo
            loz    Lozi
            ltz    Letzeburgesch
            lub    Luba-Katanga
            lug    Ganda
            lui    Luiseno
            lun    Lunda
            luo    Luo (Kenya and Tanzania)
            mac    Macedonian
            mad    Madurese
            mag    Magahi
            mah    Marshall
            mai    Maithili
            mak    Macedonian
            mak    Makasar
            mal    Malayalam
            man    Mandingo
            mao    Maori
            map    Austronesian (Other)
            mar    Marathi
            mas    Masai
            max    Manx
            may    Malay
            men    Mende
            mga    Irish, Middle (900 - 1200)
            mic    Micmac
            min    Minangkabau
            mis    Miscellaneous (Other)
            mkh    Mon-Kmer (Other)
            mlg    Malagasy
            mlt    Maltese
            mni    Manipuri
            mno    Manobo Languages
            moh    Mohawk
            mol    Moldavian
            mon    Mongolian
            mos    Mossi
            mri    Maori
            msa    Malay
            mul    Multiple Languages
            mun    Munda Languages
            mus    Creek
            mwr    Marwari
            mya    Burmese
            myn    Mayan Languages
            nah    Aztec
            nai    North American Indian (Other)
            nau    Nauru
            nav    Navajo
            nbl    Ndebele, South
            nde    Ndebele, North
            ndo    Ndongo
            nep    Nepali
            new    Newari
            nic    Niger-Kordofanian (Other)
            niu    Niuean
            nla    Dutch
            nno    Norwegian (Nynorsk)
            non    Norse, Old
            nor    Norwegian
            nso    Sotho, Northern
            nub    Nubian Languages
            nya    Nyanja
            nym    Nyamwezi
            nyn    Nyankole
            nyo    Nyoro
            nzi    Nzima
            oci    Langue d'Oc (post 1500)
            oji    Ojibwa
            ori    Oriya
            orm    Oromo
            osa    Osage
            oss    Ossetic
            ota    Turkish, Ottoman (1500 - 1928)
            oto    Otomian Languages
            paa    Papuan-Australian (Other)
            pag    Pangasinan
            pal    Pahlavi
            pam    Pampanga
            pan    Panjabi
            pap    Papiamento
            pau    Palauan
            peo    Persian, Old (ca 600 - 400 B.C.)
            per    Persian
            phn    Phoenician
            pli    Pali
            pol    Polish
            pon    Ponape
            por    Portuguese
            pra    Prakrit uages
            pro    Provencal, Old (to 1500)
            pus    Pushto
            que    Quechua
            raj    Rajasthani
            rar    Rarotongan
            roa    Romance (Other)
            roh    Rhaeto-Romance
            rom    Romany
            ron    Romanian
            rum    Romanian
            run    Rundi
            rus    Russian
            sad    Sandawe
            sag    Sango
            sah    Yakut
            sai    South American Indian (Other)
            sal    Salishan Languages
            sam    Samaritan Aramaic
            san    Sanskrit
            sco    Scots
            scr    Serbo-Croatian
            sel    Selkup
            sem    Semitic (Other)
            sga    Irish, Old (to 900)
            shn    Shan
            sid    Sidamo
            sin    Singhalese
            sio    Siouan Languages
            sit    Sino-Tibetan (Other)
            sla    Slavic (Other)
            slk    Slovak
            slo    Slovak
            slv    Slovenian
            smi    Sami Languages
            smo    Samoan
            sna    Shona
            snd    Sindhi
            sog    Sogdian
            som    Somali
            son    Songhai
            sot    Sotho, Southern
            spa    Spanish
            sqi    Albanian
            srd    Sardinian
            srr    Serer
            ssa    Nilo-Saharan (Other)
            ssw    Siswant
            ssw    Swazi
            suk    Sukuma
            sun    Sudanese
            sus    Susu
            sux    Sumerian
            sve    Swedish
            swa    Swahili
            swe    Swedish
            syr    Syriac
            tah    Tahitian
            tam    Tamil
            tat    Tatar
            tel    Telugu
            tem    Timne
            ter    Tereno
            tgk    Tajik
            tgl    Tagalog
            tha    Thai
            tib    Tibetan
            tig    Tigre
            tir    Tigrinya
            tiv    Tivi
            tli    Tlingit
            tmh    Tamashek
            tog    Tonga (Nyasa)
            ton    Tonga (Tonga Islands)
            tru    Truk
            tsi    Tsimshian
            tsn    Tswana
            tso    Tsonga
            tuk    Turkmen
            tum    Tumbuka
            tur    Turkish
            tut    Altaic (Other)
            twi    Twi
            tyv    Tuvinian
            uga    Ugaritic
            uig    Uighur
            ukr    Ukrainian
            umb    Umbundu
            und    Undetermined
            urd    Urdu
            uzb    Uzbek
            vai    Vai
            ven    Venda
            vie    Vietnamese
            vol    Volapük
            vot    Votic
            wak    Wakashan Languages
            wal    Walamo
            war    Waray
            was    Washo
            wel    Welsh
            wen    Sorbian Languages
            wol    Wolof
            xho    Xhosa
            yao    Yao
            yap    Yap
            yid    Yiddish
            yor    Yoruba
            zap    Zapotec
            zen    Zenaga
            zha    Zhuang
            zho    Chinese
            zul    Zulu
            zun    Zuni

        */

        return getid3_lib::EmbeddedLookup($languagecode, $begin, __LINE__, __FILE__, 'id3v2-languagecode');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public static function ETCOEventLookup($index) {
        if (($index >= 0x17) && ($index <= 0xDF)) {
            return 'reserved for future use';
        }
        if (($index >= 0xE0) && ($index <= 0xEF)) {
            return 'not predefined synch 0-F';
        }
        if (($index >= 0xF0) && ($index <= 0xFC)) {
            return 'reserved for future use';
        }

        static $EventLookup = array(
            0x00 => 'padding (has no meaning)',
            0x01 => 'end of initial silence',
            0x02 => 'intro start',
            0x03 => 'main part start',
            0x04 => 'outro start',
            0x05 => 'outro end',
            0x06 => 'verse start',
            0x07 => 'refrain start',
            0x08 => 'interlude start',
            0x09 => 'theme start',
            0x0A => 'variation start',
            0x0B => 'key change',
            0x0C => 'time change',
            0x0D => 'momentary unwanted noise (Snap, Crackle & Pop)',
            0x0E => 'sustained noise',
            0x0F => 'sustained noise end',
            0x10 => 'intro end',
            0x11 => 'main part end',
            0x12 => 'verse end',
            0x13 => 'refrain end',
            0x14 => 'theme end',
            0x15 => 'profanity',
            0x16 => 'profanity end',
            0xFD => 'audio end (start of silence)',
            0xFE => 'audio file ends',
            0xFF => 'one more byte of events follows'
        );

        return (isset($EventLookup[$index]) ? $EventLookup[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public static function SYTLContentTypeLookup($index) {
        static $SYTLContentTypeLookup = array(
            0x00 => 'other',
            0x01 => 'lyrics',
            0x02 => 'text transcription',
            0x03 => 'movement/part name', // (e.g. 'Adagio')
            0x04 => 'events',             // (e.g. 'Don Quijote enters the stage')
            0x05 => 'chord',              // (e.g. 'Bb F Fsus')
            0x06 => 'trivia/\'pop up\' information',
            0x07 => 'URLs to webpages',
            0x08 => 'URLs to images'
        );

        return (isset($SYTLContentTypeLookup[$index]) ? $SYTLContentTypeLookup[$index] : '');
    }

    /**
     * @param int   $index
     * @param bool $returnarray
     *
     * @return array|string
     */
    public static function APICPictureTypeLookup($index, $returnarray=false) {
        static $APICPictureTypeLookup = array(
            0x00 => 'Other',
            0x01 => '32x32 pixels \'file icon\' (PNG only)',
            0x02 => 'Other file icon',
            0x03 => 'Cover (front)',
            0x04 => 'Cover (back)',
            0x05 => 'Leaflet page',
            0x06 => 'Media (e.g. label side of CD)',
            0x07 => 'Lead artist/lead performer/soloist',
            0x08 => 'Artist/performer',
            0x09 => 'Conductor',
            0x0A => 'Band/Orchestra',
            0x0B => 'Composer',
            0x0C => 'Lyricist/text writer',
            0x0D => 'Recording Location',
            0x0E => 'During recording',
            0x0F => 'During performance',
            0x10 => 'Movie/video screen capture',
            0x11 => 'A bright coloured fish',
            0x12 => 'Illustration',
            0x13 => 'Band/artist logotype',
            0x14 => 'Publisher/Studio logotype'
        );
        if ($returnarray) {
            return $APICPictureTypeLookup;
        }
        return (isset($APICPictureTypeLookup[$index]) ? $APICPictureTypeLookup[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public static function COMRReceivedAsLookup($index) {
        static $COMRReceivedAsLookup = array(
            0x00 => 'Other',
            0x01 => 'Standard CD album with other songs',
            0x02 => 'Compressed audio on CD',
            0x03 => 'File over the Internet',
            0x04 => 'Stream over the Internet',
            0x05 => 'As note sheets',
            0x06 => 'As note sheets in a book with other sheets',
            0x07 => 'Music on other media',
            0x08 => 'Non-musical merchandise'
        );

        return (isset($COMRReceivedAsLookup[$index]) ? $COMRReceivedAsLookup[$index] : '');
    }

    /**
     * @param int $index
     *
     * @return string
     */
    public static function RVA2ChannelTypeLookup($index) {
        static $RVA2ChannelTypeLookup = array(
            0x00 => 'Other',
            0x01 => 'Master volume',
            0x02 => 'Front right',
            0x03 => 'Front left',
            0x04 => 'Back right',
            0x05 => 'Back left',
            0x06 => 'Front centre',
            0x07 => 'Back centre',
            0x08 => 'Subwoofer'
        );

        return (isset($RVA2ChannelTypeLookup[$index]) ? $RVA2ChannelTypeLookup[$index] : '');
    }

    /**
     * @param string $framename
     *
     * @return string
     */
    public static function FrameNameLongLookup($framename) {

        $begin = __LINE__;

        /** This is not a comment!

            AENC    Audio encryption
            APIC    Attached picture
            ASPI    Audio seek point index
            BUF    Recommended buffer size
            CNT    Play counter
            COM    Comments
            COMM    Comments
            COMR    Commercial frame
            CRA    Audio encryption
            CRM    Encrypted meta frame
            ENCR    Encryption method registration
            EQU    Equalisation
            EQU2    Equalisation (2)
            EQUA    Equalisation
            ETC    Event timing codes
            ETCO    Event timing codes
            GEO    General encapsulated object
            GEOB    General encapsulated object
            GRID    Group identification registration
            IPL    Involved people list
            IPLS    Involved people list
            LINK    Linked information
            LNK    Linked information
            MCDI    Music CD identifier
            MCI    Music CD Identifier
            MLL    MPEG location lookup table
            MLLT    MPEG location lookup table
            OWNE    Ownership frame
            PCNT    Play counter
            PIC    Attached picture
            POP    Popularimeter
            POPM    Popularimeter
            POSS    Position synchronisation frame
            PRIV    Private frame
            RBUF    Recommended buffer size
            REV    Reverb
            RVA    Relative volume adjustment
            RVA2    Relative volume adjustment (2)
            RVAD    Relative volume adjustment
            RVRB    Reverb
            SEEK    Seek frame
            SIGN    Signature frame
            SLT    Synchronised lyric/text
            STC    Synced tempo codes
            SYLT    Synchronised lyric/text
            SYTC    Synchronised tempo codes
            TAL    Album/Movie/Show title
            TALB    Album/Movie/Show title
            TBP    BPM (Beats Per Minute)
            TBPM    BPM (beats per minute)
            TCM    Composer
            TCMP    Part of a compilation
            TCO    Content type
            TCOM    Composer
            TCON    Content type
            TCOP    Copyright message
            TCP    Part of a compilation
            TCR    Copyright message
            TDA    Date
            TDAT    Date
            TDEN    Encoding time
            TDLY    Playlist delay
            TDOR    Original release time
            TDRC    Recording time
            TDRL    Release time
            TDTG    Tagging time
            TDY    Playlist delay
            TEN    Encoded by
            TENC    Encoded by
            TEXT    Lyricist/Text writer
            TFLT    File type
            TFT    File type
            TIM    Time
            TIME    Time
            TIPL    Involved people list
            TIT1    Content group description
            TIT2    Title/songname/content description
            TIT3    Subtitle/Description refinement
            TKE    Initial key
            TKEY    Initial key
            TLA    Language(s)
            TLAN    Language(s)
            TLE    Length
            TLEN    Length
            TMCL    Musician credits list
            TMED    Media type
            TMOO    Mood
            TMT    Media type
            TOA    Original artist(s)/performer(s)
            TOAL    Original album/movie/show title
            TOF    Original filename
            TOFN    Original filename
            TOL    Original Lyricist(s)/text writer(s)
            TOLY    Original lyricist(s)/text writer(s)
            TOPE    Original artist(s)/performer(s)
            TOR    Original release year
            TORY    Original release year
            TOT    Original album/Movie/Show title
            TOWN    File owner/licensee
            TP1    Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group
            TP2    Band/Orchestra/Accompaniment
            TP3    Conductor/Performer refinement
            TP4    Interpreted, remixed, or otherwise modified by
            TPA    Part of a set
            TPB    Publisher
            TPE1    Lead performer(s)/Soloist(s)
            TPE2    Band/orchestra/accompaniment
            TPE3    Conductor/performer refinement
            TPE4    Interpreted, remixed, or otherwise modified by
            TPOS    Part of a set
            TPRO    Produced notice
            TPUB    Publisher
            TRC    ISRC (International Standard Recording Code)
            TRCK    Track number/Position in set
            TRD    Recording dates
            TRDA    Recording dates
            TRK    Track number/Position in set
            TRSN    Internet radio station name
            TRSO    Internet radio station owner
            TS2    Album-Artist sort order
            TSA    Album sort order
            TSC    Composer sort order
            TSI    Size
            TSIZ    Size
            TSO2    Album-Artist sort order
            TSOA    Album sort order
            TSOC    Composer sort order
            TSOP    Performer sort order
            TSOT    Title sort order
            TSP    Performer sort order
            TSRC    ISRC (international standard recording code)
            TSS    Software/hardware and settings used for encoding
            TSSE    Software/Hardware and settings used for encoding
            TSST    Set subtitle
            TST    Title sort order
            TT1    Content group description
            TT2    Title/Songname/Content description
            TT3    Subtitle/Description refinement
            TXT    Lyricist/text writer
            TXX    User defined text information frame
            TXXX    User defined text information frame
            TYE    Year
            TYER    Year
            UFI    Unique file identifier
            UFID    Unique file identifier
            ULT    Unsynchronised lyric/text transcription
            USER    Terms of use
            USLT    Unsynchronised lyric/text transcription
            WAF    Official audio file webpage
            WAR    Official artist/performer webpage
            WAS    Official audio source webpage
            WCM    Commercial information
            WCOM    Commercial information
            WCOP    Copyright/Legal information
            WCP    Copyright/Legal information
            WOAF    Official audio file webpage
            WOAR    Official artist/performer webpage
            WOAS    Official audio source webpage
            WORS    Official Internet radio station homepage
            WPAY    Payment
            WPB    Publishers official webpage
            WPUB    Publishers official webpage
            WXX    User defined URL link frame
            WXXX    User defined URL link frame
            TFEA    Featured Artist
            TSTU    Recording Studio
            rgad    Replay Gain Adjustment

        */

        return getid3_lib::EmbeddedLookup($framename, $begin, __LINE__, __FILE__, 'id3v2-framename_long');

        // Last three:
        // from Helium2 [www.helium2.com]
        // from http://privatewww.essex.ac.uk/~djmrob/replaygain/file_format_id3v2.html
    }

    /**
     * @param string $framename
     *
     * @return string
     */
    public static function FrameNameShortLookup($framename) {

        $begin = __LINE__;

        /** This is not a comment!

            AENC    audio_encryption
            APIC    attached_picture
            ASPI    audio_seek_point_index
            BUF    recommended_buffer_size
            CNT    play_counter
            COM    comment
            COMM    comment
            COMR    commercial_frame
            CRA    audio_encryption
            CRM    encrypted_meta_frame
            ENCR    encryption_method_registration
            EQU    equalisation
            EQU2    equalisation
            EQUA    equalisation
            ETC    event_timing_codes
            ETCO    event_timing_codes
            GEO    general_encapsulated_object
            GEOB    general_encapsulated_object
            GRID    group_identification_registration
            IPL    involved_people_list
            IPLS    involved_people_list
            LINK    linked_information
            LNK    linked_information
            MCDI    music_cd_identifier
            MCI    music_cd_identifier
            MLL    mpeg_location_lookup_table
            MLLT    mpeg_location_lookup_table
            OWNE    ownership_frame
            PCNT    play_counter
            PIC    attached_picture
            POP    popularimeter
            POPM    popularimeter
            POSS    position_synchronisation_frame
            PRIV    private_frame
            RBUF    recommended_buffer_size
            REV    reverb
            RVA    relative_volume_adjustment
            RVA2    relative_volume_adjustment
            RVAD    relative_volume_adjustment
            RVRB    reverb
            SEEK    seek_frame
            SIGN    signature_frame
            SLT    synchronised_lyric
            STC    synced_tempo_codes
            SYLT    synchronised_lyric
            SYTC    synchronised_tempo_codes
            TAL    album
            TALB    album
            TBP    bpm
            TBPM    bpm
            TCM    composer
            TCMP    part_of_a_compilation
            TCO    genre
            TCOM    composer
            TCON    genre
            TCOP    copyright_message
            TCP    part_of_a_compilation
            TCR    copyright_message
            TDA    date
            TDAT    date
            TDEN    encoding_time
            TDLY    playlist_delay
            TDOR    original_release_time
            TDRC    recording_time
            TDRL    release_time
            TDTG    tagging_time
            TDY    playlist_delay
            TEN    encoded_by
            TENC    encoded_by
            TEXT    lyricist
            TFLT    file_type
            TFT    file_type
            TIM    time
            TIME    time
            TIPL    involved_people_list
            TIT1    content_group_description
            TIT2    title
            TIT3    subtitle
            TKE    initial_key
            TKEY    initial_key
            TLA    language
            TLAN    language
            TLE    length
            TLEN    length
            TMCL    musician_credits_list
            TMED    media_type
            TMOO    mood
            TMT    media_type
            TOA    original_artist
            TOAL    original_album
            TOF    original_filename
            TOFN    original_filename
            TOL    original_lyricist
            TOLY    original_lyricist
            TOPE    original_artist
            TOR    original_year
            TORY    original_year
            TOT    original_album
            TOWN    file_owner
            TP1    artist
            TP2    band
            TP3    conductor
            TP4    remixer
            TPA    part_of_a_set
            TPB    publisher
            TPE1    artist
            TPE2    band
            TPE3    conductor
            TPE4    remixer
            TPOS    part_of_a_set
            TPRO    produced_notice
            TPUB    publisher
            TRC    isrc
            TRCK    track_number
            TRD    recording_dates
            TRDA    recording_dates
            TRK    track_number
            TRSN    internet_radio_station_name
            TRSO    internet_radio_station_owner
            TS2    album_artist_sort_order
            TSA    album_sort_order
            TSC    composer_sort_order
            TSI    size
            TSIZ    size
            TSO2    album_artist_sort_order
            TSOA    album_sort_order
            TSOC    composer_sort_order
            TSOP    performer_sort_order
            TSOT    title_sort_order
            TSP    performer_sort_order
            TSRC    isrc
            TSS    encoder_settings
            TSSE    encoder_settings
            TSST    set_subtitle
            TST    title_sort_order
            TT1    content_group_description
            TT2    title
            TT3    subtitle
            TXT    lyricist
            TXX    text
            TXXX    text
            TYE    year
            TYER    year
            UFI    unique_file_identifier
            UFID    unique_file_identifier
            ULT    unsynchronised_lyric
            USER    terms_of_use
            USLT    unsynchronised_lyric
            WAF    url_file
            WAR    url_artist
            WAS    url_source
            WCM    commercial_information
            WCOM    commercial_information
            WCOP    copyright
            WCP    copyright
            WOAF    url_file
            WOAR    url_artist
            WOAS    url_source
            WORS    url_station
            WPAY    url_payment
            WPB    url_publisher
            WPUB    url_publisher
            WXX    url_user
            WXXX    url_user
            TFEA    featured_artist
            TSTU    recording_studio
            rgad    replay_gain_adjustment

        */

        return getid3_lib::EmbeddedLookup($framename, $begin, __LINE__, __FILE__, 'id3v2-framename_short');
    }

    /**
     * @param string $encoding
     *
     * @return string
     */
    public static function TextEncodingTerminatorLookup($encoding) {
        // http://www.id3.org/id3v2.4.0-structure.txt
        // Frames that allow different types of text encoding contains a text encoding description byte. Possible encodings:
        static $TextEncodingTerminatorLookup = array(
            0   => "\x00",     // $00  ISO-8859-1. Terminated with $00.
            1   => "\x00\x00", // $01  UTF-16 encoded Unicode with BOM. All strings in the same frame SHALL have the same byteorder. Terminated with $00 00.
            2   => "\x00\x00", // $02  UTF-16BE encoded Unicode without BOM. Terminated with $00 00.
            3   => "\x00",     // $03  UTF-8 encoded Unicode. Terminated with $00.
            255 => "\x00\x00"
        );
        return (isset($TextEncodingTerminatorLookup[$encoding]) ? $TextEncodingTerminatorLookup[$encoding] : "\x00");
    }

    /**
     * @param int $encoding
     *
     * @return string
     */
    public static function TextEncodingNameLookup($encoding) {
        // http://www.id3.org/id3v2.4.0-structure.txt
        // Frames that allow different types of text encoding contains a text encoding description byte. Possible encodings:
        static $TextEncodingNameLookup = array(
            0   => 'ISO-8859-1', // $00  ISO-8859-1. Terminated with $00.
            1   => 'UTF-16',     // $01  UTF-16 encoded Unicode with BOM. All strings in the same frame SHALL have the same byteorder. Terminated with $00 00.
            2   => 'UTF-16BE',   // $02  UTF-16BE encoded Unicode without BOM. Terminated with $00 00.
            3   => 'UTF-8',      // $03  UTF-8 encoded Unicode. Terminated with $00.
            255 => 'UTF-16BE'
        );
        return (isset($TextEncodingNameLookup[$encoding]) ? $TextEncodingNameLookup[$encoding] : 'ISO-8859-1');
    }

    /**
     * @param string $string
     * @param string $terminator
     *
     * @return string
     */
    public static function RemoveStringTerminator($string, $terminator) {
        // Null terminator at end of comment string is somewhat ambiguous in the specification, may or may not be implemented by various taggers. Remove terminator only if present.
        // https://github.com/JamesHeinrich/getID3/issues/121
        // https://community.mp3tag.de/t/x-trailing-nulls-in-id3v2-comments/19227
        if (substr($string, -strlen($terminator), strlen($terminator)) === $terminator) {
            $string = substr($string, 0, -strlen($terminator));
        }
        return $string;
    }

    /**
     * @param string $string
     *
     * @return string
     */
    public static function MakeUTF16emptyStringEmpty($string) {
        if (in_array($string, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) {
            // if string only contains a BOM or terminator then make it actually an empty string
            $string = '';
        }
        return $string;
    }

    /**
     * @param string $framename
     * @param int    $id3v2majorversion
     *
     * @return bool|int
     */
    public static function IsValidID3v2FrameName($framename, $id3v2majorversion) {
        switch ($id3v2majorversion) {
            case 2:
                return preg_match('#[A-Z][A-Z0-9]{2}#', $framename);

            case 3:
            case 4:
                return preg_match('#[A-Z][A-Z0-9]{3}#', $framename);
        }
        return false;
    }

    /**
     * @param string $numberstring
     * @param bool   $allowdecimal
     * @param bool   $allownegative
     *
     * @return bool
     */
    public static function IsANumber($numberstring, $allowdecimal=false, $allownegative=false) {
        $pattern  = '#^';
        $pattern .= ($allownegative ? '\\-?' : '');
        $pattern .= '[0-9]+';
        $pattern .= ($allowdecimal  ? '(\\.[0-9]+)?' : '');
        $pattern .= '$#';
        return preg_match($pattern, $numberstring);
    }

    /**
     * @param string $datestamp
     *
     * @return bool
     */
    public static function IsValidDateStampString($datestamp) {
        if (!preg_match('#^[12][0-9]{3}[01][0-9][0123][0-9]$#', $datestamp)) {
            return false;
        }
        $year  = substr($datestamp, 0, 4);
        $month = substr($datestamp, 4, 2);
        $day   = substr($datestamp, 6, 2);
        if (($year == 0) || ($month == 0) || ($day == 0)) {
            return false;
        }
        if ($month > 12) {
            return false;
        }
        if ($day > 31) {
            return false;
        }
        if (($day > 30) && (($month == 4) || ($month == 6) || ($month == 9) || ($month == 11))) {
            return false;
        }
        if (($day > 29) && ($month == 2)) {
            return false;
        }
        return true;
    }

    /**
     * @param int $majorversion
     *
     * @return int
     */
    public static function ID3v2HeaderLength($majorversion) {
        return (($majorversion == 2) ? 6 : 10);
    }

    /**
     * @param string $frame_name
     *
     * @return string|false
     */
    public static function ID3v22iTunesBrokenFrameName($frame_name) {
        // iTunes (multiple versions) has been known to write ID3v2.3 style frames
        // but use ID3v2.2 frame names, right-padded using either [space] or [null]
        // to make them fit in the 4-byte frame name space of the ID3v2.3 frame.
        // This function will detect and translate the corrupt frame name into ID3v2.3 standard.
        static $ID3v22_iTunes_BrokenFrames = array(
            'BUF' => 'RBUF', // Recommended buffer size
            'CNT' => 'PCNT', // Play counter
            'COM' => 'COMM', // Comments
            'CRA' => 'AENC', // Audio encryption
            'EQU' => 'EQUA', // Equalisation
            'ETC' => 'ETCO', // Event timing codes
            'GEO' => 'GEOB', // General encapsulated object
            'IPL' => 'IPLS', // Involved people list
            'LNK' => 'LINK', // Linked information
            'MCI' => 'MCDI', // Music CD identifier
            'MLL' => 'MLLT', // MPEG location lookup table
            'PIC' => 'APIC', // Attached picture
            'POP' => 'POPM', // Popularimeter
            'REV' => 'RVRB', // Reverb
            'RVA' => 'RVAD', // Relative volume adjustment
            'SLT' => 'SYLT', // Synchronised lyric/text
            'STC' => 'SYTC', // Synchronised tempo codes
            'TAL' => 'TALB', // Album/Movie/Show title
            'TBP' => 'TBPM', // BPM (beats per minute)
            'TCM' => 'TCOM', // Composer
            'TCO' => 'TCON', // Content type
            'TCP' => 'TCMP', // Part of a compilation
            'TCR' => 'TCOP', // Copyright message
            'TDA' => 'TDAT', // Date
            'TDY' => 'TDLY', // Playlist delay
            'TEN' => 'TENC', // Encoded by
            'TFT' => 'TFLT', // File type
            'TIM' => 'TIME', // Time
            'TKE' => 'TKEY', // Initial key
            'TLA' => 'TLAN', // Language(s)
            'TLE' => 'TLEN', // Length
            'TMT' => 'TMED', // Media type
            'TOA' => 'TOPE', // Original artist(s)/performer(s)
            'TOF' => 'TOFN', // Original filename
            'TOL' => 'TOLY', // Original lyricist(s)/text writer(s)
            'TOR' => 'TORY', // Original release year
            'TOT' => 'TOAL', // Original album/movie/show title
            'TP1' => 'TPE1', // Lead performer(s)/Soloist(s)
            'TP2' => 'TPE2', // Band/orchestra/accompaniment
            'TP3' => 'TPE3', // Conductor/performer refinement
            'TP4' => 'TPE4', // Interpreted, remixed, or otherwise modified by
            'TPA' => 'TPOS', // Part of a set
            'TPB' => 'TPUB', // Publisher
            'TRC' => 'TSRC', // ISRC (international standard recording code)
            'TRD' => 'TRDA', // Recording dates
            'TRK' => 'TRCK', // Track number/Position in set
            'TS2' => 'TSO2', // Album-Artist sort order
            'TSA' => 'TSOA', // Album sort order
            'TSC' => 'TSOC', // Composer sort order
            'TSI' => 'TSIZ', // Size
            'TSP' => 'TSOP', // Performer sort order
            'TSS' => 'TSSE', // Software/Hardware and settings used for encoding
            'TST' => 'TSOT', // Title sort order
            'TT1' => 'TIT1', // Content group description
            'TT2' => 'TIT2', // Title/songname/content description
            'TT3' => 'TIT3', // Subtitle/Description refinement
            'TXT' => 'TEXT', // Lyricist/Text writer
            'TXX' => 'TXXX', // User defined text information frame
            'TYE' => 'TYER', // Year
            'UFI' => 'UFID', // Unique file identifier
            'ULT' => 'USLT', // Unsynchronised lyric/text transcription
            'WAF' => 'WOAF', // Official audio file webpage
            'WAR' => 'WOAR', // Official artist/performer webpage
            'WAS' => 'WOAS', // Official audio source webpage
            'WCM' => 'WCOM', // Commercial information
            'WCP' => 'WCOP', // Copyright/Legal information
            'WPB' => 'WPUB', // Publishers official webpage
            'WXX' => 'WXXX', // User defined URL link frame
        );
        if (strlen($frame_name) == 4) {
            if ((substr($frame_name, 3, 1) == ' ') || (substr($frame_name, 3, 1) == "\x00")) {
                if (isset($ID3v22_iTunes_BrokenFrames[substr($frame_name, 0, 3)])) {
                    return $ID3v22_iTunes_BrokenFrames[substr($frame_name, 0, 3)];
                }
            }
        }
        return false;
    }

}