Generated from Markdown source. Edit the source, not this HTML.
OPM.js 使用說明(繁體中文) #
GitHub Release v1.10(套件 1.10.0)加入可攜 Arrangement 專案、選用的 MIDI 檔案表情保留與損失摘要、依工作負載限定的容量候選,以及有界本機 MIDI 擷取/驗收活動準備。保留 canonical v6 音色、表情控制、32 聲部與 Worker 匯出。GitHub 發佈不表示 npm 已上架。Node.js 需 22+;README 是公開契約。
音色格式版本與套件版本獨立:已發佈的 v1.10 與歷史 v1.7 使用 canonical v6;目前 v1.11 checkout 使用 canonical v7(新增每個運算子的 waveform 與 noiseRate);不可變的歷史 v1.6 Release 使用 v5。明確指定的舊音色版本 1–6 仍保留各自原有輸入格式,並正規化為 v7。
本次 Release 範圍:可攜 Arrangement 專案、選用的 MIDI 檔案表情保留與損失摘要、實測容量候選及跨功能契約總覽包含於 v1.10。實體手機/MIDI 與人工聽感仍須真正的原始觀察才能完成驗收,不能用自動播放冒充通過。
v1.11 新功能(目前 checkout,尚未發佈) #
音色 v7 波形與雜訊。每個運算子可指定
waveform(sine、half、abs、quarter、alternating、camel、square、saw、noise)與noiseRate(20–20000 Hz)。既有正弦音色輸出不變;square、saw、noise 不保證無混疊。內建音色庫新增原創的noise-snare、noise-hihat、explosion。詳見表情音色參考。選用立體聲效果。
createStereoEffects/applyEffects(離線)與createEffects(專用 AudioWorklet insert)提供 chorus 與 reverb;不會自動接線。詳見音訊匯流排。MIDI 程式對照與 RPN。
programVoices/drumVoices將 Program Change 與 GM 鼓音符對應到具名 FM 音色(需主動啟用;gmProgramVoices/gmDrumVoices為起手對照表);保留模式的檔案匯入與即時轉接器也會解碼 RPN 0 的 pitch-bend 靈敏度。詳見 MIDI 檔案與 MIDI 演奏。.opm匯入。opm.js/voices/opm.js的importOPM/describeOPM近似轉換 VOPM/MXDRV 系列文字音色;並非暫存器層級模擬。
v1.10 新功能 #
HTTPS 線上範例需點擊播放按鈕才能啟動音訊。以下功能包含於 v1.10;歷史 Release 保持不變。GitHub Release 不表示 npm 上架或網站部署。使用端可直接安裝 Release tarball;原始碼貢獻者執行 npm ci、npm run build。
parseArrangementProject(source: string | object)/serializeArrangementProject(project)由 root/core 匯出,讀寫獨立的ArrangementProjectversion 1。嚴格 own-data 驗證回傳獨立且深度 frozen 的定義,儲存 voices/settings、tempo/meter、layers、sections 與必要的 initialSection。解析/載入不啟動音訊;上限 8 MiB、1–16 layers、32 sections、合計 65,536 events、128 voices/256 KiB 正規化音色 JSON。重播從 beat 0 開始,不儲存即時游標、待執行轉換、淡入淡出或 DSP 狀態。詳見可攜 Arrangement 專案。importMidiFile(Uint8Array, options?)回傳{ events, tempoMap, timeSignature, warnings, lossSummary };exportMidiFile(events, options?)回傳Uint8Array,由 root、core 與opm.js/midi-file匯出。僅支援 format 0/1 PPQN。預設controls: 'omit'維持 note-only 匯入,匯出仍拒絕 control events/非零 pan;選用controls: 'preserve'可依明確 pitchBendRange 與 channel ownership 政策保留 pitch、expression、pan、modulation。lossSummary區分遺漏、近似與已保留的來源控制訊息計數;sustain 仍是音符長度近似。匯出仍拒絕 ramp/glide、gain/音色控制、非整數音高、非零 priority、channel ownership 歧義與 linear tempo ramp。無 program-to-FM 轉換或 SysEx 傳送。詳見MIDI 檔案。實測容量候選僅適用於所測工作負載、瀏覽器與機器,不是所有裝置的通用容量保證。
有界本機 MIDI 擷取與實體裝置/聽感活動準備協助收集真正原始觀察,不代表已完成實體或人工驗收。
跨功能契約總覽整理重播、表情控制、渲染與 host 邊界。
既有 API 與範例(於 v1.9 引入) #
parseScoreProject(source: string | object)/serializeScoreProject(project)讀寫 canonical version 1 專案:拍點 events、正規化 tempoMap/timeSignature、具名完整 voices 與 synthesis settings。預設為 120 BPM、4/4、44100 Hz、standard、8 聲部、mixGain 1、A4 440 Hz、oldest;嚴格 own-data 驗證且回傳 frozen snapshot。上限 8 MiB、65,536 events、128 voices/256 KiB 音色 JSON。compileBeatSequence(events, { tempoMap?, bpm?, voices? })轉為驗證過的秒制SequenceEvent[],供離線/Worker 渲染;Transport 直接使用拍點 events,先將具名音色載入 OPM。詳見樂譜專案。TransportOptions.startupLead為 0–10 秒,預設min(0.05, horizon / 2):啟動/恢復/重建以未來時刻為原點,提前量期間維持音樂位置;0 可取消提前量。這修正歷史 beat-0 冷啟動缺陷,但不保證主執行緒停頓下的 deadline。Arrangement layer
gain為 0–1(預設 1),switchSection/setLayer可指定 fade 0–10 秒;setLayerGain(name, gain, { quantize?, fade? })回傳提交拍點。共用 layer 不重新起音,gain 獨立乘上 expression,涵蓋自行擁有的 release tails 與新音符;layer score 的 gain controls 保留給 Arrangement,音樂力度請用 expression。詳見自適應音樂。MidiAdapterOptions.controllerMap至多 128 項,指定 controller、field、明確 min/max/ramp、選用 reset;operator tuple field 另需零起算 operator。重複 CC/target 與 CC64/120/121/123 拒絕。Scalar field 包含獨立NoteControls.gain(0–1、預設 1);CC121 恢復建立 adapter 時擷取的有效預設或 explicit reset,fixed-Hz 的 reset:null 回到 ratio mode。performance.getPartControls(part)回傳 frozen 有效聲部控制,不含個別 key override。詳見MIDI 表情控制。套件建置 13 個 demo scripts,包含兩首原創歌曲的播放/停止、專案存取與 WAV 匯出;demo HTML 仍僅供 checkout/網站使用。
建置在 dist 輸出後產生 HTML 文件與完整編譯器產生的 API 參考,本機新建套件將它們放在 doc/;歷史套件不變。可直接開啟 doc/index.html,或連同頂層 Markdown/法律檔案部署整個 doc/。本機搜尋支援鍵盤,不需伺服器、遠端搜尋服務或 runtime dependencies;Markdown 仍是唯一文件來源。
安裝與範例頁面 #
需要 Node.js 22+ 與 npm。不需 checkout 或建置工具鏈即可在新專案安裝 GitHub Release 的附加套件:
mkdir opm-app
cd opm-app
npm init -y
npm install https://github.com/JS-PACKAGE/OPM.js/releases/download/v1.10/opm.js-1.10.0.tgz
v1.10 Release tarball 包含這份同步文件;原 v1.8 標籤與套件維持不變。若要從原始碼自行封裝,以下另一路徑先在 OPM.js 儲存庫根目錄 執行,npm pack 會自行執行建置(prepack),不必預先重複執行 npm run build:
npm ci
npm pack
此 checkout 的套件版本產生 opm.js-1.10.0.tgz。以下從儲存庫根目錄建立同層的新專案 opm-app,假設 checkout 目錄名為 OPM.js;若名稱不同,請調整安裝指令中的路徑。若已用上面的 Release 路徑安裝,請跳過這段:
cd ..
mkdir opm-app
cd opm-app
npm init -y
npm install ../OPM.js/opm.js-1.10.0.tgz
既有專案只需在該目錄以 Release URL 或 tarball 的實際路徑執行 npm install,不需 npm init。此流程不假設已上架 npm registry;使用端不需建置依賴。封裝含 dist 的最小化 .js 模組、內嵌 TypeScript 原始碼的 .js.map、.d.ts、13 個 demo entry scripts、文件與法律檔案,不含獨立 TypeScript 原始碼檔案、tests、開發 scripts、13 個 HTML 範例頁面或 Vite 範例;維護指令應在 checkout 執行。
要直接試用儲存庫的範例,請在 OPM.js 儲存庫根目錄 啟動伺服器:
python3 -m http.server 8000
開啟 http://localhost:8000/index.html 查看 13 個範例:基本音符、前瞻排程、即時調變、共用 context、WAV、進階控制台、試聽、共用樂譜/中斷恢復、自適應音樂、表情樂器/MIDI、音色設計、音訊匯流排與原創歌曲。HTML 頁面僅供 checkout 使用,頁面訊息為英文。Python 3 只提供本機 HTTP;既有 dist 不需安裝建置,原始碼/helper 改動後才執行 npm ci、npm run build。需 ES modules/AudioWorklet 與 HTTPS 或 localhost,不能用 file://。
瀏覽器靜態部署 #
在已安裝套件的 opm-app 根目錄複製完整發佈樹與授權檔:
mkdir -p public/opm
cp -R node_modules/opm.js/dist/. public/opm/
cp node_modules/opm.js/LICENSE public/opm/LICENSE
將下列頁面存為 opm-app/public/index.html:
<!doctype html>
<html lang="zh-Hant">
<meta charset="utf-8">
<title>OPM.js example</title>
<button id="play" type="button">Play brass chord</button>
<output id="status" aria-live="polite">Ready</output>
<script type="module">
import { OPM } from './opm/api/index.js';
const opm = new OPM();
const play = document.querySelector('#play');
const status = document.querySelector('#status');
play.addEventListener('click', async () => {
play.disabled = true;
try {
await opm.start();
for (const note of [60, 64, 67]) opm.playNote({ note, duration: 0.7 });
status.textContent = 'Playing brass chord';
} catch (error) {
status.textContent = error.message;
} finally {
play.disabled = false;
}
});
</script>
</html>
在 opm-app 根目錄執行:
python3 -m http.server 8000 --directory public
開啟 http://localhost:8000/,點擊按鈕播放。上述複製指令適用 POSIX shell;其他系統可手動複製相同檔案。若完全不使用 npm,亦可將 checkout 的完整 dist/ 內容複製到網站根目錄的 site/opm/,將 checkout 的 LICENSE 複製為 site/opm/LICENSE,把同一 HTML 存成 site/index.html,並以網站根目錄 site/ 提供服務。務必連同 api/、core/、worklet/、voices/ 一起保留,不能單獨重新命名或移動 worklet 及其相依模組。引擎保留來源模組路徑,不再產生 hashed chunks。純 HTML 無 import map 時不能直接解析 import 'opm.js';即便使用 bundler,也不能假定它會複製 worklet URL 的模組相依樹。上述靜態複製與站內相對 URL 不依賴 bundler。
Worker 匯出需要完整同版本樹,包括 companions、maps 與 declarations:
public/opm/
├── api/
├── core/
├── worklet/
├── worker/render.js
└── voices/
Worker 為同源 HTTP(S) 靜態 module,不使用 blob/data/eval;正式 CSP 的 worker-src/script-src 應允許 'self',資產須有正確 JavaScript MIME。不要把 worker 請求改寫為 SPA HTML。
使用 Vite 的獨立、子路徑部署範例見 examples/vite/README.md(僅供 checkout):它安裝 tarball 並複製完整 dist 與 LICENSE,不依賴 npm registry 是否已上架。正式站點須提供正確 JavaScript MIME、真正的資產 404,且不得把 worklet 請求改寫成 SPA HTML;設定 CSP 時應使用外部 module script 與適當來源政策,不要為了範例的行內 script 放寬正式站點防護。部署安全細節見 SECURITY.md。
已安裝套件也提供 opm-assets,可取代手動複製。從 opm-app 根目錄使用尚不存在的目的目錄:
npx --no-install opm-assets copy public/opm-1.10.0
npx --no-install opm-assets check https://your-host.example/opm-1.10.0/
第二行請換成實際部署 URL;本機可用 loopback HTTP。若採此目錄,頁面匯入路徑也須改為 ./opm-1.10.0/api/index.js。copy 原子複製完整 dist/LICENSE、逐檔雜湊並建立 manifest,絕不覆寫既有目錄;check 比對實際回應的狀態、JavaScript MIME、nosniff 與 SHA-256,拒絕 redirect/SPA fallback,但不驗證頁面 CSP 或啟動音訊。一般 Python HTTP server 不提供 nosniff,不符合預設 check;正式主機須設定此標頭。完整契約見宿主整合。
瀏覽器 API 與自訂音色 #
| API | 用法與限制 |
|---|---|
new OPM({ sampleRate, context, destination, workletUrl, onEvent, mixGain=1, tuning={}, stealing='oldest', interruption='cancel', quality='standard', maxVoices=8 }={}) | 整數 sampleRate 8000–96000 Hz;maxVoices 為整數 1–32,預設 8。quality/maxVoices 建構後不可變,close/重啟仍保留。借用 context 不會被關閉/暫停;省略 destination 接到其 destination,null 不自動接線。workletUrl 可改變同源完整 worklet 模組樹的位置,但不放寬安全來源、CSP 或 MIME 限制。 |
await opm.start()/await opm.resume() | 在使用者互動中啟動/恢復並等待,再播放。並行啟動會合併;既有 context 被瀏覽器暫停後可恢復。 |
opm.connect(destination)/opm.disconnect(destination?) | 對已啟動的輸出接線/斷線,回傳 opm;目的節點必須來自相同 context。省略斷線目的地會移除所有輸出連線。 |
opm.loadVoice(name, voice) | 啟動前即可驗證、複製並註冊音色;名稱為 1–64 個 ASCII 英文字母、數字、_ 或 -,registry 最多 128 筆。覆蓋只影響未來音符。 |
opm.voices | ReadonlyMap 的防禦性快照;音色深度凍結。更換音色須用 loadVoice,修改取得的 Map 不會改變引擎。 |
opm.replaceVoiceBank(bank)/opm.exportVoiceBank()/opm.removeVoice(name) | 完整驗證後原子替換;bank 是 JSON 字串或完整音色陣列,最多 128 筆/JSON 256 KiB。直接傳 [] 清空,但 JSON "[]" 依 parser 契約拒絕。export 回傳具名不可變音色的 canonical JSON(空銀行為 []),上限 256 KiB。remove 回傳 boolean,不終止正在播放/排隊的快照。 |
opm.playNote({ voice='brass', note, time=0, at, duration=null, velocity=1, pan=0, voicePriority=0, late='start' }) | 有限 MIDI 小數 0–127;相對 time 0–60 秒或互斥的絕對 at(最多提前 60 秒)。duration 為 (0,60] 或 null 持續音;力度 0–1、聲像 −1–1、voicePriority 為整數 0–127。回傳正安全整數音符 ID,不代表接收成功。 |
opm.stop(id,{at,cancelControls}={}) | 一般 stop 取消待開始音符/開始 release,保留自然尾音的 automation。cancelControls:true 立即取消此 ID 的排隊 onset/control/off 並 release;cancelControls 欄位即使為 false,也不能與 at 共用。未知、終止或已 release 目標會回報命令拒絕。 |
opm.updateNote(id,controls,{at}={}) | 回傳命令 ID;待開始控制保留到 onset,release 中仍可更新。無效、inactive 或 queue-full 命令會回報拒絕。 |
opm.allNotesOff()/opm.panic() | 回傳命令 ID,不受已滿排程佇列阻擋。前者取消待處理事件並自然 release;後者立即移除所有尾音並發出 reset,保留路由/快取。 |
opm.setMixGain(gain)/opm.setTuning(tuning) | 須先啟動,回傳命令 ID;設定保留到 node 重建,不重設 phase/包絡。 |
await opm.getDiagnostics() | 回傳 {type:'diagnostics',requestId,activeVoices,pendingEvents,errors,rejectedNotes};context 須在 running,最多 64 個未完成請求。暫停/關閉/processor 失敗會拒絕待回覆請求,請先 resume 再重試。 |
await opm.close() | 與初始化依序處理,斷開 node,只關閉自行建立的 context。再次啟動會重建自有 context;借用的 context 仍可由宿主使用。 |
opm.subscribe(listener) | 獨立事件訂閱,回傳冪等的取消訂閱函式;各回呼例外隔離。 |
await opm.waitForCommand(commandId,{timeout,signal}={}) | 僅等待 admission:預設 5000 ms、整數 1–60000 ms,可由 AbortSignal 取消;最多 64 pending waits/128 receipts。拒絕拋出 CommandRejectedError;accepted 不是執行完成。 |
await opm.dispose() | 終止式清理,不能重新啟動;close 仍可重新 start。 |
onEvent 除 note/diagnostics/error,新增 {type:'command',command,commandId?,id?,state,reason?,frame,time}、context 狀態與全域 reset。命令 state 為 accepted/rejected;accepted 只表示接收或立即套用,不保證未來一定執行。以 commandId 對應拒絕;frame/time 為音訊邊界而非回呼送達時間。reset 原因為 close/failure/panic/interruption,收到時清除自有 gate 紀錄;close/failure 不保證每音符終止回覆。回呼例外隔離。
遲到音符預設 late:'start':在可用的第一個影格開始,數值 duration 從實際開始保留完整 gate;late:'drop' 則以 reason:'late' 拒絕,包含訊息處理延遲造成的遲到。同影格依序處理 stop、onset、controls;onset 前收到的控制保留到開始。
controls 為非空 own-data 物件:pitch −48..48 半音;glide 0..10 秒且須搭配 pitch;expression 0..1;pan −1..1;modulation 0..2(AM 上限 1、PM 1200 音分);operatorLevels 是四個 0..2 的原音色 level 倍率。新增 feedback 0..7、lfoRate 0..20 Hz、amDepth 0..1、pmDepth 0..1200 音分;operatorRatios 為四個 0.125..32,operatorFrequencies 為四個 1..20000 Hz 或 null(恢復 ratio 模式),operatorADSR 為四個完整 {a,d,s,r}。ramp 0..10 秒獨立平滑指定的 scalar/level/ratio/frequency 欄位,省略/零立即生效;glide 獨立以半音線性滑動。phase/回授歷史保留,但 ADSR 從當前 dB 重新錨定:held 音重啟 attack,released 音開始新縮放 release(最多 10 秒),零 release 立即進入 filter drain。固定 Hz 仍跟隨 pitch 控制,忽略 tuning table 的移調。未知欄位、存取器、非有限/超界值拒絕。詳細控制語意見表情音色,宿主時鐘/生命週期見宿主整合。
v1.9 新增 gain(0–1、預設 1),獨立乘上 expression,依 ramp 平滑且不重啟 phase/包絡;Arrangement layer fade 使用這個獨立倍率。
最多 maxVoices 個邏輯聲部(1–32,預設 8),另有至多八個獨立約 5 ms 搶音淡出;增加 maxVoices 不增加淡出數。滿額時只能搶走 voicePriority 不高於新音符的聲部,先選最低優先權,再依 stealing 政策決定。全部較高時回報音符拒絕 reason:'priority',不搶走既有音符。預設 oldest;release-first 優先最早 release,quietest 按 carrier 包絡 × 力度 × expression 比較,同分取最早,不以瞬間波形判定。事件/ID 各限 256,未來定時音符佔兩筆,控制/stop 也佔額度;終止會回收過期事件。預備音色採 128 槽 content-key LRU,重新驗證後替換/重用 ID,既有排程與聲部保留原快照。無 setLFO;速率/深度可用 updateNote,波形與目標屬於音色。
要自訂 LFO、排程與提早停止,保留上方 public/index.html 的 #play 按鈕與 #status 輸出,將其原本的 <script type="module">…</script> 完整替換為以下區塊;從 opm-app 根目錄照前節提供 public/:
<script type="module">
import { OPM } from './opm/api/index.js';
import { brass } from './opm/voices/brass.js';
const opm = new OPM();
const voice = structuredClone(brass);
voice.lfo.rate = 5;
voice.lfo.amDepth = 0.2;
voice.ops[0].level = 0.6;
opm.loadVoice('my-brass', voice);
const play = document.querySelector('#play');
play.textContent = 'Play custom brass note';
const status = document.querySelector('#status');
play.addEventListener('click', async () => {
play.disabled = true;
try {
await opm.start();
const at = opm.context.currentTime + 0.1;
const id = opm.playNote({ voice: 'my-brass', note: 64, at, velocity: 0.7, pan: -0.5 });
opm.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8, pan: 0.5, modulation: 1.2 }, { at: at + 0.1 });
opm.stop(id, { at: at + 0.3 });
status.textContent = 'Scheduled custom brass note and controls';
} catch (error) {
status.textContent = error.message;
} finally {
play.disabled = false;
}
});
</script>
上述 onset、控制與 release 皆由音訊時鐘執行,不靠頁面計時器停止;單次僅排三筆事件,且皆在 60 秒範圍內。完成使用後可在應用程式生命週期中 await opm.close(),下次要播放時再由使用者互動呼叫 start()。
有界前瞻排程 #
長曲用 createLookaheadScheduler 分批排程,而非一次塞滿 queue。將同一 opm-app/public/index.html 的 module script 完整替換如下,再從 opm-app 根目錄提供 public/。點擊播放會安排有限的八個音符;主執行緒停頓時跳過已錯過的窗口,不補塞過期音符:
<script type="module">
import { OPM, createLookaheadScheduler } from './opm/api/index.js';
const status = document.querySelector('#status');
const opm = new OPM();
const melody = [60, 64, 67, 72, 67, 64, 62, 60];
let origin;
const scheduler = createLookaheadScheduler(opm, ({ from, to, maxNotes }) => {
origin ??= from + 0.1;
return melody.flatMap((note, index) => {
const at = origin + index * 0.3;
return at >= from && at < to
? [{ note, at, duration: 0.2, velocity: 0.6, late: 'drop' }]
: [];
}).slice(0, maxNotes);
}, {
horizon: 0.2, interval: 0.025, maxNotes: 8,
onError(error) { status.textContent = error.message; }
});
const play = document.querySelector('#play');
play.textContent = 'Play finite melody';
play.addEventListener('click', async () => {
try {
scheduler.stop();
origin = undefined;
await scheduler.start();
status.textContent = 'Scheduling eight notes';
} catch (error) { status.textContent = error.message; }
});
const stop = document.createElement('button');
stop.textContent = 'Stop melody';
document.body.append(stop);
stop.addEventListener('click', () => scheduler.stop());
window.disposeOPM = async () => {
scheduler.dispose();
await opm.close();
};
</script>
回呼同步回傳陣列,每筆必須有窗口 [from,to) 內的絕對 at 與 (0,60] 數值 duration,不能用相對 time 或持續音。onError 必填;計時器/回呼失敗會停止排程並通知它。預設 horizon 為 0.2 秒(範圍 0.02–10),interval 為 0.025 秒(0.005–1),且 horizon 必須大於 interval;maxNotes 預設 32(整數 1–128),最多追蹤 128 個尚未關 gate 的自有音符。本例每個窗口至多一音,全部音符播放後回呼回傳空陣列,計時器仍運作直到 stop/dispose。兩者只取消/釋放自己的音符,不關閉 OPM;dispose 後不可重啟。移除介面時呼叫 await window.disposeOPM()。
任何 non-running context 或 reset 都會停止前瞻排程並通知 onError,須由使用者互動明確重啟。預設 interruption:'cancel' 取消中斷前 gate/automation;preserve 只保留直接音符/佇列,不自動恢復前瞻計時器。
共用 context 與輸出接線 #
若宿主已有 AudioContext,可將快速範例的 module script 替換成以下區塊。按播放後音符持續到按停止;disposeOPM() 只清除本合成器,不會關閉宿主 context:
<script type="module">
import { OPM } from './opm/api/index.js';
const context = new AudioContext();
const gain = context.createGain();
gain.gain.value = 0.3;
gain.connect(context.destination);
const status = document.querySelector('#status');
const opm = new OPM({
context, destination: null,
onEvent(event) {
if (event.type === 'reset') {
id = undefined;
if (event.reason === 'close' || event.reason === 'failure') connected = false;
}
status.textContent = event.type === 'error' ? event.error.message
: event.type === 'note' ? `${event.id}: ${event.state}`
: event.type === 'diagnostics' ? `Voices: ${event.activeVoices}`
: event.type === 'command' ? `${event.command}: ${event.state} ${event.reason ?? ''}`
: event.type === 'context' ? `Context: ${event.state}` : `Reset: ${event.reason}`;
}
});
const stop = document.createElement('button');
stop.textContent = 'Stop held note';
document.body.append(stop);
let id;
let connected = false;
document.querySelector('#play').addEventListener('click', async () => {
try {
await opm.resume();
if (!connected) { opm.connect(gain); connected = true; }
if (id !== undefined) opm.stop(id);
id = opm.playNote({ note: 60, velocity: 0.7, pan: 0.5 });
console.log(await opm.getDiagnostics());
} catch (error) { status.textContent = error.message; }
});
stop.addEventListener('click', () => { if (id !== undefined) opm.stop(id); });
window.disposeOPM = async () => {
await opm.close();
connected = false;
id = undefined;
gain.disconnect();
};
</script>
移除此應用程式的音訊介面時呼叫 await window.disposeOPM()。context/下游節點由宿主管理;不要為了停止單一合成器而暫停整個共用 context。
共用樂譜、全域調音與安全恢復 #
mixGain 是有限 0–1(預設 1),在 tanh 前縮放;下游 GainNode 只控制聆聽音量,無法還原已產生的飽和失真。tuning 替換完整設定:referenceHz(A4)20–20000 Hz,offsets 可省略表示零,否則為恰好 128 個 −4800..4800 的有限音分。小數 MIDI 線性插值相鄰偏移,127 使用自身端點。即時調音保留 phase/包絡與原音符 key scaling。
保留 quick-start 的 play/status,將 module script 替換為:
<script type="module">
import { OPM, playSequence } from './opm/api/index.js';
import { renderSequence } from './opm/core/index.js';
import { brass } from './opm/voices/brass.js';
const settings = { mixGain: 0.5, tuning: { referenceHz: 442 }, stealing: 'release-first' };
const opm = new OPM({ ...settings, interruption: 'cancel' });
const score = [
{ type: 'note', id: 1, time: 0, duration: 0.4, voice: 'lead', note: 60.5 },
{ type: 'control', id: 1, time: 0.1, controls: { pan: -0.5, expression: 0.7, ramp: 0.03 } },
];
let playback;
document.querySelector('#play').addEventListener('click', async () => {
try {
await opm.start();
playback?.stop();
opm.loadVoice('lead', brass);
playback = playSequence(opm, score, { at: opm.context.currentTime + 0.05 });
const audio = renderSequence(score, { ...settings, sampleRate: opm.context.sampleRate, voices: opm.voices });
document.querySelector('#status').textContent = `Rendered ${audio.left.length} frames; errors ${audio.diagnostics.errors}`;
} catch (error) { document.querySelector('#status').textContent = error.message; }
});
window.disposeOPM = async () => { playback?.stop(); await opm.close(); };
</script>
樂譜使用相對秒數;note 為唯一正 ID 與有限 duration,stop/control 指向同一樂譜音符。整份驗證後才派送;限 128 notes、256 保留槽(每音符兩槽加命令)、含 gate 的 60 秒及離線 4,000,000 影格。playSequence 回傳防禦性 scoreId→noteId Map 與冪等 stop,不會對其他 caller 原子保留 worklet 容量。renderSequence 包含尾音;相同 PCM 須設定/取樣率/相對影格原點一致且無其他競爭音符。WAV 僅傳 left/right/sampleRate。
以 HTTPS 在實體 iOS/Android 使用第 08 範例(僅供 checkout),依行動裝置驗收流程與支援矩陣記錄裝置/OS/瀏覽器/取樣率/政策、觀察與手動 pass/fail/unverified 判定。涵蓋鎖屏、切換 app、來電、耳機/Bluetooth、節電、長播放與主執行緒停頓。報告僅留本機;桌面、headless 與 analyser 訊號不代表實機恢復或聽感不中斷。恢復須手勢;借用 context 由宿主管理。
有界長樂譜 #
24 小時/65,536 個輸入事件以內,改用 opm.js/core 的 prepareLongSequence、estimateSequenceCapacity 與 renderSequenceChunks。估算會分別列出整段 buffer/單批 eligibility 與預設串流視窗密度,並不保證和其他 caller 競爭時的 admission。每塊 PCM 是借用 buffer,下一次 next() 就會覆寫;立即消費,只有需要保留時才複製。break、cancel() 或 AbortSignal 停止前進,maxFrames 可限制累計工作量;整段 PCM/WAV 預算不變。
import { estimateSequenceCapacity, renderSequenceChunks } from 'opm.js/core';
const score = [{ type: 'note', id: 1, time: 0, duration: 61, note: 60 }];
const options = { sampleRate: 96000, chunkFrames: 4096 };
console.log(estimateSequenceCapacity(score, options));
let frames = 0, energy = 0;
const render = renderSequenceChunks(score, options);
for (const chunk of render) {
for (let i = 0; i < chunk.frames; i++) energy += chunk.left[i] ** 2;
frames += chunk.frames;
}
console.log(frames, energy, render.diagnostics.errors);
瀏覽器播放時與 OPM 一起 import streamSequence,建立 const stream = streamSequence(opm, score, { onError: console.error }),再於播放按鈕手勢內 await stream.start()。stop() 僅取消此串流的音符;dispose() 移除 listener 並永久終止此實例。control、明確 stop 與自動 release 跨視窗保留 score ID 對應。兩種政策的 interruption/reset 都停止串流;先恢復 context,再由使用者明確重啟。完整瀏覽器配方、預設值與密度限制見串流契約。
拍點 Transport 與演奏聲部 #
createTransport、createArrangement、createPerformance、createMidiAdapter、requestMidiAccess、playSequence、streamSequence、renderSequenceInWorker 與套件版本常數 VERSION 由根入口 opm.js(靜態部署為 ./opm/api/index.js)匯出。Transport 事件以 beat 取代 time,note.duration 也以四分音符拍計算。提供 start/resume、pause/stop、seek、setTempo(從目前位置起)、setTempoMap、setLoop、pump、dispose 與 state/position/running/snapshot/ids。它使用 AudioContext 時鐘,只管理自有音符。
bpm 預設 120、範圍 1–1000;tempoMap 從 beat 0 開始嚴格遞增,最多 1024 點,拍點上限 86400。timeSignature 分子 1–32,分母為不超過 32 的二次冪;horizon 0.01–10 秒、interval 0.001–horizon/2、maxSlots 1–256。pause/seek/loop 重啟包絡與 phase,不是 DSP snapshot;重建當前 scalar/ratio ramp 與剩餘時間,最新 fixed-Hz/null 政策立即套用(即使 frequency ramp 未完成),最新 ADSR 在重啟 onset 重新錨定。純核心亦匯出 beatsToSeconds/secondsToBeats/beatToBarBeat/barBeatToBeat/normalizeTempoMap。
下方配方保留音樂上的一拍 count-in。目前 checkout 額外使用 startupLead(預設 min(0.05,horizon/2) 秒)將時鐘原點放在未來,避免 beat 0 冷啟動時提交到已過期時刻;歷史 v1.8.1 套件沒有這項修正。late:'drop' 仍會拒絕真正晚到的起音、停止 helper 並通知 onError。Count-in/startupLead 不保證宿主停頓下仍能準時;start() 完成也不是每個音符已發聲的確認。
createPerformance(opm,{parts:16,maxKeys:128,maxKeysPerPart:128,onError}) 提供零起算 1–16 個聲部,各自 configurePart 設定 voice、poly/mono、legato、last/high/low 按鍵選擇 priority、glide 0–10 秒、pan ±1 與 expression 0–1;兩種 key 額度皆為 1–128。先在使用者手勢中 await opm.start(),再呼叫 noteOn;回傳實體 key ID,須用 noteOff(part,key) 釋放,同音高按鍵仍獨立。另有 updatePart、sustain、allNotesOff(part?)、getPart 與 dispose。實際 held keys 優先於踏板保留;mono legato 在原 onset ±48 半音內重用 gate/包絡/原力度與 key scaling,超出時以真實音高重觸發。被偷走的 key 會移除,不自動重入;中斷/reset 即使 preserve 也清除 helper 自有狀態。可直接執行的 Transport/Performance 配方見宿主整合與串流樂譜。
v1.8 的 updateKey(part,key,controls) 對單一實體按鍵套用 NoteControls;未選取的 mono key 保留控制,不改動目前 gate。updatePartNotes(part,controls) 更新整個聲部的自有音符(含 release 尾音)與未來音符預設。configurePart 的 voiceLimit 為 1–32,自有 release 尾音也佔額度,滿額 noteOn 拋出 RangeError;它不增加引擎聲部數。voicePriority 為 0–127(預設 0),與 last/high/low 的按鍵選擇 priority 不同。
選用的 createMidiAdapter(performance,await requestMidiAccess(),options) 將使用者授權的 MIDI inputs 映射到聲部,支援 note、sustain、bend、wheel、volume/expression、pan、pressure 與 all-notes-off。匯入模組不會要求權限,永不要求 SysEx;disconnect/dispose 只釋放 adapter 自有按鍵。詳細配方與限制見表情演奏與 MIDI及第 10 範例(僅供 checkout)。
速度曲線、網格與分層編曲 #
TempoPoint 為 {beat,bpm,curve?,endBpm?};省略 curve 或 'step' 為定速段,'linear' 使 BPM 隨拍點線性變化至下一點 bpm 或指定 endBpm(僅 linear 可用;末點不可 linear)。beatsToSeconds/secondsToBeats 使用閉式對數積分與反函式。quantizeBeat、swingBeat、swingBeatEvents 從根入口 opm.js 匯入(純瀏覽器使用部署的 ./opm/api/index.js),不是 opm.js/core 匯出;後者連同音符起點與終點一起套用 swing。
createArrangement(opm,{layers,sections,initialSection,bpm?,tempoMap?,timeSignature?}) 在共用拍點網格上循環具名 layers;switchSection/setLayer 在不早於已接收音符的第一個量化邊界提交(預設 bar,也可 beat/拍數),回傳提交拍點。共用 layer 保持連續,不重觸發;移除 layer 預設在邊界 release,preserveNotes:true 可讓它自然結束。layer 與 note 的 voicePriority 取較大值,優先權拒絕計入 snapshot.priorityDrops 而不停止編曲,其他拒絕會停止並通知 onError。至多 16 layers、32 sections、65,536 events,每層循環長度至多 256 拍;只清理自有音符。Arrangement pause/resume 不還原跨越恢復點的持續音,只從後續 onset 繼續;它和 Transport 都不是 DSP checkpoint。公式、密度上限及可執行配方見自適應音樂與第 09 範例(僅供 checkout)。
保留 quick-start 的按鈕/輸出並替換 module script,可直接試用兩種 helper:
<script type="module">
import { OPM, createTransport, createPerformance } from './opm/api/index.js';
const opm = new OPM();
const transport = createTransport(opm, [
{ type: 'note', id: 1, beat: 1, duration: 1, note: 60 },
{ type: 'note', id: 2, beat: 2, duration: 1, note: 64 }
], { bpm: 120, onError: console.error });
const performance = createPerformance(opm, { parts: 1 });
performance.configurePart(0, {
voice: 'brass', mode: 'mono', legato: true, priority: 'last', glide: 0.1
});
document.querySelector('#play').addEventListener('click', async () => {
try {
transport.stop();
await transport.start();
const key = performance.noteOn(0, 67, { velocity: 0.7 });
performance.sustain(0, true);
performance.noteOff(0, key);
document.querySelector('#status').textContent = 'Release pedal to stop held note';
} catch (error) {
document.querySelector('#status').textContent = error.message;
}
});
const pedal = document.createElement('button');
pedal.textContent = 'Release pedal';
document.body.append(pedal);
pedal.addEventListener('click', () => performance.sustain(0, false));
window.disposeOPM = async () => {
transport.dispose();
performance.dispose();
await opm.close();
};
</script>
Node.js 離線渲染 #
在已安裝套件的 opm-app 根目錄建立 render.mjs:
import { Synth, prepareVoice } from 'opm.js/core';
import { brass } from 'opm.js/voices/brass.js';
const sampleRate = 44100;
const synth = new Synth(sampleRate);
const prepared = prepareVoice(brass);
const id = synth.noteOn(prepared, 60);
const left = new Float32Array(sampleRate);
const right = new Float32Array(sampleRate);
synth.render(left, right, 0, 22050);
synth.updateNote(id, { pitch: 7, glide: 0.1, expression: 0.8 });
synth.noteOff(id);
synth.render(left, right, 22050, 22050);
console.log(left.some(sample => sample !== 0), synth.errorCount);
在同一目錄執行 node render.mjs,預期顯示 true 0。中央聲像為左右相同,其他位置產生立體聲;此例只產生記憶體 PCM,不會自動播放或寫檔。checkout 匯入改為 ./dist/core/index.js 與 ./dist/voices/brass.js。使用 .mjs 或 "type":"module",沒有 CommonJS 入口。
new Synth(sampleRate,maxVoices=8,{mixGain,tuning,stealing,quality='standard'}={}) 需要有限 8000–192000 Hz 與整數 1–32 聲部(預設 8);noteOn 接受有限小數 MIDI 0–127、嚴格力度/聲像、voicePriority 0–127(預設 0)及未占用正安全 ID。滿額且所有聲部優先權較高時拋出 VoiceAdmissionError,保留既有聲部。noteOff/updateNote 回傳 boolean;allNotesOff 自然 release,panic 移除所有尾音;設定 gain/tuning 不重設 phase/包絡。onVoiceEnded 每音符一次 stolen/ended/error/cancelled;lastStolenId 無搶音時為 null。
prepareVoice(input) 一次嚴格驗證並建立不可變的可信任快照;不能用型別斷言或自製物件偽造。Synth.noteOn 可重用此快照,原始音色輸入則仍逐次驗證。核心預先配置 maxVoices + 9 個狀態槽(maxVoices 個邏輯聲部、八個淡出與一個入列暫存槽;預設 17 個),重用預備音色不需每音符配置 Float64 狀態陣列,但這不是整個 JavaScript/瀏覽器零配置保證。synth.updateNote(id, controls) 使用前述控制範圍,成功回傳 true,未知/已終止 ID 回傳 false。
onVoiceEnded 可呼叫 noteOn/noteOff;回呼中新增的音符最早在下一影格渲染,不回頭改寫已計算的當前影格。不可在回呼中遞迴呼叫 render;引擎會拒絕。通知有固定工作上限,應保持回呼簡短。
render(left,right,offset=0,length=nativeLeftLength-offset) 需原生 Float32Array,非負安全整數範圍容納於兩個實際緩衝區。遮蔽 length/fill 不能增加工作,Proxy/偽造及需數值 coercion 的輸入拒絕。自行分段渲染/保留 release;currentFrame 為累計影格,errorCount 記錄數值失敗。
若只需要單音的輔助渲染函式,在 opm-app 根目錄另存 render-note.mjs:
import { renderNote } from 'opm.js/core';
import { brass } from 'opm.js/voices/brass.js';
const { samples, sampleRate, diagnostics } = renderNote({
voice: brass,
note: 60,
duration: 0.1,
sampleRate: 44100
});
console.log(samples.length, sampleRate, diagnostics.errors);
執行 node render-note.mjs;checkout 改用 ./dist/... 匯入。renderNote({voice,note=60,duration=0.5,velocity=1,pan=0,sampleRate=44100}) 回傳 {samples,left,right,sampleRate,diagnostics:{errors}},samples === left。要求完整 Schema 音色;有限的音色數值會限制到範圍,但無效版本/algorithm/feedback 仍拒絕。有限 note/duration/velocity/pan 分別限制至 0–127/0–30 秒/0–1/−1–1;note 可為小數,sampleRate 必須是整數 8000–96000。
renderNote 也接受嚴格 mixGain/tuning/stealing/quality,與 renderSequence、分塊渲染及 Worker 共用。quality 為 eco(2×/四階)、standard(預設 4×/八階,保留原預設輸出)或 high(8×/八階);不是無混疊或即時期限保證。模式取捨見聲學品質,CPU 須在目標宿主測量。
此函式與 Synth 使用相同包絡、LFO、濾波、力度、聲像及飽和路徑。長度為 ceil((duration+最長有效release+0.01)*sampleRate),包含 note-dependent rate scaling,上限 4,000,000 影格。飽和前聲像增益為 sqrt(2)*cos/sin((pan+1)*pi/4);飽和後不保證感知音量恆定。
其他 opm.js/core 公開項目:normalizeVoice(嚴格驗證並產生獨立複本)、prepareVoice、NoteControls/PreparedVoice 型別、envelopeAt(time, gate, adsr)(以秒為單位的時間/gate 計算 dB 包絡對應振幅)、不可變的八種訊號圖 ALGORITHMS、HEADROOM = 0.7、OVERSAMPLE = 4、MAX_RENDER_SAMPLES = 4000000,以及整數 8000–96000 取樣率驗證器 sampleRateValue。
音色格式與銀行載入 #
參考 voice.schema.js 的 voiceSchema 與 examples.js 的 examples 匯出。下列為單一音色 JSON 物件,四個 ops 順序是訊號演算法的運算子順序;若要作為銀行,必須用 [ 和 ] 包成陣列:
{
"version": 6,
"name": "simple",
"algorithm": 7,
"feedback": 0,
"modIndex": 4,
"lfo": { "rate": 0, "amDepth": 0, "pmDepth": 0, "waveform": "sine", "amTargets": [1, 1, 1, 1], "pmTargets": [1, 1, 1, 1] },
"ops": [
{ "ratio": 1, "level": 0.8, "detune": 0, "adsr": { "a": 0.01, "d": 0.2, "s": 0.6, "r": 0.2 }, "keyScale": { "breakpoint": 60, "leftDbPerOctave": 0, "rightDbPerOctave": 6 } },
{ "ratio": 2, "level": 0.4, "detune": 0, "adsr": { "a": 0.01, "d": 0.2, "s": 0.5, "r": 0.2 } },
{ "ratio": 3, "level": 0.3, "detune": 0, "adsr": { "a": 0.01, "d": 0.2, "s": 0.5, "r": 0.2 } },
{ "ratio": 4, "level": 0.2, "detune": 0, "adsr": { "a": 0.01, "d": 0.2, "s": 0.5, "r": 0.2 } }
]
}
| 欄位 | 範圍 |
|---|---|
version/name | 目前 canonical 版本 7;explicit 1–6 僅接受各自原有欄位。v1 無 keyScale,v1/2 無 velocitySensitivity,v1–3 無 waveform,v1–4 不接受 v5 欄位;v5 不接受 v6 targets;v1–6 不接受運算子 waveform/noiseRate。名稱為 1–64 ASCII 字母/數字/底線/連字號。 |
algorithm/feedback/modIndex | 前兩者是 0–7 的整數;調變強度 modIndex 是 0–16。 |
四個 ops 各項 | 必須提供 ratio 0.125–32、level 0–1、detune -1200–1200 音分,以及 adsr 的 a、d、s、r;a/d/r 是 0–10 秒,s 是 0–1。 |
lfo | rate 0–20 Hz、amDepth 0–1、pmDepth 0–1200 音分;waveform 為 sine(預設)、triangle、saw 或 square。 |
ops[i].keyScale | 可省略表示平坦;breakpoint 為 MIDI 整數 0–127,leftDbPerOctave/rightDbPerOctave 為 0–24 dB/八度。 |
ops[i].velocitySensitivity | 從版本 3 起可選,0–48 dB,省略為 0;定義 velocity=0 時的運算子衰減。 |
ops[i].frequency/ops[i].rateKeyScale | v5 可選固定頻率 1–20000 Hz(ratio 仍必填)、速率鍵位縮放 0–4;ADSR 時間乘 2 ** (-scale*(note-60)/12) 並限制為最多 10 秒。 |
ops[i].waveform/ops[i].noiseRate | v7 可選的振盪波形:sine(預設)、half、abs、quarter、alternating、camel、square、saw、noise;noiseRate 為 20–20000 Hz 的保持式雜訊時脈(預設 8000),僅在波形為 noise 時有意義。雜訊運算子沒有音高。 |
pitchEnvelope | v5 可選 {a,d,r,initial,peak,sustain,final};時間 0–10 秒、音高 −4800..4800 音分;release 從目前音高連續開始。 |
lfo.delay/lfo.sync/lfo.phase | v5 可選 0–10 秒、note/global、0–1 圈,預設 0/note/0;delay 門控深度,不停止 phase 時鐘。 |
lfo.amTargets/lfo.pmTargets | v6 可選各四個 0–1 權重;boolean 輸入正規化為數字。省略為 [1,1,1,1],依運算子順序套用。 |
單一音色可省略 version/name/lfo/modIndex,仍需 algorithm/feedback 與四個完整運算子。預設目前格式、modIndex 4、LFO off/sine,輸出統一版本 7;舊版 1–6 嚴格保留原始形狀,不得挾帶後來欄位。內建音色與 DX7 匯出也採 canonical v7。所有路徑拒絕錯誤型別、未知欄位、存取器及稀疏陣列,嚴格路徑另拒絕超界數字。鍵位縮放依原音符;pan 屬於音符。完整組合語意見表情音色參考。
parseVoiceBank() 由 opm.js/voices/schema.js(瀏覽器靜態樹中為 ./opm/voices/schema.js)匯出,可接受 JSON 字串或音色陣列,要求 1–128 筆完整音色且名稱不重複,回傳 Map<name, voice>,其音色為已驗證且凍結的完整物件。字串輸入最多 256 KiB(UTF-8);陣列輸入也有數量限制。和 validateVoice()、renderNote() 相同,此路徑將有限但超界的數值限制在範圍內,與嚴格的單音色 normalizeVoice() 不同;不合法的 version/algorithm/feedback、非有限數值、錯誤型別、未知欄位仍會拒絕。schema.js 也提供 validateVoice(voice)、有限數值邊界函式 bounded(value, min, max, label = 'number')、LIMITS、MAX_BANK_BYTES 與 MAX_BANK_VOICES。
要載入已部署的可信任 examples.js 模組,保留先前 opm-app/public/index.html 的 #play 與 #status,將其原本 module script 完整替換為:
<script type="module">
import { OPM } from './opm/api/index.js';
import { parseVoiceBank } from './opm/voices/schema.js';
import { examples } from './opm/voices/examples.js';
const opm = new OPM();
const play = document.querySelector('#play');
play.textContent = 'Play bank brass note';
const status = document.querySelector('#status');
play.addEventListener('click', async () => {
play.disabled = true;
try {
await opm.start();
const bank = parseVoiceBank(examples);
for (const [name, voice] of bank) opm.loadVoice(name, voice);
opm.playNote({ voice: 'brass', note: 60, duration: 0.7 });
status.textContent = 'Playing bank brass note';
} catch (error) {
status.textContent = error.message;
} finally {
play.disabled = false;
}
});
</script>
仍在 opm-app 根目錄以 python3 -m http.server 8000 --directory public 提供頁面。此例匯入套件所附的可信任模組;parseVoiceBank 仍支援外部 JSON 銀行。若接收不可信任的遠端下載,應由應用程式先限制來源、Content-Type 與下載大小,再讀取回應內容;合成引擎沒有內建網路下載 API。
WAV 匯出與 DX7 匯入 #
在 opm-app 建立 export.mjs,執行 node export.mjs 會寫出立體聲 WAV:
import { writeFile, readFile } from 'node:fs/promises';
import { renderNote, encodeWav } from 'opm.js/core';
import { brass } from 'opm.js/voices/brass.js';
import { importDX7, describeDX7 } from 'opm.js/voices/dx7.js';
const audio = renderNote({ voice: brass, duration: 0.7, velocity: 0.8, pan: -0.5 });
await writeFile('brass.wav', encodeWav({ left: audio.left, right: audio.right, sampleRate: audio.sampleRate }));
// 有檔案時:node export.mjs /實際路徑/voice.syx
if (process.argv[2]) {
const bytes = new Uint8Array(await readFile(process.argv[2]));
const voices = importDX7(bytes);
console.log(describeDX7(bytes));
const imported = renderNote({ voice: voices[0], duration: 1 });
await writeFile('imported.wav', encodeWav({ left: imported.left, right: imported.right, sampleRate: imported.sampleRate }));
}
checkout 匯入改為 ./dist/core/index.js、./dist/voices/brass.js 與 ./dist/voices/dx7.js。encodeWav({left,right?,sampleRate,format='pcm16'}) 回傳小端序 RIFF 位元組,format 可為 pcm16/pcm24/float32;省略 right 為單聲道。所有格式的 Float32Array 樣本須有限且在 −1–1,超界拒絕而非削波;立體聲長度須相同。取樣率整數 8000–192000,上限 4,000,000 影格。只傳這些自有資料欄位:整個 renderNote 結果有額外欄位,會被拒絕。編碼不會自動寫檔;短預覽可使用有明確記憶體預算的 Blob,長檔案須直接寫 sink。
增量 createWavEncoder({sampleRate,channels:1|2,format,totalFrames}) 由 opm.js/core 匯出:先寫一次 header(),每次 encode({left,right?}) 消費 1–65536 影格並回傳有界位元組,最後寫 finalize()(精確影格數與 RIFF padding)。它不保留完整 PCM/輸出,不受完整 buffer 的四百萬影格限制;仍受 RIFF32 大小與 24 小時加 release 預算限制。
瀏覽器長檔案用 renderSequenceInWorker(score,{sampleRate,chunkFrames,quality,format,sink,signal,onProgress}),sink 提供 write(bytes) 與可選 close/abort,write 可回傳 Promise 施加 backpressure;最多一塊編碼資料未確認。progress 提供 frames/totalFrames/bytesWritten/errors,結果為 capacity/format/bytesWritten/diagnostics。AbortSignal 取消立即終止 Worker 並拒絕,不等待卡住的 write/abort;sink 負責回滾/失效化未完成寫入。此 helper 僅限瀏覽器,純核心/Node 離線工具仍可用。可指定同源靜態 workerUrl,預設為樹內 worker/render.js。增量 WAV 契約與可執行的 Worker file sink/取消配方見宿主整合,長 PCM 消費配方見串流樂譜。
Worker 在模組載入/解析/求值後送出 {type:'ready',protocol:1},宿主收到有效 ready 才傳送樂譜。startupTimeoutMs 是選用整數 1–2,147,483,647 ms 的模組啟動 watchdog,僅從 Worker 建立量到 ready,逾時以 TimeoutError 拒絕並終止 Worker/abort sink,不限制渲染或慢速寫入。phaseDiagnostics:true 在成功結果的 diagnostics.phases 保留 initializing/rendering/writing/closing 與總時間;onPhase 同步通知轉換及 completed/cancelled/failed,回呼拋錯會使工作失敗。這些是宿主 wall-clock 時間,並非 DSP benchmark。
沒有內建整體期限或自動重試;可用 signal:AbortSignal.timeout(300000) 對整份工作設五分鐘政策。取消不等待卡住的 write/abort;sink 仍須失效化已寫入的部分輸出。完整診斷、deadline 與回滾配方見Worker 診斷。
第 08 範例(僅供 checkout) 提供真正 Transport/Performance、控制/品質/銀行/WAV 格式預覽與長檔案 Worker sink/取消。長檔案僅用 File System Access,不聚合成 Blob;短預覽有明確 8 MiB 記憶體預算。
importDX7(Uint8Array) 只接受單個框架/checksum 正確的 163-byte 單音色或 4104-byte/32 音色銀行;拒絕原始、串接或非 7-bit payload,輸出完整版本 6 音色。describeDX7 回傳 name/sourceAlgorithm/algorithm/selectedOperators/droppedOperators/warnings;原 algorithm 1–32、轉換後 0–7,operator 編號依 DX7 的 1–6。
這仍是六運算子轉四運算子的有損音樂啟發式轉換,不是 DX7 合成/模擬。固定 Hz 運算子會保留;力度/速率鍵位縮放、減少階段的 pitch envelope 與 LFO delay/sync 採近似。下降 saw/sample-and-hold 波形以其他波形替代並附 warning;路由、oscillator sync、transpose 與包絡細節仍有損。轉換器將所選運算子的 AM sensitivity 簡化成共用深度,不保留逐運算子 AM,也不映射至選擇性 v6 amTargets。先看 descriptions、表情音色與轉換限制及音色品質再試聽;不可信任檔案/下載仍須先限制大小。
品質與發佈驗收 #
在 checkout 執行維護指令,不是在已安裝的套件中:
所有程式皆使用 strict TypeScript;compile 輸出忽略追蹤的 .dev,npm test 編譯並選擇行為測試,不用裸 node --test。目前 build 由實作產生宣告、保留引擎路徑並建置 13 個 demo entry scripts,再由 Markdown 與輸出宣告產生靜態 HTML 文件;歷史 v1.8.1 為 12 個 scripts。每個 JS 配對 map/型別,typecheck 包含 source/tools/tests/demo/公開型別;使用端仍只需 JS,不需建置工具鏈。
npm ci
npm run build
npm test
npm run typecheck
npm run security -- --package-smoke
npx --no-install playwright install --with-deps chromium
npm run browser-smoke -- chromium
npm run benchmark
可改用 firefox/webkit 安裝與測試其他引擎。CI 設定 Node 22/24/26 與三種瀏覽器,但設定本身不代表執行成功;實測版本與結果以 CHANGELOG 為準。瀏覽器驗收觀察真正 AudioWorklet 訊號、有限輸出、生命週期/release 與發佈路徑。發佈前置條件見發佈指南;本儲存庫沒有 npm publish workflow,本文件也不表示已設定 registry 授權或已執行 npm publish。
Headless Linux Firefox 還需要運作中的原生音訊服務,只安裝瀏覽器函式庫並不足夠。CI 安裝 pulseaudio,執行 pulseaudio --start --exit-idle-time=-1、pactl load-module module-null-sink sink_name=opm_ci、pactl set-default-sink opm_ci,並以 pactl info 確認就緒後才執行 smoke。Null sink 僅丟棄喇叭輸出,仍保留原生音訊時鐘與真正 worklet graph;若初始化/resume 卡住,log 會列出等待階段及 context 狀態。
效能報告列出暖機後 128 影格區塊 p95/p99/最慢值與超過 128/sampleRate 秒的次數;OPM_BENCH_BLOCKS、OPM_BENCH_WARMUP、OPM_BENCH_SAMPLE_RATE 調整負載,p99/worst budget ratio 可設主機相關門檻。本機預設只報告;CI 設定 48 kHz 的 OPM_BENCH_P99_BUDGET_RATIO=1(2.667 ms),最慢值/GC 停頓僅報告,非所有平台即時保證。頻譜驗收比較 48 kHz 下折疊到 18 kHz 的 30 kHz 振盪器與同強度 1760 Hz 控制,要求至少 30 dB 衰減,不能宣稱所有 FM 無混疊。
另可在 checkout 執行 npm run sound-quality、npm run voice-quality、npm run browser-stress -- chromium 與 npm run vite-smoke -- chromium。品質矩陣涵蓋多取樣率、演算法、回授、音高、力度與多音;音色工具提供低/中/高音域與力度的 peak/RMS dBFS、建議宿主 trim 及 DX7 限制報告,並不自動重寫音色音量。詳細方法見音色品質指南。已觀察到的矩陣通過與瀏覽器有限訊號,只證明所測條件;主執行緒事件間隔不等於 worklet CPU/GC 時間,也不能證明所有平台零爆音、任意 FM 無混疊或全部音色等響度。
獨立驗收含 Bessel PM、收斂/全強度回授能量界限、獨立四運算子串接/分支/多 carrier 參考,以及兩段 120 秒 LFO/glide 串流。4× 八階 Butterworth decimator 以相位/延遲取捨換取較平坦的可聽 passband 與受控 stopband 衰減;詳見聲學參考數學與限制。這些不是任意音色、聽感、實體裝置或硬體保真保證。
壓縮部署 #
dist/ 每個最小化 .js 都有對應 .js.map 與 TypeScript 自動產生的 .d.ts,包含 demo scripts。引擎保留來源模組路徑,不產生 hashed chunks;demo 無公開匯出,其宣告如實為 export {};。建置不產生 JSON 資產或壓縮側檔。Source maps 映射回並內嵌原始 TypeScript;發佈 maps 即公開這些原始碼供除錯。需要壓縮時由網站主機處理,程式仍匯入原本的 .js URL。伺服器送出壓縮內容時,須提供對應 Content-Encoding、Vary: Accept-Encoding 與 JavaScript MIME 類型;未壓縮回應不可標示為已壓縮。基本的 Python HTTP 伺服器可直接提供一般模組。整套 dist 須維持同一建置版本;詳見 README 的最佳化發佈。
疑難排解 #
| 現象 | 修正方式 |
|---|---|
file://、非安全來源或 AudioWorklet 不可用 | 使用支援 AudioWorklet 的現代瀏覽器,由 HTTPS 或 localhost 提供整個頁面與檔案。 |
瀏覽器無法解析 opm.js 裸套件名稱 | 使用複製的 ./opm/api/index.js 相對 URL;如自行設定 import map,也要確保 worklet 的完整模組樹可讀取。 |
| worklet/模組 404,或拿到 HTML、MIME 類型錯誤 | 重新複製完整 dist 目錄並確認站點路徑;不要讓 SPA rewrite 將資產請求改送 index.html。 |
| 尚未啟動就播放、瀏覽器自動播放被擋 | 在點擊事件內 await opm.start() 後才呼叫 playNote()。 |
| 音高、名稱、延遲、時長或未知音色出錯 | 依瀏覽器 API檢查範圍,先註冊自訂音色。 |
| 多音符遺失 | 檢查事件的 rejected/stolen 與診斷;注意 maxVoices 1–32(預設 8)、voicePriority 拒絕、聲部自有 voiceLimit 及有界事件/ID,分批排程。 |
| Node 無聲、找不到 WAV | PCM 不會自動播放/寫檔;依上方 WAV 範例編碼並儲存。 |
| Processor 失敗 | 處理 onEvent error 與拒絕的診斷請求,關閉/重啟;不要以不安全 fallback 掩蓋錯誤。 |
| npm 回報 E404 | 不假設 registry 有此套件;依安裝步驟安裝 GitHub Release tarball 或 checkout 封裝的本機 tarball。 |
| 音色銀行格式錯誤 | 必須是完整音色組成的陣列,並符合名稱、欄位、數量與大小限制;單一 JSON 物件須包入 [...]。 |
| 壓縮回應顯示亂碼 | 設定正確的 Content-Encoding 與 MIME 類型,或直接提供未壓縮的 .js 模組。 |