우수사례 - Technitone.com 구축

Sean Middleditch
Sean Middleditch
Technitone — 웹 오디오 환경

Technitone.com은 WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash와 Chrome의 새로운 웹 오디오 API를 합친 서비스입니다.

이 도움말에서는 제작의 모든 측면, 즉 계획, 서버, 사운드, 시각 요소, 대화형 디자인에 활용하는 워크플로 등 모든 측면을 다룹니다. 대부분의 섹션에는 코드 스니펫, 데모, 다운로드가 포함되어 있습니다. 도움말 끝부분에는 하나의 zip 파일로 압축할 수 있는 다운로드 링크가 있습니다.

gskinner.com 제작팀

공연

gskinner.com의 오디오 엔지니어가 아니더라도 도전해 보시면 다음 계획을 세우실 수 있습니다.

  • 사용자는 안드레의 ToneMatrix에서 영감을 받아 그리드에 톤을 그립니다.
  • 톤은 샘플링된 악기, 드럼 키트 또는 사용자 자체 녹음에 연결됩니다.
  • 연결된 여러 사용자가 같은 그리드에서 동시에 플레이
  • ...또는 솔로 모드로 이동하여 혼자 탐험하세요
  • 초대 세션에서는 사용자가 밴드를 편성하여 즉흥 연주를 즐길 수 있습니다.

오디오 필터와 톤에 효과를 적용하는 도구 패널을 통해 사용자에게 웹 오디오 API를 살펴볼 수 있는 기회를 제공합니다.

gskinner.com의 Technitone

또한 다음과 같은 이점이 있습니다.

  • 사용자의 컴포지션과 효과를 데이터로 저장하고 클라이언트 간에 동기화
  • 멋진 노래를 그릴 수 있도록 색상 옵션을 제공합니다.
  • 다른 사람의 작품을 듣거나, 좋아하고, 편집할 수 있는 갤러리를 제공하세요.

우리는 그리드라는 익숙한 방식을 고수하고, 그리드를 3D 공간에 띄우고, 조명, 텍스처, 입자 효과를 추가하고, 유연한 (또는 전체 화면) CSS 및 JS 기반 인터페이스에 포함했습니다.

로드 트립

계측, 효과, 그리드 데이터가 클라이언트에서 통합되고 직렬화된 후 맞춤 Node.js 백엔드로 전송되어 Socket.io에서 여러 사용자의 문제를 해결할 수 있습니다. 이 데이터는 각 플레이어의 기여가 포함된 클라이언트로 다시 전송되고, 멀티 사용자 재생 중에 UI, 샘플, 효과를 렌더링하는 관련 CSS, WebGL, WebAudio 레이어로 분산됩니다.

소켓과의 실시간 통신은 클라이언트의 JavaScript 및 서버의 JavaScript를 제공합니다.

Technitone 서버 다이어그램

서버의 모든 측면에 노드를 사용합니다. 정적 웹 서버와 소켓 서버가 하나로 묶여 있습니다. Express는 우리가 사용하게 된 제품이며, Node를 기반으로 완전히 구축된 완전한 웹 서버입니다. 확장성이 뛰어나고 맞춤설정이 용이하며 Apache 또는 Windows Server와 마찬가지로 하위 수준의 서버 측면을 자동으로 처리합니다. 그러면 개발자는 애플리케이션을 빌드하는 데만 집중하면 됩니다.

멀티 사용자 데모 (스크린샷입니다.)

이 데모는 Node 서버에서 실행해야 합니다. 이 문서는 노드가 아니므로 Node.js를 설치하고, 웹 서버를 구성하고, 로컬에서 실행한 후 데모가 어떻게 표시되는지 보여주는 스크린샷이 포함되어 있습니다. 새로운 사용자가 데모 설치를 방문할 때마다 새 그리드가 추가되고 모든 사용자의 작업이 서로에게 표시됩니다.

Node.js 데모의 스크린샷

노드는 간단합니다. Socket.io와 맞춤 POST 요청의 조합을 사용하여 동기화를 위한 복잡한 루틴을 빌드할 필요가 없었습니다. Socket.io는 이를 투명하게 처리하며, JSON이 전달됩니다.

얼마나 쉬운가요? 다음 동영상을 보자고.

