Web Audio API'sı ile oyun sesi geliştirme

Boris Smus
Boris Smus

Giriş

Ses, multimedya deneyimlerini bu kadar cazip hale getiren önemli bir unsurdur. Sesi kapalı bir film izlemeyi denediyseniz muhtemelen bunu fark etmişsinizdir.

Oyunlar da istisna değil. En sevdiğim video oyunu anılarım müzik ve ses efektleriyle ilgili. Favorilerimi çaldıktan yaklaşık yirmi yıl sonra Koji Kondo'nun Zelda kompozisyonlarını ve Matt Uelmen'in atmosferik Diablo film müziklerini hâlâ aklımdan çıkaramıyorum. Aynı ilgi çekicilik, Warcraft'ın anında fark edebileceğiniz birim tıklama yanıtları ve Nintendo'nun klasiklerinden örnekler gibi ses efektleri için de geçerlidir.

Oyun sesleri ilgi çekici meydan okumalar sunuyor. İkna edici oyun müzikleri oluşturmak için tasarımcıların bir oyuncunun içinde bulduğu öngörülemez oyun durumuna uyum sağlaması gerekir. Pratikte, oyunun bazı kısımları bilinmeyen bir süre boyunca devam edebilir, sesler ortamla etkileşime girebilir ve oda efektleri ile göreli ses konumlandırma gibi karmaşık şekillerde karıştırılabilir. Son olarak, aynı anda çok sayıda ses çalınabilir. Bunların tamamının, performans cezalarına yol açmadan birlikte iyi ses çıkarması ve oluşturulması gerekir.

Web'de Oyun Sesi

Basit oyunlarda <audio> etiketinin kullanılması yeterli olabilir. Ancak birçok tarayıcı kötü uygulamalar sunar, bu da ses arızalarına ve yüksek gecikmeye yol açar. Tedarikçiler kendi uygulamalarını iyileştirmek için çok çalıştığından bunun geçici bir sorun olduğunu umuyoruz. <audio> etiketinin durumuna genel bakış için areweplayingyet.org adresinde güzel bir test paketi bulunmaktadır.

Bununla birlikte, <audio> etiketi spesifikasyonu ayrıntılı olarak incelendiğinde, bu etiket medya oynatma için tasarlandığından şaşırtıcı olmayan birçok şey olduğu ortaya çıkar. Sınırlamalardan bazıları şunlardır:

  • Ses sinyaline filtre uygulama özelliği yok
  • Ham PCM verilerine erişim imkanı yok
  • Kaynakların ve dinleyicilerin konumu ve yönü ile ilgili bir kavram yoktur
  • Hassas zamanlama yoktur.

Makalenin geri kalanında, bu konulardan bazılarını Web Audio API'sı ile yazılmış oyun sesi bağlamında ayrıntılı olarak inceleyeceğiz. Bu API ile ilgili kısa bir giriş için başlangıç eğiticisine göz atın.

Arka plan müziği

Oyunlarda arka planda müzik genellikle döngü halinde çalınır.

Döngünüzün kısa ve öngörülebilir olması çok can sıkıcı olabilir. Oyuncu bir alanda veya seviyede takılı kaldıysa ve aynı örnek arka planda sürekli olarak oynatılıyorsa daha fazla hayal kırıklığı yaşamamak için parçayı kademeli olarak karartmak faydalı olabilir. Diğer bir strateji, oyunun bağlamına göre kademeli olarak birbirine geçen çeşitli yoğunluklardan oluşan karışımlara sahip olmaktır.

Örneğin, oyuncunuz destansı bir bölüm sonu canavarının olduğu bir bölgedeyse duygusal aralıktan yoğunluğa kadar değişiklik gösteren çeşitli mix'leriniz olabilir. Müzik sentezi yazılımı, genellikle dışa aktarılacak parça grubunu seçerek bir parçadan aynı uzunluktaki çeşitli mix'leri dışa aktarmanıza olanak tanır. Bu şekilde bir miktar iç tutarlılık elde edersiniz ve bir kanaldan diğerine geçerken sarsıcı geçişlerden kaçınırsınız.

