우수사례 - Technitone.com 구축

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

Technitone.com은 WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash, Chrome의 새로운 Web Audio API를 융합한 것입니다.

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

gskinner.com 제작팀

공연

gskinner.com은 오디오 엔지니어가 아닙니다. 하지만 도전해 보세요. 최선을 다해 도와드리겠습니다.

  • 사용자가 안드레ToneMatrix에 '영감을 받아' 그리드에 색조를 표시합니다.
  • 톤이 샘플링된 악기, 드럼 키트 또는 사용자의 자체 녹음 파일에 연결됩니다.
  • 연결된 여러 사용자가 동일한 그리드에서 동시에 플레이
  • 또는 솔로 모드로 전환하여 직접 탐색할 수 있습니다.
  • 초대 세션을 사용하면 사용자가 밴드를 구성하고 즉흥 연주를 할 수 있습니다.

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

gskinner.com의 Technitone

또한 다음과 같은 조치를 취하고 있습니다.

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

익숙한 그리드 메타포를 고수하고 3D 공간에 배치했으며 조명, 텍스처, 파티클 효과를 추가하고 유연한 (또는 전체 화면) CSS 및 JS 기반 인터페이스에 배치했습니다.

자동차 여행

악기, 효과, 그리드 데이터는 클라이언트에서 통합되고 직렬화된 후 맞춤 Node.js 백엔드로 전송되어 Socket.io와 같이 여러 사용자를 위해 확인됩니다. 이 데이터는 각 플레이어의 기여도가 포함된 상태로 클라이언트로 다시 전송된 후 다중 사용자 재생 중에 UI, 샘플, 효과 렌더링을 담당하는 상대 CSS, WebGL, WebAudio 레이어로 분산됩니다.

소켓과의 실시간 통신은 클라이언트의 JavaScript와 서버의 JavaScript에 피드합니다.

Technitone 서버 다이어그램

서버의 모든 측면에 Node를 사용합니다. 정적 웹 서버와 소켓 서버가 하나로 통합된 형태입니다. 결국 Express를 사용했습니다. Express는 Node를 기반으로 완전히 빌드된 전체 웹 서버입니다. 확장성이 뛰어나고 맞춤설정이 가능하며 Apache나 Windows Server와 마찬가지로 하위 수준의 서버 측면을 자동으로 처리합니다. 그러면 개발자는 애플리케이션 빌드에만 집중하면 됩니다.

멀티 사용자 데모 (실제로는 스크린샷일 뿐입니다)

이 데모는 Node 서버에서 실행해야 하며 이 도움말에서는 Node 서버가 아니므로 Node.js를 설치하고 웹 서버를 구성한 후 로컬에서 실행한 후의 데모 화면을 스크린샷으로 제공합니다. 신규 사용자가 데모 설치를 방문할 때마다 새 그리드가 추가되고 모든 사용자의 작업이 서로에게 표시됩니다.

Node.js 데모의 스크린샷

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

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

JavaScript 3줄로 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의 재생에 대한 Google의 접근 방식은 모두 예약에 관한 것입니다. 비트마다 사운드를 처리하도록 템포와 동일한 타이머 간격을 설정하는 대신 대기열에서 사운드를 관리하고 예약하는 더 작은 간격을 설정합니다. 이렇게 하면 CPU에 실제로 소리를 내도록 작업을 할당하기 전에 API가 오디오 데이터를 확인하고 필터 및 효과를 처리하는 사전 작업을 실행할 수 있습니다. 이 비트가 마지막으로 돌아올 때는 이미 스피커에게 최종 결과를 표시하는 데 필요한 모든 정보가 있습니다.

전반적으로 모든 것을 최적화해야 했습니다. CPU를 너무 많이 사용하면 일정을 맞추기 위해 프로세스가 건너뛰어 (튀기기, 클릭, 스크래치) Chrome에서 다른 탭으로 이동할 때 모든 불편을 중지하기 위해 많은 노력을 기울였습니다.

조명 쇼

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

WebGL은 GPU가 프로세서와 함께 작동하도록 태스크를 할당하여 웹에서 시각적 렌더링을 위한 다른 대부분의 접근 방식보다 훨씬 우수한 성능을 제공합니다. 이러한 성능 향상은 훨씬 더 가파른 학습 곡선과 훨씬 더 복잡한 개발 비용으로 얻을 수 있습니다. 하지만 웹의 양방향성에 진정으로 관심이 있고 성능 제약을 최대한 줄이고 싶다면 WebGL이 Flash와 비슷한 솔루션을 제공합니다.

WebGL 데모

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

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

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

  1. 원근 매트릭스를 설정합니다 (3D 공간을 들여다보는 카메라의 설정을 조정하여 화면 평면을 정의함).
  2. 위치 행렬을 설정합니다 (위치가 상대적으로 측정되는 3D 좌표의 원점을 선언).
  3. 셰이더를 통해 컨텍스트에 전달할 데이터 (버텍스 위치, 색상, 텍스처 등)로 버퍼를 채웁니다.
  4. 셰이더를 사용하여 버퍼에서 데이터를 추출하고 정리한 후 GPU에 전달합니다.
  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 메서드를 호출합니다.

// 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로 빌드되었으며 양방향 로직은 JavaScript로 빌드되었습니다.

처음부터 Google은 사용자가 최대한 빨리 그리드와 상호작용할 수 있어야 한다고 판단했습니다. 스플래시 화면, 안내, 튜토리얼이 없으며 '시작'만 표시됩니다. 인터페이스가 로드된 경우 속도가 느려지는 요소가 없어야 합니다.

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

프로그램 준비

LESS (CSS 전처리기)와 CodeKit (강력한 웹 개발)을 사용하면 디자인 파일을 스텁 처리된 HTML/CSS로 변환하는 데 걸리는 시간이 크게 줄었습니다. 이를 통해 변수, 믹스인 (함수), 수학을 활용하여 훨씬 더 다용도로 CSS를 구성, 작성, 최적화할 수 있습니다.

스테이지 효과

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

Technitone의 색상입니다.

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

인터페이스 요소의 대부분의 색상 전환은 배경 색상을 전환하여 만들어졌습니다. 이 배경 색상 위에 배경 색상이 비출 수 있도록 전략적인 투명 영역이 있는 배경 이미지를 배치합니다.

HTML: 기초

데모에는 사용자 선택 색상 영역 2개와 혼합 색상 영역 1개 등 3개의 색상 영역이 필요했습니다. 이 예시를 위해 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: 스타일이 있는 간단한 구조

절대 위치 지정을 사용하여 각 영역을 올바른 위치에 배치하고 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;
  }

색상 변경 이벤트를 수신 대기하는 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를 참고하세요.

JavaScript: 작동 방식

색상을 동적으로 할당하는 것은 간단합니다. DOM에서 color 클래스가 있는 요소를 검색하고 사용자의 색상 선택에 따라 background-color를 설정합니다. 클래스를 추가하여 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 아키텍처를 보여주는 그림: 세 가지 색상 전환 상자에 개성 부여

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

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

이미지 투명성

색상 상자는 서로 다른 색상이 만나는 지점에서 선명한 가장자리를 만듭니다. 이는 사실적인 조명 효과를 방해하며, 일러스트레이션을 디자인할 때 가장 큰 어려움 중 하나였습니다.

색상 영역

해결 방법은 색상 영역의 가장자리가 투명한 영역을 통해 표시되지 않도록 그림을 디자인하는 것이었습니다.

색상 영역 가장자리

빌드 계획이 중요했습니다. 디자이너, 개발자, 일러스트레이터 간에 빠른 계획 세션을 통해 팀은 조합 시 모든 요소가 함께 작동하도록 빌드해야 하는 방법을 파악할 수 있었습니다.

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

색상 영역 가장자리

Encore

Chrome이 없는 사용자의 경우 애플리케이션의 핵심을 단일 정적 이미지로 추출하는 것을 목표로 삼았습니다. 그리드 노드가 주인공이 되었고 배경 타일이 애플리케이션의 목적을 암시하며 반사에 나타난 원근법은 그리드의 몰입형 3D 환경을 암시합니다.

지역 가장자리 색상 지정

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

밴드

읽어 주셔서 감사합니다. 곧 함께 연주할 수 있기를 바랍니다.