Extensions de source multimédia pour l'audio

Dale Curtis
Dale Curtis

Introduction

Les extensions de source multimédia (MSE) fournissent des commandes étendues de mise en mémoire tampon et de lecture pour les éléments HTML5 <audio> et <video>. Bien qu'ils aient été développés à l'origine pour faciliter les lecteurs vidéo basés sur le streaming adaptatif dynamique sur HTTP (DASH), nous allons voir ci-dessous comment ils peuvent être utilisés pour l'audio. en particulier pour la lecture sans interruption.

Vous avez probablement déjà écouté un album musical où les titres coulaient naturellement d'un titre à l'autre. vous êtes peut-être en train d’en écouter une en ce moment. Les artistes créent ces expériences de lecture sans interruption à la fois sous la forme d'un choix artistique, mais aussi d'un artefact de disques vinyles et de CD dont le contenu audio était écrit sous forme de flux continu. Malheureusement, en raison du fonctionnement des codecs audio modernes tels que MP3 et AAC, cette expérience sonore fluide est souvent perdue aujourd'hui.

Nous expliquerons en détail pourquoi ci-dessous, mais pour l'instant, commençons par une démonstration. Vous trouverez ci-dessous les trente premières secondes de l'excellent Sintel, découpées en cinq fichiers MP3 distincts et réassemblées à l'aide de MSE. Les lignes rouges indiquent les écarts qui ont été introduits lors de la création (l'encodage) de chaque fichier MP3. vous entendrez des glitchs à ces moments.

Démo

Beurk ! Ce n’est pas une bonne expérience ; nous pouvons faire mieux. Avec un peu plus de travail, en utilisant exactement les mêmes fichiers MP3 dans la démo ci-dessus, nous pouvons utiliser MSE pour supprimer ces gênants écarts. Dans la prochaine démonstration, les lignes vertes indiquent où les fichiers ont été joints et où les espaces ont été supprimés. À partir de la version 38 de Chrome, la lecture se fera de manière fluide.

Démo

Il existe de nombreuses façons de créer des contenus sans interruption. Pour les besoins de cette démonstration, nous allons nous concentrer sur le type de fichiers qu'un utilisateur normal peut avoir. où chaque fichier a été encodé séparément sans tenir compte des segments audio situés avant ou après.

Configuration de base

Pour commencer, revenons sur la configuration de base d'une instance MediaSource. Comme leur nom l'indique, les extensions Media Source sont simplement des extensions des éléments multimédias existants. Ci-dessous, nous attribuons un Object URL, représentant notre instance MediaSource, à l'attribut source d'un élément audio. comme vous le feriez pour une URL standard.

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);

Une fois l'objet MediaSource connecté, il effectue une initialisation, puis déclenche un événement sourceopen. Nous pouvons alors créer un SourceBuffer. Dans l'exemple ci-dessus, nous allons créer un segment audio/mpeg, capable d'analyser et de décoder nos segments MP3. plusieurs autres types sont disponibles.

Formes d'ondes anormales

Nous reviendrons sur le code dans un instant, mais examinons de plus près le fichier que nous venons d'ajouter, spécifiquement à la fin. Le graphique ci-dessous représente les 3 000 derniers échantillons, dont la moyenne est calculée sur les deux canaux pour le titre sintel_0.mp3. Chaque pixel sur la ligne rouge est un échantillon à virgule flottante dans la plage [-1.0, 1.0].

Fin du fichier sintel_0.mp3

À quoi servent ces zéros (silencieux) échantillons ? Elles sont en fait dues à des artefacts de compression introduits lors de l'encodage. Presque tous les encodeurs introduisent un type de remplissage. Dans ce cas, LAME a ajouté exactement 576 échantillons de marge intérieure à la fin du fichier.

En plus de la marge intérieure à la fin, une marge intérieure a également été ajoutée au début de chaque fichier. Si nous jetons un coup d'œil à la piste sintel_1.mp3, nous voyons 576 autres échantillons de marge intérieure à l'avant. La quantité de marge intérieure varie selon l'encodeur et le contenu, mais nous connaissons les valeurs exactes d'après les metadata incluses dans chaque fichier.

Début de sintel_1.mp3

Début de sintel_1.mp3

Les portions de silence au début et à la fin de chaque fichier sont à l'origine des glitchs entre les segments de la démo précédente. Pour obtenir une lecture sans interruption, nous devons supprimer ces moments de silence. Heureusement, vous pouvez le faire facilement avec MediaSource. Nous allons modifier ci-dessous la méthode onAudioLoaded() afin d'utiliser une fenêtre d'ajout et un décalage temporel pour supprimer ce silence.

