Studium przypadku – tworzenie Technitone.com

Sean Middleditch
Sean Middleditch
Technitone – internetowa aplikacja audio.

Technitone.com to połączenie technologii WebGL, Canvas, Web Sockets, CSS3, JavaScriptu, Flasha oraz nowego interfejsu Web Audio API w Chrome.

W tym artykule omawiamy każdy aspekt produkcji: plan, serwer, dźwięki, elementy wizualne, a także niektóre procesy, które pomagają nam projektować treści interaktywne. Większość sekcji zawiera fragmenty kodu, wersję demonstracyjną i materiały do pobrania. Na końcu artykułu znajduje się link do pobierania, dzięki któremu możesz pobrać je wszystkie w jednym pliku ZIP.

Zespół produkcyjny gskinner.com

Koncert

W witrynie gskinner.com w żaden sposób nie zajmujemy się inżynierami dźwięku – ale spróbuj nam zmierzyć się z wyzwaniem, a my opracujemy plan:

  • Użytkownicy nakreślają tony w siatce, „inspirowani” według Andre'a ToneMatrix
  • Dźwięki są połączone z instrumentami próbkowanymi, zestawami perkusyjnymi, a nawet nagraniami użytkowników
  • W tej samej siatce jednocześnie bierze udział kilku połączonych użytkowników
  • ...lub przejdź w tryb solo, aby samodzielnie odkrywać świat
  • Sesje zawodowe umożliwiają użytkownikom zorganizowanie zespołu i imprezową imprezę.

Umożliwiamy użytkownikom poznawanie interfejsu Web Audio API za pomocą panelu narzędzi, w którym do dźwięków można dodawać filtry audio i efekty.

Technitone od gskinner.com

Co robimy też:

  • Przechowuj kompozycje i efekty użytkowników jako dane oraz synchronizuj je między klientami
  • Trzeba dodać kilka kolorów, żeby można było rysować fajne piosenki
  • Oferowanie galerii, dzięki której użytkownicy będą mogli słuchać dzieł innych osób, kochać je, a nawet edytować

Przyjęliśmy znaną metaforę siatki, unieśliśmy ją w przestrzeni 3D, dodaliśmy efekty oświetlenia, tekstury i efektów cząstek. Umieściliśmy je w elastycznym (lub pełnoekranowym) interfejsie opartym na CSS i JS.

Wycieczka samochodowa

Dane dotyczące instrumentów, efektów i sieci są konsolidowane i serializowane na kliencie, a następnie wysyłane do naszego niestandardowego backendu Node.js w celu rozwiązania problemu dla wielu użytkowników à la Socket.io. Dane te są przesyłane z powrotem do klienta z uwzględnieniem wkładu poszczególnych graczy, a następnie są przekazywane do odpowiednich warstw CSS, WebGL i WebAudio, które odpowiadają za renderowanie UI, próbek i efektów podczas odtwarzania przez wielu użytkowników.

Komunikacja w czasie rzeczywistym z gniazdami poprzez kod JavaScript po stronie klienta i JavaScript na serwerze.

Diagram serwera Technitone

Węzeł jest używany w każdym aspekcie serwera. To statyczny serwer WWW i nasz serwer gniazdek w jednym miejscu. Użyliśmy Express, czyli pełnego serwera WWW zbudowanego w całości na Node. Oferuje bardzo skalowalne rozwiązania, duże możliwości dostosowywania i obsługuje niskopoziomowe rozwiązania serwerowe (tak samo jak w przypadku serwerów Apache i Windows Server). W takim przypadku jako programista możesz się już skupić tylko na tworzeniu aplikacji.

Demonstracja dla wielu użytkowników (OK, to tylko zrzut ekranu)

Ta wersja demonstracyjna wymaga serwera węzła, a ponieważ ten artykuł nie jest jednym z nich, załączamy zrzut ekranu pokazujący, jak wygląda wersja demonstracyjna po zainstalowaniu środowiska wykonawczego Node.js, skonfigurowaniu serwera WWW i uruchomieniu go lokalnie. Za każdym razem, gdy nowy użytkownik odwiedza Twoją instalację demonstracyjną, zostanie dodana nowa siatka, a praca wszystkich użytkowników będzie widoczna dla innych użytkowników.

