(文章)

網頁步進編曲的時鐘設計:音訊時鐘、提前排程與畫面同步

Tone.Loop 單一時鐘排程全部音色,聲音用音訊時間,畫面用 Draw 延後更新,lookAhead 0.15 秒。

磨人開發團隊・2026-02-13

目錄

這套網頁取樣機的編曲引擎把時鐘放在音訊端,聲音依音訊時間排程,畫面另外跟上。以下依序說明資料模型、主時鐘、聲音與畫面的分工、lookAhead 的取捨、播放中換段與 groove 的時間換算。

輸出成 MP3 屬於同一系列的另一個主題,離線算繪的做法見瀏覽器離線算繪與 MP3 輸出。

JavaScript 計時器不直接觸發聲音

setTimeout、setInterval 與 requestAnimationFrame 都排在瀏覽器主執行緒上,頁面忙於繪製或計算時,回呼會晚到,節奏就跟著歪。Chris Wilson 在 A tale of two clocks 中建議,讓 JavaScript 計時器與音訊硬體排程合作:計時器定期醒來,把接下來要發的聲音提前排進音訊時鐘。

Web Audio 的排程時間都相對於 AudioContext 的 currentTime,`start(when)` 可以讓聲音在確切時間播放,見 W3C Web Audio API。Tone.js 的 Transport 在這個基礎上提供音樂時間,包含 BPM、小節與 tick,tick 的基準是每個 4 分音符 192 ticks(Tone.js Time)。

幾組齒輪相互咬合轉動的插圖

編曲資料模型

取樣機的一份卡帶,編曲部分由下列欄位組成。

  • 4 個序列(SEQ 1 至 4),每個序列各有 12 個音色格。
  • 每個音色格有 32 個布林值,每個值代表一步,亮起表示該步發聲。
  • 一步是一個 8 分音符,每小節 8 步,32 步等於 4 小節。
  • 每個序列的 loop 長度為 1 到 4 小節,存成 "N:0:0" 字串;引擎取小節數乘以 8 得到步數。
  • BPM 預設 120,可調範圍 30 到 300。

每個音色格另外帶音量、反轉、音高、濾波、swing 與 groove 參數,以及取樣的起訖秒數。這些參數在觸發時才讀取,不影響時鐘本身。

單一主時鐘帶動全部音色

主時鐘是一個 `Tone.Loop`,間隔 12 ticks,由 `Tone.Transport` 驅動。PPQ 為 192,8 分音符是 4 分音符的一半,所以每 8 次回呼前進一個 8 分音符。120 BPM 時,一次回呼約 31.25 ms。

回呼裡維護一個 `currentStep`,每次加 1,對「序列長度乘以 8」取模,得到目前落在序列的第幾步。12 個音色格共用這一個計數,在回呼內逐格檢查該步是否亮起,亮起的就以回呼傳入的 `time` 觸發。

單一時鐘有三個好處。各音色格的相對位置只由步數決定,不會因為各自計時而漂移。改 BPM 只需要改 Transport 一處。長度不同的序列在同一個計數上各自取模,換段與 loop 長度的處理都能集中在回呼內。

聲音與畫面分開排程

Loop 回呼收到的 `time` 是音訊時間,位於聲音實際發出之前。聲音直接用這個 `time` 呼叫 `start`,由音訊端在確切時間發聲。

畫面不能照同一個時間點立刻改。回呼觸發時聲音還沒有出來,此刻改步進燈,燈會比聲音早亮。步進燈與進度條改用 `Tone.Draw.schedule(fn, time)`,延到該時間點附近、畫面準備重繪時才執行,React 狀態在那時才改。requestAnimationFrame 的回呼本來就在下一次重繪之前執行(MDN),畫面更新與重繪因此落在同一個節拍。

步進燈每次回呼都排一次,位置是 8 分音符步數對序列長度取模。進度條只在計數為偶數時更新,降低更新頻率。換段時的畫面狀態也走同一條 Draw 路徑。

圖 1聲音用音訊時間排程,畫面延到同一時間點再更新
提前排程
觸發
呈現
音訊時鐘
Loop 回呼
畫面
1
音訊時鐘Transport 依 BPM 推進 ticks每 12 ticks 觸發一次回呼
2
Loop 回呼回呼收到音訊時間 time時間點在聲音發出之前
3
Loop 回呼依步數觸發音色格start(time),由音訊端發聲
4
畫面Draw.schedule(fn, time)只登記,不立刻改畫面
5
畫面步進燈與進度條更新畫面重繪時才改 React 狀態

