lunes, 5 de enero de 2015

El Demo del Día: Leer y Escribir la Metadata de un Archivo MP3

Leer y Escribir la Metadata de un Archivo MP3

Primer Demo del año, después de un largo periodo sin publicar ejemplos en el Blog. En esta ocasión vamos a ver como podemos leer y escribir la información (metadata) de un archivo MP3.

La especificación que define la metadata de un archivo MP3 se llama ID3 y están disponibles 2 versiones: ID3V1 y ID3V2. En este demo veremos como crear una aplicación que permita leer y escribir el ID3V1.

Existen Librerías en C# que permiten manejar el ID3, tales como:
TagLib
ID3Net
C#ID3Lib

Pero como el lema de este Blog es "Hazlo tu mismo", veremos como crear nuestra propia aplicación WinForms en C#, para lo cual debemos saber que los últimos 128 bytes de un archivo MP3 se usan para el ID3V1, es decir, para guardar información del archivo (Metadata).

La estructura seria la siguiente, para los últimos 128 bytes de un archivo MP3:
- Cabecera: 3 bytes (por ejemplo para el ID3V1 la palabra: TAG)
- Titulo: 30 bytes
- Artista: 30 bytes
- Album: 30 bytes
- Año: 4 bytes
- Comentario: 30 bytes que se dividen en 3 partes:
  -) Comentario: 28 primeros bytes
  -) Tiene Pista: 1 byte (0 si tiene numero de pista)
  -) Numero de Pista: 1 byte (numero de pista o track)
- Genero: 1 byte (Entero de 0 a 255 con los géneros, los cuales enumero en el código)

Teniendo estos conceptos claros ahora crearemos una aplicación que permita seleccionar un archivo MP3 y leer la información de la metadata y poder grabarla si se realiza cambios, para lo cual trabajaremos con archivos.

Crear una Aplicación WinForms en C#

Crear una Aplicación WinForms en C# llamada "Leer_Escribir_ID3_MP3" y al formulario cambiarle de nombre a "frmInfoMP3" y diseñarlo como se muestra en la siguiente figura:



Crear una Clase con la estructura de la MetaData del MP3

Agregar una clase a la aplicación llamada: "MP3ID3Tag" para almacenar la metadata y escribir el siguiente código:

namespace Leer_Escribir_ID3_MP3
{
    public class MP3ID3Tag
    {
        public byte[] Id = new byte[3];
        public byte[] Titulo = new byte[30];
        public byte[] Artista = new byte[30];
        public byte[] Album = new byte[30];
        public byte[] Año = new byte[4];
        public byte[] Comentario = new byte[30];
        public byte[] Genero = new byte[1];

