Phát triển âm thanh trong trò chơi bằng Web Audio API

Giới thiệu

Âm thanh là một phần quan trọng tạo nên trải nghiệm đa phương tiện trở nên hấp dẫn. Nếu từng thử xem phim mà tắt tiếng, bạn có thể đã nhận thấy điều này.

Trò chơi cũng không phải là ngoại lệ! Kỷ niệm đáng nhớ nhất của tôi về trò chơi điện tử là âm nhạc và hiệu ứng âm thanh. Giờ đây, trong nhiều trường hợp, gần hai thập kỷ sau khi chơi trò chơi mình yêu thích, tôi vẫn không thể quên bản nhạc Zelda của Koji Kondonhạc phim Diablo đầy không khí của Matt Uelmen. Mức độ hấp dẫn tương tự cũng áp dụng cho hiệu ứng âm thanh, chẳng hạn như phản hồi nhấp chuột theo đơn vị có thể nhận ra ngay lập tức từ Warcraft và các mẫu từ các trò chơi kinh điển của Nintendo.

Âm thanh trò chơi mang đến một số thách thức thú vị. Để tạo ra âm nhạc trò chơi thuyết phục, nhà thiết kế cần điều chỉnh theo trạng thái trò chơi có thể khó dự đoán mà người chơi đang gặp phải. Trong thực tế, một số phần của trò chơi có thể diễn ra trong một khoảng thời gian không xác định, âm thanh có thể tương tác với môi trường và kết hợp theo những cách phức tạp, chẳng hạn như hiệu ứng phòng và vị trí âm thanh tương đối. Cuối cùng, có thể có một số lượng lớn âm thanh phát cùng lúc, tất cả các âm thanh này cần phải kết hợp với nhau và kết xuất được mà không gây ra các hình phạt về hiệu suất.

Âm thanh trò chơi trên web

Đối với các trò chơi đơn giản, bạn có thể chỉ cần sử dụng thẻ <audio>. Tuy nhiên, nhiều trình duyệt triển khai không tốt, dẫn đến âm thanh bị lỗi và độ trễ cao. Hy vọng đây là vấn đề tạm thời vì các nhà cung cấp đang nỗ lực cải thiện việc triển khai tương ứng. Để xem nhanh trạng thái của thẻ <audio>, có một bộ kiểm thử rất hay tại areweplayingyet.org.

Tuy nhiên, khi xem xét kỹ hơn thông số kỹ thuật của thẻ <audio>, bạn sẽ thấy rõ rằng có nhiều việc đơn giản là không thể thực hiện được với thẻ này. Điều này cũng không có gì đáng ngạc nhiên vì thẻ này được thiết kế để phát nội dung nghe nhìn. Sau đây là một số giới hạn:

  • Không thể áp dụng bộ lọc cho tín hiệu âm thanh
  • Không có cách nào để truy cập vào dữ liệu PCM thô
  • Không có khái niệm về vị trí và hướng của nguồn và trình nghe
  • Không có thời gian chi tiết.

Trong phần còn lại của bài viết, tôi sẽ đi sâu vào một số chủ đề trong số này trong bối cảnh âm thanh trò chơi được viết bằng API Âm thanh trên web. Để biết thông tin giới thiệu ngắn gọn về API này, hãy xem hướng dẫn bắt đầu.

Nhạc nền

Trò chơi thường có nhạc nền phát lặp lại.

Điều này có thể gây khó chịu nếu vòng lặp của bạn ngắn và dễ đoán. Nếu người chơi bị mắc kẹt trong một khu vực hoặc cấp độ và cùng một mẫu nhạc liên tục phát trong nền, thì bạn nên làm mờ dần đường đi để tránh gây thêm phiền hà. Một chiến lược khác là sử dụng kết hợp các cường độ khác nhau dần dần chuyển đổi sang một bên khác, tuỳ thuộc vào bối cảnh của trò chơi.

Ví dụ: nếu người chơi của bạn đang ở trong một khu vực có trận chiến trùm hoành tráng, bạn có thể có một số bản phối khác nhau về phạm vi cảm xúc, từ âm thanh tạo không khí đến âm thanh báo trước và âm thanh dữ dội. Phần mềm tổng hợp âm nhạc thường cho phép bạn xuất nhiều bản phối (có cùng độ dài) dựa trên một bản nhạc bằng cách chọn nhóm các bản nhạc để sử dụng trong quá trình xuất. Bằng cách đó, bạn có một số tính nhất quán nội bộ và tránh được các chuyển đổi chói tai khi chuyển đổi từ bản nhạc này sang bản nhạc khác.

Garageband

Sau đó, bằng cách sử dụng Web Audio API, bạn có thể nhập tất cả các mẫu này bằng cách sử dụng các lớp như BufferLoader qua XHR (phần này được trình bày chi tiết trong bài viết giới thiệu về API Web Audio. Việc tải âm thanh sẽ mất nhiều thời gian, vì vậy, các tài sản dùng trong trò chơi phải được tải khi tải trang, ở đầu màn chơi hoặc có thể là tăng dần trong khi người chơi đang chơi.

Tiếp theo, bạn tạo một nguồn cho mỗi nút và một nút tăng cường cho mỗi nguồn, sau đó kết nối biểu đồ.

Sau khi thực hiện việc này, bạn có thể phát đồng thời tất cả các nguồn này trong một vòng lặp và vì tất cả các nguồn này đều có cùng độ dài, nên Web Audio API sẽ đảm bảo rằng các nguồn này sẽ luôn được căn chỉnh. Khi nhân vật tiến gần hoặc xa hơn trận chiến trùm cuối, trò chơi có thể thay đổi các giá trị tăng cho từng nút tương ứng trong chuỗi, bằng cách sử dụng thuật toán lượng tăng như sau:

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

Trong phương pháp trên, hai nguồn phát cùng một lúc và chúng ta chuyển đổi giữa các nguồn đó bằng cách sử dụng các đường cong công suất bằng nhau (như mô tả trong phần giới thiệu).

Ngày nay, nhiều nhà phát triển trò chơi sử dụng thẻ <audio> cho nhạc nền vì thẻ này phù hợp với việc phát trực tuyến nội dung. Giờ đây, bạn có thể đưa nội dung từ thẻ <audio> vào ngữ cảnh Âm thanh trên web.

Kỹ thuật này có thể hữu ích vì thẻ <audio> có thể hoạt động với nội dung phát trực tuyến, cho phép bạn phát nhạc nền ngay lập tức thay vì phải đợi tải toàn bộ nội dung xuống. Bằng cách đưa luồng vào API Âm thanh trên web, bạn có thể thao tác hoặc phân tích luồng. Ví dụ sau đây áp dụng bộ lọc thông thấp cho bản nhạc phát qua thẻ <audio>:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

Để thảo luận đầy đủ hơn về cách tích hợp thẻ <audio> với API Âm thanh trên web, hãy xem bài viết ngắn này.

Hiệu ứng âm thanh

Các trò chơi thường phát lại hiệu ứng âm thanh để phản hồi hoạt động đầu vào của người dùng hoặc các thay đổi trong trạng thái trò chơi. Tuy nhiên, giống như nhạc nền, hiệu ứng âm thanh có thể gây khó chịu rất nhanh. Để tránh điều này, bạn thường nên có một nhóm âm thanh tương tự nhưng khác nhau để phát. Điều này có thể thay đổi từ các biến thể nhẹ của mẫu bước chân đến các biến thể mạnh mẽ, như trong loạt Warcraft khi phản hồi việc nhấp vào các đơn vị.

Một đặc điểm quan trọng khác của hiệu ứng âm thanh trong trò chơi là có thể có nhiều hiệu ứng âm thanh cùng lúc. Hãy tưởng tượng bạn đang ở giữa một trận đấu súng và nhiều diễn viên đang bắn súng máy. Mỗi khẩu súng máy bắn nhiều lần mỗi giây, khiến hàng chục hiệu ứng âm thanh được phát cùng lúc. Việc phát âm thanh từ nhiều nguồn được căn chỉnh thời gian chính xác cùng một lúc là một trong những điểm mạnh của API Âm thanh trên web.

Ví dụ sau đây tạo một loạt súng máy từ nhiều mẫu đạn riêng lẻ bằng cách tạo nhiều nguồn âm thanh có thời gian phát lồng ghép.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

Giờ đây, nếu tất cả các khẩu súng máy trong trò chơi của bạn đều có âm thanh giống hệt như vậy, thì trò chơi sẽ khá nhàm chán. Tất nhiên, các âm thanh này sẽ khác nhau tuỳ theo âm thanh dựa trên khoảng cách từ mục tiêu và vị trí tương đối (sẽ nói thêm về vấn đề này sau), nhưng ngay cả điều đó có thể vẫn chưa đủ. May mắn thay, Web Audio API cung cấp một cách dễ dàng để điều chỉnh ví dụ ở trên theo hai cách:

  1. Với sự thay đổi tinh tế về thời gian giữa các viên đạn bắn ra
  2. Bằng cách thay đổi playbackRate của mỗi mẫu (cũng thay đổi độ cao) để mô phỏng tốt hơn tính ngẫu nhiên của thế giới thực.

Để biết ví dụ thực tế hơn về cách áp dụng các kỹ thuật này, hãy xem Bản minh hoạ Bida. Bản minh hoạ này sử dụng tính năng lấy mẫu ngẫu nhiên và thay đổi playbackRate để tạo ra âm thanh va chạm bóng thú vị hơn.

Âm thanh định vị 3D

Trò chơi thường được đặt trong một thế giới có một số thuộc tính hình học, ở dạng 2D hoặc 3D. Trong trường hợp này, âm thanh được định vị âm thanh nổi có thể tăng đáng kể mức độ sống động của trải nghiệm. May mắn thay, Web Audio API đi kèm với các tính năng âm thanh vị trí tăng tốc phần cứng tích hợp sẵn khá dễ sử dụng. À, bạn phải đảm bảo rằng mình có loa âm thanh nổi (tốt nhất là tai nghe) để xem ví dụ sau đây sao cho có ý nghĩa.

Trong ví dụ trên, có một trình nghe (biểu tượng người) ở giữa canvas và chuột ảnh hưởng đến vị trí của nguồn (biểu tượng loa). Trên đây là ví dụ đơn giản về cách sử dụng AudioPannerNode để đạt được loại hiệu ứng này. Ý tưởng cơ bản của mẫu trên là phản hồi chuyển động của chuột bằng cách đặt vị trí của nguồn âm thanh như sau:

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

Những điều cần biết về cách xử lý âm thanh không gian của Web Audio:

  • Trình nghe nằm ở gốc (0, 0, 0) theo mặc định.
  • API vị trí Web Audio không có đơn vị, vì vậy, tôi đã giới thiệu một hệ số để âm thanh của bản minh hoạ trở nên tốt hơn.
  • Web Audio sử dụng toạ độ Descartes y-is-up (đối lập với hầu hết các hệ thống đồ hoạ máy tính). Đó là lý do tôi hoán đổi trục y trong đoạn mã trên

Nâng cao: hình nón âm thanh

Mô hình vị trí này rất mạnh mẽ và khá tiên tiến, chủ yếu dựa trên OpenAL. Để biết thêm thông tin chi tiết, hãy xem phần 3 và 4 của thông số kỹ thuật được liên kết ở trên.

Mô hình vị trí

Có một AudioListener duy nhất được đính kèm vào ngữ cảnh API Âm thanh trên web. Bạn có thể định cấu hình AudioListener trong không gian thông qua vị trí và hướng. Mỗi nguồn có thể được truyền qua AudioPannerNode, giúp tạo âm thanh không gian cho âm thanh đầu vào. Nút kéo có vị trí và hướng, cũng như mô hình khoảng cách và hướng.

Mô hình khoảng cách chỉ định mức độ tăng cường tuỳ thuộc vào khoảng cách đến nguồn, trong khi mô hình hướng có thể được định cấu hình bằng cách chỉ định một hình nón bên trong và bên ngoài, xác định mức độ tăng cường (thường là âm) nếu trình nghe nằm trong hình nón bên trong, giữa hình nón bên trong và bên ngoài hoặc bên ngoài hình nón bên ngoài.

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

Mặc dù ví dụ của tôi ở dạng 2D, nhưng mô hình này dễ dàng khái quát hoá cho chiều thứ ba. Để xem ví dụ về âm thanh được tạo không gian ở chế độ 3D, hãy xem mẫu vị trí này. Ngoài vị trí, mô hình âm thanh Web Audio cũng có thể bao gồm tốc độ cho các hiệu ứng doppler (thay đổi doppler). Ví dụ này cho thấy hiệu ứng Doppler chi tiết hơn.

Để biết thêm thông tin về chủ đề này, hãy đọc hướng dẫn chi tiết này về việc [kết hợp âm thanh vị trí và WebGL][webgl].

Bộ lọc và hiệu ứng của Room

Trong thực tế, cách cảm nhận âm thanh phụ thuộc rất nhiều vào căn phòng mà âm thanh đó được nghe thấy. Cùng một tiếng cửa kẽo kẹt sẽ có âm thanh rất khác nhau trong một tầng hầm so với một sảnh lớn, rộng mở. Những trò chơi có giá trị sản xuất cao sẽ muốn bắt chước những hiệu ứng này, vì việc tạo một tập hợp mẫu riêng biệt cho mỗi môi trường là quá tốn kém, đồng thời sẽ dẫn đến nhiều tài sản hơn và lượng dữ liệu trò chơi lớn hơn.

Nói một cách đơn giản, thuật ngữ âm thanh cho sự khác biệt giữa âm thanh thô và âm thanh thực tế là phản hồi xung. Bạn có thể ghi lại các phản hồi xung này một cách cẩn thận và thực tế là có các trang web lưu trữ nhiều tệp phản hồi xung được ghi sẵn (được lưu trữ dưới dạng âm thanh) để thuận tiện cho bạn.

Để biết thêm nhiều thông tin về cách tạo phản hồi xung từ một môi trường nhất định, hãy đọc phần "Thiết lập bản ghi" trong phần Convolution (Trộn) của thông số kỹ thuật API Âm thanh trên web.

Quan trọng hơn đối với mục đích của chúng ta, Web Audio API cung cấp một cách dễ dàng để áp dụng các phản hồi xung này cho âm thanh của chúng ta bằng cách sử dụng ConvolverNode.

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

Ngoài ra, hãy xem bản minh hoạ hiệu ứng phòng này trên trang thông số kỹ thuật của API Âm thanh trên web, cũng như ví dụ này giúp bạn kiểm soát việc kết hợp âm thanh khô (thô) và ướt (được xử lý qua bộ chuyển đổi) của một bản nhạc Jazz chuẩn tuyệt vời.

Đếm ngược đến thời điểm kết thúc

Vậy là bạn đã tạo một trò chơi, định cấu hình âm thanh vị trí và giờ đây, bạn có một số lượng lớn AudioNodes trong biểu đồ, tất cả đều phát đồng thời. Tuyệt vời, nhưng vẫn còn một điều nữa cần cân nhắc:

Vì nhiều âm thanh chỉ xếp chồng lên nhau mà không có quá trình chuẩn hoá, nên bạn có thể thấy mình đang vượt quá ngưỡng khả năng của loa. Giống như hình ảnh vượt quá ranh giới của canvas, âm thanh cũng có thể bị cắt nếu dạng sóng vượt quá ngưỡng tối đa, tạo ra sự méo tiếng rõ ràng. Hình dạng sóng sẽ có dạng như sau:

Cắt

Đây là một ví dụ thực tế về tính năng tạo đoạn video. Hình dạng sóng không đẹp:

Cắt

Điều quan trọng là bạn phải nghe những âm thanh bị méo quá mức như ví dụ trên, hoặc ngược lại, những bản phối quá nhỏ khiến người nghe phải tăng âm lượng. Nếu đang gặp phải tình huống này, bạn thực sự cần khắc phục!

Phát hiện tình trạng cắt bớt

Từ góc độ kỹ thuật, hiện tượng cắt xảy ra khi giá trị của tín hiệu trong bất kỳ kênh nào vượt quá phạm vi hợp lệ, cụ thể là từ -1 đến 1. Sau khi phát hiện điều này, bạn nên cung cấp phản hồi trực quan cho người dùng về việc này. Để thực hiện việc này một cách đáng tin cậy, hãy đặt JavaScriptAudioNode vào biểu đồ. Biểu đồ âm thanh sẽ được thiết lập như sau:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

Và bạn có thể phát hiện hoạt động cắt trong trình xử lý processAudio sau:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

Nhìn chung, hãy cẩn thận không sử dụng quá nhiều JavaScriptAudioNode vì lý do hiệu suất. Trong trường hợp này, phương thức triển khai đo sáng thay thế có thể thăm dò RealtimeAnalyserNode trong biểu đồ âm thanh cho getByteFrequencyData, tại thời điểm kết xuất, do requestAnimationFrame xác định. Phương pháp này hiệu quả hơn, nhưng bỏ lỡ hầu hết tín hiệu (bao gồm cả những nơi có thể bị cắt), vì quá trình kết xuất diễn ra tối đa 60 lần/giây, trong khi tín hiệu âm thanh thay đổi nhanh hơn nhiều.

Vì tính năng phát hiện đoạn nhạc rất quan trọng, nên có thể chúng ta sẽ thấy một nút API Âm thanh trên web MeterNode tích hợp sẵn trong tương lai.

Ngăn chặn việc cắt bớt

Bằng cách điều chỉnh độ lợi trên AudioGainNode chính, bạn có thể giảm âm lượng phối âm xuống mức ngăn chặn hiện tượng cắt. Tuy nhiên, trong thực tế, vì âm thanh phát trong trò chơi của bạn có thể phụ thuộc vào rất nhiều yếu tố, nên bạn có thể khó quyết định giá trị tăng âm thanh chính để ngăn chặn việc cắt bớt cho tất cả các trạng thái. Nhìn chung, bạn nên điều chỉnh lợi nhuận để dự đoán trường hợp xấu nhất, nhưng đây là một nghệ thuật hơn là một khoa học.

Thêm một chút đường

Bộ nén thường được dùng trong quá trình sản xuất nhạc và trò chơi để làm mượt tín hiệu và kiểm soát các điểm tín hiệu tăng đột biến trong tín hiệu tổng thể. Chức năng này có trong Web Audio thông qua DynamicsCompressorNode (bạn có thể chèn âm thanh này vào biểu đồ âm thanh để có âm thanh to hơn, phong phú và đầy đủ hơn, đồng thời cũng hỗ trợ cắt đoạn. Trích dẫn trực tiếp thông số kỹ thuật, nút này

Nhìn chung, bạn nên sử dụng tính năng nén động, đặc biệt là trong bối cảnh trò chơi, khi mà như đã thảo luận trước đó, bạn không biết chính xác âm thanh nào sẽ phát và khi nào. Plink của phòng thí nghiệm DinahMoe là một ví dụ tuyệt vời về điều này, vì âm thanh phát lại hoàn toàn phụ thuộc vào bạn và những người tham gia khác. Bộ nén hữu ích trong hầu hết các trường hợp, ngoại trừ một số trường hợp hiếm hoi, khi bạn xử lý các bản nhạc đã được xử lý kỹ lưỡng và đã được điều chỉnh để có âm thanh "vừa phải".

Việc triển khai tính năng này chỉ là đưa DynamicsCompressorNode vào biểu đồ âm thanh, thường là nút cuối cùng trước đích đến.:

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

Để biết thêm thông tin chi tiết về tính năng nén động, bài viết này trên Wikipedia rất hữu ích.

Tóm lại, hãy nghe kỹ để phát hiện và ngăn chặn hiện tượng cắt bằng cách chèn một nút tăng âm lượng chính. Sau đó, hãy thắt chặt toàn bộ bản phối bằng cách sử dụng nút bộ nén động. Biểu đồ âm thanh của bạn có thể có dạng như sau:

Kết quả cuối cùng

Kết luận

Đó là những khía cạnh quan trọng nhất mà tôi cho là cần thiết trong quá trình phát triển âm thanh trò chơi bằng API Web âm thanh. Với các kỹ thuật này, bạn có thể tạo ra trải nghiệm âm thanh thực sự hấp dẫn ngay trong trình duyệt. Trước khi kết thúc, hãy để tôi chia sẻ với bạn một mẹo dành riêng cho trình duyệt: hãy nhớ tạm dừng âm thanh nếu thẻ của bạn chuyển sang chế độ nền bằng cách sử dụng API chế độ hiển thị trang, nếu không, bạn sẽ tạo ra trải nghiệm có thể gây khó chịu cho người dùng.

Để biết thêm thông tin về Âm thanh web, hãy xem thêm bài viết giới thiệu về cách bắt đầu sử dụng. Nếu bạn có thắc mắc, hãy xem câu hỏi đó đã có câu trả lời trong phần Câu hỏi thường gặp về âm thanh trên web hay chưa. Cuối cùng, nếu bạn có thêm câu hỏi, hãy đặt câu hỏi trên Stack Overflow bằng cách sử dụng thẻ web-audio.

Trước khi tôi đăng xuất, tôi sẽ giới thiệu cho bạn một số cách sử dụng tuyệt vời của API Web Audio trong các trò chơi thực tế hôm nay: