Un pequeño receso.

Hola a todos, solo escribo el día de hoy para avisar que el blog se toma un pequeño tiempo fuera, reanudaremos el día jueves 6 de octubre del 2016 con un post doble (o triple), les agradezco a todos por su paciencia.
Además aprovecho para recordarles que el reto de Xwork aun sigue abierto, hay algunos que me han mandado las respuesta al primer (y por ahora único) desafío de la pagina, pero hasta ahora ninguno a acertado en el. Si tu logras descifrarlo, te ganarás un lugar en el salón de la fama de Xwork.
Actualmente tengo una carga de trabajo brutal, pero si me desocupo antes, publicaré el numero 16 de la sección: "Vamos a programar".

Por ahora es todo, gracias y los leo luego.

Vamos a programar #15 - Extraer una imagen embebida en un MP3

Hace algunos días estaba hablando con un amigo y me comento un problema que le pasó. Hace algunos días quiso organizar su colección de música en formato MP3. Todas las canciones poseen los meta-datos o "TAGS" que sirven para identificar a una canción. La información básica que suele incluirse es:

  1. Nombre de la canción.
  2. Álbum.
  3. Artista.
  4. Año.
  5. Genero.
Adicionalmente los archivos MP3's suelen tener una imagen que representa la portada del disco (algunos incluso incluyen varias imágenes). El problema que me comento mi amigo fue que al convertir todas las canciones para que la información esté en la misma versión en todas las canciones, la imagen se perdía. cuando se hacia una conversion de la version ID3 2.2 a la versión 2.3.

Etiquetas ID3.

Antes que nada veamos que es una etiqueta ID3.
ID3 es un estándar de facto para incluir meta-datos (etiquetas) en un archivo contenedor audiovisual, tales como álbum, título o artista. Se utiliza principalmente en ficheros sonoros como MP3. Wikipedia/ID3.
Básicamente es eso. Para identificar cada una de las partes de la información se usan etiquetas ya predefinidas, es decir: si se quiere saber el autor se deberá de buscar una etiqueta en especifico.
La lista completa de etiquetas se puede encontrar en ID3/V2 (para la version 2.2.0) y en ID3V2.3(para la version 2.3).
Por ahora solo vamos a revisar la etiquetas que nos interesan:

Para la version 2.3
Header for 'Attached picture', ID: "APIC"
MIME type <text string> $00
Picture type $xx
Description <text string according to encoding> $00 (00)
Picture data <binary data>
Para la version 2.2
Attached picture "PIC"
Frame size $xx xx xx
Text encoding $xx
Image format $xx xx xx
Picture type $xx
Description <textstring> $00 (00)
Picture data <binary data>
Lo que haremos es buscar "PIC" o "APIC" para determinar si hay imagen o no, si la hay, después leeremos la longitud, luego el tipo (usualmente solo se usa PNG o JPG) y finalmente leeremos los datos correspondientes a la imagen en sí.

Vista previa.

Antes que nada, primero vamos a hacer una pequeña prueba "en crudo" y trataremos de seguir las instrucciones a mano, es decir solo usando un lector hexadecimal, extraeremos la imagen. Para eso necesitarás un editor hexadecimal, hay muchos disponibles actualmente, pero yo uso XVI32, puedes usar el de tu preferencia, pero el programa que escojas deberá de tener las funciones de lectura y escritura.
También recomiendo que leas las definiciones para las etiquetas antes de continuar.

Primero abriremos el archivo del cual queremos obtener la imagen. Una vez abierto procederemos a buscar directamente los datos que nos interesan. Para nuestro caso solo requerimos saber si hay una etiqueta "PIC" o "APIC". Entonces, con nuestro archivo ya abierto buscaremos eso.

