Studi Kasus - Membuat Technitone.com

Sean Middleditch
Sean Middleditch
Technitone — pengalaman audio web.

Technitone.com adalah perpaduan dari WebGL, Canvas, Web Sockets, CSS3, JavaScript, Flash, dan Web Audio API yang baru di Chrome.

Artikel ini akan membahas setiap aspek produksi: rencana, server, suara, visual, dan beberapa alur kerja yang kami manfaatkan untuk merancang desain interaktif. Sebagian besar bagian berisi cuplikan kode, demo, dan download. Di akhir artikel, terdapat tautan download yang dapat Anda gunakan untuk mengambil semuanya dalam satu file zip.

Tim produksi gskinner.com.

Pertunjukan

Kami sama sekali bukan teknisi audio di gskinner.com — tetapi goda kami dengan sebuah tantangan dan kami akan menemukan sebuah rencana:

  • Pengguna memetakan nada pada petak,"terinspirasi" oleh ToneMatrix Andre
  • Nada dihubungkan ke instrumen sampel, drum kit, atau bahkan rekaman milik pengguna
  • Beberapa pengguna yang terhubung bermain di petak yang sama secara bersamaan
  • ...atau masuk ke mode solo untuk menjelajah sendiri
  • Sesi undangan memungkinkan pengguna mengatur band dan mengadakan jamuan dadakan

Kami menawarkan kesempatan kepada pengguna untuk menjelajahi Web Audio API dengan panel alat yang menerapkan filter dan efek audio pada nada mereka.

Technitone oleh gskinner.com

Kami juga:

  • Simpan komposisi dan efek pengguna sebagai data dan sinkronkan antar-klien
  • Berikan beberapa opsi warna agar mereka dapat menggambar lagu yang tampak keren
  • Tawarkan galeri agar orang dapat mendengarkan, menyukai, atau bahkan mengedit karya orang lain

Kami tetap menggunakan metafora grid yang sudah dikenal, melayang dalam ruang 3D, menambahkan beberapa efek cahaya, tekstur dan partikel, menyimpannya dalam antarmuka yang digerakkan oleh CSS (atau layar penuh) dan berbasis JS yang fleksibel.

Perjalanan darat

Data instrumen, efek, dan petak digabungkan dan diserialisasi pada klien, lalu dikirim ke backend Node.js kustom kami untuk di-resolve bagi beberapa pengguna di Socket.io. Data ini dikirim kembali ke klien dengan menyertakan kontribusi setiap pemain, sebelum didistribusikan ke lapisan CSS, WebGL, dan WebAudio relatif yang bertanggung jawab untuk merender UI, sampel, dan efek selama pemutaran multi-pengguna.

Komunikasi real-time dengan soket feed JavaScript di klien dan JavaScript di server.

Diagram Server Technitone

Kita menggunakan {i>Node<i} untuk setiap aspek server. Ini adalah server web statis dan server soket kita semua menjadi satu. Express adalah server web lengkap yang dibangun sepenuhnya di Node. Layanan ini sangat skalabel, sangat mudah disesuaikan, dan menangani aspek server tingkat rendah untuk Anda (seperti yang dilakukan Apache atau Windows Server). Selanjutnya, sebagai developer, Anda hanya perlu fokus membangun aplikasi.

Demo Multi-Pengguna (oke, ini hanya screenshot)

Demo ini harus dijalankan di server Node, dan karena artikel ini bukan salah satunya, kami telah menyertakan screenshot tampilan demo setelah Anda menginstal Node.js, mengonfigurasi server web, dan menjalankannya secara lokal. Setiap kali pengguna baru mengunjungi penginstalan demo, petak baru akan ditambahkan dan pekerjaan setiap orang dapat dilihat satu sama lain.

Screenshot Demo Node.js

Node itu mudah. Dengan menggunakan kombinasi Socket.io dan permintaan POST kustom, kita tidak perlu membuat rutinitas yang kompleks untuk sinkronisasi. Socket.io menangani hal ini secara transparan; JSON akan diteruskan.

Seberapa mudahnya? Tonton ini.

Dengan 3 baris JavaScript, kami memiliki server web yang aktif dan berjalan dengan 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/'));

Beberapa cara lagi untuk mengaitkan socket.io untuk komunikasi {i>real-time<i}.

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

});

Sekarang kita bisa mulai mendengarkan koneksi yang masuk dari halaman 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.
}

...menyiapkan perutean modular...

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

...menerapkan efek runtime (konvolusi menggunakan respons impuls)...

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

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

...menerapkan efek runtime lain (penundaan)...

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

...lalu membuatnya terdengar.

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

