Nghiên cứu điển hình – Âm thanh của đua xe

Giới thiệu

Racer là một Thử nghiệm Chrome nhiều người chơi, nhiều thiết bị. Trò chơi ô tô chạy trên đường dốc theo phong cách cổ điển được chơi trên nhiều màn hình. Trên điện thoại hoặc máy tính bảng, Android hoặc iOS. Mọi người đều có thể tham gia. Không có ứng dụng nào. Không có bản tải xuống nào. Chỉ web dành cho thiết bị di động.

Plan8 cùng với các bạn bè tại 14islands đã tạo ra trải nghiệm âm nhạc và âm thanh sống động dựa trên bản nhạc gốc của Giorgio Moroder. Racer có âm thanh động cơ thích ứng, hiệu ứng âm thanh của cuộc đua, nhưng quan trọng hơn là bản phối nhạc động tự phân phối trên nhiều thiết bị khi các tay đua tham gia. Đây là một hệ thống lắp đặt nhiều loa bao gồm các điện thoại thông minh.

Chúng tôi đã thử kết nối nhiều thiết bị với nhau trong một thời gian. Chúng tôi đã thử nghiệm âm nhạc trong đó âm thanh sẽ phân tách trên các thiết bị khác nhau hoặc chuyển đổi giữa các thiết bị. Vì vậy, chúng tôi rất muốn áp dụng những ý tưởng đó cho Racer.

Cụ thể hơn, chúng tôi muốn kiểm thử xem liệu có thể tạo bản nhạc trên các thiết bị khi ngày càng có nhiều người tham gia trò chơi hay không, bắt đầu bằng trống và bass, sau đó thêm guitar và các nhạc cụ tổng hợp, v.v. Chúng tôi đã thực hiện một số bản minh hoạ âm nhạc và bắt đầu lập trình. Hiệu ứng nhiều loa thực sự rất đáng giá. Lúc này, chúng tôi chưa đồng bộ hoá hết, nhưng khi nghe thấy các lớp âm thanh lan tỏa trên các thiết bị, chúng tôi biết rằng mình đang làm được điều gì đó hay ho.

Tạo âm thanh

Google Creative Lab đã phác thảo hướng sáng tạo cho âm thanh và nhạc. Chúng tôi muốn sử dụng bộ tổng hợp tương tự để tạo hiệu ứng âm thanh thay vì ghi âm âm thanh thực hoặc sử dụng thư viện âm thanh. Chúng tôi cũng biết rằng loa đầu ra, trong hầu hết các trường hợp, sẽ là loa điện thoại hoặc máy tính bảng nhỏ, vì vậy, âm thanh phải được giới hạn trong phổ tần số để tránh làm méo loa. Đây là một thử thách khá lớn. Khi nhận được bản nhạc nháp đầu tiên của Giorgio, chúng tôi cảm thấy nhẹ nhõm vì bản nhạc của anh kết hợp hoàn hảo với âm thanh mà chúng tôi đã tạo.

Âm thanh động cơ

Thách thức lớn nhất trong việc lập trình âm thanh là tìm ra âm thanh động cơ tốt nhất và định hình hành vi của âm thanh đó. Đường đua giống như đường đua F1 hoặc Nascar, vì vậy, xe phải có cảm giác nhanh và mạnh mẽ. Đồng thời, những chiếc xe này rất nhỏ nên âm thanh động cơ lớn sẽ không thực sự kết nối âm thanh với hình ảnh. Chúng tôi không thể phát âm thanh động cơ gầm rú mạnh mẽ trong loa của thiết bị di động, vì vậy, chúng tôi phải tìm ra một âm thanh khác.

Để có cảm hứng, chúng tôi đã kết nối một số bộ tổng hợp mô-đun trong bộ sưu tập của bạn Jon Ekstrand và bắt đầu thử nghiệm. Chúng tôi rất thích những gì đã nghe được. Đây là âm thanh của hai bộ dao động, một số bộ lọc và LFO tuyệt vời.

Chúng tôi đã thành công trong việc thiết kế lại thiết bị tương tự bằng API Web Audio. Vì vậy, chúng tôi rất hy vọng và bắt đầu tạo một bộ tổng hợp đơn giản trong Web Audio. Âm thanh được tạo sẽ phản hồi nhanh nhất nhưng sẽ làm giảm sức mạnh xử lý của thiết bị. Chúng tôi cần phải cực kỳ tinh gọn để tiết kiệm mọi tài nguyên có thể nhằm đảm bảo hình ảnh chạy trơn tru. Vì vậy, chúng tôi đã chuyển đổi kỹ thuật để phát các mẫu âm thanh.