Exemple de code

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);
}

Une forme d'onde fluide

Voyons ce que notre nouveau code a accompli en examinant à nouveau la forme d'onde après avoir appliqué nos fenêtres d'ajout. Comme vous pouvez le constater ci-dessous, la section silencieuse à la fin de sintel_0.mp3 (en rouge) et la section silencieuse au début de sintel_1.mp3 (en bleu) ont été supprimées. ce qui offre une transition fluide entre les segments.

Jointure de sintel_0.mp3 et sintel_1.mp3

Conclusion

Nous avons réuni les cinq segments de manière fluide en un seul, et nous arrivons à la fin de notre démonstration. Avant de terminer, vous avez peut-être remarqué que la méthode onAudioLoaded() ne tient pas compte des conteneurs ni des codecs. Cela signifie que toutes ces techniques fonctionneront quel que soit le type de conteneur ou de codec. Ci-dessous, vous pouvez revoir la démo d'origine en MP4, compatible avec DASH, au lieu de MP3.

Démo

Pour en savoir plus sur la création de contenu et l'analyse des métadonnées, consultez les annexes ci-dessous. Vous pouvez également explorer gapless.js pour examiner de plus près le code sur lequel repose cette démonstration.

Merci de votre attention,

Annexe A: Créer du contenu sans lacune

Il peut être difficile de créer des contenus sans lacune. Voyons ci-dessous comment créer le média Sintel utilisé dans cette démonstration. Pour commencer, vous avez besoin d'une copie de la bande-son FLAC sans perte pour Sintel. pour la postérité, le SHA1 est inclus ci-dessous. Pour les outils, vous aurez besoin de FFmpeg, MP4Box, LAME et d'une installation OSX avec afconvert.

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

Pour commencer, nous allons séparer les 31,5 premières secondes du titre 1-Snow_Fight.flac. Nous souhaitons également ajouter un fondu de 2,5 secondes à partir de 28 secondes pour éviter les clics une fois la lecture terminée. La ligne de commande FFmpeg ci-dessous nous permet d'effectuer toutes ces opérations et de placer les résultats dans sintel.flac.

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

Nous allons ensuite diviser le fichier en 5 fichiers wave de 6, 5 secondes chacun. il est plus facile d'utiliser une vague puisque presque tous les encodeurs prennent en charge l'ingestion de celle-ci. Là encore, nous pouvons effectuer cette opération avec FFmpeg, après quoi nous obtenons: sintel_0.wav, sintel_1.wav, sintel_2.wav, sintel_3.wav et 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

Ensuite, créons les fichiers MP3. LAME propose plusieurs options pour créer des contenus sans intervalles. Si vous contrôlez le contenu, vous pouvez envisager d'utiliser --nogap avec un encodage par lot de tous les fichiers afin d'éviter toute marge intérieure entre les segments. Toutefois, pour les besoins de cette démonstration, nous souhaitons utiliser cette marge intérieure. Nous allons donc utiliser un encodage VBR standard de haute qualité pour les fichiers d'ondes.

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

C'est tout ce qui est nécessaire pour créer les fichiers MP3. Abordons maintenant la création des fichiers MP4 fragmentés. Nous suivrons les instructions d'Apple pour créer des contenus multimédias masqués pour iTunes. Nous allons maintenant convertir les fichiers de vagues en fichiers CAF intermédiaires, conformément aux instructions, avant de les encoder en AAC dans un conteneur MP4 avec les paramètres recommandés.

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

Nous disposons à présent de plusieurs fichiers M4A que nous devons fragmenter de manière appropriée avant de pouvoir les utiliser avec MediaSource. Pour les besoins de cet atelier, nous utiliserons une taille de fragment d'une seconde. MP4Box écrit chaque fichier MP4 fragmenté en tant que sintel_#_dashinit.mp4 avec un fichier manifeste MPEG-DASH (sintel_#_dash.mpd) qui peut être supprimé.

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

Et voilà ! Nous disposons à présent de fichiers MP4 et MP3 fragmentés avec les métadonnées appropriées, nécessaires à une lecture sans interruption. Voir l’annexe B pour plus de détails sur ce à quoi ressemblent ces métadonnées.

Annexe B: Analyse des métadonnées sans lacune

Tout comme pour la création de contenu sans intervalles, l'analyse des métadonnées sans intervalles peut s'avérer délicate, car il n'existe pas de méthode de stockage standard. Nous allons voir ci-dessous comment les deux encodeurs les plus courants, LAME et iTunes, stockent leurs métadonnées sans intervalles. Commençons par configurer quelques méthodes d'assistance et un aperçu du ParseGaplessData() utilisé ci-dessus.