Garaj Bandı

Daha sonra, Web Audio API'sını kullanarak, XHR üzerinden BufferLoader sınıfı gibi bir şey kullanarak tüm bu örnekleri içe aktarabilirsiniz (Bu, Web Audio API'sının giriş makalesinde ayrıntılı olarak ele alınmıştır. Seslerin yüklenmesi zaman alır. Bu nedenle, oyunda kullanılan öğeler sayfa yüklenirken, seviyenin başında veya oyuncu oyun oynarken aşamalı olarak yüklenmelidir.

Daha sonra, her düğüm için bir kaynak ve her kaynak için bir kazanç düğümü oluşturup grafiği bağlarsınız.

Bunu yaptıktan sonra, bu kaynakların tümünü aynı anda döngü halinde oynatabilirsiniz. Hepsi aynı uzunlukta oldukları için Web Audio API'sı bunların hizalı kalacağını garanti eder. Karakter, son canavar savaşına yaklaştıkça veya ilerledikçe oyun aşağıdaki gibi bir kazanç miktarı algoritması kullanarak zincirdeki ilgili düğümlerin her birinin kazanç değerlerini değiştirebilir:

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

Yukarıdaki yaklaşımda, iki kaynak aynı anda oynar ve eşit güç eğrileri kullanarak (girişte açıklandığı gibi) aralarında geçiş yaparız.

Günümüzde birçok oyun geliştirici, içerik akışı için uygun olduğundan arka plan müziklerinde <audio> etiketini kullanıyor. Artık <audio> etiketindeki içeriği Web Audio bağlamına taşıyabilirsiniz.

<audio> etiketi akışlı içerikle çalışabileceğinden bu teknik yararlı olabilir. Böylece, tüm içeriğin indirilmesini beklemek zorunda kalmadan arka plan müziğini anında çalabilirsiniz. Akışı Web Audio API'sına getirerek akışı değiştirebilir veya analiz edebilirsiniz. Aşağıdaki örnek, <audio> etiketiyle çalınan müziğe bir düşük geçiş filtresi uygular:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

<audio> etiketinin Web Audio API'sıyla entegre edilmesi hakkında daha kapsamlı bir açıklama için bu kısa makaleyi inceleyin.

Ses efektleri

Oyunlar genellikle kullanıcı girişine veya oyun durumundaki değişikliklere yanıt olarak ses efektleri oynatır. Ancak arka plan müziği gibi ses efektleri de çok hızlı rahatsız edici olabilir. Bunu önlemek için benzer ama farklı seslerden oluşan bir havuz oluşturmak çoğu zaman işe yarar. Bu, birim tıklamalarına yanıt olarak Warcraft serisinde görüldüğü gibi ayak izi örneklerinin hafif varyasyonlarından önemli farklılıklara kadar değişiklik gösterebilir.

Oyunlardaki ses efektlerinin bir başka önemli özelliği de aynı anda birçok ses efekti kullanabilmenizdir. Çok sayıda oyuncunun makineli tüfeklerle vurduğu bir silahlı çatışmanın ortasında olduğunuzu hayal edin. Her makineli tüfek saniyede birkaç kez ateş ederek aynı anda onlarca ses efektinin çalınmasına neden olur. Birden fazla, hassas şekilde zamanlanmış kaynaktan ses oynatmak Web Audio API'sının gerçekten başarılı olduğu alanlardan biridir.

Aşağıdaki örnekte, oynatma sayısı kademeli olarak ayarlanmış birden fazla ses kaynağı oluşturularak birden fazla ayrı mermi örneğinden bir makineli tüfek mermisi oluşturmaktadır.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