Zrzut ekranu z wersją demonstracyjną Node.js

To proste. Dzięki połączeniu żądań Socket.io i niestandardowych żądań POST nie musieliśmy tworzyć skomplikowanych procedur synchronizacji. Socket.io w przejrzysty sposób to obsługuje, a JSON je omija.

Czy to łatwe? Obejrzyj ten film.

Mamy 3 wiersze kodu JavaScript i serwer WWW, który działa z Express.

//Tell  our Javascript file we want to use express.
var express = require('express');

//Create our web-server
var server = express.createServer();

//Tell express where to look for our static files.
server.use(express.static(__dirname + '/static/'));

Jeszcze tylko kilka połączeń, aby umożliwić komunikację w czasie rzeczywistym z socket.io.

var io = require('socket.io').listen(server);
//Start listening for socket commands
io.sockets.on('connection', function (socket) {
    //User is connected, start listening for commands.
    socket.on('someEventFromClient', handleEvent);

});

Teraz tylko zaczynamy nasłuchiwać połączeń przychodzących ze strony HTML.

<!-- Socket-io will serve it-self when requested from this url. -->
<script type="text/javascript" src="/socket.io/socket.io.js"></script>

 <!-- Create our socket and connect to the server -->
 var sock = io.connect('http://localhost:8888');
 sock.on("connect", handleConnect);

 function handleConnect() {
    //Send a event to the server.
    sock.emit('someEventFromClient', 'someData');
 }
 ```

## Sound check

A big unknown was the effort entailed with using the Web Audio API. Our initial findings confirmed that [Digital Signal Processing](http://en.wikipedia.org/wiki/Digital_Signal_Processing) (DSP) is very complex, and we were likely in way over our heads. Second realization: [Chris Rogers](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html) has already done the heavy lifting in the API.
Technitone isn't using any really complex math or audioholicism; this functionality is easily accessible to interested developers. We really just needed to brush up on some terminology and [read the docs](https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html). Our advice? Don't skim them. Read them. Start at the top and end at the bottom. They are peppered with diagrams and photos, and it's really cool stuff.

If this is the first you've heard of the Web Audio API, or don't know what it can do, hit up Chris Rogers' [demos](http://chromium.googlecode.com/svn/trunk/samples/audio/index.html). Looking for inspiration? You'll definitely find it there.

### Web Audio API Demo

Load in a sample (sound file)…

```js
/**
 * The XMLHttpRequest allows you to get the load
 * progress of your file download and has a responseType
 * of "arraybuffer" that the Web Audio API uses to
 * create its own AudioBufferNode.
 * Note: the 'true' parameter of request.open makes the
 * request asynchronous - this is required!
 */
var request = new XMLHttpRequest();
request.open("GET", "mySample.mp3", true);
request.responseType = "arraybuffer";
request.onprogress = onRequestProgress; // Progress callback.
request.onload = onRequestLoad; // Complete callback.
request.onerror = onRequestError; // Error callback.
request.onabort = onRequestError; // Abort callback.
request.send();

// Use this context to create nodes, route everything together, etc.
var context = new webkitAudioContext();

// Feed this AudioBuffer into your AudioBufferSourceNode:
var audioBuffer = null;

function onRequestProgress (event) {
    var progress = event.loaded / event.total;
}

function onRequestLoad (event) {
    // The 'true' parameter specifies if you want to mix the sample to mono.
    audioBuffer = context.createBuffer(request.response, true);
}

function onRequestError (event) {
    // An error occurred when trying to load the sound file.
}

...skonfiguruj routing modułowy...

/**
 * Generally you'll want to set up your routing like this:
 * AudioBufferSourceNode > [effect nodes] > CompressorNode > AudioContext.destination
 * Note: nodes are designed to be able to connect to multiple nodes.
 */

// The DynamicsCompressorNode makes the loud parts
// of the sound quieter and quiet parts louder.
var compressorNode = context.createDynamicsCompressor();
compressorNode.connect(context.destination);

// [other effect nodes]

// Create and route the AudioBufferSourceNode when you want to play the sample.

