Unir ficheros mp3 en un único fichero mp3 con CakePHP

Reading time ~13 minutes

Recientemente he creado un podcast para la página de Música Vermella con el inconveniente añadido de que se suben mp3 independientes para cada publicación.

Para solucionarlo he utilizado la librería getid3 para unir los ficheros mp3 de cada publicación en un único fichero mp3.

Para verlo podéis agregar el Podcast de Música Vermella a vuestro cliente de Podcast (iTunes, Rythmbox, Banshee, Miro…).

Quería hacer un tutorial sobre cómo crear un podcast con CakePHP pero lo dejaré para otro tutorial por tal de no complicar este.

Puedes ver la segunda parte aquí: Crear un Podcast en CakePHP

Para empezar necesitaréis descargar la librería Getid3. Descargad la versión estable por si acaso ya que la versión beta falla con las etiquetas id3 (que no utilizaremos) así que si queréis utilizarla es bajo vuestra propia responsabilidad.

Getid3 versión 1.8.2 (estable)

Descomprimid el contenido del fichero que descarguéis en Getid3 y ponedlo en la carpeta /app/vendors, de manera que quede así:

1
2
3
4
/app/vendors/getid3/demos/
/app/vendors/getid3/getid3/
/app/vendors/getid3/helperapps/
/app/vendors/getid3/demás ficheros

Ahora necesitaremos el método para unir los mp3. Lo podéis encontrar en la carpeta de demos de getid3. De todos modos os dejo aquí un pequeño componente que tengo yo para utilizar getid3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
<?php
// /app/controllers/components/getid3.php
class Getid3Component extends Object
{
  public $errors = array();

  function __construct()
  {
    set_time_limit(20*3600);
    ignore_user_abort(false);
  }

  function error($text)
  {
    array_push($this->errors, $text);
  }

  function extract($filename)
  {

    App::import('vendor','getid3/getid3',array('file'=>'getid3.php'));
    // Initialize getID3 engine
    $getID3 = new getID3;
    $getID3->setOption(array('encoding' => Configure::read('App.encoding')));

    // Analyze file and store returned data in $ThisFileInfo
    $ThisFileInfo = $getID3->analyze($filename);

    return $ThisFileInfo;
  }

  function read($filename) { return $this->extract($filename); }

  function getId3Clean($filename)
  {
    $info = $this->read($filename);

    $id3 = array();
    foreach ($info['tags'] as $tag) {
      foreach ($tag as $key => $val) {
        if (empty($id3[$key])) {
          $id3[$key] = $val[0];
        }
        else {
          if (strlen($val[0]) > strlen($id3[$key])) {
            $id3[$key] = $val[0];
          }
        }
      }
    }
    return $id3;
  }

  function getCustomTags($filename)
  {
    $id3 = $this->getId3Clean($filename);
    $vars = array(
      'description' => 'content_group_description',
      'set'         => 'part_of_a_set'
    );
    foreach ($vars as $k => $v) {
      if (!empty($id3[$v])) {
        $id3[$k] = $id3[$v];
        unset($id3[$v]);
      }
    }
    return $id3;
  }

  function write($filename, $data)
  {
    App::import('vendor', 'getid3/getid3/getid3');

    // Initialize getID3 engine
    $getID3 = new getID3;
    $getID3->setOption(array('encoding' => Configure::read('App.encoding')));

    App::import('vendor','getid3/getid3', array('file' => 'write.php'));

    // Initialize getID3 tag-writing module
    $tagwriter = new getid3_writetags;

    //$tagwriter->filename       = '/path/to/file.mp3';
    $tagwriter->filename   = $filename;
    $tagwriter->tagformats = array('id3v1', 'id3v2.3');

    // set various options (optional)
    $tagwriter->overwrite_tags    = true;
    $tagwriter->tag_encoding      = Configure::read('App.encoding');
    $tagwriter->remove_other_tags = true;

    // populate data array
    $TagData['title'][]   = !empty($data['title'])?$data['title']:null;
    $TagData['artist'][]  = !empty($data['artist'])?$data['artist']:null;
    $TagData['album'][]   = !empty($data['album'])?$data['album']:null;;
    $TagData['year'][]    = !empty($data['year'])?$data['year']:null;;
    $TagData['genre'][]   = !empty($data['genre'])?$data['genre']:null;;
    $TagData['comment'][] = 'from www.underave.net';
    $TagData['track'][]   = !empty($data['track'])?$data['track']:null;;

    $tagwriter->tag_data = $TagData;

    // write tags
    if ($tagwriter->WriteTags()) {
      if (!empty($tagwriter->warnings)) {
        return $tagwriter->warnings;
      }
      return true;
    } else {
      return $tagwriter->errors;
    }
  }

