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

Boris Smus
Boris Smus

Giriş

Ses, multimedya deneyimlerini ilgi çekici kılan en önemli unsurlardan biridir. Filmi sessiz izlemeyi denediyseniz bunu fark etmişsinizdir.

Oyunlar için de durum farklı değil. Video oyunlarıyla ilgili en güzel anılarımın müzik ve ses efektleriyle ilgili olduğunu söyleyebilirim. Şimdi en sevdiklerimi çaldıktan 20 yıl sonra bile Koji Kondo'nun Zelda bestelerini ve Matt Uelmen'in atmosferik Diablo müziğini hâlâ aklımdan çıkaramıyorum. Aynı akılda kalıcılık, Warcraft'tan anında tanınabilen birim tıklama yanıtları ve Nintendo'nun klasiklerinden örnekler gibi ses efektleri için de geçerli.

Oyun sesi, bazı ilginç zorluklar içeriyor. Tasarımcıların, inandırıcı oyun müzikleri oluşturmak için oyuncuların kendilerini bulabileceği, tahmin edilemeyen oyun durumuna uyum sağlaması gerekir. Uygulamada, oyunun bazı bölümleri bilinmeyen bir süre boyunca devam edebilir, sesler çevreyle etkileşime girebilir ve oda efektleri ile göreceli ses konumlandırması gibi karmaşık şekillerde karıştırılabilir. Son olarak, aynı anda çok sayıda ses çalınabilir. Bu seslerin birlikte iyi duyulması ve performans cezası uygulanmadan oluşturulması gerekir.

Web'de Oyun Sesi

Basit oyunlar için <audio> etiketini kullanmak yeterli olabilir. Ancak birçok tarayıcı, ses sorunlarına ve yüksek gecikmeye neden olan kötü uygulamalar sunar. Tedarikçiler bu uygulamaları geliştirmek için yoğun çalıştığından, bunun geçici bir sorun olduğunu düşünebiliriz. <audio> etiketinin durumunu incelemek için areweplayingyet.org adresindeki test paketini kullanabilirsiniz.

Ancak <audio> etiketi spesifikasyonunu daha ayrıntılı incelediğinizde, bu etiketle yapılamayacak çok şey olduğu anlaşılır. Bu durum, medya oynatma için tasarlandığından şaşırtıcı değildir. Sınırlılıklardan bazıları şunlardır:

  • Ses sinyaline filtre uygulayamazsınız
  • Ham PCM verilerine erişme imkanı yoktur.
  • Kaynakların ve dinleyicilerin konumu ve yönü gibi kavramlar yoktur.
  • Ayrıntılı zamanlama yoktur.

Makalenin geri kalanında, Web Audio API ile yazılmış oyun sesleri bağlamında bu konuların bazılarını ayrıntılı olarak ele alacağım. Bu API'ye kısa bir giriş için başlangıç eğiticisine göz atın.

Arka plan müziği

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

Döngü kısa ve tahmin edilebilirse çok can sıkıcı olabilir. Bir oyuncu bir alanda veya seviyede takılı kaldıysa ve arka planda sürekli olarak aynı kesit çalıyorsa daha fazla hayal kırıklığına yol açmamak için parçayı kademeli olarak azaltmak yararlı olabilir. Diğer bir strateji de oyunun bağlamına bağlı olarak çeşitli yoğunluklarda seslerin kademeli olarak birbirine geçiş yaptığı bir mix oluşturmaktır.

Örneğin, oyuncunuz destansı bir canavar savaşının olduğu bir bölgedeyse atmosferden tahmine ve yoğunluğa kadar duygusal çeşitlilik gösteren çeşitli mix'leriniz olabilir. Müzik sentez yazılımları, genellikle bir parçaya ait farklı mix'leri (aynı uzunlukta) dışa aktarma işleminde kullanılacak parça grubunu seçerek dışa aktarmanıza olanak sağlar. Böylece, parçalar arasında geçiş yaparken rahatsız edici geçişler yaşamazsınız.

Garageband

Ardından, Web Audio API'yi kullanarak bu örneklerin tümünü XHR üzerinden BufferLoader sınıfı gibi bir şey kullanarak içe aktarabilirsiniz (bu konu Web Audio API tanıtım makalesinde ayrıntılı olarak ele alınmıştır. Seslerin yüklenmesi zaman aldığından oyunda kullanılan öğeler sayfa yüklenirken, seviyenin başında veya oyuncu oyun oynarken kademeli olarak yüklenmelidir.