// 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.

Nous allons commencer par aborder le format de métadonnées iTunes d'Apple, car il est le plus simple à analyser et à expliquer. Dans les fichiers MP3 et M4A, iTunes (et afconvert) écrivez une courte section en ASCII comme suit:

iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00

Il est écrit dans une balise ID3 au sein du conteneur MP3 et dans un atome de métadonnées à l'intérieur du conteneur MP4. Pour les besoins de cet atelier, nous pouvons ignorer le premier jeton 0000000. Les trois jetons suivants sont la marge intérieure avant, la marge intérieure de fin et le nombre total d'échantillons sans remplissage. Diviser chacun de ces éléments par le taux d'échantillonnage de l'audio nous donne la durée de chacun d'entre eux.

// 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);
}

À l'inverse, la plupart des encodeurs MP3 Open Source stockent les métadonnées sans espaces dans un en-tête Xing spécial placé à l'intérieur d'une trame MPEG silencieuse (il est silencieux, de sorte que les décodeurs qui ne comprennent pas l'en-tête Xing diffusent simplement le silence). Malheureusement, cette balise n'est pas toujours présente et comporte un certain nombre de champs facultatifs. Pour les besoins de cette démonstration, nous contrôlons les médias, mais dans la pratique, des vérifications supplémentaires seront nécessaires pour savoir quand des métadonnées sans intervalles sont effectivement disponibles.

Commençons par analyser le nombre total d'échantillons. Par souci de simplicité, nous le lisons à partir de l'en-tête Xing, mais il peut être créé à partir de l'en-tête audio MPEG normal. Les en-têtes Xing peuvent être marqués par une balise Xing ou Info. Exactement 4 octets après ce tag, 32 bits représentent le nombre total de trames dans le fichier ; multiplier cette valeur par le nombre d'échantillons par trame nous donnera le nombre total d'échantillons dans le fichier.

// 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.

Maintenant que nous avons le nombre total d'échantillons, nous pouvons passer à la lecture du nombre d'échantillons de marge intérieure. Selon votre encodeur, ce code peut être écrit sous une balise LAME ou Lavf imbriquée dans l'en-tête Xing. Exactement 17 octets après cet en-tête, 3 octets représentent respectivement la marge intérieure de 12 bits de l'interface et celle de la fin.

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
};
}

Nous disposons ainsi d'une fonction complète permettant d'analyser la grande majorité des contenus sans lacune. Les cas limites abondent cependant, il est donc recommandé d'être prudent avant d'utiliser un code similaire en production.

Annexe C: Récupération de mémoire

La mémoire appartenant aux instances SourceBuffer est activement récupération de mémoire en fonction du type de contenu, des limites spécifiques à la plate-forme et de la position de lecture actuelle. Dans Chrome, la mémoire est d'abord récupérée à partir des tampons déjà lus. Toutefois, si l'utilisation de la mémoire dépasse les limites spécifiques à la plate-forme, la mémoire des tampons non lus sera supprimée.

Lorsqu'un blanc est détecté dans la timeline en raison d'une quantité de mémoire récupérée, des problèmes peuvent survenir si l'intervalle est suffisamment faible ou se bloquer complètement s'il est trop important. Ce n'est pas non plus une expérience utilisateur de qualité. Il est donc important d'éviter d'ajouter trop de données en une seule fois et de supprimer manuellement les plages qui ne sont plus nécessaires dans la chronologie multimédia.

Les plages peuvent être supprimées via la méthode remove() sur chaque SourceBuffer. ce qui prend une plage [start, end] en secondes. Comme pour appendBuffer(), chaque remove() déclenche un événement updateend une fois l'opération terminée. D'autres suppressions ou ajouts ne doivent être émis que lorsque l'événement est déclenché.

Dans Chrome pour ordinateur, vous pouvez conserver environ 12 mégaoctets de contenu audio et 150 mégaoctets de contenu vidéo en mémoire à la fois. Vous ne devez pas vous fier à ces valeurs sur l'ensemble des navigateurs ou plates-formes. Par exemple, ils ne sont certainement pas représentatifs des appareils mobiles.

La récupération de mémoire n'affecte que les données ajoutées à SourceBuffers. la quantité de données que vous pouvez conserver en mémoire tampon dans les variables JavaScript n'est pas limitée. Vous pouvez également rajouter les mêmes données dans la même position si nécessaire.