        public enum Generos
        {
            Blues =0,
            Classic_Rock =1,
            Country=2,
            Dance=3,
            Disco=4,
            Funk=5,
            Grunge=6,
            HipHop=7,
            Jazz=8,
            Metal=9,
            New_Age=10,
            Oldies=11,
            Other=12,
            Pop=13,
            Rhythm_And_Blues=14,
            Rap=15,
            Reggae=16,
            Rock=17,
            Techno=18,
            Industrial=19,
            Alternative=20,
            Ska=21,
            Death_Metal=22,
            Pranks=23,
            Soundtrack=24,
            Euro_Techno=25,
            Ambient=26,
            Trip_Hop=27,
            Vocal=28,
            Jazz_Funk=29,
            Fusion=30,
            Trance=31,
            Classical=32,
            Instrumental=33,
            Acid=34,
            House=35,
            Game=36,
            Sound_Clip = 37,
            Gospel=38,
            Noise=39,          
            Alternative_Rock = 40,
            Bass=41,
            Soul=42,
            Punk_Rock=43,
            Space=44,
            Meditative=45,
            Instrumental_Pop=46,
            Instrumental_Rock = 47,
            Ethnic=48,
            Gothic=49,
            Darkwave=50,
            Techno_Industrial=51,
            Electronic=52,
            Pop_Folk=53,
            Eurodance=54,
            Dream=55,
            Southern_Rock=56,
            Comedy=57,
            Cult=58,
            Gangsta=59,
            Top_40=60,
            Christian_Rap=61,
            Pop_Funk=62,
            Jungle=63,
            Native_American=64,
            Cabaret=65,
            New_Wave=66,
            Psychedelic=67,
            Rave=68,
            Showtunes=69,
            Trailer=70,
            Lo_Fi=71,
            Tribal=72,
            Acid_Punk=73,
            Acid_Jazz = 74,
            Polka=75,
            Retro=76,
            Musical=77,
            Rock_And_Roll=78,
            Hard_Rock=79,
            Folk=80,
            Folk_Rock=81,
            National_Folk=82,
            Swing=83,
            Fast_Fusion=84,
            Bebop=85,
            Latin=86,
            Revival=87,
            Celtic=88,
            Bluegrass=89,
            Avantgarde=90,
            Gothic_Rock=91,
            Progresive_Rock=92,
            Psychedelic_Rock = 93,
            Symphonic_Rock = 94,
            Slow_Rock=95,
            Big_Band=96,
            Chorus=97,
            Easy_Listening=98,
            Acoustic=99,
            Humour=100,
            Speech=101,
            Chanson=102,
            Opera=103,
            Chamber_Music=104,
            Sonata=105,
            Symphony=106,
            Booty_Bass=107,
            Primus=108,
            Porn_Groove=109,
            Satire=110,
            Slow_Jam=111,
            Club=112,
            Tango=113,
            Samba=114,
            Folklore=115,
            Ballad=116,
            Power_Ballad=117,
            Rhythmic_Soul=118,
            Freestyle=119,
            Duet=120,
            PunkRock=121,
            Drum_Solo=122,
            Acapella=123,
            Euro_House=124,
            Dance_Hall=125,
            Goa_Trance=126,
            Drum_Bass=127,
            Club_House=128,
            Hardcore_Techno=129,
            Terror=130,
            Indie=131,
            Britpop=132,
            Afro_Punk=133,
            Polsk_Punk=134,
            Beat=135,
            Christian_Gangsta_Rap=136,
            Heavy_Metal=137,
            Black_Metal=138,
            Crossover=139,
            Contemporary_Christian=140,
            Christian_Rock=141,
            Merengue=142,
            Salsa=143,
            Thrash_Metal=144,
            Anime=145,
            Jpop=146,
            Synthpop=147,
            Abstract=148,
            Art_Rock=149,
            Baroque=150,
            Bhangra=151,
            Big_Beat=152,
            Breakbeat=153,
            Chillout=154,
            Downtempo=155,
            Dub=156,
            EBM=157,
            Eclectic=158,
            Electro=159,
            Electroclash=160,
            Emo=161,
            Experimental=162,
            Garage=163,
            Global = 164,
            IDM=165,
            Illbient=166,
            Industro_Goth=167,
            Jam_Band=168,
            Krautrock=169,
            Leftfield=170,
            Lounge=171,
            Math_Rock=172,
            New_Romantic=173,
            Nu_Breakz=174,
            Post_Punk=175,
            Post_Rock=176,
            Psytrance=177,
            Shoegaze=178,
            Space_Rock=179,
            Trop_Rock=180,
            World_Music=181,
            Neoclassical=182,
            Audiobook=183,
            Audio_Theatre=184,
            Neue_Deutsche_Welle=185,
            Podcast=186,
            Indie_Rock=187,
            G_Funk=188,
            Dubstep=189,
            Garage_Rock=190,
            Psybient=191
        }
    }
}

Programar con Archivos en el Formulario WinForms en C#

Escribir el siguiente código en el formulario "frmInfoMP3":

using System;
using System.Text;
using System.IO;
using System.Windows.Forms;

namespace Leer_Escribir_ID3_MP3
{
    public partial class frmInfoMP3 : Form
    {
        public frmInfoMP3()
        {
            InitializeComponent();
        }

        private void cargarGeneros(object sender, EventArgs e)
        {
            Type tipo = typeof(MP3ID3Tag.Generos);
            cboGenero.DataSource = Enum.GetNames(tipo);
            cboGenero.SelectedIndex = -1;
        }