...zastosuj efekt w czasie działania (splot wykorzystujący odpowiedź impulsową)...

/**
 * Your routing now looks like this:
 * AudioBufferSourceNode > ConvolverNode > CompressorNode > AudioContext.destination
 */

var convolverNode = context.createConvolver();
convolverNode.connect(compressorNode);
convolverNode.buffer = impulseResponseAudioBuffer;

...zastosuj inny efekt w czasie działania (opóźnienie)...

/**
 * The delay effect needs some special routing.
 * Unlike most effects, this one takes the sound data out
 * of the flow, reinserts it after a specified time (while
 * looping it back into itself for another iteration).
 * You should add an AudioGainNode to quieten the
 * delayed sound...just so things don't get crazy :)
 *
 * Your routing now looks like this:
 * AudioBufferSourceNode -> ConvolverNode > CompressorNode > AudioContext.destination
 *                       |  ^
 *                       |  |___________________________
 *                       |  v                          |
 *                       -> DelayNode > AudioGainNode _|
 */

var delayGainNode = context.createGainNode();
delayGainNode.gain.value = 0.7; // Quieten the feedback a bit.
delayGainNode.connect(convolverNode);

var delayNode = context.createDelayNode();
delayNode.delayTime = 0.5; // Re-sound every 0.5 seconds.
delayNode.connect(delayGainNode);

delayGainNode.connect(delayNode); // make the loop

...i dołącz do dźwięku.

/**
 * Once your routing is set up properly, playing a sound
 * is easy-shmeezy. All you need to do is create an
 * AudioSourceBufferNode, route it, and tell it what time
 * (in seconds relative to the currentTime attribute of
 * the AudioContext) it needs to play the sound.
 *
 * 0 == now!
 * 1 == one second from now.
 * etc...
 */

var sourceNode = context.createBufferSource();
sourceNode.connect(convolverNode);
sourceNode.connect(delayNode);
sourceNode.buffer = audioBuffer;
sourceNode.noteOn(0); // play now!

W naszym podejściu do odtwarzania w Technitone najważniejsza jest planowanie. Zamiast ustawiać odstęp licznika czasu równy naszemu tempowi, aby przetwarzać dźwięki w każdym uderzeniu, ustawiliśmy krótszy interwał, który zarządza dźwiękami w kolejce i planuje ich harmonogram. Dzięki temu interfejs API może od razu wykonać pracę związaną z rozpoznawaniem danych dźwiękowych oraz przetwarzaniem filtrów i efektów, zanim oddamy procesorowi głos, aby mógł je włączyć. Kiedy w końcu dochodzi do tego rytmu, mamy już wszystkie informacje potrzebne do przedstawienia użytkownikom efektu końcowego.

Ogólnie rzecz biorąc, wszystko wymaga optymalizacji. Gdy zbyt mocno obciążaliśmy nasze procesory, pomijaliśmy procesy (np. „pop”, „klikanie”, „zdrapanie”) w celu dotrzymania terminów. Dokładamy wszelkich starań, by powstrzymać to szaleństwo na innej karcie w Chrome.

Pokaz świateł

Centralnym punktem jest nasza siatka i tunel cząstek. To jest warstwa WebGL firmy Technitone.

WebGL oferuje znacznie lepsze wyniki niż większość innych metod renderowania elementów wizualnych w internecie, ponieważ GPU może działać razem z procesorem. Taki wzrost wydajności wiąże się z kosztem znacznie bardziej skomplikowanego programowania o znacznie węższej krzywej uczenia się. Jeśli jednak uwielbiasz interaktywność w internecie i chcesz jak najmniej ograniczać wydajność, WebGL oferuje rozwiązanie porównywalne do Flasha.

Wersja demonstracyjna WebGL

Zawartość WebGL jest renderowana w przestrzeni roboczej (dosłownie HTML5 Canvas) i składa się z tych podstawowych elementów:

  • wierzchołki obiektu (geometria)
  • macierze pozycji (współrzędne 3D)
    • cieniowanie (opis wyglądu geometrii powiązany bezpośrednio z GPU)
    • kontekst (skróty do elementów, do których odnosi się GPU).
    • bufory (potoki do przekazywania danych kontekstowych do GPU)
    • kod główny (logika biznesowa specyficzna dla danej reklamy interaktywnej);
    • metoda „draw” – aktywuje cieniowanie i rysuje piksele na obszarze roboczym,

