Узнать исполнителя, название композиции и продолжительность mp3-файла с помощью PHP

На одном из проектов мне понадобилось получить некоторые данные о музыкальных файлах в формате mp3. Конкретно мне были нужны: имя исполнителя, название композиции и ее продолжительность. Так как я заранее знал, что во всех mp3-файлах были заполнены теги ID3v1, то задача заключалась в том, чтобы эти данные выудить из файла с помощью PHP.

Mp3 файл информацию о продолжительности композиции в явном виде внутри себя не содержит, поэтому здесь необходим совсем другой подход — подсчет фреймов в файле.

Чтобы реализовать весь этот функционал можно воспользоваться уже готовыми php-библиотеками, чего мне совсем не хотелось, хотелось чего-то попроще и полегче. В результате поиска и отбора подходящих вариантов в интернете, я скомбинировал пару решений в одно.

Определение длительности mp3 работает для переменного VBR и постоянного CBR битрейта. Применение скрипта к 150 файлам, выявило 3 ошибки: на двух файлах он выдал нулевую продолжительность, а на одном 2 сек, хотя реальная продолжительность этих файлов была по несколько минут — видимо что-то не так с файлами. У остальных 147 файлов продолжительность была определена верно. Причем выяснилось, что для некоторых файлов иногда даже музыкальный проигрыватель Winamp выдавал неверную продолжительность, а скрипт правильную.

Вот что получилось в итоге:

<?php
include ('mp3file.class.php'); // подключим файл с классом для работы с mp3 (должен лежать в той же папке)
$mp3_dir = $_SERVER['DOCUMENT_ROOT'].'/hits/'; // путь до папки альбома на сервере
$fl = 'Elton_John-Believe.mp3'; // название mp3 файла
$mp3file = new MP3File($mp3_dir.$fl); // создадим объект на основании класса
list($art, $pes) = $mp3file->get_id3v1(); // узнаем исполнителя и название композиции
$dur = $mp3file->getDuration(); // узнаем продолжительность композиции
$dur = MP3File::formatTime($dur); // отформатируем продолжительность композиции в формате [ч]ч:[м]м:сс
echo 'Исполнитель: '.$art; // выведем исполнителя
echo ', Композиция: '.$pes; // выведем название композиции
echo ', Длительность: '.MP3File::formatTime($dur); // выведем продолжительность композиции
?>

Нижеследующий класс для работы с mp3 нужно записать в файл mp3file.class.php и положить его в той же папке, что и файл с кодом выше.

<?php
class MP3File {
 protected $filename;
 public function __construct($filename) {
  $this->filename = $filename;
 }

 public static function formatTime($duration) {
  $hours = floor($duration / 3600);
  $minutes = floor(($duration - ($hours * 3600)) / 60);
  $seconds = floor($duration - ($hours * 3600) - ($minutes * 60));
  if ($seconds < 10) {$seconds = '0'.$seconds;}
  if ($hours == 0) {
   return $minutes.':'.$seconds;
  } else {
   if ($minutes < 10) {$minutes = '0'.$minutes;}
   return $hours.':'.$minutes.':'.$seconds;
  }
 }

 public function get_id3v1() {
  $f = fopen($this->filename, 'rb');
  rewind($f);
  fseek($f, -128, SEEK_END);
  $tmp = fread($f,128);
  if ($tmp[125] == Chr(0) and $tmp[126] != Chr(0)) {
   $format = 'a3TAG/a30NAME/a30ARTISTS/a30ALBUM/a4YEAR/a28COMMENT/x1/C1TRACK/C1GENRENO'; // ID3 v1.1
  } else {
   $format = 'a3TAG/a30NAME/a30ARTISTS/a30ALBUM/a4YEAR/a30COMMENT/C1GENRENO'; // ID3 v1
  }
  $id3tag = unpack($format, $tmp);
  $art = mb_convert_encoding($id3tag['ARTISTS'], 'utf-8', "CP1251");
  $pes = mb_convert_encoding($id3tag['NAME'], 'utf-8', "CP1251");
  return array($art, $pes);
 }

 //Read first mp3 frame only...  use for CBR constant bit rate MP3s
 public function getDurationEstimate() {
  return $this->getDuration($use_cbr_estimate=true);
 }

 //Read entire file, frame by frame... ie: Variable Bit Rate (VBR)
 public function getDuration($use_cbr_estimate=false) {
  $fd = fopen($this->filename, "rb");
  $duration=0;
  $block = fread($fd, 100);
  $offset = $this->skipID3v2Tag($block);
  fseek($fd, $offset, SEEK_SET);
  while (!feof($fd)) {
   $block = fread($fd, 10);
   if (strlen($block) < 10) { break; }
   // looking for 1111 1111 111 (frame synchronization bits)
   else if ($block[0]=="\xff" && (ord($block[1])&0xe0) ) {
	$info = self::parseFrameHeader(substr($block, 0, 4));
	if (empty($info['Framesize'])) { return $duration; } // some corrupt mp3 files
	fseek($fd, $info['Framesize']-10, SEEK_CUR);
	$duration += ( $info['Samples'] / $info['Sampling Rate'] );
   }
   else if (substr($block, 0, 3)=='TAG') {
	fseek($fd, 128-10, SEEK_CUR); // skip over id3v1 tag size
   } else {
	fseek($fd, -9, SEEK_CUR);
   }
   if ($use_cbr_estimate && !empty($info)) { 
	return $this->estimateDuration($info['Bitrate'],$offset); 
   }
  }
  return floor($duration);
 }