  function joinMp3($file_out, $files_in)
  {
    foreach ($files_in as $nextinputfilename) {
      if (!is_readable($nextinputfilename)) {
        $this->error('Cannot read "' . $nextinputfilename . '"');
      }
    }
    if (!empty($this->errors)) return false;

    if (!is_writeable(dirname($file_out))) {
      $this->error('Cannot write "' . $file_out . '"');
      return false;
    }

    App::import('vendor', 'getid3/getid3', array('file' => 'getid3.php'));
    if ($fp_output = @fopen($file_out, 'wb')) {
      // Initialize getID3 engine
      $getID3 = new getID3;
      foreach ($files_in as $nextinputfilename) {
        $current_file_info = $getID3->analyze($nextinputfilename);
        if ($current_file_info['fileformat'] == 'mp3') {
          if ($fp_source = @fopen($nextinputfilename, 'rb')) {
            $current_output_position = ftell($fp_output);

            // copy audio data from first file
            fseek($fp_source, $current_file_info['avdataoffset'], SEEK_SET);
            while (!feof($fp_source) &amp;&amp; (ftell($fp_source) < $current_file_info['avdataend'])) {
              fwrite($fp_output, fread($fp_source, 32768));
            }

            fclose($fp_source);
            // trim post-audio data (if any) copied from first file that we don't need or want
            $end_offset = $current_output_position + ($current_file_info['avdataend'] - $current_file_info['avdataoffset']);
            fseek($fp_output, $end_offset, SEEK_SET);
            ftruncate($fp_output, $end_offset);
          } else {
            $this->error('failed to open ''.$nextinputfilename.'' for reading');
            fclose($fp_output);
            return false;
          }
        } else {
          $this->error('''.$nextinputfilename.'' is not MP3 format');
          fclose($fp_output);
          return false;
        }
      }
    } else {
      $this->error('failed to open ''.$file_out.'' for writing');
      return false;
    }
    fclose($fp_output);
    return true;
  }
}

Con este componente podéis tanto unir mp3 como leer y escribir etiquetas id3.

Evidentemente, antes de poder utilizar el componente debéis declararlo en el array de componentes de vuestro controlador:

1
2
3
class FooController extends AppController {
  $components = array('Getid3');
}

Para unir mp3 en un solo fichero no tenéis más que pasarle como primer parámetro la ruta del fichero de salida y como segundo parámetro pasarle un array con las ubicaciones de los ficheros mp3:

1
2
3
4
5
6
7
8
9
10
11
12
13
$destino = WWW_ROOT . 'files' . DS . 'podcasts' . DS . 'fichero_destino.mp3';
$mp3 = array(
  WWW_ROOT . 'files' . DS . 'mp3' . DS . 'fichero1.mp3',
  WWW_ROOT . 'files' . DS . 'mp3' . DS . 'fichero2.mp3',
  WWW_ROOT . 'files' . DS . 'mp3' . DS . 'fichero3.mp3',
  WWW_ROOT . 'files' . DS . 'mp3' . DS . 'fichero4.mp3',
  WWW_ROOT . 'files' . DS . 'mp3' . DS . 'fichero5.mp3'
);
if ($this->Getid3->joinMp3($destino, $mp3)) {
  // fichero creado correctamente
} else {
  pr($this->Getid3->errors);
}

Y con esto termina este sencillo tutorial sobre cómo crear un podcast a partir de varios mp3.

Como he dicho al inicio, pronto explicaré cómo crear el XML de dicho Podcast para que podáis agregarlo a iTunes o cualquier otro podcatcher.