Studi Kasus - Membuat Technitone.com

Sean Middleditch
Sean Middleditch
Technitone — pengalaman audio web.

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

Artikel ini akan membahas setiap aspek produksi: rencana, server, suara, visual, dan beberapa alur kerja yang kami manfaatkan untuk mendesain secara interaktif. Sebagian besar bagian berisi cuplikan kode, demo, dan download. Di akhir artikel, ada link download tempat Anda dapat mengambil semuanya sebagai satu file zip.

Tim produksi gskinner.com.

Acara

Kami di gskinner.com bukanlah engineer audio, tetapi jika Anda memberikan tantangan, kami akan mencari tahu rencananya:

  • Pengguna membuat plot nada pada petak,"terinspirasi" oleh ToneMatrix Andre
  • Nada dihubungkan ke instrumen sampel, kit drum, atau bahkan rekaman pengguna
  • Beberapa pengguna yang terhubung bermain di petak yang sama secara bersamaan
  • …atau beralih ke mode solo untuk menjelajahi sendiri
  • Sesi undangan memungkinkan pengguna mengatur band dan melakukan jam session mendadak

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

Technitone oleh gskinner.com

Kami juga:

  • Menyimpan komposisi dan efek pengguna sebagai data dan menyinkronkannya di seluruh klien
  • Berikan beberapa opsi warna agar mereka dapat menggambar lagu yang terlihat keren
  • Menawarkan galeri agar orang dapat mendengarkan, menyukai, atau bahkan mengedit karya orang lain

Kami tetap menggunakan metafora petak yang sudah dikenal, mengambangkannya di ruang 3D, menambahkan beberapa efek pencahayaan, tekstur, dan partikel, serta menempatkannya di antarmuka fleksibel (atau layar penuh) yang didukung CSS dan JS.

Perjalanan

Data instrumen, efek, dan petak digabungkan dan diserialisasi di klien, lalu dikirim ke backend Node.js kustom kami untuk di-resolve bagi beberapa pengguna seperti 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 memberi makan JavaScript di klien dan JavaScript di server.

Diagram Server Technitone

Kami menggunakan Node untuk setiap aspek server. Server ini adalah server web statis dan server soket kami dalam satu paket. Express adalah server web lengkap yang dibuat sepenuhnya di Node. Server ini sangat skalabel, sangat dapat disesuaikan, dan menangani aspek server tingkat rendah untuk Anda (seperti yang dilakukan Apache atau Windows Server). Kemudian, Anda sebagai developer hanya perlu fokus pada pembuatan aplikasi.

Demo Multi-Pengguna (oke, ini benar-benar hanya screenshot)

Demo ini harus dijalankan dari server Node, dan karena artikel ini bukan server Node, 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 Anda, petak baru akan ditambahkan dan pekerjaan semua orang akan terlihat oleh 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 rumit untuk sinkronisasi. Socket.io menangani hal ini secara transparan; JSON akan diteruskan.

Seberapa mudah? Tonton video ini.

Dengan 3 baris JavaScript, kita memiliki server web yang sudah siap 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 lagi untuk mengikat socket.io untuk komunikasi real-time.

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 hanya mulai memproses koneksi 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 pemilihan rute 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 dapat didengar.

/**
 * 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 menetapkan interval timer yang sama dengan tempo untuk memproses suara setiap ketukan, kita menyiapkan interval yang lebih kecil yang mengelola dan menjadwalkan suara dalam antrean. Hal ini memungkinkan API melakukan pekerjaan awal untuk me-resolve data audio serta memproses filter dan efek sebelum kita memberi tugas kepada CPU untuk benar-benar membuatnya terdengar. Saat akhirnya muncul, beat tersebut sudah memiliki semua informasi yang diperlukan untuk menampilkan hasil bersih ke speaker.

Secara keseluruhan, semuanya perlu dioptimalkan. Jika CPU dipaksa terlalu keras, proses akan dilewati (pop, klik, goresan) untuk mengikuti jadwal; kami berupaya keras untuk menghentikan semua kegilaan jika Anda beralih ke tab lain di Chrome.

Pertunjukan cahaya

Di bagian depan dan tengah adalah petak dan tunnel partikel. Ini adalah lapisan WebGL Technitone.

WebGL menawarkan performa yang jauh lebih unggul daripada sebagian besar pendekatan lain untuk merender visual di web, dengan memberi tugas kepada GPU untuk bekerja bersama dengan prosesor. Peningkatan performa tersebut disertai dengan biaya pengembangan yang jauh lebih rumit dengan kurva pembelajaran yang jauh lebih curam. Meskipun demikian, jika Anda benar-benar menyukai interaktif di web, dan menginginkan batasan performa sesedikit mungkin, WebGL menawarkan solusi yang sebanding dengan Flash.

Demo WebGL

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

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

Proses dasar untuk merender konten WebGL ke layar terlihat seperti:

  1. Menetapkan matriks perspektif (menyesuaikan setelan untuk kamera yang mengintip ke ruang 3D, yang menentukan bidang gambar).
  2. Menetapkan matriks posisi (mendeklarasikan asal dalam koordinat 3D yang digunakan sebagai referensi pengukuran posisi).
  3. Isi buffer dengan data (posisi vertex, warna, tekstur…) untuk diteruskan ke konteks melalui shader.
  4. Mengekstrak dan mengatur data dari buffering dengan shader dan meneruskannya ke GPU.
  5. Panggil metode gambar untuk memberi tahu konteks agar mengaktifkan shader, berjalan dengan data, dan memperbarui kanvas.

Tampilannya seperti ini:

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

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

…menentukan 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]);

…isi buffer dengan data dan teruskan 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 frame, jangan lupa untuk menghapus kanvas jika Anda tidak ingin visual berbasis alfa menumpuk satu sama lain.

The venue

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

Sejak awal, kami memutuskan bahwa pengguna harus berinteraksi dengan petak secepat mungkin. Tidak ada layar pembuka, tidak ada petunjuk, tidak ada tutorial, hanya 'Mulai'. Jika antarmuka dimuat, tidak akan ada yang memperlambatnya.

Hal ini mengharuskan kami untuk melihat dengan cermat cara memandu pengguna baru melalui interaksi mereka. Kami menyertakan isyarat halus, seperti mengubah properti kursor CSS berdasarkan posisi mouse pengguna dalam ruang WebGL. Jika kursor berada di atas petak, kita akan mengalihkannya ke kursor tangan (karena dapat berinteraksi dengan memetakan nada). Jika kursor diarahkan ke ruang kosong di sekitar petak, kita akan menukarnya dengan kursor silang berarah (untuk menunjukkan bahwa kursor dapat memutar, atau memunculkan petak menjadi beberapa lapisan).

Bersiap untuk Pertunjukan

LESS (pra-pemroses CSS), dan CodeKit (pengembangan web yang canggih) benar-benar mengurangi waktu yang diperlukan untuk menerjemahkan file desain menjadi HTML/CSS yang di-stub. Hal ini memungkinkan kita mengatur, menulis, dan mengoptimalkan CSS dengan cara yang jauh lebih fleksibel — memanfaatkan variabel, mix-in (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 isyarat visual yang menunjukkan instrumen yang mereka gunakan.

Warna Technitone.

Backbone.js memungkinkan kita 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 pada performa.

Sebagian besar transisi warna pada elemen antarmuka dibuat dengan mentransisikan warna latar belakang. Di atas warna latar belakang ini, kami menempatkan gambar latar belakang dengan area transparansi strategis agar warna latar belakang terlihat.

HTML: Dasar-Dasarnya

Kita memerlukan tiga wilayah warna untuk demo: dua wilayah warna yang dipilih pengguna dan wilayah warna campuran ketiga. Kami membuat struktur DOM paling sederhana yang dapat kami pikirkan yang mendukung transisi CSS3 dan permintaan HTTP paling sedikit untuk ilustrasi kami.

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

CSS: Struktur Sederhana dengan Gaya

Kami menggunakan pemosisian absolut untuk menempatkan setiap wilayah di lokasi yang benar dan menyesuaikan properti background-position untuk menyelaraskan ilustrasi latar belakang dalam setiap wilayah. Tindakan ini 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 meningkatkan durasi dan mengubah easing pada .color-mixed untuk menciptakan kesan bahwa perlu waktu agar warna bercampur.

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

Buka HTML5please untuk mengetahui dukungan browser saat ini dan penggunaan yang direkomendasikan untuk transisi CSS3.

JavaScript: Membuatnya Berfungsi

Menetapkan warna secara dinamis itu mudah. Kita menelusuri DOM untuk menemukan elemen apa pun dengan class warna kita dan menetapkan warna latar belakang berdasarkan pilihan warna pengguna. Kita menerapkan efek transisi ke elemen apa pun di 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: Memberi Tiga Kotak Pengalihan Warna Kepribadian

Tujuan kami adalah menciptakan efek pencahayaan yang menyenangkan dan realistis yang mempertahankan integritasnya saat warna 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 tepi yang tegas saat warna yang berbeda bertemu. Hal ini mengganggu efek pencahayaan yang realistis dan merupakan salah satu tantangan yang lebih besar saat mendesain ilustrasi.

Region Warna

Solusinya adalah mendesain ilustrasi agar tidak pernah membiarkan tepi wilayah warna terlihat melalui area transparan.

Tepi Wilayah Warna

Perencanaan untuk build sangatlah penting. Sesi perencanaan singkat antara desainer, developer, dan ilustrator membantu tim memahami cara membangun semuanya agar dapat berfungsi bersama saat dirakit.

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

Tepi Wilayah Warna

Encore

Untuk pengguna yang tidak memiliki Chrome, kami menetapkan sasaran untuk menyaring esensi aplikasi menjadi satu gambar statis. Node petak menjadi hero, ubin latar belakang menyinggung tujuan aplikasi, dan perspektif yang ada dalam refleksi mengisyaratkan lingkungan 3D yang imersif dari petak.

Warna Tepi Wilayah.

Jika Anda tertarik untuk mempelajari Technitone lebih lanjut, nantikan info terbaru di blog kami.

Band

Terima kasih telah membaca, mungkin kami akan segera bermain musik bersama Anda.