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

Giới thiệu

Racer là Thử nghiệm Chrome gồm nhiều người chơi, nhiều thiết bị. Trò chơi ô tô đánh bạc theo phong cách hoài cổ đượ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ần tải xuống. Chỉ web dành cho thiết bị di động.

Plan8 cùng với những người bạn tại 14islands đã tạo nên trải nghiệm âm nhạc và âm thanh sôi động dựa trên bản sáng tác gốc của Giorgio Moroder. Trò chơi đua xe có âm thanh động cơ phản hồi nhanh, hiệu ứng âm thanh đua xe, nhưng quan trọng hơn là một danh sách kết hợp âm nhạc sôi động giúp phân phối trên nhiều thiết bị khi những tay đua tham gia. Đó là hệ thống nhiều loa được lắp đặt từ điện thoại thông minh.

Chúng tôi đã từng nghĩ đến việc kết nối nhiều thiết bị với nhau. Chúng tôi đã thực hiện các thử nghiệm âm nhạc trong đó âm thanh sẽ bị tách ra 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 đó vào Racer.

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

Tạo âm thanh

Google Creative Lab đã đưa ra hướng sáng tạo về âm thanh và âm nhạc. Chúng tôi muốn sử dụng bộ tổng hợp analog để tạo hiệu ứng âm thanh thay vì ghi lại âm thanh thực hoặc 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à một 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 loa bị méo. Việc này được chứng minh là khá khó khăn. Khi nhận được những bản nháp âm nhạc đầu tiên từ Giorgio, chúng tôi cảm thấy thật nhẹ nhõm vì sáng tác của ông hợp với những âm thanh mà chúng tôi tạo ra.

Â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 tốt nhất của động cơ và điêu khắc hành vi của nó. Đường đua giống với đường đua F1 hoặc Nascar, vì vậy những chiếc xe phải mang lại cảm giác nhanh và bùng nổ. Đồng thời, những chiếc xe thực sự 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. Dù sao thì chúng tôi cũng không thể có một công cụ phát ra âm thanh mạnh mẽ trên loa di động, vì vậy chúng tôi phải tìm cách khác.

Để tìm cảm hứng, chúng tôi kết nối với bộ sưu tập nhạc tổng hợp mô-đun của một số người bạn Jon Ekstrand và bắt đầu chơi đùa. Chúng tôi thích nội dung mình nghe được. Đây là âm thanh của âm thanh với hai bộ dao động, một số bộ lọc đẹp và LFO.

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

Âm thanh tổng hợp theo mô-đun để lấy cảm hứng từ âm thanh của động cơ

Có một số kỹ thuật có thể sử dụng để tạo âm thanh công cụ từ mẫu. Phương pháp phổ biến nhất cho các trò chơi trên máy chơi game là áp dụng một lớp nhiều âm thanh (càng nhiều càng tốt) của động cơ ở các tốc độ vòng/phút khác nhau (khi tải), sau đó chuyển dần và chuyển vùng âm thanh. Sau đó, thêm một lớp gồm nhiều âm thanh của động cơ vừa quay vòng (không tải) ở cùng một vòng quay/phút, đồng thời chuyển đổi và chuyển vùng âm thanh giữa các âm thanh. Việc chuyển đổi giữa các lớp đó khi chuyển số, nếu được thực hiện đúng cách, sẽ nghe rất chân thực nhưng chỉ khi bạn có một số lượng lớn tệp âm thanh. Vạch chéo không được quá rộng, nếu không sẽ nghe có vẻ rất tổng hợp. 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 năm hoặc sáu tệp âm thanh cho mỗi lớp, nhưng âm thanh thật đáng thất vọng. Chúng tôi phải tìm cách có ít tệp hơn.

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

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

Giống như thế này

Đồ hoạ âm thanh của công cụ

Đối với sự kiện chạm / tăng tốc đầu tiên, chúng tôi sẽ phát tệp đầu tiên từ đầu và nếu người chơi thả ga, chúng tôi sẽ tính thời gian từ vị trí của chúng tôi trong tệp âm thanh được phát hành để khi van tiết lưu hoạt động trở lại, nó sẽ nhảy đến đúng vị trí trong tệp tăng tốc sau khi tệp thứ hai (quay xuống) được phát.

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 vào nút "Bi tiết lưu".

<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 ba tệp âm thanh nhỏ và một công cụ tạo âm thanh tốt, chúng tôi đã quyết định chuyển sang thử thách tiếp theo.

Đang tải dữ liệu đồng bộ hóa

Cùng với David Lindkvist của kênh 14islands, chúng tôi bắt đầu xem xét sâu hơn về việc các thiết bị có thể 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ị sẽ 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 đều có cùng khái niệm về thời gian. Thật dễ dàng đúng không? (Một lần nữa, về lý thuyết.)

Đang tính toán độ trễ mạng

Chúng tôi có thể giả định rằng độ trễ là 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à quá trình trả về 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ì ảnh hưởng bất đối xứ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.

Thật may là bộ não của chúng ta có khả năng không phát hiện được âm thanh có bị trễ đôi chút hay không. Các nghiên cứu đã chỉ ra rằng phải mất từ 20 đến 30 mili giây (mili giây) trước khi não của chúng ta nhận biết âm thanh là tách biệt. Tuy nhiên, trong khoảng 12 đến 15 mili giây, bạn sẽ bắt đầu "cảm nhận" được tác động của một tín hiệu bị trễ ngay cả khi bạn không thể "nhận thức" được nó hoàn toàn. Chúng tôi đã nghiên cứu một vài 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 có thể 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.

