Studium przypadku – JAM w Chrome

Jak stworzyliśmy audio rock

Oskar Eriksson
Oskar Eriksson

Wstęp

JAM with Chrome to internetowy projekt muzyczny stworzony przez Google. Dzięki JAM with Chrome ludzie z całego świata mogą tworzyć zespoły i słuchać muzyki w czasie rzeczywistym, korzystając z przeglądarki. Cieszymy się, że mogliśmy wziąć udział w tym projekcie DinahMoe. Naszym zadaniem było produkowanie muzyki do aplikacji oraz projektowanie i rozwijanie elementu muzycznego. Projektowanie obejmował 3 główne obszary: „muzyczną stację roboczą”, w tym odtwarzanie dźwięku w MIDI, kompilacje oprogramowania, efekty dźwiękowe, routing i miksowanie, silnik logiczny do interaktywnego sterowania muzyką w czasie rzeczywistym oraz komponent synchronizacji, który sprawia, że wszyscy gracze podczas sesji słyszą muzykę w tym samym czasie. Jest to warunek wstępny do wspólnego grania.

Aby osiągnąć najwyższy możliwy poziom autentyczności, dokładności i jakości dźwięku, zdecydowaliśmy się użyć interfejsu Web Audio API. W tym studium przypadku omówimy wybrane wyzwania i sposoby ich rozwiązania. Na stronie HTML5Rocks znajdziesz już wiele artykułów wprowadzających, które pomogą Ci zacząć korzystać z Web Audio.

Tworzenie niestandardowych efektów dźwiękowych

Interfejs Web Audio API zawiera wiele przydatnych efektów w specyfikacji, ale potrzebowaliśmy bardziej zaawansowanych efektów dla naszych instrumentów w JAM z Chrome. Na przykład w Web Audio jest natywny węzeł opóźnienia, ale jest wiele rodzajów opóźnień – takich jak opóźnienie stereo, ping-ponga czy slapback. Na szczęście wszystkie z tych elementów można utworzyć w Web Audio, korzystając z natywnych węzłów efektowych i z wykorzystaniem wyobraźni.

Chcieliśmy móc korzystać z węzłów natywnych i naszych własnych efektów niestandardowych w najbardziej przejrzysty sposób, dlatego zdecydowaliśmy się stworzyć format kodu, który to umożliwi. Węzły natywne w Web Audio używają metody łączenia węzłów, więc musieliśmy emulować to zachowanie. Tak wygląda podstawowa koncepcja:

var MyCustomNode = function(){
    this.input = audioContext.createGain();
    var output = audioContext.createGain();

    this.connect = function(target){
       output.connect(target);
    };
};

Dzięki temu wzorowi jesteśmy bardzo blisko węzłów natywnych. Zobaczmy, jak zostanie to wykorzystane.

//create a couple of native nodes and our custom node
var gain = audioContext.createGain(),
    customNode = new MyCustomNode(),
    anotherGain = audioContext.createGain();

//connect our custom node to the native nodes and send to the output
gain.connect(customNode.input);
customNode.connect(anotherGain);
anotherGain.connect(audioContext.destination);
Routing węzła niestandardowego

Jedyną różnicą między węzłem niestandardowym a węzłem natywnym jest to, że musimy połączyć się z właściwością wejściową węzłów niestandardowych. Na pewno są sposoby na obejście tego problemu, ale było to wystarczająco blisko do naszych celów. Ten wzorzec można dalej rozwijać, aby symulować metody rozłączenia natywnych węzłów audio, a także obsługiwać wejścia/wyjścia zdefiniowane przez użytkownika podczas łączenia itd. Zapoznaj się ze specyfikacją, aby dowiedzieć się, jakie możliwości dają węzły natywne.

Skoro mieliśmy już podstawowy wzór do tworzenia efektów niestandardowych, kolejnym krokiem było nadanie węzłowi niestandardowego zachowania. Przyjrzyjmy się węzłom opóźnienia slapback.

Sprzątnij sobie głowę

Opóźnienie slapbacku, czasami nazywane echo slapback, to efekt charakterystyczny dla różnych instrumentów, od wokalu lat 50. po gitary surfingowe. Efekt wykorzystuje przychodzący dźwięk i odtwarza jego kopię z niewielkim opóźnieniem wynoszącym około 75–250 milisekund. Dzięki temu masz wrażenie, że dźwięk jest cofany, a tym samym nazwa. Możemy utworzyć efekt w następujący sposób:

