Практический пример - Строительство Technitone.com

Шон Миддлдич
Sean Middleditch
Technitone — опыт веб-аудио.

Technitone.com — это сочетание WebGL, Canvas, Web Sockets, CSS3, Javascript, Flash и нового API веб-аудио в Chrome.

В этой статье будут затронуты все аспекты производства: план, сервер, звуки, визуальные эффекты и некоторые рабочие процессы, которые мы используем для интерактивного дизайна. Большинство разделов содержат фрагменты кода, демо-версию и файлы для загрузки. В конце статьи есть ссылка для скачивания, по которой вы можете скачать их все в одном zip-файле.

Производственная группа gskinner.com.

Концерт

Мы ни в коем случае не являемся звукоинженерами на gskinner.com — но соблазните нас вызовом, и мы придумаем план:

  • Пользователи наносят тона на сетку , «вдохновленную» ToneMatrix Андре.
  • Тоны подключаются к семплам инструментов, ударным установкам или даже к собственным записям пользователей.
  • Несколько подключенных пользователей одновременно играют в одной сетке.
  • …или перейдите в одиночный режим, чтобы исследовать самостоятельно
  • Пригласительные сессии позволяют пользователям организовать группу и устроить импровизированный джем.

Мы предлагаем пользователям возможность изучить API веб-аудио с помощью панели инструментов, которая применяет аудиофильтры и эффекты к их тонам.

Технитон от gskinner.com

Мы также:

  • Сохраняйте композиции и эффекты пользователей в виде данных и синхронизируйте их между клиентами.
  • Предоставьте несколько вариантов цвета, чтобы они могли рисовать классные песни.
  • Предложите галерею, чтобы люди могли слушать, любить или даже редактировать работы других людей.

Мы придерживались знакомой метафоры сетки, разместили ее в трехмерном пространстве, добавили немного освещения, текстур и эффектов частиц, разместили ее в гибком (или полноэкранном) интерфейсе на основе CSS и JS.

Дорожное путешествие

Данные инструментов, эффектов и сетки консолидируются и сериализуются на клиенте, а затем отправляются в наш собственный бэкэнд Node.js для обработки для нескольких пользователей, как в Socket.io . Эти данные отправляются обратно клиенту с учетом вклада каждого игрока, а затем распределяются по соответствующим слоям CSS, WebGL и WebAudio, отвечающим за рендеринг пользовательского интерфейса, образцов и эффектов во время многопользовательского воспроизведения.

Взаимодействие с сокетами в режиме реального времени передает Javascript на клиенте и Javascript на сервере.

Схема сервера Technitone

Мы используем Node для каждого аспекта сервера. Это статический веб-сервер и наш сервер сокетов в одном лице. Express — это то, что мы в итоге использовали, это полноценный веб-сервер, полностью построенный на Node. Он супермасштабируемый, легко настраиваемый и обрабатывает низкоуровневые аспекты сервера за вас (так же, как это сделали бы Apache или Windows Server). Тогда вам, как разработчику, останется сосредоточиться только на создании своего приложения.

Многопользовательская демо-версия (ок, это всего лишь скриншот)

Эту демонстрацию необходимо запускать с сервера Node, и поскольку эта статья не является таковой, мы включили снимок экрана того, как выглядит демо-версия после того, как вы установили Node.js, настроили свой веб-сервер и запустили его локально. . Каждый раз, когда новый пользователь посещает вашу демонстрационную установку, будет добавляться новая сетка, и работа каждого будет видна друг другу.

Скриншот демо-версии Node.js

Узел простой. Используя комбинацию Socket.io и пользовательских запросов POST, нам не пришлось создавать сложные процедуры для синхронизации. Socket.io прозрачно обрабатывает это; JSON распространяется.

Как легко? Смотри.

Благодаря трем строкам Javascript у нас есть веб-сервер, работающий с 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/'));

Еще несколько, чтобы связать 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);

});

Теперь мы просто начинаем прослушивать входящие соединения с 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.
}

…настроить модульную маршрутизацию…

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

…применить эффект времени выполнения (свертка с использованием импульсной характеристики)…

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

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

