rlm@3: | rlm@3: // | Allan Hansen | rlm@3: // +----------------------------------------------------------------------+ rlm@3: // | module.audio.xiph.php | rlm@3: // | Module for analyzing Xiph.org audio file formats: | rlm@3: // | Ogg Vorbis, FLAC, OggFLAC and Speex - not Ogg Theora | rlm@3: // | dependencies: module.lib.image_size.php (optional) | rlm@3: // +----------------------------------------------------------------------+ rlm@3: // rlm@3: // $Id: module.audio.xiph.php,v 1.5 2006/12/03 21:12:43 ah Exp $ rlm@3: rlm@3: rlm@3: rlm@3: class getid3_xiph extends getid3_handler rlm@3: { rlm@3: rlm@3: public function Analyze() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: if ($getid3->option_tags_images) { rlm@3: $getid3->include_module('lib.image_size'); rlm@3: } rlm@3: rlm@3: fseek($getid3->fp, $getid3->info['avdataoffset'], SEEK_SET); rlm@3: rlm@3: $magic = fread($getid3->fp, 4); rlm@3: rlm@3: if ($magic == 'OggS') { rlm@3: return $this->ParseOgg(); rlm@3: } rlm@3: rlm@3: if ($magic == 'fLaC') { rlm@3: return $this->ParseFLAC(); rlm@3: } rlm@3: rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function ParseOgg() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: fseek($getid3->fp, $getid3->info['avdataoffset'], SEEK_SET); rlm@3: rlm@3: $getid3->info['audio'] = $getid3->info['ogg'] = array (); rlm@3: $info_ogg = &$getid3->info['ogg']; rlm@3: $info_audio = &$getid3->info['audio']; rlm@3: rlm@3: $getid3->info['fileformat'] = 'ogg'; rlm@3: rlm@3: rlm@3: //// Page 1 - Stream Header rlm@3: rlm@3: $ogg_page_info = $this->ParseOggPageHeader(); rlm@3: $info_ogg['pageheader'][$ogg_page_info['page_seqno']] = $ogg_page_info; rlm@3: rlm@3: if (ftell($getid3->fp) >= getid3::FREAD_BUFFER_SIZE) { rlm@3: throw new getid3_exception('Could not find start of Ogg page in the first '.getid3::FREAD_BUFFER_SIZE.' bytes (this might not be an Ogg file?)'); rlm@3: } rlm@3: rlm@3: $file_data = fread($getid3->fp, $ogg_page_info['page_length']); rlm@3: $file_data_offset = 0; rlm@3: rlm@3: rlm@3: // OggFLAC rlm@3: if (substr($file_data, 0, 4) == 'fLaC') { rlm@3: rlm@3: $info_audio['dataformat'] = 'flac'; rlm@3: $info_audio['bitrate_mode'] = 'vbr'; rlm@3: $info_audio['lossless'] = true; rlm@3: rlm@3: } rlm@3: rlm@3: rlm@3: // Ogg Vorbis rlm@3: elseif (substr($file_data, 1, 6) == 'vorbis') { rlm@3: rlm@3: $info_audio['dataformat'] = 'vorbis'; rlm@3: $info_audio['lossless'] = false; rlm@3: rlm@3: $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int($file_data[0]); rlm@3: $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['stream_type'] = substr($file_data, 1, 6); // hard-coded to 'vorbis' rlm@3: rlm@3: getid3_lib::ReadSequence('LittleEndian2Int', $info_ogg, $file_data, 7, rlm@3: array ( rlm@3: 'bitstreamversion' => 4, rlm@3: 'numberofchannels' => 1, rlm@3: 'samplerate' => 4, rlm@3: 'bitrate_max' => 4, rlm@3: 'bitrate_nominal' => 4, rlm@3: 'bitrate_min' => 4 rlm@3: ) rlm@3: ); rlm@3: rlm@3: $n28 = getid3_lib::LittleEndian2Int($file_data{28}); rlm@3: $info_ogg['blocksize_small'] = pow(2, $n28 & 0x0F); rlm@3: $info_ogg['blocksize_large'] = pow(2, ($n28 & 0xF0) >> 4); rlm@3: $info_ogg['stop_bit'] = $n28; rlm@3: rlm@3: $info_audio['channels'] = $info_ogg['numberofchannels']; rlm@3: $info_audio['sample_rate'] = $info_ogg['samplerate']; rlm@3: rlm@3: $info_audio['bitrate_mode'] = 'vbr'; // overridden if actually abr rlm@3: rlm@3: if ($info_ogg['bitrate_max'] == 0xFFFFFFFF) { rlm@3: unset($info_ogg['bitrate_max']); rlm@3: $info_audio['bitrate_mode'] = 'abr'; rlm@3: } rlm@3: rlm@3: if ($info_ogg['bitrate_nominal'] == 0xFFFFFFFF) { rlm@3: unset($info_ogg['bitrate_nominal']); rlm@3: } rlm@3: rlm@3: if ($info_ogg['bitrate_min'] == 0xFFFFFFFF) { rlm@3: unset($info_ogg['bitrate_min']); rlm@3: $info_audio['bitrate_mode'] = 'abr'; rlm@3: } rlm@3: } rlm@3: rlm@3: rlm@3: // Speex rlm@3: elseif (substr($file_data, 0, 8) == 'Speex ') { rlm@3: rlm@3: // http://www.speex.org/manual/node10.html rlm@3: rlm@3: $info_audio['dataformat'] = 'speex'; rlm@3: $getid3->info['mime_type'] = 'audio/speex'; rlm@3: $info_audio['bitrate_mode'] = 'abr'; rlm@3: $info_audio['lossless'] = false; rlm@3: rlm@3: getid3_lib::ReadSequence('LittleEndian2Int', $info_ogg['pageheader'][$ogg_page_info['page_seqno']], $file_data, 0, rlm@3: array ( rlm@3: 'speex_string' => -8, // hard-coded to 'Speex ' rlm@3: 'speex_version' => -20, // string rlm@3: 'speex_version_id' => 4, rlm@3: 'header_size' => 4, rlm@3: 'rate' => 4, rlm@3: 'mode' => 4, rlm@3: 'mode_bitstream_version' => 4, rlm@3: 'nb_channels' => 4, rlm@3: 'bitrate' => 4, rlm@3: 'framesize' => 4, rlm@3: 'vbr' => 4, rlm@3: 'frames_per_packet' => 4, rlm@3: 'extra_headers' => 4, rlm@3: 'reserved1' => 4, rlm@3: 'reserved2' => 4 rlm@3: ) rlm@3: ); rlm@3: rlm@3: $getid3->info['speex']['speex_version'] = trim($info_ogg['pageheader'][$ogg_page_info['page_seqno']]['speex_version']); rlm@3: $getid3->info['speex']['sample_rate'] = $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['rate']; rlm@3: $getid3->info['speex']['channels'] = $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['nb_channels']; rlm@3: $getid3->info['speex']['vbr'] = (bool)$info_ogg['pageheader'][$ogg_page_info['page_seqno']]['vbr']; rlm@3: $getid3->info['speex']['band_type'] = getid3_xiph::SpeexBandModeLookup($info_ogg['pageheader'][$ogg_page_info['page_seqno']]['mode']); rlm@3: rlm@3: $info_audio['sample_rate'] = $getid3->info['speex']['sample_rate']; rlm@3: $info_audio['channels'] = $getid3->info['speex']['channels']; rlm@3: rlm@3: if ($getid3->info['speex']['vbr']) { rlm@3: $info_audio['bitrate_mode'] = 'vbr'; rlm@3: } rlm@3: } rlm@3: rlm@3: // Unsupported Ogg file rlm@3: else { rlm@3: rlm@3: throw new getid3_exception('Expecting either "Speex " or "vorbis" identifier strings, found neither'); rlm@3: } rlm@3: rlm@3: rlm@3: //// Page 2 - Comment Header rlm@3: rlm@3: $ogg_page_info = $this->ParseOggPageHeader(); rlm@3: $info_ogg['pageheader'][$ogg_page_info['page_seqno']] = $ogg_page_info; rlm@3: rlm@3: switch ($info_audio['dataformat']) { rlm@3: rlm@3: case 'vorbis': rlm@3: $file_data = fread($getid3->fp, $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['page_length']); rlm@3: $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($file_data, 0, 1)); rlm@3: $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['stream_type'] = substr($file_data, 1, 6); // hard-coded to 'vorbis' rlm@3: $this->ParseVorbisCommentsFilepointer(); rlm@3: break; rlm@3: rlm@3: case 'flac': rlm@3: if (!$this->FLACparseMETAdata()) { rlm@3: throw new getid3_exception('Failed to parse FLAC headers'); rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'speex': rlm@3: fseek($getid3->fp, $info_ogg['pageheader'][$ogg_page_info['page_seqno']]['page_length'], SEEK_CUR); rlm@3: $this->ParseVorbisCommentsFilepointer(); rlm@3: break; rlm@3: } rlm@3: rlm@3: rlm@3: //// Last Page - Number of Samples rlm@3: rlm@3: fseek($getid3->fp, max($getid3->info['avdataend'] - getid3::FREAD_BUFFER_SIZE, 0), SEEK_SET); rlm@3: $last_chunk_of_ogg = strrev(fread($getid3->fp, getid3::FREAD_BUFFER_SIZE)); rlm@3: rlm@3: if ($last_OggS_postion = strpos($last_chunk_of_ogg, 'SggO')) { rlm@3: fseek($getid3->fp, $getid3->info['avdataend'] - ($last_OggS_postion + strlen('SggO')), SEEK_SET); rlm@3: $getid3->info['avdataend'] = ftell($getid3->fp); rlm@3: $info_ogg['pageheader']['eos'] = $this->ParseOggPageHeader(); rlm@3: $info_ogg['samples'] = $info_ogg['pageheader']['eos']['pcm_abs_position']; rlm@3: $info_ogg['bitrate_average'] = (($getid3->info['avdataend'] - $getid3->info['avdataoffset']) * 8) / ($info_ogg['samples'] / $info_audio['sample_rate']); rlm@3: } rlm@3: rlm@3: if (!empty($info_ogg['bitrate_average'])) { rlm@3: $info_audio['bitrate'] = $info_ogg['bitrate_average']; rlm@3: } elseif (!empty($info_ogg['bitrate_nominal'])) { rlm@3: $info_audio['bitrate'] = $info_ogg['bitrate_nominal']; rlm@3: } elseif (!empty($info_ogg['bitrate_min']) && !empty($info_ogg['bitrate_max'])) { rlm@3: $info_audio['bitrate'] = ($info_ogg['bitrate_min'] + $info_ogg['bitrate_max']) / 2; rlm@3: } rlm@3: if (isset($info_audio['bitrate']) && !isset($getid3->info['playtime_seconds'])) { rlm@3: $getid3->info['playtime_seconds'] = (float)((($getid3->info['avdataend'] - $getid3->info['avdataoffset']) * 8) / $info_audio['bitrate']); rlm@3: } rlm@3: rlm@3: if (isset($info_ogg['vendor'])) { rlm@3: $info_audio['encoder'] = preg_replace('/^Encoded with /', '', $info_ogg['vendor']); rlm@3: rlm@3: // Vorbis only rlm@3: if ($info_audio['dataformat'] == 'vorbis') { rlm@3: rlm@3: // Vorbis 1.0 starts with Xiph.Org rlm@3: if (preg_match('/^Xiph.Org/', $info_audio['encoder'])) { rlm@3: rlm@3: if ($info_audio['bitrate_mode'] == 'abr') { rlm@3: rlm@3: // Set -b 128 on abr files rlm@3: $info_audio['encoder_options'] = '-b '.round($info_ogg['bitrate_nominal'] / 1000); rlm@3: rlm@3: } elseif (($info_audio['bitrate_mode'] == 'vbr') && ($info_audio['channels'] == 2) && ($info_audio['sample_rate'] >= 44100) && ($info_audio['sample_rate'] <= 48000)) { rlm@3: // Set -q N on vbr files rlm@3: $info_audio['encoder_options'] = '-q '.getid3_xiph::GetQualityFromNominalBitrate($info_ogg['bitrate_nominal']); rlm@3: } rlm@3: } rlm@3: rlm@3: if (empty($info_audio['encoder_options']) && !empty($info_ogg['bitrate_nominal'])) { rlm@3: $info_audio['encoder_options'] = 'Nominal bitrate: '.intval(round($info_ogg['bitrate_nominal'] / 1000)).'kbps'; rlm@3: } rlm@3: } rlm@3: } rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function ParseOggPageHeader() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: // http://xiph.org/ogg/vorbis/doc/framing.html rlm@3: $ogg_header['page_start_offset'] = ftell($getid3->fp); // where we started from in the file rlm@3: rlm@3: $file_data = fread($getid3->fp, getid3::FREAD_BUFFER_SIZE); rlm@3: $file_data_offset = 0; rlm@3: rlm@3: while ((substr($file_data, $file_data_offset++, 4) != 'OggS')) { rlm@3: if ((ftell($getid3->fp) - $ogg_header['page_start_offset']) >= getid3::FREAD_BUFFER_SIZE) { rlm@3: // should be found before here rlm@3: return false; rlm@3: } rlm@3: if ((($file_data_offset + 28) > strlen($file_data)) || (strlen($file_data) < 28)) { rlm@3: if (feof($getid3->fp) || (($file_data .= fread($getid3->fp, getid3::FREAD_BUFFER_SIZE)) === false)) { rlm@3: // get some more data, unless eof, in which case fail rlm@3: return false; rlm@3: } rlm@3: } rlm@3: } rlm@3: rlm@3: $file_data_offset += 3; // page, delimited by 'OggS' rlm@3: rlm@3: getid3_lib::ReadSequence('LittleEndian2Int', $ogg_header, $file_data, $file_data_offset, rlm@3: array ( rlm@3: 'stream_structver' => 1, rlm@3: 'flags_raw' => 1, rlm@3: 'pcm_abs_position' => 8, rlm@3: 'stream_serialno' => 4, rlm@3: 'page_seqno' => 4, rlm@3: 'page_checksum' => 4, rlm@3: 'page_segments' => 1 rlm@3: ) rlm@3: ); rlm@3: rlm@3: $file_data_offset += 23; rlm@3: rlm@3: $ogg_header['flags']['fresh'] = (bool)($ogg_header['flags_raw'] & 0x01); // fresh packet rlm@3: $ogg_header['flags']['bos'] = (bool)($ogg_header['flags_raw'] & 0x02); // first page of logical bitstream (bos) rlm@3: $ogg_header['flags']['eos'] = (bool)($ogg_header['flags_raw'] & 0x04); // last page of logical bitstream (eos) rlm@3: rlm@3: $ogg_header['page_length'] = 0; rlm@3: for ($i = 0; $i < $ogg_header['page_segments']; $i++) { rlm@3: $ogg_header['segment_table'][$i] = getid3_lib::LittleEndian2Int($file_data{$file_data_offset++}); rlm@3: $ogg_header['page_length'] += $ogg_header['segment_table'][$i]; rlm@3: } rlm@3: $ogg_header['header_end_offset'] = $ogg_header['page_start_offset'] + $file_data_offset; rlm@3: $ogg_header['page_end_offset'] = $ogg_header['header_end_offset'] + $ogg_header['page_length']; rlm@3: fseek($getid3->fp, $ogg_header['header_end_offset'], SEEK_SET); rlm@3: rlm@3: return $ogg_header; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function ParseVorbisCommentsFilepointer() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: $original_offset = ftell($getid3->fp); rlm@3: $comment_start_offset = $original_offset; rlm@3: $comment_data_offset = 0; rlm@3: $vorbis_comment_page = 1; rlm@3: rlm@3: switch ($getid3->info['audio']['dataformat']) { rlm@3: rlm@3: case 'vorbis': rlm@3: $comment_start_offset = $getid3->info['ogg']['pageheader'][$vorbis_comment_page]['page_start_offset']; // Second Ogg page, after header block rlm@3: fseek($getid3->fp, $comment_start_offset, SEEK_SET); rlm@3: $comment_data_offset = 27 + $getid3->info['ogg']['pageheader'][$vorbis_comment_page]['page_segments']; rlm@3: $comment_data = fread($getid3->fp, getid3_xiph::OggPageSegmentLength($getid3->info['ogg']['pageheader'][$vorbis_comment_page], 1) + $comment_data_offset); rlm@3: $comment_data_offset += (strlen('vorbis') + 1); rlm@3: break; rlm@3: rlm@3: rlm@3: case 'flac': rlm@3: fseek($getid3->fp, $getid3->info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4, SEEK_SET); rlm@3: $comment_data = fread($getid3->fp, $getid3->info['flac']['VORBIS_COMMENT']['raw']['block_length']); rlm@3: break; rlm@3: rlm@3: rlm@3: case 'speex': rlm@3: $comment_start_offset = $getid3->info['ogg']['pageheader'][$vorbis_comment_page]['page_start_offset']; // Second Ogg page, after header block rlm@3: fseek($getid3->fp, $comment_start_offset, SEEK_SET); rlm@3: $comment_data_offset = 27 + $getid3->info['ogg']['pageheader'][$vorbis_comment_page]['page_segments']; rlm@3: $comment_data = fread($getid3->fp, getid3_xiph::OggPageSegmentLength($getid3->info['ogg']['pageheader'][$vorbis_comment_page], 1) + $comment_data_offset); rlm@3: break; rlm@3: rlm@3: rlm@3: default: rlm@3: return false; rlm@3: } rlm@3: rlm@3: $vendor_size = getid3_lib::LittleEndian2Int(substr($comment_data, $comment_data_offset, 4)); rlm@3: $comment_data_offset += 4; rlm@3: rlm@3: $getid3->info['ogg']['vendor'] = substr($comment_data, $comment_data_offset, $vendor_size); rlm@3: $comment_data_offset += $vendor_size; rlm@3: rlm@3: $comments_count = getid3_lib::LittleEndian2Int(substr($comment_data, $comment_data_offset, 4)); rlm@3: $comment_data_offset += 4; rlm@3: rlm@3: $getid3->info['avdataoffset'] = $comment_start_offset + $comment_data_offset; rlm@3: rlm@3: for ($i = 0; $i < $comments_count; $i++) { rlm@3: rlm@3: $getid3->info['ogg']['comments_raw'][$i]['dataoffset'] = $comment_start_offset + $comment_data_offset; rlm@3: rlm@3: if (ftell($getid3->fp) < ($getid3->info['ogg']['comments_raw'][$i]['dataoffset'] + 4)) { rlm@3: $vorbis_comment_page++; rlm@3: rlm@3: $ogg_page_info = $this->ParseOggPageHeader(); rlm@3: $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']] = $ogg_page_info; rlm@3: rlm@3: // First, save what we haven't read yet rlm@3: $as_yet_unused_data = substr($comment_data, $comment_data_offset); rlm@3: rlm@3: // Then take that data off the end rlm@3: $comment_data = substr($comment_data, 0, $comment_data_offset); rlm@3: rlm@3: // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct rlm@3: $comment_data .= str_repeat("\x00", 27 + $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']]['page_segments']); rlm@3: $comment_data_offset += (27 + $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']]['page_segments']); rlm@3: rlm@3: // Finally, stick the unused data back on the end rlm@3: $comment_data .= $as_yet_unused_data; rlm@3: rlm@3: $comment_data .= fread($getid3->fp, getid3_xiph::OggPageSegmentLength($getid3->info['ogg']['pageheader'][$vorbis_comment_page], 1)); rlm@3: } rlm@3: $getid3->info['ogg']['comments_raw'][$i]['size'] = getid3_lib::LittleEndian2Int(substr($comment_data, $comment_data_offset, 4)); rlm@3: rlm@3: // replace avdataoffset with position just after the last vorbiscomment rlm@3: $getid3->info['avdataoffset'] = $getid3->info['ogg']['comments_raw'][$i]['dataoffset'] + $getid3->info['ogg']['comments_raw'][$i]['size'] + 4; rlm@3: rlm@3: $comment_data_offset += 4; rlm@3: while ((strlen($comment_data) - $comment_data_offset) < $getid3->info['ogg']['comments_raw'][$i]['size']) { rlm@3: rlm@3: if (($getid3->info['ogg']['comments_raw'][$i]['size'] > $getid3->info['avdataend']) || ($getid3->info['ogg']['comments_raw'][$i]['size'] < 0)) { rlm@3: throw new getid3_exception('Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($getid3->info['ogg']['comments_raw'][$i]['size']).' bytes) - aborting reading comments'); rlm@3: } rlm@3: rlm@3: $vorbis_comment_page++; rlm@3: rlm@3: $ogg_page_info = $this->ParseOggPageHeader(); rlm@3: $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']] = $ogg_page_info; rlm@3: rlm@3: // First, save what we haven't read yet rlm@3: $as_yet_unused_data = substr($comment_data, $comment_data_offset); rlm@3: rlm@3: // Then take that data off the end rlm@3: $comment_data = substr($comment_data, 0, $comment_data_offset); rlm@3: rlm@3: // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct rlm@3: $comment_data .= str_repeat("\x00", 27 + $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']]['page_segments']); rlm@3: $comment_data_offset += (27 + $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']]['page_segments']); rlm@3: rlm@3: // Finally, stick the unused data back on the end rlm@3: $comment_data .= $as_yet_unused_data; rlm@3: rlm@3: //$comment_data .= fread($getid3->fp, $getid3->info['ogg']['pageheader'][$ogg_page_info['page_seqno']]['page_length']); rlm@3: $comment_data .= fread($getid3->fp, getid3_xiph::OggPageSegmentLength($getid3->info['ogg']['pageheader'][$vorbis_comment_page], 1)); rlm@3: rlm@3: //$filebaseoffset += $ogg_page_info['header_end_offset'] - $ogg_page_info['page_start_offset']; rlm@3: } rlm@3: $comment_string = substr($comment_data, $comment_data_offset, $getid3->info['ogg']['comments_raw'][$i]['size']); rlm@3: $comment_data_offset += $getid3->info['ogg']['comments_raw'][$i]['size']; rlm@3: rlm@3: if (!$comment_string) { rlm@3: rlm@3: // no comment? rlm@3: $getid3->warning('Blank Ogg comment ['.$i.']'); rlm@3: rlm@3: } elseif (strstr($comment_string, '=')) { rlm@3: rlm@3: $comment_exploded = explode('=', $comment_string, 2); rlm@3: $getid3->info['ogg']['comments_raw'][$i]['key'] = strtoupper($comment_exploded[0]); rlm@3: $getid3->info['ogg']['comments_raw'][$i]['value'] = @$comment_exploded[1]; rlm@3: $getid3->info['ogg']['comments_raw'][$i]['data'] = base64_decode($getid3->info['ogg']['comments_raw'][$i]['value']); rlm@3: rlm@3: $getid3->info['ogg']['comments'][strtolower($getid3->info['ogg']['comments_raw'][$i]['key'])][] = $getid3->info['ogg']['comments_raw'][$i]['value']; rlm@3: rlm@3: if ($getid3->option_tags_images) { rlm@3: $image_chunk_check = getid3_lib_image_size::get($getid3->info['ogg']['comments_raw'][$i]['data']); rlm@3: $getid3->info['ogg']['comments_raw'][$i]['image_mime'] = image_type_to_mime_type($image_chunk_check[2]); rlm@3: } rlm@3: rlm@3: if (!@$getid3->info['ogg']['comments_raw'][$i]['image_mime'] || ($getid3->info['ogg']['comments_raw'][$i]['image_mime'] == 'application/octet-stream')) { rlm@3: unset($getid3->info['ogg']['comments_raw'][$i]['image_mime']); rlm@3: unset($getid3->info['ogg']['comments_raw'][$i]['data']); rlm@3: } rlm@3: rlm@3: rlm@3: } else { rlm@3: rlm@3: $getid3->warning('[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$comment_string); rlm@3: } rlm@3: } rlm@3: rlm@3: rlm@3: // Replay Gain Adjustment rlm@3: // http://privatewww.essex.ac.uk/~djmrob/replaygain/ rlm@3: if (isset($getid3->info['ogg']['comments']) && is_array($getid3->info['ogg']['comments'])) { rlm@3: foreach ($getid3->info['ogg']['comments'] as $index => $commentvalue) { rlm@3: switch ($index) { rlm@3: case 'rg_audiophile': rlm@3: case 'replaygain_album_gain': rlm@3: $getid3->info['replay_gain']['album']['adjustment'] = (float)$commentvalue[0]; rlm@3: unset($getid3->info['ogg']['comments'][$index]); rlm@3: break; rlm@3: rlm@3: case 'rg_radio': rlm@3: case 'replaygain_track_gain': rlm@3: $getid3->info['replay_gain']['track']['adjustment'] = (float)$commentvalue[0]; rlm@3: unset($getid3->info['ogg']['comments'][$index]); rlm@3: break; rlm@3: rlm@3: case 'replaygain_album_peak': rlm@3: $getid3->info['replay_gain']['album']['peak'] = (float)$commentvalue[0]; rlm@3: unset($getid3->info['ogg']['comments'][$index]); rlm@3: break; rlm@3: rlm@3: case 'rg_peak': rlm@3: case 'replaygain_track_peak': rlm@3: $getid3->info['replay_gain']['track']['peak'] = (float)$commentvalue[0]; rlm@3: unset($getid3->info['ogg']['comments'][$index]); rlm@3: break; rlm@3: rlm@3: case 'replaygain_reference_loudness': rlm@3: $getid3->info['replay_gain']['reference_volume'] = (float)$commentvalue[0]; rlm@3: unset($getid3->info['ogg']['comments'][$index]); rlm@3: break; rlm@3: } rlm@3: } rlm@3: } rlm@3: rlm@3: fseek($getid3->fp, $original_offset, SEEK_SET); rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function ParseFLAC() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: // http://flac.sourceforge.net/format.html rlm@3: rlm@3: $getid3->info['fileformat'] = 'flac'; rlm@3: $getid3->info['audio']['dataformat'] = 'flac'; rlm@3: $getid3->info['audio']['bitrate_mode'] = 'vbr'; rlm@3: $getid3->info['audio']['lossless'] = true; rlm@3: rlm@3: return $this->FLACparseMETAdata(); rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function FLACparseMETAdata() { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: do { rlm@3: rlm@3: $meta_data_block_offset = ftell($getid3->fp); rlm@3: $meta_data_block_header = fread($getid3->fp, 4); rlm@3: $meta_data_last_block_flag = (bool)(getid3_lib::BigEndian2Int($meta_data_block_header[0]) & 0x80); rlm@3: $meta_data_block_type = getid3_lib::BigEndian2Int($meta_data_block_header[0]) & 0x7F; rlm@3: $meta_data_block_length = getid3_lib::BigEndian2Int(substr($meta_data_block_header, 1, 3)); rlm@3: $meta_data_block_type_text = getid3_xiph::FLACmetaBlockTypeLookup($meta_data_block_type); rlm@3: rlm@3: if ($meta_data_block_length < 0) { rlm@3: throw new getid3_exception('corrupt or invalid METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$meta_data_block_type.') at offset '.$meta_data_block_offset); rlm@3: } rlm@3: rlm@3: $getid3->info['flac'][$meta_data_block_type_text]['raw'] = array ( rlm@3: 'offset' => $meta_data_block_offset, rlm@3: 'last_meta_block' => $meta_data_last_block_flag, rlm@3: 'block_type' => $meta_data_block_type, rlm@3: 'block_type_text' => $meta_data_block_type_text, rlm@3: 'block_length' => $meta_data_block_length, rlm@3: 'block_data' => @fread($getid3->fp, $meta_data_block_length) rlm@3: ); rlm@3: $getid3->info['avdataoffset'] = ftell($getid3->fp); rlm@3: rlm@3: switch ($meta_data_block_type_text) { rlm@3: rlm@3: case 'STREAMINFO': rlm@3: if (!$this->FLACparseSTREAMINFO($getid3->info['flac'][$meta_data_block_type_text]['raw']['block_data'])) { rlm@3: return false; rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'PADDING': rlm@3: // ignore rlm@3: break; rlm@3: rlm@3: case 'APPLICATION': rlm@3: if (!$this->FLACparseAPPLICATION($getid3->info['flac'][$meta_data_block_type_text]['raw']['block_data'])) { rlm@3: return false; rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'SEEKTABLE': rlm@3: if (!$this->FLACparseSEEKTABLE($getid3->info['flac'][$meta_data_block_type_text]['raw']['block_data'])) { rlm@3: return false; rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'VORBIS_COMMENT': rlm@3: $old_offset = ftell($getid3->fp); rlm@3: fseek($getid3->fp, 0 - $meta_data_block_length, SEEK_CUR); rlm@3: $this->ParseVorbisCommentsFilepointer($getid3->fp, $getid3->info); rlm@3: fseek($getid3->fp, $old_offset, SEEK_SET); rlm@3: break; rlm@3: rlm@3: case 'CUESHEET': rlm@3: if (!$this->FLACparseCUESHEET($getid3->info['flac'][$meta_data_block_type_text]['raw']['block_data'])) { rlm@3: return false; rlm@3: } rlm@3: break; rlm@3: rlm@3: case 'PICTURE': rlm@3: if (!$this->FLACparsePICTURE($getid3->info['flac'][$meta_data_block_type_text]['raw']['block_data'])) { rlm@3: return false; rlm@3: } rlm@3: break; rlm@3: rlm@3: default: rlm@3: $getid3->warning('Unhandled METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$meta_data_block_type.') at offset '.$meta_data_block_offset); rlm@3: } rlm@3: rlm@3: } while ($meta_data_last_block_flag === false); rlm@3: rlm@3: rlm@3: if (isset($getid3->info['flac']['STREAMINFO'])) { rlm@3: $getid3->info['flac']['compressed_audio_bytes'] = $getid3->info['avdataend'] - $getid3->info['avdataoffset']; rlm@3: $getid3->info['flac']['uncompressed_audio_bytes'] = $getid3->info['flac']['STREAMINFO']['samples_stream'] * $getid3->info['flac']['STREAMINFO']['channels'] * ($getid3->info['flac']['STREAMINFO']['bits_per_sample'] / 8); rlm@3: $getid3->info['flac']['compression_ratio'] = $getid3->info['flac']['compressed_audio_bytes'] / $getid3->info['flac']['uncompressed_audio_bytes']; rlm@3: } rlm@3: rlm@3: // set md5_data_source - built into flac 0.5+ rlm@3: if (isset($getid3->info['flac']['STREAMINFO']['audio_signature'])) { rlm@3: rlm@3: if ($getid3->info['flac']['STREAMINFO']['audio_signature'] === str_repeat("\x00", 16)) { rlm@3: $getid3->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)'); rlm@3: rlm@3: } else { rlm@3: rlm@3: $getid3->info['md5_data_source'] = ''; rlm@3: $md5 = $getid3->info['flac']['STREAMINFO']['audio_signature']; rlm@3: for ($i = 0; $i < strlen($md5); $i++) { rlm@3: $getid3->info['md5_data_source'] .= str_pad(dechex(ord($md5{$i})), 2, '00', STR_PAD_LEFT); rlm@3: } rlm@3: if (!preg_match('/^[0-9a-f]{32}$/', $getid3->info['md5_data_source'])) { rlm@3: unset($getid3->info['md5_data_source']); rlm@3: } rlm@3: rlm@3: } rlm@3: rlm@3: } rlm@3: rlm@3: $getid3->info['audio']['bits_per_sample'] = $getid3->info['flac']['STREAMINFO']['bits_per_sample']; rlm@3: if ($getid3->info['audio']['bits_per_sample'] == 8) { rlm@3: // special case rlm@3: // must invert sign bit on all data bytes before MD5'ing to match FLAC's calculated value rlm@3: // MD5sum calculates on unsigned bytes, but FLAC calculated MD5 on 8-bit audio data as signed rlm@3: $getid3->warning('FLAC calculates MD5 data strangely on 8-bit audio, so the stored md5_data_source value will not match the decoded WAV file'); rlm@3: } rlm@3: if (!empty($getid3->info['ogg']['vendor'])) { rlm@3: $getid3->info['audio']['encoder'] = $getid3->info['ogg']['vendor']; rlm@3: } rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function FLACparseSTREAMINFO($meta_data_block_data) { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: getid3_lib::ReadSequence('BigEndian2Int', $getid3->info['flac']['STREAMINFO'], $meta_data_block_data, 0, rlm@3: array ( rlm@3: 'min_block_size' => 2, rlm@3: 'max_block_size' => 2, rlm@3: 'min_frame_size' => 3, rlm@3: 'max_frame_size' => 3 rlm@3: ) rlm@3: ); rlm@3: rlm@3: $sample_rate_channels_sample_bits_stream_samples = getid3_lib::BigEndian2Bin(substr($meta_data_block_data, 10, 8)); rlm@3: rlm@3: $getid3->info['flac']['STREAMINFO']['sample_rate'] = bindec(substr($sample_rate_channels_sample_bits_stream_samples, 0, 20)); rlm@3: $getid3->info['flac']['STREAMINFO']['channels'] = bindec(substr($sample_rate_channels_sample_bits_stream_samples, 20, 3)) + 1; rlm@3: $getid3->info['flac']['STREAMINFO']['bits_per_sample'] = bindec(substr($sample_rate_channels_sample_bits_stream_samples, 23, 5)) + 1; rlm@3: $getid3->info['flac']['STREAMINFO']['samples_stream'] = bindec(substr($sample_rate_channels_sample_bits_stream_samples, 28, 36)); // bindec() returns float in case of int overrun rlm@3: $getid3->info['flac']['STREAMINFO']['audio_signature'] = substr($meta_data_block_data, 18, 16); rlm@3: rlm@3: if (!empty($getid3->info['flac']['STREAMINFO']['sample_rate'])) { rlm@3: rlm@3: $getid3->info['audio']['bitrate_mode'] = 'vbr'; rlm@3: $getid3->info['audio']['sample_rate'] = $getid3->info['flac']['STREAMINFO']['sample_rate']; rlm@3: $getid3->info['audio']['channels'] = $getid3->info['flac']['STREAMINFO']['channels']; rlm@3: $getid3->info['audio']['bits_per_sample'] = $getid3->info['flac']['STREAMINFO']['bits_per_sample']; rlm@3: $getid3->info['playtime_seconds'] = $getid3->info['flac']['STREAMINFO']['samples_stream'] / $getid3->info['flac']['STREAMINFO']['sample_rate']; rlm@3: $getid3->info['audio']['bitrate'] = (($getid3->info['avdataend'] - $getid3->info['avdataoffset']) * 8) / $getid3->info['playtime_seconds']; rlm@3: rlm@3: } else { rlm@3: rlm@3: throw new getid3_exception('Corrupt METAdata block: STREAMINFO'); rlm@3: } rlm@3: rlm@3: unset($getid3->info['flac']['STREAMINFO']['raw']); rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function FLACparseAPPLICATION($meta_data_block_data) { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: $application_id = getid3_lib::BigEndian2Int(substr($meta_data_block_data, 0, 4)); rlm@3: rlm@3: $getid3->info['flac']['APPLICATION'][$application_id]['name'] = getid3_xiph::FLACapplicationIDLookup($application_id); rlm@3: $getid3->info['flac']['APPLICATION'][$application_id]['data'] = substr($meta_data_block_data, 4); rlm@3: rlm@3: unset($getid3->info['flac']['APPLICATION']['raw']); rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function FLACparseSEEKTABLE($meta_data_block_data) { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: $offset = 0; rlm@3: $meta_data_block_length = strlen($meta_data_block_data); rlm@3: while ($offset < $meta_data_block_length) { rlm@3: $sample_number_string = substr($meta_data_block_data, $offset, 8); rlm@3: $offset += 8; rlm@3: if ($sample_number_string == "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") { rlm@3: rlm@3: // placeholder point rlm@3: @$getid3->info['flac']['SEEKTABLE']['placeholders']++; rlm@3: $offset += 10; rlm@3: rlm@3: } else { rlm@3: rlm@3: $sample_number = getid3_lib::BigEndian2Int($sample_number_string); rlm@3: rlm@3: $getid3->info['flac']['SEEKTABLE'][$sample_number]['offset'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 8)); rlm@3: $offset += 8; rlm@3: rlm@3: $getid3->info['flac']['SEEKTABLE'][$sample_number]['samples'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 2)); rlm@3: $offset += 2; rlm@3: rlm@3: } rlm@3: } rlm@3: rlm@3: unset($getid3->info['flac']['SEEKTABLE']['raw']); rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function FLACparseCUESHEET($meta_data_block_data) { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: $getid3->info['flac']['CUESHEET']['media_catalog_number'] = trim(substr($meta_data_block_data, 0, 128), "\0"); rlm@3: $getid3->info['flac']['CUESHEET']['lead_in_samples'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, 128, 8)); rlm@3: $getid3->info['flac']['CUESHEET']['flags']['is_cd'] = (bool)(getid3_lib::BigEndian2Int($meta_data_block_data[136]) & 0x80); rlm@3: $getid3->info['flac']['CUESHEET']['number_tracks'] = getid3_lib::BigEndian2Int($meta_data_block_data[395]); rlm@3: rlm@3: $offset = 396; rlm@3: rlm@3: for ($track = 0; $track < $getid3->info['flac']['CUESHEET']['number_tracks']; $track++) { rlm@3: rlm@3: $track_sample_offset = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 8)); rlm@3: $offset += 8; rlm@3: rlm@3: $track_number = getid3_lib::BigEndian2Int($meta_data_block_data{$offset++}); rlm@3: rlm@3: $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['sample_offset'] = $track_sample_offset; rlm@3: $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['isrc'] = substr($meta_data_block_data, $offset, 12); rlm@3: $offset += 12; rlm@3: rlm@3: $track_flags_raw = getid3_lib::BigEndian2Int($meta_data_block_data{$offset++}); rlm@3: $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['flags']['is_audio'] = (bool)($track_flags_raw & 0x80); rlm@3: $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['flags']['pre_emphasis'] = (bool)($track_flags_raw & 0x40); rlm@3: rlm@3: $offset += 13; // reserved rlm@3: rlm@3: $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['index_points'] = getid3_lib::BigEndian2Int($meta_data_block_data{$offset++}); rlm@3: rlm@3: for ($index = 0; $index < $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['index_points']; $index++) { rlm@3: rlm@3: $index_sample_offset = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 8)); rlm@3: $offset += 8; rlm@3: rlm@3: $index_number = getid3_lib::BigEndian2Int($meta_data_block_data{$offset++}); rlm@3: $getid3->info['flac']['CUESHEET']['tracks'][$track_number]['indexes'][$index_number] = $index_sample_offset; rlm@3: rlm@3: $offset += 3; // reserved rlm@3: } rlm@3: } rlm@3: rlm@3: unset($getid3->info['flac']['CUESHEET']['raw']); rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: private function FLACparsePICTURE($meta_data_block_data) { rlm@3: rlm@3: $getid3 = $this->getid3; rlm@3: rlm@3: $picture = &$getid3->info['flac']['PICTURE'][sizeof($getid3->info['flac']['PICTURE']) - 1]; rlm@3: rlm@3: $offset = 0; rlm@3: rlm@3: $picture['type'] = $this->FLACpictureTypeLookup(getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4))); rlm@3: $offset += 4; rlm@3: rlm@3: $length = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $picture['mime_type'] = substr($meta_data_block_data, $offset, $length); rlm@3: $offset += $length; rlm@3: rlm@3: $length = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $picture['description'] = substr($meta_data_block_data, $offset, $length); rlm@3: $offset += $length; rlm@3: rlm@3: $picture['width'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $picture['height'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $picture['color_depth'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $picture['colors_indexed'] = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $length = getid3_lib::BigEndian2Int(substr($meta_data_block_data, $offset, 4)); rlm@3: $offset += 4; rlm@3: rlm@3: $picture['image_data'] = substr($meta_data_block_data, $offset, $length); rlm@3: $offset += $length; rlm@3: rlm@3: unset($getid3->info['flac']['PICTURE']['raw']); rlm@3: rlm@3: return true; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function SpeexBandModeLookup($mode) { rlm@3: rlm@3: static $lookup = array ( rlm@3: 0 => 'narrow', rlm@3: 1 => 'wide', rlm@3: 2 => 'ultra-wide' rlm@3: ); rlm@3: return (isset($lookup[$mode]) ? $lookup[$mode] : null); rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function OggPageSegmentLength($ogg_info_array, $segment_number=1) { rlm@3: rlm@3: for ($i = 0; $i < $segment_number; $i++) { rlm@3: $segment_length = 0; rlm@3: foreach ($ogg_info_array['segment_table'] as $key => $value) { rlm@3: $segment_length += $value; rlm@3: if ($value < 255) { rlm@3: break; rlm@3: } rlm@3: } rlm@3: } rlm@3: return $segment_length; rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function GetQualityFromNominalBitrate($nominal_bitrate) { rlm@3: rlm@3: // decrease precision rlm@3: $nominal_bitrate = $nominal_bitrate / 1000; rlm@3: rlm@3: if ($nominal_bitrate < 128) { rlm@3: // q-1 to q4 rlm@3: $qval = ($nominal_bitrate - 64) / 16; rlm@3: } elseif ($nominal_bitrate < 256) { rlm@3: // q4 to q8 rlm@3: $qval = $nominal_bitrate / 32; rlm@3: } elseif ($nominal_bitrate < 320) { rlm@3: // q8 to q9 rlm@3: $qval = ($nominal_bitrate + 256) / 64; rlm@3: } else { rlm@3: // q9 to q10 rlm@3: $qval = ($nominal_bitrate + 1300) / 180; rlm@3: } rlm@3: return round($qval, 1); // 5 or 4.9 rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function FLACmetaBlockTypeLookup($block_type) { rlm@3: rlm@3: static $lookup = array ( rlm@3: 0 => 'STREAMINFO', rlm@3: 1 => 'PADDING', rlm@3: 2 => 'APPLICATION', rlm@3: 3 => 'SEEKTABLE', rlm@3: 4 => 'VORBIS_COMMENT', rlm@3: 5 => 'CUESHEET', rlm@3: 6 => 'PICTURE' rlm@3: ); rlm@3: return (isset($lookup[$block_type]) ? $lookup[$block_type] : 'reserved'); rlm@3: } rlm@3: rlm@3: rlm@3: rlm@3: public static function FLACapplicationIDLookup($application_id) { rlm@3: rlm@3: // http://flac.sourceforge.net/id.html rlm@3: rlm@3: static $lookup = array ( rlm@3: 0x46746F6C => 'flac-tools', // 'Ftol' rlm@3: 0x46746F6C => 'Sound Font FLAC', // 'SFFL' rlm@3: 0x7065656D => 'Parseable Embedded Extensible Metadata (specification)', // 'peem' rlm@3: 0x786D6364 => 'xmcd' rlm@3: rlm@3: ); rlm@3: return (isset($lookup[$application_id]) ? $lookup[$application_id] : 'reserved'); rlm@3: } rlm@3: rlm@3: rlm@3: public static function FLACpictureTypeLookup($type_id) { rlm@3: rlm@3: static $lookup = array ( rlm@3: rlm@3: 0 => 'Other', rlm@3: 1 => "32x32 pixels 'file icon' (PNG only)", rlm@3: 2 => 'Other file icon', rlm@3: 3 => 'Cover (front)', rlm@3: 4 => 'Cover (back)', rlm@3: 5 => 'Leaflet page', rlm@3: 6 => 'Media (e.g. label side of CD)', rlm@3: 7 => 'Lead artist/lead performer/soloist', rlm@3: 8 => 'Artist/performer', rlm@3: 9 => 'Conductor', rlm@3: 10 => 'Band/Orchestra', rlm@3: 11 => 'Composer', rlm@3: 12 => 'Lyricist/text writer', rlm@3: 13 => 'Recording Location', rlm@3: 14 => 'During recording', rlm@3: 15 => 'During performance', rlm@3: 16 => 'Movie/video screen capture', rlm@3: 17 => 'A bright coloured fish', rlm@3: 18 => 'Illustration', rlm@3: 19 => 'Band/artist logotype', rlm@3: 20 => 'Publisher/Studio logotype' rlm@3: ); rlm@3: return (isset($lookup[$type_id]) ? $lookup[$type_id] : 'reserved'); rlm@3: } rlm@3: rlm@3: } rlm@3: rlm@3: ?>