Extensiones de fuentes de medios para audio

Dale Curtis
Dale Curtis

Introducción

Las extensiones de fuente de medios (MSE) proporcionan almacenamiento en búfer extendido y control de reproducción para los elementos <audio> y <video> de HTML5. Si bien se desarrolló originalmente para facilitar la transmisión adaptable y dinámica a través de reproductores de video basados en HTTP (DASH), a continuación, veremos cómo se pueden utilizar para audio. específicamente para la reproducción sin interrupciones.

Es probable que hayas escuchado un álbum de música en el que las canciones fluían sin interrupciones. incluso es posible que estés escuchando uno en este momento. Los artistas crean estas experiencias de reproducción sin interrupciones como una elección artística y como artefacto de discos de vinilo y CDs en los que el audio se escribió como una transmisión continua. Lamentablemente, debido al funcionamiento de los códecs de audio modernos, como MP3 y AAC, esta experiencia auditiva fluida suele perderse en la actualidad.

A continuación, entraremos en detalle por qué, pero por ahora empecemos con una demostración. A continuación, se muestran los primeros treinta segundos de la excelente Sintel cortada en cinco archivos MP3 independientes y reensamblados con ECM. Las líneas rojas indican las brechas que se introdujeron durante la creación (codificación) de cada MP3. escucharás fallas en estos puntos.

Datos demográficos

¡Qué asco! No fue una buena experiencia; podemos hacerlo mejor. Con un poco más de trabajo, usando exactamente los mismos archivos MP3 de la demostración anterior, podemos usar ECM para quitar esas brechas molestas. Las líneas verdes de la siguiente demostración indican dónde se unieron los archivos y se quitaron los espacios. En Chrome 38 y versiones posteriores, esto se reproducirá sin problemas.

Datos demográficos

Existen varias formas de crear contenido sin espacios. Para los fines de esta demostración, nos enfocaremos en los tipos de archivos que un usuario normal podría tener. Cuando cada archivo se codificó por separado, independientemente de los segmentos de audio anteriores o posteriores.

Configuración básica

Primero, hagamos un seguimiento y veamos la configuración básica de una instancia MediaSource. Como su nombre lo indica, las extensiones de fuente de medios son simplemente extensiones de los elementos de medios existentes. A continuación, asignamos un Object URL, que representa nuestra instancia de MediaSource, al atributo fuente de un elemento de audio. tal como lo harías con una URL estándar.

var audio = document.createElement('audio');
var mediaSource = new MediaSource();
var SEGMENTS = 5;

mediaSource.addEventListener('sourceopen', function() {
    var sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

    function onAudioLoaded(data, index) {
    // Append the ArrayBuffer data into our new SourceBuffer.
    sourceBuffer.appendBuffer(data);
    }

    // Retrieve an audio segment via XHR.  For simplicity, we're retrieving the
    // entire segment at once, but we could also retrieve it in chunks and append
    // each chunk separately.  MSE will take care of assembling the pieces.
    GET('sintel/sintel_0.mp3', function(data) { onAudioLoaded(data, 0); } );
});

audio.src = URL.createObjectURL(mediaSource);

Una vez que el objeto MediaSource esté conectado, realizará una inicialización y, al final, activará un evento sourceopen. en ese momento, podemos crear un SourceBuffer. En el ejemplo anterior, creamos un elemento audio/mpeg, que puede analizar y decodificar nuestros segmentos MP3. Existen varios otros tipos disponibles.

Formas de onda anómalas

Volveremos al código en un momento, pero ahora veamos en detalle el archivo que acabamos de agregar, específicamente al final. A continuación, se muestra un gráfico de las últimas 3,000 muestras promediadas entre ambos canales del segmento sintel_0.mp3. Cada píxel de la línea roja es una muestra de punto flotante en el rango de [-1.0, 1.0].

Fin de sintel_0.mp3

¿Qué pasa con todas esas muestras cero (silenciosas)? En realidad, se deben a artefactos de compresión que se introdujeron durante la codificación. Casi todos los codificadores introducen algún tipo de relleno. En este caso, LAME agregó exactamente 576 muestras de padding al final del archivo.

Además del padding al final, a cada archivo también se le agregó padding al comienzo. Si miramos el segmento de sintel_1.mp3, veremos otras 576 muestras de padding al frente. La cantidad de padding varía según el codificador y el contenido, pero sabemos los valores exactos según los elementos metadata que se incluyen en cada archivo.