세 줄의 자바스크립트로 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에서 재생하는 방법은 바로 일정입니다. 모든 박자의 사운드를 처리하는 템포와 동일한 타이머 간격을 설정하는 대신, 대기열의 사운드를 관리하고 예약하는 더 짧은 간격을 설정합니다. 이를 통해 CPU에 실제로 청취 가능하도록 만들기 전에 API가 오디오 데이터를 해결하고 필터 및 효과를 처리하는 선행 작업을 수행할 수 있습니다. 마침내 비트가 나오면 스피커에 순 결과를 제공하는 데 필요한 모든 정보를 갖추게 됩니다.

전반적으로 모든 것을 최적화할 필요가 있었습니다. CPU의 성능을 과하게 했을 때는 일정에 맞추기 위해 프로세스를 건너뛰었습니다 (팝업, 클릭, 긁기).

라이트 쇼

앞쪽과 중앙에는 그리드와 입자 터널이 있습니다. 이는 Technitone의 WebGL 레이어입니다.

WebGL은 GPU가 프로세서와 함께 작동하도록 하여 웹에서 시각적 요소를 렌더링하는 대부분의 다른 접근 방식보다 훨씬 우수한 성능을 제공합니다. 이러한 성과 향상은 학습 과정이 훨씬 더 가파른 상태에서 훨씬 더 많은 관련 개발에 드는 비용을 수반하기 때문에 발생합니다. 그렇더라도 웹에서의 대화형 기능에 대한 열정이 있고 성능 제한을 최소화하려는 사용자를 위해 WebGL은 플래시와 견줄 수 있는 솔루션을 제공합니다.

WebGL 데모

WebGL 콘텐츠는 캔버스 (말 그대로 HTML5 캔버스)로 렌더링되며 다음과 같은 핵심 요소로 구성됩니다.

  • 객체 꼭짓점 (도형)
  • 위치 행렬 (3D 좌표)
    • 셰이더 (도형 모양에 대한 설명, GPU에 직접 연결됨)
    • 컨텍스트 ('GPU가 참조하는 요소의 바로가기')
    • 버퍼 (컨텍스트 데이터를 GPU로 전달하는 파이프라인)
    • 기본 코드 (원하는 상호작용과 관련된 비즈니스 로직)
    • 'draw' 메서드 (셰이더를 활성화하고 캔버스에 픽셀을 그립니다.)

WebGL 콘텐츠를 화면에 렌더링하는 기본 프로세스는 다음과 같습니다.

  1. 원근 매트릭스를 설정합니다 (3D 공간을 들여다보는 카메라의 설정을 조정하여 영상 평면을 정의함).
  2. 위치 매트릭스를 설정합니다 (위치를 기준으로 측정되는 3D 좌표에서 원점을 선언).
  3. 셰이더를 통해 컨텍스트에 전달할 데이터 (꼭짓점 위치, 색상, 텍스처 등)로 버퍼를 채웁니다.
  4. 셰이더를 사용하여 버퍼에서 데이터를 추출 및 구성하고 GPU로 전달합니다.
  5. 그리기 메서드를 호출하여 셰이더를 활성화하고 데이터를 사용하여 실행하며 캔버스를 업데이트하도록 컨텍스트에 알립니다.

예를 들면 다음과 같습니다.

원근 행렬 설정...

// 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 메서드 호출

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

알파 기반 시각 자료가 서로 겹치지 않게 하려면 프레임마다 캔버스를 지우세요.

개최지

그리드 및 입자 터널 외에도 다른 모든 UI 요소는 HTML / CSS로 빌드되었으며 자바스크립트의 대화형 로직으로 빌드되었습니다.

처음부터 우리는 사용자가 그리드와 최대한 빠르게 상호작용해야 한다고 결정했습니다. 스플래시 화면, 안내, 튜토리얼 없이 '이동'하기만 하면 됩니다. 인터페이스가 로드되면 아무것도 느려지지 않습니다.

이를 위해서는 신규 사용자에게 상호작용을 어떻게 안내하는 지를 주의 깊게 살펴봐야 했습니다. 우리는 WebGL 공간 내에서 사용자의 마우스 위치에 따라 CSS 커서 속성을 변경하는 것과 같은 미묘한 신호를 포함했습니다. 커서가 그리드 위에 있으면 손 모양 커서로 전환됩니다. 손 모양 커서는 색조를 표시하여 상호작용할 수 있기 때문입니다. 그리드 주변의 공백에 마우스를 가져가면 방향 교차 커서로 바꿉니다 (그리드가 회전하거나 그리드를 레이어로 분해할 수 있음을 나타냄).