Al terminar la búsqueda, si existe, el cursor se moverá a la posición en donde se encuentra el inicio del lo que buscamos.
Si hacemos memoria recordaremos que después de la etiqueta "APIC", los siguientes 4 bytes indican el tamaño total de la etiqueta y dos bytes más son usados cómo flags (no nos fijaremos en esto), entonces los datos que nos interesan son lo que estan en el recuadro rojo de la siguiente imagen:
Para este caso, la longitud de esta etiqueta es 0x00027225, hay que recordar que esta longitud no incluye los 10 bytes que se usan para definir la etiqueta, el tamaño y los flag. Si seleccionamos los 0x00027225 bytes, estaríamos seleccionando los datos que describen la imagen, el tipo de imagen (icono, cubierta frontal, etc) y formato de imagen (jpg o png).
Para poder guardar la imagen, seleccionaremos tantos bytes como nos indica, pero le restaremos 14 bytes que corresponden a los frames que describen para el tag APIC.
Si convertimos el numero 0x00027225 a decimal,obtenemos el numero decimal 160,293 restandole 14 obtenemos 160,279.
En nuestro editor primero pondremos el cursor al final de todos los descriptores de la etiqueta. y si tiene disponible la opcion de seleccionar una cantidad "n" de caracteres, usaremos esa indicando la cantidad de bytes que obtuvimos al hacer la resta.
Finalmente solo guardaremos la parte seleccionada (si el editor lo soporta).


 Hay algo importante por remarcar, según la descripción en ID3.org es preferible usar solo dos tipos de imágenes, puede ser Jpeg o PNG, dependiendo de cual se use, el descriptor que se usa es diferente. En este ejemplo al ser una imagen jpg la que está incrustada en el mp3, al ver la descripción tenemos: "image/jpeg" pero si la imagen esta en png tendremos algo como: image/png. Eso causaría que si solo restamos a la longitud 14 bytes, la imagen no la obtendríamos completa, por eso es necesario revisar el tipo de imagen, ademas de que hay información adicional que en este caso no la hay.


Y bien es todo por hoy, en el siguiente post continuaremos ahora si con el código de C# para extraer la imágenes dentro de los mp3. Este post solo trata de exponer como es que funciona y de hecho ya hiciste a mano lo que C# haría internamente.
Si solo quieres seguir los pasos tal  como están en el post. La canción que use como prueba está aquí.

Los leo luego.


Vamos a programar #14 - Un poco de MatLab

Hola de nuevo a todos, el día de hoy vamos a hacer una pausa a VEncoder y al código de C# para ver un poco de otro lenguaje.
MATLAB (abreviatura de MATrix LABoratory, "laboratorio de matrices") es una herramienta de software matemático que ofrece un entorno de desarrollo integrado (IDE) con un lenguaje de programación propio (lenguaje M). Wikipedia/MatLab


Recientemente una persona me pidió ayuda para hacer algunos scripts que le pedían en la universidad, a pesar de que nunca antes lo había manejado, resulto relativamente fácil empezar a hacer los scripts básicos con los que generalmente se empieza en cualquier lenguaje de programación (llámese "hola mundo").
Lo bueno del scripting de MatLab es que la sintaxis es muy similar a C y VB, de hecho desde mi punto de vista es una mezcla de ambos, entonces eso me facilito un poco comprender como hacer lo sencillo. Cosas especificas de MatLab requieren un poco más de tiempo. MatLab es una herramienta realmente potente y sin ningún esfuerzo se puede empezar con lo básico, pero eso si, para dominarlo tendrás que dedicarlo un buen rato.

Ahora vayamos a lo que quería mostrarles. Hace algunos días publiqué esto:

Si miras la imagen (ignorando la parte de pokemon), notarás que es un script que sirve para calcular los años, meses y días que han transcurrido desde una fecha dada hasta la fecha actual que MatLab obtenga del sistema en el que se ejecuta.
Cuando me lo plantearon, sabia que este tipo de problemas representan un problema para aquellos que apenas inician en el mundo de la programación, este tipo de problemas puede ser un poco "truculento" y cuando le pedí a muchos que los resolvieran, siempre cometían el mismo error.

Resolviendo el problema.

El código en MatLab para calcular las diferencia entre dos fechas es el siguiente:

%%Código por Xwork
%Calcula la edad
clear all
clc
BornDay = input('Que dia naciste?\n');
BornMonth = input('Que numero de mes naciste?\n');
BornYear = input('De que año?\n');
if mod(BornYear,4) == 0
days = [31 29 31 30 31 30 31 31 30 31 30 31];
else
days = [31 28 31 30 31 30 31 31 30 31 30 31];
end
%AA MM DD
ActualDate = fix(clock);

%Calcular años
TotalYear = ActualDate(1) - BornYear;
%Calcular los meses
TotalMonth = ActualDate(2) - BornMonth;
%calcular los dias
TotalDays = ActualDate(3) - BornDay;
if TotalDays < 0
    TotalMonth = TotalMonth - 1;
    if TotalMonth < 0
        TotalYear = TotalYear - 1;
        TotalMonth = TotalMonth + 12;
    end
    TotalDays = days(BornMonth) + TotalDays;