…применить другой эффект времени выполнения (задержку)…

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

…а затем сделать это слышимым.

/**
 * 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!

Наш подход к воспроизведению в Technitone основан на планировании. Вместо установки интервала таймера, равного нашему темпу, для обработки звуков на каждом такте, мы устанавливаем меньший интервал, который управляет звуками и распределяет их по очереди. Это позволяет API выполнять предварительную работу по разрешению аудиоданных и обработке фильтров и эффектов, прежде чем мы поручим процессору фактически сделать их слышимыми. Когда этот бит наконец появляется, он уже имеет всю информацию, необходимую для представления конечного результата выступающим.

В общем, все нужно было оптимизировать. Когда мы слишком сильно нагружали наши процессоры, процессы пропускались (хлопки, щелчки, царапины), чтобы не отставать от расписания; мы прилагаем серьезные усилия, чтобы остановить все это безумие, если вы перейдете на другую вкладку в Chrome.

Световое шоу

Впереди и в центре находится наша сетка и туннель частиц. Это слой WebGL от Technitone.

WebGL предлагает значительно более высокую производительность, чем большинство других подходов к рендерингу визуальных изображений в Интернете, поскольку графический процессор работает совместно с процессором. За этот прирост производительности приходится платить за значительно более сложную разработку с гораздо более крутой кривой обучения. Тем не менее, если вы действительно увлечены интерактивностью в Интернете и хотите как можно меньше ограничений производительности, WebGL предлагает решение , сравнимое с Flash.

Демонстрация WebGL

Содержимое WebGL отображается на холсте (буквально HTML5 Canvas) и состоит из следующих основных строительных блоков:

  • вершины объекта (геометрия)
  • матрицы положений (3D-координаты)
    • шейдеры (описание внешнего вида геометрии, связанное непосредственно с графическим процессором)
    • контекст («ярлыки» для элементов, на которые ссылается графический процессор)
    • буферы (конвейеры для передачи контекстных данных в графический процессор)
    • основной код (бизнес-логика, специфичная для желаемого интерактива)
    • метод «draw» (активирует шейдеры и рисует пиксели на холсте)

Основной процесс вывода содержимого WebGL на экран выглядит следующим образом:

  1. Установите матрицу перспективы (регулирует настройки камеры, которая смотрит в трехмерное пространство, определяя плоскость изображения).
  2. Установите матрицу положения (объявите начало координат в 3D-координатах, относительно которых измеряются положения).
  3. Заполните буферы данными (положение вершин, цвет, текстуры…) для передачи в контекст через шейдеры.
  4. Извлекайте и систематизируйте данные из буферов с помощью шейдеров и передайте их в графический процессор.
  5. Вызовите метод draw, чтобы сообщить контексту активировать шейдеры, запустить данные и обновить холст.

В действии это выглядит так:

Установите матрицу перспективы…

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

…установить матрицу позиций…

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

…определить геометрию и внешний вид…

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

…заполнить буферы данными и передать их в контекст…

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

… и вызвать метод рисования

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

Не забывайте очищать холст в каждом кадре, если вы не хотите, чтобы альфа-изображения накладывались друг на друга.

Место проведения

Помимо сетки и туннеля частиц, все остальные элементы пользовательского интерфейса были построены на HTML/CSS, а интерактивная логика – на Javascript.

С самого начала мы решили, что пользователи должны взаимодействовать с сеткой как можно быстрее. Ни заставки, ни инструкций, ни учебных пособий, просто «Давай». Если интерфейс загружен — ничего его тормозить не должно.

Это потребовало от нас тщательного изучения того, как направлять начинающего пользователя во время взаимодействия. Мы включили тонкие подсказки, такие как изменение свойства курсора CSS в зависимости от положения мыши пользователя в пространстве WebGL. Если курсор находится над сеткой, мы переключаем его на курсор в виде руки (потому что они могут взаимодействовать, рисуя тона). Если он наведен на свободное пространство вокруг сетки, мы заменяем его направленным крестообразным курсором (чтобы указать, что они могут вращать или разбивать сетку на слои).

Готовимся к шоу

LESS (препроцессор CSS) и CodeKit (веб-разработка на стероидах) действительно сокращают время, необходимое для перевода файлов дизайна в заглушку HTML/CSS. Они позволяют нам организовывать, писать и оптимизировать CSS гораздо более универсальным способом — используя переменные, дополнения (функции) и даже математические вычисления!

Сценические эффекты

Используя переходы CSS3 и backbone.js , мы создали несколько действительно простых эффектов, которые помогают оживить приложение и предоставляют пользователям визуальные очереди, указывающие, какой инструмент они используют.

Цвета Технитон.

Backbone.js позволяет нам перехватывать события изменения цвета и применять новый цвет к соответствующим элементам DOM. Переходы CSS3 с ускорением графического процессора обрабатывают изменения цветового стиля практически без влияния на производительность.

Большинство цветовых переходов на элементах интерфейса были созданы путем перехода цветов фона. Поверх этого цвета фона мы размещаем фоновые изображения со стратегическими областями прозрачности, чтобы цвет фона просвечивал.

HTML: Фонд

Для демонстрации нам понадобились три цветовых региона: два выбранных пользователем цветовых региона и третий смешанный цветовой регион. Мы создали самую простую структуру DOM, какую только могли придумать, которая поддерживает переходы CSS3 и наименьшее количество HTTP-запросов для нашей иллюстрации.

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

CSS: простая структура со стилем

Мы использовали абсолютное позиционирование, чтобы разместить каждый регион в правильном месте, и скорректировали свойство background-position, чтобы выровнять фоновую иллюстрацию внутри каждого региона. Благодаря этому все регионы (каждый с одинаковым фоновым изображением) выглядят как один элемент.

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

Были применены переходы с ускорением графического процессора, которые отслеживают события изменения цвета. Мы увеличили продолжительность и изменили плавность .color-mixed, чтобы создать впечатление, что цветам нужно время для смешивания.

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

Посетите HTML5, чтобы узнать о текущей поддержке браузера и рекомендуемом использовании переходов CSS3.

Javascript: как заставить это работать

Динамически назначать цвета очень просто. Мы ищем в DOM любой элемент с нашим цветовым классом и устанавливаем цвет фона на основе выбора цвета пользователем. Мы применяем наш эффект перехода к любому элементу в DOM, добавляя класс. Это создает легкую, гибкую и масштабируемую архитектуру.

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

После того, как основной и дополнительный цвета выбраны, мы вычисляем их смешанное цветовое значение и присваиваем полученное значение соответствующему элементу 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+')';
}

Иллюстрация архитектуры HTML/CSS: придание индивидуальности трем меняющимся цветам блокам

Нашей целью было создать забавный и реалистичный эффект освещения, который сохранял бы целостность при размещении контрастных цветов в соседних цветовых областях.

24-битный PNG позволяет отображать цвет фона наших HTML-элементов сквозь прозрачные области изображения.

Прозрачные изображения

Цветные коробки создают четкие края на стыке разных цветов. Это мешает реалистичным световым эффектам и является одной из самых больших проблем при разработке иллюстрации.

Цветовые регионы

Решение заключалось в том, чтобы спроектировать иллюстрацию так, чтобы края цветовых областей никогда не проступали сквозь прозрачные области.

Края цветной области

Планирование строительства имело решающее значение. Быстрое планирование между дизайнером, разработчиком и иллюстратором помогло команде понять, как все нужно построить, чтобы при сборке все работало вместе.

Посмотрите файл Photoshop как пример того, как именование слоев может передавать информацию о построении CSS.

Края цветной области

бис

Для пользователей без Chrome мы поставили цель свести суть приложения в одно статичное изображение. Узел сетки стал главным героем, фоновые плитки намекают на цель приложения, а перспектива, присутствующая в отражении, намекает на захватывающую трехмерную среду сетки.

Края цветной области.

Если вы хотите узнать больше о Technitone, следите за обновлениями в нашем блоге.

Группа

Спасибо за внимание, возможно, мы скоро с вами поиграем !