Ardından, 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üde oynatabilirsiniz. Hepsi aynı uzunlukta olduğundan Web Audio API, bunların hizalı kalmasını sağlar. Karakter son patron savaşına yaklaştıkça veya uzaklaştıkça oyun, aşağıdaki gibi bir kazanç miktarı algoritması kullanarak zincirdeki ilgili düğümlerin her biri için 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 çalınır 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 yayınlamaya uygun olduğu için arka plan müzikleri için <audio> etiketini kullanır. Artık <audio> etiketindeki içeriği Web Audio bağlamına getirebilirsiniz.

<audio> etiketi, akış içeriğiyle çalışabildiğinden bu teknik yararlı olabilir. Böylece, içeriğin tamamını indirmek yerine arka plan müziğini hemen çalabilirsiniz. Akışı Web Audio API'sine getirerek akışı değiştirebilir veya analiz edebilirsiniz. Aşağıdaki örnekte, <audio> etiketi aracılığıyla çalınan müziğe düşük geçiş filtresi uygulanmaktadır:

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> etiketini Web Audio API ile entegre etme hakkında daha ayrıntılı bilgi 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 çalar. Ancak arka plan müziği gibi ses efektleri de çok çabuk can sıkıcı hale gelebilir. Bunu önlemek için genellikle benzer ancak farklı seslerden oluşan bir havuz bulundurmak faydalıdır. Bu durum, ayak izi örneklerinin hafif varyasyonlarından Warcraft serisinde görülen, birimlere yapılan tıklamalara göre önemli farklılıklara kadar değişiklik gösterebilir.

Oyunlardaki ses efektlerinin bir diğer önemli özelliği de aynı anda çok sayıda ses efektinin bulunabilmesidir. Birden fazla aktör tarafından makineli tüfekle ateşlenen bir silahlı çatışmanın ortasında olduğunuzu hayal edin. Her makineli tüfek saniyede birçok kez ateş eder ve aynı anda onlarca ses efektinin çalınmasına neden olur. Web Audio API'nin en iyi performans gösterdiği alanlardan biri, birden fazla kaynaktan gelen sesleri tam olarak zamanlanmış şekilde aynı anda oynatmaktır.

Aşağıdaki örnekte, oynatma süresi kademeli olarak ayarlanmış birden fazla ses kaynağı oluşturarak birden fazla ayrı mermi örneğinden makineli tüfek sesi oluşturulmaktadır.

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

Oyununuzdaki tüm makineli tüfeklerin sesi tam olarak böyle olsaydı bu oldukça sıkıcı olurdu. Elbette bunlar, hedefe olan mesafeye ve göreceli konuma göre (bu konu hakkında daha sonra daha fazla bilgi verilecektir) sese göre değişir ancak bu bile yeterli olmayabilir. Neyse ki Web Audio API, yukarıdaki örneği iki şekilde kolayca değiştirmenizi sağlar:

  1. Mermilerin ateşlenmesi arasındaki zamanda küçük bir kayma
  2. Gerçek dünyadaki rastgeleliği daha iyi simüle etmek için her bir örneğin playbackRate değerini değiştirerek (ayrıca perdeyi de değiştirerek).

Bu tekniklerin gerçek hayatta nasıl kullanıldığını gösteren daha gerçekçi bir örnek için rastgele örnekleme kullanan ve top çarpışması sesinin daha ilgi çekici olması için playbackRate değerini değiştiren bilardo masası demosuna göz atın.

3D konumsal ses

Oyunlar genellikle 2D veya 3D olarak bazı geometrik özelliklere sahip bir dünyada geçer. Bu durumda, stereo konumlandırılmış 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 örneğin anlaşılır olması için stereo hoparlörleriniz (tercihen kulaklık) olduğundan emin olmanız gerekir.

Yukarıdaki örnekte, kanvasın ortasında bir dinleyici (kişi simgesi) vardır ve fare, kaynağın (hoparlör simgesi) konumunu etkiler. Yukarıda, bu tür bir etki elde etmek için AudioPannerNode'un kullanıldığı basit bir örnek verilmiştir. Yukarıdaki örnekte temel fikir, ses kaynağının konumunu ayarlayarak fare hareketine yanıt 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 uzamsallaştırma işlemiyle ilgili bilmeniz gerekenler:

  • Dinleyici varsayılan olarak orijindedir (0, 0, 0).
  • Web Audio konum API'leri birimsizdir. Bu nedenle, demonun sesini daha iyi hale getirmek için bir çarpan ekledim.
  • Web Audio, y yukarı olan Kartezyen koordinatları kullanır (çoğu bilgisayar grafik sistemiyle tam tersi). Bu nedenle, yukarıdaki snippet'te y eksenini değiştiriyorum.

Gelişmiş: ses konileri

Konumlandırma modeli, büyük ölçüde OpenAL'e dayanan çok güçlü ve oldukça gelişmiş bir modeldir. Daha ayrıntılı bilgi için yukarıdaki bağlantıdaki spesifikasyonun 3. ve 4. bölümlerine bakın.

Konum modeli

Web Audio API bağlamına bağlı tek bir AudioListener vardır. Bu dinleyici, konum ve yön aracılığıyla uzayda yapılandırılabilir. Her kaynak, giriş sesini üç boyutlu hale getiren bir AudioPannerNode üzerinden iletilebilir. Kaydırma düğümünde konum ve yönün yanı sıra mesafe ve yön modeli bulunur.

Mesafe modeli, kaynağa olan yakınlığa bağlı olarak kazanç miktarını belirtir. Yönsel model ise dinleyicinin iç koni içinde, iç ve dış koni arasında veya dış koni dışında olması durumunda kazanç miktarını (genellikle negatif) belirleyen bir 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 olsa da bu model kolayca üçüncü boyuta genelleştirilir. 3D olarak uzamsallaştırılmış ses örneği için bu konumsal örnek bölümüne bakın. Web Audio ses modeli, konuma ek olarak isteğe bağlı olarak Doppler kaymaları için hızı da içerir. Bu örnekte Doppler etkisi daha ayrıntılı bir şekilde gösterilmektedir.

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

Oda efektleri ve filtreleri

Gerçekte sesin algılanma şekli büyük ölçüde sesin duyulduğu odaya bağlıdır. Bodrum katta, büyük bir açık salona kıyasla gıcırdama yapan aynı kapının sesi çok daha farklı olacaktır. Üretim değeri yüksek oyunlarda bu efektler taklit edilmelidir. Çünkü her ortam için ayrı bir örnek grubu oluşturmak çok pahalı ve daha fazla öğe ve daha fazla oyun verisi anlamına gelir.

Yani ham ses ile gerçekte kulağa nasıl geldiğini ifade eden ses terimi, dürtü yanıtı olarak adlandırılır. Bu impuls yanıtları zahmetli bir şekilde kaydedilebilir. Hatta size kolaylık sağlamak için önceden kaydedilmiş bu impuls yanıt dosyalarının çoğunu barındıran siteler vardır (ses olarak depolanır).

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

Amacımız açısından daha da önemlisi, Web Audio API'nin ConvolverNode'u kullanarak bu dürtü yanıtlarını seslerimize uygulamanın kolay bir yolunu sunmasıdır.

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

Web Audio API spesifikasyonu sayfasındaki oda efektleri demosuna ve harika bir caz standardının kuru (ham) ve ıslak (örnekleyici aracılığıyla işlenmiş) miksajı üzerinde kontrol sahibi olmanızı sağlayan bu örneğe de göz atın.

Son geri sayım

Bir oyun oluşturdunuz, konumsal sesinizi yapılandırdınız ve şimdi grafınızda aynı anda çalınan çok sayıda AudioNode'unuz var. Güzel, ancak dikkate alınması gereken bir nokta daha var:

Birden fazla ses normalleştirme olmadan birbirinin üzerine yükselir. Bu nedenle hoparlörünüzün yeterlilik eşiğini aştığı bir durumda olabilirsiniz. Tuval sınırlarını aşan resimler gibi, ses dalga biçimi maksimum eşiğini aşarsa sesler de kırpılabilir ve belirgin bir bozulma meydana gelir. Dalga biçimi şöyle görünür:

Kırpma

Aşağıda, kırpma işleminin kullanıldığı gerçek bir örnek verilmiştir. Dalga biçimi kötü görünüyor:

Kırpma

Yukarıdaki gibi sert bozulmaları veya tam tersine, dinleyicilerinizi sesi yükseltmeye zorlayan aşırı bastırılmış miksleri dinlemeniz önemlidir. Böyle bir durumdaysanız bu sorunu düzeltmeniz gerekir.

Kırpma algılama

Teknik açıdan bakıldığında, kırpma, herhangi bir kanaldaki sinyalin değeri geçerli aralığı (-1 ile 1 arasında) aştığında gerçekleşir. Bu durum tespit edildiğinde, bunun gerçekleştiğine dair görsel geri bildirim vermek faydalı olur. Bunu güvenilir bir şekilde yapmak için grafiğinize bir JavaScriptAudioNode ekleyin. Ses grafiği aşağıdaki gibi ayarlanır:

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

Aşağıdaki processAudio işleyicisinde de kırpma 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, ölçümün alternatif bir uygulaması, requestAnimationFrame tarafından belirlenen oluşturma zamanında ses grafiğinde getByteFrequencyData için bir RealtimeAnalyserNode anketi yapabilir. Bu yaklaşım daha verimlidir ancak oluşturma işlemi saniyede en fazla 60 kez gerçekleşirken ses sinyali çok daha hızlı değiştiğinden sinyalin çoğunu (klip olabileceği yerler dahil) kaçırır.

Klip algılama çok önemli olduğu için gelecekte yerleşik bir MeterNode Web Audio API düğümü görebiliriz.

Kırpma işlemini önleme

Ana AudioGainNode'daki kazancı ayarlayarak, sesinizi kırpmayı önleyecek bir seviyeye indirebilirsiniz. Ancak pratikte, oyununuzda çalınan sesler çok çeşitli faktörlere bağlı olabileceğinden, tüm durumlar için kırpmayı önleyen ana kazanç değerine karar vermek zor olabilir. Genel olarak, en kötü senaryoyu öngörebilmek için kazançlar üzerinde değişiklik yapmalısınız, ancak bu iş bilimden çok sanattır.

Biraz şeker ekleyin

Sıkıştırıcılar, müzik ve oyun prodüksiyonunda sinyali yumuşatmak ve genel sinyaldeki ani artışları kontrol etmek için yaygın olarak kullanılır. Bu işlev, Web Audio dünyasında DynamicsCompressorNode aracılığıyla kullanılabilir. DynamicsCompressorNode, ses grafiğinize eklenebilir. Böylece daha yüksek, daha zengin ve daha dolgun bir ses elde edebilir, ayrıca ses kırpmaya yardımcı olabilirsiniz. Spesifikasyonu doğrudan alıntılayan bu düğüm

Dinamik sıkıştır DinahMoe Labs'in Plink uygulaması, çalınan seslerin tamamen size ve diğer katılımcılara bağlı olması nedeniyle bu duruma mükemmel bir örnektir. Kompresörler, zaten "tam olarak gerektiği gibi" ayarlanmış ve titizlikle kontrol edilmiş parçalarla uğraştığınız nadir durumlar dışında çoğu durumda faydalıdır.

Bunu uygulamak için ses grafiğinize genellikle hedeften önceki son düğüm olarak bir DynamicsCompressorNode eklemeniz yeterlidir:

// 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 bilgi için bu Wikipedia makalesine göz atın.

Özetlemek gerekirse kırpma işlemini dikkatli bir şekilde dinleyin ve bir ana kazanç düğümü ekleyerek işlemi önleyin. Ardından, dinamik sıkıştırıcı düğümü kullanarak tüm miksi sıkılaştırın. Ses grafiğiniz şöyle görünebilir:

Kesin sonuç

Sonuç

Web Audio API'yi kullanarak oyun sesleri geliştirmenin en önemli yönlerini ele aldık. Bu tekniklerle, doğrudan tarayıcınızda gerçekten ilgi çekici ses deneyimleri oluşturabilirsiniz. Sohbet oturumunu kapatmadan önce size tarayıcıya özgü bir ipucu vermek istiyorum: Sekmeniz sayfa görünürlüğü API'sini kullanarak arka plana giderse 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 giriş niteliğinde olan başlangıç makalesine göz atın. Sorularınız varsa Web Audio SSS bölümünde yanıtlarını bulabilirsiniz. Son olarak, başka sorularınız varsa Stack Overflow'da web-audio etiketini kullanarak sorun.

Sohbet oturumunu sonlandırmadan önce, Web Audio API'nin günümüzde gerçek oyunlarda kullanıldığı bazı harika örnekleri paylaşmak isterim: