Ses İçin Medya Kaynağı Uzantıları

Dale Curtis
Dale Curtis

Giriş

Medya Kaynağı Uzantıları (MSE), HTML5 <audio> ve <video> öğeleri için genişletilmiş arabelleğe alma ve oynatma kontrolü sağlar. İlk olarak HTTP (DASH) üzerinden Dinamik Uyarlanabilir Akış tabanlı video oynatıcıları kolaylaştırmak için geliştirilmiş olsa da ses için nasıl kullanılabileceğini aşağıda göreceğiz; bu yöntemin özellikle aralıksız oynatma için kullanılması önerilir.

Şarkıların birbirinden pürüzsüz şekilde çıktığı bir müzik albümü dinlemişsinizdir. şu anda birini dinliyor bile olabilirsiniz. Sanatçılar, bu boşluksuz oynatma deneyimlerini hem sanatsal bir seçim hem de sesin kesintisiz bir akış olarak yazıldığı vinil plaklar ve CD'lerden oluşan bir eser olarak hazırlıyor. MP3 ve AAC gibi modern ses codec'lerinin çalışma şekli nedeniyle bu sorunsuz işitsel deneyim, maalesef günümüzde sıklıkla kayboluyor.

Bunun nedenlerini aşağıda açıklayacağız ancak şimdilik bir tanıtımla başlayalım. Aşağıda, beş ayrı MP3 dosyası olarak parçalara ayırıp MSE ile yeniden bir araya getirilen mükemmel Sintel'in ilk otuz saniyesini görebilirsiniz. Kırmızı çizgiler, her bir MP3'ün oluşturulması (kodlama) sırasında ortaya çıkan boşlukları gösterir; arızalar duyacaksınız.

Demo

Hay aksi! Pek iyi bir deneyim değil, yapabiliriz. Biraz daha çalışırsanız yukarıdaki demoda bulunan MP3 dosyalarının aynısını kullanarak, MSE'yi kullanarak bu rahatsız edici boşlukları kaldırabiliriz. Bir sonraki demodaki yeşil çizgiler, dosyaların nerede birleştirildiğini ve boşlukların kaldırıldığını gösterir. Bu video, Chrome 38 ve sonraki sürümlerde sorunsuz bir şekilde oynatılır.

Demo

Boşluksuz içerik oluşturmanın çeşitli yolları vardır. Bu demoda, normal bir kullanıcının etrafta durabileceği dosya türlerine odaklanacağız. Her bir dosyanın, öncesindeki veya sonrasındaki ses segmentleri dikkate alınmaksızın ayrı olarak kodlandığı yer.

Temel Kurulum

Öncelikle geriye dönüp MediaSource örneğinin temel kurulumuna bakalım. Adından da anlaşılacağı gibi Medya Kaynağı Uzantıları, yalnızca mevcut medya öğelerinin uzantılarıdır. Aşağıda, bir ses öğesinin kaynak özelliğine MediaSource örneğimizi temsil eden bir Object URL atıyoruz; Tıpkı standart bir URL ayarlamak gibi.

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

MediaSource nesnesi bağlandığında bir miktar başlatma gerçekleştirir ve sonunda bir sourceopen etkinliği tetikler. bu noktada bir SourceBuffer oluşturabiliriz. Yukarıdaki örnekte, MP3 segmentlerimizi ayrıştırıp kodunu çözebilen bir audio/mpeg oluşturuyoruz; birkaç başka tür vardır.

Anormal Dalga Biçimleri

Birazdan koda geri döneceğiz. Ancak şimdi, az önce eklediğimiz dosyayı, özellikle de dosyanın sonuna daha yakından bakalım. Aşağıda, sintel_0.mp3 kanalından her iki kanaldaki ortalaması alınan son 3.000 örneğin grafiği verilmiştir. Kırmızı çizgideki her piksel, [-1.0, 1.0] aralığındaki bir kayan nokta örneğidir.

sintel_0.mp3 sonu

O kadar sıfır (sessiz) örnek neden oluyor! Bunlar, aslında kodlama sırasında ortaya çıkan sıkıştırma kusurlarından kaynaklanır. Neredeyse her kodlayıcı, bir tür dolgu ekler. Bu durumda LAME, dosyanın sonuna tam olarak 576 dolgu örneği eklemiştir.

Sondaki dolguya ek olarak, her dosyanın başına dolgu da eklenmiştir. sintel_1.mp3 kanalına göz atarsak ön tarafta dolgunun 576 örneğini daha görürüz. Dolgu miktarı kodlayıcıya ve içeriğe göre değişir ancak her bir dosyaya eklenen metadata değerine göre tam değerleri belirleriz.

sintel_1.mp3 başlangıcı

sintel_1.mp3 başlangıcı

Her dosyanın başındaki ve sonundaki sessiz bölümler, önceki demoda segmentler arasında aksamalara neden olur. Boşluksuz oynatma için bu sessiz bölümleri kaldırmamız gerekir. Neyse ki MediaSource ile bu işlemi kolayca yapabilirsiniz. Aşağıda, bu sessizliği kaldırmak için onAudioLoaded() yöntemimizi bir ek penceresi ve bir zaman damgası ofseti kullanacak şekilde değiştireceğiz.

Örnek Kod

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

Sorunsuz Bir Dalga Biçimi

Ekleme pencerelerimizi uyguladıktan sonra dalga formuna bir kez daha bakarak yepyeni kodumuzun neleri yaptığına bakalım. Aşağıda, sintel_0.mp3 metninin sonundaki sessiz bölümün (kırmızı) ve sintel_1.mp3 ifadesinin başındaki sessiz bölümün (mavi) kaldırıldığını görebilirsiniz; Böylece segmentler arasında sorunsuz bir geçiş elde edebiliriz.

sintel_0.mp3 ile sintel_1.mp3&#39;ün birleştirilmesi

Sonuç

Böylece, beş segmenti sorunsuz bir şekilde tek bir segmentte birleştirdik ve demomuzun sonuna geldik. Son olarak, onAudioLoaded() yöntemimizin kapsayıcılar veya codec'ler ile ilgili olmadığını fark etmiş olabilirsiniz. Bu, kapsayıcı veya codec türünden bağımsız olarak tüm bu tekniklerin çalışacağı anlamına gelir. Aşağıda, MP3 yerine DASH kullanıma hazır, parçalara ayrılmış orijinal MP4 demosunu tekrar oynatabilirsiniz.

Demo

Daha fazla bilgi edinmek isterseniz boşluksuz içerik oluşturma ve meta veri ayrıştırma konularında daha ayrıntılı bir inceleme için aşağıdaki eklere göz atabilirsiniz. Bu demoyu destekleyen kodu daha yakından incelemek için gapless.js sayfasını da ziyaret edebilirsiniz.

Okuduğunuz için teşekkür ederiz.

Ek A: Boşluksuz İçerik Oluşturma

Boşluksuz içerikler üretmekte zorlanabilirsiniz. Aşağıda, bu demoda kullanılan Sintel medyasının nasıl oluşturulduğu adım adım açıklanmıştır. Başlamak için Sintel'in kayıpsız FLAC müziğinin bir kopyasına ihtiyacınız var; SHA1 aşağıya eklenmiştir. Araçlar için FFmpeg, MP4Box, LAME ve afconvert özellikli bir OSX kurulumuna ihtiyacınız vardır.

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

İlk olarak, 1-Snow_Fight.flac parçasının ilk 31,5 saniyesini ayıracağız. Ayrıca, oynatma bittikten sonra tıklamaları önlemek için 28.saniyeden itibaren 2,5 saniyelik bir sönme geçişi eklemek istiyoruz. Aşağıdaki FFmpeg komut satırını kullanarak tüm bunları yapabilir ve sonuçları sintel.flac konumuna yerleştirebiliriz.

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

Daha sonra dosyayı, her biri 6,5 saniyelik 5 wave dosyasına böleceğiz; hemen hemen her kodlayıcı dalganın beslenmesini desteklediğinden wave, en kolay yöntemdir. Bu işlemi de FFmpeg ile tam olarak yapabiliriz. Sonrasında, şunları elde ederiz: sintel_0.wav, sintel_1.wav, sintel_2.wav, sintel_3.wav ve 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

Şimdi MP3 dosyalarını oluşturalım. LAME, boşluksuz içerik oluşturmak için çeşitli seçeneklere sahiptir. İçeriğin kontrolü sizdeyse segmentler arasında dolgunun tamamen önüne geçmek için tüm dosyaları toplu olarak kodlamayla --nogap kullanabilirsiniz. Ancak bu demoda bu dolguyu istediğimizden dalga dosyaları için standart, yüksek kaliteli VBR kodlamasını kullanacağız.

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

MP3 dosyalarını oluşturmak için tüm gerekli olan budur. Şimdi, parçalara ayrılmış MP4 dosyalarının oluşturulmasını ele alalım. Apple'ın iTunes için kontrol edilen medya oluşturma talimatlarını uygulayacağız. Aşağıda, wave'leri, önerilen parametreleri kullanarak bir MP4 kapsayıcısında AAC olarak kodlamadan önce, talimatlar doğrultusunda ara CAF dosyalarına dönüştüreceğiz.

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

Şu anda MediaSource ile kullanılabilmesi için önce uygun şekilde parçalamamız gereken birkaç M4A dosyamız var. Amacımız için bir saniyelik parça boyutu kullanacağız. MP4Box, parçalara ayrılmış her MP4'ü sintel_#_dashinit.mp4 olarak yazar. MPEG-DASH manifesti (sintel_#_dash.mpd) silinebilir.

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

İşte bu kadar. Artık aralıksız oynatma için gereken doğru meta verilere sahip, parçalara ayrılmış MP4 ve MP3 dosyalarımız bulunuyor. Meta verilerin nasıl göründüğüyle ilgili daha fazla bilgi için Ek B'ye bakın.

Ek B: Boşluksuz Meta Verileri Ayrıştırma

Tıpkı boşluksuz içerik oluşturmak gibi, boşluksuz meta verileri ayrıştırmak da standart bir depolama yöntemi olmadığından zor olabilir. Aşağıda, en yaygın kodlayıcılar olan LAME ve iTunes'un boşluksuz meta verilerini nasıl depoladığını ele alacağız. Bazı yardımcı yöntemler ve yukarıda kullanılan ParseGaplessData() için bir ana hat oluşturarak başlayalım.

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

Ayrıştırılması ve açıklanması en kolay yöntem olduğu için ilk olarak Apple'ın iTunes meta veri biçimini ele alacağız. iTunes (ve afconvert) MP3 ve M4A dosyalarında ASCII'de aşağıdaki gibi kısa bir bölüm yazın:

iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00

Bu metin, MP3 kapsayıcısındaki bir ID3 etiketinin içine ve MP4 kapsayıcısındaki bir meta veri atomunun içine yazılır. Amaçlarımız doğrultusunda ilk 0000000 jetonunu yok sayabiliriz. Sonraki üç jeton ön dolgu, bitiş dolgusu ve toplam dolgu olmayan örnek sayısıdır. Bunların her birini sesin örnek hızına böldüğünüzde her birinin süresini buluruz.

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

Öte yandan, açık kaynaklı MP3 kodlayıcıların çoğu boşluksuz meta verileri sessiz bir MPEG çerçevesinin içine yerleştirilmiş özel bir Xing üstbilgisinde depolar (sessiz olduğundan, Xing başlığını anlamayan kod çözücüler sadece sessizlik yapar). Maalesef bu etiket her zaman mevcut değildir ve birçok isteğe bağlı alanı içerir. Bu demoda, medya üzerinde kontrol sahibiyiz ancak pratikte, boşluksuz meta verilerin ne zaman kullanılabilir olduğunu bilmek için bazı ek kontrollerin yapılması gerekir.

Öncelikle toplam örnek sayısını ayrıştıracağız. Basitlik sağlaması için bunu Xing başlığından okuyacağız, ancak normal MPEG ses başlığından oluşturulabilir. Xing başlıkları, Xing veya Info etiketiyle işaretlenebilir. Bu etiketten tam olarak 4 bayt sonra, dosyadaki toplam kare sayısını temsil eden 32 bit olur; bu değerin kare başına örnek sayısıyla çarpılması, bize dosyadaki toplam örnek sayısını verir.

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

Toplam örnek sayısına sahip olduğumuza göre dolgu örneklerinin sayısını okumaya geçebiliriz. Kodlayıcınıza bağlı olarak bu, Xing başlığı içine yerleştirilmiş bir LAME veya Lavf etiketi altında yazılabilir. Bu başlıktan tam olarak 17 bayt sonra, her biri 12 bitlik ön ve son dolgusunu temsil eden 3 bayt bulunur.

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

Bu sayede, boşluksuz içeriklerin büyük çoğunluğunu ayrıştırmak için eksiksiz bir işleve sahibiz. Yine de uç durumlar elbette çok fazladır. Bu nedenle, üretimde benzer kodu kullanmadan önce dikkatli olmanız önerilir.

Ek C: Çöp Toplama Hakkında

SourceBuffer örneklerine ait bellek içerik türüne, platforma özel sınırlara ve geçerli oynatma konumuna göre aktif olarak çöp toplanır. Chrome'da bellek önceden oynatılmış arabelleklerden geri alınır. Bununla birlikte, bellek kullanımı platforma özgü sınırları aşarsa, oynatılmayan arabelleklerdeki bellek kaldırılır.

Geri yüklenen bellek nedeniyle oynatmayla ilgili zaman çizelgesinde boş bir boşluk olduğunda, boşluk yeterince küçükse bozulabilir ya da boşluk çok büyükse tamamen durdurulabilir. İkisi de mükemmel bir kullanıcı deneyimi değildir. Bu nedenle, bir seferde çok fazla veri eklemekten kaçınmak ve artık gerekli olmayan aralıkları medya zaman çizelgesinden manuel olarak kaldırmak önemlidir.

Aralıklar, her bir SourceBuffer üzerinde remove() yöntemiyle kaldırılabilir; Saniye cinsinden [start, end] aralığındadır. appendBuffer() özelliğine benzer şekilde, her remove() tamamlandığında bir updateend etkinliği tetiklenir. Diğer kaldırma veya ekleme işlemleri, etkinlik tetiklenene kadar yayınlanmamalıdır.

Masaüstü Chrome'da tek seferde yaklaşık 12 megabayt ses içeriği ve 150 megabayt video içeriğini bellekte tutabilirsiniz. Farklı tarayıcılarda veya platformlarda bu değerlere güvenmemelisiniz; Örneğin, bunlar kesinlikle mobil cihazları temsil etmez.

Çöp toplama işlemi yalnızca SourceBuffers ürününe eklenen verileri etkiler; JavaScript değişkenlerinde arabelleğe alınmış ne kadar veri tutabileceğiniz konusunda bir sınırlama yoktur. Ayrıca, gerekirse aynı verileri aynı konuma yeniden ekleyebilirsiniz.