        private void abrirArchivoMp3(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Title = "Abrir archivos mp3";
            ofd.Filter = "Archivos mp3|*.mp3";
            ofd.InitialDirectory = @"C:\Musica\The Best";
            if (ofd.ShowDialog().Equals(DialogResult.OK))
            {
                btnGrabar.Enabled = true;
                txtArchivoMp3.Text = ofd.FileName;
                leerInfoMP3();
            }
        }

        private void leerInfoMP3()
        {
            using (FileStream fs = File.OpenRead(txtArchivoMp3.Text))
            {
                if (fs.Length >= 128)
                {
                    MP3ID3Tag tag = new MP3ID3Tag();
                    fs.Seek(-128, SeekOrigin.End);
                    fs.Read(tag.Id, 0, tag.Id.Length);
                    fs.Read(tag.Titulo, 0, tag.Titulo.Length);
                    fs.Read(tag.Artista, 0, tag.Artista.Length);
                    fs.Read(tag.Album, 0, tag.Album.Length);
                    fs.Read(tag.Año, 0, tag.Año.Length);
                    fs.Read(tag.Comentario, 0, tag.Comentario.Length);
                    fs.Read(tag.Genero, 0, tag.Genero.Length);
                    string tagId = Encoding.Default.GetString(tag.Id);
                    if (tagId.Equals("TAG"))
                    {
                        txtTitulo.Text = Encoding.Default.GetString(tag.Titulo).Trim();
                        txtArtista.Text = Encoding.Default.GetString(tag.Artista).Trim();
                        txtAlbum.Text = Encoding.Default.GetString(tag.Album).Trim();
                        txtAño.Text = Encoding.Default.GetString(tag.Año);
                        byte[] comentario = new byte[28];
                        Buffer.BlockCopy(tag.Comentario, 0, comentario, 0, 28);
                        txtComentario.Text = Encoding.Default.GetString(comentario).Trim();
                        Type tipo = typeof(MP3ID3Tag.Generos);
                        byte n = (byte)tag.Genero[0];
                        string genero = Enum.GetName(tipo, n);
                        int pos = cboGenero.Items.IndexOf(genero);
                        if (pos > -1) cboGenero.SelectedIndex = pos;
                        if (tag.Comentario[28].Equals(0))
                        {
                            txtPista.Text = tag.Comentario[29].ToString();
                        }
                    }
                }
            }
        }

        private void escribirInfoMP3()
        {
            using (FileStream fs = File.OpenWrite(txtArchivoMp3.Text))
            {
                if (fs.Length >= 128)
                {
                    MP3ID3Tag tag = new MP3ID3Tag();
                    tag.Id = Encoding.Default.GetBytes("TAG");
                    tag.Titulo = Encoding.Default.GetBytes(txtTitulo.Text.PadRight(30, ' '));
                    tag.Artista = Encoding.Default.GetBytes(txtArtista.Text.PadRight(30, ' '));
                    tag.Album = Encoding.Default.GetBytes(txtAlbum.Text.PadRight(30, ' '));
                    tag.Año = Encoding.Default.GetBytes(txtAño.Text);
                    byte[] comentario = new byte[28];
                    comentario = Encoding.Default.GetBytes(txtComentario.Text.PadRight(28, ' '));
                    byte[] tienePista = new byte[] { 0 };
                    byte pista = byte.Parse(txtPista.Text);
                    byte[] numeroPista = new byte[] { pista };                  
                    Buffer.BlockCopy(comentario, 0, tag.Comentario, 0, 28);
                    Buffer.BlockCopy(tienePista, 0, tag.Comentario, 28, 1);
                    Buffer.BlockCopy(numeroPista, 0, tag.Comentario,29, 1);
                    Type tipo = typeof(MP3ID3Tag.Generos);
                    MP3ID3Tag.Generos genero = (MP3ID3Tag.Generos)
                    Enum.Parse(tipo, cboGenero.SelectedItem.ToString());
                    byte n = (byte)genero;
                    tag.Genero = new byte[] {n};
                    fs.Seek(-128, SeekOrigin.End);
                    fs.Write(tag.Id, 0, tag.Id.Length);
                    fs.Write(tag.Titulo, 0, tag.Titulo.Length);
                    fs.Write(tag.Artista, 0, tag.Artista.Length);
                    fs.Write(tag.Album, 0, tag.Album.Length);
                    fs.Write(tag.Año, 0, tag.Año.Length);
                    fs.Write(tag.Comentario, 0, tag.Comentario.Length);
                    fs.Write(tag.Genero, 0, tag.Genero.Length);
                    MessageBox.Show("ID Tag V1 del archivo mp3 fue cambiado");
                }
            }
        }

