Lên lịch phát âm thanh trên web một cách chính xác
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à nhạc tuyệt vời bằng nền tảng web là quản lý thời gian. Không phải là "thời gian viết mã" mà là thời gian đồng hồ – một trong những chủ đề ít được hiểu rõ nhất về Web Audio là cách làm việc đúng cách với đồng hồ âm thanh. Đối tượng AudioContext của Web Audio có 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 trên web – không chỉ viết trình tự và bộ tổng hợp, mà còn sử dụng bất kỳ nhịp điệu nào của sự kiện âm thanh như máy trống, trò chơi và cá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 của các sự kiện âm thanh; không chỉ bắt đầu và dừng âm thanh, mà còn lên lịch thay đổi âm thanh (chẳng hạn như thay đổi tần số hoặc âm lượng). Đôi khi chúng ta cầ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ần Phát triển âm thanh trong trò chơi bằng API âm thanh trên web). Tuy nhiên, 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 ghi chú bằng cách sử dụng tham số thời gian của phương thức ghi chú và ghi chú âm thanh trên web (nay được đổi tên thành bắt đầu và dừng) trong phần Bắt đầu sử dụng Âm thanh trên web và trong cả bài viết Phát triển âm thanh trong 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ề những tình huống phức tạp hơn, chẳng hạn như chơi các giai điệu hoặc chuỗi nhạc dài. Để tìm hiểu sâu về vấn đề này, trước tiên chúng ta cần biết một chút về đồng hồ.
Thời khắc đẹp nhất – Đồ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 số giây dấu phẩy động kể từ khi AudioContext được tạo. Điều này cho phép đồng hồ này (sau đây gọi là "đồng hồ âm thanh") có độ chính xác rất cao; đồng hồ này được thiết kế để có thể chỉ định căn chỉnh ở cấp độ mẫu âm thanh riêng lẻ, ngay cả khi có tốc độ lấy mẫu cao. Vì có khoảng 15 chữ số thập phân chính xác trong một "double", nên ngay cả khi đồng hồ âm thanh đã chạy trong nhiều ngày, đồng hồ này 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 được dùng để lên lịch các thông số và sự kiện âm thanh trong toàn bộ API Web Audio – tất nhiên là cho start() và stop(), nhưng cũng cho các 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 có thể chỉ cần thiết lập mọi thứ trong Web Audio dưới dạng thời gian bắt đầu/dừng. Tuy nhiên, trong thực tế, cách này sẽ gặp vấn đề.
Ví dụ: hãy xem đoạn mã đã rút gọn này từ Phần giới thiệu về âm thanh web, đoạn mã này thiết lập hai thanh của mẫu hình mũ cao cấp 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 muốn thay đổi tốc độ giữa hai thanh đó – hoặc dừng phát trước khi hai thanh đó kết thúc – thì bạn sẽ không thể làm được. (Tôi đã thấy các nhà phát triển thực hiện những việc như chèn một nút tăng giữa AudioBufferSourceNodes được lên lịch trước và đầu ra, chỉ để họ có thể tắt tiếng âm thanh của riêng mình!)
Tóm lại, vì bạn cần linh hoạt thay đổi tốc độ hoặc các thông số như tần số hoặc độ lợi (hoặc ngừng lên lịch hoàn toàn), nên bạn không nên đẩy quá nhiều sự kiện âm thanh vào hàng đợi – hoặc chính xác hơn là bạn không nên xem trước quá xa, vì bạn có thể muốn thay đổi hoàn toàn lịch biểu đó.
The Worst of Times – Đồng hồ JavaScript
Chúng ta cũng có đồng hồ JavaScript được yêu thích và bị chỉ trích nhiều, được biểu thị bằng Date.now() và setTimeout(). Mặt tốt của đồng hồ JavaScript là nó có một số phương thức call-me-back-later window.setTimeout() và window.setInterval() rất hữu ích, cho phép hệ thống gọi lại mã của chúng ta vào những thời điểm cụ thể.
Mặt kém của đồng hồ JavaScript là không chính xác lắm. Đối với người mới bắt đầu, Date.now() trả về một giá trị tính bằng mili giây – một số nguyên tính bằng mili giây – vì vậy, độ chính xác tốt nhất mà bạn có thể hy vọng là một phần nghìn giây. Điều này không quá tệ trong một số ngữ cảnh âm nhạc – nếu nốt nhạc của bạn bắt đầu sớm hoặc muộn một mili giây, bạn có thể không nhận thấy điều đó – nhưng ngay cả ở tốc độ phần cứng âm thanh tương đối thấp là 44,1 kHz, tốc độ này vẫn chậm hơn khoảng 44,1 lần để dùng làm đồng hồ lên lịch âm thanh. Hãy nhớ rằng việc bỏ qua bất kỳ mẫu nào cũng có thể gây ra sự cố âm thanh. Vì vậy, nếu nối các mẫu với nhau, chúng ta có thể cần các mẫu đó phải theo trình tự chính xác.
Thông số kỹ thuật Thời gian độ phân giải cao sắp tới thực sự cung cấp cho chúng tôi thời gian hiện tại chính xác hơn nhiều thông qua window.performance.now(); 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ể giúp ích trong một số trường hợp, mặc dù cách này không thực sự liên quan đến phần kém nhất của API thời gian JavaScript.
Phần tệ nhất của các API tính thời gian JavaScript là mặc dù độ chính xác theo mili giây của Date.now() không quá tệ, nhưng lệnh gọi lại thực tế của các sự kiện hẹn giờ trong JavaScript (thông qua window.setTimeout() hoặc window.setInterval) 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à XMLHTTPRequest cũng như các lệnh gọi lại khác – nói ngắn gọn là do bất kỳ số lượng sự kiện nào đang diễn ra trên luồng thực thi chính. Bạn còn nhớ tôi đã đề cập đến "sự kiện âm thanh" mà chúng ta có thể lên lịch bằng Web Audio API không? Tất cả những việ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ị đình trệ 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ẽ phát đúng thời điểm được yêu cầu – trên thực tế, ngay cả khi bạn dừng ở một điểm ngắt trong trình gỡ lỗi, luồng âm thanh vẫn sẽ tiếp tục phát các sự kiện đã lên lịch!
Sử dụng setTimeout() trong JavaScript trong Ứng dụng âm thanh
Vì luồng chính có thể dễ dàng bị đình trệ trong nhiều mili giây cùng một lúc, nên bạn không nên sử dụng setTimeout của JavaScript để trực tiếp bắt đầu phát các sự kiện âm thanh, vì tốt nhất là các nốt nhạc sẽ kích hoạt trong vòng một mili giây hoặc lâu hơn so với thời điểm thực sự cần thiết, và tệ nhất là chúng sẽ bị trì hoãn lâu hơn nữa. Tệ nhất là đối với những trình tự nhịp nhàng, chúng sẽ không kích hoạt theo các khoảng thời gian chính xác vì thời gian sẽ nhạy cảm với những việc khác đang diễn ra trên luồng JavaScript chính.
Để minh hoạ điều này, tôi đã viết một ứng dụng metronome "không tốt" mẫu – tức là một ứng dụng sử dụng trực tiếp setTimeout để lên lịch ghi chú – và cũng thực hiện nhiều bố cục. Mở ứng dụng này, nhấp vào "phát", sau đó nhanh chóng đổi kích thước cửa sổ trong khi phát; bạn sẽ nhận thấy thời gian bị giật đáng kể (bạn có thể nghe thấy nhịp điệu không nhất quán). Bạn có thể nói rằng "Nhưng điều này là giả tạo!" Tất nhiên, nhưng điều đó không có nghĩa là điều này 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 setTimeout do các lượt chuyển tiếp – ví dụ: tôi nhận thấy việc đổi kích thước cửa sổ nhanh chóng sẽ khiến thời gian trên WebkitSynth vốn rất tốt bị giật đáng kể. Bây giờ, hãy hình dung điều gì sẽ xảy ra khi bạn muốn thực hiện một cách mượt mà toàn bộ bản nhạc phối với âm thanh. Bạn có thể dễ dàng hình dung việc này sẽ ảnh hưởng như thế nào đến 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 nghe thấy là "Tại sao tôi không nhận được lệnh gọi lại từ các sự kiện âm thanh?" Mặc dù có thể sử dụng 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ể hiện tại. Điều quan trọng là bạn phải hiểu rằng các sự kiện đó sẽ được kích hoạt trong luồng JavaScript chính, vì vậy, chúng sẽ chịu mọi độ trễ tiềm ẩn giống như setTimeout; tức là chúng có thể bị trễ một số mili giây không xác định và thay đổi so với thời điểm chính xác được lên lịch trước khi thực sự đượ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 chế độ cộng tác giữa bộ hẹn giờ JavaScript (setTimeout(), setInterval() hoặc requestAnimationFrame() – sẽ nói thêm về điều này sau) và lịch biểu phần cứng âm thanh.
Xem trước để có được thời gian chính xác
Hãy quay lại ví dụ về máy đếm nhịp đó – thực tế, tôi đã viết đúng phiên bản đầu tiên của ví dụ về máy đếm nhịp đơn giản này để minh hoạ kỹ thuật lập lịch biểu cộng tác này. (Mã này cũng có trên GitHub) Bản minh hoạ này phát âm thanh bíp (do một Biến động tạo ra) với độ chính xác cao trên mỗi nốt thứ 16, thứ 8 hoặc thứ 4, thay đổi độ cao tuỳ thuộc vào nhịp. Ứng dụng này cũng cho phép bạn thay đổi tốc độ và khoảng thời gian của nốt nhạc 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ự nhịp điệu thực tế. Bạn cũng có thể dễ dàng thêm mã để thay đổi âm thanh mà máy đếm nhịp này sử dụng ngay lập tức.
Cách thức để cho phép kiểm soát tạm thời trong khi vẫn duy trì thời gian chính xác là một hoạt động cộng tác: bộ hẹn giờ setTimeout kích hoạt một lần mỗi khi cần và thiết lập lịch biểu Web Audio trong tương lai cho từng nốt nhạc. Về cơ bản, bộ hẹn giờ setTimeout chỉ kiểm tra xem có cần lên lịch bất kỳ nốt nhạc nào "sắp tới" dựa trên tốc độ hiện tại hay không, sau đó lên lịch cho các nốt nhạc đó, như sau:
Trong thực tế, các lệnh gọi set khoát() có thể bị trì hoãn, vì vậy thời gian của các lệnh gọi lên lịch có thể dao động (và sai lệch, tuỳ thuộc vào cách bạn sử dụng set đồ ) 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, chúng thường sẽ dài hơn một chút (và đôi khi nhiều hơn nhiều). Tuy nhiên, trong mỗi lệnh gọi, chúng ta không chỉ lên lịch sự kiện Âm thanh trên web cho mọi nốt cần phát ngay (ví dụ: nốt đầu tiên), mà còn cho mọi nốt cần phát trong khoảng thời gian từ hiện tại đến khoảng thời gian tiếp theo.
Trên thực tế, chúng ta không chỉ muốn xem trước chính xác khoảng thời gian giữa các lệnh gọi setTimeout() – chúng ta cũng cần một số lịch biểu trùng lặp giữa lệnh gọi bộ hẹn giờ này và lệnh gọi tiếp theo, để phù hợp với hành vi của luồng chính trong trường hợp xấu nhất – tức 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ộ hẹn 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ữ lại trong bộ đệm xử lý – có thể thay đổi theo hệ điều hành và phần cứng, từ các chữ số đơn lẻ thấp (mili giây) đến khoảng 50 mili giây. Mỗi lệnh gọi setHết hạn() hiển thị ở trên có một khoảng thời gian màu xanh lam cho biết toàn bộ khoảng thời gian mà lệnh gọi này sẽ cố gắng lên lịch sự kiện; ví dụ: sự kiện âm thanh web thứ tư được lên lịch trong sơ đồ ở trên có thể đã được phát “trễ” nếu chúng ta đã đợi phát nó cho đến khi lệnh gọi setHết hạn tiếp theo xảy ra, nếu lệnh gọi set Vậy chỉ vài mili giây sau đó. Trong thực tế, độ trễ trong những khoảng thời gian này có thể còn nghiêm trọng hơn thế nữa. Việc chồng chéo này càng trở nên 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ể ảnh hưởng đến mức độ chặt chẽ của tính năng kiểm soát nhịp độ (và các chế độ kiểm soát theo thời gian thực khác). Khoảng thời gian giữa các lần lên lịch gọi 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 độ trùng lặp của tính năng xem trước với thời gian bắt đầu của khoảng thời gian tiếp theo sẽ xác định khả năng phục hồi của ứng dụng trên nhiều máy và khi ứng dụng trở nên phức tạp hơn (và bố cục cũng như việc thu gom rác có thể mất nhiều thời gian hơn). Nhìn chung, để thích ứng với các máy và hệ điều hành chậm hơn, tốt nhất là bạn nên có khoảng thời gian xem trước tổng thể lớn và khoảng thời gian ngắn hợp lý. Bạn có thể điều chỉnh để có các khoảng chồng chéo 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 tại một thời điểm nào đó, bạn có thể bắt đầu nghe thấy độ trễ lớn gây ra các thay đổi về tốc độ, v.v. không có hiệu lực ngay lập tức; ngược lại, nếu giảm quá nhiều độ trễ trước, bạn có thể bắt đầu nghe thấy một số hiện tượng giật (vì lệnh gọi lên lịch có thể phải "bù" các sự kiện lẽ ra đã xảy ra trong quá khứ).
Sơ đồ thời gian sau đây cho biết mã minh hoạ máy đếm nhịp thực sự làm gì: mã này có khoảng thời gian setTimeout là 25 mili giây, nhưng có độ trễ chồng chéo linh hoạt hơn nhiều: mỗi lệnh gọi sẽ lên lịch cho 100 mili giây tiếp theo. Nhược điểm của việc xem trước lâu này là các thay đổi về tốc độ, v.v. sẽ mất 1/10 giây để có hiệu lực; tuy nhiên, chúng ta có khả năng chống chịu tốt hơn nhiều đối với các sự cố gián đoạn:
Trên thực tế, bạn có thể thấy trong ví dụ này, chúng ta đã bị gián đoạn setTimeout ở giữa – chúng ta lẽ ra phải có lệnh gọi lại setTimeout vào khoảng 270 mili giây, nhưng lệnh gọi lại này bị trì hoãn vì một số lý do cho đến khoảng 320 mili giây – muộn hơn 50 mili giây so với dự kiến! Tuy nhiên, độ trễ xem trước lớn vẫn đảm bảo thời gian diễn ra mà không gặp vấn đề gì và chúng tôi không bỏ lỡ nhịp nào, mặc dù chúng tôi đã tăng tốc độ ngay trước đó để phát nốt 16 tại 240 nhịp/phút (thậm chí còn vượt quá tốc độ trống và bass mạnh mẽ!)
Cũng có thể mỗi lệnh gọi trình lập lịch biểu sẽ lên lịch nhiều nốt nhạc – hãy xem điều gì sẽ xảy ra nếu chúng ta sử dụng khoảng thời gian lên lịch dài hơn (250 mili giây trước, cách nhau 200 mili giây) và tăng tốc độ ở giữa:
Trường hợp này minh hoạ rằng mỗi lệnh gọi setTimeout() có thể kết thúc bằng 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 đơn giản, mỗi lần chỉ phát một nốt, nhưng bạn có thể dễ dàng thấy cách phương pháp này hoạt động đối với máy trống (thường có nhiều nốt cùng lúc) hoặc trình tự (thường có khoảng thời gian không đều giữa các nốt).
Trong thực tế, bạn nên điều chỉnh khoảng thời gian lên lịch và thời gian xem trước để xem khoảng thời gian này 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, cũng như để điều chỉnh mức độ chi tiết của chế độ kiểm soát tốc độ, v.v. Ví dụ: nếu có bố cục rất phức tạp xảy ra thường xuyên, bạn có thể muốn tăng thời gian xem trước. Điểm chính là chúng tôi muốn thời lượng “lên lịch trước” sẽ đủ lớn để tránh mọi sự chậm trễ, nhưng không lớn đến mức gây ra độ trễ đáng kể khi tinh chỉnh điều khiển nhịp độ. Ngay cả trường hợp trên cũng có mức độ trùng lặp rất nhỏ, vì vậy, nó sẽ không linh hoạt lắm trên một máy tính chạy chậm với ứng dụng web phức tạp. Bạn nên bắt đầu với khoảng thời gian "xem trước" là 100 mili giây, với khoảng thời gian đặt là 25 mili giây. Điều này có thể vẫn gặp vấn đề trong các ứng dụng phức tạp trên các máy có nhiều độ trễ hệ thống âm thanh. Trong trường hợp đó, bạn nên tăng thời gian xem xét; hoặc nếu bạn cần kiểm soát chặt chẽ hơn và mất đi khả năng phục hồi, hãy sử dụng một dự án ngắn hơn.
Mã cốt lõi của quy trình lập lịch biểu nằm trong hàm scheduler() –
while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
scheduleNote( current16thNote, nextNoteTime );
nextNote();
}
Hàm này chỉ nhận thời gian phần cứng âm thanh hiện tại và so sánh với thời gian cho ghi chú tiếp theo trong chuỗi – hầu hết thời gian* trong trường hợp chính xác này thì việc này sẽ không có tác dụng gì (vì không có “ghi chú” của máy đếm nhịp đang chờ được lên lịch. Tuy nhiên, khi thành công, hàm sẽ lên lịch ghi chú đó bằng API Web âm thanh rồi chuyển sang ghi chú tiếp theo.
Hàm scheduleNote() chịu trách nhiệm thực sự lên lịch phát "note" (lưu ý) tiếp theo của Web Audio. Trong trường hợp này, tôi đã sử dụng các bộ dao động để tạo ra âm thanh bíp ở các tần số khác nhau; bạn cũng có thể dễ dàng tạo các nút AudioBufferSource và đặt vùng đệ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 các 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; các dao động đó sẽ bắt đầu, sau đó dừng, rồi tự động được thu gom rác.
Phương thức nextNote() chịu trách nhiệm tiế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;
}
}
Việc này khá đơn giản – mặc dù bạn cần hiểu rằng trong ví dụ về việc lập lịch biểu 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 chạy 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úng ta phát nốt cuối cùng và tìm ra lịch biểu phát nốt nhạc tiếp theo. Bằng cách đó, chúng ta có thể dễ dàng thay đổi tốc độ (hoặc dừng phát).
Một số ứng dụng âm thanh khác trên web cũng sử dụng kỹ thuật lập lịch biểu này, chẳng hạn như Web Audio Drum Machine (Máy trống âm thanh trên web), trò chơi Acid Defender rất thú vị và thậm chí là các ví dụ âm thanh chuyên sâu hơn như bản minh hoạ Hiệu ứng chi tiết.
Vẫn còn một hệ thống tính giờ khác
Bây giờ, như bất kỳ nhạc sĩ tài năng nào cũng biết, điều mà mọi ứng dụng âm thanh cần là có nhiều quả chuông hơn - nhiều hơn nữa. Xin lưu ý rằng cách chính xác để hiển thị hình ảnh là sử dụng hệ thống định thời THỨ BA!
Tại sao, tại sao, tại sao chúng ta cần một hệ thống tính giờ khác? Kết quả này được đồng bộ hoá với màn hình trực quan (tức là tốc độ làm mới đồ hoạ) thông qua requestAnimationFrame API. Đối với việc vẽ các hộp trong ví dụ về máy đếm nhịp của chúng ta, điều này có vẻ không thực sự là một vấn đề lớn, nhưng khi đồ hoạ của bạn ngày càng phức tạp hơn, việc sử dụng requestAnimationFrame() để đồng bộ hoá với tốc độ làm mới hình ảnh thực sự dễ dàng ngay từ đầu như sử dụng phương thức set
Chúng tôi theo dõi các nhịp trong hàng đợi trong trình lập lịch biểu:
notesInQueue.push( { note: beatNumber, time: time } );
Bạn có thể tìm thấy hoạt động tương tác với thời gian hiện tại của máy đếm nhịp trong phương thức draw(), đượ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
}
Xin nhắc lại, bạn sẽ thấy chúng ta đang kiểm tra đồng hồ của hệ thống âm thanh – vì đó thực sự là đồng hồ mà chúng ta muốn đồng bộ hoá, vì đồng hồ này sẽ thực sự phát các nốt nhạc – để xem chúng ta có nên vẽ một hộp mới hay không. Trên thực tế, chúng ta không thực sự 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 hiện tại.
Tất nhiên, tôi có thể đã bỏ qua hoàn toàn bằng cách sử dụng lệnh gọi lại set sát() và đặt trình lập lịch biểu ghi chú của tôi vào lệnh gọi lại requestAnimationFrame – sau đó chúng ta sẽ quay lại 2 đồng hồ hẹn giờ một lần nữa. Bạn cũng có thể làm như vậy, nhưng điều quan trọng là bạn phải hiểu rằng requestAnimationFrame chỉ là một phần thay thế cho setTimeout() trong trường hợp này; bạn vẫn muốn có độ chính xác về lịch biểu của thời gian Âm thanh trên web cho các nốt thực tế.
Kết luận
Tôi hy vọng hướng dẫn này đã giúp bạn hiểu rõ về đồng hồ, bộ hẹn giờ và cách tạo thời gian chính xác cho các ứng dụng âm thanh trên web. Bạn có thể dễ dàng ngoại suy những kỹ thuật này để tạo trình phát trình tự, máy trống và nhiều công cụ khác. Hẹn gặp lại!