Câu chuyện về hai chiếc đồng hồ

Lên lịch cho âm thanh trên web một cách chính xác

Chris Wilson
Chris Wilson

Giới thiệu

Một trong những thách thức lớn nhất trong việc xây dựng phần mềm âm thanh và âm nhạc tuyệt vời bằng nền tảng web là quản lý thời gian. Không giống như "thời gian viết mã", mà giống như thời gian đồng hồ - một trong những chủ đề khó hiểu nhất về Âm thanh web là cách hoạt động đúng cách với đồng hồ âm thanh. Đối tượng Web Audio AudioContext có một thuộc tính currentTime hiển thị đồng hồ âm thanh này.

Đặc biệt đối với các ứng dụng âm nhạc của âm thanh trên web – không chỉ để viết bộ nối âm và bộ tổng hợp, mà còn sử dụng các sự kiện âm thanh theo nhịp điệu như máy chơi trống, trò chơicác ứng dụng khác – điều quan trọng là phải có thời gian nhất quán, chính xác cho các sự kiện âm thanh; không chỉ bắt đầu và dừng âm thanh mà còn cả việc lên lịch thay đổi âm thanh (như thay đổi tần số hoặc âm lượng). Đôi khi, bạn nên có các sự kiện được sắp xếp ngẫu nhiên một chút về thời gian (ví dụ: trong bản minh hoạ súng máy trong Phát triển âm thanh trò chơi bằng API âm thanh web), nhưng thông thường, chúng ta muốn có thời gian nhất quán và chính xác cho các nốt nhạc.

Chúng tôi đã hướng dẫn bạn cách lên lịch cho các ghi chú bằng tham số thời gian của phương thức ghi chú Âm thanh trên web và ghi chú (hiện được đổi tên thành bắt đầu và dừng) trong phần Bắt đầu với âm thanh trên web và trong phần Phát triển âm thanh trò chơi bằng API âm thanh trên web. Tuy nhiên, chúng tôi chưa tìm hiểu sâu về các tình huống phức tạp hơn, chẳng hạn như phát các đoạn nhạc hoặc giai điệu dài. Để tìm hiểu sâu hơn, trước tiên chúng ta cần một chút thông tin cơ bản về đồng hồ.

Tốt nhất của thời đại – Đồng hồ âm thanh trên web

Web Audio API cho phép truy cập vào đồng hồ phần cứng của hệ thống con âm thanh. Đồng hồ này được hiển thị trên đối tượng AudioContext thông qua thuộc tính .currentTime, dưới dạng một số giây dấu phẩy động kể từ khi AudioContext được tạo. Điều này giúp chiếc đồng hồ này (sau đây gọi là "đồng hồ âm thanh") có độ chính xác rất cao; đồng hồ được thiết kế để có thể chỉ định sự căn chỉnh ở từng cấp độ mẫu âm thanh, ngay cả với tốc độ lấy mẫu cao. Vì có khoảng 15 chữ số thập phân với độ chính xác trong dữ liệu "cặp đôi", ngay cả khi đồng hồ âm thanh đã chạy nhiều ngày, nên vẫn còn nhiều bit để trỏ đến một mẫu cụ thể ngay cả ở tốc độ lấy mẫu cao.

Đồng hồ âm thanh dùng để lên lịch tham số và sự kiện âm thanh trên toàn bộ API Web Audio – tất nhiên là cho start()stop(), nhưng cũng cho phương thức set*ValueAtTime() trên AudioParams. Điều này cho phép chúng tôi thiết lập trước các sự kiện âm thanh được xác định thời gian rất chính xác. Trên thực tế, bạn sẽ rất muốn thiết lập mọi thứ trong tính năng Âm thanh trên web làm thời gian bắt đầu/kết thúc. Tuy nhiên, trong thực tế, việc này có vấn đề.

Ví dụ: hãy xem đoạn mã rút gọn này trong Giới thiệu về âm thanh trên web của chúng tôi, đoạn mã này thiết lập hai thanh của mẫu hi-hat nốt thứ tám:

