Administración del Audio con LiquidSoap

Premisas para el Script:

En primer lugar se tienen los programas en vivo. Lo ideal es que estos se transmitan permanentemente. En lo que no exista un programa en vivo se transmitirá música o programas en diferido, pero bajo los siguientes conceptos:

Si un programa en vivo deja de transmitir porque se ha desconectado, se espera a ver si hay reconexiones, con un silencio de 10 segundos. En caso de que se cumplan estos 10 segundos y no regrese el flujo de datos en vivo, se procede a tomar audio de una fuente. Actualmente de Podcasts en diferido.

En el script propuesto se incluye un canal para transmitir contínuamente podcasts diferidos de los locutores.

Script Inicial

Como estaba al principio, el LS administra el punto de montaje /vivo.ogg y lo retransmite en /radiognu.ogg

De no existir /vivo.ogg (nadie está transmitiendo), envía al punto de montaje /radiognu.ogg, audio de una lista de podcasts. Los usuarios no son ya notificados de los cambios en el track, al igual que cuando regresa la señal, pero la metadata está vacía. En esta configuración la metadata está fija con un solo texto y los clientes no se caen nunca por envío de metadata. En esas caídas suenan cambios bruscos de una fuenta a otra.

#!/usr/bin/liquidsoap

# registro
set("log.file.path","/home/radiognu/liquidsoap/liquidsoap.log")
set("log.level",3)

# fuentes de sonido
podcasts =playlist("/home/radiognu/liquidsoap/musica.txt")
vivo =input.http("http://radiognu.org:8000/vivo.ogg")

radio=fallback(track_sensitive=false, [vivo, podcasts])

# Salida al ICECAST
output.icecast.vorbis(mount="radiognu.ogg",
                      samplerate=32000,
                      stereo=false,
                      password="--------",
                      port=8000,
                      name="RadioGNU",
                      description="La emisora del GNU que te da nota",
                      genre="Live Libre :-)",
                      url="http://radiognu.org:8000/radiognu.ogg",
                      quality=3.0,
                      mksafe(clear_metadata(radio)))

Investigación

Algunos reproductores no reciben correctamente la metadata y dejan de reproducir la radio. El primer paso era eliminar la metadata:

mksafe(clear_metadata(radio)))

Eso mantenía a los clientes conectados, pero siempre colocaba “Unknown” como título.

rewrite_metadata([("artist","RadioGNU"),("title","El ñu que te da nota")],clear_metadata(radio)))

Hacía que cada vez que se recibía metadata, ésta se reescribía, pero se seguía enviando la misma metadata al cliente. Permanece el problema inicial.

Cuando fueron eliminados (con clear_metadata), aparecía “unknown” en los navegadores. Cómo hacer que aparezca una metadata fija.

Soluciones Propuesta por Hackers de LiquidSoap

(traducidos del inglés)

toots@rastageeks.org

El problema viende del hecho de que LiquidSoap tiene una noción de track que es mantenido incluso si se remueve la metadata. Entonces, rewrite_metadata agrega nueva data por cada track (parametro: inser_missing). Lo que hace falta es remover todos los marcadores de track antes de reescribir la metadata. El siguiente código puede lograr eso:

s = add([blank(),s])
s = rewrite_metadata([("title","test")], s)

La primera línea agrega una fuente en blanco a “s”.

El comportamiento de add es relevar (relay) tracks y metadata de la primera fuente solamente, por lo cual la fuente resultate tendrá será un track vacío y sin metadata.

Entonces la segunda línea agrega la metadata encima.

Se debe poner ese código justo antes del output. En muchos casos los marcadores de track son útiles, por ejemplo cuando se usa el fallback con track_sensitive, así que mejor quitarlos en el último momento.

Además, esto hace que la fuente sea infalible (pero dará silencio si la fuente real no está disponible), así que cuando se usa esto se pueden remover los mksafe()

david.baelde@ens-lyon.org

Nos dice lo siguiente respecto a esa forma de quitar metadata:

This is probably not the best way to merge tracks (remove track
limits). As you point out it makes the source infallible, playing
silence when there's no track. Also, it divides the volume by two (but
setting normalize=false will fix that) and removes metadata (but
changing the order of s and blank() will fix that.

Lo cual hace resultar nuestro código de esa línea así:

radio =add(normalize=false,[blank(),radio])

Solución Salomónica: Dos emisoras

Dos emisoras: una CON metadata y una SIN metadata. Se sustituye el script para que quede así:

# la radio es un conjunto de relevos de puntos de montaje
radio = transicion(vivo,podcasts)
# la que tiene metadata es simplemente, la radio infalible (mksafe)
radio_metadata = mksafe(radio);
# la que no lleva metadata necesita ser limpiada
radio =add(normalize=false,[blank(),radio])
radio = rewrite_metadata([("title","Lee los títulos de las canciones en irc.radiognu.org")],radio)

y se transmiten dos radios.

output.icecast.vorbis(mount="tal.ogg", stereo=true, ........ ,radio_metadata)
output.icecast.vorbis(mount="pacual.ogg", stereo=true, ........ ,radio)

def delay_fallback(a,b)
  def immediate(a,b)
    b
  end
  def delayed(a,b)
    sequence([blank(duration=10.),b])
  end
  fallback(track_sensitive=false,[a,b],transitions=[immediate,delayed])
end

Con esto, si una fuente en vivo (b) existe se le da entrada de inmediato al streaming.

Si el locutor pierde conexión, habrá una espera de 10 segundos (en este ejemplo) y se le dará paso a la programación en diferido. Si en esos 10 segundos regresa una conexión, se transmitirá inmediatamente, dejando sólo un pequeño silencio de bache.

Si bien añadiendo las sugerencias anteriores la radio funcionó mejor, se hacía necesario tener más de una fuente de audio en vivo, de manera de poder tener más de una transmisión en vivo y poder alternar entre ellas, dando paso a una u otra, permitiendo hacer relevos entre más de un locutor a la vez. Esto será ideal para transmisiones de eventos simultáneos como el FLISOL, SFD y demás ocasiones similares.

Al preguntar nos respondieron con algo tan simple como:

locutores = fallback(track_sensitive=false, [input.http("http://foo/vivo.ogg"), input.http("http://bar/vivo2.ogg")])

en nuestro caso agregamos entonces:

vivo      = fallback(track_sensitive=false, transitions=[immediate,delayed], [locutores,podcasts] )

El resultado esperado es:

  1. Primera prioridad: suena “vivo.ogg” y cada vez que aparezca el punto de montaje sobreescribirá a los demás
  2. Segunda prioridad: suena “vivo2.ogg” y si aparece “vivo.ogg” será relevado.
  3. Tercera prioridad: suena el playlist y será relevado por “vivo.ogg” o “vivo2.ogg” apenas aparezcan. Se espera 10 segundos hasta que suene de nuevo

Script Usado

Por lo anteriormente mencionado y de acuerdo con el acuerdo notariado de las partes afectadas, se declara el siguiente Script:

#!/usr/bin/liquidsoap

# Realizado para RadioGNU

# ---- CONFIGURACIONES
set("log.file.path","/home/radiognu/liquidsoap/liquidsoap.log")
set("log.level",3)

# Lista de programas en diferido, se cargan al azar y cada "ronda"
podcasts=playlist("/home/radiognu/liquidsoap/podcasts.txt")

# Audio proveniente de los (presuntos) locutores, es decir, transmisiones en vivo
vivo =input.http("http://radiognu.org:8000/vivo.ogg")
vivo2 =input.http("http://radiognu.org:8000/vivo2.ogg")

#vivo = input.http("http://ctmcorp.no-ip.org/live.ogg")
#identif=single("estas_escuchando.ogg")

# Transicion. Con esto pasan inadvertidas las interrupciones instantáneas de los programas en vivo.
def transicion(vivo,podcasts)
  def immediate(vivo,podcasts)
    podcasts
  end
  def delayed(vivo,podcasts)
    sequence([blank(duration=10.),podcasts])
  end
  fallback(track_sensitive=false,[vivo,vivo2,podcasts],transitions=[immediate,immediate,delayed])
end

# definimos la radio
radio=transicion(vivo,podcasts)

# una radio con metadata se transmitirá en "/radiognu_metadata.ogg"
radio_metadata=mksafe(radio);

# limpiamos correctamente la metadata
radio =add(normalize=false,[blank(),radio])
radio = rewrite_metadata([("title","Lee los títulos de las canciones en irc.radiognu.org")],radio)

# Salidas al ICECAST
# Salida general al publico: radiognu.ogg
output.icecast.vorbis(mount="radiognu.ogg",
                      samplerate=44100,
                      stereo=false,
                      password="",
                      port=8000,
                      name="RadioGNU",
                      description="La emisora del GNU que te da nota",
                      genre="Live Libre :-)",
                      url="http://radiognu.org:8000/radiognu.ogg",
                      quality=3.0,
                      radio)

# RadioGNU CON METADATA
output.icecast.vorbis(mount="radiognu_metadata.ogg",
                      samplerate=44100,
                      stereo=false,
                      password="",
                      port=8000,
                      name="RadioGNU",
                      description="La emisora del GNU que te da nota",
                      genre="Live Libre :-)",
                      url="http://radiognu.org:8000/radiognu.ogg",
                      quality=3.0,
                      radio_metadata)


# Segunda Salida al publico con menos ancho de banda (sin metadata): radiognu2.ogg
output.icecast.vorbis(mount="radiognu2.ogg",
                      samplerate=22050,
                      stereo=false,
                      password="",
                      port=8000,
                      name="RadioGNU Descremado (para poco ancho de banda)",
                      description="La emisora del GNU que te da nota (version para poco ancho de banda)",
                      genre="Live Libre :-)",
                      url="http://radiognu.org:8000/radiognu2.ogg",
                      quality=0.0,
                      radio)

¡ Y funcionó !

El script anterior da el resultado deseado: permite que mientras se transmite en vivo haya un tiempo prudencial de espera antes de que se haga el cambio hacia los PODCASTS en diferido. Esto hace que las interrupciones menores por conexión de los locutores pase totalmente inadvertida, salvo por un par de segundos en silencio. En total son 7 segundos de espera antes de dar comienzo a los podcasts en diferido.

Script Actual

Con unas modificaciones, logramos esto actualmente:

#!/usr/bin/liquidsoap

# Realizado por octavio@gnu.org.ve, gerardo@lestat.org.ve y hmansilla@gnuchile.cl
# con la colaboración de david.baelde@ens-lyon.org y toots@rastageeks.org

# CONFIGURACIONES
set("log.level",3)
set("log.file",true)
set("log.file.path","/home/radiognu/liquidsoap-log/log.log")
set("server.telnet",true)
set("server.telnet.bind_addr","127.0.0.1")
set("server.telnet.port", 0000)

# Activando la interfaz socket de liquidsoap (por defecto ubicado en /var/run/liquidsoap/radio.sock)
#set("server.socket",true);

# Lista de programas en diferido, se cargan al azar y cada ronda se rehace la lista
podcasts = mksafe(playlist(
        mode            = "randomize",
        reload_mode     = "rounds",
        reload          = 1,
        "/home/radiognu/liquidsoap-recursos/podcasts.txt"))

# Lista de Jingles
jingles = playlist(
        mode            = "normal",
        reload_mode     = "rounds",
        reload          = 1,
        "/home/radiognu/liquidsoap-recursos/jingles.txt")

# se reproducen 3 jingles por cada podcast
diferido = random(weights = [3, 1],[jingles, podcasts])

# Audio proveniente de los (presuntos) locutores
vivo  = input.http("http://localhost:8000/envivo.ogg")

# Transicion. Con esto pasan inadvertidas las interrupciones instantáneas de los programas en vivo.
def transicion(vivo,diferido)
  def immediate(vivo,diferido)
    diferido
  end
  def delayed(vivo,diferido)
    sequence([blank(duration=10.),diferido])
  end
  fallback(track_sensitive=false,[vivo,diferido],transitions=[immediate,delayed])
end

# definimos la radio es _meta porque contiene la metadata
radio_meta = transicion(vivo,diferido)

# agregando fallback para peticiones en vivo (activar cuando se estime conveniente ;) )
#radio=fallback([request.queue(id="request"),radio])

# limpiamos correctamente la metadata de la emisora que no la requiere
radio = add(normalize=false,[blank(),radio_meta])
radio = rewrite_metadata([("artist","Bienvenid@ a RadioGNU, el ñú que te da nota"),("title","Te invitamos a unirte a irc.radiognu.org")],radio)

# Salidas al ICECAST
output.icecast.vorbis(
                mount="radiognu.ogg",
                samplerate=44100,
                password="",
                port=80,
                stereo=true,
                name="RadioGNU",
                description="RadioÑú, la emisora del ñú que te da nota",
                genre="Live Libre :-)",
                url="http://audio.radiognu.org/radiognu.ogg",
                quality=2.0,
                restart=true,
                radio)

# metadata
output.icecast.vorbis(mount="radiometagnu.ogg",
                samplerate=44100,
                password="",
                port=80,
                stereo=true,
                name="RadioÑú con Metadata",
                description="Flujo con metadatos (cuando hay metadatos)",
                genre="Live Libre :-)",
                url="http://audio.radiognu.org/radiometagnu.ogg",
                quality=2.0,
                restart=true,
                radio_meta)

output.icecast.vorbis(mount="radiognu2.ogg",
                samplerate=32000,
                stereo=false,
                password="",
                port=80,
                name="RadioGNU Alterno (version para poco ancho de banda)",
                description="La emisora del GNU que te da nota (version para poco ancho de banda)",
                genre="Live Libre :-)",
                url="http://audio.radiognu.org/radiognu2.ogg",
                quality=0.0,
                restart=true,
                radio)

Propuesto por BreadMaker.

Es una mejora menor, pero interesante para la transición de una transmisión en vivo a la transmisión en diferido.

Usando la función sequence(), se puede lograr que se reproduzcan una serie de fuentes en orden. Es usada en la función de transición propuesta por los hackers de liquidsoap.

La transición puede estar formada de las fuentes que sean necesarias. Ejemplos de aquello pueden ser, archivos estáticos (single(“/ruta/hacia/el/archivo.ogg”)), sonidos generados (sine(), noise(), etc), etc.

Para éste ejemplo lo realizaré en ésta secuencia:

  • Ruido blanco (de 0.1 seg de duración y volumen reducido al 25%)
  • Señal sinusoide de 1Khz (de 0.75 seg de duración y volumen reducido al 25%)
  • Silencio de 5 seg de duración.
  • Vuelta a la programación con efecto de fadeIn de 3 seg.

Y el script con las modificaciones quedaría entonces así. Ojo que el cambio es en la definición de la función delayed():

# Transicion. Con esto pasan inadvertidas las interrupciones instantáneas de los programas en vivo.
def transicion(vivo,diferido)
  def immediate(vivo,diferido)
    diferido
  end
  def delayed(vivo,diferido)
    sequence([amplify(.25,noise(duration=.1)),amplify(.25,sine(1000.,duration=.75)),blank(duration=5.),fade.initial(duration=3.,diferido)])
  end
  fallback(track_sensitive=false,[vivo,diferido],transitions=[immediate,delayed])
end

Por supuesto, el script final dependerá de cómo se quiera que quede, lol.

Las pruebas arrojaron un comportamiento extraño, quedando a veces la radio en silencio permanente. Es necesaria mayor revisión para implementar la idea. Una forma puede ser colocando un audio de entrada, que sea silencio, beep, silencio, así:

sequence( merge=true, [ audio_de_transicion, fade.initial(duration=3.,diferido) ])

Gracias a los avances en la tecnología, ya no es necesario forzar estéreo para la transmisión. Sin embargo, se deja esto documentado, en caso de que en el futuro si sea necesario, quien sabe.

Éste cambio sólo es válido a partir de la versión 1.0.0-beta1 de Liquidsoap hacia adelante.

La función audio_to_stereo() convierte cualquier fuente de sonido en una fuente en estéreo.

Su implementación es la siguiente:

radio=audio_to_stereo(radio)

Así de simple, por la CTM!