Pendekatan kami terhadap pemutaran di Technitone adalah tentang penjadwalan. Daripada menyetel interval timer yang sama dengan tempo kami untuk memproses suara setiap irama, kami membuat interval yang lebih kecil yang mengelola dan menjadwalkan suara dalam antrean. Hal ini memungkinkan API melakukan tugas di muka untuk me-resolve data audio serta memproses filter dan efek sebelum kita menugaskan CPU untuk membuatnya dapat didengar. Ketika irama itu akhirnya muncul, musik sudah memiliki semua informasi yang diperlukan untuk menyajikan hasil bersih kepada pembicara.

Secara keseluruhan, semua hal yang perlu dioptimalkan. Saat kami mendorong CPU terlalu keras, beberapa prosesnya dilewati (pop, klik, gores) untuk mengikuti jadwal. Kami berupaya keras untuk menghentikan semua kegilaan jika Anda melompat ke tab lain di Chrome.

Pertunjukan lampu

Bagian depan dan tengah adalah terowongan dan partikel kita. Ini adalah lapisan WebGL Technitone.

WebGL menawarkan performa yang jauh lebih unggul daripada kebanyakan pendekatan lain untuk merender visual di web, dengan menugaskan GPU untuk bekerja bersama dengan prosesor. Peningkatan performa tersebut disertai dengan biaya pengembangan yang secara signifikan lebih terlibat dengan kurva pembelajaran yang jauh lebih curam. Meskipun demikian, jika Anda benar-benar menggemari dunia interaktif di web dan menginginkan upaya yang minimal, WebGL menawarkan solusi yang sebanding dengan Flash.

Demo WebGL

Konten WebGL dirender ke kanvas (secara harfiah, Canvas HTML5) dan terdiri dari elemen penyusun inti ini:

  • verteks objek (geometri)
  • matriks posisi (koordinat 3D)
    • shader (deskripsi tampilan geometri, yang ditautkan langsung ke GPU)
    • konteks ("pintasan" ke elemen yang dirujuk oleh GPU)
    • buffer (pipeline untuk meneruskan data konteks ke GPU)
    • kode utama (logika bisnis khusus untuk interaksi interaktif yang diinginkan)
    • metode"draw" (mengaktifkan shader dan menggambar piksel ke kanvas)

Proses dasar untuk merender konten WebGL ke layar akan terlihat seperti:

  1. Tetapkan matriks perspektif (menyesuaikan setelan untuk kamera yang mengintai ke ruang 3D, menentukan bidang gambar).
  2. Tetapkan matriks posisi (deklarasikan asal dalam koordinat 3D yang posisi diukur relatif).
  3. Isi buffer dengan data (posisi verteks, warna, tekstur...) untuk diteruskan ke konteks melalui shader.
  4. Mengekstrak dan mengatur data dari buffer dengan shader dan meneruskannya ke GPU.
  5. Panggil metode gambar guna memberi tahu konteks untuk mengaktifkan shader, menjalankan dengan data, dan mengupdate kanvas.

Tampilannya terlihat seperti ini:

Tetapkan matriks perspektif...

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

...menyetel matriks posisi...

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

...mendefinisikan beberapa geometri dan tampilan...

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

...mengisi buffer dengan data dan meneruskannya ke konteks...

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

...dan panggil metode gambar

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

Setiap bingkai, ingatlah untuk membersihkan kanvas jika Anda tidak ingin visual berbasis alfa saling bertumpuk.

The venue

Selain terowongan partikel dan petak, setiap elemen UI lainnya dibuat dalam HTML / CSS dan logika interaktif dalam JavaScript.

Sejak awal, kami memutuskan bahwa pengguna harus berinteraksi dengan grid secepat mungkin. Tidak ada layar pembuka, tidak ada petunjuk, tanpa tutorial, cukup 'Buka'. Jika antarmuka dimuat — tidak boleh ada yang memperlambatnya.

Ini mengharuskan kami untuk memperhatikan secara cermat cara memandu pengguna pemula melalui interaksi mereka. Kami menyertakan isyarat halus, seperti mengubah properti kursor CSS berdasarkan posisi mouse pengguna dalam ruang WebGL. Jika kursor berada di atas grid, kita mengalihkannya ke kursor tangan (karena mereka dapat berinteraksi dengan memetakan nada). Jika kursor diarahkan ke spasi kosong di sekitar grid, kita akan menukarnya dengan kursor silang terarah (untuk menunjukkan bahwa petak dapat memutar, atau memperluas petak menjadi lapisan).

Bersiap untuk Pertunjukan

LEBIH SEDIKIT (pra-prosesor CSS), dan CodeKit (pengembangan web pada steroid) benar-benar mengurangi waktu yang dibutuhkan untuk menerjemahkan file desain menjadi HTML/CSS yang berhenti berfungsi. Ini memungkinkan kita mengatur, menulis, dan mengoptimalkan CSS dengan cara yang jauh lebih fleksibel — memanfaatkan variabel, kombinasi (fungsi), dan bahkan matematika!

Efek Panggung

Dengan menggunakan transisi CSS3 dan backbone.js kami membuat beberapa efek yang sangat sederhana yang membantu menghidupkan aplikasi dan memberi pengguna antrean visual yang menunjukkan instrumen yang mereka gunakan.

Warna Technitone.

Backbone.js memungkinkan kita untuk menangkap peristiwa perubahan warna dan menerapkan warna baru ke elemen DOM yang sesuai. Transisi CSS3 yang dipercepat GPU menangani perubahan gaya warna dengan sedikit atau tanpa dampak terhadap performa.

Sebagian besar transisi warna pada elemen antarmuka dibuat dengan mentransisikan warna latar belakang. Selain warna latar belakang ini, kami menempatkan gambar latar dengan area transparansi yang strategis agar warna latar belakang tetap bersinar.

HTML: Dasar

Kami memerlukan tiga region warna untuk demo: dua region warna pilihan pengguna dan region warna campuran ketiga. Kita membangun struktur DOM paling sederhana yang bisa kita pikirkan yang mendukung transisi CSS3 dan permintaan HTTP paling sedikit untuk ilustrasi kita.

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

CSS: Struktur Simpel dengan Gaya

Kami menggunakan penempatan posisi absolut untuk menempatkan setiap wilayah di lokasi yang benar dan menyesuaikan properti posisi latar belakang untuk menyejajarkan ilustrasi latar belakang dalam setiap wilayah. Ini akan membuat semua region (masing-masing dengan gambar latar yang sama), terlihat seperti satu elemen.

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

Transisi yang dipercepat GPU diterapkan yang memproses peristiwa perubahan warna. Kami menambah durasi dan memodifikasi easing pada campuran warna .color untuk menciptakan kesan bahwa warna tersebut perlu waktu cukup lama untuk dicampur.

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

Kunjungi HTML5harap untuk mendapatkan dukungan browser saat ini dan penggunaan yang direkomendasikan untuk transisi CSS3.

JavaScript: Membuatnya Berfungsi

Menetapkan warna secara dinamis adalah langkah yang mudah. Kita menelusuri DOM untuk menemukan elemen apa pun dengan class warna dan menetapkan warna latar belakang berdasarkan pilihan warna pengguna. Kita menerapkan efek transisi ke elemen mana pun dalam DOM dengan menambahkan class. Hal ini menciptakan arsitektur yang ringan, fleksibel, dan skalabel.

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

Setelah warna primer dan sekunder dipilih, kita menghitung nilai warna campurannya dan menetapkan nilai yang dihasilkan ke elemen DOM yang sesuai.

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

Mengilustrasikan untuk Arsitektur HTML/CSS: Memberikan Kepribadian Kotak Pergeseran Tiga Warna

Tujuan kami adalah menciptakan efek pencahayaan yang menyenangkan dan realistis, yang mempertahankan integritasnya saat warna-warna yang kontras ditempatkan di wilayah warna yang berdekatan.

PNG 24-bit memungkinkan warna latar belakang elemen HTML kita ditampilkan melalui area transparan gambar.

Transparansi Gambar

Kotak berwarna menciptakan tepian yang keras di mana warna yang berbeda bertemu. Hal ini menghalangi efek pencahayaan yang realistis dan merupakan salah satu tantangan besar ketika mendesain ilustrasi.

Region Warna

Solusinya adalah mendesain ilustrasi sehingga tidak akan pernah memungkinkan tepi area warna terlihat melalui area transparan.

Tepi Wilayah Warna

Perencanaan pembangunan sangatlah penting. Sesi perencanaan cepat antara desainer, developer, dan ilustrator membantu tim memahami bagaimana semua yang perlu dibuat sehingga dapat bekerja sama saat dirangkai.

Lihat file Photoshop sebagai contoh bagaimana penamaan lapisan dapat mengomunikasikan informasi tentang konstruksi CSS.

Tepi Wilayah Warna

Pertunjukan Tambahan

Bagi pengguna tanpa Chrome, kami menetapkan sasaran untuk menyaring esensi aplikasi menjadi satu gambar statis. Node grid menjadi pahlawan, ubin latar belakang menyinggung tujuan aplikasi, dan perspektif yang ada dalam petunjuk refleksi pada lingkungan 3D grid yang imersif.

Tepi Wilayah Warna.

Jika Anda tertarik untuk mempelajari Technitone lebih lanjut, pantau terus blog kami.

Band

Terima kasih sudah membaca, mungkin kami akan segera menghubungi Anda.