        private void grabarArchivoMP3(object sender, EventArgs e)
        {
            escribirInfoMP3();
        }
    }
}

Nota: La función "cargarGeneros" esta asociada al evento "Load" del formulario, la función "abrirArchivoMp3" esta asociada al evento "Click" del botón "btnAbrirMp3" y la función "grabarArchivoMP3" esta asociada al evento "Click" del botón "btnGrabar".

Probar la Aplicación Windows Forms

Grabar la aplicación y ejecutarla con F5. Se mostrará la siguiente ventana:


Nota: Ver como se ha llenado la lista con los géneros disponibles en la enumeración.

Clic al botón de la parte superior derecha y seleccionar un archivo MP3 en el diálogo de abrir, luego se mostrará la información de dicho archivo, similar a la siguiente figura:


Cambiar la información del archivo, por ejemplo el Album, cambiar el Genero, agregar un Comentario y luego "Grabar".


Cerrar la aplicación y volverla a ejecutar nuevamente para verificar si la información del archivo se guardó o también abrir el Winamp, reproducir la canción y pulsar Alt + 3 para ver la información del archivo y verificar que los cambios se han realizado satisfactoriamente.

Comentario Final

En este post hemos aprendido a leer y escribir la metadata de un archivo MP3 manejando los 128 últimos bytes del archivo usando el método "Seek" de la clase "FileStream" para ubicarnos en los últimos 128 bytes y a partir de esa posición usar "Read" para leer y "Write" para escribir.

Espero que les guste este primer post y tratare de que sean mas seguidos, ya que es lo que mas interesa a los visitantes del Blog.

Descarga como rar
Demo28_Leer_Escribir_ID3_MP3

Descarga sin extension (luego renombrar a rar y descomprimir)
Leer_Escribir_ID3_MP3

10 comentarios:

  1. Excelente aplicación. Buena explicación. Que mas puedo decir. No hay mayor satisfacción que desarrollar uno mismo sin usar Infragistics ni depender de librerías hechas por otros. Lo mejor de sus ejemplos son las explicaciones. ¡¡Gracias totales!!

    ResponderBorrar
  2. Gracias profesor por el aporte, pero como llego a esas conclusiones, como llega a saber que en tal momento esta tal cosa.

    ResponderBorrar
  3. David, justamente el ID3 es la especificación que te indica cual es la estructura que debe tener la metadata:
    http://en.wikipedia.org/wiki/ID3

    ResponderBorrar
  4. La cantidad de caracteres que muestra solo es según la definición de los arreglos? verdad. En algunos casos los títulos almacenan mas caracteres y las descripciones también.

    ResponderBorrar
  5. cuando pregunta if (tagId.Equals("TAG")) y solo en ese caso arroja la metadata a las casillas, a que se refiere exactamente? Por que veo que en algunos casos el tagId me devuelve TAGA o TAGD

    ResponderBorrar
  6. Wilder el ID3 V1 define solo 30 caracteres para el Titulo, Album y Comentario. Si deseas un MP3 con mas caracteres para las descripciones se usa ID3 V2.
    Si la propiedad Id es de solo 3 bytes es imposible que sea TAGA o TAGD debes estar tomando 4 caracteres y no 3.
    Acabo de volver a publicar el enlace para que descarguen la aplicación.

    ResponderBorrar
  7. Que tal, Luis... Acabo de probar los enlaces y Google Drive no permite descargarlos por sus politicas... Aunque si el código esta completo, bastará con seleccionar, copiar y pegar de acuerdo a tus indicaciones... Gracias por el aporte.

    ResponderBorrar