Podstawowy proces renderowania treści WebGL na ekranie wygląda tak:

  1. Ustaw matrycę perspektywy (dostosowuje ustawienia aparatu, który jest skierowany w przestrzeń 3D, definiując płaszczyznę obrazu).
  2. Ustaw macierz pozycji (zadeklaruj punkt początkowy we współrzędnych 3D, względem których są mierzone pozycje).
  3. Wypełnij bufory danymi (położenie wierzchołków, kolor, tekstury...), aby przekazać je do kontekstu przez cieniowanie.
  4. Wyodrębnianie i porządkowanie danych z buforów za pomocą narzędzi do cieniowania i przekazywanie ich do GPU.
  5. Wywołaj metodę rysowania, aby określić kontekst, który aktywuje cieniowanie, uruchomić go z użyciem danych i zaktualizować obszar roboczy.

Jak to wygląda w praktyce:

Ustaw matrycę perspektywy...

// Aspect ratio (usually based off the viewport,
// as it can differ from the canvas dimensions).
var aspectRatio = canvas.width / canvas.height;

// Set up the camera view with this matrix.
mat4.perspective(45, aspectRatio, 0.1, 1000.0, pMatrix);

// Adds the camera to the shader. [context = canvas.context]
// This will give it a point to start rendering from.
context.uniformMatrix4fv(shader.pMatrixUniform, 0, pMatrix);

...ustaw macierz pozycji...

// This resets the mvMatrix. This will create the origin in world space.
mat4.identity(mvMatrix);

// The mvMatrix will be moved 20 units away from the camera (z-axis).
mat4.translate(mvMatrix, [0,0,-20]);

// Sets the mvMatrix in the shader like we did with the camera matrix.
context.uniformMatrix4fv(shader.mvMatrixUniform, 0, mvMatrix);

...określić geometrię i wygląd...

// Creates a square with a gradient going from top to bottom.
// The first 3 values are the XYZ position; the last 4 are RGBA.
this.vertices = new Float32Array(28);
this.vertices.set([-2,-2, 0,    0.0, 0.0, 0.7, 1.0,
                   -2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2, 2, 0,    0.0, 0.4, 0.9, 1.0,
                    2,-2, 0,    0.0, 0.0, 0.7, 1.0
                  ]);

// Set the order of which the vertices are drawn. Repeating values allows you
// to draw to the same vertex again, saving buffer space and connecting shapes.
this.indices = new Uint16Array(6);
this.indices.set([0,1,2, 0,2,3]);

...zapełniają bufory danymi i przekazują je do kontekstu...

// Create a new storage space for the buffer and assign the data in.
context.bindBuffer(context.ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ARRAY_BUFFER, this.vertices, context.STATIC_DRAW);

// Separate the buffer data into its respective attributes per vertex.
context.vertexAttribPointer(shader.vertexPositionAttribute,3,context.FLOAT,0,28,0);
context.vertexAttribPointer(shader.vertexColorAttribute,4,context.FLOAT,0,28,12);

// Create element array buffer for the index order.
context.bindBuffer(context.ELEMENT_ARRAY_BUFFER, context.createBuffer());
context.bufferData(context.ELEMENT_ARRAY_BUFFER, this.indices, context.STATIC_DRAW);

...i wywołaj metodę rysowania

// Draw the triangles based off the order: [0,1,2, 0,2,3].
// Draws two triangles with two shared points (a square).
context.drawElements(context.TRIANGLES, 6, context.UNSIGNED_SHORT, 0);

Pamiętaj o wyczyszczeniu obszaru roboczego każdej klatki, jeśli nie chcesz, aby treści wizualne w wersji alfa nakładały się na siebie.

Obiekt

Oprócz siatki i tunelu cząstek pozostałe elementy interfejsu zostały utworzone w języku HTML / CSS i interaktywnej logice w języku JavaScript.