依引擎設計整理。時間點以 Loop 回呼傳入的 time 為準。

lookAhead 的取捨

Tone.js 的 lookAhead 決定排程比發聲提早多少。官方的 Performance 說明寫明,這讓排程器有時間在聲音被聽到之前完成排程,代價是延遲;要降低延遲,可以把 lookAhead 調小或設為 0。

編曲引擎在每次初始化卡帶時把 lookAhead 設為 0.15 秒,目的是避免音訊斷續。回呼因主執行緒忙碌而晚到時,仍有 0.15 秒的餘裕可以在聲音發出前完成排程。

按鍵試聽沒有排程的問題,在意的是按下去到出聲的延遲。每個音色格因此有兩組獨立的 `Tone.Player`:預覽組不經音高與濾波,起始時間取 `Tone.immediate() + lookAhead / 8`,約 18.75 ms,只留一點餘裕避免爆音;編曲組有音高與濾波,走排程。兩組可以同時發聲,也能只停其中一組。

圖 2按鍵試聽與編曲播放走兩條路徑
  1. 音色格要發聲每格有預覽與編曲兩組 Player
  • 使用者按下音色格
    預覽組約 18.75 ms 後發聲,不經音高與濾波
  • Loop 回呼排程
    編曲組使用回呼的 time,lookAhead 0.15 秒,經音高與濾波

數值為程式設定值,非實測。

輸送帶上的物件依序向前移動的插圖

播放中換段在 loop 起點生效

播放中按下別的序列,引擎只記下待切換的序列,同步更新畫面上的選取狀態,聲音不動。回呼走到 `currentStep` 為 0 且有待切換序列時,才切換並清除記錄。

這個 0 是目前序列的 loop 起點,不是每一小節。序列長 4 小節時,要等整段 4 小節播完才換。目前的樂句因此不會在中途被切斷,下一個序列從頭接上。停止狀態下按序列則直接切換。

groove 換算成時間偏移

每個音色格可以選一組 groove。groove 資料對每一步各有一個 offset,單位是 8 分音符的比例,負值提早、正值延後。每格另有 swing 設定。

觸發時間等於排程時間,加上「該步 offset 乘以該格的 groove 強度,再加 swing 乘以 0.5」,整個結果再乘以一個 8 分音符的秒長。8 分音符的秒長是 60 除以 BPM 再除以 2。偏移量依 BPM 換算成秒,所以速度改變時,groove 與 swing 的感覺跟著縮放。偏移加在傳給 Tone 的 `time` 上,仍在音訊時鐘內完成,不另起計時器。

不需要這套時鐘的情況

只播放單一音效或介面提示音,例如按鈕點擊,在操作當下直接發聲即可,不需要時鐘與提前排程。需要把麥克風輸入即時監聽並回放的場景,lookAhead 的延遲屬於額外負擔,要另外評估。

延伸閱讀與後續主題

官方教學影片可以對照本文的序列與換段:1-5 建立基礎循環段落示範基礎循環,2-3 段落設計示範多段落的安排。取樣機的整體做法見北流雲取樣機案例。

網頁音訊還包含取樣的載入與快取、每格音色的處理鏈與輸出檔案的離線算繪,輸出部分見瀏覽器離線算繪與 MP3 輸出。規劃網頁音訊、取樣機或編曲工具時,可參考網頁音訊與取樣機開發,也可以與開發團隊聯繫討論範圍。

(聯絡)

Start a project.

三個步驟,取得可判斷的做法與預算區間。

  1. 01預約第一場會議:選擇時段,不需要完整規格。
  2. 02線上諮詢 15 至 30 分鐘:取得做法方向、預算區間與時程草案。
  3. 03檢視素材後,約 3 至 5 個工作天提供報價單暨交付規格明細表。
信箱
hello@polish-design.com.tw
通訊地址
(104) 台北市中山區植福路191-1號3F

預約第一場會議

週一至週五 16:00–18:00線上 30 分鐘

先選時段,再用 Google 帳號或信箱確認,預約完成可加入行事曆。

選擇時段

或先送出資料

還沒想好時段,可以先留下專案內容,專人以信箱回覆。送出後也能接著預約。

補充資料(選填):服務項目、目前階段、現有連結、得知管道

資料僅用於本次專案評估。

ISO/IEC 27001:2022 證書

(Certificate)

ISO/IEC 27001:2022

驗證機構
ARES International Certification
驗證範圍
互動沉浸式 3D 系統之開發流程,及網路與基礎設施之維運與管理。

到驗證機構網站查證