Trượt đồng hồ chiến đấu

Cách này hiệu quả! Chúng tôi có hơn 5 thiết bị phát xung nhịp đồng bộ hoàn hảo, nhưng chỉ trong một thời gian. Sau khi phát vài phút, thiết bị sẽ trôi rời nhau ngay cả khi chúng tôi đã lên lịch âm thanh bằng thời gian theo bối cảnh của API Web Audio có độ chính xác cao. Độ trễ tích luỹ chậm, chỉ vài mili giây mỗi lần và lúc đầu không thể 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 khoảng thời gian dài hơn. Xin chào, đồng hồ trôi.

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

Lên lịch bài hát và chuyển đổi nhạ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 được thời điểm phát các phần của bài hát, 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 chuyển đổi kịp thời giữa các cách sắp xếp trong bài hát. Điều này nghĩa là bộ lập lịch biểu của chúng tôi phải tính được thời lượng còn lại của thanh đang phát trước khi chuyển sang cách sắp xếp tiếp theo. Thuật toán của chúng tôi đã có dạng như sau:

  • Client(1) bắt đầu bài hát.
  • Client(n) hỏi khách hàng đầu tiên khi bài hát bắt đầu.
  • Client(n) tính toán điểm tham chiếu đến thời điểm bài hát bắt đầu bằng bối cảnh Âm thanh trên web, tính đến SyncOffset và khoảng thời gian trôi qua kể từ khi bối cảnh âm thanh được tạo.
  • 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 biểu của bài hát sử dụng thanh này để biết thanh nào trong bản sắp xếp hiện tại sẽ được phát tiếp theo.
  • playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars

Để đảm bảo sự tỉnh táo, chúng tôi đã giới hạn cách sắp xếp nhạc của mình sao cho luôn dài 8 thanh và có cùng nhịp điệu (nhịp đập mỗi phút).

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

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

Âm thanh sprite

Kết hợp âm thanh vào một tệp là một cách tuyệt vời để giảm yêu cầu HTTP, cho cả HTML Audio và Web Audio API. Đây cũng là cách tốt nhất để phát âm thanh một cách thích ứng bằng đối tượng Âm thanh, vì đối tượng này không cần phải tải đối tượng âm thanh mới trước khi phát. Hiện đã 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ến thiết bị ngủ.

Trên Android, các thành phần Âm thanh sẽ tiếp tục phát ngay cả khi bạn chuyển thiết bị sang chế độ ngủ. Ở chế độ ngủ, quá trình thực thi JavaScript bị giới hạn để tiết kiệm pin và bạn không thể dựa vào requestAnimationFrame, setInterval hoặc setTimeout để kích hoạt các lệnh gọi lại. Đây là vấn đề do 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 dưới dạng phương án dự phòng không phải là Âm thanh trên web.

Phần tử âm thanh

Khi chúng tôi bắt đầu phát triển Racer, Chrome dành cho Android chưa hỗ trợ API Web Audio. Logic của việc sử dụng Âm thanh HTML cho một số thiết bị, API Web âm thanh 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 đã đưa ra một số thách thức thú vị. Thật may, giờ đây mọi thứ đã đi vào lịch sử. API Web âm thanh được triển khai trong Android M28 beta.

  • Vấn đề về độ trễ/thời gian. Không phải lúc nào phần tử Âm thanh cũng phát chính xác khi bạn yêu cầu phát. Vì JavaScript là đơn luồng nên trình duyệt có thể bận, gây ra độ trễ phát lên tới 2 giây.
  • Độ trễ khi phát đồng nghĩa với việc không phải lúc nào cũng có thể lặp lại mượt mà. Trên máy tính, bạn có thể sử dụng tính năng lưu vào bộ đệm kép để đạt được vòng lặp không có khoảng trống, nhưng trên thiết bị di động thì không thể làm như vậy vì:
    • Hầu hết thiết bị di động sẽ không phát nhiều thành phần Â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ừ phi bạn bắt đầu phát trong trình xử lý touchStart.
  • Đang tìm vấn đề. Không tải được duration hoặc cài đặt currentTime trừ phi máy chủ của bạn hỗ trợ Dải byte HTTP. Hãy cẩn trọng với phần này nếu bạn đang xây dựng ảnh sprite như chúng tôi đã làm.
  • Không xác thực được cơ bản trên MP3. Một số thiết bị không thể tải tệp MP3 được bảo vệ bởi tính năng 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 tôi đã đi được một chặng đường dài kể từ khi nhấn nút tắt tiếng với tư cách là lựa chọn tốt nhất để xử lý âm thanh cho web, nhưng đây chỉ là bước khởi đầu và âm thanh trên web sẽ trở nên khó khăn. Chúng tôi mới chỉ đề cập đến những việc có thể thực hiện liên quan đến việc đồng bộ hoá nhiều thiết bị. Chúng tôi không có khả năng xử lý trên điện thoại và máy tính bảng để tìm hiểu sâu về việc xử lý tín hiệu và hiệu ứng (như âm vang), nhưng khi hiệu suất của thiết bị tăng lên, các trò chơi dựa trên nền tảng web cũng sẽ tận dụng được những tính năng đó. Đây là những thời điểm thú vị để tiếp tục khai thác tiềm năng của âm thanh.