Od samego początku zdecydowaliśmy, że użytkownicy powinni jak najszybciej korzystać z sieci. Bez ekranu powitalnego, instrukcji, samouczków – wystarczy kliknąć. Jeśli interfejs się wczyta, nic nie powinno go spowolnić.

Wymagało to uważnego przyjrzenia się, jak przeprowadzić nowego użytkownika przez proces interakcji. Dodaliśmy też subtelne informacje, takie jak zmiana właściwości kursora CSS w zależności od pozycji myszy w przestrzeni WebGL. Jeśli kursor znajduje się nad siatką, przestawiamy go na kursor z dłońmi (ponieważ można wchodzić w interakcje poprzez nanoszenie tonów). Jeśli jest on najechany na pusty obszar wokół siatki, zamieniamy go w kierunkowy kursor krzyżowy (aby wskazać, że może on obracać siatkę lub rozbić siatkę na warstwy).

Przygotowanie do występu

LESS (wstępny procesor CSS) i CodeKit (programowanie stron internetowych przy użyciu sterydów) znacznie skróciły czas potrzebny na przekształcenie plików projektu w skrócony kod HTML/CSS. Dzięki temu możemy porządkować, pisać i optymalizować CSS w bardziej uniwersalny sposób, wykorzystując zmienne, połączenia (funkcje), a nawet obliczenia matematyczne.

Efekty sceniczne

Wykorzystując przejścia CSS3 i backbone.js, stworzyliśmy kilka prostych efektów, które pomagają ożywić aplikację i dostarczają użytkownikom wizualnych kolejek wskazujących, jakiego instrumentu używają.

Kolory Technitone.

Backbone.js pozwala nam przechwytywać zdarzenia zmiany koloru i stosować nowy kolor do odpowiednich elementów DOM. Przyspieszone przez GPU przejścia CSS3 eliminowały zmiany stylu kolorów z minimalnym wpływem na wydajność.

Większość przejść kolorów elementów interfejsu powstała przez przejście kolorów tła. Do tego koloru tła umieszczamy obrazy tła ze strategicznymi obszarami przezroczystości, dzięki którym kolor tła będzie błyszczał.

HTML: Podstawowe informacje

Do wersji demonstracyjnej potrzebne były 3 regiony kolorystyczne: 2 regiony kolorystyczne wybrane przez użytkownika oraz 3 region mieszany. Na potrzeby przykładu stworzyliśmy najprostszą strukturę DOM, która obsługuje przejścia CSS3 i najmniejszą liczbę żądań HTTP.

<!-- Basic HTML Setup -->
<div class="illo color-mixed">
  <div class="illo color-primary"></div>
  <div class="illo color-secondary"></div>
</div>

CSS: prosta struktura w stylu

Zastosowaliśmy pozycjonowanie bezwzględne, aby umieścić każdy obszar w odpowiednim miejscu, a dostosowaliśmy właściwość „tle-position”, aby wyrównać ilustrację tła w każdym z nich. Dzięki temu wszystkie regiony (z tym samym obrazem tła) wyglądają jak jeden element.

.illo {
  background: url('../img/illo.png') no-repeat;
  top:        0;
  cursor:     pointer;
}
  .illo.color-primary, .illo.color-secondary {
    position: absolute;
    height:   100%;
  }
  .illo.color-primary {
    width:                350px;
    left:                 0;
    background-position:  top left;
  }
  .illo.color-secondary {
    width:                355px;
    right:                0;
    background-position:  top right;
  }

Zastosowano przejścia przyspieszone przez GPU, które nasłuchują zdarzeń zmiany kolorów. Zwiększyliśmy czas trwania i zmodyfikowaliśmy wygładzanie w komponencie .color mix, aby uzyskać wrażenie, że połączenie kolorów zajmuje dużo czasu.

/* Apply Transitions To Backgrounds */
.color-primary, .color-secondary {
  -webkit-transition: background .5s linear;
  -moz-transition:    background .5s linear;
  -ms-transition:     background .5s linear;
  -o-transition:      background .5s linear;
}