Máy tổng hợp mô-đun để lấy cảm hứng cho âm thanh động cơ

Có một số kỹ thuật có thể dùng để tạo âm thanh động cơ từ các mẫu. Phương pháp phổ biến nhất cho trò chơi trên máy chơi trò chơi là có một lớp nhiều âm thanh (càng nhiều càng tốt) của công cụ ở các RPM khác nhau (có tải), sau đó chuyển đổi và chuyển đổi giữa các âm thanh đó. Sau đó, thêm một lớp nhiều âm thanh của động cơ chỉ tăng tốc (không tải) ở cùng một RPM và hiệu ứng chuyển tiếp và độ cao giữa các âm thanh đó. Nếu được thực hiện đúng cách, hiệu ứng chuyển đổi giữa các lớp đó khi chuyển số sẽ nghe rất chân thực, nhưng chỉ khi bạn có một lượng lớn tệp âm thanh. Độ lệch pha không được quá lớn, nếu không âm thanh sẽ nghe rất nhân tạo. Vì chúng tôi phải tránh thời gian tải lâu nên lựa chọn này không phù hợp với chúng tôi. Chúng tôi đã thử với 5 hoặc 6 tệp âm thanh cho mỗi lớp, nhưng âm thanh không như mong đợi. Chúng tôi phải tìm cách giảm số lượng tệp.

Giải pháp hiệu quả nhất đã được chứng minh là:

  • Một tệp âm thanh có âm thanh tăng tốc và chuyển số được đồng bộ hoá với âm thanh tăng tốc của ô tô kết thúc bằng một vòng lặp được lập trình ở âm độ / RPM cao nhất. Web Audio API rất giỏi trong việc lặp lại chính xác để chúng ta có thể thực hiện việc này mà không gặp sự cố hoặc âm thanh bật lên.
  • Một tệp âm thanh có âm thanh giảm tốc / giảm tốc độ động cơ.
  • Cuối cùng, một tệp âm thanh phát âm thanh tĩnh / rảnh trong một vòng lặp.

Có dạng như sau

Đồ hoạ âm thanh động cơ

Đối với sự kiện chạm / gia tốc đầu tiên, chúng ta sẽ phát tệp đầu tiên từ đầu và nếu người chơi nhả ga, chúng ta sẽ tính thời gian từ vị trí chúng ta đang ở trong tệp âm thanh tại thời điểm nhả ga để khi ga được bật lại, tệp đó sẽ chuyển đến vị trí thích hợp trong tệp tăng tốc sau khi phát tệp thứ hai (giảm tốc).

function throttleOn(throttle) {
    //Calculate the start position depending 
    //on the current amount of throttle.
    //By multiplying throttle we get a start position 
    //between 0 and 3 seconds.
    var startPosition = throttle * 3;

    var audio = context.createBufferSource();
    audio.buffer = loadedBuffers["accelerate_and_loop"];

    //Sets the loop positions for the buffer source.
    audio.loopStart = 5;
    audio.loopEnd = 9;

    //Starts the buffer source at the current time
    //with the calculated offset.
    audio.start(context.currentTime, startPosition);
}

Hãy thử

Khởi động động cơ và nhấn nút "Throttle" (Chân ga).

<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>

Vì vậy, chỉ với 3 tệp âm thanh nhỏ và một công cụ phát âm thanh tốt, chúng tôi quyết định chuyển sang thử thách tiếp theo.

Đồng bộ hoá

Cùng với David Lindkvist của 14islands, chúng tôi bắt đầu tìm hiểu sâu hơn về cách để các thiết bị phát đồng bộ một cách hoàn hảo. Lý thuyết cơ bản rất đơn giản. Thiết bị yêu cầu máy chủ cung cấp thời gian, các yếu tố về độ trễ mạng, sau đó tính toán độ lệch đồng hồ cục bộ.

syncOffset = localTime - serverTime - networkLatency

Với độ lệch này, mỗi thiết bị được kết nối sẽ có cùng khái niệm về thời gian. Có dễ không? (Lại nói về lý thuyết.)

Tính toán độ trễ mạng

Chúng ta có thể giả định rằng độ trễ bằng một nửa thời gian cần thiết để yêu cầu và nhận phản hồi từ máy chủ:

networkLatency = (receivedTime - sentTime) × 0.5