for (var bar = 0; bar < 2; bar++) {
  var time = startTime + bar * 8 * eighthNoteTime;

  // Play the hi-hat every eighth note.
  for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
  }

Mã này sẽ hoạt động rất hiệu quả. Tuy nhiên, nếu bạn muốn thay đổi nhịp ở giữa hai thanh đó hoặc dừng phát trước khi hai thanh này hoạt động thì bạn không gặp may. (Tôi thấy các nhà phát triển làm những việc như chèn nút tăng âm giữa AudioBufferSourceNodes và AudioBufferSourceNodes được lên lịch trước và đầu ra, chỉ để họ có thể tắt âm thanh của chính mình!)

Tóm lại, vì bạn cần sự linh hoạt để thay đổi nhịp độ hoặc các tham số như tần suất hoặc mức tăng (hoặc dừng hoàn toàn việc lên lịch), nên bạn sẽ không muốn đẩy quá nhiều sự kiện âm thanh vào hàng đợi, hoặc nói chính xác hơn là bạn không muốn chuyển sang quá nhiều thời gian vì bạn có thể muốn thay đổi toàn bộ việc lập lịch biểu.

Thời đại tệ nhất – Đồng hồ JavaScript

Chúng ta cũng có đồng hồ JavaScript rất được ưa chuộng và phù hợp, được biểu thị bằng Date.now() và setTiming(). Ưu điểm của đồng hồ JavaScript là có một vài phương thức call-me-back- Sau cực kỳ hữu ích, cho phép hệ thống gọi lại mã của chúng ta vào các thời điểm cụ thể.

Nhược điểm của đồng hồ JavaScript là không chính xác lắm. Đối với điều kiện bắt đầu, Date.now() sẽ trả về một giá trị tính bằng mili giây – số nguyên mili giây – vì vậy, độ chính xác tốt nhất mà bạn có thể mong đợi là một phần nghìn giây. Điều này không hẳn là xấu trong một số bối cảnh âm nhạc – nếu ghi chú của bạn bắt đầu sớm hoặc muộn một mili giây, bạn thậm chí có thể không nhận thấy – nhưng ngay cả ở tốc độ phần cứng âm thanh tương đối thấp 44,1kHz, nó quá chậm khoảng 44,1 lần để sử dụng làm đồng hồ lên lịch âm thanh. Hãy nhớ rằng việc thả bất kỳ mẫu nào cũng có thể gây ra sự cố âm thanh. Vì vậy, nếu chúng ta xâu chuỗi các mẫu với nhau, chúng ta có thể cần chúng phải theo tuần tự chính xác.

Thông số kỹ thuật về Thời gian độ phân giải cao sắp ra mắt thực sự mang lại cho chúng ta thời gian hiện tại chính xác hơn nhiều thông qua window.performance.now( Thông số kỹ thuật này thậm chí còn được triển khai (mặc dù có tiền tố) trong nhiều trình duyệt hiện tại. Điều đó có thể có ích trong một số trường hợp, mặc dù điều đó không thực sự liên quan đến phần xấu nhất trong API thời gian JavaScript.

Điều tồi tệ nhất trong API thời gian JavaScript là mặc dù độ chính xác mili giây của Date.now() có vẻ quá kém, nhưng lệnh gọi lại thực tế của các sự kiện bộ tính giờ trong JavaScript (thông qua window.setthời gian() hoặc window.setInterval) có thể dễ dàng bị lệch hàng chục mili giây trở lên theo bố cục, hiển thị, thu gom rác, XMLHTTPRequest và các lệnh gọi lại khác – tóm lại là do bất kỳ điều gì xảy ra trên luồng thực thi chính. Bạn có nhớ cách tôi đề cập đến "sự kiện âm thanh" mà chúng ta có thể lên lịch bằng cách sử dụng API Web âm thanh không? Chà, tất cả đều được xử lý trên một luồng riêng biệt – vì vậy ngay cả khi luồng chính tạm thời bị trì hoãn khi thực hiện một bố cục phức tạp hoặc một tác vụ dài khác, âm thanh vẫn sẽ diễn ra đúng thời điểm được yêu cầu – thực tế, ngay cả khi bạn bị dừng ở một điểm ngắt trong trình gỡ lỗi, luồng âm thanh sẽ tiếp tục phát các sự kiện theo lịch biểu!

Sử dụng JavaScript setTiming() trong ứng dụng âm thanh

Vì luồng chính có thể dễ dàng bị trì hoãn trong nhiều mili giây cùng một lúc, bạn không nên sử dụng setthời gian chờ của JavaScript để bắt đầu trực tiếp phát các sự kiện âm thanh, vì tốt nhất là ghi chú của bạn sẽ kích hoạt trong vòng một mili giây hoặc lâu hơn khi chúng thực sự cần và tệ nhất là chúng sẽ bị trì hoãn lâu hơn nữa. Tệ hơn cả, đối với những chuỗi thể hiện có nhịp điệu, chúng sẽ không kích hoạt theo khoảng thời gian chính xác vì thời gian sẽ nhạy cảm với những sự kiện khác diễn ra trên luồng JavaScript chính.

Để chứng minh điều này, tôi đã viết một ứng dụng máy đếm nhịp "không tốt" mẫu – tức là một ứng dụng sử dụng trực tiếp setTiming để lên lịch ghi chú – và cũng tạo rất nhiều bố cục. Mở ứng dụng này, nhấp vào “phát” rồi nhanh chóng đổi kích thước cửa sổ trong khi cửa sổ đang phát; bạn sẽ nhận thấy thời gian bị dao động đáng kể (bạn có thể nghe thấy nhịp điệu không nhất quán). Bạn nói: "Nhưng mật khẩu này là đã có sẵn!" Tất nhiên là được, nhưng điều đó không có nghĩa là điều này cũng không xảy ra trong thế giới thực. Ngay cả giao diện người dùng tương đối tĩnh cũng sẽ gặp vấn đề về thời gian trong settime do khả năng chuyển tiếp (relayout) – ví dụ: tôi nhận thấy việc nhanh chóng đổi kích thước cửa sổ sẽ khiến thời gian trên WebkitSynth xuất sắc khác nhau đáng kể. Giờ thì hãy hình dung điều gì sẽ xảy ra khi bạn cố gắng cuộn mượt một bản nhạc đầy đủ cùng với âm thanh và bạn có thể dễ dàng hình dung được tác động của việc này đối với các ứng dụng nhạc phức tạp trong thế giới thực.

Một trong những câu hỏi thường gặp nhất mà tôi nhận được là "Tại sao tôi không thể nhận lệnh gọi lại từ sự kiện âm thanh?" Mặc dù có thể có cách sử dụng cho các loại lệnh gọi lại này, nhưng chúng sẽ không giải quyết được vấn đề cụ thể. Điều quan trọng là phải hiểu rằng các sự kiện đó sẽ được kích hoạt trong chuỗi JavaScript chính, vì vậy chúng sẽ bị trì hoãn tất cả các độ trễ tiềm năng như setTiming; tức là chúng có thể bị trì hoãn trong một số khoảng thời gian không xác định và thực tế là tính bằng mili giây trước khi chúng được xử lý.

Vậy chúng ta có thể làm gì? Cách tốt nhất để xử lý thời gian là thiết lập hoạt động cộng tác giữa các đồng hồ hẹn giờ JavaScript (setTiming(), setInterval() hoặc requestAnimationFrame() – đề cập thêm sau) và tính năng lên lịch cho phần cứng âm thanh.

Đạt được thời gian vững vàng bằng cách nhìn về phía trước

Hãy trở lại bản minh hoạ máy đếm nhịp đó. Thực tế, tôi đã viết đúng phiên bản đầu tiên của bản minh hoạ máy đếm nhịp đơn giản này để minh hoạ kỹ thuật lập lịch cộng tác này. (Mã này cũng có trên GitHub Bản minh hoạ này phát tiếng bíp (do Máy tạo dao động) tạo ra với độ chính xác cao ở mỗi nốt thứ 16, 8 hoặc 15, thay đổi cao độ tuỳ theo nhịp. Tính năng này cũng cho phép bạn thay đổi nhịp độ và khoảng thời gian nốt trong khi phát hoặc dừng phát bất cứ lúc nào. Đây là một tính năng chính của mọi trình tạo trình tự nhịp điệu ngoài đời thực. Bạn cũng có thể dễ dàng thêm mã để thay đổi âm thanh mà máy đánh nhịp này sử dụng một cách nhanh chóng.

Cách mà tính năng này quản lý để cho phép kiểm soát nhiệt độ trong khi vẫn duy trì thời gian ổn định là cộng tác: bộ tính giờ setTiming kích hoạt thỉnh thoảng và thiết lập lên lịch Âm thanh trên web trong tương lai cho từng ghi chú riêng lẻ. Về cơ bản, bộ tính giờ setTiming chỉ kiểm tra xem có ghi chú nào cần được lên lịch "sớm" dựa trên nhịp độ hiện tại hay không, sau đó lên lịch cho chúng như sau:

setTiming() và sự kiện âm thanh tương tác.
setTiming() và tương tác với sự kiện âm thanh.

Trong thực tế, các lệnh gọi setTiming() có thể bị trì hoãn, do đó thời gian của các lệnh gọi lập lịch có thể dao động (và lệch, tuỳ thuộc vào cách bạn sử dụng setTiming) theo thời gian – mặc dù các sự kiện trong ví dụ này kích hoạt cách nhau khoảng 50 mili giây, nhưng thường sẽ cao hơn một chút (và đôi khi hơn rất nhiều). Tuy nhiên, trong mỗi cuộc gọi, chúng tôi lên lịch cho các sự kiện Âm thanh trên web không chỉ cho bất kỳ ghi chú nào cần được phát ngay bây giờ (ví dụ: ghi chú đầu tiên), mà còn cho mọi ghi chú cần được phát trong khoảng thời gian từ bây giờ đến khoảng thời gian tiếp theo.

Trên thực tế, chúng ta không muốn chỉ xem trước chính xác khoảng thời gian giữa các lệnh gọi setTiming() - chúng ta cũng cần có một số chồng chéo về lịch biểu giữa lệnh gọi bộ tính giờ này và lệnh gọi tiếp theo, để đáp ứng hành vi của luồng chính trong trường hợp xấu nhất – đó là trường hợp xấu nhất của việc thu gom rác, bố cục, kết xuất hoặc mã khác xảy ra trên luồng chính làm chậm lệnh gọi bộ tính giờ tiếp theo. Chúng tôi cũng cần tính đến thời gian lên lịch chặn âm thanh – tức là lượng âm thanh mà hệ điều hành giữ trong vùng đệm xử lý – thay đổi giữa các hệ điều hành và phần cứng, từ các chữ số thấp một mili giây đến khoảng 50 mili giây. Mỗi lệnh gọi setTiming() được trình bày ở trên có một khoảng thời gian màu xanh dương cho thấy toàn bộ khoảng thời gian mà nó sẽ cố gắng lên lịch sự kiện; ví dụ: sự kiện âm thanh trên web thứ tư được lên lịch trong sơ đồ ở trên có thể đã bị phát "trễ" nếu chúng ta chờ để phát sự kiện đó cho đến khi lệnh gọi setThời gian tiếp theo xảy ra, nếu lệnh gọi setTiming đó chỉ vài mili giây sau đó. Trong thực tế, sự dao động trong những thời điểm này thậm chí còn nghiêm trọng hơn và sự chồng chéo này càng quan trọng hơn khi ứng dụng của bạn trở nên phức tạp hơn.

Độ trễ tổng thể có thể ảnh hưởng đến mức độ chặt chẽ việc kiểm soát nhịp độ (và các biện pháp kiểm soát khác theo thời gian thực). Khoảng thời gian giữa các cuộc gọi lên lịch là sự đánh đổi giữa độ trễ tối thiểu và tần suất mã của bạn tác động đến bộ xử lý. Mức độ mong đợi trùng lặp với thời gian bắt đầu của khoảng thời gian tiếp theo sẽ quyết định khả năng phục hồi của ứng dụng trên các máy khác nhau và khi ứng dụng trở nên phức tạp hơn (và việc thu thập bố cục và rác có thể mất nhiều thời gian hơn). Nói chung, để thích ứng với các máy và hệ điều hành chậm hơn, tốt nhất bạn nên có cái nhìn tổng thể rộng và khoảng thời gian ngắn hợp lý. Bạn có thể điều chỉnh để có các khoảng thời gian trùng lặp ngắn hơn và khoảng thời gian dài hơn để xử lý ít lệnh gọi lại hơn, nhưng vào một thời điểm nào đó, bạn có thể bắt đầu nghe thấy độ trễ lớn khiến nhịp độ thay đổi, v.v. không có tác dụng ngay lập tức; ngược lại, nếu bạn giảm bớt tính năng xem trước quá nhiều, bạn có thể bắt đầu nghe thấy một chút dao động (vì cuộc gọi lên lịch có thể phải "tạo ra" các sự kiện đã xảy ra trong quá khứ).

Sơ đồ thời gian sau đây cho thấy mã minh hoạ máy đếm nhịp thực sự hoạt động như thế nào: mã này có khoảng thời gian setthời gian chờ là 25 mili giây, nhưng có thời gian chồng chéo linh hoạt hơn nhiều: mỗi lệnh gọi sẽ được lên lịch trong 100 mili giây tiếp theo. Nhược điểm của cái nhìn dài này là sự thay đổi nhịp độ, v.v., sẽ mất một phần mười giây để có hiệu lực; tuy nhiên, chúng ta có khả năng chống chọi cao hơn nhiều với sự gián đoạn:

Lên lịch bị trùng lặp trong thời gian dài.
lên lịch bị trùng lặp dài

Trên thực tế, trong ví dụ này, chúng ta có thể biết được rằng chúng ta đã bị gián đoạn setTiming ở giữa – chúng ta đáng lẽ phải có một lệnh gọi lại setout ở khoảng 270 mili giây, nhưng vì một lý do nào đó mà nó bị trì hoãn cho đến khoảng 320 mili giây – 50 mili giây so với lẽ ra phải có! Tuy nhiên, độ trễ lớn tính ra trước giúp thời gian diễn ra suôn sẻ và chúng tôi không bỏ lỡ nhịp nào, ngay cả khi chúng tôi đã tăng nhịp độ ngay trước đó để chơi các nốt thứ 16 ở tốc độ 240 nhịp/phút (thậm chí vượt ra ngoài cả nhịp trống cứng và nhịp độ bass!)

Cũng có khả năng mỗi lệnh gọi trình lập lịch biểu sẽ lên lịch nhiều ghi chú. Hãy xem điều gì sẽ xảy ra nếu chúng ta sử dụng khoảng thời gian lập lịch dài hơn (250 mili giây, cách nhau 200 mili giây) và nhịp độ tăng ở giữa:

setTIME() với khoảng thời gian dài và khoảng thời gian dài.
setTIME() với khoảng thời gian dài và khoảng thời gian dài

Trường hợp này chứng minh rằng mỗi lệnh gọi setTiming() có thể kết thúc việc lên lịch nhiều sự kiện âm thanh. Trên thực tế, máy đếm nhịp này là một ứng dụng phát từng nốt đơn giản, nhưng bạn có thể dễ dàng xem cách phương pháp này hoạt động đối với máy trống (nơi thường có nhiều ghi chú đồng thời) hoặc một bộ sắp xếp (thường có các khoảng thời gian không đều đặn giữa các ghi chú).

Trong thực tế, bạn sẽ muốn điều chỉnh khoảng thời gian lên lịch của mình và dự tính để xem nó bị ảnh hưởng như thế nào bởi bố cục, việc thu gom rác và các hoạt động khác đang diễn ra trong luồng thực thi JavaScript chính, đồng thời điều chỉnh mức độ kiểm soát chi tiết đối với nhịp độ, v.v. Nếu có một bố cục rất phức tạp xảy ra thường xuyên, chẳng hạn, bạn có thể sẽ muốn làm cho giao diện lớn hơn. Điểm chính là chúng ta muốn thời gian “lên lịch trước” mà chúng ta đang thực hiện đủ lớn để tránh mọi sự chậm trễ, nhưng không quá lớn đến mức có thể tạo ra độ trễ đáng chú ý khi điều chỉnh việc kiểm soát tốc độ. Ngay cả trường hợp trên cũng có độ chồng chéo rất nhỏ, vì vậy, nó sẽ không đủ khả năng phục hồi trên máy chậm có ứng dụng web phức tạp. Một vị trí tốt để bắt đầu có thể là thời gian "xem xét" trong 100 mili giây, với các khoảng thời gian được đặt là 25 mili giây. Điều này có thể vẫn gặp sự cố trong các ứng dụng phức tạp trên những máy có độ trễ lớn của hệ thống âm thanh, trong trường hợp này, bạn nên tăng thời gian kiểm soát; hoặc, nếu bạn cần kiểm soát chặt chẽ hơn do mất đi một số khả năng phục hồi, hãy sử dụng bản xem trước ngắn hơn.

Mã cốt lõi của quá trình lên lịch nằm trong hàm scheduler() –

while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
  scheduleNote( current16thNote, nextNoteTime );
  nextNote();
}

Hàm này chỉ lấy thời gian phần cứng âm thanh hiện tại và so sánh với thời gian của ghi chú tiếp theo trong trình tự - hầu hết thời gian* trong trường hợp chính xác này thì hàm này sẽ không làm gì cả (vì không có "ghi chú" của máy đánh nhịp đang chờ để lên lịch, nhưng khi thành công, hàm sẽ lên lịch ghi chú đó bằng cách sử dụng API Web Audio và chuyển sang ghi chú tiếp theo.

Hàm ScheduleNote() trên thực tế chịu trách nhiệm lên lịch phát “ghi chú” tiếp theo của Âm thanh web. Trong trường hợp này, tôi đã sử dụng bộ tạo dao động để tạo ra tiếng bíp ở các tần số khác nhau; bạn có thể dễ dàng tạo các nút AudioBufferSource và đặt bộ đệm của chúng thành âm thanh trống hoặc bất kỳ âm thanh nào khác mà bạn muốn.

currentNoteStartTime = time;

// create an oscillator
var osc = audioContext.createOscillator();
osc.connect( audioContext.destination );

if (! (beatNumber % 16) )         // beat 0 == low pitch
  osc.frequency.value = 220.0;
else if (beatNumber % 4)          // quarter notes = medium pitch
  osc.frequency.value = 440.0;
else                              // other 16th notes = high pitch
  osc.frequency.value = 880.0;
osc.start( time );
osc.stop( time + noteLength );

Sau khi những bộ tạo dao động đó được lên lịch và kết nối, mã này có thể hoàn toàn quên chúng; chúng sẽ bắt đầu rồi dừng lại, sau đó được thu gom rác tự động.

Phương thức nextNote() chịu trách nhiệm chuyển đến ghi chú thứ 16 tiếp theo – tức là đặt các biến nextNoteTime và current16thNote thành ghi chú tiếp theo:

function nextNote() {
  // Advance current note and time by a 16th note...
  var secondsPerBeat = 60.0 / tempo;    // picks up the CURRENT tempo value!
  nextNoteTime += 0.25 * secondsPerBeat;    // Add 1/4 of quarter-note beat length to time

  current16thNote++;    // Advance the beat number, wrap to zero
  if (current16thNote == 16) {
    current16thNote = 0;
  }
}

Điều này khá đơn giản. Mặc dù điều quan trọng cần hiểu là trong ví dụ về lập lịch này, tôi không theo dõi "thời gian trình tự" - tức là thời gian kể từ khi bắt đầu bắt đầu máy đếm nhịp. Tất cả những gì chúng ta cần làm là nhớ thời điểm chơi nốt cuối cùng và xác định thời điểm lên lịch phát nốt tiếp theo. Bằng cách đó, chúng ta có thể thay đổi nhịp điệu (hoặc ngừng chơi) rất dễ dàng.

Kỹ thuật lên lịch này được một số ứng dụng âm thanh khác trên web sử dụng – ví dụ: Web Audio Drum Machine, trò chơi Acid Defender rất thú vị và các ví dụ âm thanh chi tiết hơn như bản minh hoạ Hiệu ứng hạt.

Vẫn còn một hệ thống thời gian khác

Giờ đây, như bất kỳ nhạc sĩ giỏi nào cũng đều biết, điều mà mọi ứng dụng âm thanh đều cần là nhiều cowbell hơn - nhiều hơn nữa. Cần lưu ý rằng cách đúng để hiển thị hình ảnh là sử dụng hệ thống thời gian thứ ba!

Vì sao chúng ta cần một hệ thống thời gian khác? Tính năng này được đồng bộ hoá với màn hình hiển thị trực quan – tức là tốc độ làm mới đồ hoạ – qua requestAnimationFrame API. Đối với việc vẽ hộp chính xác trong ví dụ về máy đếm nhịp, điều này có vẻ không quá khó khăn, nhưng khi đồ hoạ của bạn ngày càng phức tạp hơn, điều quan trọng hơn là phải sử dụng requestAnimationFrame() để đồng bộ hoá với tốc độ làm mới hình ảnh. Việc sử dụng requestAnimationFrame() ngay từ đầu cũng rất dễ sử dụng!

Chúng ta đã theo dõi nhịp âm thanh trong hàng đợi trong trình lập lịch biểu:

notesInQueue.push( { note: beatNumber, time: time } );

Sự tương tác với thời gian hiện tại của máy đếm nhịp có thể được tìm thấy trong phương thức draw(), phương thức này được gọi (sử dụng requestAnimationFrame) bất cứ khi nào hệ thống đồ hoạ sẵn sàng cập nhật:

var currentTime = audioContext.currentTime;

while (notesInQueue.length && notesInQueue[0].time < currentTime) {
  currentNote = notesInQueue[0].note;
  notesInQueue.splice(0,1);   // remove note from queue
}

Một lần nữa, bạn sẽ thấy rằng chúng ta đang kiểm tra đồng hồ của hệ thống âm thanh - bởi vì đó thực sự là đồng hồ mà chúng ta muốn đồng bộ hoá, vì nó sẽ thực sự phát các ghi chú - để xem liệu chúng ta có nên vẽ một hộp mới hay không. Trên thực tế, chúng ta hoàn toàn không sử dụng dấu thời gian requestAnimationFrame vì chúng ta đang sử dụng đồng hồ hệ thống âm thanh để xác định thời điểm của chúng ta.

Tất nhiên, tôi hoàn toàn có thể bỏ qua bằng cách sử dụng lệnh gọi lại setTiming() và đặt trình lập lịch biểu ghi chú của mình vào lệnh gọi lại requestAnimationFrame - sau đó chúng ta sẽ quay lại sử dụng hai đồng hồ hẹn giờ một lần nữa. Bạn cũng có thể làm vậy, nhưng điều quan trọng cần hiểu là requestAnimationFrame chỉ là một phương thức thay thế cho set ít thời gian chờ() trong trường hợp này; bạn vẫn muốn lập lịch biểu chính xác của thời gian Âm thanh trên web cho các ghi chú thực tế.

Kết luận

Tôi hy vọng hướng dẫn này đã giúp ích cho bạn trong việc giải thích về đồng hồ, bộ tính giờ và cách tạo thời gian phù hợp cho các ứng dụng âm thanh trên web. Các kỹ thuật tương tự này có thể dễ dàng được ngoại suy để xây dựng máy nghe nhạc theo trình tự, máy đánh trống, v.v. Tạm biệt và hẹn gặp lại!