Oyundaki makinelerin tümü tam olarak böyle sesler çıkarsa ama çok sıkıcı olurdu. Hedeften uzaklığa ve göreceli konuma göre elbette sese göre değişiklik gösterebilir (bu konuda daha sonra bahsedilecek) ancak bu rakam bile yeterli olmayabilir. Neyse ki Web Audio API, yukarıdaki örneği iki şekilde kolayca düzenlemenizi sağlar:

  1. Kurşunlar arasında kısa süreli geçiş
  2. Gerçek dünyanın rastgeleliğini daha iyi simüle etmek için her bir örneğin oynatma Hızı'nı değiştirerek (ayrıca ses perdesini de değiştirebilirsiniz).

Bu tekniklerin daha gerçek hayattan bir örneği için rastgele örnekleme kullanan ve daha ilginç bir top çarpışma sesi için oynatma hızını değiştiren Havuz Masası demosuna göz atın.

3D konumsal ses

Oyunlar genellikle bazı geometrik özelliklere sahip bir dünyada 2D veya 3D olarak geçer. Böyle bir durumda, stereo olarak yerleştirilmiş ses, deneyimin sürükleyiciliğini büyük ölçüde artırabilir. Neyse ki Web Audio API, kullanımı oldukça basit olan yerleşik donanım hızlandırmalı konumsal ses özellikleriyle birlikte gelir. Bu arada, aşağıdaki örnekte anlatılanların anlamlı olması için stereo hoparlörler (tercihen kulaklık) satın aldığınızdan emin olmalısınız.

Yukarıdaki örnekte, tuvalin ortasında bir işleyici (kişi simgesi) bulunmaktadır ve fare, kaynağın konumunu (hoparlör simgesi) etkiler. Yukarıda, bu tür bir efekt elde etmek için AudioPannerNode'un kullanılmasına ilişkin basit bir örnek verilmiştir. Yukarıdaki örneğin temel fikri, ses kaynağının konumunu aşağıdaki gibi ayarlayarak fare hareketine karşılık vermektir:

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

Web Audio'nun üç boyutlulaştırma yaklaşımı hakkında bilinmesi gerekenler:

  • İşleyici varsayılan olarak kaynaktadır (0, 0, 0).
  • Web Audio konumlandırma API'leri birimsiz olduğundan, demonun sesini daha iyi hale getirmek için bir çarpan oluşturdum.
  • Web Sesi, y-up kartezyen koordinatlarını (çoğu bilgisayar grafik sisteminin tersi) kullanır. Bu nedenle yukarıdaki snippet'te yer alan y eksenini,

Gelişmiş: ses konileri

Konum modeli, büyük ölçüde OpenAL'a dayanan çok güçlü ve son derece gelişmiş bir modeldir. Daha ayrıntılı bilgi için yukarıda bağlantısı verilen spesifikasyonun 3. ve 4. bölümlerine bakın.

Konum modeli

Web Audio API içeriğine bağlı tek bir AudioListener vardır. Bu öğe, alan içinde konum ve yön aracılığıyla yapılandırılabilir. Her kaynak, giriş sesini üç boyutlu hale getiren bir AudioPannerNode üzerinden iletilebilir. Kaydırma düğümü, konum ve yönün yanı sıra mesafe ve yön modeline sahiptir.

Mesafe modeli, kaynağa yakınlığa bağlı olarak kazanç miktarını belirtir. Yön modeli ise, dinleyicinin iç koninin içinde, iç ve dış koninin arasında veya dış koninin dışında olması durumunda kazanım miktarını (genellikle negatif) belirleyen iç ve dış koni belirterek yapılandırılabilir.

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

Örneğim 2D olmasına rağmen, bu model kolayca üçüncü boyuta genellenebilir. 3D olarak üç boyutlu hâle getirilen sesin bir örneğini görmek için bu konumsal örneğe bakın. Web Audio ses modeli, konuma ek olarak isteğe bağlı olarak doppler kaymaları için de hız içerir. Bu örnekte, doppler etkisini daha ayrıntılı bir şekilde görebilirsiniz.

Bu konu hakkında daha fazla bilgi için [konumsal ses ve WebGL'yi karıştırma][webgl] ile ilgili bu ayrıntılı eğiticiyi okuyun.

Oda efektleri ve filtreleri

Gerçekte, sesin algılanma şekli büyük ölçüde sesin duyulduğu odaya bağlıdır. Aynı gürültülü kapı, büyük bir açık salondan çok farklı duyulur. Her ortam için ayrı örnek grubu oluşturmak aşırı derecede pahalı olduğundan ve daha da fazla öğe ile daha fazla oyun verisine yol açacağından üretim değeri yüksek oyunlar bu efektleri taklit etmek isteyecektir.

Daha genel konuşmak gerekirse, ham ses ile gerçekte ortaya çıkan ses arasındaki fark, dürtüsel yanıt olarak adlandırılır. Bu dürtü tepkileri titizlikle kaydedilebilir ve hatta size kolaylık olması için önceden kaydedilmiş bu dürtü yanıt dosyalarının birçoğunu (ses olarak depolanan) barındıran siteler vardır.

Belirli bir ortamdan anlık yanıtların nasıl oluşturulabileceği hakkında çok daha fazla bilgi edinmek için Web Audio API spesifikasyonunun Convolution bölümündeki "Recording Setup" (Kayıt Kurulumu) bölümünü okuyun.

Amaçlarımız açısından daha da önemlisi, Web Audio API'si, ConvolverNode'u kullanarak bu dürtüleri seslerimize uygulamanın kolay bir yolunu sağlar.

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

Ayrıca, Web Audio API spesifikasyon sayfasındaki oda efektlerinin tanıtımının yanı sıra mükemmel bir Caz standardının kuru (ham) ve ıslak (konsolver ile işlenmiş) karışımını kontrol etmenizi sağlayan bu örneğin görebilirsiniz.

Son geri sayım

Bir oyun oluşturdunuz, konumlandırmalı sesi yapılandırdınız ve şimdi grafiğinizde, tümü eş zamanlı olarak oynatılan çok sayıda AudioNodes var. Harika, ancak dikkate almanız gereken bir nokta daha var:

Birden fazla ses normalleştirme olmadan üst üste yığılır. Bu nedenle kendinizi hoparlörünüzün kapasite eşiğini aştığınız bir durumda bulabilirsiniz. Tuval sınırlarını aşan görüntüler gibi, dalga formu maksimum eşiğini aşarsa sesler de kesilebilir ve bu da belirgin bir distorsiyona neden olabilir. Dalga formu aşağıdaki gibi görünür:

Kırpma

Kırpma işleminin gerçek bir örneğini burada görebilirsiniz. Dalga formu kötü görünüyor:

Kırpma

Yukarıdaki gibi sert distorsiyonları veya tersine, dinleyicilerinizi sesi yükseltmeye zorlayan aşırı dinlendirici mix'leri dinlemek önemlidir. Bu durumdaysanız mutlaka düzeltmeniz gerekir!

Kırpmayı algıla

Teknik açıdan bakıldığında, herhangi bir kanaldaki sinyal değeri geçerli aralığı, yani -1 ile 1 aralığını aştığında kırpma işlemi gerçekleşir. Böyle bir durum algılandıktan sonra, bu durumun meydana geldiğine dair görsel geri bildirim vermek yararlı olur. Bunu güvenilir bir şekilde yapmak için grafiğinize bir JavaScriptAudioNode yerleştirin. Ses grafiği şu şekilde oluşturulur:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

Kırpma işlemi şu processAudio işleyicide de algılanabilir:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

Genel olarak, performans nedeniyle JavaScriptAudioNode öğesini aşırı kullanmamaya dikkat edin. Bu durumda alternatif bir ölçüm uygulaması, oluşturma zamanında requestAnimationFrame ile belirlenen getByteFrequencyData için ses grafiğindeki bir RealtimeAnalyserNode'yi yoklayabilir. Bu yaklaşım daha verimlidir ancak sinyalin çoğunu (potansiyel olarak kliplerin kesildiği yerler dahil) kaçırır. Bunun nedeni, oluşturma işleminin saniyede en çok 60 kez gerçekleşirken ses sinyali çok daha hızlı bir şekilde değişmesidir.

Klip algılama çok önemli olduğundan, gelecekte dahili bir MeterNode Web Audio API düğümü görmemiz olasıdır.

Kırpmayı önleme

Ana AudioGainNode'un kazancını ayarlayarak mix'inizi kırpmayı önleyen bir seviyeye azaltabilirsiniz. Ancak pratikte, oyununuzdaki sesler çok çeşitli faktörlere bağlı olabileceğinden, tüm eyaletlerde atlamayı önleyen ana kazanç değerine karar vermek zor olabilir. Genel olarak, en kötü durumu öngörmek için kazançlarda değişiklik yapmanız gerekir. Ancak bu, bilimden çok sanattır.

Biraz şeker ekleyin

Sinyali yumuşatmak ve genel sinyaldeki sıçramaları kontrol etmek için müzik ve oyun prodüksiyonlarında yaygın olarak kompresör kullanılır. Bu işlev, Web Audio dünyasında DynamicsCompressorNode aracılığıyla kullanılabilir. Bu işlev daha yüksek, daha zengin ve daha zengin bir ses sağlamak ve kırpma konusunda yardımcı olmak için ses grafiğinize eklenebilir. Doğrudan spesifikasyondan alıntı olarak, bu düğüm

Dinamik sıkıştırma kullanmak, özellikle de daha önce de belirtildiği gibi hangi seslerin ne zaman çalınacağını tam olarak bilmediğiniz oyun ortamlarında, genellikle iyi bir fikirdir. DinahMoe laboratuvarlarından Plink buna mükemmel bir örnektir. Çünkü oynatılan sesler tamamen size ve diğer katılımcılara bağlıdır. Kompresör pek çok durumda faydalıdır. Nadir de olsa, kulağa "en uygun" ses verecek şekilde ayarlanmış titizlikle hazırlanmış parçalarla uğraşırsınız.

Bunu uygulamak, genellikle ses grafiğinize, genellikle hedeften önceki son düğüm olarak bir DynamicsFrequencyorNode eklemektir.

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

Dinamik sıkıştırma hakkında daha fazla ayrıntı için bu Wikipedia makalesi çok bilgilendiricidir.

Özetlemek gerekirse, kırpma işlemini dikkatle dinleyin ve bir ana kazanç düğümü ekleyerek bunu önleyin. Daha sonra, bir dinamik kompresör düğümü kullanarak tüm karışımı sıkılaştırın. Ses grafiğiniz şuna benzer şekilde görünebilir:

Kesin sonuç

Sonuç

Web Audio API'sını kullanarak oyunlarda ses geliştirmenin en önemli yönlerini ele aldık. Bu tekniklerle, doğrudan tarayıcınızda gerçekten etkileyici ses deneyimleri oluşturabilirsiniz. Oturumu kapatmadan önce size tarayıcıya özel bir ipucu vermek istiyorum: Sayfa görünürlüğü API'sini kullanarak sekmeniz arka plana gidiyorsa sesi duraklattığınızdan emin olun. Aksi takdirde, kullanıcınız için can sıkıcı bir deneyim oluşturabilirsiniz.

Web Audio hakkında daha fazla bilgi için daha tanıtıcı başlangıç makalesine göz atın ve sorunuz varsa web sesiyle ilgili SSS bölümünde önceden yanıtlanıp yanıtlanmadığına bakın. Son olarak, başka sorularınız varsa web-audio etiketini kullanarak Stack Overflow'da sorularınızı sorabilirsiniz.

Son olarak, bugünkü Web Audio API'sının gerçek oyunlarda bazı muhteşem kullanımlarından bahsetmek istiyorum: