Determinar automáticamente el desfase de audio en un fichero MKV (III)

Entradas anteriores sobre este tema:

Hoy me he encontrado con un caso nuevo: tasa de audio irregular:

$ ffmpeg -i FUENTE.mkv -af ashowinfo -map 0:2 -y -f alaw -frames:a 5 /dev/null
[Parsed_ashowinfo_0 @ 48dde0] n:0 pts:0 pts_time:0 pos:22428 fmt:s16p channels:2 chlayout:stereo rate:48000 nb_samples:1152 checksum:A45225DC plane_checksums: [ B6F411EF 7CE013ED ]
[Parsed_ashowinfo_0 @ 48dde0] n:1 pts:110592 pts_time:2.304 pos:94534 fmt:s16p channels:2 chlayout:stereo rate:48000 nb_samples:1152 checksum:7497E13F plane_checksums: [ 1D98739E 8C006DA1 ]
[Parsed_ashowinfo_0 @ 48dde0] n:2 pts:111744 pts_time:2.328 pos:94918 fmt:s16p channels:2 chlayout:stereo rate:48000 nb_samples:1152 checksum:214BB177 plane_checksums: [ 63345BBA 03B155BD ]
[Parsed_ashowinfo_0 @ 48dde0] n:3 pts:112896 pts_time:2.352 pos:96214 fmt:s16p channels:2 chlayout:stereo rate:48000 nb_samples:1152 checksum:9DE0FD2F plane_checksums: [ D6BB799A 1CFC8395 ]
[Parsed_ashowinfo_0 @ 48dde0] n:4 pts:114048 pts_time:2.376 pos:96598 fmt:s16p channels:2 chlayout:stereo rate:48000 nb_samples:1152 checksum:0499F32E plane_checksums: [ D707739B 7D847F93 ]
size=      11kB time=00:00:02.40 bitrate=  38.4kbits/s

Aquí vemos que el primer frame de audio está bien, pero el segundo se reproduce a los 2.304 segundos. Hay más de dos segundos de silencio sin audio en el archivo. Vemos también que hay un frame de audio cada 24 milisegundos. El ajuste de audio que hay que aplicar es, por tanto, de 2.280 milisegundos:

$ mkvmerge -o DESTINO.mkv -y 1:2280 -y 2:2280 FUENTE.mkv

No deja de sorprenderme la cantidad de cosas extrañas que uno se encuentra por ahí...

Addendum: Cómo acceder al "code object" de una clase Python

En Cómo acceder al "code object" de una clase Python explico cómo acceder al code object de una clase Python y comprobar así que estamos deserializando datos con la versión correcta del código.

Dejando al margen la conveniencia o no de recurrir a estos chanchullos, hay un problema grave para mi caso de uso: el code object de una clase Python cambia no solo cuando cambia el código fuente de la clase, sino también cuando cambia su entorno.

Veamos un ejemplo:

 #!/usr/bin/env python3

 import dis, marshal, hashlib

 def wrapper():
     class prueba:
         pass

 code = wrapper.__code__
 dis.dis(code)
 code_dump = marshal.dumps(code)
 print()
 print(hashlib.sha256(code_dump).hexdigest())

Si ejecutamos el código varias veces, siempre obtendremos el mismo resultado:

6             0 LOAD_BUILD_CLASS
              2 LOAD_CONST               1 (<code object prueba at 0x7f7dcba98930, file "./z.py", line 6>)
              4 LOAD_CONST               2 ('prueba')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               2 ('prueba')
             10 CALL_FUNCTION            2
             12 STORE_FAST               0 (prueba)
             14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

d2945184e581612f4f30458a3e70afa92382356e83422c9c8f95bc28fd83fd8c

Aquí vemos el contenido de la función y el hash del code object.

Observemos qué ocurre si hacemos un cambio mínimo en el programa. Añadamos simplemente una línea en blanco antes de la función que nos interesa:

 #!/usr/bin/env python3


 import dis, marshal, hashlib

 def wrapper():
     class prueba:
         pass

 code = wrapper.__code__
 dis.dis(code)
 code_dump = marshal.dumps(code)
 print()
 print(hashlib.sha256(code_dump).hexdigest())

Hemos añadido una línea en blanco antes de importar los módulos. Ejecutar este código produce lo siguiente:

7             0 LOAD_BUILD_CLASS
              2 LOAD_CONST               1 (<code object prueba at 0x7fe54e688930, file "./z.py", line 7>)
              4 LOAD_CONST               2 ('prueba')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               2 ('prueba')
             10 CALL_FUNCTION            2
             12 STORE_FAST               0 (prueba)
             14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

e0298eb04b1d22a786bbac588204830ecec87310294c1308d3dbde820fa90a03

La función es idéntica, pero su hash ha cambiado.

El problema fundamental es que un dato incluído en el code object es el número de línea donde empieza la función:

>>> import z
>>> z.wrapper.__code__
>>> dir(z.wrapper.__code__)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> z.wrapper.__code__.co_firstlineno
6

Por tanto, el code object puede variar no solo porque hemos modificado el código fuente relevante, sino también porque hemos cambiado la posición relativa del código en el fichero.

Dada esta situación y mi caso de uso, he movido los objetos relevantes a su propio fichero de código fuente. De esta forma evito que cambios periféricos alteren el hash de lo que me interesa.

Determinar automáticamente el desfase de audio en un fichero MKV (II)

En Determinar automáticamente el desfase de audio en un fichero MKV explico cómo determinar automáticamente el desajuste de audio en un archivo multimedia cuando el audio está desfasado. ¿Qué ocurre cuando lo que está desfasado es el vídeo?

En Sincronización de sonido cuando la tasa de refresco de vídeo es irregular explico cómo extraer las marcas temporales de cada fotograma del archivo. Si el desajuste es constante a lo largo de todo el fichero multimedia, lo que nos interesa es conocer el desajuste de los primeros fotogramas. Para ello podemos hacer algo como:

$ ffmpeg -i FUENTE.mkv -vf "showinfo" -map 0:0 -y -f mjpeg -frames:v 10 /dev/null

Leer más…

Que tus "futures" de larga duración no impidan que tu programa Python termine

En Python: Olvídate de los hilos y usa "futures" defiendo el uso de Futures en vez de hilos tradicionales por su mayor facilidad de uso, pero vemos que no existe una forma simple de marcar un Future como Daemon. Es decir, que el programa no terminará su ejecución hasta que todos los Futures se completen.

Esto no está mal. De hecho nos asegura que los recursos utilizados por los Futures se liberen de forma ordenada. Puede ser necesario borrar ficheros temporales o desconectarse de la base de datos de forma adecuada, por ejemplo. [1]

Con el modelo de actores que empleo habitualmente, los Futures no terminan nunca. Se pueden quedar ahí esperando eternamente a que alguien les pida algo. Por supuesto es posible definir un mensaje global que solicite un suicidio colectivo, pero ello implica la gestión de un mensaje adicional y el registro de todos los actores en algún sitio. Ninguna de las dos cosas es realmente un problema, pero en muchos casos no necesitamos estas complejidades.

Un ejemplo común es el caso de Futures de larga duración esperando trabajo mientras el hilo principal supervisa que no falle ninguno. Si alguno muere inesperadamente, el hilo principal lo detecta, notifica la excepción y el programa termina.

Existen dos posibilidades básicas, a priori:

Leer más…

Python: Olvídate de los hilos y usa "futures"

Soy un usuario masivo de hilos a la hora de programar en Python. Mi gestión de la complejidad y las condiciones de carrera se basa en ser cuidadoso y, cuando el problema lo permite, usar colas u otros mecanismos de sincronización y comunicación. Podría decirse que uso un modelo de actores en Python.

Con cuidado y experiencia me las apaño bastante bien. Es raro que tenga problemas.

Python 3.2 introdujo el concepto de Futures, un tipo de paralelismo que abstrae el uso de hilos o multiprocesamiento. En mi caso personal lo que más me interesa es la facilidad para seguir la pista a los diferentes hilos que lanzo, más concretamente para detectar de forma sencilla cuándo terminan correctamente o cuándo mueren debido a una excepción.

Antes de los futures, mis hilos estaban contenidos en algo similar a:

Leer más…

Cómo acceder al "code object" de una clase Python

Una de las características más interesantes de Python es su capacidad de introspección. Recientemente necesité acceder al code object de una clase Python y lo cierto es que no es tarea trivial.

Contexto: Creo una clase caché con persistencia transparente. Normalmente incluiría un número de versión en la persistencia y un mecanismo de migración para que pueda reutilizar los datos persistentes si modifico el código que interactúa con ella. Eso requiere estar atento y actualizar la versión y el proceso de migración cuando hay cambios relevantes en el código. Esto es manual. En este caso concreto no me interesa tener un mecanismo de migración de versiones con su mantenimiento manual. Dado que esto es una caché, si hay un cambio de código lo que quiero es que se detecte y se ignore la persistencia antigua.

He encontrado tres formas de acceder al code object de una clase Python:

Leer más…

Por qué es mala idea (a primera vista) desplegar IPv6 con 'Unique Local Address'

En Cómo prestar servicio IPv6 a través de OpenVPN explico cómo configurar OpenVPN para dar servicio IPv6. Los entendidos del tema se habrán sorprendido por el uso de NAT cuando estoy utilizando direcciones IPv6