 private function estimateDuration($bitrate,$offset) {
  $kbps = ($bitrate*1000)/8;
  $datasize = filesize($this->filename) - $offset;
  return round($datasize / $kbps);
 }

 private function skipID3v2Tag(&$block) {
  if (substr($block, 0,3)=="ID3") {
   $id3v2_major_version = ord($block[3]);
   $id3v2_minor_version = ord($block[4]);
   $id3v2_flags = ord($block[5]);
   $flag_unsynchronisation  = $id3v2_flags & 0x80 ? 1 : 0;
   $flag_extended_header    = $id3v2_flags & 0x40 ? 1 : 0;
   $flag_experimental_ind   = $id3v2_flags & 0x20 ? 1 : 0;
   $flag_footer_present     = $id3v2_flags & 0x10 ? 1 : 0;
   $z0 = ord($block[6]);
   $z1 = ord($block[7]);
   $z2 = ord($block[8]);
   $z3 = ord($block[9]);
   if ( (($z0&0x80)==0) && (($z1&0x80)==0) && (($z2&0x80)==0) && (($z3&0x80)==0) ) {
	$header_size = 10;
	$tag_size = (($z0&0x7f) * 2097152) + (($z1&0x7f) * 16384) + (($z2&0x7f) * 128) + ($z3&0x7f);
	$footer_size = $flag_footer_present ? 10 : 0;
	return $header_size + $tag_size + $footer_size; // bytes to skip
   }
  }
  return 0;
 }
 
 public static function parseFrameHeader($fourbytes) {
  static $versions = array(
   0x0=>'2.5',0x1=>'x',0x2=>'2',0x3=>'1', // x=>'reserved'
  );
  static $layers = array(
   0x0=>'x',0x1=>'3',0x2=>'2',0x3=>'1', // x=>'reserved'
  );
  static $bitrates = array(
   'V1L1'=>array(0,32,64,96,128,160,192,224,256,288,320,352,384,416,448),
   'V1L2'=>array(0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384),
   'V1L3'=>array(0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320),
   'V2L1'=>array(0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256),
   'V2L2'=>array(0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160),
   'V2L3'=>array(0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160),
  );
  static $sample_rates = array(
   '1'   => array(44100,48000,32000),
   '2'   => array(22050,24000,16000),
   '2.5' => array(11025,12000, 8000),
  );
  static $samples = array(
   1 => array( 1 => 384, 2 =>1152, 3 =>1152, ), // MPEGv1,     Layers 1,2,3
   2 => array( 1 => 384, 2 =>1152, 3 => 576, ), // MPEGv2/2.5, Layers 1,2,3
  );
  $b1=ord($fourbytes[1]);
  $b2=ord($fourbytes[2]);
  $b3=ord($fourbytes[3]);
  $version_bits = ($b1 & 0x18) >> 3;
  $version = $versions[$version_bits];
  $simple_version =  ($version=='2.5' ? 2 : $version);
  $layer_bits = ($b1 & 0x06) >> 1;
  $layer = $layers[$layer_bits];
  $protection_bit = ($b1 & 0x01);
  $bitrate_key = sprintf('V%dL%d', $simple_version , $layer);
  $bitrate_idx = ($b2 & 0xf0) >> 4;
  $bitrate = isset($bitrates[$bitrate_key][$bitrate_idx]) ? $bitrates[$bitrate_key][$bitrate_idx] : 0;
  $sample_rate_idx = ($b2 & 0x0c) >> 2; // 0xc => b1100
  $sample_rate = isset($sample_rates[$version][$sample_rate_idx]) ? $sample_rates[$version][$sample_rate_idx] : 0;
  $padding_bit = ($b2 & 0x02) >> 1;
  $private_bit = ($b2 & 0x01);
  $channel_mode_bits = ($b3 & 0xc0) >> 6;
  $mode_extension_bits = ($b3 & 0x30) >> 4;
  $copyright_bit = ($b3 & 0x08) >> 3;
  $original_bit = ($b3 & 0x04) >> 2;
  $emphasis = ($b3 & 0x03);
  $info = array();
  $info['Version'] = $version; // MPEGVersion
  $info['Layer'] = $layer;
  $info['Bitrate'] = $bitrate;
  $info['Sampling Rate'] = $sample_rate;
  $info['Framesize'] = self::framesize($layer, $bitrate, $sample_rate, $padding_bit);
  $info['Samples'] = $samples[$simple_version][$layer];
  return $info;
 }
 
 private static function framesize($layer, $bitrate,$sample_rate,$padding_bit) {
  if ($layer==1) {
   return intval(((12 * $bitrate*1000 /$sample_rate) + $padding_bit) * 4);
  } else { // layer 2, 3
   return intval(((144 * $bitrate*1000)/$sample_rate) + $padding_bit);
  }
 }
}
?>

Поделиться статьей:  

Поделитесь своим мнением

Правила сообщений

Для оформления сообщений Вы можете использовать следующие тэги:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Копирование материалов разрешено только с указанием источника и прямой индексируемой ссылкой на оригинал.
Яндекс.Метрика
© 2018 Хороший отзыв · Искусство выбирать · Лично · Разумно · Честно