var SlapbackDelayNode = function(){
    //create the nodes we'll use
    this.input = audioContext.createGain();
    var output = audioContext.createGain(),
        delay = audioContext.createDelay(),
        feedback = audioContext.createGain(),
        wetLevel = audioContext.createGain();

    //set some decent values
    delay.delayTime.value = 0.15; //150 ms delay
    feedback.gain.value = 0.25;
    wetLevel.gain.value = 0.25;

    //set up the routing
    this.input.connect(delay);
    this.input.connect(output);
    delay.connect(feedback);
    delay.connect(wetLevel);
    feedback.connect(delay);
    wetLevel.connect(output);

    this.connect = function(target){
       output.connect(target);
    };
};
Wewnętrzne kierowanie węzła zwrotnego

Niektórzy z Was już zauważyli, że takie opóźnienie można wykorzystać z większym czasem oczekiwania i w efekcie w sposób zwykły monofoniczny. Oto przykład, w którym wykorzystano to opóźnienie, aby usłyszeć, jak brzmi.

Przekierowuję dźwięk

Gdy pracujesz z różnymi instrumentami i partiami muzycznymi w profesjonalnych aplikacjach audio, musisz mieć elastyczny system kierowania, który umożliwia efektywne miksowanie i modyfikowanie dźwięków. W projekcie JAM with Chrome opracowaliśmy system magistrali audio, podobny do fizycznego miksera. W ten sposób możemy podłączyć wszystkie instrumenty, które wymagają efektu pogłosu do wspólnego autobusu lub kanału, a potem dodać pogłos do tego autobusu, zamiast dodawać pogłos do każdego instrumentu. Jest to duża optymalizacja i zalecamy zrobienie podobnej rzeczy zaraz po tym, jak zaczniesz wykonywać bardziej złożone aplikacje.

Routing audioBus

Na szczęście w przypadku Web Audio można to łatwo osiągnąć. Możemy po prostu użyć szkieletu określonego dla efektów i użyć go w ten sam sposób.

var AudioBus = function(){
    this.input = audioContext.createGain();
    var output = audioContext.createGain();

    //create effect nodes (Convolver and Equalizer are other custom effects from the library presented at the end of the article)
    var delay = new SlapbackDelayNode(),
        convolver = new tuna.Convolver(),
        equalizer = new tuna.Equalizer();

    //route 'em
    //equalizer -> delay -> convolver
    this.input.connect(equalizer);
    equalizer.connect(delay.input);
    delay.connect(convolver);
    convolver.connect(output);

    this.connect = function(target){
       output.connect(target);
    };
};

Użytoby to w następujący sposób:

//create some native oscillators and our custom audio bus
var bus = new AudioBus(),
    instrument1 = audioContext.createOscillator(),
    instrument2 = audioContext.createOscillator(),
    instrument3 = audioContext.createOscillator();

//connect our instruments to the same bus
instrument1.connect(bus.input);
instrument2.connect(bus.input);
instrument3.connect(bus.input);
bus.connect(audioContext.destination);

Do tego zastosowaliśmy opóźnienie, korektę i pogłos (co jest dość kosztowne pod względem wydajności) za o połowę niższe koszty, tak jak w przypadku każdego instrumentu. Gdybyśmy chcieli urozmaicić dźwięk w autobusie, moglibyśmy dodać 2 nowe węzły wzmocnienia – przed Zyskaj i Po wzmocnieniu – które pozwoliłyby na wyłączenie lub zagłuszanie dźwięków w autobusie na 2 różne sposoby. Wartość „Wstępnie Zysk” jest przesunięta przed efekty, a „PostZyskaj” na końcu łańcucha. Jeśli potem zanikamy, efekty wzmocnienia będą nadal reagować po osiągnięciu najniższego poziomu, ale jeśli zanikamy, wszystkie dźwięki zostaną wyciszone w tym samym czasie.

Skąd wyruszyć?

Omówione przeze mnie metody można i powinien być dalej rozwijane. Elementy takie jak dane wejściowe i wyjściowe węzłów niestandardowych oraz metody łączenia można wdrożyć za pomocą dziedziczenia opartego na prototypach. Autobusy powinny mieć możliwość dynamicznego generowania efektów przez przekazywanie listy efektów.

Z okazji premiery JAM z Chrome postanowiliśmy stworzyć platformę efektów na licencji open source. Jeśli spodobało Ci się to krótkie wprowadzenie, obejrzyj je i podziel się swoją opinią. Tutaj toczy się dyskusja na temat standaryzacji formatu dla niestandardowych elementów Web Audio. Zaangażuj się