Comienzo de sintel_1.mp3

Comienzo de sintel_1.mp3

Las secciones de silencio al principio y al final de cada archivo son las que causan las fallas entre segmentos en la demostración anterior. Para lograr una reproducción sin espacios, debemos quitar esas secciones de silencio. Por suerte, esto se puede hacer fácilmente con MediaSource. A continuación, modificaremos nuestro método onAudioLoaded() para usar una ventana de agregado y un desplazamiento de marca de tiempo para quitar este silencio.

Código de ejemplo

function onAudioLoaded(data, index) {
    // Parsing gapless metadata is unfortunately non trivial and a bit messy, so
    // we'll glaze over it here; see the appendix for details.
    // ParseGaplessData() will return a dictionary with two elements:
    //
    //    audioDuration: Duration in seconds of all non-padding audio.
    //    frontPaddingDuration: Duration in seconds of the front padding.
    //
    var gaplessMetadata = ParseGaplessData(data);

    // Each appended segment must be appended relative to the next.  To avoid any
    // overlaps, we'll use the end timestamp of the last append as the starting
    // point for our next append or zero if we haven't appended anything yet.
    var appendTime = index > 0 ? sourceBuffer.buffered.end(0) : 0;

    // Simply put, an append window allows you to trim off audio (or video) frames
    // which fall outside of a specified time range.  Here, we'll use the end of
    // our last append as the start of our append window and the end of the real
    // audio data for this segment as the end of our append window.
    sourceBuffer.appendWindowStart = appendTime;
    sourceBuffer.appendWindowEnd = appendTime + gaplessMetadata.audioDuration;

    // The timestampOffset field essentially tells MediaSource where in the media
    // timeline the data given to appendBuffer() should be placed.  I.e., if the
    // timestampOffset is 1 second, the appended data will start 1 second into
    // playback.
    //
    // MediaSource requires that the media timeline starts from time zero, so we
    // need to ensure that the data left after filtering by the append window
    // starts at time zero.  We'll do this by shifting all of the padding we want
    // to discard before our append time (and thus, before our append window).
    sourceBuffer.timestampOffset =
        appendTime - gaplessMetadata.frontPaddingDuration;

    // When appendBuffer() completes, it will fire an updateend event signaling
    // that it's okay to append another segment of media.  Here, we'll chain the
    // append for the next segment to the completion of our current append.
    if (index == 0) {
    sourceBuffer.addEventListener('updateend', function() {
        if (++index < SEGMENTS) {
        GET('sintel/sintel_' + index + '.mp3',
            function(data) { onAudioLoaded(data, index); });
        } else {
        // We've loaded all available segments, so tell MediaSource there are no
        // more buffers which will be appended.
        mediaSource.endOfStream();
        URL.revokeObjectURL(audio.src);
        }
    });
    }

    // appendBuffer() will now use the timestamp offset and append window settings
    // to filter and timestamp the data we're appending.
    //
    // Note: While this demo uses very little memory, more complex use cases need
    // to be careful about memory usage or garbage collection may remove ranges of
    // media in unexpected places.
    sourceBuffer.appendBuffer(data);
}

Una forma de onda impecable

Veamos lo que logró nuestro nuevo código. Para ello, echemos un vistazo a la forma de onda después de aplicar nuestras ventanas de anexo. A continuación, puedes ver que se quitaron la sección de silencio al final de sintel_0.mp3 (en rojo) y la sección de silencio al comienzo de sintel_1.mp3 (en azul). lo que nos deja una transición sin problemas entre segmentos.

Unión de sintel_0.mp3 y sintel_1.mp3

Conclusión

Con eso, unimos los cinco segmentos a la perfección en uno y, luego, hemos llegado al final de la demostración. Antes de continuar, tal vez hayas notado que nuestro método onAudioLoaded() no tiene en cuenta los contenedores ni los códecs. Esto significa que todas estas técnicas funcionarán independientemente del tipo de contenedor o códec. A continuación, puedes volver a reproducir la demostración original MP4 fragmentada y preparada para DASH, en lugar de MP3.

Datos demográficos

Si quieres obtener más información, consulta los siguientes apéndices para ver un análisis más detallado de la creación de contenido sin espacios y el análisis de metadatos. También puedes explorar gapless.js para observar con más detalle el código que impulsa esta demostración.

¡Gracias por leer esta información!

Apéndice A: Creación de contenido sin espacios

Crear contenido sin espacios puede ser difícil hacerlo bien. A continuación, veremos la creación del contenido multimedia de Sintel usado en esta demostración. Para comenzar, necesitas una copia de la banda sonora de FLAC sin pérdidas para Sintel. para la posteridad, el SHA1 se incluye a continuación. Para las herramientas, necesitarás FFmpeg, MP4Box, LAME y una instalación de OSX con afconvert.

unzip Jan_Morgenstern-Sintel-FLAC.zip
sha1sum 1-Snow_Fight.flac
# 0535ca207ccba70d538f7324916a3f1a3d550194  1-Snow_Fight.flac

Primero, dividiremos los primeros 31.5 segundos de la pista 1-Snow_Fight.flac. También queremos agregar un fundido de salida de 2.5 segundos que comience a los 28 segundos para evitar clics una vez finalizada la reproducción. Con la siguiente línea de comandos de FFmpeg, podemos lograr todo esto y colocar los resultados en sintel.flac.

ffmpeg -i 1-Snow_Fight.flac -t 31.5 -af "afade=t=out:st=28:d=2.5" sintel.flac

A continuación, dividiremos el archivo en 5 archivos wave de 6.5 segundos cada uno. es más fácil de usar onda, ya que casi todos los codificadores admiten la transferencia. Una vez más, podemos hacerlo con precisión con FFmpeg, después de lo cual tendremos: sintel_0.wav, sintel_1.wav, sintel_2.wav, sintel_3.wav y sintel_4.wav.

ffmpeg -i sintel.flac -acodec pcm_f32le -map 0 -f segment \
        -segment_list out.list -segment_time 6.5 sintel_%d.wav

A continuación, creemos los archivos MP3. LAME tiene varias opciones para crear contenido sin espacios. Si tienes el control del contenido, puedes usar --nogap con una codificación por lotes de todos los archivos para evitar el relleno entre segmentos por completo. Sin embargo, para los fines de esta demostración, queremos usar ese relleno, por lo que usaremos una codificación VBR estándar de alta calidad de los archivos de conjuntos.

lame -V=2 sintel_0.wav sintel_0.mp3
lame -V=2 sintel_1.wav sintel_1.mp3
lame -V=2 sintel_2.wav sintel_2.mp3
lame -V=2 sintel_3.wav sintel_3.mp3
lame -V=2 sintel_4.wav sintel_4.mp3

Eso es todo lo que se necesita para crear los archivos MP3. Ahora, hablemos sobre la creación de los archivos MP4 fragmentados. Seguiremos las instrucciones de Apple para crear contenido multimedia masterizado para iTunes. A continuación, convertiremos los archivos de conjuntos en archivos CAF intermedios, según las instrucciones, antes de codificarlos como AAC en un contenedor MP4 con los parámetros recomendados.

afconvert sintel_0.wav sintel_0_intermediate.caf -d 0 -f caff \
            --soundcheck-generate
afconvert sintel_1.wav sintel_1_intermediate.caf -d 0 -f caff \
            --soundcheck-generate
afconvert sintel_2.wav sintel_2_intermediate.caf -d 0 -f caff \
            --soundcheck-generate
afconvert sintel_3.wav sintel_3_intermediate.caf -d 0 -f caff \
            --soundcheck-generate
afconvert sintel_4.wav sintel_4_intermediate.caf -d 0 -f caff \
            --soundcheck-generate
afconvert sintel_0_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
            -b 256000 -q 127 -s 2 sintel_0.m4a
afconvert sintel_1_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
            -b 256000 -q 127 -s 2 sintel_1.m4a
afconvert sintel_2_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
            -b 256000 -q 127 -s 2 sintel_2.m4a
afconvert sintel_3_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
            -b 256000 -q 127 -s 2 sintel_3.m4a
afconvert sintel_4_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
            -b 256000 -q 127 -s 2 sintel_4.m4a

Ahora tenemos varios archivos M4A que debemos fragmentar correctamente antes de que se puedan usar con MediaSource. Para nuestros fines, usaremos un tamaño de fragmento de un segundo. MP4Box escribirá cada MP4 fragmentado como sintel_#_dashinit.mp4 junto con un manifiesto MPEG-DASH (sintel_#_dash.mpd) que se puede descartar.

MP4Box -dash 1000 sintel_0.m4a && mv sintel_0_dashinit.mp4 sintel_0.mp4
MP4Box -dash 1000 sintel_1.m4a && mv sintel_1_dashinit.mp4 sintel_1.mp4
MP4Box -dash 1000 sintel_2.m4a && mv sintel_2_dashinit.mp4 sintel_2.mp4
MP4Box -dash 1000 sintel_3.m4a && mv sintel_3_dashinit.mp4 sintel_3.mp4
MP4Box -dash 1000 sintel_4.m4a && mv sintel_4_dashinit.mp4 sintel_4.mp4
rm sintel_{0,1,2,3,4}_dash.mpd

Eso es todo. Ahora tenemos archivos MP4 y MP3 fragmentados con los metadatos correctos necesarios para una reproducción sin interrupciones. Consulta el Apéndice B para obtener más detalles sobre cómo son esos metadatos.

Apéndice B: Análisis de metadatos sin espacios

Al igual que crear contenido sin espacios, analizar los metadatos sin espacios puede ser complicado, ya que no existe un método estándar de almacenamiento. A continuación, veremos cómo los dos codificadores más comunes, LAME y iTunes, almacenan sus metadatos sin espacios. Para comenzar, configuremos algunos métodos auxiliares y un esquema para el ParseGaplessData() que se usó anteriormente.

// Since most MP3 encoders store the gapless metadata in binary, we'll need a
// method for turning bytes into integers.  Note: This doesn't work for values
// larger than 2^30 since we'll overflow the signed integer type when shifting.
function ReadInt(buffer) {
    var result = buffer.charCodeAt(0);
    for (var i = 1; i < buffer.length; ++i) {
    result <<../= 8;
    result += buffer.charCodeAt(i);
    }
    return result;
}

function ParseGaplessData(arrayBuffer) {
    // Gapless data is generally within the first 512 bytes, so limit parsing.
    var byteStr = new TextDecoder().decode(arrayBuffer.slice(0, 512));

    var frontPadding = 0, endPadding = 0, realSamples = 0;

    // ... we'll fill this in as we go below.

Primero hablaremos sobre el formato de metadatos de iTunes de Apple, ya que es el más fácil de analizar y explicar. Dentro de los archivos MP3 y M4A, iTunes (y afconvert) escribe una sección corta en ASCII de la siguiente manera:

iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00

Esto se escribe en una etiqueta ID3, dentro del contenedor de MP3, y en un átomo de metadatos dentro del contenedor de MP4. Para nuestros fines, podemos ignorar el primer token de 0000000. Los siguientes tres tokens son el padding frontal, el padding final y el recuento total de muestras sin padding. Dividir cada uno de ellos por la tasa de muestreo del audio nos da la duración de cada uno.

// iTunes encodes the gapless data as hex strings like so:
//
//    'iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00'
//    'iTunSMPB[ 26 bytes ]####### frontpad  endpad    real samples'
//
// The approach here elides the complexity of actually parsing MP4 atoms. It
// may not work for all files without some tweaks.
var iTunesDataIndex = byteStr.indexOf('iTunSMPB');
if (iTunesDataIndex != -1) {
    var frontPaddingIndex = iTunesDataIndex + 34;
    frontPadding = parseInt(byteStr.substr(frontPaddingIndex, 8), 16);

    var endPaddingIndex = frontPaddingIndex + 9;
    endPadding = parseInt(byteStr.substr(endPaddingIndex, 8), 16);

    var sampleCountIndex = endPaddingIndex + 9;
    realSamples = parseInt(byteStr.substr(sampleCountIndex, 16), 16);
}

Por otro lado, la mayoría de los codificadores MP3 de código abierto almacenarán los metadatos sin espacios en un encabezado Xing especial ubicado dentro de un marco MPEG silencioso (es silencioso, por lo que los decodificadores que no entienden el encabezado Xing simplemente reproducen silencio). Lamentablemente, esta etiqueta no siempre está presente y tiene varios campos opcionales. Para los fines de esta demostración, tenemos el control del contenido multimedia, pero, en la práctica, se requerirán algunas verificaciones adicionales para saber cuándo están realmente disponibles los metadatos sin espacios.

Primero, analizaremos el recuento total de muestras. Para simplificar, leeremos esto desde el encabezado Xing, pero podría construirse a partir del encabezado de audio MPEG normal. Los encabezados Xing se pueden marcar con una etiqueta Xing o Info. Exactamente 4 bytes después de esta etiqueta hay 32 bits que representan la cantidad total de tramas en el archivo. multiplicar este valor por la cantidad de muestras por fotograma nos dará el total de muestras en el archivo.

// Xing padding is encoded as 24bits within the header.  Note: This code will
// only work for Layer3 Version 1 and Layer2 MP3 files with XING frame counts
// and gapless information.  See the following document for more details:
// http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
var xingDataIndex = byteStr.indexOf('Xing');
if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Info');
if (xingDataIndex != -1) {
    // See section 2.3.1 in the link above for the specifics on parsing the Xing
    // frame count.
    var frameCountIndex = xingDataIndex + 8;
    var frameCount = ReadInt(byteStr.substr(frameCountIndex, 4));

    // For Layer3 Version 1 and Layer2 there are 1152 samples per frame.  See
    // section 2.1.5 in the link above for more details.
    var paddedSamples = frameCount * 1152;

    // ... we'll cover this below.

Ahora que tenemos la cantidad total de muestras, podemos pasar a la lectura de la cantidad de muestras de relleno. Según tu codificador, esto puede escribirse en una etiqueta LAME o Lavf anidada en el encabezado Xing. Exactamente 17 bytes después de este encabezado, hay 3 bytes que representan el relleno de frontend y extremo en cada uno de 12 bits, respectivamente.

xingDataIndex = byteStr.indexOf('LAME');
if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Lavf');
if (xingDataIndex != -1) {
    // See http://gabriel.mp3-tech.org/mp3infotag.html#delays for details of
    // how this information is encoded and parsed.
    var gaplessDataIndex = xingDataIndex + 21;
    var gaplessBits = ReadInt(byteStr.substr(gaplessDataIndex, 3));

    // Upper 12 bits are the front padding, lower are the end padding.
    frontPadding = gaplessBits >> 12;
    endPadding = gaplessBits & 0xFFF;
}

realSamples = paddedSamples - (frontPadding + endPadding);
}

return {
audioDuration: realSamples * SECONDS_PER_SAMPLE,
frontPaddingDuration: frontPadding * SECONDS_PER_SAMPLE
};
}

Con esa información, tenemos una función completa para analizar la gran mayoría del contenido sin espacios. Sin embargo, los casos extremos abundan, por lo que se recomienda tener cuidado antes de usar código similar en producción.

Apéndice C: Sobre la recolección de elementos no utilizados

La memoria que pertenece a instancias de SourceBuffer es activamente una recolección de elementos no utilizados según el tipo de contenido, los límites específicos de la plataforma y la posición de reproducción actual. En Chrome, primero se recuperará la memoria de los búferes ya reproducidos. Sin embargo, si el uso de memoria excede los límites específicos de la plataforma, se quitará la memoria de los búferes sin reproducir.

Cuando la reproducción alcanza un intervalo en la línea de tiempo debido a la memoria recuperada, puede fallar si el intervalo es lo suficientemente pequeño, o detenerse por completo si es demasiado grande. Ninguna de las dos es una excelente experiencia del usuario, por lo que es importante evitar agregar demasiados datos a la vez y quitar manualmente los rangos del cronograma de medios que ya no son necesarios.

Los rangos se pueden quitar a través del método remove() en cada SourceBuffer. que toma un rango [start, end] en segundos. Al igual que appendBuffer(), cada remove() activará un evento updateend una vez que se complete. Otras eliminaciones o anexos no deben emitirse hasta que se active el evento.

En la versión de Chrome para escritorio, puedes conservar aproximadamente 12 megabytes de contenido de audio y 150 megabytes de contenido de video en la memoria a la vez. No debes confiar en estos valores en diferentes navegadores o plataformas. p.ej., sin duda no son representativos de los dispositivos móviles.

La recolección de elementos no utilizados solo afecta los datos agregados a SourceBuffers. No hay límites para la cantidad de datos que se pueden almacenar en búfer en las variables de JavaScript. También puedes volver a agregar los mismos datos en la misma posición si es necesario.