end
if TotalMonth < 0
   TotalYear = TotalYear - 1;
   TotalMonth = TotalMonth + 12;
end
fprintf('Tienes %g Años(s), ',TotalYear)
fprintf('%g Mes(es), ',TotalMonth)
fprintf('%g Dia(s)',TotalDays)

El código es realmente simple, primero le pedimos al usuario que introduzca la información requerida en formato numérico; es decir: se usara el numero de mes en lugar del nombre; ahora supongamos una fecha importante 28/12/1993; se introducirá de la siguiente manera:

  • Que dia naciste?
  • 28
  • Que numero de mes naciste?
  • 12
  • De que año?
  • 1993
Con los datos introducidos, el script avanzará y primeramente encontrará un if que sirve para determinar si un año es bisiesto o no, esto se logra dividiendo el año de nacimiento entre 4 (solo por si acaso, recuerda que los años bisiestos ocurren cada 4 años y se le agrega un día mas a febrero), esto solo lo hacemos para agregar un poco mas de precisión. Después se crea una matriz que contiene la cantidad de días para cada mes; enero tiene 31 días, febrero tiene 28 días normalmente, pero en año bisiesto 29, incluso se podía crear la matriz afuera del if y ya dentro solo editar los días para febrero, pero preferí dejarlo así para obviarlo un poco.
Luego obtenemos la fecha actual con la función clock, pero ademas usamos la función fix para que los valores dentro de la matriz no estén en decimales.
Si llamamos a la función clock esta devolverá la siguiente matriz

   1.0e+03 *
    2.0160    0.0090    0.0050    0.0140    0.0060    0.0513

La primera linea nos indica que todos los valores dentro de la matriz se deben de multiplicar por 1.0e+03, luego los valores de la matriz en orden de izquierda a derecha representan:

  • Año.
  • Mes.
  • Día.
  • Hora
  • Minuto
  • Segundo
Al usar la función fix en la matriz obtenemos los siguiente:

ans =
2016           9           5          14           6          51

Luego vienen los "cálculos". en primer lugar restamos directamente el año de nacimiento (o de la fecha que se quiera) y esto nos daría la diferencia en años que hay entre las dos fechas. Antes de proseguir, debo de mencionar que asi como esta, es propenso a errores, suponiendo que es el año 2016 y alguien pone que nació en el año 2020 obviamente no es posible, pero el codigo asi como esta ahorita devolvería -4, que técnicamente está bien, pero para términos de edad; "menos algo" no es válido.
Para los meses hacemos exactamente lo mismo que con los años, hacemos la resta directamente entre la fecha actual y la fecha de nacimiento (o de lo que sea), en este caso si es posible que nos resulte un numero negativo, supongamos que el mes actual es el mes 10 y el mes de nacimiento es el 12; al hacer nuevamente el calculo obtenemos un valor de -2, por ahora conservaremos ese valor asi como esta.
Luego hacemos el calculo de los días,para eso repetimos lo anterior y al igual que los meses, podemos esperar un resultado negativo, luego procedemos a ajustar los días.
Primero comprobamos si la cantidad de días son menores que 0; si es así restamos a la cantidad de meses que teníamos almacenados y dentro de esa misma comprobación si al momento de restarle a la cantidad de meses, está resulta en un numero negativo, procederemos a restarle 1 a la cantidad de años. Finalmente obtenemos los días agregando los días que contiene el mes al calculo de días que teníamos almacenado.
Luego volvemos a hacer otra comprobación para la cantidad de meses, ya que en el if anterior primero se comprueban los días pero no necesariamente va a entrar a esa condición y como tenemos almacenados los meses directamente como los dio el resultado del calculo previo. Para evitar números negativos le agregamos 12 (cantidad de meses que tiene el año).
Y ya solo resta mostrar el resultado al usuario.

Una explicación un tanto mas humana.

De nuevo tomemos una fecha, la misma que para la explicación anterior: 28/12/1993 y el dia actual es 5/9/2016

  1. 2016-1993=23
  2. 9-12=-3
  3. 5-28=-23
  4. Como el resultado de la resta de los días es negativo, restamos 1 a los meses y nos queda -4. Cómo el resultado es negativo; a los años le restamos 1 y queda 22, luego sumamos 12 a la cantidad de meses que tenemos almacenados y resulta 8. Hacemos la suma de los días que contiene el mes de nacimiento y el numero que tenemos almacenado para los días; diciembre tiene 31 días +(-23)=8
  5. El resultado: 22 años, 8 meses y 8 días.
Hay que entender como usamos los datos, para el caso de los días veamos lo siguiente. para nuestro caso, el dia es 28 de diciembre, este mes tiene 31 días, para "cerrar" al mes, faltan 3 días + 5 que han transcurrido del mes, esto nos da 8, lo mismo para lo meses.

Y bueno por ahora es todo. Dudas o bugs que quieras reportar, no dudes en hacerlo.

Los leo luego.

Vamos a programar #13 - El código de VEncoder 2

Vamos a programar #13 - El código de VEncoder 2
El día de hoy vamos a continuar con la revisión del código de VEncoder,

Funcion DoWork.

El Código de la función DoWork es el siguiente:

/// <summary>
         /// Inicia la conversion de los archivos
         /// </summary>
         /// <param name="Params">Una lista del tipo string que contiene las lineas de comandos a usarse</param>
         /// <param name="Durations">Una lista del tipo int que contiene las duraciones de los archivos</param>
         
         private void DoWork(List<string>  Params, List<int> Durations)
         {
             Thread DoConversion = new Thread(new ThreadStart(() => {
                                                                  int CD = 0;
                                                                  foreach (string Param in Params)
                                                                  {
                                                                      if (RequestForCancel == true)
                                                                      {
                                                                          break;
                                                                      }
                                                                      
                                                                      int TotalPercentConvert = CD * 100;
                                                                      int CurrentDuration = Durations[CD];
                                                                      Process Proc = new Process();
                                                                      StreamReader FFResult;
                                                                      string CurrentResult;
                                                                      ProcessStartInfo FFProcess = new ProcessStartInfo("ffmpeg.exe");
                                                                      FFProcess.Arguments = Param;
                                                                      FFProcess.UseShellExecute = false;
                                                                      FFProcess.WindowStyle = ProcessWindowStyle.Hidden;
                                                                      FFProcess.RedirectStandardError = true;
                                                                      FFProcess.RedirectStandardOutput = true;
                                                                      FFProcess.CreateNoWindow = true;
                                                                      Proc.StartInfo = FFProcess;
                                                                      Proc.Start();
                                                                      PBProgress.BeginInvoke(new Action(() => PBProgress.Maximum = CurrentDuration));
                                                                      PBGlobalProgress.BeginInvoke(new Action(() => PBGlobalProgress.Maximum = Durations.Count * 100));
                                                                      FFResult = Proc.StandardError;
                                                                      do
                                                                      {
                                                                          if (RequestForCancel == true)
                                                                          {
                                                                              Proc.Kill();
                                                                              break;
                                                                          }
                                                                          
                                                                          CurrentResult = FFResult.ReadLine();
                                                                          Debug.Print(CurrentResult);
                                                                          this.BeginInvoke(new Action(() => {
                                                                                                          this.lblProgressInfo.Text = TimeSpan.FromSeconds(GetPositionFromFFline(CurrentResult)).ToString();
                                                                                                          this.PBProgress.Value = (int)GetPositionFromFFline(CurrentResult);
                                                                                                          this.PBGlobalProgress.Value = TotalPercentConvert + (int)Porcentaje(GetPositionFromFFline(CurrentResult),CurrentDuration);
                                                                                                          TaskbarProgress.SetValue(this.Handle, this.PBGlobalProgress.Value, this.PBGlobalProgress.Maximum);
                                                                                                      }));
                                                                      }while (Proc.HasExited == false & string.Compare(CurrentResult, "") == 1);
                                                                      CD = CD + 1;
                                                                      ElapsedTimeCurrent = 0;
                                                                  }
                                                                  IsConvertingSomething = false;
                                                                  RequestForCancel = false;
                                                                  ElapsedTimeCurrent = 0;
                                                                  ElapsedTimeGlobal = 0;
                                                                  this.BeginInvoke(new Action(() => {
                                                                                                  this.lblProgressInfo.Text = "Esperando";
                                                                                                  this.PBProgress.Value = 0;
                                                                                                  this.PBGlobalProgress.Value = 0;
                                                                                                  TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.NoProgress);
                                                                                              }));
}));

DoConversion.Start();

}



