Vamos a programar #107 - Agregando imagenes a archivos FLAC usando C# (Parte 1).
Hola de nuevo a todos. El día de hoy vamos a continuar con mas de los archivos FLAC. En post anteriores vimos cómo insertar la imagen usando solo un editor hexadecimal, pero hoy, vamos a usar C# para crear un programa que lo haga por nosotros.
Antes de continuar te recomiendo que leas los post relacionados:
- Vamos a programar #102
- Vamos a programar #103
- Vamos a programar #104
- Vamos a programar #105
- Vamos a programar #106
Una vez leidos los post anteriores, vamos a ver el código que hace funcionar el programa. Algo importante que debo aclarar, es que por primera vez en la historia del blog, voy a usar visual studio 2022 y .Net 8.0, la mayor parte del código es retro-compatible, pero en c# 7.0 se agregaron algunas caracteristicas que hacen las cosas mas sencillas, por lo que si quieres probar el código deberías de instalar al menos esa version de visual studio.Además el código es mas que nada una prueba de concepto por lo que se puede optimizar, pero además, todo se hace para que sea lo mas similar al proceso que se hizo a mano (Vamos a programar #102) por lo que si quieres optimizarlo, puedes hacerlo sin ningun problema.
Una vez dicho eso, el código es el siguiente:
using System.Diagnostics.Eventing.Reader;
using System.Runtime.InteropServices;
using System.Text;
using System.Drawing.Imaging;
using System.Net.Http.Headers;
using System.ComponentModel;
namespace FlacPictureWriter
{
public partial class Form1 : Form
{
///Picture type:
/// $00 Other
/// $01 32x32 pixels 'file icon' (PNG only)
/// $02 Other file icon
/// $03 Cover (front)
/// $04 Cover (back)
/// $05 Leaflet page
/// $06 Media (e.g. lable side of CD)
/// $07 Lead artist/lead performer/soloist
/// $08 Artist/performer
/// $09 Conductor
/// $0A Band/Orchestra
/// $0B Composer
/// $0C Lyricist/text writer
/// $0D Recording Location
/// $0E During recording
/// $0F During performance
/// $10 Movie/video screen capture
/// $11 A bright coloured fish
/// $12 Illustration
/// $13 Band/artist logotype
/// $14 Publisher/Studio logotype
private enum APICType
{
/// <summary>
/// Imagen génerica
/// </summary>
APIC_TYPE_Other = 0,
/// <summary>
/// ícono (solo PNG)
/// </summary>
APIC_TYPE_FileIcon,
/// <summary>
/// Otro ícono
/// </summary>
APIC_TYPE_OtherFile_Icon,
/// <summary>
/// Cubierta frontal del disco
/// </summary>
APIC_TYPE_CoverFront,
/// <summary>
/// Cubierta trasera del disco
/// </summary>
APIC_TYPE_CoverBack,
/// <summary>
/// Folleto
/// </summary>
APIC_TYPE_LeafletPage,
/// <summary>
/// Otra ímagen
/// </summary>
APIC_TYPE_Media,
/// <summary>
/// Artista principal
/// </summary>
APIC_TYPE_LeadArtist,
/// <summary>
/// Artista
/// </summary>
APIC_TYPE_Artist,
/// <summary>
/// Conductor
/// </summary>
APIC_TYPE_Conductor,
/// <summary>
/// Banda
/// </summary>
APIC_TYPE_Band,
/// <summary>
/// Compositor
/// </summary>
APIC_TYPE_Composer,
/// <summary>
/// Letrista
/// </summary>
APIC_TYPE_Lyricist,
/// <summary>
/// Lugar de la grabacion
/// </summary>
APIC_TYPE_RecordingLocation,
/// <summary>
/// Captura durante la grabacion
/// </summary>
APIC_TYPE_DuringRecording,
/// <summary>
/// Captura durante concierto
/// </summary>
APIC_TYPE_DuringPerformance,
/// <summary>
/// Captura de videclip
/// </summary>
APIC_TYPE_VideoScreenCapture,
/// <summary>
/// ???
/// </summary>
APIC_TYPE_ABrightColouredFish,
/// <summary>
/// Ilustracion
/// </summary>
APIC_TYPE_Illustration,
/// <summary>
/// Logotipo de la banda/artista
/// </summary>
APIC_TYPE_ArtistLogotype,
/// <summary>
/// Logotipo de la discografica
/// </summary>
APIC_TYPE_PublisherLogotype,
APIC_TYPE_NoValid
}
// 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();
}
List<UInt32> Direcciones;
bool TieneImagen = false;
UInt32 ImageSize = 0;
UInt32 LastBLockPosition = 0;
UInt32 VorbisCommentPosition = 0;
UInt32 ImagePosition = 0;
private byte[] NormalizeToFourBytes(UInt32 Value, bool Reverse = false)
{
byte[] buff = BitConverter.GetBytes(Value);
if (Reverse)
Array.Reverse(buff);
return buff;
}
/// <summary>
/// Obtiene el "Mime Type" basado en la extension de un archivo
/// </summary>
/// <param name="FilePath">Ruta de la cual se va a extraer la información</param>
/// <returns></returns>
private string GetMIMEType(string FilePath)
{
FileInfo FI = new(FilePath);
if (string.Compare(FI.Extension, ".jpg", StringComparison.OrdinalIgnoreCase) == 0)
{
return @"image/jpeg";
}
else if (string.Compare(FI.Extension, ".png", StringComparison.OrdinalIgnoreCase) == 0)
{
return @"image/PNG";
}
else
{
return "No valido";
}
}
/// <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(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>
/// Actualiza la imagen de un archivo FLAC.
/// </summary>
/// <param name="ImagePath">Ruta de la imagen JPG</param>
private byte[] BuildAPICFrame(string ImagePath, int PictureType, string MimeType, UInt32 ImageSize)
{
List<byte> Data = new List<byte>();
byte[] Buff = new byte[ImageSize];
using (FileStream FS = new FileStream(ImagePath, FileMode.Open))
{
using (BinaryReader BR = new(FS))
{
Buff = BR.ReadBytes((int)ImageSize);
}
}
//Tipo de Imagen APIC
Data.AddRange(NormalizeToFourBytes((UInt32)PictureType, true));
//Tamaño del tipo de contenido
Data.AddRange(NormalizeToFourBytes((UInt32)MimeType.Length, true));
//Tipo de contenido
Data.AddRange(Encoding.ASCII.GetBytes(MimeType));
//Descripcion del tipo de contenido
Data.AddRange(NormalizeToFourBytes((UInt32)MimeType.Length, true));
//Tamaño de la descripcion del contenido
Data.AddRange(Encoding.ASCII.GetBytes(MimeType));
//Ancho de la imagen
Data.AddRange(NormalizeToFourBytes(0, true));
//Alto de la imagen
Data.AddRange(NormalizeToFourBytes(0, true));
//Profundidad del color
Data.AddRange(NormalizeToFourBytes(0, true));
//Indice de la paleta de colores
Data.AddRange(NormalizeToFourBytes(0, true));
//Tamaño de la imagen en bytes
Data.AddRange(NormalizeToFourBytes(ImageSize, true));
//Datos de la Imagen
Data.AddRange(Buff);
return Data.ToArray();
}
/// <summary>
/// Construye el bloque de imagen
/// </summary>
/// <param name="APICData"></param>
/// <returns></returns>
private byte[] BuildPictureBlock(byte[] APICData)
{
byte[] Buff = new byte[APICData.Length + 4];
byte[] PICSize = new byte[4];
PICSize = NormalizeToFourBytes((UInt32)APICData.Length, true);
PICSize.CopyTo(Buff, 0);
//siempre pondremos la imagen al final de los metadatos
//Establecemos el marcador de ultimo bloque y el tipo de bloque en 6 (Block_Type_Picture)
//Quedando 10000110b o 134
Buff[0] = 134;
APICData.CopyTo(Buff, 4);
return Buff;
}
private List<UInt32> GetOffsets(string FileName)
{
byte[] DataBuff;
List<UInt32> offsets = new List<UInt32>();
bool LastFrame = false;
using (FileStream FS = new(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
offsets.Add((UInt32)FS.Position);
using (BinaryReader BR = new(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 = NumberToBlockType(GetBlockType(DataBuff[0]));
while (!LastFrame)
{
//primer bloque siempre ser� Block_Type_StreamInfo
offsets.Add((UInt32)FS.Position);
DataBuff = BR.ReadBytes(4);
LastFrame = IsLastFrame(DataBuff[0]);
CurrentBlockType = NumberToBlockType(GetBlockType(DataBuff[0]));
CurrentBlockSize = GetBlockSize(DataBuff);
FS.Seek(FS.Position + CurrentBlockSize, SeekOrigin.Begin);
if (CurrentBlockType == BlocksTypes.Block_Type_VorbisComment)
{
VorbisCommentPosition = offsets[offsets.Count - 1];
}
if (CurrentBlockType == BlocksTypes.Block_Type_Picture)
{
TieneImagen = true;
ImagePosition = offsets[offsets.Count - 1];
}
if (LastFrame)
LastBLockPosition = offsets[offsets.Count - 1];
}
}
else
{
MessageBox.Show("Archivo Flac no v�lido");
}
}
}
return offsets;
}
/// <summary>
/// Construye el encabezado de un bloque de informacion
/// </summary>
/// <param name="Number">Valor que indica la longitud del bloque en bytes</param>
/// <param name="BlockType">Valor que indica el tipo de bloque de acuerdo a <c>BlockTypes</c></param>
/// <param name="IsLastFrame">Indica si es el ultimo bloque de informacion</param>
/// <returns>Regresa un arreglo de cuatro bytes que representan el encabezado del bloque</returns>
private byte[] BuildHeader(uint Number, BlocksTypes BlockType, bool IsLastFrame)
{
byte[] HeaderBuff = new byte[4];
HeaderBuff = BitConverter.GetBytes(Number);
HeaderBuff[3] = (byte)BlockType;
if (IsLastFrame)
{
HeaderBuff[3] |= 128;
}
Array.Reverse(HeaderBuff);
return HeaderBuff;
}
/// <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 uint 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>
/// <returns>Regresa el tamaño del bloque actual</returns>
private uint 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 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 uint 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 una imagen desde un archivo
/// </summary>
private void GetImageFromFile()
{
using OpenFileDialog OpDiag = new()
{
Filter = "Imagenes|*.jpg;*.png|Archivos JPG|*.jpg|Imagenes PNG|*.png",
Multiselect = false
};
if (OpDiag.ShowDialog() == DialogResult.OK)
{
TxtInputPic.Text = OpDiag.FileName;
PicInput.ImageLocation = TxtInputPic.Text;
PicInput.SizeMode = PictureBoxSizeMode.StretchImage;
FileInfo FileSize = new FileInfo(TxtInputPic.Text);
ImageSize = (UInt32)FileSize.Length;
LblPicInfo.Text = "Tamaño de la imagen en bytes " + ImageSize.ToString() + " bytes";
}
}
/// <summary>
/// Obtiene un bloque de datos de un archivo a partir de una posicion definida
/// </summary>
/// <param name="FileName">Archivo del cual se va a extraer el bloque</param>
/// <param name="StartPosition">Posicion des la cual se va a obtener el bloque relativo al inicio del archivo</param>
/// <returns>Regresa un arreglo de bytes con el bloque completo</returns>
private byte[] GetBlockData(string FileName, int StartPosition)
{
byte[] DataBuff;
using (FileStream FS = new(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (BinaryReader BR = new(FS, Encoding.ASCII))
{
UInt32 CurrentBlockSize;
UInt32 CurrentPosition = (UInt32)FS.Position;
FS.Seek(StartPosition, SeekOrigin.Begin);
DataBuff = BR.ReadBytes(4);
CurrentBlockSize = GetBlockSize(DataBuff);
FS.Seek(StartPosition, SeekOrigin.Begin);
return (BR.ReadBytes((int)(CurrentBlockSize + 4)));
}
}
}
/// <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, int StartPosition)
{
byte[] DataBuff;
using (FileStream FS = new(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (BinaryReader BR = new(FS, Encoding.ASCII))
{
UInt32 CurrentBlockSize;
UInt32 CurrentPosition = (UInt32)FS.Position;
BlocksTypes CurrentBlockType = BlocksTypes.Block_Type_NoValid;
FS.Seek(StartPosition, SeekOrigin.Begin);
DataBuff = BR.ReadBytes(4);
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));
TxtInfo.Clear();
for (int i = 0; i < Fields.Length; i++)
{
TxtInfo.Text += Fields[i] + " | ";
}
}
else if (TypeOfBlock == BlocksTypes.Block_Type_Picture)
{
CurrentBlockSize = GetBlockSize(DataBuff);
PicFlac.Image = ReadPictureData(BR.ReadBytes((int)CurrentBlockSize));
}
}
else
{
return;
}
}
}
}
private void BtnOpen_Click(object sender, EventArgs e)
{
TieneImagen = false;
VorbisCommentPosition = 0;
ImagePosition = 0;
OpenFileDialog OpDiag = new()
{
Filter = "Archivos Flac|*.flac|Todos los arvhivos|*.*",
Multiselect = false
};
if (OpDiag.ShowDialog() == DialogResult.OK)
{
txtInputFlac.Text = OpDiag.FileName;
PicFlac.SizeMode = PictureBoxSizeMode.StretchImage;
Direcciones = GetOffsets(txtInputFlac.Text);
GetBlock(txtInputFlac.Text, BlocksTypes.Block_Type_VorbisComment, (int)VorbisCommentPosition);
if (TieneImagen)
GetBlock(txtInputFlac.Text, BlocksTypes.Block_Type_Picture, (int)ImagePosition);
}
}
private void BtnOpenPic_Click(object sender, EventArgs e)
{
ImageSize = 0;
GetImageFromFile();
}
private void BtnUpdateImage_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(txtInputFlac.Text))
{
MessageBox.Show("No hay arhivo de entrada", "Flac Image Writer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}else if (string.IsNullOrEmpty(TxtInputPic.Text))
{
MessageBox.Show("No hay imagen de entrada", "Flac Image Writer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string SaveFile;
bool HasImage = false;
List<UInt32> Direcciones = GetOffsets(txtInputFlac.Text);
int CurrentSize = (int)Direcciones[Direcciones.Count - 1];
int bufferSize = 1024 * 1024;
using (SaveFileDialog SavDiag = new SaveFileDialog())
{
SavDiag.Filter = "Archivos FLAC|*.flac";
SavDiag.AddExtension = true;
SavDiag.OverwritePrompt = true;
if (SavDiag.ShowDialog() == DialogResult.OK)
SaveFile = SavDiag.FileName;
else
return;
}
using (FileStream FSWrite = new FileStream(SaveFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
{
using (BinaryWriter BW = new(FSWrite))
{
BW.Write(Encoding.ASCII.GetBytes("fLaC"));
for (int x = 1; x < Direcciones.Count; x++)
{
byte[] FileBuff = GetBlockData(txtInputFlac.Text, (int)Direcciones[x]);
BlocksTypes CurrentBlockType = BlocksTypes.Block_Type_NoValid;
CurrentBlockType = NumberToBlockType(GetBlockType(FileBuff[0]));
if (CurrentBlockType == BlocksTypes.Block_Type_Picture)
HasImage = true;
if (x == Direcciones.Count - 1)
{
if (!HasImage)
{
CurrentSize += FileBuff.Length;
BW.Write(BuildHeader((uint)(FileBuff.Length - 4), CurrentBlockType, false));
}
else
{
CurrentSize += FileBuff.Length;
BW.Write(BuildHeader((uint)(FileBuff.Length - 4), CurrentBlockType, true));
}
}
else
{
BW.Write(BuildHeader((uint)(FileBuff.Length - 4), CurrentBlockType, false));
}
BW.Write(FileBuff, 4, FileBuff.Length - 4);
}
if (HasImage == false)
BW.Write(BuildPictureBlock(BuildAPICFrame(TxtInputPic.Text, CboPictureType.SelectedIndex, GetMIMEType(TxtInputPic.Text), ImageSize)));
FileStream fs = new FileStream(txtInputFlac.Text, FileMode.Open, FileAccess.ReadWrite);
fs.Position = CurrentSize;
int bytesRead = -1;
byte[] bytes = new byte[bufferSize];
while ((bytesRead = fs.Read(bytes, 0, bufferSize)) > 0)
{
BW.Write(bytes, 0, bytesRead);
}
fs.Close();
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
CboPictureType.SelectedIndex = 3;
}
private void PicFlac_DoubleClick(object sender, EventArgs e)
{
try
{
if (TieneImagen)
{
using (SaveFileDialog SavDiag = new SaveFileDialog())
{
SavDiag.Filter = "Arhivo Jpeg|*.jpg";
if (SavDiag.ShowDialog() == DialogResult.OK)
PicFlac.Image.Save(SavDiag.FileName);
}
//PicFlac.Image.Save
}
}
catch
{
MessageBox.Show("Error al guardar la imagen","Error",MessageBoxButtons.OK,MessageBoxIcon.Error);
}
}
private void button1_Click(object sender, EventArgs e)
{
}
}
}
Ahora veamos las funciones.
Primero empezamos con la enumeración "APICType", en ella se definen los posibles valores para el tipo de imagen que se va incrustarde acuerdo a lo establecido en la documentacion. Esto solo se hace para poder identificar las cosas de la forma mas humana posible, no es del todo necesario, pero ayuda bastante.
La primera funcion es "NormalizeToFourBytes()". Esta función recibe dos parámetros, el primero, es un valor del tipo UInt32 llamado "Value" y el otro es un valor del tipo bool llamado "Reverse". Esta función se encarga de convertir un numero en su equivalente en una secuencia de bytes, pero además tiene la opción de invertir el orden de la secuencia de bytes, dependiendo de la maquina en la que se trabaje o del idioma, los números se pueden representar de forma distinta, por ejemplo, si tenemos el número hexadecimal 0x3259 , puede representarse cómo 0x32 0x59 o 0x59 0x32. La función devuelve un arreglo de bytes con la representación del número.
La siguiente función es "GetMIMEType()". Esta función recibe un valor del tipo string con una cadena de texto. Se espera solo la extensión de las imágenes compatibles, es decir solo .jpg o .png. Regresa el Mime Type basado en la cadena de texto.
La siguiente funcion es "ByteArrayToImage()" Esta funcion recibe un arreglo de bytes, se espera que la secuencia de bytes en si conformen una imagen. Regresa un valor del tipo Image que contiene la imagen de la secuencia de bytes.
Y bien, por ahora es todo, las siguientes funciones merecen una explicacion mas detallada para poder entender mejor cada una. el código del formulario principal lo puedes copiar y pegar para probarlo, pero al final cuando todo este listo lo publicaré en mi dropbox para que lo puedas descargar y probar.
Los leo luego
Vamos a programar #105 - Agregando imagenes a archivos FLAC parte 1.
Hola de nuevo a todos. Tiempo ha pasado desde la última vez que un post se publicó. Cómo no es bueno dejar cosas pendiente, el día de hoy vamos a continuar con mas programación.
En alguno de los post anterior, vimos cómo leer los metadatos de los archivos FLAC, vimos cómo recuperar información importante: álbum, artista, título de la canción (solo por mencionar algunos), pero además, vimos cómo leer la imagen del archivo; eso si contiene una. El día de hoy, nos concentraremos en cómo está estructurado solo esta parte y servirá de preámbulo para hacer un programa en c# que escriba la imagen en el archivo.
Antes que nada, recordemos cómo esta estructurada la imagen. Hay que recordar que sigue los lineamientos de la etiqueta APIC de las etiquetas ID3v2. Desde este momento, toda la notación estará en bytes (al menos que se indique lo contrario).
- <4> El tipo de imagen que va a representar.
- 0 - Other
- 1 - 32x32 pixels 'file icon' (PNG only)
- 2 - Other file icon
- 3 - Cover (front)
- 4 - Cover (back)
- 5 - Leaflet page
- 6 - Media (e.g. label side of CD)
- 7 - Lead artist/lead performer/soloist
- 8 - Artist/performer
- 9 - Conductor
- 10 - Band/Orchestra
- 11 - Composer
- 12 - Lyricist/text writer
- 13 - Recording Location
- 14 - During recording
- 15 - During performance
- 16 - Movie/video screen capture
- 17 - A bright coloured fish
- 18 - Illustration
- 19 - Band/artist logotype
- 20 - Publisher/Studio logotype
- <4> El tamaño de el tipo de archivo (MIME Type).
- <N> La cadena de texto con el tipo de archivo (Solo usaremos "image/jpeg" o "image/png" ).
- <4> El tamaño de la descripción de la imagen.
- <N> La cadena de texto con la descripción de la imagen.
- <4> El ancho de la imagen en píxeles.
- <4> El alto de la imagen en píxeles.
- <4> La profundidad de colo de la imagen.
- <4> Para imágenes con indice de color, el número de colores a usar.
- <4> El tamaño de la imagen en bytes
- <N> Los datos de la imagen.
![]() |
| En esta imagen se puede apreciar cada apartado (si no se ve bien hay que darle zoom). |
Lo relevante de todo esto, y si miras bien la imagen, es los apartados cuatro, cinco, seis, siete; son totalmente innecesarios y se pueden dejar en cero o 0x00, 0x00, 0x00, 0x00; aun sin se establecen estas propiedades, en la documentacion oficial, no se aconseja hacer uso de ellas al momento de mostrar la imagen.
Habiendo descrito lo anterior, solo queda escribir código en c# para agregar la imagen.
Y bien, por ahora es todo. cómo de costumbre, hago la promesa de que trataré de publicar mas seguido. realmente agradezco a todos los que se toman la molestia de darse una vuelta por acá.
Los sigo leyendo.
Vamos a programar #105 - Enmascaramiento y secuencias de bits - Camino a ClockView 3.0
Hola de nuevo a todos, el día de hoy vamos a ver como hacer uso del enmascaramiento y cómo ahorrar un poco de memoria para algunos tipos de variables.
Desde hace tiempo actualicé ClockView, si no sabes de lo que hablo, puedes darte una vuelta por el post de la versión dos.
Desde el principio planee poner una serie de alarmas muy similares a las de un reloj casio que tengo, este tiene la particularidad de que se puede establecer por fecha: es decir, si queremos que suene el tres de julio, eso hará; por mes: es decir, si establecemos solo el mes, sonará todos los días de octubre (por solo decir una fecha); por año: la alarma sonará todos los días del año. Además se podía establecer para que la alarma sonara ciertos días de la semana, está función me es realmente útil, ya que normalmente solo algunos días de la semana requiero de la alarma (aunque ya no tanto, creo que llegué a la edad en que le gano al despertador).
Si recordamos la versión anterior de clock view, este, carecía de una función de alarma, pero el modulo de reloj en tiempo real DS1302, además de llevar la cuenta del tiempo, posee la capacidad de llevar los días de la semana. Para poder guardar los datos de los días de la semana en los cuales queremos que la alarma suene, vamos a usar un solo byte.
Para eso, podemos estructurar los días de la semana cómo se muestra en la siguiente imagen:
Oportunamente para nosotros, la semana consta de siete días, y cada byte consta de ocho bits, con lo que podemos asignar un valor cómo se muestra en la imagen anterior. Pero mas que su valor numérico, debemos de ver cómo es su representación en binario.
Pero ahora que hemos asignado un valor a cada día de la semana ¿cómo debemos de usarlos? Antes de entrar esos detalles, supongamos que queremos que la alarma suene los días: domingo, jueves y sábado, deberíamos de hacer algo cómo lo que sigue:
- Tomar el valor del primer día deseado.
- Tomar el valor del segundo día deseado
- Aplicar el operador "or" a ambos números y asignarlos a una variable.
- Si se desean agregar mas días, al valor anterior se le aplica de nuevo el operador "or" junto con el día a agregar
Si lo convertimos a código en c, quedaría algo cómo lo que sigue:
uint8_t BuildTheDaysOfAlarms(uint8_t TheDay1, uint8_t TheDay2)
{
return TheDay1|TheDay2;
}
Entonces los días que queremos usar en la alarma quedarían cómo:
- 00000001 or 00100000 or 01000000 = 01010001.
- que es igual en decimal a: 1 or 16 or 64 = 81.
- Lunes a viernes = 00111110 = 62.
- Sábado = 01000000 = 64
- 62 and 64 = 0.
- El día no está en el rango.
- Lunes, miércoles y viernes = 00101010 = 42.
- Miércoles = 00001000 = 8
- 42 and 8 = 8
- El día está en el rango.
Learning machine #18 - Programando en Pascal - Estructuras de datos (Record).
Hola de nuevo a todos, el día de hoy vamos a continuar con mas del aprendizaje del lenguaje de programación en pascal.
"En el capítulo anterior de <<Learning machine>>" vimos cómo es que se usan los arreglos de una o varias dimensiones. Hoy vamos a ver cómo es que se usan las estructuras de datos o "Record" (registro en español). Un registro básicamente es un conjunto de variables bajo un mismo nombre o tipo.
Para crear un registro, hay que crear un tipo usando la palabra "Type" seguido del nombre o identificador que le daremos al registro, hasta aquí todo es igual a cuando creamos los tipos, ahora la diferencia viene en que hay que asignarlo al tipo "Record", es decir tendríamos algo cómo: "Type NombreDelRegistro = Record" una vez creado, dentro de este nuevo tipo, agregaremos "campos"; cada campo sera una variable de cualquiera de los tipos soportados por Pascal. Una vez agregados todos los campos, finalizaremos la declaracion, con la palabra reservada "end;". Para apreciar un poco mejor cómo es todo el proceso, tomemos el siguiente ejemplo:
Type MiCancion = Record Cancion,Artista,Album : string; Year : Integer; BeatPerMinute : single end;
Con esto habremos creado un nuevo registro llamado "MiCancion"; este contendrá los campos: "Cancion, Artista, Album, Year y BeatPerMinute" del tipo "string, string, string, integer y single" respectivamente. Para poder usar el registro, crearemos variables del tipo que acabamos de crear.
Si queremos acceder a los campos, bastará con escribir el nombre de la variable seguido de "." y despues el nombre del campo. Para visualizar mejor cómo es que se hace, observemos el siguiente código:
KillerQueen.Cancion := 'Killer Queen.flac'; KillerQueen.Artista := 'Queen'; KillerQueen.Album := 'Sheer Heart Attack'; KillerQueen.Year := 1974; KillerQueen.BeatPerMinute := 117.27; writeln(KillerQueen.Cancion); writeln(KillerQueen.Artista); writeln(KillerQueen.Album); writeln(KillerQueen.Year); writeln(KillerQueen.BeatPerMinute);
Una forma de agilizar el acceso al registro, es hacer uso de la palabra reservada "with", para ello, escribiremos "with" seguido del nombre del registro al cual queremos acceder seguido de de la palabra reservada "do", luego "begin", para poder acceder a los miembros solo bastará con escribir el nombre de cada campo; y cómo cada sección, la finalizaremos con su respectivo "end;". Ahora revisemos el siguiente código que ejemplifica lo anterior.
with Epitaph do begin Cancion := 'Epitaph.flac'; Artista := 'King Crimson'; Album := 'In the court of the crimson king'; Year := 1969; BeatPerMinute := 100.37; end;
Los registros son utiles a la hora de trabajar con estructuras de datos ya que una vez que se crean y se asignan los valores correspondientes, todo el conjunto pasara a ser parte de la misma variable y si por ejemplo asignamos "KillerQueen := Epitaph" tendremos una copia exacta del primero.
Solo por si se ofrece, dejo el código de un programa completo que hace uso de los registros.
program Registros(output);
uses crt, sysutils;
Type MiCancion = Record
Cancion,Artista,Album : string;
Year : Integer;
BeatPerMinute : single
end;
var
KillerQueen, Epitaph : MiCancion;
begin
KillerQueen.Cancion := 'Killer Queen.flac';
KillerQueen.Artista := 'Queen';
KillerQueen.Album := 'Sheer Heart Attack';
KillerQueen.Year := 1974;
KillerQueen.BeatPerMinute := 117.27;
writeln(KillerQueen.Cancion);
writeln(KillerQueen.Artista);
writeln(KillerQueen.Album);
writeln(KillerQueen.Year);
writeln(KillerQueen.BeatPerMinute);
with Epitaph do
begin
Cancion := 'Epitaph.flac';
Artista := 'King Crimson';
Album := 'In the court of the crimson king';
Year := 1969;
BeatPerMinute := 100.37;
end;
writeln(Epitaph.Cancion);
writeln(Epitaph.Artista);
writeln(Epitaph.Album);
writeln(Epitaph.Year);
writeln(Epitaph.BeatPerMinute);
readln();
end.Y bien, por ahora es todo. El código lo puedes copiar para probarlo. Antes de terminar, hay que aclarar algo, si bien en español, se puede leer "Record" cómo registro, no hay que confundirlo con "Register" que es una cosa completamente diferente (que eventualmente veremos).
Los leo luego.
Vamos a programar #104 - Leyendo metadatos de archivos FLAC (Pt. 3 Ver. C#)
Hola de nuevo a todos, el día de hoy vamos a continuar con la última parte de FlacTagReader.
En el post anterior vimos cómo es que funcionan algunas funciones (valga la redundancia) y hoy veremos las faltantes.
La siguiente función es: "NumberToBlockType()". Esta función sirve para convertir un número en su equivalente al de la numeración "BlockTypes". Recibe un parámetro del tipo "uint" que es el número que queremos convertir. Regresa un valor del tipo enumeración "BlockTypes" equivalente. Hay que recordar que nos basamos en la de los tipos de bloques de la documentación.
La siguiente función es: "ReadVorbisData()". Cómo su nombre lo podría sugerir, esta función sirve para leer los datos de tipo vorbis ¿Qué es esto? te preguntaras, este bloque en particular es el que contiene los metadatos tales cómo: álbum, artista, año, etc. Siguen la estructura para los comentarios en archivos ogg vorbis.
La estructura de los comentarios es cómo la que sigue:
- Tamaño de la descripción del proveedor (32 bits).
- Cadena de texto para describir al proveedor.
- Tamaño de la lista de comentarios del usuario (32 bits).
- Tamaño de la lista de comentarios.
- Tamaño del elemento actual.
- Cadena de texto en formato UTF-8
- Marcador de final del bloque (si es verdadero o no hay se marca error o fin del bloque).
- Nombre del campo.
- = (símbolo de igual o símbolo ASCII 0x3D).
- Descripción del campo.
- Los nombres de cada campo no deben de contener símbolos que escapen del estándar ASCII de caracteres imprimibles, es decir: de 0x20 a 0x7D (excluyendo el símbolo de igual "=" o 0x3D) y de 0x41 a 0x5A.
- Se pueden usar los siguientes nombres de campo:
- TITLE
- VERSION
- ALBUM
- TRACKNUMBER
- ARTIST
- PERFORMER
- COPYRIGHT
- LICENSE
- ORGANIZATION
- DESCRIPTION
- GENRE
- DATE
- LOCATION
- CONTACT
- ISRC
- Aunque los campos anteriores son sugeridos, podemos usar los propios (sin abusar de ello) y por ejemplo, un campo llamado BEATPERMINUTE, es totalmente válido.
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#.
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; } } } } }
- <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.
Vamos a programar #102 - Leyendo metadatos de archivos FLAC (pt.1)
Hola de nuevo a todos. El día de hoy vamos a continuar con más de los archivos FLAC.
En los post mas recientes hemos visto las cualidades de los archivos FLAC, pero además cómo crear los nuestros. Al igual que en MP3, existen metadatos embebidos en ellos, y sirven para describir algunos datos importantes tales cómo: el nombre de la canción, el artista, el nombre del álbum, el genero, etc.
Estructura de un archivo FLAC
La estructura de los archivos FLAC es totalmente diferente a la de un MP3 por lo que vale la pena revisarla por separado. Cada archivo FLAC consiste de cuatro partes principales:
- Una cadena de cuatro bytes con la secuencia "fLaC" (así tal cual se ve).
- Un bloque de metadatos con la información del "stream"
- Cero o mas bloques de metadatos.
- Uno o mas "frames" de audio.
Para mas detalles se puede revisar la página de xiph.org, donde esta la descripción completa para los archivos FLAC.
De las cuatro partes que se mencionan anteriormente, nos concentraremos en la segunda y tercera (Técnicamente las dos entran en el mismo conjunto, solo que la segunda es obligatoria, mientras que las subsecuentes son opcionales, hablando solamente de los bloques de metadatos), por lo que revisaremos cómo es que está compuesta cada una.
Pero antes una representación de cómo está estructurado el archivo:
Metadatos
- STREAM
- METADATA_BLOCK
- METADATA_BLOCK_HEADER (Encabezado)
- <1> Indicador de último bloque
- <7> Descriptor del tipo de bloque
- <24> Tamaño del bloque en bytes (BYTES!!!)
- 0: STREAMINFO
- Contiene información acerca de todo el archivo, este bloque es mandatorio y siempre será e primero de la secuencia después de la secuencia 0x66, 0x4c, 0x61, 0x43 (o de manera simple después de "fLaC" ).
- 1: PADDING
- Este tipo de bloque sirve cómo reserva y el contenido no representará nada, normalmente se llena con 0x00. Cuando se crea el archivo sin los metadatos completos, en lugar des escribir un archivo nuevo con los bloques nuevos, simplemente se reemplaza este bloque conservando el archivo original y agilizando la escritura del mismo.
- 2: APPLICATION
- Nombre de la aplicación que se uso para codificar el archivo, puede ser cualquiera que el usuario elija, aunque se recomienda el registro de la aplicación para obtener un ID único para cada quien.
- 3: SEEKTABLE
- Sirve cómo marcador para desplazarse en el archivo a la hora de la reproducción, aunque se puede omitir, se recomienda incluirlo para agilizar la lectura.
- 4: VORBIS_COMMENT
- Este bloque contiene la información relevante a los creadores del contenido multimedia y la descripción del mismo, se usa la especificación de "Vorbis comment"
- 5: CUESHEET
- Contiene información que se puede usar en un CD
- 6: PICTURE
- Imagen representativa del archivo, similar a la imagen embebida en un archivo MP3.
- 7-126: Reservado
- Reservado por ahora, por lo que se podría decir que si un bloque tiene descriptor en este rango, no es valido. Se planea su uso futuro.
- 127: No válido
- Bloque no valido, ya que la misma secuencia se usa cómo marcador para sincronización.
Bloque "STREAMINFO"
- <16> Tamaño mínimo del bloque.
- <16> Tamaño máximo del bloque.
- <24> Tamaño mínimo del "FRAME" en bytes.
- <24> Tamaño máximo del "FRAME" en bytes.
- <20> Frecuencia de sampleo.
- <3> Número de canales.
- <36> Total de muestras por segundo.
- <128> MD5 del audio.


























