MediaTime
MediaTime es una utilidad para control de tiempos multimedia, tanto de vídeo como de audio.
En vídeo, si hablamos de televisión, el tiempo se representa como h:mm:ss.ff:
- h: horas
- mm: minutos
- ss: segundos
- ff: frames
Es muy importante entender que el número de frames por segundo no altera la duración de un vídeo. Como ejemplo se puede decir que un vídeo de dos segundos de duración, si tiene veinticinco frames por segundo tiene cincuenta frames, si tiene treinta frames por segundo tiene sesenta frames, pero en ambos casos tiene una duración de dos segundos, la duración no cambia.
También es muy importante entender que un vídeo codificado a veinticinco frames por segundo, con una duración de un segundo y sesenta milésimas, tiene veintiséis frames que se reproducen durante cuarenta milésimas y un último frame que se reproduce durante veinte milésimas. El vídeo no tiene veintiséis frames y medio, sino veintisiete frames. El último frame se reproduce durante tan solo veinte milésimas, la mitad de tiempo que los otros veintiséis frames.
En el caso de esta utilidad, «ff» representa fracciones por segundo, y su valor se cambia a través de la propiedad FractionsPerSeconds. Al trabajar con vídeo en televisión, las fracciones por segundo más comunes son veinticinco o treinta, mientras que si se trabaja con audio la fracción más común es cien, o lo que es lo mismo centésimas.
Implementando MediaTime
La implementación que voy explicar es la de C#, aunque al final del artículo podéis descargar el código fuente en C#, Visual Basic .Net y C++ (es la primera vez que hago algo en C++). El espacio de nombres que voy a usar es Espamatica.Multimedia.Core, aunque sois libres de cambiarlo.
Lo primero que implemento es un enumerado para indicar como se van a redondear las fracciones para representarlas como texto: esto es muy importante a la hora de representar una parrilla de televisión.
namespace Espamatica.Multimedia.Core
{
/// <summary>
/// Enumera los modos en lo que se pueden redondear las fracciones a la hora de su representación en texto.
/// </summary>
public enum FractionRoundMode
{
/// <summary>
/// La fracción de trunca.
/// </summary>
Truncate,
/// <summary>
/// Si la fracción tiene decimales, se trunca y se le suma 1.
/// </summary>
Real,
/// <summary>
/// La fracción se redondea.
/// </summary>
Round
} // FractionRoundMode
} // Espamatica.Multimedia.Core
He implementado tres tipos de redondeo de fracción. Si tenemos un vídeo a veinticinco frames por segundo, esto implica que cada frame se va a reproducir durante cuarenta milésimas. Según esto, si el vídeo tiene una duración de cincuenta milésimas, podríamos decir que el vídeo tiene un frame y cuarto (1.25), aunque en realidad tiene dos: uno se reproduce durante cuarenta milésimas y otro que se reproduce durante diez milésimas.
Teniendo en cuenta estos datos, el comportamiento según el valor seleccionado del enumerado sería:
Valor (milisegundos) | FractionPerSeconds | Fracciones | FractionRoundMode | Resultado (fracciones) |
---|---|---|---|---|
50 | 25 | 1.25 | Truncate | 1 |
50 | 25 | 1.25 | Real | 2 |
50 | 25 | 1.25 | Round | 1 |
60 | 25 | 1.50 | Truncate | 1 |
60 | 25 | 1.50 | Real | 2 |
60 | 25 | 1.50 | Round | 2 |
La implementación de MediaTime la realizo como una estructura, al modo de TimeSpam o DateTime.
namespace Espamatica.Multimedia.Core
{
using System;
/// <summary>
/// Estructura para manejo de tiempos multimedia.
/// </summary>
public struct MediaTime
{
} // MediaTime
} // Espamatica.Multimedia.Core
Lo primero que implemento son las constantes que voy a usar para definir el número de unidades inferiores que hay en una unidad superior (las defino como long para evitar posteriores transformaciones).
/// <summary>
/// Número de ticks por milisegundo.
/// </summary>
public const long TicksMillisecond = 10000;
/// <summary>
/// Número de milisegundos por segundo.
/// </summary>
public const long MillisecondsSecond = 1000;
/// <summary>
/// Número de segundos por minuto.
/// </summary>
public const long SecondsMinute = 60;
/// <summary>
/// Número de minutos por hora.
/// </summary>
public const long MinutesHour = 60;
/// <summary>
/// Número de horas por día.
/// </summary>
public const long HoursDay = 24;
/// <summary>
/// Número de ticks por segundo.
/// </summary>
public const long TicksSecond = TicksMillisecond * MillisecondsSecond;
/// <summary>
/// Número de ticks por minuto.
/// </summary>
public const long TicksMinute = TicksSecond * SecondsMinute;
/// <summary>
/// Número de ticks por hora.
/// </summary>
public const long TicksHour = TicksMinute * MinutesHour;
/// <summary>
/// Número de ticks por día.
/// </summary>
public const long TicksDay = TicksHour * HoursDay;
Una vez implementadas estas constantes, implemento los campos privados que uso para las propiedades.
/// <summary>
/// Campo de control para la propiedad Days.
/// </summary>
private int days;
/// <summary>
/// Campo de control para la propiedad DigitPerFraction.
/// </summary>
private int digitPerFraction;
/// <summary>
/// Campo de control para la propiedad Fractions.
/// </summary>
private double fractions;
/// <summary>
/// Campo de control para la propiedad FractionsPerSecond.
/// </summary>
private int fractionsPerSecond;
/// <summary>
/// Campo de control para la propiedad Hours.
/// </summary>
private int hours;
/// <summary>
/// Campo de control para la propiedad Milliseconds.
/// </summary>
private int milliseconds;
/// <summary>
/// Campo de control para la propiedad MillisecondsPerFraction.
/// </summary>
private double millisecondsPerFraction;
/// <summary>
/// Campo de control para la propiedad Minutes.
/// </summary>
private int minutes;
/// <summary>
/// Campo de control para la propiedad RoundMode.
/// </summary>
private FractionRoundMode roundMode;
/// <summary>
/// Campo de control para la propiedad Seconds.
/// </summary>
private int seconds;
/// <summary>
/// Campo de control para la propiedad Ticks.
/// </summary>
private int ticks;
/// <summary>
/// Campo de control para la propiedad Time.
/// </summary>
private long time;
Lo siguiente es implementar la propiedad donde indicaremos el número de fracciones por segundo con el que trabajaremos.
/// <summary>
/// Obtiene el número de milisegundos por fracción.
/// </summary>
public double MillisecondsPerFraction
{
get
{
return this.millisecondsPerFraction;
}
private set
{
if (value != this.millisecondsPerFraction)
{
this.millisecondsPerFraction = value;
}
}
} // MillisecondsPerFraction
Existen varias propiedades que al cambiar de valor refrescan el tiempo (time) y el tiempo multimedia (days, minutes, seconds, fractions, milliseconds y ticks). Lo siguiente que implemento son dos métodos que se encargan de realizar estas operaciones.
/// <summary>
/// Actualiza los valores del tiempo mutimedia.
/// </summary>
private void RefreshMediaTime()
{
long remainder = this.Time;
this.days = (int)((remainder - (remainder % TicksDay)) / TicksDay);
remainder -= this.days * TicksDay;
this.hours = (int)((remainder - (remainder % TicksHour)) / TicksHour);
remainder -= this.hours * TicksHour;
this.minutes = (int)((remainder - (remainder % TicksMinute)) / TicksMinute);
remainder -= this.minutes * TicksMinute;
this.seconds = (int)((remainder - (remainder % TicksSecond)) / TicksSecond);
remainder -= this.seconds * TicksSecond;
this.fractions = ((double)remainder / (double)TicksMillisecond) / this.MillisecondsPerFraction;
this.milliseconds = (int)((remainder - (remainder % TicksMillisecond)) /TicksMillisecond);
remainder -= this.milliseconds * TicksMillisecond;
this.ticks = (int)remainder;
} // RefreshMediaTime
/// <summary>
/// Actualiza el tiempo.
/// </summary>
private void RefreshTime()
{
this.time = (this.Days * TicksDay) + (this.Hours * TicksHour) + (this.Minutes * TicksMinute)
+ (this.Seconds *TicksSecond) + (this.Milliseconds * TicksMillisecond) + this.Ticks;
} // RefreshTime
En RefreshMediaTime lo primero que hago es cargar los ticks totales que representan el tiempo (Time) en la variable remainder. Lo siguiente es calcular el número de días enteros que representan esos ticks, y se los resto a remainder para quedarme con los ticks que quedan. Calculo las horas, minutos, segundos y milisegundos de la misma manera. Antes de calcular los milisegundos, calculo las fracciones que representan estos milisegundos (recordad que las fracciones pueden cambiar pero los milisegundos no). Por último, una vez realizados todos los cálculos, ya solo quedan los ticks sobrantes.
De esta manera he calculado que un número determinado de ticks representan N días, N horas, N minutos, N segundos, N milisegundos y N ticks, como si de un TimeSpan se tratara, pero además he calculado que los N milisegundos representan N fracciones, que pueden cambiar en base a las fracciones por segundo que se hayan especificado.
En RefreshTime hago la operación inversa, tomo todos los componentes del tiempo y calculo los ticks totales (observad que no tengo en cuenta las fracciones).
El siguiente paso es implementar las propiedades que representan los distintos componentes de tiempo en nuestra estructura.
/// <summary>
/// Obtiene o establece los días.
/// </summary>
public int Days
{
get
{
return this.days;
}
set
{
if (value != this.days)
{
this.days = value;
this.RefreshTime();
this.RefreshMediaTime();
}
}
} // Days
/// <summary>
/// Obtiene o establece las fracciones.
/// </summary>
public double Fractions
{
get
{
return this.fractions;
}
set
{
if (value != this.fractions)
{
this.Milliseconds = (int)Math.Truncate(value * this.MillisecondsPerFraction);
}
}
} // Fractions
/// <summary>
/// Obtiene o establece las horas.
/// </summary>
public int Hours
{
get
{
return this.hours;
}
set
{
if (value != this.hours)
{
this.hours = value;
this.RefreshTime();
this.RefreshMediaTime();
}
}
} // Hours
/// <summary>
/// Obtiene o establece los milisegundos.
/// </summary>
public int Milliseconds
{
get
{
return this.milliseconds;
}
set
{
if (value != this.milliseconds)
{
this.milliseconds = value;
this.RefreshTime();
this.RefreshMediaTime();
}
}
} // Milliseconds
/// <summary>
/// Obtiene o establece los minutos.
/// </summary>
public int Minutes
{
get
{
return this.minutes;
}
set
{
if (value != this.minutes)
{
this.minutes = value;
this.RefreshTime();
this.RefreshMediaTime();
}
}
} // Minutes
/// <summary>
/// Obtiene o establece los segundos.
/// </summary>
public int Seconds
{
get
{
return this.seconds;
}
set
{
if (value != this.seconds)
{
this.seconds = value;
this.RefreshTime();
this.RefreshMediaTime();
}
}
} // Seconds
/// <summary>
/// Obtiene o establece los ticks.
/// </summary>
public int Ticks
{
get
{
return this.ticks;
}
set
{
if (value != this.ticks)
{
this.ticks = value;
this.RefreshTime();
this.RefreshMediaTime();
}
}
} // Ticks
/// <summary>
/// Obtiene o establece el tiempo en ticks.
/// </summary>
public long Time
{
get
{
return this.time;
}
set
{
if (value != this.time)
{
this.time = value;
this.RefreshMediaTime();
}
}
} // Time
Podéis observar como la mayoría de estas propiedades llaman a RefreshMediaTime y RefreshTime cuando cambian su valor.
El siguiente paso es implementar la propiedades que faltan: dígitos necesarios para representar las fracciones, el número de fracciones por segundo y el modo de redondeo a la hora de representar las fracciones.
/// <summary>
/// Obtiene el número de dígitos usados para representar las fracciones.
/// </summary>
public int DigitPerFraction
{
get
{
return this.digitPerFraction;
}
private set
{
this.digitPerFraction = value;
}
} // DigitPerFraction
/// <summary>
/// Obtiene o establece el número de fracciones por segundo.
/// </summary>
public int FractionsPerSeconds
{
get
{
return this.fractionsPerSecond;
}
set
{
if (value != this.fractionsPerSecond)
{
if (value < 1 || value > (int)MillisecondsSecond)
{
value = 1000;
}
this.fractionsPerSecond = value;
this.RefreshFractionPerSeconds();
this.RefreshMediaTime();
}
}
} // FractionsPerSeconds
/// <summary>
/// Obtiene o establece el modo en el que se redondean las fracciones a la hora de representarlas en texto.
/// </summary>
public FractionRoundMode RoundMode
{
get
{
return this.roundMode;
}
set
{
this.roundMode = value;
}
} // RoundMode
Ahora mismo tenemos un error en el código ya que al asignar el valor a FractionsPerSeconds se llama a un método que todavía no está implementado, y ese es el siguiente paso.
/// <summary>
/// Actualiza los datos relativos a las fracciones por segundo.
/// </summary>
private void RefreshFractionPerSeconds()
{
if (this.FractionsPerSeconds > 0 && this.FractionsPerSeconds < (int)MillisecondsSecond)
{
this.MillisecondsPerFraction = Math.Round(1.0 / (double)this.FractionsPerSeconds * (double)MillisecondsSecond, 4);
this.DigitPerFraction = this.FractionsPerSeconds.ToString().Length;
if (Math.Pow(10.0, (double)this.DigitPerFraction - 1.0) == (double)this.FractionsPerSeconds)
{
this.DigitPerFraction--;
}
}
} // RefreshFractionPerSeconds
En RefreshFractionPerSeconds calculo el número de milisegundos que dura cada fracción, y el número de dígitos necesarios para representar las fracciones. El ajuste que hago en el número de dígitos necesarios para representar las fracciones (Math.Pow …) es debido a que las fracciones las represento empezando desde 0 (0 a 9, 0 a 99, 0 a 999, etcétera), por lo que si las fracciones por segundo son potencia de 10, hay que restar un dígito.
Cuando estamos trabajando con tiempo, es muy habitual que queramos sumar y restar tiempo, así como saber si es mayor, menor, igual, etcétera, motivo por el cual implemento la sobrecarga de diversos operadores.
/// <summary>
/// Compara dos objetos para determinar si el primer objeto especificado es menor que el segundo objeto especificado.
/// </summary>
/// <param name="obj1">Primer objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Si el primer objeto especificado es menos que el segundo objeto especificado true, en caso contrario false.</returns>
public static bool operator <(MediaTime obj1, MediaTime obj2)
{
return obj1.Time < obj2.Time;
} // <
/// <summary>
/// Compara dos objetos para determinar si el primer objeto especificado es menor o igual que el segundo objeto especificado.
/// </summary>
/// <param name="obj1">Primer objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Si el primer objeto especificado es menor o igual que el segundo objeto especificado true, en caso contrario false.</returns>
public static bool operator <=(MediaTime obj1, MediaTime obj2)
{
return obj1.Time <= obj2.Time;
} // <=
/// <summary>
/// Campara dos objetos para determinar si el primer objeto especificado no es igual al segundo objeto especificado.
/// </summary>
/// <param name="obj1">Primer objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Si los dos objetos son distintos true, en caso contrario false.</returns>
public static bool operator !=(MediaTime obj1, MediaTime obj2)
{
return !obj1.Equals(obj2);
} // !=
/// <summary>
/// Resta dos objetos especificados.
/// </summary>
/// <param name="obj1">Primero objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Objeto resultante de la resta.</returns>
public static MediaTime operator -(MediaTime obj1, MediaTime obj2)
{
MediaTime mt = new MediaTime(obj1.Time, obj1.FractionsPerSeconds);
mt.Time -= obj2.Time;
return mt;
} // -
/// <summary>
/// Suma dos objetos especificados.
/// </summary>
/// <param name="obj1">Primero objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Objeto resultante de la suma.</returns>
public static MediaTime operator +(MediaTime obj1, MediaTime obj2)
{
MediaTime mt = new MediaTime(obj1.Time, obj1.FractionsPerSeconds);
mt.Time += obj2.Time;
return mt;
} // +
/// <summary>
/// Campara dos objetos para determinar si el primer objeto especificado es igual al segundo objeto especificado.
/// </summary>
/// <param name="obj1">Primer objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Si los dos objetos son iguales true, en caso contrario false.</returns>
public static bool operator ==(MediaTime obj1, MediaTime obj2)
{
return obj1.Equals(obj2);
} // ==
/// <summary>
/// Compara dos objetos para determinar si el primer objeto especificado es mayor que el segundo objeto especificado.
/// </summary>
/// <param name="obj1">Primer objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Si el primer objeto especificado es mayor que el segundo objeto especificado true, en caso contrario false.</returns>
public static bool operator >(MediaTime obj1, MediaTime obj2)
{
return obj1.Time > obj2.Time;
} // >
/// <summary>
/// Compara dos objetos para determinar si el primer objeto especificado es mayor o igual que el segundo objeto especificado.
/// </summary>
/// <param name="obj1">Primer objeto especificado.</param>
/// <param name="obj2">Segundo objeto especificado.</param>
/// <returns>Si el primer objeto especificado es mayor o igual que el segundo objeto especificado true, en caso contrario false.</returns>
public static bool operator >=(MediaTime obj1, MediaTime obj2)
{
return obj1.Time >= obj2.Time;
} // >=
Lo último que implemento es una serie de métodos que permiten sumar o restar unidades de tiempo, determinar si dos objetos son iguales, obtener la cantidad total de tiempo representada por uno de sus componentes u obtener la representación en texto del tiempo.
Empiezo por los métodos que permiten sumar o restar unidades de tiempo.
/// <summary>
/// Añade el número de días especificado al tiempo actual.
/// </summary>
/// <param name="days">Número de días especificado.</param>
public void AddDays(int days)
{
this.Time += (long)days * TicksDay;
} // AddDays
/// <summary>
/// Añade el número de horas especificado al tiempo actual.
/// </summary>
/// <param name="hours">Número de horas especificado.</param>
public void AddHours(int hours)
{
this.Time += (long)hours * TicksHour;
} // AddHours
/// <summary>
/// Añade el número de fracciones especificado al tiempo actual.
/// </summary>
/// <param name="fractions">Número de fracciones especificado.</param>
public void AddFractions(int fractions)
{
this.Time += (long)((double)fractions * this.MillisecondsPerFraction * (double)TicksMillisecond);
} // AddFractions
/// <summary>
/// Añade el número de milisegundos especificado al tiempo actual.
/// </summary>
/// <param name="milliseconds">Número de milisegundos especificado.</param>
public void AddMilliseconds(int milliseconds)
{
this.Time += (long)milliseconds * TicksMillisecond;
} // AddMilliseconds
/// <summary>
/// Añade el número de minutos especificado al tiempo actual.
/// </summary>
/// <param name="minutes">Número de minutos especificado.</param>
public void AddMinutes(int minutes)
{
this.Time += (long)minutes * TicksMinute;
} // AddMinutes
/// <summary>
/// Añade el número de segundos especificado al tiempo actual.
/// </summary>
/// <param name="seconds">Número de segundos especificado.</param>
public void AddSeconds(int seconds)
{
this.Time += (long)seconds * TicksSecond;
} // AddSeconds
/// <summary>
/// Añade el número de ticks especificado al tiempo actual.
/// </summary>
/// <param name="ticks">Número de ticks especificado.</param>
public void AddTicks(int ticks)
{
this.Time += (long)ticks;
} // AddTicks
Aunque todos los métodos son Add, para restar solo hay que pasarle valores negativos. Por otro lado, al contrario de lo que pasa con los operadores que permiten sumar o restar objetos MediaTime y devuelven el resultado en un nuevo objeto, los métodos Add alteran el valor Time de la instancia con la que se está trabajando.
El siguiente par de métodos que implemento evalúa si dos instancias de MediaTime son consideradas iguales, el primero, y obtiene el código hash de la instancia, el segundo.
/// <summary>
/// Compara el objeto especificado con la instacia actual para determinar si se consideran iguales.
/// </summary>
/// <param name="obj">Objeto especificado.</param>
/// <returns>Si los dos objetos son considerados iguales true, en caso contrario false.</returns>
public override bool Equals(object obj)
{
return obj is MediaTime && ((MediaTime)obj).Time == this.Time;
} // Equals
/// <summary>
/// Obtiene el código hash de la instancia actual.
/// </summary>
/// <returns>Código hash de la instancia actual.</returns>
public override int GetHashCode()
{
return this.Time.GetHashCode();
} // GetHashCode
Y vamos llegando al final. Implemento una serie de métodos Get que obtienen las unidades totales de los distintos componentes de tiempo que representa el valor de MediaTime.
/// <summary>
/// Obtiene el número total de días.
/// </summary>
/// <returns>Número total de días.</returns>
public double GetTotalDays()
{
return (double)this.Time / (double)TicksDay;
} // GetTotalDays
/// <summary>
/// Obtiene el número total de fracciones.
/// </summary>
/// <returns>Número total de fracciones.</returns>
public double GetTotalFractions()
{
return (double)this.Time / (double)TicksMillisecond / this.MillisecondsPerFraction;
} // GetTotalFractions
/// <summary>
/// Obtiene el número total de horas.
/// </summary>
/// <returns>Número total de horas.</returns>
public double GetTotalHours()
{
return (double)this.Time / (double)TicksHour;
} // GetTotalHours
/// <summary>
/// Obtiene el número total de milisegundos.
/// </summary>
/// <returns>Número total de milisegundos.</returns>
public double GetTotalMilliseconds()
{
return (double)this.Time / (double)TicksMillisecond;
} // GetTotalMilliseconds
/// <summary>
/// Obtiene el número total de minutos.
/// </summary>
/// <returns>Número total de minutos.</returns>
public double GetTotalMinutes()
{
return (double)this.Time / (double)TicksMinute;
} // GetTotalMinutes
/// <summary>
/// Obtiene el número total de segundos.
/// </summary>
/// <returns>Número total de segundos.</returns>
public double GetTotalSeconds()
{
return (double)this.Time / (double)TicksSecond;
} // GetTotalSeconds
Y por último, pero no menos importante, implemento el método con el que obtengo la representación en cadena de texto del valor de MediaTime.
/// <summary>
/// Obtiene la representación en cadena del tiempo actual.
/// </summary>
/// <returns>Representación en cadena del tiempo actual.</returns>
public override string ToString()
{
int fractionsRounded = 0;
switch (this.RoundMode)
{
case FractionRoundMode.Real:
fractionsRounded = this.fractions % 1 > 0.0
? (int)Math.Truncate(this.fractions) + 1
: (int)this.fractions;
break;
case FractionRoundMode.Round:
fractionsRounded = (int)Math.Round(this.fractions, 0, MidpointRounding.AwayFromZero);
break;
default:
fractionsRounded = (int)Math.Truncate(this.fractions);
break;
}
string fractions = fractionsRounded.ToString().PadLeft(this.DigitPerFraction, '0');
return string.Format("{0:d2}:{1:d2}:{2:d2}:{3:d2}.{4}",
new object[]
{
this.Days,
this.Hours,
this.Minutes,
this.Seconds,
fractions });
} // ToString
Ya solo queda ver si todo funciona, lo cual voy a comprobar con una aplicación de consola.
namespace Espamatica.Multimedia.Core
{
using System;
/// <summary>
/// Programa de ejemplo.
/// </summary>
public class Sample
{
/// <summary>
/// Punto de inicio del programa.
/// </summary>
public static void Main()
{
MediaTime time = new MediaTime((new TimeSpan(0, 27, 34, 45, 60)).Ticks, 25);
Console.WriteLine(time.ToString());
time.AddFractions(1);
Console.WriteLine(time.ToString());
time.FractionsPerSeconds = 30;
Console.WriteLine(time.ToString());
time.AddFractions(1);
Console.WriteLine(time.ToString());
time.RoundMode = FractionRoundMode.Round;
Console.WriteLine(time.ToString());
time.RoundMode = FractionRoundMode.Real;
Console.WriteLine(time.ToString());
Console.Read();
}
}
} // Espamatica.Multimedia.Core
El resultado de la ejecución es el siguiente.
Aquí puedes descargar el código fuente en C#, Visual Basic .Net y C++.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.