La función DoWork inicia la ejecución de FFMpeg indicándole todos los parámetros para realizar la conversión de todos los archivos que se contenían en la lista, recibe dos parámetros: el primero, una lista del tipo string; que contiene todas las lineas de comandos generadas previamente con la función BuildCommandLine junto con la funcion PrepareWork.
El segundo parámetro es una lista del tipo int, que contiene la duración de los archivos en segundos.
Esta función se ejecuta dentro de la funcion PrepareWork  si se pasa el parámetro booleano HardWork cómo true.
Para la ejecución de FFMpeg se usa un Thread y se implementa de forma básica, dentro de la ejecución de este hilo, creamos un proceso que sera el encargado de abrir FFMpeg, ademas de que le diremos que todo la salida la redireccione a nuestro programa para poder extraer los datos que necesitamos.
Debido a que la conversión se hace en lotes, se usa un ciclo for para recorrer todos los elementos contenidos en la lista "Params" y ya dentro de ese mismo ciclo, se usa un bucle while que estará activo mientras nuestro proceso (ffmpeg) no haya finalizado o mientras el proceso no devuelva una cadena vacía.
Para cancelar enteramente el proceso usamos dos comprobaciones, una de ellas en el ciclo for y otra en el bucle while, esto para poder detener en cualquier parte. Si la interrupción se da cuando se esta ejecutando el bucle while, al salir de este continuara en el ciclo for y nuevamente iniciaría el bucle while si aun quedan elementos en la lista Params, entonces para evitar esto, lo único que hacemos es crear las interrupciones en ambos lugares.
Todas las instrucciones "invoke" se usan para interactuar con la interfaz principal, siempre le resulta al usuario útil saber cuanto es el progreso que se lleva.
En esta función se usa de forma muy básica todas las características de la clase Thread, en realidad es muy potente, pero para fines prácticos solo usamos lo mas importante: iniciar y detener Threads, analizando todos los elementos contenidos en esta clase, podemos encontrar que se puede tener un control más preciso de cada Thread: su estado, si responde o no, etc.
Lo mismo sucede con la clase Procces, pero por ahora solo usamos los métodos que están implementados en la función.

Funcion SetInfo.

El codigo de la funcion SetInfo es el siguiente:


         /// <summary>
         /// Obtiene la informacion de los archivos de entrada
         /// </summary>
         /// <param name="Files">Archivos de los que se obtendra la informacion.</param>
         
         private void SetInfo(List<string> Files)
         {
             Thread GetInfo = new Thread(new ThreadStart(() => {
                                                             foreach (string Archivo in Files)
                                                             {
                                                                 ListViewItem InfoItem = new ListViewItem(Archivo);
                                                                 MediaFile mFile = new MediaFile(Archivo);
                                                                 InfoItem.SubItems.AddRange(new string[] { mFile.Video[0].FrameSize.ToString(),mFile.Video[0].FrameRate.ToString(),
                                                                                                mFile.Video[0].DurationString,mFile.Video[0].Bitrate.ToString(),mFile.Audio.Count.ToString(),
                                                                                                mFile.Audio[0].SamplingRate.ToString(),mFile.Audio[0].Bitrate.ToString(),mFile.Audio[0].Channels.ToString()});
                                                                 lvfilesin.BeginInvoke(new Action(() => lvfilesin.Items.Add(InfoItem)));
                                                             }
                                                             this.BeginInvoke(new Action(() => TaskbarProgress.SetState(this.Handle,TaskbarProgress.TaskbarStates.NoProgress)));
                                                         }));
             GetInfo.Start();
         }

La función SetInfo se encarga de obtener la información de los archivos. La información incluye: Tamaño, resolución, cuadros por segundo, aspecto, velocidad de bits, etc.
Recibe como parámetro una lista del tipo string que contiene las rutas de los archivos de los cuales se quiere obtener la información.
Al igual que en la función anterior, se usa un Thread para hacer este proceso, esto debido a que cuando se procesan algunos archivos, dependiendo de sus características, puede tomar un poco de tiempo para obtener la información, y esto causaría que la interfaz principal se bloquee, para evitar esto, se inicia un thread diferente y se usa el TaskBar de windows para mostrar el progreso. Hay resaltar que debido a que se usan características disponibles a partir de Windows vista, no es posible hacer compatible la aplicación con versiones anteriores a vista.
Finalmente esta función agrega toda la información a lvfilesin, que es un ListView de la interfaz principal.

Y bien, esas son todas las funciones que componen a VEncoder, como verás, no es tan complejo y el resultado es muy útil, en el siguiente Post vamos a implementar una nueva función que apagará el equipo cuando todas la conversiones se hayan realizado.

Por ahora es todo, nuevamente les agradezco por el interés en el código de VEncoder y por el apoyo recibido en el post anterior.

Los leo luego.