.color-mixed {
  position:           relative;
  width:              750px;
  height:             600px;
  -webkit-transition: background 1.5s cubic-bezier(.78,0,.53,1);
  -moz-transition:    background 1.5s cubic-bezier(.78,0,.53,1);
  -ms-transition:     background 1.5s cubic-bezier(.78,0,.53,1);
  -o-transition:      background 1.5s cubic-bezier(.78,0,.53,1);
}

Wejdź na stronę HTML5Please, aby poznać informacje o obsłudze przeglądarek i zalecane sposoby przejścia na CSS3.

JavaScript: Jak to działa

Dynamiczne przypisywanie kolorów jest proste. W DOM wyszukujemy dowolny element z użyciem naszej klasy kolorów i ustawiamy kolor tła na podstawie kolorów wybranych przez użytkownika. Efekt przejścia stosujemy do dowolnego elementu w modelu DOM, dodając klasę. Dzięki temu powstaje architektura, która jest lekka, elastyczna i skalowalna.

function createPotion() {

    var primaryColor = $('.picker.color-primary > li.selected').css('background-color');
    var secondaryColor = $('.picker.color-secondary > li.selected').css('background-color');
    console.log(primaryColor, secondaryColor);
    $('.illo.color-primary').css('background-color', primaryColor);
    $('.illo.color-secondary').css('background-color', secondaryColor);

    var mixedColor = mixColors (
            parseColor(primaryColor),
            parseColor(secondaryColor)
    );

    $('.color-mixed').css('background-color', mixedColor);
}

Po wybraniu kolorów podstawowych i drugorzędnych obliczamy ich wartość mieszanych kolorów i przypisujemy wynikową wartość do odpowiedniego elementu DOM.

// take our rgb(x,x,x) value and return an array of numeric values
function parseColor(value) {
    return (
            (value = value.match(/(\d+),\s*(\d+),\s*(\d+)/)))
            ? [value[1], value[2], value[3]]
            : [0,0,0];
}

// blend two rgb arrays into a single value
function mixColors(primary, secondary) {

    var r = Math.round( (primary[0] * .5) + (secondary[0] * .5) );
    var g = Math.round( (primary[1] * .5) + (secondary[1] * .5) );
    var b = Math.round( (primary[2] * .5) + (secondary[2] * .5) );

    return 'rgb('+r+', '+g+', '+b+')';
}

Ilustracja przedstawiająca architekturę HTML/CSS: nadawanie osobowości kwadratów o różnych kolorach

Naszym celem było stworzenie zabawnego i realistycznego efektu oświetleniowego, który zachowa integralność po nałożeniu kontrastowych kolorów w sąsiednich kolorach.

W 24-bitowym pliku PNG w przezroczystych obszarach obrazu jest widoczny kolor tła elementów HTML.

Foliogramy

Kolorowe prostokąty tworzą twarde krawędzie w miejscach, gdzie stykają się różne kolory. To utrudniało w obsłudze realistyczne efekty świetlne i było jednym z większych wyzwań przy projektowaniu ilustracji.

Regiony koloru

Celem było zaprojektowanie ilustracji w taki sposób, aby krawędzie kolorów nie były widoczne przez przezroczyste obszary.

Krawędzie regionu kolorów

Planowanie kompilacji było kluczowe. Krótka sesja planowania z udziałem projektanta, programisty i ilustratora pomogła zespołowi zrozumieć, jak należy stworzyć wszystko tak, aby współpracowało ze sobą po złożeniu.

Zobacz plik programu Photoshop, który pokazuje, w jaki sposób nazwy warstw mogą przekazywać informacje o konstrukcji CSS.

Krawędzie regionu kolorów

Bis

Użytkownikom, którzy nie korzystają z Chrome, celem jest wydobycie najistotniejszej części aplikacji w jeden statyczny obraz. Węzeł siatki stał się bohaterem, kafelki tła wskazują na przeznaczenie aplikacji, a perspektywa przedstawiona w odbiciu odzwierciedla realistyczne środowisko 3D siatki.

Krawędzie regionu koloru.

Jeśli chcesz dowiedzieć się więcej o technice Technitone, zaglądaj na naszego bloga.

Zespół

Dziękuję za zapoznanie się z tymi informacjami. Może wkrótce będziemy coś z Tobą robić.