La práctica habitual sería o bien no usar NAT en absoluto por ser innecesario o bien usar NAT para poder emplear internamente direcciones Unique Local Address.

En una primera versión de todo esto empleé direcciones Unique Local Address, concretamente fd00::/8. Esto me permitía entregar a cada uno de mis usuarios OpenVPN un rango ::/48. Estupendo.

Una vez montado, sin embargo, llega el jarro de agua fría:

Leer más…

Cómo prestar servicio IPv6 a través de OpenVPN

Cansado de que mi proveedor de Internet no me ofrezca IPv6 en mi red doméstica de forma nativa, decidí tomar cartas en el asunto.

Hace una década que navego con mi portátil y mi móvil a través de OpenVPN. Así no tengo que preocuparme de direcciones IP dinámicas o que la WIFI que esté utilizando sea una red hostil.

Las versiones recientes de OpenVPN soportan IPv6.

Veamos los cambios que realizo en mi configuración actual:

Leer más…

Parche de autenticación SMTP para Mailman 2

Increíblemente Mailman 2 no ha incluído autenticación SMTP hasta la versión 2.1.23 publicada el 27 de agosto de 2016. Y eso a pesar o, tal vez, a causa de que el parche es trivial y de que todo el mundo tenía su propia versión. Por ejemplo:

Yo también he tenido mi versión propia durante ocho años:

--- SMTPDirect.py.OLD   2017-11-24 00:38:36.601046958 +0100
+++ SMTPDirect.py       2017-11-24 00:38:07.104575066 +0100
@@ -62,6 +62,14 @@
     def __connect(self):
         self.__conn = smtplib.SMTP()
         self.__conn.connect(mm_cfg.SMTPHOST, mm_cfg.SMTPPORT)
+
+        # MI CODIGO - jcea@jcea.es - 20091016
+        if mm_cfg.SMTPTLS :
+            self.__conn.starttls()
+        if mm_cfg.SMTPUSER :
+            self.__conn.login(mm_cfg.SMTPUSER, mm_cfg.SMTPPASSWORD)
+        #
+
         self.__numsessions = mm_cfg.SMTP_MAX_SESSIONS_PER_CONNECTION

     def sendmail(self, envsender, recips, msgtext):

Me alegro de que ya no sea necesario.

Actualización 20171205: Tras ser un poco pesado (no mucho), he conseguido que pkgsrc actualice a la versión 2.1.25 de Mailman. Estará disponible en la distribución 2017Q4.

Uso de "nullmailer" en zonas nativas SmartOS

Las zonas nativas SmartOS incorporan un servidor de correo Postfix que hay que configurar correctamente para procesar los mensajes que puedan generarse de forma local como resultado de ejecuciones fallidas del demonio CRON o servicios similares. Enfrentado a este problema, lo que necesito exclusivamente es un sistema de "mail forwarding" que envíe todo el correo generado de forma local a mi servidor de correo normal, que reside en otra máquina diferente. No quiero inteligencia de ningún tipo, tan solo un simple "mail forwarding" ciego.

Se puede configurar Postfix para que haga "mail forwarding", de hecho es una configuración frecuente, pero me parece matar moscas a cañonazos. Preguntando sobre el particular en el canal IRC de SmartOS, muy amigable, se me sugirió un paquete disponible directamente en Pkgsrc: nullmailer.

Nullmailer hace exacta y exclusivamente lo que necesito: enviar a un servidor de correo remoto todo el correo que se genere de forma local.

No obstante, hay un problema: Nullmailer contiene un bug que hace que los mensajes generados por CRON, precisamente mi caso de uso, se reciban sin cabeceras; las cabeceras acaban como cuerpo del mensaje. Es decir, el mensaje que te llega es algo de este tipo:

From root Mon Nov 14 23:42 UTC 2016
To: root
Subject: Cron <root@5ca32ab7-57b7-4af4-8740-69fcd29cef86> echo hi
Auto-Submitted: auto-generated
X-Mailer: cron (SunOS 5.11)
X-Cron-User: root
X-Cron-Host: 5ca32ab7-57b7-4af4-8740-69fcd29cef86
X-Cron-Job-Name: echo hi
X-Cron-Job-Type: cron
MIME-Version: 1.0
Content-Type: text/plain
Content-Length: 3

hi

El problema es que a nullmailer no le gustan los mensajes cuya primera línea empieza por "From " o ">From ", aunque son legales.

La solución evidente sería parchear nullmailer, pero no quiero modificar paquetes binarios de pkgsrc. En vez de ello lo que he hecho es un wrapper en Python que filtra el mensaje entrante y simplemente se salta la primera línea si empieza por "From " o ">From ".

Leer más…