공연 준비하기

LESS (CSS 전처리기) 및 CodeKit (스테로이드 웹 개발)은 디자인 파일을 스텁 처리된 HTML/CSS로 변환하는 데 걸리는 시간을 실제로 줄였습니다. 이를 통해 변수, 믹스인 (함수), 수학까지 훨씬 다양한 방식으로 CSS를 구성, 작성 및 최적화할 수 있습니다.

무대 효과

우리는 CSS3 전환backbone.js를 사용하여 애플리케이션에 활기를 불어넣고 사용자가 사용 중인 도구를 나타내는 시각적 대기열을 제공하는 매우 간단한 효과를 만들었습니다.

테크니톤 색상

Backbone.js를 사용하면 색상 변경 이벤트를 포착하고 적절한 DOM 요소에 새 색상을 적용할 수 있습니다. GPU 가속 CSS3 전환이 성능에 거의 영향을 미치지 않고 색상 스타일 변경을 처리했습니다.

인터페이스 요소에서 색상 전환은 대부분 배경색 전환으로 만들어졌습니다. 이 배경 색상 위에 전략적으로 투명한 영역이 있는 배경 이미지를 배치하여 배경 색상을 빛나게 합니다.

HTML: 재단

데모에는 3개의 색상 영역(사용자가 선택한 색상 영역 2개와 세 번째 혼합 색 영역)이 필요했습니다. 우리는 일러스트레이션을 위한 CSS3 전환과 가장 적은 수의 HTTP 요청을 지원하는 가장 단순한 DOM 구조를 구축했습니다.

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

CSS: 스타일의 단순한 구조

절대 위치를 사용하여 각 지역을 올바른 위치에 배치했으며 배경 위치 속성을 조정하여 각 지역 내에서 배경 일러스트를 정렬했습니다. 이렇게 하면 모든 영역 (각각 동일한 배경 이미지를 가진 영역)이 단일 요소처럼 보입니다.

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

색상 변경 이벤트를 수신 대기하는 GPU 가속 전환이 적용되었습니다. 색상이 혼합되는 데 시간이 걸린 것처럼 보이도록 재생 시간을 늘리고 .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);
}

현재 브라우저 지원 기능 및 CSS3 전환에 권장되는 사용법을 알아보려면 HTML5please를 방문하세요.

자바스크립트: 작동하기

색상을 동적으로 할당하는 것은 간단합니다. 색상 클래스가 있는 요소를 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 아키텍처 그림: 3가지 색상으로 변하는 상자 만들기

우리의 목표는 인접한 색상 영역에 대비되는 색상을 배치할 때 무결성을 유지하는 재미있고 사실적인 조명 효과를 만드는 것이었습니다.

24비트 PNG를 사용하면 HTML 요소의 배경색이 이미지의 투명한 영역을 통해 표시될 수 있습니다.

이미지 투명도

색상이 지정된 상자는 서로 다른 색상이 만나는 단단한 가장자리를 만듭니다. 이 효과는 사실적인 조명 효과를 방해하며 삽화를 디자인할 때 더 큰 과제 중 하나였습니다.

색상 영역

해결책은 색상 영역의 가장자리가 투명한 영역을 통해 표시되지 않도록 일러스트레이션을 디자인하는 것이었습니다.

색상 영역 가장자리

빌드 계획이 매우 중요했습니다. 디자이너, 개발자, 일러스트레이터가 참여한 간단한 계획 세션을 통해 팀은 조립 시 함께 작동하도록 모든 것을 어떻게 제작해야 하는지 이해할 수 있었습니다.

레이어 이름 지정으로 CSS 구성에 관한 정보를 전달하는 방법의 예로 Photoshop 파일을 확인하세요.

색상 영역 가장자리

Encore

Chrome을 사용하지 않는 사용자의 경우 애플리케이션의 핵심을 단일 정적 이미지로 정제하는 것을 목표로 하고 있습니다. 그리드 노드가 주인공이 되고, 배경 타일은 애플리케이션의 목적을 암시하며, 반사 힌트에 나타나는 원근감은 그리드의 몰입형 3D 환경을 나타냅니다.

색상 영역 가장자리

Technitone에 대해 자세히 알아보려면 Google 블로그를 계속 확인해 주세요.

밴드

읽어 주셔서 감사합니다. 곧 함께 흥미진진한 시간을 보내실 수 있기를 바랍니다.