Vấn đề với giả định này là việc đi và về đến máy chủ không phải lúc nào cũng đối xứng, tức là yêu cầu có thể mất nhiều thời gian hơn phản hồi hoặc ngược lại. Độ trễ mạng càng cao thì mức độ không cân bằng này càng lớn, khiến âm thanh bị trễ và phát không đồng bộ với các thiết bị khác.

May mắn là não bộ của chúng ta được thiết kế để không nhận thấy nếu âm thanh bị trễ một chút. Các nghiên cứu đã chỉ ra rằng phải mất từ 20 đến 30 mili giây (ms) thì não bộ mới nhận biết được các âm thanh riêng biệt. Tuy nhiên, khi độ trễ đạt khoảng 12 đến 15 mili giây, bạn sẽ bắt đầu "cảm nhận" được tác động của độ trễ tín hiệu ngay cả khi không thể "nhận biết" đầy đủ. Chúng tôi đã điều tra một số giao thức đồng bộ hoá thời gian đã thiết lập, các phương án thay thế đơn giản hơn và cố gắng triển khai một số giao thức trong thực tế. Cuối cùng, nhờ cơ sở hạ tầng có độ trễ thấp của Google, chúng tôi chỉ cần lấy mẫu một loạt yêu cầu và sử dụng mẫu có độ trễ thấp nhất làm tham chiếu.

Chống hiện tượng trễ đồng hồ

Đã thành công! Chúng tôi có hơn 5 thiết bị phát xung đồng bộ hoàn hảo – nhưng chỉ trong một thời gian ngắn. Sau khi phát trong vài phút, các thiết bị sẽ bị lệch nhau mặc dù chúng ta đã lên lịch phát âm thanh bằng thời gian ngữ cảnh API Âm thanh trên web cực kỳ chính xác. Tình trạng trễ tích luỹ dần dần, chỉ vài mili giây mỗi lần và ban đầu không phát hiện được, nhưng dẫn đến các lớp nhạc hoàn toàn không đồng bộ sau khi phát trong thời gian dài hơn. Xin chào, sự trễ đồng hồ.

Giải pháp là đồng bộ hoá lại vài giây một lần, tính toán độ lệch đồng hồ mới và truyền dữ liệu này vào trình lập lịch biểu âm thanh một cách liền mạch. Để giảm nguy cơ xảy ra thay đổi đáng kể trong âm nhạc do độ trễ mạng, chúng tôi quyết định làm mượt sự thay đổi bằng cách lưu giữ nhật ký về độ lệch đồng bộ hoá mới nhất và tính trung bình.

Lên lịch phát bài hát và chuyển đổi bản phối

Việc tạo trải nghiệm âm thanh tương tác có nghĩa là bạn không còn kiểm soát thời điểm phát các phần của bài hát nữa, vì bạn phụ thuộc vào hành động của người dùng để thay đổi trạng thái hiện tại. Chúng tôi phải đảm bảo có thể chuyển đổi giữa các bản phối trong bài hát một cách kịp thời. Điều này có nghĩa là trình lập lịch biểu phải có thể tính toán thời lượng còn lại của thanh đang phát trước khi chuyển sang bản phối tiếp theo. Cuối cùng, thuật toán của chúng ta sẽ có dạng như sau:

  • Client(1) bắt đầu bài hát.
  • Client(n) hỏi ứng dụng đầu tiên về thời điểm bắt đầu bài hát.
  • Client(n) tính toán điểm tham chiếu đến thời điểm bắt đầu bài hát bằng cách sử dụng ngữ cảnh Âm thanh trên web, tính đến syncOffset và thời gian đã trôi qua kể từ khi tạo ngữ cảnh âm thanh.
  • playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
  • Client(n) tính thời lượng bài hát đã chạy bằng cách sử dụng playDelta. Trình lập lịch phát bài hát sử dụng thông tin này để biết nên phát thanh nào trong bản sắp xếp hiện tại tiếp theo.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Để đảm bảo tính hợp lý, chúng tôi giới hạn các bản phối luôn có độ dài 8 ô nhịp và có cùng tốc độ (bản nhạc/phút).

Nhìn về phía trước

Bạn luôn phải lên lịch trước khi sử dụng setTimeout hoặc setInterval trong JavaScript. Điều này là do đồng hồ JavaScript không chính xác lắm và các lệnh gọi lại theo lịch có thể dễ dàng bị lệch vài chục mili giây trở lên do bố cục, kết xuất, thu gom rác và XMLHTTPRequests. Trong trường hợp của chúng tôi, chúng tôi cũng phải tính đến thời gian để tất cả ứng dụng khách nhận được cùng một sự kiện qua mạng.

Ảnh động âm thanh

