Vamos a programar #103 - Leyendo metadatos de archivos FLAC (pt 2. Ver c#)

Hola de nuevo a todos. El día de hoy vamos a continuar con mas de los archivos FLAC y sus metadatos.

En el post anterior vimos cómo es que está estructurado cada bloque de metadatos, incluso vimos cómo es que se hace la lectura de cada uno (y leímos el bloque "STREAMINFO"). Ahora que sabemos cómo, solo nos queda automatizarlo y para eso vamos a usar el lenguaje de programación C#.
Para empezar, veamos el código (sin ningún orden particular en las funciones):


using System; using System.Drawing; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace FLACTagReader { public partial class Form1 : Form { // 0 : STREAMINFO 0000000 // 1 : PADDING 0000001 // 2 : APPLICATION 0000010 // 3 : SEEKTABLE 0000011 // 4 : VORBIS_COMMENT 0000100 // 5 : CUESHEET 0000101 // 6 : PICTURE 0000110 // 7-126 : reserved 1111000 - Invalid // 127 : invalid, to avoid confusion with a frame sync code /// <summary> /// Enumeración con los posibles bloques contenidos en un archivo FLAC /// </summary> private enum BlocksTypes { /// <summary> /// Bloque mandatorio con la información del Stream /// </summary> Block_Type_StreamInfo = 0, /// <summary> /// Bloque de Padding /// </summary> Block_Type_Padding = 1, /// <summary> /// Bloque con la informacion de la aplicación /// </summary> Block_Type_Application = 2, /// <summary> /// Bloque Seektable /// </summary> Block_Type_SeekTable = 3, /// <summary> /// Bloque con metadatos /// </summary> Block_Type_VorbisComment = 4, /// <summary> /// Bloque con cuesheet /// </summary> Block_Type_CueSheet = 5, /// <summary> /// Bloque con imagen /// </summary> Block_Type_Picture = 6, /// <summary> /// Bloque no válido /// </summary> Block_Type_NoValid = 7 } public Form1() { InitializeComponent(); } /// <summary> /// Crea una imagen a partir de una secuencia de bytes /// </summary> /// <param name="bytesArr">Matriz de bytes que contiene los datos de la imagen</param> /// <returns>Regresa una imagen</returns> public Image ByteArrayToImage(byte[] bytesArr) { using (MemoryStream memstr = new MemoryStream(bytesArr)) { Image img = Image.FromStream(memstr); return img; } } // 4 4 n*4 4 n*4 4 4 4 4 !4 !n*4 //<32>,<32>,<n*8>,<32>,<n*8>,<32>,<32>,<32>,<32>,<32>,<n*8> // 1 2 3 4 5 6 7 8 9 10 11 /// <summary> /// Lee todo los datos del bloque PICTURE y extrae la imagen contenida /// </summary> /// <param name="TheData">Arreglos bytes que contiene todo el bloque PICTURE</param> /// <returns>Regresa una imagen</returns> private Image ReadPictureData(byte[] TheData) { uint CurrentPos = 4;//1 uint CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4); CurrentPos += 4;//2 string MimeType = Encoding.ASCII.GetString(TheData, (int)CurrentPos, (int)CurrentSize); CurrentPos += CurrentSize;//3 CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4); CurrentPos += (CurrentSize + (4 * 5));//4-9 CurrentSize = GetBlockSize(TheData, (int)CurrentPos, 4); CurrentPos += 4; byte[] TempImage = new byte[CurrentSize]; Array.Copy(TheData, CurrentPos, TempImage, 0, CurrentSize); return ByteArrayToImage(TempImage); } /// <summary> /// Convierte un número en su valor equivalente al tipo de bloque /// </summary> /// <param name="Data"> /// Entero sin signo con la representacion del bloque</param> /// <returns>Regresa un valor de la enumeración <typeparamref name="BlockTypes"/>BlockTypes</returns> private BlocksTypes NumberToBlockType(uint Data) { switch (Data) { case 0: return BlocksTypes.Block_Type_StreamInfo; case 1: return BlocksTypes.Block_Type_Padding; case 2: return BlocksTypes.Block_Type_Application; case 3: return BlocksTypes.Block_Type_SeekTable; case 4: return BlocksTypes.Block_Type_VorbisComment; case 5: return BlocksTypes.Block_Type_CueSheet; case 6: return BlocksTypes.Block_Type_Picture; default: return BlocksTypes.Block_Type_NoValid; } } /// <summary> /// Lee el bloque con la informacion estructurada /// </summary> /// <param name="TheData">Arreglo de bytes del cual se extraera la información</param> /// <returns>Regresa un arreglo del tipo string con todos los campos que se encontraron</returns> private string[] ReadVorbisData(byte[] TheData) { uint NumberOfFields = 0; uint CurrentField = 1; uint CurrentPosition = 0; //Vendor uint CurrentSize = BitConverter.ToUInt32(TheData, (int)CurrentPosition); CurrentPosition += 4; byte[] CurrentChunk = new byte[CurrentSize]; Array.Copy(TheData, CurrentPosition, CurrentChunk, 0, CurrentSize); string Currenttext = Encoding.UTF8.GetString(CurrentChunk); CurrentPosition += CurrentSize; NumberOfFields = BitConverter.ToUInt32(TheData, (int)CurrentPosition); CurrentPosition += 4; string[] Fields = new string[NumberOfFields]; //CommentField while (CurrentField <= NumberOfFields) { CurrentSize = BitConverter.ToUInt32(TheData, (int)CurrentPosition); CurrentPosition += 4; byte[] CurrenUserCommentList = new byte[CurrentSize]; Array.Copy(TheData, CurrentPosition, CurrenUserCommentList, 0, CurrentSize); Currenttext = Encoding.UTF8.GetString(CurrenUserCommentList); CurrentPosition += CurrentSize; Fields[CurrentField - 1] = Currenttext; CurrentField += 1; } return Fields; } /// <summary> /// Obtiene el valor del bloque desde un byte /// </summary> /// <param name="Data">byte del cual se va a obtener la información</param> /// <returns>Regresa un valor que es equivalente al tipo de bloque</returns> private UInt32 GetBlockType(byte Data) { Data <<= 3; Data >>= 3; return Data; } /// <summary> /// Obtiene si el bloque es el último de la serie /// </summary> /// <param name="Data">byte del cual se obtendra la información</param> /// <returns>true si el bloque es el último, false en caso contrario</returns> private bool IsLastFrame(byte Data) { Data >>= 7; if (Data == 1) return true; else return false; } /// <summary> /// Obtiene el tamaño de un bloque /// </summary> /// <param name="Data">Arreglo de bytes que contiene el tamaño del bloque</param> /// <param name="StartIndex">Indica la posicion en donde se empezará a leer</param> /// <param name="Size">Indica el tamaño en bytes que se van a leer</param> /// <returns>Regresa el tamaño del bloque actual</returns> private UInt32 GetBlockSize(byte[] Data, int StartIndex, int Size) { byte[] CurrentData = new byte[4]; Array.Copy(Data, StartIndex, CurrentData, 0, Size); Array.Reverse(CurrentData); return BitConverter.ToUInt32(CurrentData, 0); } /// <summary> /// Obtiene el tamaño de un bloque /// </summary> /// <param name="Data">Arreglo de bytes que contiene el tamaño del bloque</param> /// <returns>Regresa el tamaño del bloque actual</returns> private UInt32 GetBlockSize(byte[] Data) { byte[] CurrentData = Data; //Ponemos el primer byte en 0 porque se usa para identificar el bloque //a la hora de convertir a UInt32 se esperan 4 bytes, pero este siempre será 0 u otro valor no relevante //para el tamaño del bloque CurrentData[0] = 0; Array.Reverse(CurrentData); return BitConverter.ToUInt32(CurrentData, 0); } /// <summary> /// Obtiene los datos de un bloque /// </summary> /// <param name="FileName">Nombre del archivo del cual se obtendra</param> /// <param name="TypeOfBlock">Tipo de bloque que se va a buscar</param> private void GetBlock(string FileName, BlocksTypes TypeOfBlock) { byte[] DataBuff; bool LastFrame = false; FileStream FS = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read); using (BinaryReader BR = new BinaryReader(FS, Encoding.ASCII)) { DataBuff = BR.ReadBytes(4); if (string.Equals(Encoding.ASCII.GetString(DataBuff), "fLaC", StringComparison.Ordinal)) { UInt32 CurrentBlockSize; UInt32 CurrentPosition = (UInt32)FS.Position; BlocksTypes CurrentBlockType = BlocksTypes.Block_Type_NoValid; while (!LastFrame) { //primer bloque siempre será Block_Type_StreamInfo DataBuff = BR.ReadBytes(4); LastFrame = IsLastFrame(DataBuff[0]); CurrentBlockType = NumberToBlockType(GetBlockType(DataBuff[0])); if (CurrentBlockType == TypeOfBlock) { if (TypeOfBlock == BlocksTypes.Block_Type_VorbisComment) { string[] Fields; CurrentBlockSize = GetBlockSize(DataBuff); Fields = ReadVorbisData(BR.ReadBytes((int)CurrentBlockSize)); textBox1.Clear(); for (int i = 0; i < Fields.Length; i++) { textBox1.Text += Fields[i] + " | "; } } else if (TypeOfBlock == BlocksTypes.Block_Type_Picture) { CurrentBlockSize = GetBlockSize(DataBuff); pictureBox1.Image = ReadPictureData(BR.ReadBytes((int)CurrentBlockSize)); } } else { CurrentBlockSize = GetBlockSize(DataBuff); FS.Seek(FS.Position + CurrentBlockSize, SeekOrigin.Begin); } } } else { MessageBox.Show("Archivo Flac no válido"); } } } private void button1_Click(object sender, EventArgs e) { using (OpenFileDialog OpDiag = new OpenFileDialog()) { OpDiag.Filter = "Archivos FLAC|*.flac|Todos los arhivos|*.*"; if (OpDiag.ShowDialog() == DialogResult.OK) { GetBlock(OpDiag.FileName, BlocksTypes.Block_Type_VorbisComment); GetBlock(OpDiag.FileName, BlocksTypes.Block_Type_Picture); pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage; } } } } }
Lo primero que hacemos es crear una enumeración llamada "BlockTypes" que contiene las posibles enumeraciones para cualquier tipo de bloque. Si bien la especificación dice que los valores de 0 hasta 127 son validos (aunque no tienen uso todos, al menos  al momento de escribir esto), decidí declarar cómo no validos a los que superan al 6 y por eso, después de ahí, su nombre es "Block_Type_NoValid". Se podrían usar solo números, pero me pareció mas fácil de identificar cada uno de esta forma.

La primera función es "ByteArrayToImage()" del tipo "Image". Esta función, cómo su nombre lo pudiera indicar, se encarga de convertir una secuencia de bytes en una imagen (si los datos son válidos), recibe cómo parámetro un arreglo del tipo "byte[]" que contiene la secuencia de bytes de la imagen. Regresa un valor del tipo "Image" que es la imagen dentro del archivo (si es que hay).

La siguiente función es "ReadPictureData()". Esta función es la encargada de procesar los datos del bloque  "PICTURE". Recibe cómo parámetro un arreglo del tipo "byte[]" que contiene la secuencia de bytes del bloque "PICTURE" completo. Para procesarlo, se siguen las siguientes:
  • <32> El tipo de imagen de acuerdo a la descripción de la etiqueta ID3 "APIC".
  • <32> El tamaño de la descripción del tipo de archivo o MIME type.
  • <n*8> Cadena de texto con la descripción del tipo de archivo.
  • <32> El tamaño de la descripción de la imagen.
  • <n*8> La descripción de la imagen en UTF8
  • <32> El ancho de la imagen en píxeles
  • <32> El alto de la imagen en pixeles.
  • <32> La profundidad del color de la imagen en bits por pixel.
  • <32> Para las imágenes con índice de color, el número de colores usados, para el resto 0.
  • <32> El tamaño de los datos de la imagen.
  • <n*8> Los datos de la imagen.
Para realizar la lectura de la imagen se asume que solo se pasan los datos del bloque completos, solo omitiendo el tamaño del bloque mismo. Y en este caso, cómo solo nos interesa obtener la imagen "tal cual" solo nos concentraremos en leer las ultimas dos secuencias, pero en el código lo hacemos paso a paso para poder apreciar mejor lo que esta ocurriendo y para simplificarlo, lo podemos apreciar mejor en el siguiente diagrama:


Todo este bloque se representa en 11 partes (parte de abajo de la imagen), cada una de la partes ya tiene su tamaño en bits establecido (parte media), que es lo mismo en bytes (parte superior). Si bien hace un momento mencione que vamos a ignorar todo salvo la última y penúltima parte, no podemos ignorar del todo a los otros datos, sobre todo a los que son variables. Ahora vamos leyendo el código.
Primero creamos una variable del tipo "uint" llamada "CurrentPos", esta la vamos a ir usando para saber en que parte del bloque estamos, si observaste bien, te darás cuenta que esta inicializada con un valor de 4, al hacer esto, estaremos brincando la primer secuencia de 32 bits 

Seguido, creamos una variable llamada "CurrentSize" del tipo "uint" y la asignamos con el valor de la llamada a la función "GetBlockSIze()" (que veremos pronto), con esto tendremos el tamaño de la siguiente secuencia (3) por lo que podemos avanzar 4 bytes mas el tamaño que nos dio con la función anterior 
.
Aunque C# nos da muchas facilidades, a la hora de tratar de hacer lo mismo en otro lenguaje las cosas se pueden complicar, este bloque es una cadena de texto con la descripción del tipo de archivo por lo que es importante tenerlo en cuenta, para eso creamos una variable llamada "MimeType" del tipo "string" y se le asigna el valor de la llamada a la función "Encoding.ASCII.GetString()", esta recibe tres parámetro, el primero, un arreglo del tipo "byte[]" con los datos de la cadena ASCII que esperamos obtener; el segundo es un valor del tipo "int" que sirve cómo indicador de que parte del arreglo es donde se va a empezar a leer; el tercero es un valor del tipo "int" que sirve para indicar cuantos bytes de la secuencia se van a leer. Al realizar la "conversión" de una secuencia de "bytes" a "string", tendremos algo cómo lo de la imagen que sigue:

Luego sumamos la posición actual y el tamaño de la secuencia anterior. Para las dos siguientes partes, repetimos lo mismo: obtenemos el tamaño del bloque, lo sumamos a la posición actual y avanzamos cuatro bytes mas.

Las siguientes 4 secuencias son iguales en tamaño, por lo que podemos brincar 4*4 (en el código es 4*5 porque brinque la secuencia anterior tambien).

Una vez leído las partes anteriores, solo queda leer los datos de la imagen por lo que repetimos nuevamente: obtenemos el tamaño de los datos de la imagen, creamos una nueva variable del tipo "byte[]" llamada "TempImage" y al mismo tiempo le asignamos el tamaño de la secuencia actual, luego copiamos la porción del bloque al arreglo que acabamos de crear y devolvemos el resultado de mandar a llamar la función "ByteArrayToImage()".

Y bien, por ahora es todo, en el siguiente post publicaré el proyecto completo y terminaremos de revisar las funciones que no vimos.

Los leo luego.

No hay comentarios.