Việc kết hợp âm thanh vào một tệp là một cách tuyệt vời để giảm số lượng yêu cầu HTTP, cả đối với Âm thanh HTML và API Âm thanh trên web. Đây cũng là cách tốt nhất để phát âm thanh một cách linh hoạt bằng đối tượng Âm thanh, vì đối tượng này không phải tải đối tượng âm thanh mới trước khi phát. Đã có một số cách triển khai hiệu quả mà chúng tôi sử dụng làm điểm xuất phát. Chúng tôi đã mở rộng sprite để hoạt động ổn định trên cả iOS và Android, cũng như xử lý một số trường hợp lạ khi thiết bị chuyển sang trạng thái ngủ.

Trên Android, các phần tử Âm thanh sẽ tiếp tục phát ngay cả khi bạn đặt thiết bị ở chế độ ngủ. Ở chế độ ngủ, việc thực thi JavaScript bị hạn chế để tiết kiệm pin và bạn không thể dựa vào requestAnimationFrame, setInterval hoặc setTimeout để kích hoạt lệnh gọi lại. Đây là vấn đề vì các sprite âm thanh dựa vào JavaScript để tiếp tục kiểm tra xem có nên dừng phát hay không. Tệ hơn nữa, trong một số trường hợp, currentTime của phần tử Âm thanh không cập nhật mặc dù âm thanh vẫn đang phát.

Hãy xem cách triển khai AudioSprite mà chúng tôi đã sử dụng trong Chrome Racer làm phương án dự phòng không phải Web Audio.

Phần tử âm thanh

Khi chúng tôi bắt đầu làm việc trên Racer, Chrome dành cho Android chưa hỗ trợ API Âm thanh trên web. Logic sử dụng Âm thanh HTML cho một số thiết bị, API Âm thanh web cho các thiết bị khác, kết hợp với đầu ra âm thanh nâng cao mà chúng tôi muốn đạt được đã tạo ra một số thách thức thú vị. Rất may, giờ đây, vấn đề này đã được giải quyết. Web Audio API được triển khai trong Android M28 beta.

  • Vấn đề về độ trễ/thời gian. Thành phần Âm thanh không phải lúc nào cũng phát chính xác khi bạn yêu cầu phát. Vì JavaScript là luồng đơn nên trình duyệt có thể bị bận, gây ra độ trễ phát lên đến 2 giây.
  • Độ trễ phát có nghĩa là không phải lúc nào bạn cũng có thể phát lặp lại một cách mượt mà. Trên máy tính, bạn có thể sử dụng tính năng vùng đệm đôi để đạt được các vòng lặp gần như không có khoảng trống, nhưng trên thiết bị di động, bạn không thể sử dụng tính năng này vì:
    • Hầu hết các thiết bị di động sẽ không phát nhiều phần tử Âm thanh cùng một lúc.
    • Âm lượng cố định. Cả Android và iOS đều không cho phép bạn thay đổi âm lượng của đối tượng Âm thanh.
  • Không tải trước. Trên thiết bị di động, phần tử Âm thanh sẽ không bắt đầu tải nguồn trừ khi quá trình phát được bắt đầu trong trình xử lý touchStart.
  • Tìm vấn đề. Việc lấy duration hoặc đặt currentTime sẽ không thành công trừ phi máy chủ của bạn hỗ trợ HTTP Byte-Range. Hãy chú ý đến vấn đề này nếu bạn đang tạo một sprite âm thanh như chúng tôi đã làm.
  • Không xác thực được bằng phương thức Xác thực cơ bản trên MP3. Một số thiết bị không tải được tệp MP3 được bảo vệ bằng phương thức Xác thực cơ bản, bất kể bạn đang sử dụng trình duyệt nào.

Kết luận

Chúng ta đã đi một chặng đường dài kể từ khi nút tắt tiếng là lựa chọn tốt nhất để xử lý âm thanh trên web, nhưng đây mới chỉ là bước khởi đầu và âm thanh trên web sắp bùng nổ. Chúng ta mới chỉ tìm hiểu sơ bộ về những việc có thể làm khi đồng bộ hoá nhiều thiết bị. Chúng tôi không có sức mạnh xử lý trong điện thoại và máy tính bảng để đi sâu vào xử lý tín hiệu và hiệu ứng (như âm vang), nhưng khi hiệu suất thiết bị tăng lên, các trò chơi dựa trên web cũng sẽ tận dụng các tính năng đó. Đây là thời điểm thú vị để tiếp tục đẩy mạnh khả năng của âm thanh.