{"schema":1,"version":"0.1.0","p5Version":"0.8.4","categories":["midi","notes","mic","sound","drums","timeline","chop","record","options","oavp5","midi-io","controllers","keys","leap","pose","chord","osc","mapping","core","remote","backstage","fab","mcp"],"count":489,"entries":[
{"id":"controlChange","key":"controlChange","name":"controlChange","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function controlChange(cc, value, channel) {}","source":{"file":"p5/oav-p5.js","line":255,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L255"},"summary":{"en":"You define this function; oav-p5 calls it for every MIDI control change (a knob, a fader, the mod wheel, the sustain pedal), whether or not the controller is known.","zh":"你來定義這個函式。每收到一個 MIDI 控制變化（旋鈕、推桿、調變輪、延音踏板），oav-p5 就呼叫它，不管控制器認不認得。"},"description":[{"en":"Declare it the way you declare `keyPressed()`:\n\n```js\nfunction controlChange(cc, value, channel) {\n  // a control moved\n}\n```\n\nIt hears every control, including the ones that are not a knob or a fader, such as the mod wheel and the pedals, which [`knobMoved()`](#knobMoved) never reports. The latest value of every controller number is also in [`OAVp5.cc`](#OAVp5.cc), so a sketch that only wants to look can read `OAVp5.cc[1]` in `draw()`.","zh":"用宣告 `keyPressed()` 的方式宣告它：\n\n```js\nfunction controlChange(cc, value, channel) {\n  // 有一個控制動了\n}\n```\n\n每個控制它都聽得到，包括不是旋鈕也不是推桿的那些，像調變輪和踏板，[`knobMoved()`](#knobMoved) 永遠不會回報它們。每個控制器號碼的最新值也放在 [`OAVp5.cc`](#OAVp5.cc)，所以只想看一眼的草圖可以在 `draw()` 裡讀 `OAVp5.cc[1]`。"},{"en":"For one incoming message the order is: `midiMessage()`, then the specific one (`noteOn()`, `controlChange()`, `pitchBend()`), then the friendly one (`padPressed()`, `knobMoved()`, `notePressed()` and their partners).","zh":"同一則訊息進來時，呼叫的順序是：先 `midiMessage()`，再來是對應的那一個（`noteOn()`、`controlChange()`、`pitchBend()`），最後才是比較親切的那一個（`padPressed()`、`knobMoved()`、`notePressed()` 和它們的另一半）。"}],"params":[{"name":"cc","type":"Number","en":"The controller number, 0 to 127 (1 is the mod wheel, 64 the sustain pedal, 74 the first knob on a MiniLab 3).","zh":"控制器號碼，0 到 127（1 是調變輪，64 是延音踏板，MiniLab 3 的第一顆旋鈕是 74）。"},{"name":"value","type":"Number","en":"Where the control is now, 0 to 1 (MIDI's 0 to 127 divided by 127).","zh":"這個控制現在的位置，0 到 1（MIDI 的 0 到 127 除以 127）。"},{"name":"channel","type":"Number","en":"The MIDI channel, 1 to 16.","zh":"MIDI 通道，1 到 16。"}],"needs":{"en":"Defining the function is enough. oav-p5 asks the browser for Web MIDI when it loads (the default, [`?oav_hardware=auto`](#option-hardware)), and a sketch that defines it also gets a card if Web MIDI is missing or blocked. Keys from 🎹, a phone, [`OAVp5.input()`](#OAVp5.input) and other programs reach it too. `OAVp5.on('controlChange', fn)` does the same without defining a global function, and returns a function that stops it.","zh":"只要定義這個函式就夠了。oav-p5 在載入的時候就會向瀏覽器要 Web MIDI（預設，[`?oav_hardware=auto`](#option-hardware)），而草圖有定義這個函式的話，Web MIDI 不存在或被擋下時還會多跳出一張卡片。🎹、手機、[`OAVp5.input()`](#OAVp5.input) 和其他程式送來的訊息也都會呼叫它。`OAVp5.on('controlChange', fn)` 是不必定義全域函式的同樣功能，它會回傳一個停止監聽的函式。"},"notes":[{"en":"An error inside it does not stop the music: the next message is still handled, and the message shows in the console and on a card beside the 🎹, with the file and line.","zh":"裡面出錯不會讓音樂停掉：下一則訊息還是照樣處理，錯誤訊息會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案與行號。"}],"examples":[{"code":"let shown = 'move a knob, the mod wheel or a pedal';\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(24);\n}\n\nfunction controlChange(cc, value, channel) {\n  shown = 'CC ' + cc + ' = ' + nf(value, 1, 2) + '  (channel ' + channel + ')';\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(shown, width / 2, height / 2);\n}","caption":{"en":"Shows the controller number, its value and the channel of whatever you moved last. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"顯示你最後動的那個控制的號碼、值和通道。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"let sustain = false;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction controlChange(cc, value) {\n  if (cc === 64) sustain = value >= 0.5;   // the sustain pedal: 64 and up counts as down\n}\n\nfunction mousePressed() {\n  OAVp5.input([0xB0, 64, 127]);   // no pedal? press the mouse\n}\n\nfunction mouseReleased() {\n  OAVp5.input([0xB0, 64, 0]);\n}\n\nfunction draw() {\n  background(sustain ? 90 : 20);\n}","caption":{"en":"The background lights up while the sustain pedal is down. No pedal? Hold the mouse button: it sends the same message.","zh":"延音踏板踩著的時候背景變亮。沒有踏板的話，按住滑鼠：它送出同樣的訊息。"},"run":"sketch","lang":"js"}],"related":["knobMoved","faderMoved","OAVp5.cc","midiMessage","OAVp5.on","OAVp5.input"],"keywords":{"en":["control change","cc","controller","mod wheel","sustain pedal","raw midi","knob","callback","event"],"zh":["控制變化","cc","控制器","調變輪","延音踏板","原始 midi","旋鈕","事件","回呼"]},"tbd":[],"claims":[{"before":"window.controlChange = (cc, v, c) => { window.__cc = [cc, Math.round(v * 127), c]; }; midi.input([0xB2, 74, 127], 'screen')","expr":"JSON.stringify(window.__cc)","equals":"[74,127,3]"}]},
{"id":"fader","key":"fader","name":"fader","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"fader(n)","source":{"file":"p5/oav-p5.js","line":361,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L361"},"summary":{"en":"Returns the position of fader number `n`, from 0 to 1: the same as `fader1` to `fader8`, for any `n`.","zh":"回傳第 `n` 支推桿的位置，0 到 1：跟 `fader1` 到 `fader8` 一樣，但 `n` 可以是任何數字。"},"description":[{"en":"The function form of [`fader1`](#fader1), for when the fader's number is a variable.","zh":"[`fader1`](#fader1) 的函式版本，推桿的編號是變數的時候用。"}],"params":[{"name":"n","type":"Number","en":"Which fader, counting from 1.","zh":"第幾支推桿，從 1 開始算。"}],"returns":{"type":"Number","en":"The fader's position, 0..1; 0 until it has moved.","zh":"推桿的位置，0..1。還沒推過之前是 0。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  for (let i = 1; i <= 4; i++) circle(i * 80, height - 40 - fader(i) * 200, 30);\n}","caption":{"en":"Four dots that rise with four faders.","zh":"四個點，跟著四支推桿升高。"},"run":"sketch","lang":"js"}],"related":["fader1","knob","pad"],"keywords":{"en":["fader","slider","number","index"],"zh":["推桿","滑桿","編號"]},"tbd":[],"claims":[]},
{"id":"fader1","key":"fader1","name":"fader1","aliases":["fader2","fader3","fader4","fader5","fader6","fader7","fader8"],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"fader1","source":{"file":"p5/oav-p5.js","line":1,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1"},"summary":{"en":"The position of a controller fader (slider), from 0 to 1: `fader1` is the first fader, up to `fader8`.","zh":"控制器上推桿（滑桿）的位置，從 0 到 1：`fader1` 是第一支推桿，一直到 `fader8`。"},"description":[{"en":"Works exactly like [`knob1`](#knob1), for the controller's faders, numbered in the order the controller's profile lists them (as printed on the device). A controller with only four faders, like the MiniLab 3, has `fader1` to `fader4`.","zh":"用法跟 [`knob1`](#knob1) 完全一樣，只是對應控制器上的推桿，編號依控制器設定檔列出的順序（也就是機器上印的順序）。只有四支推桿的控制器（像 MiniLab 3）就只有 `fader1` 到 `fader4`。"}],"value":{"type":"Number","range":"0..1","en":"Changes when the fader moves; 0 until it has moved.","zh":"推桿移動的時候才會改變。還沒推過之前是 0。"},"params":[],"needs":{"en":"Nothing to call. Web MIDI is asked for at load, exactly as for a knob ([`knob1`](#knob1)); reading a fader also makes a missing or refused Web MIDI show as a card.","zh":"不用呼叫任何東西。Web MIDI 在載入時就會去要，跟旋鈕一樣（見 [`knob1`](#knob1)）。讀推桿還會讓「Web MIDI 不存在或被拒絕」顯示成一張卡片。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  rect(40, height - 40, fader1 * (width - 80), 24);   // fader 1 is a level bar\n}","caption":{"en":"A bar that follows fader 1. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"一條跟著推桿 1 走的橫條。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["fader","knob1","faderMoved"],"keywords":{"en":["fader","slider","controller","midi"],"zh":["推桿","滑桿","控制器","midi"]},"tbd":[],"claims":[{"before":"midi.input([0xB0, 82, 127], 'screen')","expr":"fader1","equals":1}]},
{"id":"faderMoved","key":"faderMoved","name":"faderMoved","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function faderMoved(n, value) {}","source":{"file":"p5/oav-p5.js","line":254,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L254"},"summary":{"en":"You define this function; oav-p5 calls it whenever a fader moves.","zh":"你來定義這個函式。推桿一移動，oav-p5 就呼叫它。"},"description":[{"en":"The fader twin of [`knobMoved()`](#knobMoved): `function faderMoved(n, value) { … }`.","zh":"[`knobMoved()`](#knobMoved) 的推桿版本：`function faderMoved(n, value) { … }`。"}],"params":[{"name":"n","type":"Number","en":"Which fader (1 is the first).","zh":"哪一支推桿（1 是第一支）。"},{"name":"value","type":"Number","en":"Where it is now, 0..1.","zh":"現在的位置，0..1。"}],"notes":[],"examples":[{"code":"let level = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction faderMoved(n, value) {\n  if (n === 1) level = value;   // fader 1 only\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  rect(40, height - 40, level * (width - 80), 24);\n}","caption":{"en":"A bar that follows fader 1, driven by the event instead of a read.","zh":"一條跟著推桿 1 的橫條，用事件驅動，不是每個畫面去讀。"},"run":"sketch","lang":"js"}],"related":["fader1","knobMoved","controlChange"],"keywords":{"en":["fader moved","slider","callback","event"],"zh":["推桿","滑桿","事件","回呼"]},"tbd":[],"claims":[]},
{"id":"knob","key":"knob","name":"knob","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"knob(n)","source":{"file":"p5/oav-p5.js","line":360,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L360"},"summary":{"en":"Returns the position of knob number `n`, from 0 to 1: the same as `knob1` to `knob8`, for any `n`.","zh":"回傳第 `n` 顆旋鈕的位置，0 到 1：跟 `knob1` 到 `knob8` 一樣，但 `n` 可以是任何數字。"},"description":[{"en":"Use it when the number of the knob is a variable: `for (let i = 1; i <= 16; i++) bar(i, knob(i));`. It also works in a sketch that has its own `knob1`, which oav-p5 then leaves alone.","zh":"旋鈕的編號是變數的時候用它：`for (let i = 1; i <= 16; i++) bar(i, knob(i));`。草圖自己已經有 `knob1` 的話也可以用，oav-p5 不會去動那個名稱。"}],"params":[{"name":"n","type":"Number","en":"Which knob, counting from 1 (as printed on the controller).","zh":"第幾顆旋鈕，從 1 開始算（照控制器上印的）。"}],"returns":{"type":"Number","en":"The knob's position, 0..1; 0 until it has been turned.","zh":"旋鈕的位置，0..1。還沒轉過之前是 0。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  for (let i = 1; i <= 8; i++) {\n    rect(i * 44 - 20, height - 20, 30, -10 - knob(i) * 250);   // one bar per knob\n  }\n}","caption":{"en":"Eight bars, one per knob. Knobs you have not touched stay at the bottom.","zh":"八根長條，一顆旋鈕一根。還沒碰過的旋鈕停在最低。"},"run":"sketch","lang":"js"}],"related":["knob1","fader","pad"],"keywords":{"en":["knob","dial","number","index","controller"],"zh":["旋鈕","編號","控制器"]},"tbd":[],"claims":[]},
{"id":"knob1","key":"knob1","name":"knob1","aliases":["knob2","knob3","knob4","knob5","knob6","knob7","knob8"],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"knob1","source":{"file":"p5/oav-p5.js","line":288,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L288"},"summary":{"en":"The position of a controller knob, from 0 to 1: `knob1` is the first knob, `knob2` the second, up to `knob8`.","zh":"控制器上旋鈕的位置，從 0 到 1：`knob1` 是第一顆旋鈕，`knob2` 是第二顆，一直到 `knob8`。"},"description":[{"en":"Read it in `draw()` the way you read `mouseX`: `background(knob1 * 255)`. Knobs are numbered the way they are printed on the controller, so knob 1 is knob 1 on any controller OAV has a [profile](/controllers/) for: the MiniLab 3's first knob is CC 74, the LPD8's is CC 70. The sketch never needs to know a CC number.","zh":"像讀 `mouseX` 一樣在 `draw()` 裡讀：`background(knob1 * 255)`。旋鈕的編號照控制器上印的順序，所以在 OAV 有[設定檔](/zh/controllers/)的任何控制器上，旋鈕 1 就是旋鈕 1：MiniLab 3 的第一顆是 CC 74，LPD8 的是 CC 70。草圖完全不用知道 CC 號碼。"},{"en":"On a controller OAV has no profile for, the first knob you turn becomes `knob1`, the next one `knob2`, and so on, in the order you turn them. Past eight, use [`knob(n)`](#knob).","zh":"遇到 OAV 沒有設定檔的控制器，你轉的第一顆旋鈕就變成 `knob1`，下一顆是 `knob2`，依你轉的順序往下排。超過八顆就用 [`knob(n)`](#knob)。"}],"value":{"type":"Number","range":"0..1","en":"Changes when the knob moves, not once per frame; the sketch just reads the latest value. 0 until the knob has been turned.","zh":"旋鈕轉動的時候才會改變，不是每個畫面一次。草圖只要讀最新的值。旋鈕還沒轉過之前是 0。"},"params":[],"needs":{"en":"Nothing to call. With the default `hardware=auto`, oav-p5 asks the browser for Web MIDI as soon as it loads, whatever the sketch reads (Chrome, Edge and Firefox have it; Safari and iPad do not: use 🎹 or a phone there). Reading a knob also tells oav-p5 the sketch plays from MIDI, so when Web MIDI is missing or refused a card beside the 🎹 says so; a sketch that uses none of this never sees that card. [`?oav_hardware=click`](#option-hardware) waits for the 🎹 panel instead.","zh":"不用呼叫任何東西。預設的 `hardware=auto` 下，oav-p5 一載入就會向瀏覽器要 Web MIDI，不管草圖有沒有讀東西（Chrome、Edge、Firefox 有。Safari 和 iPad 沒有，那裡改用 🎹 或手機）。讀旋鈕還會讓 oav-p5 知道這份草圖要用 MIDI 玩，所以 Web MIDI 不存在或被拒絕的時候，🎹 旁邊會有一張卡片說明。完全沒用到這些的草圖不會看到那張卡片。[`?oav_hardware=click`](#option-hardware) 則是等你按 🎹 面板裡的按鈕才去要。"},"notes":[{"en":"A sketch that already has its own `knob1` (a `let`, a function) keeps it, and a card beside the 🎹 says what to read instead (`knob(1)`).","zh":"草圖如果本來就有自己的 `knob1`（`let`、函式都算），就由草圖保留，🎹 旁邊會有一張卡片說明改讀什麼（`knob(1)`）。"},{"en":"With OP Configurator, knobs also drive the OPC sliders in the order they are declared. A knob the sketch reads itself stays the sketch's: that slider no longer follows it.","zh":"搭配 OP Configurator 時，旋鈕也會依宣告順序推動 OPC 的滑桿。草圖自己有讀的那顆旋鈕就歸草圖：對應的滑桿不再跟著動。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(knob1 * 255);   // knob 1 sets the brightness\n}","caption":{"en":"Turn knob 1 and the whole canvas goes from black to white. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"轉旋鈕 1，整個畫布從黑變到白。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  colorMode(HSB, 1);\n}\n\nfunction draw() {\n  background(0.65, 0.5, 0.15);\n  fill(knob2, 0.8, 1);                        // knob 2: colour\n  circle(width / 2, height / 2, 40 + knob1 * 300);   // knob 1: size\n}","caption":{"en":"Two knobs, two jobs: size and colour.","zh":"兩顆旋鈕，兩件事：大小與顏色。"},"run":"sketch","lang":"js"}],"related":["knob","fader1","pad1","knobMoved","noteIsDown"],"keywords":{"en":["knob","dial","rotary","controller","cc","control change","midi","potentiometer"],"zh":["旋鈕","控制器","轉鈕","midi","控制訊號"]},"tbd":[],"claims":[{"before":"midi.input([0xB0, 74, 127], 'screen')","expr":"knob1","equals":1},{"before":"midi.input([0xB0, 74, 0], 'screen')","expr":"knob1","equals":0}]},
{"id":"knobMoved","key":"knobMoved","name":"knobMoved","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function knobMoved(n, value) {}","source":{"file":"p5/oav-p5.js","line":254,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L254"},"summary":{"en":"You define this function; oav-p5 calls it whenever a knob moves.","zh":"你來定義這個函式。旋鈕一轉動，oav-p5 就呼叫它。"},"description":[{"en":"Like `mouseMoved()` for knobs:\n\n```js\nfunction knobMoved(n, value) {\n  // knob n is now at value\n}\n```\n\nIt is called for every step the knob sends, so many times per second while it turns. To read a knob once a frame, use [`knob1`](#knob1) instead.","zh":"就像旋鈕版的 `mouseMoved()`：\n\n```js\nfunction knobMoved(n, value) {\n  // 旋鈕 n 現在在 value\n}\n```\n\n旋鈕每送出一格就呼叫一次，轉動的時候每秒會呼叫很多次。想要每個畫面讀一次，就改用 [`knob1`](#knob1)。"}],"params":[{"name":"n","type":"Number","en":"Which knob (1 is the first, as printed on the controller).","zh":"哪一顆旋鈕（1 是第一顆，照控制器上印的）。"},{"name":"value","type":"Number","en":"Where it is now, 0..1.","zh":"現在的位置，0..1。"}],"notes":[],"examples":[{"code":"let last = 'turn a knob';\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction knobMoved(n, value) {\n  last = 'knob ' + n + ': ' + nf(value, 1, 2);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(last, width / 2, height / 2);\n}","caption":{"en":"Shows which knob moved and where it is. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"顯示哪一顆旋鈕動了、現在在哪。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["knob1","faderMoved","controlChange"],"keywords":{"en":["knob moved","callback","event","turn"],"zh":["旋鈕","轉動","事件","回呼"]},"tbd":[],"claims":[]},
{"id":"lastNoteFrameCount","key":"lastNoteFrameCount","name":"lastNoteFrameCount","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"lastNoteFrameCount","source":{"file":"p5/oav-p5.js","line":333,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L333"},"summary":{"en":"The `frameCount` of the frame that saw the last key go down: `frameCount - lastNoteFrameCount` is how many frames ago.","zh":"看到上一個鍵按下的那一格的 `frameCount`：`frameCount - lastNoteFrameCount` 就是幾格以前。"},"description":[{"en":"The frame-counting twin of [`lastNoteTs`](#lastNoteTs): 0 in the first frame after the key, then 1, 2, 3 …","zh":"[`lastNoteTs`](#lastNoteTs) 的「數格子」版本：按下之後的第一格是 0，接下來 1、2、3……"}],"value":{"type":"Number","en":"A frame number, on the same count as `frameCount`, set with [`note`](#note); `-Infinity` before any key. Since oav-p5 0.8.3.","zh":"畫面的編號，跟 `frameCount` 同一套數法，跟 [`note`](#note) 一起更新。還沒按過任何鍵之前是 `-Infinity`。oav-p5 0.8.3 起。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const f = frameCount - lastNoteFrameCount;    // frames since a key went down\n  fill(120, 190, 255);\n  circle(200, 150, max(0, 200 - f * 5));        // shrinks by 5 px a frame\n}","caption":{"en":"Each key you press (on 🎹 too) brings back a big circle that shrinks 5 px a frame.","zh":"每按一個鍵（🎹 也可以），大圓就回來，然後每一格縮 5 px。"},"run":"sketch","lang":"js"}],"related":["lastNoteTs","note","notePressed","lastKickFrameCount"],"keywords":{"en":["last note","frameCount","frames since","key pressed"],"zh":["上一個音","幾格以前","畫面數","按鍵"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 100], 'screen')","expr":"lastNoteFrameCount > -Infinity","equals":true}]},
{"id":"lastNoteTs","key":"lastNoteTs","name":"lastNoteTs","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"lastNoteTs","source":{"file":"p5/oav-p5.js","line":330,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L330"},"summary":{"en":"When the last key went down, on the `millis()` clock: `millis() - lastNoteTs` is how many ms ago.","zh":"上一個鍵是什麼時候按下的，用 `millis()` 的時鐘：`millis() - lastNoteTs` 就是幾毫秒前。"},"description":[{"en":"Any key counts: a MIDI keyboard, the 🎹 on screen, a phone. Releasing a key does not change it.","zh":"任何鍵都算：MIDI 鍵盤、畫面上的 🎹、手機。放開鍵不會改變它。"}],"value":{"type":"Number","en":"Milliseconds on the same clock as `millis()`, set with [`note`](#note) and [`velocity`](#velocity); `-Infinity` before any key. Since oav-p5 0.8.2.","zh":"毫秒，跟 `millis()` 同一個時鐘，跟 [`note`](#note)、[`velocity`](#velocity) 一起更新。還沒按過任何鍵之前是 `-Infinity`。oav-p5 0.8.2 起。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const ago = millis() - lastNoteTs;    // ms since a key went down\n  fill(255, 200 * exp(-ago / 400) + 30);\n  circle(200, 150, 60 + 160 * exp(-ago / 400));\n}","caption":{"en":"Each key you press (on 🎹 too) makes the circle bright and big, then it dims.","zh":"每按一個鍵（🎹 也可以），圓就變亮變大，然後慢慢暗下來。"},"run":"sketch","lang":"js"}],"related":["note","velocity","notePressed","lastKickTs"],"keywords":{"en":["last note","timestamp","time since","millis","key pressed"],"zh":["上一個音","時間","多久以前","毫秒","按鍵"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 100], 'screen')","expr":"lastNoteTs > -Infinity","equals":true}]},
{"id":"midiMessage","key":"midiMessage","name":"midiMessage","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function midiMessage(bytes, source) {}","source":{"file":"p5/oav-p5.js","line":255,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L255"},"summary":{"en":"You define this function; oav-p5 calls it for every MIDI message it receives, with the raw bytes and where they came from.","zh":"你來定義這個函式。每收到一則 MIDI 訊息，oav-p5 就呼叫它，帶著原始的位元組和它從哪裡來。"},"description":[{"en":"Declare it the way you declare `keyPressed()`:\n\n```js\nfunction midiMessage(bytes, source) {\n  // anything that came in\n}\n```\n\nIt is the widest net: it is called first for every note, control change and pitch bend, and it is the only function that hears the messages oav-p5 has no friendlier name for, such as channel pressure (`0xD0`), polyphonic aftertouch (`0xA0`) and program change (`0xC0`). The `OAVp5.last` object keeps the latest note, control change or pitch bend, but not those.","zh":"用宣告 `keyPressed()` 的方式宣告它：\n\n```js\nfunction midiMessage(bytes, source) {\n  // 任何進來的訊息\n}\n```\n\n它是網子最大的一個：每個音符、控制變化、音高輪訊息都會先呼叫它，而且 oav-p5 沒有比較親切的名字可以叫的訊息，只有它聽得到，像是通道壓力（`0xD0`）、各鍵觸後（`0xA0`）、音色切換（`0xC0`）。`OAVp5.last` 只留最新的音符、控制變化或音高輪，不留這些。"},{"en":"It does not hear system messages: the MIDI clock, start, stop and song position go to [`OAVp5.clock`](#OAVp5.clock), and system-exclusive messages are dropped. A note-on with velocity 0 arrives here as it was sent (a note-on with 0), while [`noteOff()`](#noteOff) is the one that treats it as a release.","zh":"系統訊息它聽不到：MIDI 時鐘、start、stop、song position 都交給 [`OAVp5.clock`](#OAVp5.clock)，系統專用（sysex）訊息直接丟掉。力道是 0 的 note-on 在這裡照送出的樣子到達（note-on，力道 0），把它當成放開的是 [`noteOff()`](#noteOff)。"},{"en":"For one incoming message the order is: `midiMessage()`, then the specific one (`noteOn()`, `controlChange()`, `pitchBend()`), then the friendly one (`padPressed()`, `knobMoved()`, `notePressed()` and their partners).","zh":"同一則訊息進來時，呼叫的順序是：先 `midiMessage()`，再來是對應的那一個（`noteOn()`、`controlChange()`、`pitchBend()`），最後才是比較親切的那一個（`padPressed()`、`knobMoved()`、`notePressed()` 和它們的另一半）。"}],"params":[{"name":"bytes","type":"Array<Number>","en":"The message as three numbers `[status, data1, data2]`. The status byte holds the kind in its top half and the channel in its bottom half (`0x90` is a note-on on channel 1, `0x93` on channel 4). The data bytes are 0 to 127, and a byte the message does not have is 0.","zh":"這則訊息，三個數字 `[status, data1, data2]`。status 位元組的上半是種類，下半是通道（`0x90` 是通道 1 的 note-on，`0x93` 是通道 4 的）。資料位元組是 0 到 127，訊息沒有的那個位元組是 0。"},{"name":"source","type":"String","en":"Where it came from: `'hardware'` (a real controller), `'screen'` (the 🎹), `'phone'`, `'virtual-out'` (a program playing into OAV Virtual MIDI), `'link'` (another page in the same room), or whatever label you gave [`OAVp5.input()`](#OAVp5.input).","zh":"它從哪裡來：`'hardware'`（真的控制器）、`'screen'`（🎹）、`'phone'`、`'virtual-out'`（某個程式送進 OAV Virtual MIDI）、`'link'`（同一個房間的另一頁），或你在 [`OAVp5.input()`](#OAVp5.input) 給的標籤。"}],"needs":{"en":"Defining the function is enough. oav-p5 asks the browser for Web MIDI when it loads (the default, [`?oav_hardware=auto`](#option-hardware)), and a sketch that defines it also gets a card if Web MIDI is missing or blocked. Keys from 🎹, a phone, [`OAVp5.input()`](#OAVp5.input) and other programs reach it too. `OAVp5.on('midiMessage', fn)` does the same without defining a global function, and returns a function that stops it.","zh":"只要定義這個函式就夠了。oav-p5 在載入的時候就會向瀏覽器要 Web MIDI（預設，[`?oav_hardware=auto`](#option-hardware)），而草圖有定義這個函式的話，Web MIDI 不存在或被擋下時還會多跳出一張卡片。🎹、手機、[`OAVp5.input()`](#OAVp5.input) 和其他程式送來的訊息也都會呼叫它。`OAVp5.on('midiMessage', fn)` 是不必定義全域函式的同樣功能，它會回傳一個停止監聽的函式。"},"notes":[{"en":"An error inside it does not stop the music: the next message is still handled, and the message shows in the console and on a card beside the 🎹, with the file and line.","zh":"裡面出錯不會讓音樂停掉：下一則訊息還是照樣處理，錯誤訊息會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案與行號。"}],"examples":[{"code":"let shown = 'play something';\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(20);\n}\n\nfunction midiMessage(bytes, source) {\n  shown = bytes.map((b) => '0x' + b.toString(16).toUpperCase()).join(' ') + '  from ' + source;\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(shown, width / 2, height / 2);\n}","caption":{"en":"Prints the raw bytes of the last message and where it came from. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"印出最後一則訊息的原始位元組，和它從哪裡來。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"let pressure = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction midiMessage(bytes) {\n  if ((bytes[0] & 0xF0) === 0xD0) pressure = bytes[1] / 127;   // channel pressure (aftertouch)\n}\n\nfunction mouseMoved() {\n  OAVp5.input([0xD0, constrain(round(mouseX / width * 127), 0, 127)], 'mouse');   // no aftertouch? move the mouse\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2, height / 2, 40 + pressure * 260);\n}","caption":{"en":"The circle follows aftertouch, which no friendlier function reports. No keyboard with aftertouch? Move the mouse across the canvas: it sends the same message.","zh":"圓跟著觸後（aftertouch）走，這個沒有比較親切的函式會回報。沒有支援觸後的鍵盤的話，在畫布上移動滑鼠：它送出同樣的訊息。"},"run":"sketch","lang":"js"}],"related":["noteOn","controlChange","pitchBend","OAVp5.last","OAVp5.on","OAVp5.input"],"keywords":{"en":["midi message","raw midi","bytes","status byte","aftertouch","program change","any message","callback","event"],"zh":["midi 訊息","原始 midi","位元組","觸後","音色切換","任何訊息","事件","回呼"]},"tbd":[],"claims":[{"before":"window.midiMessage = (b, s) => { window.__mm = [b, s]; }; midi.input([0xD0, 100], 'screen')","expr":"JSON.stringify(window.__mm)","equals":"[[208,100,0],\"screen\"]"}]},
{"id":"note","key":"note","name":"note","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"note","source":{"file":"p5/oav-p5.js","line":326,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L326"},"summary":{"en":"The MIDI number (0 to 127) of the last key pressed.","zh":"最後被按下的那個鍵的 MIDI 號碼（0 到 127）。"},"description":[{"en":"Middle C is 60, A4 is 69. [`noteName(note)`](#noteName) writes the number as a name (`60` → `'C4'`) and [`noteFreq(note)`](#noteFreq) as a frequency in Hz. [`velocity`](#velocity) says how hard it was pressed. For every key, not just the last, define [`notePressed()`](#notePressed).","zh":"中央 C 是 60，A4 是 69。[`noteName(note)`](#noteName) 把號碼寫成音名（`60` → `'C4'`），[`noteFreq(note)`](#noteFreq) 換成赫茲。[`velocity`](#velocity) 告訴你按得多用力。想要每個鍵都知道、不只最後一個，就定義 [`notePressed()`](#notePressed)。"}],"value":{"type":"Number","range":"0..127","en":"Set when a key goes down and kept after it is released; 0 before any key has been pressed.","zh":"鍵按下的時候設定，放開之後也保留。還沒有按過任何鍵之前是 0。"},"params":[],"needs":{"en":"Nothing to call. Web MIDI is asked for at load (see [`knob1`](#knob1)). Unlike a knob, merely reading `note` does not tell oav-p5 the sketch plays from MIDI: defining [`notePressed()`](#notePressed) or reading a knob or pad does, which is what makes a missing or refused Web MIDI show as a card. 🎹 and a phone always work.","zh":"不用呼叫任何東西。Web MIDI 在載入時就會去要（見 [`knob1`](#knob1)）。跟旋鈕不一樣，單單讀 `note` 並不會讓 oav-p5 知道草圖要用 MIDI 玩：定義 [`notePressed()`](#notePressed)，或讀旋鈕、墊子才會，也才會讓「Web MIDI 不存在或被拒絕」顯示成卡片。🎹 和手機隨時都能用。"},"notes":[{"en":"A sketch with its own `note` (a `let note`, a function) keeps it; read `midi.last.note` instead. oav-p5 says so in a card the first time.","zh":"草圖如果自己有 `note`（`let note`、函式都算），就由草圖保留。改讀 `midi.last.note`。oav-p5 第一次會用卡片告訴你。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(64);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(noteName(note), width / 2, height / 2);   // the last key, as a name\n}","caption":{"en":"Shows the name of the last key you played. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"顯示你最後彈的那個鍵的名字。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["velocity","noteIsPressed","notePressed","noteName"],"keywords":{"en":["note","key","pitch","keyboard","midi number"],"zh":["音符","琴鍵","音高","鍵盤","midi 號碼"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 127], 'screen')","expr":"note","equals":60}]},
{"id":"noteIsDown","key":"noteIsDown","name":"noteIsDown","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"noteIsDown(n)","source":{"file":"p5/oav-p5.js","line":363,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L363"},"summary":{"en":"True while the key `n` (a MIDI number or a name like `'C4'`) is held down.","zh":"鍵 `n`（MIDI 號碼，或像 `'C4'` 這樣的音名）按著的時候是 true。"},"description":[{"en":"The MIDI version of `keyIsDown()`. `noteIsDown('C4')` is the same as `noteIsDown(60)`: C4 is middle C. A name that is not a note (`'H4'`, `'C-2'`) stops the sketch with a one-line card saying how to write it.","zh":"就是 MIDI 版的 `keyIsDown()`。`noteIsDown('C4')` 跟 `noteIsDown(60)` 一樣：C4 是中央 C。不是音符的名稱（`'H4'`、`'C-2'`）會讓草圖停下來，並用一張卡片告訴你怎麼寫。"}],"params":[{"name":"n","type":"Number | String","en":"The key: a MIDI number (60) or a note name (`'C4'`, `'c#4'`, `'Db3'`).","zh":"哪個鍵：MIDI 號碼（60）或音名（`'C4'`、`'c#4'`、`'Db3'`）。"}],"returns":{"type":"Boolean","en":"True if that key is down right now.","zh":"那個鍵現在按著就是 true。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(noteIsDown('C4') ? color(255, 211, 107) : 80);\n  circle(width / 2, height / 2, 140);   // lights up while middle C is held\n}","caption":{"en":"A circle that lights up only while C4 (middle C) is held. On the on-screen keyboard, middle C is the C labelled C4.","zh":"只有按著 C4（中央 C）的時候才亮的圓。螢幕鍵盤上，標著 C4 的那個 C 就是中央 C。"},"run":"sketch","lang":"js"}],"related":["noteIsPressed","noteNumber","notePressed"],"keywords":{"en":["key down","held","keyIsDown","note name","C4"],"zh":["按著","琴鍵","音名","keyIsDown"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 127], 'screen')","expr":"noteIsDown('C4')","equals":true},{"before":"midi.input([0x80, 60, 0], 'screen')","expr":"noteIsDown(60)","equals":false}]},
{"id":"noteIsPressed","key":"noteIsPressed","name":"noteIsPressed","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"noteIsPressed","source":{"file":"p5/oav-p5.js","line":326,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L326"},"summary":{"en":"True while at least one key is held down.","zh":"只要有任何一個鍵按著，就是 true。"},"description":[{"en":"To ask about one particular key, use [`noteIsDown()`](#noteIsDown). `midi.held` lists every key held right now, lowest first.","zh":"想問某一個特定的鍵，用 [`noteIsDown()`](#noteIsDown)。`midi.held` 列出現在按著的所有鍵，由低到高。"}],"value":{"type":"Boolean","en":"Like `keyIsPressed`, for MIDI keys. False when nothing is held.","zh":"像 `keyIsPressed`，但是對 MIDI 琴鍵。沒有任何鍵按著就是 false。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(noteIsPressed ? 90 : 20);   // lighter while any key is down\n}","caption":{"en":"The background lightens while any key is held.","zh":"只要有鍵按著，背景就變亮。"},"run":"sketch","lang":"js"}],"related":["noteIsDown","note","notePressed"],"keywords":{"en":["key pressed","held","any key","keyIsPressed"],"zh":["按著","任一個鍵","琴鍵"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 100], 'screen')","expr":"noteIsPressed","equals":true}]},
{"id":"noteOff","key":"noteOff","name":"noteOff","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function noteOff(note, channel) {}","source":{"file":"p5/oav-p5.js","line":255,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L255"},"summary":{"en":"You define this function; oav-p5 calls it for every MIDI note-off, with the key and the channel.","zh":"你來定義這個函式。每收到一個 MIDI 的 note-off，oav-p5 就呼叫它，帶著是哪個鍵、哪個通道。"},"description":[{"en":"The partner of [`noteOn()`](#noteOn):\n\n```js\nfunction noteOff(note, channel) {\n  // a key or pad came up\n}\n```\n\nIt is called for a note-off message and also for a note-on with velocity 0, which many keyboards send instead. There is no velocity: oav-p5 does not pass the release velocity.","zh":"[`noteOn()`](#noteOn) 的另一半：\n\n```js\nfunction noteOff(note, channel) {\n  // 有一個鍵或打擊墊放開了\n}\n```\n\nnote-off 訊息會呼叫它，力道是 0 的 note-on 也會（很多鍵盤用那個來代替 note-off）。沒有力道這個參數：oav-p5 不傳放開時的力道。"},{"en":"For one incoming message the order is: `midiMessage()`, then the specific one (`noteOn()`, `controlChange()`, `pitchBend()`), then the friendly one (`padPressed()`, `knobMoved()`, `notePressed()` and their partners).","zh":"同一則訊息進來時，呼叫的順序是：先 `midiMessage()`，再來是對應的那一個（`noteOn()`、`controlChange()`、`pitchBend()`），最後才是比較親切的那一個（`padPressed()`、`knobMoved()`、`notePressed()` 和它們的另一半）。"}],"params":[{"name":"note","type":"Number","en":"The MIDI number of the key that came up, 0 to 127.","zh":"放開的那個鍵的 MIDI 號碼，0 到 127。"},{"name":"channel","type":"Number","en":"The MIDI channel, 1 to 16.","zh":"MIDI 通道，1 到 16。"}],"needs":{"en":"Defining the function is enough. oav-p5 asks the browser for Web MIDI when it loads (the default, [`?oav_hardware=auto`](#option-hardware)), and a sketch that defines it also gets a card if Web MIDI is missing or blocked. Keys from 🎹, a phone, [`OAVp5.input()`](#OAVp5.input) and other programs reach it too. `OAVp5.on('noteOff', fn)` does the same without defining a global function, and returns a function that stops it.","zh":"只要定義這個函式就夠了。oav-p5 在載入的時候就會向瀏覽器要 Web MIDI（預設，[`?oav_hardware=auto`](#option-hardware)），而草圖有定義這個函式的話，Web MIDI 不存在或被擋下時還會多跳出一張卡片。🎹、手機、[`OAVp5.input()`](#OAVp5.input) 和其他程式送來的訊息也都會呼叫它。`OAVp5.on('noteOff', fn)` 是不必定義全域函式的同樣功能，它會回傳一個停止監聽的函式。"},"notes":[{"en":"An error inside it does not stop the music: the next message is still handled, and the message shows in the console and on a card beside the 🎹, with the file and line.","zh":"裡面出錯不會讓音樂停掉：下一則訊息還是照樣處理，錯誤訊息會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案與行號。"}],"examples":[{"code":"const down = {};\nlet held = 'play a key and let it go';\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(24);\n}\n\nfunction noteOn(note) { down[note] = millis(); }\n\nfunction noteOff(note) {\n  if (down[note] !== undefined) {\n    held = noteName(note) + ' was held ' + round(millis() - down[note]) + ' ms';\n    delete down[note];\n  }\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(held, width / 2, height / 2);\n}","caption":{"en":"Pairs each noteOn() with its noteOff() and says how long the key was held. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"把每個 noteOn() 和它的 noteOff() 配成一對，說出這個鍵按了多久。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["noteOn","noteReleased","padReleased","midiMessage","OAVp5.on"],"keywords":{"en":["note off","key up","key released","release","raw midi","midi channel","callback","event"],"zh":["音符結束","放開","琴鍵放開","原始 midi","midi 通道","事件","回呼"]},"tbd":[],"claims":[{"before":"window.noteOff = (n, c) => { window.__noteOff = [n, c]; }; midi.input([0x84, 60, 0], 'screen')","expr":"JSON.stringify(window.__noteOff)","equals":"[60,5]"}]},
{"id":"noteOn","key":"noteOn","name":"noteOn","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function noteOn(note, velocity, channel) {}","source":{"file":"p5/oav-p5.js","line":255,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L255"},"summary":{"en":"You define this function; oav-p5 calls it for every MIDI note-on, with the key, how hard and the channel.","zh":"你來定義這個函式。每收到一個 MIDI 的 note-on，oav-p5 就呼叫它，帶著是哪個鍵、多用力、哪個通道。"},"description":[{"en":"Declare it the way you declare `keyPressed()`:\n\n```js\nfunction noteOn(note, velocity, channel) {\n  // a key or pad went down\n}\n```\n\nIt is called once per note-on message, as it came in. It is the level below [`notePressed()`](#notePressed): the same moment, but with the channel, and for pads too, even when the sketch defines [`padPressed()`](#padPressed). A note-on with velocity 0, which some keyboards send instead of a note-off, is not a note-on here: it calls [`noteOff()`](#noteOff).","zh":"用宣告 `keyPressed()` 的方式宣告它：\n\n```js\nfunction noteOn(note, velocity, channel) {\n  // 有一個鍵或打擊墊按下去了\n}\n```\n\n每則 note-on 訊息進來就呼叫一次，照它進來的樣子。它比 [`notePressed()`](#notePressed) 低一層：同一個時刻，但多了通道，而且打擊墊也會呼叫，就算草圖有定義 [`padPressed()`](#padPressed) 也一樣。力道是 0 的 note-on（有些鍵盤用它來代替 note-off）在這裡不算 note-on，會去呼叫 [`noteOff()`](#noteOff)。"},{"en":"For one incoming message the order is: `midiMessage()`, then the specific one (`noteOn()`, `controlChange()`, `pitchBend()`), then the friendly one (`padPressed()`, `knobMoved()`, `notePressed()` and their partners).","zh":"同一則訊息進來時，呼叫的順序是：先 `midiMessage()`，再來是對應的那一個（`noteOn()`、`controlChange()`、`pitchBend()`），最後才是比較親切的那一個（`padPressed()`、`knobMoved()`、`notePressed()` 和它們的另一半）。"}],"params":[{"name":"note","type":"Number","en":"The MIDI number of the key, 0 to 127 (60 is middle C). [`noteName(note)`](#noteName) turns it into a name.","zh":"這個鍵的 MIDI 號碼，0 到 127（60 是中央 C）。[`noteName(note)`](#noteName) 可以把它轉成音名。"},{"name":"velocity","type":"Number","en":"How hard, above 0 and up to 1 (MIDI's 1 to 127 divided by 127).","zh":"多用力，大於 0、最大 1（MIDI 的 1 到 127 除以 127）。"},{"name":"channel","type":"Number","en":"The MIDI channel, 1 to 16. Pads on the MiniLab 3 and the LPD8 send on channel 10.","zh":"MIDI 通道，1 到 16。MiniLab 3 和 LPD8 的打擊墊都是從通道 10 送出。"}],"needs":{"en":"Defining the function is enough. oav-p5 asks the browser for Web MIDI when it loads (the default, [`?oav_hardware=auto`](#option-hardware)), and a sketch that defines it also gets a card if Web MIDI is missing or blocked. Keys from 🎹, a phone, [`OAVp5.input()`](#OAVp5.input) and other programs reach it too. `OAVp5.on('noteOn', fn)` does the same without defining a global function, and returns a function that stops it.","zh":"只要定義這個函式就夠了。oav-p5 在載入的時候就會向瀏覽器要 Web MIDI（預設，[`?oav_hardware=auto`](#option-hardware)），而草圖有定義這個函式的話，Web MIDI 不存在或被擋下時還會多跳出一張卡片。🎹、手機、[`OAVp5.input()`](#OAVp5.input) 和其他程式送來的訊息也都會呼叫它。`OAVp5.on('noteOn', fn)` 是不必定義全域函式的同樣功能，它會回傳一個停止監聽的函式。"},"notes":[{"en":"An error inside it does not stop the music: the next message is still handled, and the message shows in the console and on a card beside the 🎹, with the file and line.","zh":"裡面出錯不會讓音樂停掉：下一則訊息還是照樣處理，錯誤訊息會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案與行號。"}],"examples":[{"code":"let shown = 'play a key';\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(24);\n}\n\nfunction noteOn(note, velocity, channel) {\n  shown = noteName(note) + '  velocity ' + nf(velocity, 1, 2) + '  channel ' + channel;\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(shown, width / 2, height / 2);\n}","caption":{"en":"Shows the key you played, how hard and on which channel. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"顯示你彈的那個鍵、多用力、哪個通道。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"let hits = [];\n\nfunction setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction noteOn(note, velocity, channel) {\n  hits.push({ note, velocity, pad: channel === 10, born: millis() });   // channel 10: the pads\n}\n\nfunction draw() {\n  background(20);\n  hits = hits.filter((h) => millis() - h.born < 1500);\n  for (const h of hits) {\n    const fade = 1 - (millis() - h.born) / 1500;\n    const x = map(h.note, 24, 96, 20, width - 20);\n    fill(h.pad ? color(255, 90, 31, fade * 255) : color(120, 190, 255, fade * 255));\n    if (h.pad) square(x - 25, 125, 50 + h.velocity * 40);\n    else circle(x, 150, 20 + h.velocity * 80);\n  }\n}","caption":{"en":"Keys leave blue circles and pads leave orange squares, placed by pitch, bigger for harder hits, fading after a moment. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"鍵留下藍色圓，打擊墊留下橘色方塊，依音高排開，打得越用力越大，過一下就淡掉。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["noteOff","notePressed","padPressed","midiMessage","OAVp5.on","OAVp5.input"],"keywords":{"en":["note on","midi note","key down","key pressed","raw midi","midi channel","callback","event","velocity"],"zh":["音符開始","琴鍵按下","原始 midi","midi 通道","事件","回呼","力道"]},"tbd":[],"claims":[{"before":"window.noteOn = (n, v, c) => { window.__noteOn = [n, Math.round(v * 127), c]; }; midi.input([0x93, 60, 100], 'screen')","expr":"JSON.stringify(window.__noteOn)","equals":"[60,100,4]"}]},
{"id":"notePressed","key":"notePressed","name":"notePressed","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function notePressed(note, velocity) {}","source":{"file":"p5/oav-p5.js","line":254,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L254"},"summary":{"en":"You define this function; oav-p5 calls it each time a key (or a pad) goes down.","zh":"你來定義這個函式。每當有一個鍵（或打擊墊）按下，oav-p5 就呼叫它。"},"description":[{"en":"Declare it the way you declare `keyPressed()`:\n\n```js\nfunction notePressed(note, velocity) {\n  // a key went down\n}\n```\n\nIt is called once per key, with the key and how hard. A pad also calls it, unless the sketch defines [`padPressed()`](#padPressed).","zh":"用宣告 `keyPressed()` 的方式宣告它：\n\n```js\nfunction notePressed(note, velocity) {\n  // 有一個鍵按下去了\n}\n```\n\n每個鍵呼叫一次，帶著是哪個鍵、多用力。打擊墊也會呼叫它，除非草圖有定義 [`padPressed()`](#padPressed)。"}],"params":[{"name":"note","type":"Number","en":"The MIDI number of the key (60 is middle C). [`noteName(note)`](#noteName) turns it into a name.","zh":"這個鍵的 MIDI 號碼（60 是中央 C）。[`noteName(note)`](#noteName) 可以把它轉成音名。"},{"name":"velocity","type":"Number","en":"How hard, 0..1.","zh":"多用力，0..1。"}],"needs":{"en":"Defining it is enough: oav-p5 calls it from every source, and if Web MIDI is missing or refused it says so in a card beside the 🎹 (Web MIDI itself is asked for at load, see [`knob1`](#knob1)). Keys from 🎹 and from a phone call it too.","zh":"只要定義了它就夠了：不管是哪一種來源，oav-p5 都會呼叫它。Web MIDI 不存在或被拒絕的時候，🎹 旁邊會有一張卡片說明（Web MIDI 本身在載入時就會去要，見 [`knob1`](#knob1)）。🎹 和手機上彈的鍵也會呼叫它。"},"notes":[{"en":"An error inside it does not stop the music: the next key still plays, and the message shows in the console and on a card beside the 🎹, with the file and line.","zh":"裡面出錯不會讓音樂停掉：下一個鍵還是照樣處理，錯誤訊息會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案與行號。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  background(20);\n  noStroke();\n}\n\nfunction notePressed(note, velocity) {\n  fill(random(255), random(255), random(255), 200);\n  circle(map(note, 36, 96, 20, width - 20), height / 2, 10 + velocity * 120);\n}","caption":{"en":"Every key leaves a circle: low keys on the left, high on the right, harder keys bigger.","zh":"每個鍵留下一個圓：低音在左、高音在右，按得越用力圓越大。"},"run":"sketch","lang":"js"}],"related":["noteReleased","padPressed","noteOn","note","noteName"],"keywords":{"en":["key pressed","note on","callback","event","keyboard"],"zh":["按下","琴鍵","事件","回呼","鍵盤"]},"tbd":[],"claims":[]},
{"id":"noteReleased","key":"noteReleased","name":"noteReleased","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function noteReleased(note) {}","source":{"file":"p5/oav-p5.js","line":254,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L254"},"summary":{"en":"You define this function; oav-p5 calls it each time a key (or a pad) is let go.","zh":"你來定義這個函式。每當有一個鍵（或打擊墊）放開，oav-p5 就呼叫它。"},"description":[{"en":"The partner of [`notePressed()`](#notePressed), like `keyReleased()` to `keyPressed()`:\n\n```js\nfunction noteReleased(note) {\n  // a key came up\n}\n```","zh":"[`notePressed()`](#notePressed) 的另一半，就像 `keyReleased()` 之於 `keyPressed()`：\n\n```js\nfunction noteReleased(note) {\n  // 有一個鍵放開了\n}\n```"}],"params":[{"name":"note","type":"Number","en":"The MIDI number of the key that was released.","zh":"被放開的那個鍵的 MIDI 號碼。"}],"notes":[],"examples":[{"code":"let held = {};\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction notePressed(note) { held[note] = millis(); }\nfunction noteReleased(note) { delete held[note]; }\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(Object.keys(held).map((n) => noteName(+n)).join('  '), 20, 40);   // the keys down now\n}","caption":{"en":"Lists the names of the keys that are down right now.","zh":"列出現在按著的那些鍵的名字。"},"run":"sketch","lang":"js"}],"related":["notePressed","padReleased","noteOff","noteIsDown"],"keywords":{"en":["key released","note off","callback","event"],"zh":["放開","琴鍵","事件","回呼"]},"tbd":[],"claims":[]},
{"id":"pad","key":"pad","name":"pad","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"pad(n)","source":{"file":"p5/oav-p5.js","line":362,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L362"},"summary":{"en":"Returns how hard pad number `n` is pressed, 0 to 1: the same as `pad1` to `pad16`, for any `n`.","zh":"回傳第 `n` 個打擊墊被按得多用力，0 到 1：跟 `pad1` 到 `pad16` 一樣，但 `n` 可以是任何數字。"},"description":[{"en":"The function form of [`pad1`](#pad1).","zh":"[`pad1`](#pad1) 的函式版本。"}],"params":[{"name":"n","type":"Number","en":"Which pad, counting from 1.","zh":"第幾個墊子，從 1 開始算。"}],"returns":{"type":"Number","en":"The velocity while the pad is held (0..1), 0 when it is released.","zh":"墊子按著的時候是力道（0..1），放開是 0。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  for (let i = 1; i <= 8; i++) {\n    fill(255, 90, 31, 60 + pad(i) * 195);\n    square(20 + ((i - 1) % 4) * 92, 40 + floor((i - 1) / 4) * 110, 80);\n  }\n}","caption":{"en":"A grid of eight squares that light up under the pads you hit.","zh":"八個方塊排成格子，你打哪個墊子，哪個方塊就亮起來。"},"run":"sketch","lang":"js"}],"related":["pad1","knob","padPressed"],"keywords":{"en":["pad","drum pad","index","velocity"],"zh":["打擊墊","墊子","編號","力道"]},"tbd":[],"claims":[]},
{"id":"pad1","key":"pad1","name":"pad1","aliases":["pad2","pad3","pad4","pad5","pad6","pad7","pad8","pad9","pad10","pad11","pad12","pad13","pad14","pad15","pad16"],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"pad1","source":{"file":"p5/oav-p5.js","line":1,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1"},"summary":{"en":"How hard a drum pad is pressed, from 0 to 1: `pad1` is the first pad, up to `pad16`; 0 when it is let go.","zh":"打擊墊被按得多用力，從 0 到 1：`pad1` 是第一個墊子，一直到 `pad16`。放開就是 0。"},"description":[{"en":"Read it like a held mouse button that also knows how hard: `if (pad1 > 0) fill(255, 90, 31)`. Pads are numbered in the order the controller's profile lists them. To react once per hit rather than ask every frame, define [`padPressed()`](#padPressed).","zh":"像一個知道力道的滑鼠按鍵那樣讀：`if (pad1 > 0) fill(255, 90, 31)`。墊子編號依控制器設定檔列出的順序。想「每打一下反應一次」而不是每個畫面都問，就定義 [`padPressed()`](#padPressed)。"}],"value":{"type":"Number","range":"0..1","en":"The velocity of the hit while the pad is held (1 is as hard as the controller sends), and 0 as soon as it is released.","zh":"墊子按著的時候是這一下的力道（1 是控制器能送出的最大力道），一放開就變成 0。"},"params":[],"needs":{"en":"Nothing to call. Web MIDI is asked for at load, exactly as for a knob ([`knob1`](#knob1)); reading a pad also makes a missing or refused Web MIDI show as a card.","zh":"不用呼叫任何東西。Web MIDI 在載入時就會去要，跟旋鈕一樣（見 [`knob1`](#knob1)）。讀墊子還會讓「Web MIDI 不存在或被拒絕」顯示成一張卡片。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255, 90, 31);\n  circle(width / 2, height / 2, 40 + pad1 * 260);   // swells while pad 1 is held\n}","caption":{"en":"The circle swells while pad 1 is down, more for a harder hit. On the on-screen controller, click the first pad.","zh":"按著墊子 1 的時候圓會脹大，打得越用力越大。用螢幕上的控制器就點第一個墊子。"},"run":"sketch","lang":"js"}],"related":["pad","padPressed","padReleased","knob1"],"keywords":{"en":["pad","drum pad","button","velocity","trigger","controller"],"zh":["打擊墊","墊子","按鍵","力道","觸發","控制器"]},"tbd":[],"claims":[{"before":"midi.input([0x99, 36, 127], 'screen')","expr":"pad1","equals":1},{"before":"midi.input([0x89, 36, 0], 'screen')","expr":"pad1","equals":0}]},
{"id":"padPressed","key":"padPressed","name":"padPressed","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function padPressed(n, velocity) {}","source":{"file":"p5/oav-p5.js","line":254,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L254"},"summary":{"en":"You define this function; oav-p5 calls it each time a drum pad is hit.","zh":"你來定義這個函式。每打一下打擊墊，oav-p5 就呼叫它一次。"},"description":[{"en":"```js\nfunction padPressed(n, velocity) {\n  // pad n was hit\n}\n```\n\nWhen the sketch defines `padPressed()`, a pad calls it and **not** [`notePressed()`](#notePressed); without it, a pad calls `notePressed()` like any key.","zh":"```js\nfunction padPressed(n, velocity) {\n  // 墊子 n 被打了一下\n}\n```\n\n草圖有定義 `padPressed()` 的話，打擊墊會呼叫它，而**不會**呼叫 [`notePressed()`](#notePressed)。沒有定義的話，打擊墊會像任何一個鍵一樣呼叫 `notePressed()`。"}],"params":[{"name":"n","type":"Number","en":"Which pad (1 is the first).","zh":"哪個墊子（1 是第一個）。"},{"name":"velocity","type":"Number","en":"How hard, 0..1.","zh":"多用力，0..1。"}],"notes":[],"examples":[{"code":"let hits = [];\n\nfunction setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction padPressed(n, velocity) {\n  hits.push({ x: 40 + ((n - 1) % 4) * 105, y: 70 + floor((n - 1) / 4) * 130, r: 20 + velocity * 60 });\n}\n\nfunction draw() {\n  background(20, 40);\n  fill(255, 90, 31);\n  for (const h of hits) { circle(h.x, h.y, h.r); h.r *= 0.95; }\n  hits = hits.filter((h) => h.r > 2);\n}","caption":{"en":"Each hit pops a circle where its pad would sit on a 4×2 grid, and it fades away.","zh":"每打一下，就在那個墊子在 4×2 格子裡的位置冒出一個圓，然後慢慢消失。"},"run":"sketch","lang":"js"}],"related":["padReleased","pad1","notePressed"],"keywords":{"en":["pad hit","drum pad","callback","event","trigger"],"zh":["打擊墊","事件","回呼","觸發"]},"tbd":[],"claims":[]},
{"id":"padReleased","key":"padReleased","name":"padReleased","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function padReleased(n) {}","source":{"file":"p5/oav-p5.js","line":254,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L254"},"summary":{"en":"You define this function; oav-p5 calls it each time a drum pad is let go.","zh":"你來定義這個函式。每當打擊墊放開，oav-p5 就呼叫它。"},"description":[{"en":"The partner of [`padPressed()`](#padPressed): `function padReleased(n) { … }`. Without it, a pad calls [`noteReleased()`](#noteReleased).","zh":"[`padPressed()`](#padPressed) 的另一半：`function padReleased(n) { … }`。沒有定義的話，打擊墊會呼叫 [`noteReleased()`](#noteReleased)。"}],"params":[{"name":"n","type":"Number","en":"Which pad (1 is the first).","zh":"哪個墊子（1 是第一個）。"}],"notes":[],"examples":[{"code":"let down = {};\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction padPressed(n) { down[n] = true; }\nfunction padReleased(n) { down[n] = false; }\n\nfunction draw() {\n  background(20);\n  fill(255, 90, 31);\n  for (let i = 1; i <= 8; i++) if (down[i]) square(10 + (i - 1) * 48, 130, 40);   // lit while held\n}","caption":{"en":"A square per pad, lit from press to release.","zh":"一個墊子一個方塊，從按下到放開都亮著。"},"run":"sketch","lang":"js"}],"related":["padPressed","pad1","noteReleased"],"keywords":{"en":["pad released","callback","event"],"zh":["打擊墊","放開","事件","回呼"]},"tbd":[],"claims":[]},
{"id":"pitchBend","key":"pitchBend","name":"pitchBend","aliases":[],"kind":"event","package":"oav-p5","category":"midi","documented":true,"signature":"function pitchBend(value, channel) {}","source":{"file":"p5/oav-p5.js","line":255,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L255"},"summary":{"en":"You define this function; oav-p5 calls it whenever the pitch wheel moves, with its position from −1 to 1.","zh":"你來定義這個函式。音高輪一動，oav-p5 就呼叫它，帶著它的位置，從 −1 到 1。"},"description":[{"en":"Declare it the way you declare `keyPressed()`:\n\n```js\nfunction pitchBend(value, channel) {\n  // the wheel moved\n}\n```\n\nA pitch wheel usually springs back to the middle when you let go, so you will hear a stream of values ending at 0. MIDI sends the position as 14 bits (0 to 16383, with 8192 in the middle). oav-p5 hands it to you as −1 to 1. The latest position is also in [`OAVp5.bend`](#OAVp5.bend).","zh":"用宣告 `keyPressed()` 的方式宣告它：\n\n```js\nfunction pitchBend(value, channel) {\n  // 輪子動了\n}\n```\n\n音高輪放開之後通常會自己彈回中間，所以你會收到一串值，最後停在 0。MIDI 用 14 位元送出位置（0 到 16383，中間是 8192），oav-p5 轉成 −1 到 1 給你。最新的位置也放在 [`OAVp5.bend`](#OAVp5.bend)。"},{"en":"For one incoming message the order is: `midiMessage()`, then the specific one (`noteOn()`, `controlChange()`, `pitchBend()`), then the friendly one (`padPressed()`, `knobMoved()`, `notePressed()` and their partners).","zh":"同一則訊息進來時，呼叫的順序是：先 `midiMessage()`，再來是對應的那一個（`noteOn()`、`controlChange()`、`pitchBend()`），最後才是比較親切的那一個（`padPressed()`、`knobMoved()`、`notePressed()` 和它們的另一半）。"}],"params":[{"name":"value","type":"Number","en":"The wheel's position: −1 all the way down, 0 in the middle, 1 all the way up.","zh":"輪子的位置：−1 是推到底往下，0 是中間，1 是推到底往上。"},{"name":"channel","type":"Number","en":"The MIDI channel, 1 to 16.","zh":"MIDI 通道，1 到 16。"}],"needs":{"en":"Defining the function is enough. oav-p5 asks the browser for Web MIDI when it loads (the default, [`?oav_hardware=auto`](#option-hardware)), and a sketch that defines it also gets a card if Web MIDI is missing or blocked. Keys from 🎹, a phone, [`OAVp5.input()`](#OAVp5.input) and other programs reach it too. `OAVp5.on('pitchBend', fn)` does the same without defining a global function, and returns a function that stops it.","zh":"只要定義這個函式就夠了。oav-p5 在載入的時候就會向瀏覽器要 Web MIDI（預設，[`?oav_hardware=auto`](#option-hardware)），而草圖有定義這個函式的話，Web MIDI 不存在或被擋下時還會多跳出一張卡片。🎹、手機、[`OAVp5.input()`](#OAVp5.input) 和其他程式送來的訊息也都會呼叫它。`OAVp5.on('pitchBend', fn)` 是不必定義全域函式的同樣功能，它會回傳一個停止監聽的函式。"},"notes":[{"en":"Pitch bend is not mapped to anything else: there is no `bend` variable, only this function and `OAVp5.bend`.","zh":"音高輪沒有對應到別的東西：沒有叫 `bend` 的變數，只有這個函式和 `OAVp5.bend`。"},{"en":"An error inside it does not stop the music: the next message is still handled, and the message shows in the console and on a card beside the 🎹, with the file and line.","zh":"裡面出錯不會讓音樂停掉：下一則訊息還是照樣處理，錯誤訊息會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案與行號。"}],"examples":[{"code":"let bend = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction pitchBend(value) {\n  bend = value;\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2 + bend * 150, height / 2, 50);   // wheel up: right, wheel down: left\n}","caption":{"en":"The circle slides left and right with the pitch wheel and returns to the middle when it springs back. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"圓隨音高輪往左右滑，輪子彈回中間時它也回到中間。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"let bend = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction pitchBend(value) { bend = value; }\n\nfunction mouseDragged() {\n  OAVp5.input([0xE0, 0, constrain(round(mouseX / width * 127), 0, 127)]);   // no wheel? drag the mouse\n}\n\nfunction draw() {\n  background(20);\n  const x = spring(width / 2 + bend * 150, 120, 8);   // a spring smooths the steps\n  circle(x, height / 2, 50);\n}","caption":{"en":"The same wheel through spring(). No wheel? Drag the mouse across the canvas: it sends the same message.","zh":"同一個輪子，再用 spring() 處理。沒有輪子的話，在畫布上拖動滑鼠：它送出同樣的訊息。"},"run":"sketch","lang":"js"}],"related":["OAVp5.bend","controlChange","midiMessage","OAVp5.on","OAVp5.input"],"keywords":{"en":["pitch bend","pitch wheel","bender","wheel","raw midi","callback","event"],"zh":["音高輪","彎音","滑音輪","輪子","原始 midi","事件","回呼"]},"tbd":[],"claims":[{"before":"window.pitchBend = (v, c) => { window.__bend = [v, c]; }; midi.input([0xE1, 127, 127], 'screen')","expr":"JSON.stringify(window.__bend)","equals":"[1,2]"}]},
{"id":"velocity","key":"velocity","name":"velocity","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"velocity","source":{"file":"p5/oav-p5.js","line":326,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L326"},"summary":{"en":"How hard the last key was pressed, from 0 to 1.","zh":"最後那個鍵被按得多用力，從 0 到 1。"},"description":[{"en":"MIDI sends velocity as 0 to 127; oav-p5 hands you it divided by 127, so 1 is the hardest hit the controller can send.","zh":"MIDI 的力道是 0 到 127。oav-p5 給你的是除以 127 之後的值，所以 1 就是控制器能送出的最大力道。"}],"value":{"type":"Number","range":"0..1","en":"Set together with [`note`](#note) when a key goes down; kept after release; 0 before any key.","zh":"跟 [`note`](#note) 一起在鍵按下的時候設定。放開之後保留。還沒按過任何鍵之前是 0。"},"params":[],"notes":[{"en":"A sketch with its own `velocity` keeps it; read `midi.last.value`.","zh":"草圖自己有 `velocity` 的話就由草圖保留。改讀 `midi.last.value`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255, 90, 31);\n  circle(width / 2, height / 2, 20 + velocity * 300);   // a harder key makes a bigger circle\n}","caption":{"en":"The circle stays at the size of the last key's velocity.","zh":"圓的大小停在最後那個鍵的力道上。"},"run":"sketch","lang":"js"}],"related":["note","notePressed","noteIsPressed"],"keywords":{"en":["velocity","force","how hard","dynamics"],"zh":["力道","力度","多用力","強弱"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 127], 'screen')","expr":"velocity","equals":1}]},
{"id":"OAVp5.bend","key":"OAVp5.bend","name":"OAVp5.bend","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.bend","source":{"file":"p5/oav-p5.js","line":549,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L549"},"summary":{"en":"Where the pitch wheel is, from −1 (all the way down) through 0 (middle) to 1 (all the way up).","zh":"音高輪現在的位置，從 −1（推到底往下）、經過 0（中間）到 1（推到底往上）。"},"description":[{"en":"Read it in `draw()` like a knob that goes both ways: `x = width / 2 + OAVp5.bend * 150`. MIDI sends the position as 14 bits (0 to 16383, with 8192 in the middle). oav-p5 maps the two halves to −1..0 and 0..1, so full travel reaches exactly −1 and 1. To be told on every movement, define [`pitchBend()`](#pitchBend).","zh":"像一顆可以往兩邊轉的旋鈕那樣在 `draw()` 裡讀：`x = width / 2 + OAVp5.bend * 150`。MIDI 用 14 位元送出位置（0 到 16383，中間是 8192）。oav-p5 把兩半分別對應成 −1 到 0 和 0 到 1，所以推到底剛好是 −1 和 1。想每次移動都被通知，就定義 [`pitchBend()`](#pitchBend)。"}],"value":{"type":"Number","range":"-1..1","en":"Changes when the wheel moves. 0 before it has moved, and again when a spring-loaded wheel returns to the middle.","zh":"輪子動的時候才會變。還沒動過之前是 0，有彈簧的輪子彈回中間時也是 0。"},"params":[],"notes":[{"en":"`midi.bend` is the same number. There is no plain `bend` variable.","zh":"`midi.bend` 是同一個數字。沒有叫 `bend` 的變數。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mouseDragged() {\n  OAVp5.input([0xE0, 0, constrain(round(mouseX / width * 127), 0, 127)]);   // no wheel? drag the mouse\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2 + OAVp5.bend * 150, height / 2, 50);   // wheel up: right, wheel down: left\n  text('bend ' + nf(OAVp5.bend, 1, 2), 20, 30);\n}","caption":{"en":"The circle follows the pitch wheel. No wheel? Drag the mouse across the canvas: it sends the same message.","zh":"圓跟著音高輪走。沒有輪子的話，在畫布上拖動滑鼠：它送出同樣的訊息。"},"run":"sketch","lang":"js"}],"related":["pitchBend","OAVp5.cc","OAVp5.note"],"keywords":{"en":["pitch bend value","pitch wheel","bend","wheel position","midi.bend"],"zh":["音高輪","彎音","輪子位置","midi.bend"]},"tbd":[],"claims":[{"before":"midi.input([0xE0, 127, 127], 'screen')","expr":"OAVp5.bend","equals":1},{"before":"midi.input([0xE0, 0, 0], 'screen')","expr":"OAVp5.bend","equals":-1}]},
{"id":"OAVp5.cc","key":"OAVp5.cc","name":"OAVp5.cc","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.cc","source":{"file":"p5/oav-p5.js","line":258,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L258"},"summary":{"en":"An array of 128 numbers, one per MIDI controller number: where each control last was, 0 to 1. `OAVp5.cc[1]` is the mod wheel, `OAVp5.cc[64]` the sustain pedal.","zh":"128 個數字的陣列，每個 MIDI 控制器號碼一個：每個控制最後停在哪裡，0 到 1。`OAVp5.cc[1]` 是調變輪，`OAVp5.cc[64]` 是延音踏板。"},"description":[{"en":"The array version of [`controlChange()`](#controlChange): read any controller in `draw()` without defining a function. It holds every CC, including the ones that are not a knob or a fader, such as the mod wheel (1), the volume (7) and the sustain pedal (64, which counts as down at 0.5 and above). The MIDI channel is ignored: the latest value of that controller number wins, whichever channel it came on.","zh":"[`controlChange()`](#controlChange) 的陣列版：不用定義函式，在 `draw()` 裡讀任何控制器。每個 CC 它都記著，包括不是旋鈕也不是推桿的那些，像調變輪（1）、音量（7）和延音踏板（64，0.5 以上算踩下去）。MIDI 通道被忽略：那個控制器號碼的最新值勝出，不管它是從哪個通道來的。"}],"value":{"type":"Array<Number>","range":"128 numbers, 0..1 each","en":"The index is the controller (CC) number, 0 to 127; the value is the last one received divided by 127. Changes when a message arrives. 0 before a control has moved, which is not the same as \"at 0\".","zh":"索引是控制器（CC）號碼，0 到 127，值是最後收到的那個除以 127。訊息進來的時候才會變。控制還沒動過之前是 0，這跟「剛好在 0」不是同一件事。"},"params":[],"notes":[{"en":"`midi.cc` is the same array. The friendly names `knob1` and `fader1` are looked up through the controller's profile and read the same data.","zh":"`midi.cc` 是同一個陣列。比較親切的 `knob1` 和 `fader1` 是透過控制器的設定檔對應過去的，讀的是同樣的資料。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mouseDragged() {\n  OAVp5.input([0xB0, 1, constrain(round(mouseX / width * 127), 0, 127)]);   // no wheel? drag the mouse\n}\n\nfunction draw() {\n  background(OAVp5.cc[64] >= 0.5 ? 90 : 20);   // sustain pedal down: lighter\n  circle(width / 2, height / 2, 40 + OAVp5.cc[1] * 300);   // mod wheel: size\n}","caption":{"en":"The mod wheel (CC 1) sets the size of the circle and the sustain pedal (CC 64) lightens the background. No controller? Drag the mouse across the canvas: it sends CC 1.","zh":"調變輪（CC 1）決定圓的大小，延音踏板（CC 64）讓背景變亮。沒有控制器的話，在畫布上拖動滑鼠：它會送出 CC 1。"},"run":"sketch","lang":"js"}],"related":["controlChange","knob1","fader1","OAVp5.note","OAVp5.bend"],"keywords":{"en":["cc array","control change values","mod wheel","sustain pedal","controller number","midi.cc"],"zh":["cc 陣列","控制變化的值","調變輪","延音踏板","控制器號碼","midi.cc"]},"tbd":[],"claims":[{"before":"midi.input([0xB0, 1, 127], 'screen')","expr":"OAVp5.cc[1]","equals":1}]},
{"id":"OAVp5.connectHardware","key":"OAVp5.connectHardware","name":"OAVp5.connectHardware","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.connectHardware()","source":{"file":"p5/oav-p5.js","line":274,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L274"},"summary":{"en":"Asks the browser for Web MIDI now, and returns a promise that says whether a real controller can be heard.","zh":"現在就向瀏覽器要 Web MIDI，並回傳一個 promise，告訴你聽不聽得到真的控制器。"},"description":[{"en":"It does what [`?oav_hardware=auto`](#option-hardware) does at load, at a moment you pick: from a button, after an intro, or when [`hardware`](#option-hardware) is `click` or `off`. It works whatever the option says. After a refusal you can call it again to retry, but browsers usually remember a refusal and answer at once, so the way out is the lock icon left of the address.","zh":"它做的就是 [`?oav_hardware=auto`](#option-hardware) 在載入時做的事，只是時間由你挑：從一個按鈕、片頭結束之後，或是 [`hardware`](#option-hardware) 設成 `click` 或 `off` 的時候。不管選項怎麼設它都能用。被拒絕之後可以再呼叫一次重試，但瀏覽器通常會記住拒絕、立刻回答，所以要解決得靠網址列左邊的鎖頭。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"Resolves to `true` once the browser has allowed Web MIDI (with or without a device plugged in), and to `false` if it refused or there is no Web MIDI in this browser. Calling it again while it is asking, or after it said yes, gives the same answer.","zh":"瀏覽器允許 Web MIDI 之後 resolve 成 `true`（不管有沒有插設備），被拒絕或這個瀏覽器沒有 Web MIDI 就 resolve 成 `false`。還在詢問的時候、或是已經答應之後再呼叫，會得到同樣的答案。"},"needs":{"en":"A browser with Web MIDI (Chrome, Edge, Firefox), on `https` or `localhost`. Some browsers want a click first: call it from `mousePressed()` or a button handler if `setup()` is too early. Safari and iPad have none: the promise resolves `false` and [`OAVp5.hardware`](#OAVp5.hardware) is `'unavailable'`.","zh":"要有 Web MIDI 的瀏覽器（Chrome、Edge、Firefox），網頁在 `https` 或 `localhost` 底下。有些瀏覽器要先有一次點擊：如果在 `setup()` 裡太早，就放在 `mousePressed()` 或按鈕的處理函式裡呼叫。Safari 和 iPad 沒有：promise 會 resolve 成 `false`，[`OAVp5.hardware`](#OAVp5.hardware) 是 `'unavailable'`。"},"notes":[],"examples":[{"code":"window.OAV_P5 = { hardware: 'click' };   // do not ask at load\n\nlet answer = 'click the canvas to ask for Web MIDI';\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.connectHardware().then((ok) => { answer = ok ? 'Web MIDI allowed' : 'no Web MIDI for this page'; });\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(answer, 20, 30);\n  text('state: ' + OAVp5.hardware, 20, 60);\n}","caption":{"en":"Nothing is asked at load. Click the canvas: the browser asks, the state goes from \"off\" through \"asking\" to \"connected\", \"waiting\" or \"denied\", and the text follows.","zh":"載入的時候什麼都沒有問。點一下畫布：瀏覽器才詢問，狀態從 \"off\" 經過 \"asking\" 變成 \"connected\"、\"waiting\" 或 \"denied\"，文字也跟著變。"},"run":"sketch","lang":"js"}],"related":["OAVp5.hardware","OAVp5.devices","option:hardware"],"keywords":{"en":["connect hardware","ask for midi","web midi permission","request midi","start midi"],"zh":["連接硬體","要 midi","web midi 權限","請求 midi","啟動 midi"]},"tbd":[],"claims":[]},
{"id":"OAVp5.devices","key":"OAVp5.devices","name":"OAVp5.devices","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.devices","source":{"file":"p5/oav-p5.js","line":598,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L598"},"summary":{"en":"The names of the MIDI controllers plugged in, as the browser reports them: an array of text, empty when there is none.","zh":"插著的 MIDI 控制器的名稱，照瀏覽器回報的樣子：一個文字陣列，沒有的時候是空的。"},"description":[{"en":"Use it for a line like \"playing: Arturia MiniLab3\", or to tell whether a real controller is there. The names are whatever the operating system calls the port, so match them loosely (`name.toLowerCase().includes('minilab')`). The 🎹 panel uses them to follow the controller that is plugged in.","zh":"用來顯示像「正在彈：Arturia MiniLab3」這樣的一行，或判斷有沒有真的控制器。名稱是作業系統給連接埠取的名字，所以比對時要寬鬆一點（`name.toLowerCase().includes('minilab')`）。🎹 面板就是用它們來跟著插上去的那一台控制器。"}],"value":{"type":"Array<String>","en":"One entry per connected input port. Replaced whenever a device is plugged in or out. Empty until Web MIDI has been allowed ([`OAVp5.hardware`](#OAVp5.hardware)), and it never lists the on-screen controller or the phone.","zh":"每個連上的輸入埠一項。每次有設備插上或拔掉都會換成新的。在 Web MIDI 被允許之前是空的（[`OAVp5.hardware`](#OAVp5.hardware)），而且永遠不會列出螢幕上的控制器或手機。"},"params":[],"notes":[{"en":"`midi.devices` is the same array, replaced as a whole, not changed in place: read it again after a change.","zh":"`midi.devices` 是同一個陣列，是整個換掉，不是就地修改：變動之後要重新讀一次。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.devices.length ? 'playing: ' + OAVp5.devices.join(', ') : 'no MIDI controller plugged in', 20, 30);\n}","caption":{"en":"Names the controllers found, or says there are none. Plug a controller in while the page is open and the line changes.","zh":"寫出找到的控制器，或說沒有。頁面開著的時候插上一台控制器，這一行就會改變。"},"run":"sketch","lang":"js"}],"related":["OAVp5.hardware","OAVp5.connectHardware","option:profile","option:hardware"],"keywords":{"en":["devices","connected controllers","port names","midi inputs","what is plugged in"],"zh":["設備","連上的控制器","連接埠名稱","midi 輸入","插了什麼"]},"tbd":[],"claims":[{"expr":"Array.isArray(OAVp5.devices)","equals":true}]},
{"id":"OAVp5.hardware","key":"OAVp5.hardware","name":"OAVp5.hardware","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.hardware","source":{"file":"p5/oav-p5.js","line":260,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L260"},"summary":{"en":"Where Web MIDI stands: `'off'`, `'asking'`, `'connected'`, `'waiting'`, `'denied'` or `'unavailable'`.","zh":"Web MIDI 目前的情形：`'off'`、`'asking'`、`'connected'`、`'waiting'`、`'denied'` 或 `'unavailable'`。"},"description":[{"en":"`'off'`: Web MIDI has not been asked for yet (see [`hardware`](#option-hardware)). `'asking'`: waiting for the browser's permission answer. `'connected'`: allowed, and at least one device is plugged in ([`OAVp5.devices`](#OAVp5.devices) lists them). `'waiting'`: allowed, but no device yet: plug one in and it becomes `'connected'` by itself. `'denied'`: the permission was refused. `'unavailable'`: the browser has no Web MIDI (Safari, iPad) or could not open it.","zh":"`'off'`：還沒有去要過 Web MIDI（見 [`hardware`](#option-hardware)）。`'asking'`：等瀏覽器的權限回答。`'connected'`：允許了，而且至少插著一台設備（[`OAVp5.devices`](#OAVp5.devices) 列出它們）。`'waiting'`：允許了，但還沒有設備：插上一台就會自己變成 `'connected'`。`'denied'`：權限被拒絕。`'unavailable'`：這個瀏覽器沒有 Web MIDI（Safari、iPad），或是打不開它。"},{"en":"It is only about real hardware. The on-screen controller and a phone work in every state.","zh":"它只管真的硬體。螢幕上的控制器和手機在每一種狀態下都能用。"}],"value":{"type":"String","range":"'off' | 'asking' | 'connected' | 'waiting' | 'denied' | 'unavailable'","en":"Changes when the browser answers and whenever a MIDI device is plugged in or out.","zh":"瀏覽器回答的時候，以及每次有 MIDI 設備插上或拔掉的時候會變。"},"params":[],"notes":[{"en":"In the 🩺 check-up, `waiting` is written as \"no device found\".","zh":"在 🩺 體檢裡，`waiting` 寫成「沒偵測到裝置」。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(OAVp5.hardware === 'connected' ? color(20, 60, 30) : 20);\n  fill(255);\n  text('Web MIDI: ' + OAVp5.hardware, 20, 30);\n  text('devices: ' + (OAVp5.devices.join(', ') || 'none'), 20, 60);\n}","caption":{"en":"Shows the state of Web MIDI and the devices found. The background turns green once a controller is connected. Plug one in to see it change.","zh":"顯示 Web MIDI 的狀態和找到的設備。控制器一連上，背景就變綠。插一台上去看它改變。"},"run":"sketch","lang":"js"}],"related":["OAVp5.devices","OAVp5.connectHardware","option:hardware","OAVp5.check"],"keywords":{"en":["hardware state","web midi","permission","connected","denied","no device","safari midi"],"zh":["硬體狀態","web midi","權限","已連上","被拒絕","沒有設備","safari midi"]},"tbd":[],"claims":[{"expr":"['off', 'asking', 'connected', 'waiting', 'denied', 'unavailable'].includes(OAVp5.hardware)","equals":true}]},
{"id":"OAVp5.held","key":"OAVp5.held","name":"OAVp5.held","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.held","source":{"file":"p5/oav-p5.js","line":258,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L258"},"summary":{"en":"A list of the MIDI numbers of the keys that are down right now, lowest first.","zh":"現在按著的鍵的 MIDI 號碼清單，由低到高。"},"description":[{"en":"The short version of [`OAVp5.note`](#OAVp5.note): only the keys that are down, so `OAVp5.held.length` is how many, `OAVp5.held[0]` is the lowest and `OAVp5.held.map((n) => noteName(n))` spells the chord. [`noteIsPressed`](#noteIsPressed) is `OAVp5.held.length > 0`.","zh":"[`OAVp5.note`](#OAVp5.note) 的精簡版：只列按著的鍵，所以 `OAVp5.held.length` 是幾個鍵，`OAVp5.held[0]` 是最低的那個，`OAVp5.held.map((n) => noteName(n))` 把和弦寫出來。[`noteIsPressed`](#noteIsPressed) 就是 `OAVp5.held.length > 0`。"}],"value":{"type":"Array<Number>","en":"Empty when nothing is held. A key is added when its note-on arrives and removed at its note-off, and the list is kept sorted from low to high.","zh":"沒有按著任何鍵的時候是空的。note-on 進來時加入，note-off 時移除，清單一直照由低到高排好。"},"params":[],"notes":[{"en":"`midi.held` is the same array. Read it, do not change it. It is the same array object every time, so copy it (`[...OAVp5.held]`) if you want to keep a moment of it.","zh":"`midi.held` 是同一個陣列。只讀，不要改它。它每次都是同一個陣列物件，想把某一刻存下來的話要複製一份（`[...OAVp5.held]`）。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(40);\n}\n\nfunction mousePressed() { OAVp5.play(60 + floor(random(12)), 0.8); }   // no keys? click\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.held.map((n) => noteName(n)).join('  ') || 'play a chord', width / 2, height / 2);\n}","caption":{"en":"Spells the keys held down, lowest first. Click the canvas to hold one more key. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"把按著的鍵由低到高寫出來。點一下畫布就多按著一個鍵。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["OAVp5.note","noteIsPressed","noteIsDown","noteName"],"keywords":{"en":["held keys","chord","pressed keys","keys down","midi.held"],"zh":["按著的鍵","和弦","按下的鍵","琴鍵","midi.held"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 64, 100], 'screen'); midi.input([0x90, 60, 100], 'screen')","expr":"JSON.stringify(OAVp5.held)","equals":"[60,64]"}]},
{"id":"OAVp5.input","key":"OAVp5.input","name":"OAVp5.input","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.input(bytes, [source], [device])","source":{"file":"p5/oav-p5.js","line":273,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L273"},"summary":{"en":"Sends one MIDI message into oav-p5 by hand, as bytes: the whole sketch reacts as if a controller had sent it.","zh":"用位元組手動送一則 MIDI 訊息給 oav-p5：整個草圖的反應就像是控制器送出來的。"},"description":[{"en":"It is the door every input goes through, and you can knock on it yourself: a sketch can test itself without a controller, drive itself from a timer, or take messages from a WebSocket or another library. The sketch cannot tell the difference: [`knob1`](#knob1), [`notePressed()`](#notePressed), [`OAVp5.note`](#OAVp5.note), the sound if [`startSound()`](#startSound) is on and the drum cues all react. [`OAVp5.play()`](#OAVp5.play) is the friendlier way to send a note.","zh":"每一種輸入都從這扇門進來，你也可以自己來敲：草圖可以在沒有控制器的時候測試自己、用計時器自己推動自己，或接收來自 WebSocket 或其他函式庫的訊息。草圖分不出差別：[`knob1`](#knob1)、[`notePressed()`](#notePressed)、[`OAVp5.note`](#OAVp5.note)、開著 [`startSound()`](#startSound) 時的聲音，還有鼓機的 cue 都會有反應。要送音符的話，[`OAVp5.play()`](#OAVp5.play) 是比較親切的方式。"}],"params":[{"name":"bytes","type":"Array<Number>","en":"One message, `[status, data1, data2]`: `[0x90, 60, 100]` is a note-on for key 60 with velocity 100 on channel 1, `[0xB0, 74, 127]` a control change, `[0xE0, 0, 64]` a pitch bend in the middle. The status byte sets the kind (top half) and the channel (bottom half). Only the first three numbers are read, and the data bytes are held to 0 to 127. System messages are not for this: the clock bytes (`0xF8`, `0xFA`, `0xFB`, `0xFC`, `0xF2`) go to [`OAVp5.clock`](#OAVp5.clock), and anything else from `0xF0` up is ignored.","zh":"一則訊息，`[status, data1, data2]`：`[0x90, 60, 100]` 是通道 1 上鍵 60、力道 100 的 note-on，`[0xB0, 74, 127]` 是控制變化，`[0xE0, 0, 64]` 是在中間的音高輪。status 位元組決定種類（上半）和通道（下半）。只讀前三個數字，資料位元組會被限制在 0 到 127。系統訊息不是給它用的：時鐘位元組（`0xF8`、`0xFA`、`0xFB`、`0xFC`、`0xF2`）會交給 [`OAVp5.clock`](#OAVp5.clock)，從 `0xF0` 起的其他東西都忽略。"},{"name":"source","type":"String","optional":true,"default":"'screen'","en":"A label for where it came from, which shows up as `OAVp5.last.source` and as the second argument of [`midiMessage()`](#midiMessage). Any text. `'hardware'` and `'virtual-out'` are taken as those real sources and are not repeated to a sketch's own Web MIDI code.","zh":"一個說明來源的標籤，會出現在 `OAVp5.last.source`，也是 [`midiMessage()`](#midiMessage) 的第二個參數。任何文字都行。`'hardware'` 和 `'virtual-out'` 會被當作真的那兩種來源，不會再轉給草圖自己的 Web MIDI 程式。"},{"name":"device","type":"String","optional":true,"en":"A profile id such as `'akai-lpd8'`: the numbering of `knob1`, `pad1` … follows that controller for this message. Left out, it is the controller on the on-screen panel.","zh":"設定檔 id，像 `'akai-lpd8'`：這則訊息裡 `knob1`、`pad1` … 的編號照那一台控制器來對應。不寫的話，就是螢幕面板上的那一台控制器。"}],"notes":[{"en":"`midi.input(…)` is the same function. It returns nothing. A message with a source other than `'screen'` also lights the keys of the on-screen controller if it is open.","zh":"`midi.input(…)` 是同一個函式。它沒有回傳值。來源不是 `'screen'` 的訊息，會在螢幕控制器打開的時候也讓它的琴鍵亮起來。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(24);\n}\n\nfunction mousePressed() {\n  OAVp5.input([0x90, 60 + floor(random(12)), 100]);   // a note-on, as a key would send it\n}\n\nfunction notePressed(note, velocity) {\n  background(20);\n  fill(255);\n  text(noteName(note) + ' (sent by hand)', width / 2, height / 2);\n}","caption":{"en":"Click the canvas: the sketch hears a note-on exactly as if you had played a key, and names it.","zh":"點一下畫布：草圖聽到一個 note-on，就跟你彈了一個鍵一樣，並說出它的名字。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mouseDragged() {\n  OAVp5.input([0xB0, 74, constrain(round(mouseX / width * 127), 0, 127)]);   // CC 74 is knob 1 on a MiniLab 3\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2, height / 2, 40 + knob1 * 300);\n}","caption":{"en":"Drag the mouse across the canvas: it sends CC 74, which is knob 1, and the circle follows.","zh":"在畫布上拖動滑鼠：它送出 CC 74，也就是旋鈕 1，圓跟著動。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  OAVp5.input([0xB0, 70, 127], 'test', 'akai-lpd8');   // CC 70 is knob 1 on an LPD8\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('knob1 = ' + knob1 + ' (from the LPD8 numbering)', 20, 30);\n  text('last: ' + OAVp5.last.control + ' from ' + OAVp5.last.source, 20, 60);\n}","caption":{"en":"The third argument says which controller the message comes from, so CC 70 is knob 1 here, whatever profile the panel shows.","zh":"第三個參數說明這則訊息來自哪一台控制器，所以不管面板顯示哪個設定檔，這裡的 CC 70 都是旋鈕 1。"},"run":"sketch","lang":"js"}],"related":["OAVp5.play","OAVp5.last","OAVp5.on","midiMessage","noteOn","controlChange"],"keywords":{"en":["input","send midi","fake midi","simulate controller","raw bytes","test without controller","midi.input"],"zh":["輸入","送 midi","假的 midi","模擬控制器","原始位元組","沒有控制器也能測","midi.input"]},"tbd":[],"claims":[{"before":"midi.input([0xB0, 70, 127], 'test', 'akai-lpd8')","expr":"knob1","equals":1},{"before":"midi.input([0x93, 61, 127], 'test')","expr":"JSON.stringify([OAVp5.last.note, OAVp5.last.channel, OAVp5.last.source])","equals":"[61,4,\"test\"]"}]},
{"id":"OAVp5.isDown","key":"OAVp5.isDown","name":"OAVp5.isDown","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.isDown(n)","source":{"file":"p5/oav-p5.js","line":271,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L271"},"summary":{"en":"True while the key `n` (a MIDI number or a name like `'C4'`) is held down: the same as `noteIsDown(n)`.","zh":"鍵 `n`（MIDI 號碼，或像 `'C4'` 這樣的音名）按著的時候是 true：跟 `noteIsDown(n)` 一樣。"},"description":[{"en":"It is [`noteIsDown()`](#noteIsDown) under the `OAVp5` name, for a sketch that cannot use the plain one: `OAVp5.isDown('C4')`. A name that is not a note stops the sketch with a card that says how to write one.","zh":"它是用 `OAVp5` 這個名字呼叫的 [`noteIsDown()`](#noteIsDown)，給不能用一般名稱的草圖：`OAVp5.isDown('C4')`。不是音符的名稱會讓草圖停下來，並用卡片說明怎麼寫。"}],"params":[{"name":"n","type":"Number | String","en":"The key: a MIDI number (60) or a note name (`'C4'`, `'c#4'`, `'Db3'`).","zh":"哪個鍵：MIDI 號碼（60）或音名（`'C4'`、`'c#4'`、`'Db3'`）。"}],"returns":{"type":"Boolean","en":"True if that key is down right now.","zh":"那個鍵現在按著就是 true。"},"notes":[{"en":"`midi.isDown(…)` is the same function.","zh":"`midi.isDown(…)` 是同一個函式。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction mousePressed() { for (const n of ['C4', 'E4', 'G4']) OAVp5.play(n, 0.8); }   // no keys? click for a chord\nfunction mouseReleased() { for (const n of ['C4', 'E4', 'G4']) OAVp5.play(n, 0); }\n\nfunction draw() {\n  const chord = OAVp5.isDown('C4') && OAVp5.isDown('E4') && OAVp5.isDown('G4');\n  background(chord ? color(60, 100, 40) : 20);\n  fill(255);\n  text(chord ? 'C major' : 'hold C4 + E4 + G4', width / 2, height / 2);\n}","caption":{"en":"The background turns green while C, E and G are held together. Hold the mouse down to play the chord. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"C、E、G 同時按著的時候，背景變綠。按住滑鼠就會彈出這個和弦。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["noteIsDown","OAVp5.note","OAVp5.held","noteNumber"],"keywords":{"en":["is down","key down","is key held","note name","midi.isDown"],"zh":["按著嗎","琴鍵按著","音名","midi.isDown"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 127], 'screen')","expr":"OAVp5.isDown('C4')","equals":true},{"before":"midi.input([0x80, 60, 0], 'screen')","expr":"OAVp5.isDown(60)","equals":false}]},
{"id":"OAVp5.last","key":"OAVp5.last","name":"OAVp5.last","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.last","source":{"file":"p5/oav-p5.js","line":524,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L524"},"summary":{"en":"The latest note, control change or pitch bend that arrived: its type, number, value, channel, where it came from and which control it is. `null` before any.","zh":"最近收到的音符、控制變化或音高輪訊息：它的種類、號碼、值、通道、來自哪裡、是哪個控制。在任何訊息之前是 `null`。"},"description":[{"en":"A single object that is replaced on every message, so read it, do not keep it. It is how a sketch finds out where a message came from (`OAVp5.last.source`) or which knob moved (`OAVp5.last.control`), and what to read when the sketch has its own `note` or `velocity` that keeps the plain names: `OAVp5.last.note` and `OAVp5.last.value`.","zh":"它是每收到一則訊息就整個換掉的單一物件，所以只讀，不要留著。草圖用它知道訊息從哪裡來（`OAVp5.last.source`）或哪顆旋鈕動了（`OAVp5.last.control`），草圖自己有 `note` 或 `velocity` 而保留了一般名稱的時候，也是改讀它：`OAVp5.last.note` 和 `OAVp5.last.value`。"},{"en":"It keeps only notes, control changes and pitch bends. Aftertouch and program change reach [`midiMessage()`](#midiMessage) but are not stored here.","zh":"它只留音符、控制變化和音高輪。觸後和音色切換只會到 [`midiMessage()`](#midiMessage)，這裡不存。"}],"value":{"type":"Object | null","en":"`{ type, note | cc, value, channel, source, control }`. `type` is `'noteOn'`, `'noteOff'`, `'controlChange'` or `'pitchBend'` (a note-on with velocity 0 is a `'noteOff'`). `note` is there for notes, `cc` for control changes. `value` is the velocity 0..1 (0 for a note-off), the control's position 0..1, or the wheel −1..1. `channel` is 1 to 16. `source` is `'hardware'`, `'screen'`, `'phone'`, `'virtual-out'`, `'link'` or the label you gave [`OAVp5.input()`](#OAVp5.input). `control` is `'knob1'`, `'fader2'`, `'pad3'` … when the controller's profile knows it, otherwise `null`.","zh":"`{ type, note | cc, value, channel, source, control }`。`type` 是 `'noteOn'`、`'noteOff'`、`'controlChange'` 或 `'pitchBend'`（力道 0 的 note-on 算 `'noteOff'`）。音符有 `note`，控制變化有 `cc`。`value` 是力道 0 到 1（note-off 是 0）、控制的位置 0 到 1，或輪子的 −1 到 1。`channel` 是 1 到 16。`source` 是 `'hardware'`、`'screen'`、`'phone'`、`'virtual-out'`、`'link'`，或你在 [`OAVp5.input()`](#OAVp5.input) 給的標籤。`control` 在控制器的設定檔認得它的時候是 `'knob1'`、`'fader2'`、`'pad3'` …，否則是 `null`。"},"params":[],"notes":[{"en":"`midi.last` is the same object. Clock messages (tick, start, stop) are not here: see [`OAVp5.clock`](#OAVp5.clock).","zh":"`midi.last` 是同一個物件。時鐘訊息（tick、start、stop）不在這裡，請看 [`OAVp5.clock`](#OAVp5.clock)。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(16);\n}\n\nfunction mousePressed() { OAVp5.input([0x90, 60 + floor(random(12)), 100], 'mouse'); }   // no controller? click\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.last ? JSON.stringify(OAVp5.last, null, 1) : 'play something', 20, 30);\n}","caption":{"en":"Prints the last message as an object. Click the canvas to send one from the mouse, and watch `source`. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"把最後一則訊息印成物件。點一下畫布就從滑鼠送一則，並看 `source`。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"}],"related":["midiMessage","noteOn","note","velocity","OAVp5.input"],"keywords":{"en":["last message","latest midi","source","which knob","midi.last","channel"],"zh":["最後一則訊息","最新的 midi","來源","哪顆旋鈕","midi.last","通道"]},"tbd":[],"claims":[{"expr":"OAVp5.last === null","equals":true},{"before":"midi.input([0x93, 60, 127], 'test')","expr":"JSON.stringify([OAVp5.last.type, OAVp5.last.note, OAVp5.last.value, OAVp5.last.channel, OAVp5.last.source])","equals":"[\"noteOn\",60,1,4,\"test\"]"}]},
{"id":"OAVp5.note","key":"OAVp5.note","name":"OAVp5.note","aliases":[],"kind":"variable","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.note","source":{"file":"p5/oav-p5.js","line":258,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L258"},"summary":{"en":"An array of 128 numbers, one per MIDI key: how hard that key is held, 0 to 1, and 0 when it is up. `OAVp5.note[60]` is middle C.","zh":"128 個數字的陣列，每個 MIDI 鍵一個：那個鍵被按得多用力，0 到 1，放開就是 0。`OAVp5.note[60]` 是中央 C。"},"description":[{"en":"Read every key at once, in `draw()`: `OAVp5.note[60]` for one key, or a loop over a range to draw a piano roll. It is the array version of [`noteIsDown()`](#noteIsDown), with the velocity in it. It is not the variable [`note`](#note), which is just the number of the last key pressed. [`OAVp5.held`](#OAVp5.held) is the same information as a short list of the keys that are down.","zh":"在 `draw()` 裡一次讀每個鍵：`OAVp5.note[60]` 讀一個鍵，或用迴圈跑過一個範圍來畫鋼琴捲簾。它是 [`noteIsDown()`](#noteIsDown) 的陣列版，而且裡面有力道。它不是變數 [`note`](#note)，那個只是最後按下的鍵的號碼。[`OAVp5.held`](#OAVp5.held) 是同樣的資訊，做成只列出按著的鍵的短清單。"}],"value":{"type":"Array<Number>","range":"128 numbers, 0..1 each","en":"The index is the MIDI note number, 0 to 127. A key that is down holds the velocity of its note-on divided by 127, a key that is up (or never pressed) holds 0. Changes when a message arrives, not once per frame.","zh":"索引是 MIDI 音符號碼，0 到 127。按著的鍵放的是它 note-on 的力道除以 127，放開（或從沒按過）的鍵是 0。訊息進來的時候才會變，不是每個畫面一次。"},"params":[],"notes":[{"en":"`midi.note` is the same array. A note-on with velocity 0 sets the key to 0, like a note-off.","zh":"`midi.note` 是同一個陣列。力道是 0 的 note-on 會讓那個鍵變成 0，就像 note-off。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  for (let n = 36; n < 96; n++) {   // five octaves, C2 to B6\n    const v = OAVp5.note[n];\n    fill(v > 0 ? color(255, 211, 107) : 60);\n    rect(20 + (n - 36) * 6, height - 40 - v * 200, 5, 40 + v * 200);\n  }\n}","caption":{"en":"A bar for each key: it rises while the key is down, higher for a harder press. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"每個鍵一根長條：按著的時候升起，按得越用力越高。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const v = lag(OAVp5.note[60], 0.05, 0.4);   // middle C, softened\n  circle(width / 2, height / 2, 40 + v * 260);\n}","caption":{"en":"Middle C (60) swells the circle by how hard it is held, and the circle shrinks back slowly.","zh":"中央 C（60）按得多用力，圓就脹得多大，放開之後慢慢縮回去。"},"run":"sketch","lang":"js"}],"related":["OAVp5.held","OAVp5.isDown","noteIsDown","note","velocity","OAVp5.cc"],"keywords":{"en":["note array","all keys","key velocity","piano roll","held keys","midi.note"],"zh":["音符陣列","所有琴鍵","琴鍵力道","鋼琴捲簾","按著的鍵","midi.note"]},"tbd":[],"claims":[{"before":"midi.input([0x90, 60, 127], 'screen')","expr":"OAVp5.note[60]","equals":1},{"before":"midi.input([0x80, 60, 0], 'screen')","expr":"OAVp5.note[60]","equals":0}]},
{"id":"OAVp5.on","key":"OAVp5.on","name":"OAVp5.on","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.on(name, fn)","source":{"file":"p5/oav-p5.js","line":262,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L262"},"summary":{"en":"Listens to a MIDI event by name without defining a global function, and returns a function that stops listening.","zh":"用名字監聽一個 MIDI 事件，不必定義全域函式，並回傳一個停止監聽的函式。"},"description":[{"en":"The event functions ([`notePressed()`](#notePressed), [`knobMoved()`](#knobMoved), [`noteOn()`](#noteOn) …) are defined once, by name. `OAVp5.on('notePressed', fn)` is the other way: any number of listeners, added and removed while the sketch runs, with no global names taken. It is how a library, a class or a Hydra sketch listens, and the way to listen for a while and stop.","zh":"事件函式（[`notePressed()`](#notePressed)、[`knobMoved()`](#knobMoved)、[`noteOn()`](#noteOn) …）是用名字定義一次的。`OAVp5.on('notePressed', fn)` 是另一種方式：想要幾個監聽者都行，草圖在跑的時候可以加也可以拿掉，也不占用任何全域名稱。函式庫、類別或 Hydra 草圖就是這樣監聽的，要聽一陣子再停下來也用它。"},{"en":"A listener counts as a defined function for the pad rule: once anything listens to `'padPressed'`, a pad stops calling `notePressed()`. An error inside `fn` shows in the console and on a card, and the other listeners still run. For the microphone's events use [`OAVp5.mic.on()`](#OAVp5.mic.on).","zh":"對打擊墊的規則來說，監聽者就跟定義過的函式一樣：只要有東西在聽 `'padPressed'`，打擊墊就不再呼叫 `notePressed()`。`fn` 裡面出錯會顯示在 console 和卡片上，其他的監聽者照樣執行。麥克風的事件要用 [`OAVp5.mic.on()`](#OAVp5.mic.on)。"}],"params":[{"name":"name","type":"String","en":"The event: `'notePressed'`, `'noteReleased'`, `'knobMoved'`, `'faderMoved'`, `'padPressed'`, `'padReleased'`, `'noteOn'`, `'noteOff'`, `'controlChange'`, `'pitchBend'` or `'midiMessage'`. `'noteon'` and `'noteoff'` work in any case. Anything else throws an error that lists the names.","zh":"事件名稱：`'notePressed'`、`'noteReleased'`、`'knobMoved'`、`'faderMoved'`、`'padPressed'`、`'padReleased'`、`'noteOn'`、`'noteOff'`、`'controlChange'`、`'pitchBend'` 或 `'midiMessage'`。`'noteon'` 和 `'noteoff'` 大小寫隨便。其他的名稱會丟出一個錯誤，並列出可用的名稱。"},{"name":"fn","type":"Function","en":"Called with the same arguments the function of that name would get (`notePressed(note, velocity)`, `knobMoved(n, value)` …).","zh":"呼叫它的時候帶的參數跟同名函式會拿到的一樣（`notePressed(note, velocity)`、`knobMoved(n, value)` …）。"}],"returns":{"type":"Function","en":"A function that takes `fn` off again: `const off = OAVp5.on(…); off();`.","zh":"一個把 `fn` 取消的函式：`const off = OAVp5.on(…); off();`。"},"notes":[{"en":"`midi.on(…)` is the same function.","zh":"`midi.on(…)` 是同一個函式。"}],"examples":[{"code":"let heard = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  OAVp5.on('noteOn', (note, velocity) => { heard++; });\n}\n\nfunction mousePressed() { OAVp5.play(60 + floor(random(12)), 0.8); }   // no keys? click\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('notes heard: ' + heard, 20, 30);\n}","caption":{"en":"Counts the notes with a listener instead of a global function. Click the canvas to play one. Plug in a controller, or press 🎹 (bottom right) and use the on-screen one.","zh":"用監聽者而不是全域函式來數音符。點一下畫布就彈一個。接上控制器，或按右下角的 🎹 用螢幕上的控制器。"},"run":"sketch","lang":"js"},{"code":"let heard = 0;\nlet off;\n\nfunction setup() {\n  createCanvas(400, 300);\n  off = OAVp5.on('noteOn', () => {\n    heard++;\n    if (heard === 5) off();   // stop listening after five notes\n  });\n}\n\nfunction mousePressed() { OAVp5.play(60 + floor(random(12)), 0.8); }\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('notes heard: ' + heard + (heard >= 5 ? ' (stopped listening)' : ''), 20, 30);\n}","caption":{"en":"The number counts up to five and then stops, even though you keep playing: the listener took itself off.","zh":"數字數到五就停了，就算你繼續彈也一樣：監聽者把自己取消掉了。"},"run":"sketch","lang":"js"}],"related":["OAVp5.mic.on","notePressed","noteOn","knobMoved","OAVp5.input"],"keywords":{"en":["subscribe","listen","event listener","unsubscribe","off","callback without global","Hydra"],"zh":["訂閱","監聽","事件監聽","取消訂閱","不用全域函式","Hydra"]},"tbd":[],"claims":[{"before":"window.__n = 0; window.__off = OAVp5.on('noteOn', () => window.__n++); midi.input([0x90, 60, 100], 'screen'); midi.input([0x90, 62, 100], 'screen'); window.__off(); midi.input([0x90, 64, 100], 'screen')","expr":"window.__n","equals":2}]},
{"id":"OAVp5.panel","key":"OAVp5.panel","name":"OAVp5.panel","aliases":[],"kind":"object","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.panel","source":{"file":"p5/oav-p5.js","line":275,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L275"},"summary":{"en":"The 🎹 panel as an object: `open()`, `close()` and `toggle()` show and hide it from code.","zh":"把 🎹 面板當成物件：用 `open()`、`close()`、`toggle()` 從程式裡顯示和收起它。"},"description":[{"en":"The panel behind the 🎹 button holds the on-screen controller and the buttons for 🎤, 🔊, 🥁, ⏺, 📷, 📱 and 🩺. [`OAVp5.panel.open()`](#OAVp5.panel.open), `close()` and `toggle()` do what pressing 🎹 does, from your code: open it on a key, or open it at the start for a workshop ([`?oav_panel=open`](#option-panel) does the same at load).","zh":"🎹 按鈕後面的面板裝著螢幕上的控制器，和 🎤、🔊、🥁、⏺、📷、📱、🩺 的按鈕。[`OAVp5.panel.open()`](#OAVp5.panel.open)、`close()`、`toggle()` 做的是按 🎹 做的事，只是從你的程式裡：按一個鍵就打開它，或是工作坊一開始就打開（[`?oav_panel=open`](#option-panel) 在載入時做同樣的事）。"}],"params":[],"notes":[{"en":"With `?oav_panel=off` there is no 🎹 and these do nothing.","zh":"設了 `?oav_panel=off` 就沒有 🎹，這幾個也什麼都不做。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'p') OAVp5.panel.toggle();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('press p to show or hide the 🎹 panel', 20, 30);\n}","caption":{"en":"Press p: the 🎹 panel opens and, pressed again, closes.","zh":"按 p：🎹 面板打開，再按一次就關起來。"},"run":"sketch","lang":"js"}],"related":["OAVp5.panel.open","option:panel","option:compact","OAVp5.phone"],"keywords":{"en":["panel","open the panel","show piano panel","toggle panel","on-screen controller"],"zh":["面板","打開面板","顯示鋼琴面板","切換面板","螢幕控制器"]},"tbd":[],"claims":[]},
{"id":"OAVp5.panel.open","key":"OAVp5.panel.open","name":"OAVp5.panel.open","aliases":["OAVp5.panel.close","OAVp5.panel.toggle"],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.panel.open()","source":{"file":"p5/oav-p5.js","line":275,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L275"},"summary":{"en":"Opens the 🎹 panel (`open()`), closes it (`close()`) or flips it (`toggle()`).","zh":"打開 🎹 面板（`open()`）、收起它（`close()`），或是切換（`toggle()`）。"},"description":[{"en":"Same as pressing the 🎹 button. The first time the panel opens, oav-p5 loads the on-screen controller (a few seconds on a slow connection, which the panel says while it loads), and if [`hardware`](#option-hardware) is `click` it asks for Web MIDI then. `OAVp5.panel.open()` when it is already open does nothing new.","zh":"就跟按 🎹 按鈕一樣。面板第一次打開的時候，oav-p5 才載入螢幕控制器（連線慢的話要幾秒，載入的時候面板會這麼說），而且 [`hardware`](#option-hardware) 是 `click` 的話，就在這時去要 Web MIDI。已經打開的時候再呼叫 `OAVp5.panel.open()` 不會有新的動作。"}],"params":[],"returns":{"type":"Promise<Element | null>","en":"A promise. `open()` (and `toggle()` when it opens) resolves to the panel's element once the on-screen controller inside it has loaded, and the panel is on screen at once, before that. `close()` (and `toggle()` when it closes) resolves to `null`, and so does everything when the panel option is `off`.","zh":"一個 promise。`open()`（以及 `toggle()` 打開時）在面板裡的螢幕控制器載好之後 resolve 成面板的元素，面板本身一開始就會顯示，不用等。`close()`（以及 `toggle()` 關起來時）resolve 成 `null`，面板選項是 `off` 的時候全部都是 `null`。"},"notes":[{"en":"The three are listed together here: `OAVp5.panel.close` and `OAVp5.panel.toggle` take no arguments either. `midi.panel.open()` is the same function.","zh":"這三個放在一起說明：`OAVp5.panel.close` 和 `OAVp5.panel.toggle` 也都不帶參數。`midi.panel.open()` 是同一個函式。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'o') OAVp5.panel.open();\n  if (key === 'c') OAVp5.panel.close();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('o: open the 🎹 panel,  c: close it', 20, 30);\n}","caption":{"en":"Press o to open the 🎹 panel and c to close it.","zh":"按 o 打開 🎹 面板，按 c 收起來。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.panel.open().then((panel) => console.log('the panel is', panel ? 'open and loaded' : 'not available'));\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('click the canvas to open the panel', 20, 30);\n}","caption":{"en":"Click the canvas: the panel opens at once, and the console says when the controller inside it has loaded.","zh":"點一下畫布：面板馬上打開，裡面的控制器載好的時候，console 會說一聲。"},"run":"sketch","lang":"js"}],"related":["OAVp5.panel","option:panel","OAVp5.phone","OAVp5.check"],"keywords":{"en":["open panel","close panel","toggle panel","show 🎹","hide 🎹"],"zh":["打開面板","收起面板","切換面板","顯示 🎹","隱藏 🎹"]},"tbd":[],"claims":[]},
{"id":"OAVp5.phone","key":"OAVp5.phone","name":"OAVp5.phone","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.phone()","source":{"file":"p5/oav-p5.js","line":276,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L276"},"summary":{"en":"Shows the 📱 card: a QR code that sends a phone to the remote page, whose MIDI tab then plays this sketch. Returns the room name.","zh":"顯示 📱 卡片：一個 QR code，讓手機打開遙控頁面，手機上的 MIDI 分頁就能彈這個草圖。回傳房間名稱。"},"description":[{"en":"The same as the 📱 button in the panel. The card shows a QR code, the room and the link underneath; opening the link on a phone gives that phone an on-screen controller whose MIDI tab plays your sketch over the relay, as one more input (`OAVp5.last.source` is `'phone'`). Calling it again hides the card. The first call connects to the [`relay`](#option-relay) and loads a small QR-code library from jsDelivr.","zh":"跟面板裡的 📱 按鈕一樣。卡片上有 QR code、房間，和下面的連結。在手機上打開連結，那支手機就有一個螢幕控制器，它的 MIDI 分頁透過中繼來彈你的草圖，當作多一個輸入（`OAVp5.last.source` 是 `'phone'`）。再呼叫一次就把卡片收起來。第一次呼叫會連到 [`relay`](#option-relay)，並從 jsDelivr 載入一個很小的 QR code 函式庫。"}],"params":[],"returns":{"type":"String","en":"The name of the room the phone joins: the [`room`](#option-room) option, or the [`link`](#option-link) room, or a name made up for this browser (`p5-…`) and kept in it.","zh":"手機要加入的房間名稱：[`room`](#option-room) 選項、[`link`](#option-link) 的房間，或是為這個瀏覽器隨機取、並存在裡面的名字（`p5-…`）。"},"needs":{"en":"A network connection, to the relay (`wss://remote.cheyuwu.com/relay` unless you set another) and, for the picture of the QR code, to jsDelivr. Without the library the card shows the link as text.","zh":"要有網路，連到中繼（沒另外設的話是 `wss://remote.cheyuwu.com/relay`），顯示 QR code 的圖還要連到 jsDelivr。沒有那個函式庫的話，卡片就把連結用文字顯示。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  const room = OAVp5.phone();   // the QR code card opens\n  console.log('phones join room', room);\n}\n\nfunction notePressed(note) {\n  background(random(255), random(255), random(255));   // a phone key changes the colour\n}\n\nfunction draw() {\n  fill(255);\n  text('click the canvas, scan the QR code, open the MIDI tab', 20, 30);\n}","caption":{"en":"Click the canvas and scan the QR code with a phone: the keys on its MIDI tab change the background of this sketch.","zh":"點一下畫布，用手機掃 QR code：手機 MIDI 分頁上的鍵會改變這個草圖的背景顏色。"},"run":"static","lang":"js"}],"related":["option:room","option:relay","option:remote","OAVp5.panel.open"],"keywords":{"en":["phone","qr code","play from phone","remote","mobile controller","scan"],"zh":["手機","qr code","用手機彈","遙控","行動控制器","掃描"]},"tbd":[{"en":"A phone playing a sketch through the relay was not tried for this page: the headless check cannot reach the relay, so the example is not run. The behaviour is as the code and the project's README describe it.","zh":"這一頁沒有試過用手機透過中繼彈草圖：無頭瀏覽器的檢查連不到中繼，所以範例沒有跑。行為依程式碼和專案的 README 所述。"}],"claims":[]},
{"id":"OAVp5.play","key":"OAVp5.play","name":"OAVp5.play","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.play(note, [velocity])","source":{"file":"p5/oav-p5.js","line":269,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L269"},"summary":{"en":"Plays a key as if a controller did: `midi.play(60, 0.8)` presses middle C, `midi.play(60, 0)` lets it go.","zh":"像控制器那樣彈一個鍵：`midi.play(60, 0.8)` 按下中央 C，`midi.play(60, 0)` 放開。"},"description":[{"en":"It sends the same MIDI bytes a keyboard would, with `midi.last.source` set to `'screen'`, so everything reacts: [`notePressed()`](#notePressed), [`noteIsDown()`](#noteIsDown), [`note`](#note), the sound if [`startSound()`](#startSound) is on. `midi` and `OAVp5` are the same object, so `OAVp5.play` and `midi.play` are one function.","zh":"它送出的 MIDI 位元組跟鍵盤送出的一樣，`midi.last.source` 會是 `'screen'`，所以所有東西都會有反應：[`notePressed()`](#notePressed)、[`noteIsDown()`](#noteIsDown)、[`note`](#note)，還有開著 [`startSound()`](#startSound) 時的聲音。`midi` 和 `OAVp5` 是同一個物件，所以 `OAVp5.play` 和 `midi.play` 是同一個函式。"}],"params":[{"name":"note","type":"Number | String","en":"The key: a MIDI number or a name (`'C#4'`).","zh":"哪個鍵：MIDI 號碼或音名（`'C#4'`）。"},{"name":"velocity","type":"Number","optional":true,"default":"1","en":"How hard, 0..1; 0 releases the key. Leave it out for full force.","zh":"多用力，0..1。0 表示放開。不給就是最大力道。"}],"notes":[{"en":"The key stays down until you play it again with velocity 0.","zh":"鍵會一直按著，直到你再用力道 0 彈一次。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction mousePressed() { midi.play('C4', 0.8); }\nfunction mouseReleased() { midi.play('C4', 0); }\n\nfunction draw() {\n  background(noteIsDown('C4') ? 90 : 20);\n  fill(255);\n  text('hold the mouse: C4 is ' + (noteIsDown('C4') ? 'down' : 'up'), width / 2, height / 2);\n}","caption":{"en":"The mouse plays middle C: the sketch hears it the same as a key.","zh":"用滑鼠彈中央 C：草圖聽到的跟按鍵一模一樣。"},"run":"sketch","lang":"js"}],"related":["noteIsDown","notePressed","noteNumber","OAVp5.input"],"keywords":{"en":["play","send note","trigger","simulate","virtual key"],"zh":["彈","送出音符","觸發","模擬","虛擬琴鍵"]},"tbd":[],"claims":[{"before":"midi.play('C4', 0.8)","expr":"noteIsDown(60)","equals":true}]},
{"id":"OAVp5.wiring","key":"OAVp5.wiring","name":"OAVp5.wiring","aliases":[],"kind":"function","package":"oav-p5","category":"midi","documented":true,"signature":"OAVp5.wiring([dev])","source":{"file":"p5/oav-p5.js","line":278,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L278"},"summary":{"en":"Lists which controller knob, fader or pad drives which OpenProcessing OPC variable right now: `[{ control: 'knob1', name: 'radius' }, …]`.","zh":"列出現在哪一顆旋鈕、推桿或墊子在推動哪一個 OpenProcessing OPC 變數：`[{ control: 'knob1', name: 'radius' }, …]`。"},"description":[{"en":"With OP Configurator, `OPC.slider('radius', 10)` declares a variable and oav-p5 wires the controller to the OPC variables in order: knob 1 to the first slider, knob 2 to the second, the faders after the knobs, pad 1 to the first toggle or button. This is the list of who drives what, the same one the panel prints under the controller and the console says once. Use it when a knob moves the wrong thing.","zh":"搭配 OP Configurator 時，`OPC.slider('radius', 10)` 宣告一個變數，oav-p5 會依序把控制器接到 OPC 的變數：旋鈕 1 接第一個滑桿，旋鈕 2 接第二個，推桿接在旋鈕後面，打擊墊 1 接第一個開關或按鈕。這份清單就是誰推動什麼，面板在控制器下面印的、console 說一次的也是同一份。旋鈕推動錯東西的時候就看它。"}],"params":[{"name":"dev","type":"String","optional":true,"default":"'screen'","en":"Whose numbering to use: `'screen'` (the controller shown in the panel) or a profile id such as `'akai-lpd8'`. It matters for the faders, which are numbered after the knobs.","zh":"用誰的編號：`'screen'`（面板上顯示的那台控制器）或設定檔 id，像 `'akai-lpd8'`。它會影響推桿，因為推桿的編號接在旋鈕後面。"}],"returns":{"type":"Array<Object>","en":"One `{ control, name }` per wired variable: `control` is `'knob1'`, `'fader2'`, `'pad1'` …, `name` is the OPC variable. A control the sketch reads itself also has `sketch: true`, and its slider no longer follows it. An empty array when there is no OPC on the page or [`opc`](#option-opc) is `off`.","zh":"每個接上線的變數一筆 `{ control, name }`：`control` 是 `'knob1'`、`'fader2'`、`'pad1'` …，`name` 是 OPC 的變數。草圖自己有讀的控制還會多一個 `sketch: true`，對應的滑桿就不再跟著它。頁面上沒有 OPC，或 [`opc`](#option-opc) 是 `off` 的時候是空陣列。"},"notes":[{"en":"`midi.wiring(…)` is the same function.","zh":"`midi.wiring(…)` 是同一個函式。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const wired = OAVp5.wiring();\n  text(wired.length ? wired.map((w) => w.control + ' -> ' + w.name).join('\\n') : 'nothing wired (no OPC here)', 20, 30);\n}","caption":{"en":"Prints the controls and the OPC variables they drive, one per line. Outside OpenProcessing there is no OPC, so it says nothing is wired.","zh":"一行印一組控制和它推動的 OPC 變數。在 OpenProcessing 以外沒有 OPC，所以會說沒有接上任何東西。"},"run":"sketch","lang":"js"}],"related":["option:opc","knob1","knob","pad1","OAVp5.check"],"keywords":{"en":["wiring","opc","op configurator","which knob drives what","map controller to sliders"],"zh":["接線","opc","op configurator","哪顆旋鈕推動什麼","控制器對應滑桿"]},"tbd":[{"en":"Not tried with OpenProcessing's real OPC library: it was checked against a stand-in object with the same `options` shape (`{ type: 'slider', name, min, max }`). Outside OpenProcessing the result is an empty array.","zh":"沒有用 OpenProcessing 真正的 OPC 函式庫試過：是拿一個有同樣 `options` 結構（`{ type: 'slider', name, min, max }`）的替身物件檢查的。在 OpenProcessing 以外，結果是空陣列。"}],"claims":[{"expr":"OAVp5.wiring().length","equals":0},{"before":"window.OPC = { options: { radius: { type: 'slider', name: 'radius', min: 0, max: 100, value: 10 }, go: { type: 'toggle', name: 'go', value: false } }, set() {} }","expr":"JSON.stringify(OAVp5.wiring())","equals":"[{\"control\":\"knob1\",\"name\":\"radius\"},{\"control\":\"pad1\",\"name\":\"go\"}]"}]},
{"id":"noteFreq","key":"noteFreq","name":"noteFreq","aliases":[],"kind":"function","package":"oav-p5","category":"notes","documented":true,"signature":"noteFreq(v)","source":{"file":"p5/oav-p5.js","line":244,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L244"},"summary":{"en":"Returns the frequency in Hz of a note: `noteFreq('A4')` is 440.","zh":"回傳音符的頻率（赫茲）：`noteFreq('A4')` 是 440。"},"description":[{"en":"For a sketch that makes its own sound (p5.sound, Web Audio): `osc.freq(noteFreq(note))`. If you just want the sound, [`startSound()`](#startSound) plays every key for you.","zh":"給自己發聲的草圖用（p5.sound、Web Audio）：`osc.freq(noteFreq(note))`。如果只是想要有聲音，[`startSound()`](#startSound) 會幫你把每個鍵都彈出來。"}],"params":[{"name":"v","type":"Number | String","en":"A note name or a MIDI number. A number may be fractional (69.5 is a quarter tone above A4) and is not held to 0..127.","zh":"音名或 MIDI 號碼。數字可以有小數（69.5 是比 A4 高四分之一音），而且不會被限制在 0..127。"}],"returns":{"type":"Number","en":"Hz, equal temperament with A4 = 69 = 440 Hz.","zh":"赫茲，十二平均律，A4 = 69 = 440 Hz。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(36);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('A4 = ' + noteFreq('A4') + ' Hz\\nC4 = ' + noteFreq('C4').toFixed(2) + ' Hz', width / 2, height / 2);\n}","caption":{"en":"Prints 440 Hz and 261.63 Hz.","zh":"印出 440 Hz 和 261.63 Hz。"},"run":"sketch","lang":"js"}],"related":["noteNumber","noteName","startSound"],"keywords":{"en":["frequency","hz","pitch","tuning","A440","oscillator"],"zh":["頻率","赫茲","音高","調音","振盪器"]},"tbd":[],"claims":[{"expr":"noteFreq('A4')","equals":440},{"expr":"noteFreq(69)","equals":440},{"expr":"noteFreq(81)","equals":880}]},
{"id":"noteName","key":"noteName","name":"noteName","aliases":[],"kind":"function","package":"oav-p5","category":"notes","documented":true,"signature":"noteName(v, [o])","source":{"file":"p5/oav-p5.js","line":239,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L239"},"summary":{"en":"Turns a MIDI number into a note name: `noteName(61)` is `'C#4'`.","zh":"把 MIDI 號碼轉成音名：`noteName(61)` 是 `'C#4'`。"},"description":[{"en":"[`notePressed(note, velocity)`](#notePressed) hands you the number; `noteName(note)` writes it back as a name for the screen or a lookup table. The same C4-is-60 convention as [`noteNumber()`](#noteNumber).","zh":"[`notePressed(note, velocity)`](#notePressed) 給你的是號碼。`noteName(note)` 把它寫回音名，拿去顯示在畫面上或查表。跟 [`noteNumber()`](#noteNumber) 一樣，C4 就是 60。"}],"params":[{"name":"v","type":"Number | String","en":"A MIDI number (or a name, which is normalised).","zh":"MIDI 號碼（給音名的話會被整理成標準寫法）。"},{"name":"o","type":"Object","optional":true,"en":"Options: `{ flats: true }` writes `'Db4'` instead of `'C#4'`; `{ octave: false }` drops the octave number (`'C#'`).","zh":"選項：`{ flats: true }` 寫成 `'Db4'` 而不是 `'C#4'`。`{ octave: false }` 不寫八度數字（`'C#'`）。"}],"returns":{"type":"String","en":"The name, sharps by default: `noteName(60)` is `'C4'`.","zh":"音名，預設用升記號：`noteName(60)` 是 `'C4'`。"},"notes":[],"examples":[{"code":"let shown = 'play a key';\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(64);\n}\n\nfunction notePressed(note) {\n  shown = noteName(note) + '  ' + noteName(note, { flats: true });\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(shown, width / 2, height / 2);\n}","caption":{"en":"Shows each key twice, with sharps and with flats (they differ on black keys). Open 🎹 and play.","zh":"每個鍵用升記號和降記號各寫一次（黑鍵兩種寫法不同）。打開 🎹 彈彈看。"},"run":"sketch","lang":"js"}],"related":["noteNumber","noteFreq","notePressed","note"],"keywords":{"en":["note name","midi number","C4","convert","sharp","flat","display"],"zh":["音名","midi 號碼","轉換","升記號","降記號"]},"tbd":[],"claims":[{"expr":"noteName(61)","equals":"C#4"},{"expr":"noteName(61, { flats: true })","equals":"Db4"},{"expr":"noteName(61, { octave: false })","equals":"C#"}]},
{"id":"noteNumber","key":"noteNumber","name":"noteNumber","aliases":[],"kind":"function","package":"oav-p5","category":"notes","documented":true,"signature":"noteNumber(v)","source":{"file":"p5/oav-p5.js","line":225,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L225"},"summary":{"en":"Turns a note name like `'C4'` into its MIDI number, 0 to 127.","zh":"把像 `'C4'` 這樣的音名轉成 MIDI 號碼，0 到 127。"},"description":[{"en":"A name is a letter A to G (either case), an optional `#` or `b` (`♯` and `♭` too), and the octave: `C4`, `c#4`, `Db3`, `C-1` (0) up to `G9` (127). **C4 is 60.** Anything else, such as `'H4'` or `'C-2'`, throws an error, and a card beside the 🎹 says how to write a note.","zh":"音名由一個 A 到 G 的字母（大小寫都可以）、可有可無的 `#` 或 `b`（`♯`、`♭` 也行），加上八度數字組成：`C4`、`c#4`、`Db3`，從 `C-1`（0）到 `G9`（127）。**C4 是 60。** 其他寫法（像 `'H4'`、`'C-2'`）會丟出錯誤，🎹 旁邊也會有一張卡片告訴你音名怎麼寫。"},{"en":"You rarely call it yourself: every place that takes a note number also takes a name, such as [`noteIsDown()`](#noteIsDown) and [`midi.play()`](#OAVp5.play).","zh":"通常不用自己呼叫：凡是收音符號碼的地方也都收音名，像 [`noteIsDown()`](#noteIsDown) 和 [`midi.play()`](#OAVp5.play)。"}],"params":[{"name":"v","type":"String | Number","en":"A note name (`'C4'`, `'c#4'`, `'Db3'`, `'F♯2'`) or a number. A number comes back rounded and held to 0..127.","zh":"音名（`'C4'`、`'c#4'`、`'Db3'`、`'F♯2'`）或數字。數字會四捨五入並限制在 0..127。"}],"returns":{"type":"Number","en":"The MIDI number: `noteNumber('C4')` is 60, `noteNumber('A4')` is 69.","zh":"MIDI 號碼：`noteNumber('C4')` 是 60，`noteNumber('A4')` 是 69。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(40);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('C4 = ' + noteNumber('C4') + '\\nA4 = ' + noteNumber('A4') + '\\nF#2 = ' + noteNumber('F#2'), width / 2, height / 2);\n}","caption":{"en":"Prints 60, 69 and 42.","zh":"印出 60、69、42。"},"run":"sketch","lang":"js"}],"related":["noteName","noteFreq","noteIsDown"],"keywords":{"en":["note name","midi number","C4","convert","pitch","sharp","flat"],"zh":["音名","midi 號碼","轉換","音高","升記號","降記號"]},"tbd":[],"claims":[{"expr":"noteNumber('C4')","equals":60},{"expr":"noteNumber('A4')","equals":69},{"expr":"noteNumber('Db3')","equals":49}]},
{"id":"bass","key":"bass","name":"bass","aliases":["mid","treble"],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"bass","source":{"file":"p5/oav-p5.js","line":1017,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1017"},"summary":{"en":"How much of the microphone's sound is low (`bass`), middle (`mid`) or high (`treble`), each from 0 to 1.","zh":"麥克風聽到的聲音裡，低音（`bass`）、中音（`mid`）、高音（`treble`）各佔多少，各自從 0 到 1。"},"description":[{"en":"Use them like [`micLevel`](#micLevel), one number per band: `fill(200 + bass * 160, 70, 100)` pushes the colour with the low end. The three share one entry because they behave the same; only the frequency range differs.","zh":"用法跟 [`micLevel`](#micLevel) 一樣，每個頻段一個數字：`fill(200 + bass * 160, 70, 100)` 讓顏色跟著低音走。三個名稱共用這一條，因為它們的行為完全相同，只有頻率範圍不同。"},{"en":"Unlike `micLevel` they are not measured in decibels: each is the average strength of the frequencies in its band, scaled by [`micSensitivity()`](#micSensitivity) and held to 1. A band that is faint in your room can stay close to 0; raise the sensitivity rather than multiplying by a large number.","zh":"跟 `micLevel` 不一樣，它們不是用分貝量的：每一個是該頻段所有頻率的平均強度，乘上 [`micSensitivity()`](#micSensitivity) 之後最多算到 1。如果某個頻段在你的房間裡很微弱，數字會一直貼近 0。這時調高靈敏度，比自己乘一個很大的數字好。"}],"value":{"type":"Number","range":"0..1","en":"The energy in 20–250 Hz (`bass`), 250–2000 Hz (`mid`) and 2000–8000 Hz (`treble`). Updated once per display frame while the microphone is on; 0 otherwise.","zh":"`bass` 是 20–250 Hz 的能量，`mid` 是 250–2000 Hz，`treble` 是 2000–8000 Hz。麥克風開著時每個畫面更新一次，其他時候是 0。"},"params":[],"needs":{"en":"Reading any of them asks for the microphone the first time, exactly like [`micLevel`](#micLevel); until you allow it they are `0`.","zh":"第一次讀其中任何一個，就會跟 [`micLevel`](#micLevel) 一樣去要麥克風。在你允許之前都是 `0`。"},"notes":[{"en":"A sketch that already has its own variable, function or library called `bass`, `mid` or `treble` keeps it: oav-p5 leaves that name alone and never starts the microphone for it. Assigning one later turns the name back into a plain variable.","zh":"如果草圖本來就有自己的 `bass`、`mid` 或 `treble`（變數、函式、函式庫都算），oav-p5 就不動那個名稱，也不會為它去開麥克風。之後自己指定值的話，那個名稱就變回一般變數。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  colorMode(HSB, 360, 100, 100);\n}\n\nfunction draw() {\n  background(230, 40, 10);\n  fill(200 + bass * 160, 70, 100);   // low sounds push the colour toward magenta\n  circle(width / 2, height / 2, 80 + bass * 220);\n}","caption":{"en":"A circle that swells and changes colour with the low end: tap the desk or play something with a kick.","zh":"一個隨低音變大、變色的圓：敲桌子或放一段有大鼓的音樂。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const bands = [bass, mid, treble];\n  for (let i = 0; i < 3; i++) {\n    fill(255, 80 + i * 70, 120);\n    rect(60 + i * 110, height - 20, 80, -20 - bands[i] * 240);\n  }\n}","caption":{"en":"Three bars, one per band, from low to high. Whistle for the right one, hum for the left.","zh":"三根長條，依序是低、中、高音。吹口哨看右邊那根，哼低音看左邊那根。"},"run":"sketch","lang":"js"}],"related":["micLevel","beat","micSensitivity","kickDetected"],"keywords":{"en":["bass","mid","treble","low","high","frequency","band","spectrum","equalizer","fft"],"zh":["低音","中音","高音","頻段","頻率","頻譜","麥克風"]},"tbd":[{"en":"Typical values for speech and for music depend on the microphone and the room; no typical numbers are given here until they have been measured.","zh":"說話和音樂的典型數值要看麥克風和房間。在量過之前，這裡不給典型數字。"}],"claims":[{"expr":"bass","min":0,"max":1,"needs":"mic"},{"expr":"mid","min":0,"max":1,"needs":"mic"},{"expr":"treble","min":0,"max":1,"needs":"mic"}]},
{"id":"beat","key":"beat","name":"beat","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"beat","source":{"file":"p5/oav-p5.js","line":1020,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1020"},"summary":{"en":"True for one frame each time the microphone (or the drum machine) hears a beat.","zh":"麥克風（或鼓機）每聽到一個拍子，就在那一個畫面是 true。"},"description":[{"en":"It behaves like `mouseIsPressed` while a button is down, except the \"press\" lasts one frame: `if (beat) flash = 1;` is all it takes to react to the music. A beat is a jump in loudness over what the room has been doing, with at least 100 ms between two beats; two beats between two frames count as one.","zh":"它的用法像按著滑鼠鍵時的 `mouseIsPressed`，只是「按下」只持續一個畫面：`if (beat) flash = 1;` 就能對音樂做出反應。一個拍子是音量比房間一直以來的聲音突然跳高，兩個拍子之間至少隔 100 毫秒。兩個畫面之間發生的兩個拍子只算一次。"},{"en":"When the drum machine is playing, `beat` is also true for a frame on every kick and snare, so a sketch written for the microphone moves over to the machine unchanged.","zh":"鼓機在播的時候，每一下大鼓和小鼓也會讓 `beat` 在那個畫面是 true，所以為麥克風寫的草圖不用改就能換成鼓機。"}],"value":{"type":"Boolean","en":"True in the first `draw()` after a beat, and false until the next one. Reading it twice in the same frame gives the same answer.","zh":"拍子之後的第一個 `draw()` 是 true，到下一個拍子之前都是 false。同一個畫面裡讀兩次，答案一樣。"},"params":[],"needs":{"en":"Reading `beat` asks for the microphone the first time, like [`micLevel`](#micLevel). If you would rather be called, define [`beatDetected()`](#beatDetected).","zh":"第一次讀 `beat` 就會跟 [`micLevel`](#micLevel) 一樣去要麥克風。如果你比較想被呼叫，就定義 [`beatDetected()`](#beatDetected)。"},"notes":[],"examples":[{"code":"let flash = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  if (beat) flash = 1;        // true for exactly one frame per beat\n  background(20 + flash * 200);\n  flash *= 0.9;               // fade back down\n}","caption":{"en":"The screen flashes on every beat and fades out. Clap or tap the desk.","zh":"每個拍子畫面閃一下再慢慢暗下去。拍手或敲桌子試試。"},"run":"sketch","lang":"js"},{"code":"let count = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(96);\n}\n\nfunction draw() {\n  if (beat) count++;\n  background(20);\n  fill(255);\n  text(count, width / 2, height / 2);\n}","caption":{"en":"A counter that goes up by one per beat, never twice for the same one.","zh":"每個拍子加一的計數器，同一個拍子不會算兩次。"},"run":"sketch","lang":"js"}],"related":["beatDetected","micLevel","kickDetected","startDrums"],"keywords":{"en":["beat","onset","tempo","rhythm","clap","trigger","transient"],"zh":["拍子","節拍","節奏","鼓點","觸發","麥克風"]},"tbd":[],"claims":[]},
{"id":"beatDetected","key":"beatDetected","name":"beatDetected","aliases":[],"kind":"event","package":"oav-p5","category":"mic","documented":true,"signature":"function beatDetected(level) {}","source":{"file":"p5/oav-p5.js","line":865,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L865"},"summary":{"en":"You define this function; oav-p5 calls it each time the microphone (or the drum machine) hears a beat.","zh":"你來定義這個函式。麥克風（或鼓機）每聽到一個拍子，oav-p5 就呼叫它一次。"},"description":[{"en":"Declare it the way you declare `keyPressed()`:\n\n```js\nfunction beatDetected(level) {\n  // flash, spawn, jump to the next scene\n}\n```\n\nIt is called once per beat, right when the beat lands, not once per frame. Use [`beat`](#beat) instead if you would rather ask each frame.","zh":"用宣告 `keyPressed()` 的方式宣告它：\n\n```js\nfunction beatDetected(level) {\n  // 閃一下、生出東西、跳到下一個場景\n}\n```\n\n每個拍子呼叫一次，在拍子落下的當下，不是每個畫面一次。如果你比較想每個畫面自己問，改用 [`beat`](#beat)。"}],"params":[{"name":"level","type":"Number","en":"How loud it was at that moment, 0..1 (the value of [`micLevel`](#micLevel)).","zh":"那一刻有多大聲，0..1（就是 [`micLevel`](#micLevel) 的值）。"}],"needs":{"en":"A sketch that defines `beatDetected()` and reads no microphone value starts the microphone on the first click or key. When the drum machine is on, the same function is called for its kicks and snares and the microphone is not opened for it. `OAVp5.mic.on('beat', fn)` hears the same beats without defining a global and returns a function that stops listening, but a listener alone never starts the microphone: read [`micLevel`](#micLevel) or call [`startMic()`](#startMic) as well.","zh":"草圖只要定義了 `beatDetected()` 而沒有讀任何麥克風的值，第一次點擊或按鍵時就會啟動麥克風。鼓機開著的時候，鼓機的大鼓和小鼓也會呼叫同一個函式，而且不會為它去開麥克風。`OAVp5.mic.on('beat', fn)` 聽到的是同樣的拍子，不用定義全域函式，也會回傳一個停止監聽的函式，但只有監聽並不會啟動麥克風：還要讀 [`micLevel`](#micLevel) 或呼叫 [`startMic()`](#startMic)。"},"notes":[{"en":"If your function throws, the sketch keeps running: the error appears in the console and as a card beside the 🎹 with the file and line.","zh":"你的函式如果出錯，草圖會繼續跑：錯誤會出現在 console，也會在 🎹 旁邊的卡片上顯示檔案和行號。"}],"examples":[{"code":"let flash = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction beatDetected(level) {\n  flash = 0.4 + level * 0.6;   // louder beat, brighter flash\n}\n\nfunction draw() {\n  background(20 + flash * 200);\n  flash *= 0.9;\n}","caption":{"en":"A flash per beat, brighter for a louder one.","zh":"每個拍子閃一下，拍子越大聲閃得越亮。"},"run":"sketch","lang":"js"}],"related":["beat","kickDetected","micLevel","startDrums"],"keywords":{"en":["beat","callback","event","onset","rhythm","clap"],"zh":["拍子","事件","回呼","節拍","節奏","麥克風"]},"tbd":[],"claims":[]},
{"id":"kickDetected","key":"kickDetected","name":"kickDetected","aliases":["snareDetected","hatDetected"],"kind":"event","package":"oav-p5","category":"mic","documented":true,"signature":"function kickDetected(level) {}","source":{"file":"p5/oav-p5.js","line":865,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L865"},"summary":{"en":"You define these; oav-p5 calls them when the microphone (or the drum machine) hears a kick, a snare or a hat.","zh":"你來定義這幾個函式。麥克風（或鼓機）聽到大鼓、小鼓或鈸的時候，oav-p5 會呼叫 `kickDetected(level)`、`snareDetected(level)`、`hatDetected(level)`。"},"description":[{"en":"The microphone splits what it hears into three drum-like bands and calls the matching function when each band jumps: 20–120 Hz for the kick, 150–800 Hz for the snare, 6–14 kHz for the hat. There is no machine learning in it, just a transient in a frequency band, so a bass voice can trigger a kick.","zh":"麥克風把聽到的聲音分成三個像鼓的頻段，哪個頻段突然跳高，就呼叫對應的函式：大鼓是 20–120 Hz，小鼓是 150–800 Hz，鈸是 6–14 kHz。裡面沒有機器學習，只是某個頻段出現瞬間的衝擊，所以低沉的人聲也可能觸發大鼓。"},{"en":"When the drum machine plays, each of its hits calls the same functions: kick for the kick, snare for a snare, clap or rim, hat for a closed, open or pedal hat. A sketch written for the microphone therefore works with [`startDrums()`](#startDrums) unchanged.","zh":"鼓機在播的時候，它的每一下也會呼叫同樣的函式：大鼓呼叫 kick，小鼓、拍手或 rim 呼叫 snare，閉合、開放或腳踏的鈸呼叫 hat。所以為麥克風寫的草圖，不用改就能搭配 [`startDrums()`](#startDrums)。"},{"en":"While the drum machine plays, only its hits call these functions (from oav-p5 0.8.1). A microphone that is on at the same time mostly hears the drums coming out of the speakers, so its hits would call the same function twice and hear a kick as a snare. [`micLevel`](#micLevel), [`bass`](#bass), `mid` and `treble` still follow the microphone; the microphone's hits come back when the drums stop.","zh":"鼓機在打的時候，只有鼓機的每一下會呼叫這幾個函式（oav-p5 0.8.1 起）。這時候如果麥克風也開著，它聽到的多半是喇叭放出來的鼓聲，同一下會被叫兩次，大鼓也可能被聽成小鼓。[`micLevel`](#micLevel)、[`bass`](#bass)、`mid`、`treble` 照樣跟著麥克風。鼓機停下來，麥克風的打擊就會回來。"}],"params":[{"name":"level","type":"Number","en":"How strong the hit was, 0..1 (for the drum machine, its velocity).","zh":"這一下有多強，0..1（鼓機的話就是力道）。"}],"needs":{"en":"Defining one of them is not enough to open the microphone once the drum machine has started; reading [`micLevel`](#micLevel) (or calling [`startMic()`](#startMic)) is. Without the drum machine, a sketch that defines any of them and reads nothing starts the microphone on the first click or key.","zh":"鼓機啟動之後，只定義其中一個函式並不會去開麥克風。要讀 [`micLevel`](#micLevel)（或呼叫 [`startMic()`](#startMic)）才會。沒有鼓機的時候，草圖只要定義了其中任何一個、又沒有讀任何值，第一次點擊或按鍵時會啟動麥克風。"},"notes":[],"examples":[{"code":"let kick = 0, snare = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction kickDetected(level) { kick = level; }\nfunction snareDetected(level) { snare = level; }\n\nfunction draw() {\n  background(20);\n  fill(255, 90, 31);\n  circle(120, 150, 40 + kick * 160);\n  fill(120, 190, 255);\n  circle(280, 150, 40 + snare * 160);\n  kick *= 0.9;\n  snare *= 0.9;\n}","caption":{"en":"Two circles, orange for kicks and blue for snares, each swelling and shrinking back on its own hits.","zh":"兩個圓，橘色對應大鼓、藍色對應小鼓，各自在自己的聲音出現時脹大再縮回去。"},"run":"sketch","lang":"js"}],"related":["beatDetected","beat","bass","startDrums"],"keywords":{"en":["kick","snare","hat","drum","callback","event","percussion"],"zh":["大鼓","小鼓","鈸","鼓","事件","回呼","打擊","麥克風"]},"tbd":[],"claims":[]},
{"id":"kickIsDetected","key":"kickIsDetected","name":"kickIsDetected","aliases":["snareIsDetected","hatIsDetected"],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"kickIsDetected","source":{"file":"p5/oav-p5.js","line":1044,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1044"},"summary":{"en":"True in the one frame after a kick (snare, hat) is heard, from the drum machine or the microphone, like `mouseIsPressed` for a hit.","zh":"聽到一下大鼓（小鼓、鈸）之後的那一格是 true，鼓機或麥克風都算。就像打擊版的 `mouseIsPressed`。"},"description":[{"en":"The same hits that call [`kickDetected()`](#kickDetected), `snareDetected()` and `hatDetected()`, as a value you read in `draw()` instead of a function you write. Hand it to [`lag()`](#lag) and it becomes a number that jumps to 1 on the hit and fades: `lag(kickIsDetected, 0, 0.3)` is 1 on a kick and back near 0 about 0.3 s later, ready for an alpha or a size. `lag()` takes `true` as 1.","zh":"跟呼叫 [`kickDetected()`](#kickDetected)、`snareDetected()`、`hatDetected()` 的是同一批打擊，只是改成在 `draw()` 裡讀的值，不用寫函式。交給 [`lag()`](#lag) 就變成一個數字：打到時跳到 1，然後慢慢退回去。`lag(kickIsDetected, 0, 0.3)` 在大鼓那一下是 1，大約 0.3 秒後回到 0 附近，可以直接拿來控制透明度或大小。`lag()` 把 `true` 當成 1。"},{"en":"While the drum machine plays, only its hits count. Reading these does not ask for the microphone by itself: with no drums, it counts as wanting the microphone's hits, the way defining `kickDetected()` does, so the first click on the page opens it.","zh":"鼓機在打的時候只算鼓機的。讀這幾個名字本身不會去要麥克風。沒有鼓機的時候，它就跟定義 `kickDetected()` 一樣，算是想要麥克風的打擊，所以第一次點畫面會開麥克風。"}],"value":{"type":"Boolean","en":"True for exactly one p5 frame per hit, however often that frame reads it; false otherwise. Since oav-p5 0.8.2.","zh":"每一下只在一個 p5 畫面裡是 true，那一格讀幾次都一樣。其他時候是 false。oav-p5 0.8.2 起。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, ~ sd ~ sd', 110);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  fill(255, 90, 31, 255 * lag(kickIsDetected, 0, 0.3));    // a kick lights it, then it fades\n  circle(130, 200, 140);\n  fill(120, 190, 255, 255 * lag(snareIsDetected, 0, 0.3));\n  circle(270, 200, 140);\n}","caption":{"en":"The orange circle flashes on every kick and fades out, the blue one on every snare.","zh":"橘色的圓每一下大鼓亮起來再淡掉，藍色的圓跟著小鼓。"},"run":"sketch","lang":"js"}],"related":["kickDetected","beat","lastKickTs","lag"],"keywords":{"en":["kick","snare","hat","is detected","this frame","flash","trigger","boolean"],"zh":["大鼓","小鼓","鈸","這一格","閃一下","觸發","瞬間"]},"tbd":[],"claims":[{"before":"OAVp5.drums._hit('kick', 0.9)","expr":"kickIsDetected && !snareIsDetected","equals":true}]},
{"id":"lastKickFrameCount","key":"lastKickFrameCount","name":"lastKickFrameCount","aliases":["lastSnareFrameCount","lastHatFrameCount","lastBeatFrameCount"],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"lastKickFrameCount","source":{"file":"p5/oav-p5.js","line":333,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L333"},"summary":{"en":"The `frameCount` of the frame that saw the last kick (snare, hat, beat): `frameCount - lastKickFrameCount` is how many frames ago.","zh":"看到上一下大鼓（小鼓、鈸、拍子）的那一格的 `frameCount`：`frameCount - lastKickFrameCount` 就是幾格以前。"},"description":[{"en":"The frame-counting twin of [`lastKickTs`](#lastKickTs). `frameCount - lastKickFrameCount` is 0 in the frame where [`kickIsDetected`](#kickIsDetected) is true, then 1, 2, 3 …, so a shape can last a fixed number of frames after each hit. Use the `Ts` version when it should last a fixed time whatever the frame rate.","zh":"[`lastKickTs`](#lastKickTs) 的「數格子」版本。在 [`kickIsDetected`](#kickIsDetected) 是 true 的那一格，`frameCount - lastKickFrameCount` 是 0，接下來是 1、2、3……，所以可以讓一個形狀在每一下之後固定留幾格。想要不管幀率都留固定秒數，就用 `Ts` 那一組。"}],"value":{"type":"Number","en":"A frame number, on the same count as `frameCount`; `-Infinity` before the first hit. Since oav-p5 0.8.3.","zh":"畫面的編號，跟 `frameCount` 同一套數法。第一下之前是 `-Infinity`。oav-p5 0.8.3 起。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, ~ sd ~ sd', 110);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const k = frameCount - lastKickFrameCount;    // frames since the kick\n  if (k < 10) {                                 // the ring stays for 10 frames after each kick\n    fill(255, 90, 31);\n    circle(200, 200, 60 + k * 16);\n  }\n}","caption":{"en":"After every kick a circle shows for ten frames, growing each frame.","zh":"每一下大鼓之後，圓會出現 10 格，每一格變大一點。"},"run":"sketch","lang":"js"}],"related":["lastKickTs","kickIsDetected","lastNoteFrameCount"],"keywords":{"en":["last kick","frameCount","frames since","frames ago","last snare","last hat","last beat"],"zh":["上一下","幾格以前","畫面數","frameCount","大鼓","小鼓","鈸","拍子"]},"tbd":[],"claims":[{"before":"OAVp5.drums._hit('kick', 0.9)","expr":"lastKickFrameCount > -Infinity && lastSnareFrameCount === -Infinity","equals":true}]},
{"id":"lastKickTs","key":"lastKickTs","name":"lastKickTs","aliases":["lastSnareTs","lastHatTs","lastBeatTs"],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"lastKickTs","source":{"file":"p5/oav-p5.js","line":330,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L330"},"summary":{"en":"When the last kick (snare, hat, beat) came, on the `millis()` clock: `millis() - lastKickTs` is how many ms ago.","zh":"上一下大鼓（小鼓、鈸、拍子）是什麼時候來的，用 `millis()` 的時鐘：`millis() - lastKickTs` 就是幾毫秒前。"},"description":[{"en":"Set by every hit that calls [`kickDetected()`](#kickDetected), `snareDetected()`, `hatDetected()` or [`beatDetected()`](#beatDetected): the drum machine's, or the microphone's when no drums play. Use it for anything that depends on how long ago: `exp(-(millis() - lastKickTs) / 200)` is 1 on the kick and fades with a 0.2 s time constant.","zh":"每一下會呼叫 [`kickDetected()`](#kickDetected)、`snareDetected()`、`hatDetected()` 或 [`beatDetected()`](#beatDetected) 的打擊都會更新它：鼓機的，或沒有鼓機時麥克風的。要算「多久以前」就用它，例如 `exp(-(millis() - lastKickTs) / 200)` 在大鼓那一下是 1，然後以 0.2 秒的時間常數淡掉。"}],"value":{"type":"Number","en":"Milliseconds on the same clock as `millis()`; `-Infinity` before the first hit, so `millis() - lastKickTs` is then `Infinity`. Since oav-p5 0.8.2.","zh":"毫秒，跟 `millis()` 同一個時鐘。第一下之前是 `-Infinity`，所以那時 `millis() - lastKickTs` 是 `Infinity`。oav-p5 0.8.2 起。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, hh*8', 100);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const ago = millis() - lastKickTs;    // ms since the last kick\n  fill(255, 90, 31);\n  circle(200, 200, 40 + 160 * exp(-ago / 200));\n  fill(255);\n  text(isFinite(ago) ? round(ago) + ' ms since the kick' : 'no kick yet', 20, 290);\n}","caption":{"en":"The circle swells on each kick and shrinks back; the line says how many ms ago the kick was.","zh":"每一下大鼓圓就鼓起來再縮回去，下面那行字寫離上一下大鼓幾毫秒。"},"run":"sketch","lang":"js"}],"related":["kickIsDetected","kickDetected","lastNoteTs"],"keywords":{"en":["last kick","timestamp","time since","millis","ago","last snare","last hat","last beat"],"zh":["上一下","時間","多久以前","毫秒","大鼓","小鼓","鈸","拍子"]},"tbd":[],"claims":[{"before":"OAVp5.drums._hit('kick', 0.9)","expr":"lastKickTs > -Infinity && lastSnareTs === -Infinity","equals":true}]},
{"id":"micIsOn","key":"micIsOn","name":"micIsOn","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"micIsOn","source":{"file":"p5/oav-p5.js","line":326,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L326"},"summary":{"en":"Whether the microphone is running right now.","zh":"麥克風現在有沒有在運作。"},"description":[{"en":"Use it to show a hint until sound arrives: `if (!micIsOn) text(\"Allow the microphone\", 20, 30);`. Reading it never starts the microphone (unlike [`micLevel`](#micLevel)), so it is safe to check anywhere. The reason behind a `false` is in `OAVp5.mic.state` (`off`, `asking`, `on`, `denied` or `unavailable`) and, when something failed, `OAVp5.mic.error`.","zh":"可以用它在聲音進來之前顯示提示：`if (!micIsOn) text(\"請允許麥克風\", 20, 30);`。讀它不會啟動麥克風（跟 [`micLevel`](#micLevel) 不一樣），所以在哪裡檢查都安全。`false` 的原因看 `OAVp5.mic.state`（`off`、`asking`、`on`、`denied` 或 `unavailable`），失敗的時候還有 `OAVp5.mic.error`。"}],"value":{"type":"Boolean","en":"True once the browser has said yes and sound is coming in; false while the microphone is off, still asking, blocked or missing.","zh":"瀏覽器答應了、聲音進來之後是 true。麥克風關著、還在詢問、被擋下或不存在的時候是 false。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(micIsOn ? 20 : 90);\n  fill(255);\n  textAlign(CENTER, CENTER);\n  text(micIsOn ? 'listening' : 'allow the microphone', width / 2, height / 2);\n}","caption":{"en":"Grey with a prompt until the microphone is on, then dark. (Nothing here reads micLevel, so nothing asks for the microphone: press 🎤 in the 🎹 panel.)","zh":"麥克風開起來之前是灰色加提示，開了之後變暗。（這裡沒有讀 micLevel，所以不會自己去要麥克風：按 🎹 面板裡的 🎤。）"},"run":"sketch","lang":"js"}],"related":["micLevel","startMic","stopMic"],"keywords":{"en":["microphone","mic","on","status","permission","allowed"],"zh":["麥克風","開著","狀態","權限","允許"]},"tbd":[],"claims":[]},
{"id":"micLevel","key":"micLevel","name":"micLevel","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"micLevel","source":{"file":"p5/oav-p5.js","line":1016,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1016"},"summary":{"en":"How loud the microphone hears things, from 0 (a quiet room) to 1 (shouting).","zh":"麥克風聽到的聲音有多大聲，從 0（安靜的房間）到 1（大喊）。"},"description":[{"en":"Read it in `draw()` the way you read `mouseX`: `circle(width / 2, height / 2, 50 + micLevel * 300)` grows when someone talks. It is a loudness in decibels, which is how an ear hears it, measured from the room itself: the bottom of the scale follows the room's own noise (it settles within a second or two and rises at most 1.5 dB a second, so a voice never becomes the new silence), and −10 dBFS, shouting into a laptop, is 1.","zh":"像讀 `mouseX` 一樣在 `draw()` 裡直接讀：`circle(width / 2, height / 2, 50 + micLevel * 300)`，有人說話圓就變大。它是以分貝量的音量，跟耳朵聽起來的感覺一致，而且是從房間本身量起：量表的底部跟著房間的底噪走（一兩秒內安頓下來，每秒最多往上爬 1.5 dB，所以人聲不會變成新的安靜），−10 dBFS（對著筆電大喊）就是 1。"},{"en":"In practice a quiet room reads 0, a quiet voice about 0.4 to 0.6, shouting close to 1. If the microphone is quiet or far away, raise it with [`micSensitivity()`](#micSensitivity).","zh":"實際上，安靜的房間是 0，小聲說話約 0.4 到 0.6，大喊接近 1。麥克風很小聲或離得很遠的話，用 [`micSensitivity()`](#micSensitivity) 把它調大。"},{"en":"It follows the browser's animation frames rather than `draw()`, so a sketch running at `frameRate(10)` still reads the latest value, and the number stops changing while the tab is hidden (browsers pause animation frames there).","zh":"它跟著瀏覽器的動畫畫面更新，不是跟著 `draw()`，所以用 `frameRate(10)` 跑的草圖讀到的還是最新的值。分頁被藏起來時數字會停住（瀏覽器在那時會暫停動畫畫面）。"}],"value":{"type":"Number","range":"0..1","en":"Updated once per display frame while the microphone is on. It is 0 while the microphone is off, still asking, blocked or missing.","zh":"麥克風開著的時候，每個畫面更新一次。麥克風關著、還在詢問、被擋下或根本沒有的時候，一直是 0。"},"params":[],"needs":{"en":"Nothing to call. The first time a sketch reads `micLevel` (or [`bass`](#bass), `mid`, `treble`, [`beat`](#beat)), oav-p5 asks the browser for the microphone, and every value stays `0` until you allow it. The page needs `https` or `localhost`. If the browser kept audio quiet until a click, the first click or key starts it. [`?oav_mic=click`](#option-mic) makes only the 🎤 button and [`startMic()`](#startMic) turn it on; `?oav_mic=off` means nothing can.","zh":"不用呼叫任何東西。草圖第一次讀 `micLevel`（或 [`bass`](#bass)、`mid`、`treble`、[`beat`](#beat)）的時候，oav-p5 就會向瀏覽器要麥克風，在你按下允許之前所有的值都是 `0`。網頁要在 `https` 或 `localhost` 底下。如果瀏覽器在你點一下之前不讓聲音出來，第一次點擊或按鍵就會把它啟動。[`?oav_mic=click`](#option-mic) 表示只有 🎤 按鈕和 [`startMic()`](#startMic) 能開。`?oav_mic=off` 表示什麼都開不了。"},"notes":[{"en":"In the 🎹 panel the 🎤 button turns the microphone on and off by hand and shows the level. When the browser blocked it, a card beside the 🎹 says why and what to press: click the lock left of the address, set Microphone to Allow, then click the canvas.","zh":"🎹 面板裡的 🎤 按鈕可以手動開關麥克風，並顯示音量。瀏覽器擋下麥克風的時候，🎹 旁邊會跳出一張卡片說明原因和怎麼處理：點網址列左邊的鎖頭，把麥克風設成允許，再點一下畫布。"},{"en":"On openprocessing.org the sketch frame allows the microphone and MIDI (checked 2026-10-19: its `allow` list contains `microphone` and `midi`), and the first read of `micLevel` there asks for the microphone. An embed code copied from OpenProcessing does not carry that: add `allow=\"microphone\"` to that `<iframe>`.","zh":"在 openprocessing.org 上，草圖的框允許麥克風和 MIDI（2026-10-19 實際檢查過：它的 `allow` 清單有 `microphone` 和 `midi`），而且在那裡第一次讀 `micLevel` 就會去要麥克風。從 OpenProcessing 複製出來的嵌入碼沒有這個設定：要在那個 `<iframe>` 加上 `allow=\"microphone\"`。"},{"en":"The raw numbers behind it are [`OAVp5.mic.rms`](#OAVp5.mic.rms) (0..1), `OAVp5.mic.db` and `OAVp5.mic.floor` (the room's own noise, in dBFS).","zh":"它背後的原始數字是 [`OAVp5.mic.rms`](#OAVp5.mic.rms)（0..1）、`OAVp5.mic.db` 和 `OAVp5.mic.floor`（房間本身的底噪，單位 dBFS）。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2, height / 2, 50 + micLevel * 300);\n}","caption":{"en":"Allow the microphone when the browser asks, then talk: the circle grows with your voice.","zh":"瀏覽器詢問的時候按允許，然後說話：圓會跟著你的聲音變大。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const level = lag(micLevel, 0.03, 0.4);   // opens fast, closes slowly\n  fill(255, 120 + level * 135, 60);\n  circle(width / 2, height / 2, 50 + level * 300);\n}","caption":{"en":"The same circle through lag(): it opens at once when someone speaks and closes slowly, like a VU meter.","zh":"同一個圓，加上 lag()：有人開口就立刻張開，之後慢慢收回去，像音量表。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  micSensitivity(3);   // a quiet or far-away microphone\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  if (!micIsOn) text('Allow the microphone, then make a sound', 20, 30);\n  rect(20, height - 40, micLevel * (width - 40), 20);\n}","caption":{"en":"A bar that fills with the level. micIsOn tells you whether the browser has said yes yet.","zh":"一條隨音量變長的橫條。micIsOn 告訴你瀏覽器是不是已經答應了。"},"run":"sketch","lang":"js"}],"related":["bass","beat","micIsOn","micSensitivity","startMic","lag"],"keywords":{"en":["microphone","mic","volume","loudness","audio","sound level","input level","vu meter","decibel"],"zh":["麥克風","音量","聲音大小","聲音","收音","音量表","分貝"]},"tbd":[{"en":"On openprocessing.org the pasted sketch runs, loads oav-p5 and asks for the microphone, but the sound itself was not tried there: the browser used for the check blocks the microphone, so the allow-and-listen step is still unconfirmed on that site.","zh":"貼進 openprocessing.org 的草圖會跑、會載入 oav-p5、也會去要麥克風，但那裡的聲音本身沒有試：檢查用的瀏覽器擋掉了麥克風，所以在那個網站上「按允許、然後聽到聲音」這一步還沒確認。"}],"claims":[{"expr":"micLevel","min":0,"max":1,"needs":"mic","moves":true}]},
{"id":"micSensitivity","key":"micSensitivity","name":"micSensitivity","aliases":[],"kind":"function","package":"oav-p5","category":"mic","documented":true,"signature":"micSensitivity([v])","source":{"file":"p5/oav-p5.js","line":1047,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1047"},"summary":{"en":"Makes the microphone more or less sensitive: `micSensitivity(2)` is twice as sensitive, `micSensitivity()` says what it is now.","zh":"調整麥克風的靈敏度：`micSensitivity(2)` 是兩倍靈敏，`micSensitivity()` 會告訴你現在是多少。"},"description":[{"en":"The number multiplies what the microphone hears. On [`micLevel`](#micLevel), 2 adds 6 dB; on [`bass`](#bass), `mid` and `treble` it multiplies by 2 (they stay held to 1). Use it for a quiet microphone or a far-away voice: `micSensitivity(3)` in `setup()` adds about 9.5 dB.","zh":"這個數字乘在麥克風聽到的東西上。對 [`micLevel`](#micLevel) 來說，2 等於加 6 dB。對 [`bass`](#bass)、`mid`、`treble` 是乘以 2（最多還是 1）。麥克風很小聲或人離得很遠的時候用它：在 `setup()` 寫 `micSensitivity(3)` 大約加 9.5 dB。"}],"params":[{"name":"v","type":"Number","optional":true,"en":"The new sensitivity, greater than 0 (kept between 0.05 and 20). Leave it out, or pass something that is not greater than 0, to change nothing.","zh":"新的靈敏度，要大於 0（會被限制在 0.05 到 20 之間）。不傳，或傳不大於 0 的值，就什麼都不改。"}],"returns":{"type":"Number","en":"The sensitivity now in effect (1 until you change it).","zh":"現在生效的靈敏度（沒改過就是 1）。"},"notes":[{"en":"`OAVp5.mic.gain` is the same knob under the name oav-p5 0.3.0 used.","zh":"`OAVp5.mic.gain` 是同一個旋鈕，用的是 oav-p5 0.3.0 的舊名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  micSensitivity(3);   // three times as sensitive\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2, height / 2, 50 + micLevel * 300);\n  text('sensitivity ' + micSensitivity(), 20, 30);   // no argument: just reads it\n}","caption":{"en":"The same loudness circle on a microphone that needed a boost; the number shows the sensitivity in effect.","zh":"同樣的音量圓，用在需要加強的麥克風上。數字顯示現在的靈敏度。"},"run":"sketch","lang":"js"}],"related":["micLevel","bass","startMic"],"keywords":{"en":["sensitivity","gain","boost","quiet microphone","volume","threshold"],"zh":["靈敏度","增益","放大","麥克風很小聲","音量"]},"tbd":[],"claims":[]},
{"id":"startMic","key":"startMic","name":"startMic","aliases":[],"kind":"function","package":"oav-p5","category":"mic","documented":true,"signature":"startMic()","source":{"file":"p5/oav-p5.js","line":1045,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1045"},"summary":{"en":"Turns the microphone on and returns a promise that says whether it worked.","zh":"開啟麥克風，並回傳一個 promise，告訴你有沒有成功。"},"description":[{"en":"You rarely need it: reading [`micLevel`](#micLevel) and its friends already starts the microphone. Call it when you want to choose the moment (a button, the end of an intro) or know the answer: `startMic().then((ok) => { … })`. Calling it again while it is on, or while the browser is still asking, returns the same result.","zh":"通常用不到：只要讀 [`micLevel`](#micLevel) 之類的名稱，麥克風就會自己啟動。想自己決定時機（一個按鈕、片頭結束的時候）或想知道結果時才呼叫：`startMic().then((ok) => { … })`。已經開著、或瀏覽器還在詢問的時候再呼叫，會回傳同一個結果。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"Resolves to `true` once the microphone is on, `false` if it was blocked, is missing, or the `mic` option is `off`.","zh":"麥克風開起來之後 resolve 成 `true`。被擋下、不存在、或載入選項 `mic` 是 `off` 的時候 resolve 成 `false`。"},"needs":{"en":"Browsers want a click before audio plays: call it from `mousePressed()` or a button handler if `setup()` is too early. It works whatever the `mic` option is, except `?oav_mic=off`, where nothing can start the microphone.","zh":"瀏覽器要先有一次點擊才讓聲音出來：如果在 `setup()` 裡太早，就放在 `mousePressed()` 或按鈕的處理函式裡呼叫。不管載入選項 `mic` 是什麼都能用，只有 `?oav_mic=off` 例外，那時什麼都開不了麥克風。"},"notes":[{"en":"After [`stopMic()`](#stopMic), only `startMic()` (or 🎤) turns it back on; reading `micLevel` does not.","zh":"呼叫過 [`stopMic()`](#stopMic) 之後，只有 `startMic()`（或 🎤）能再把它打開。讀 `micLevel` 不會。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  startMic().then((ok) => console.log(ok ? 'microphone on' : 'no microphone'));\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(micIsOn ? 'level ' + nf(micLevel, 1, 2) : 'click to start the microphone', width / 2, height / 2);\n}","caption":{"en":"Nothing happens until you click; then the browser asks, and the number starts moving.","zh":"點下去之前什麼都不會發生。點了之後瀏覽器才詢問，數字就開始動。"},"run":"sketch","lang":"js"}],"related":["stopMic","micIsOn","micLevel"],"keywords":{"en":["microphone","start","enable","permission","getUserMedia","promise"],"zh":["麥克風","開啟","啟動","權限","允許"]},"tbd":[],"claims":[]},
{"id":"stopMic","key":"stopMic","name":"stopMic","aliases":[],"kind":"function","package":"oav-p5","category":"mic","documented":true,"signature":"stopMic()","source":{"file":"p5/oav-p5.js","line":1048,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1048"},"summary":{"en":"Turns the microphone off and sets every microphone value back to 0.","zh":"關掉麥克風，所有麥克風的值都歸零。"},"description":[{"en":"It stops the microphone's tracks and closes its audio context, so the browser's \"recording\" indicator goes away. [`micLevel`](#micLevel), [`bass`](#bass), `mid`, `treble` and the rest read `0` again, and [`micIsOn`](#micIsOn) is `false`.","zh":"它會停掉麥克風的音軌並關掉它的音訊環境，所以瀏覽器上「正在錄音」的指示會消失。[`micLevel`](#micLevel)、[`bass`](#bass)、`mid`、`treble` 等等都回到 `0`，[`micIsOn`](#micIsOn) 變成 `false`。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"Resolves to `false`, the new state of the microphone.","zh":"resolve 成 `false`，也就是麥克風的新狀態。"},"notes":[{"en":"After `stopMic()`, reading `micLevel` does not turn the microphone back on: that needs [`startMic()`](#startMic) or the 🎤 button.","zh":"`stopMic()` 之後，讀 `micLevel` 不會再把麥克風打開：要用 [`startMic()`](#startMic) 或 🎤 按鈕。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n}\n\nfunction keyPressed() {\n  if (key === 'm') { if (micIsOn) stopMic(); else startMic(); }\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  circle(width / 2, height / 2, 50 + micLevel * 300);\n  text('press m to turn the microphone ' + (micIsOn ? 'off' : 'on'), width / 2, height - 24);\n}","caption":{"en":"Press m to switch the microphone on and off; the circle drops to its smallest size when it is off.","zh":"按 m 開關麥克風。關掉的時候圓縮到最小。"},"run":"sketch","lang":"js"}],"related":["startMic","micIsOn","micLevel"],"keywords":{"en":["microphone","stop","disable","mute","off"],"zh":["麥克風","關閉","停止","靜音"]},"tbd":[],"claims":[]},
{"id":"OAVp5.mic","key":"OAVp5.mic","name":"OAVp5.mic","aliases":[],"kind":"object","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic","source":{"file":"p5/oav-p5.js","line":882,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L882"},"summary":{"en":"The microphone under the plain names: its state, the numbers of `micLevel` and `bass` that never turn it on when read, the raw loudness, start, stop and events.","zh":"平常那些名稱底下的麥克風：它的狀態、跟 `micLevel` 和 `bass` 一樣的數字（讀它們不會把麥克風打開）、原始的音量，還有 start、stop 和事件。"},"description":[{"en":"Everything the microphone names ([`micLevel`](#micLevel), [`bass`](#bass), [`micIsOn`](#micIsOn), [`startMic()`](#startMic) …) read from here. The difference is that reading `OAVp5.mic.level` never asks for the microphone: the plain `micLevel` does, the first time. So `OAVp5.mic` is for code that must look without waking anything: a status line, a library, a sketch with `?oav_mic=click`.","zh":"所有麥克風的名稱（[`micLevel`](#micLevel)、[`bass`](#bass)、[`micIsOn`](#micIsOn)、[`startMic()`](#startMic) …）讀的都是這裡。差別是讀 `OAVp5.mic.level` 永遠不會去要麥克風，而一般的 `micLevel` 第一次讀就會。所以 `OAVp5.mic` 是給「只想看、不想吵醒任何東西」的程式用的：狀態列、函式庫、設了 `?oav_mic=click` 的草圖。"},{"en":"The members: [`state`](#OAVp5.mic.state), [`error`](#OAVp5.mic.error), [`sensitivity`](#OAVp5.mic.sensitivity) (also `gain`), [`level`](#OAVp5.mic.level), [`bass`](#OAVp5.mic.bass) (also `mid`, `treble`), [`rms`](#OAVp5.mic.rms), [`db`](#OAVp5.mic.db), [`floor`](#OAVp5.mic.floor), [`analyzer`](#OAVp5.mic.analyzer), [`start()`](#OAVp5.mic.start), [`stop()`](#OAVp5.mic.stop) and [`on()`](#OAVp5.mic.on).","zh":"成員有：[`state`](#OAVp5.mic.state)、[`error`](#OAVp5.mic.error)、[`sensitivity`](#OAVp5.mic.sensitivity)（也叫 `gain`）、[`level`](#OAVp5.mic.level)、[`bass`](#OAVp5.mic.bass)（還有 `mid`、`treble`）、[`rms`](#OAVp5.mic.rms)、[`db`](#OAVp5.mic.db)、[`floor`](#OAVp5.mic.floor)、[`analyzer`](#OAVp5.mic.analyzer)、[`start()`](#OAVp5.mic.start)、[`stop()`](#OAVp5.mic.stop) 和 [`on()`](#OAVp5.mic.on)。"}],"params":[],"notes":[{"en":"This is advanced. For most sketches the plain names are all you need.","zh":"這是進階用法。大多數草圖只要用一般的名稱就夠了。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  if (OAVp5.mic.state === 'on') OAVp5.mic.stop(); else OAVp5.mic.start();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('microphone: ' + OAVp5.mic.state + ' (click the canvas to switch it)', 20, 30);   // reading this asks for nothing\n  circle(width / 2, height / 2, 50 + OAVp5.mic.level * 300);\n}","caption":{"en":"A circle that follows the microphone, which only starts when you click: reading OAVp5.mic never turns it on by itself.","zh":"一個跟著麥克風走的圓，要點一下畫布才會開始收音：讀 OAVp5.mic 不會自己把麥克風打開。"},"run":"sketch","lang":"js"}],"related":["micLevel","bass","micIsOn","startMic","stopMic","option:mic"],"keywords":{"en":["mic object","microphone state","read without starting","raw microphone","mic members"],"zh":["麥克風物件","麥克風狀態","讀取但不啟動","原始麥克風","麥克風成員"]},"tbd":[],"claims":[{"expr":"OAVp5.mic.state","equals":"off"}]},
{"id":"OAVp5.mic.analyzer","key":"OAVp5.mic.analyzer","name":"OAVp5.mic.analyzer","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.analyzer","source":{"file":"p5/oav-p5.js","line":871,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L871"},"summary":{"en":"The `AudioAnalyzer` from @openav/audio that listens to the microphone, while the microphone is on; `null` otherwise.","zh":"麥克風開著的時候，負責聽麥克風的那個 @openav/audio 的 `AudioAnalyzer`，其他時候是 `null`。"},"description":[{"en":"A way into the audio itself when the 0 to 1 numbers are not enough: read the spectrum of the microphone with the browser's own `OAVp5.mic.analyzer.analyser.getByteFrequencyData(array)`, or connect your own Web Audio nodes to it. oav-p5 updates the analyzer once per frame, so do not call its `update()` yourself.","zh":"0 到 1 的數字不夠用的時候，直接進到聲音本身的路：用瀏覽器自己的 `OAVp5.mic.analyzer.analyser.getByteFrequencyData(array)` 讀麥克風的頻譜，或把你自己的 Web Audio 節點接上去。oav-p5 每個畫面會更新一次這個分析器，所以不要自己去呼叫它的 `update()`。"}],"value":{"type":"AudioAnalyzer | null","en":"Made when the microphone starts and dropped when it stops. It has its own `AudioContext` (`OAVp5.mic.analyzer.ctx`) and the Web Audio `AnalyserNode` it reads from (`OAVp5.mic.analyzer.analyser`).","zh":"麥克風啟動時產生，停止時丟掉。它有自己的 `AudioContext`（`OAVp5.mic.analyzer.ctx`），也有它讀取的 Web Audio `AnalyserNode`（`OAVp5.mic.analyzer.analyser`）。"},"params":[],"notes":[{"en":"This is advanced. Check it is not `null` first, and read it again after `stopMic()` and `startMic()`: it is a new object each time.","zh":"這是進階用法。先確認它不是 `null`，而且 `stopMic()` 再 `startMic()` 之後要重新讀：每次都是新的物件。"}],"examples":[{"code":"let bins;\n\nfunction setup() {\n  createCanvas(400, 300);\n  noStroke();\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  const a = OAVp5.mic.analyzer;\n  if (!a) return;   // the microphone is not on yet\n  bins = bins || new Uint8Array(a.analyser.frequencyBinCount);\n  a.analyser.getByteFrequencyData(bins);   // the spectrum, 0..255 per frequency bin\n  fill(255, 211, 107);\n  for (let i = 0; i < 64; i++) {\n    const h = bins[i] / 255 * (height - 20);\n    rect(20 + i * 5.8, height - 10 - h, 4, h);\n  }\n}","caption":{"en":"The lowest 64 bins of the microphone's spectrum as bars, read straight from the analyser. Whistle and watch the bars move.","zh":"麥克風頻譜最低的 64 個頻格畫成長條，直接從分析器讀。吹口哨，看長條動起來。"},"run":"sketch","lang":"js"}],"related":["OAVp5.mic.start","OAVp5.mic.state","bass","micLevel"],"keywords":{"en":["analyzer","spectrum","fft","AnalyserNode","audio context","frequency data"],"zh":["分析器","頻譜","fft","AnalyserNode","音訊環境","頻率資料"]},"tbd":[],"claims":[{"expr":"OAVp5.mic.analyzer === null","equals":true}]},
{"id":"OAVp5.mic.bass","key":"OAVp5.mic.bass","name":"OAVp5.mic.bass","aliases":["OAVp5.mic.mid","OAVp5.mic.treble"],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.bass","source":{"file":"p5/oav-p5.js","line":871,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L871"},"summary":{"en":"The same 0 to 1 low, middle and high energy as `bass`, `mid` and `treble`, read without ever turning the microphone on.","zh":"跟 `bass`、`mid`、`treble` 一樣的 0 到 1 低音、中音、高音能量，讀它們不會把麥克風打開。"},"description":[{"en":"The plain [`bass`](#bass), `mid` and `treble` ask for the microphone when first read, and a sketch that already has a variable of that name keeps it. `OAVp5.mic.bass`, `.mid` and `.treble` are the numbers themselves: always there, never starting anything. They are also the answer when the plain names are taken: on strudel.cc, where Strudel owns `bass`, this is what the card tells you to read.","zh":"一般的 [`bass`](#bass)、`mid`、`treble` 第一次被讀的時候會去要麥克風，而且草圖本來就有同名變數的話就由草圖保留。`OAVp5.mic.bass`、`.mid`、`.treble` 就是數字本身：一直都在，也不會啟動任何東西。一般名稱被占用的時候也要讀它們：在 strudel.cc 上 `bass` 是 Strudel 的，卡片會告訴你改讀這個。"}],"value":{"type":"Number","range":"0..1","en":"`bass` is 20–250 Hz, `mid` 250–2000 Hz and `treble` 2000–8000 Hz, scaled by the sensitivity and held to 1. Updated once per display frame while the microphone is on; 0 otherwise.","zh":"`bass` 是 20–250 Hz，`mid` 是 250–2000 Hz，`treble` 是 2000–8000 Hz，乘上靈敏度、最多算到 1。麥克風開著時每個畫面更新一次，其他時候是 0。"},"params":[],"notes":[{"en":"The three share this entry because they behave the same: only the frequency range differs.","zh":"三個名稱共用這一條，因為它們的行為完全相同，只有頻率範圍不同。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  const bands = [OAVp5.mic.bass, OAVp5.mic.mid, OAVp5.mic.treble];\n  for (let i = 0; i < 3; i++) {\n    fill(255, 80 + i * 70, 120);\n    rect(60 + i * 110, height - 20, 80, -20 - bands[i] * 240);\n  }\n}","caption":{"en":"Three bars for low, middle and high, written with the raw members. Whistle for the right one, hum for the left.","zh":"低、中、高音三根長條，用原始的成員寫。吹口哨看右邊那根，哼低音看左邊那根。"},"run":"sketch","lang":"js"}],"related":["bass","OAVp5.mic.level","micSensitivity","startMic"],"keywords":{"en":["mic bass","mic mid","mic treble","frequency bands","low mid high","read without starting"],"zh":["麥克風低音","麥克風中音","麥克風高音","頻段","低中高","讀取但不啟動"]},"tbd":[],"claims":[{"before":"startMic()","expr":"OAVp5.mic.bass","min":0,"max":1,"needs":"mic"},{"before":"startMic()","expr":"OAVp5.mic.treble","min":0,"max":1,"needs":"mic"}]},
{"id":"OAVp5.mic.db","key":"OAVp5.mic.db","name":"OAVp5.mic.db","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.db","source":{"file":"p5/oav-p5.js","line":872,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L872"},"summary":{"en":"The loudness of the microphone in decibels relative to full scale (dBFS): 0 is as loud as it gets, −100 is silence or the microphone being off.","zh":"麥克風的音量，單位是相對滿刻度的分貝（dBFS）：0 是最大聲，−100 是無聲或麥克風關著。"},"description":[{"en":"Decibels are what an ear hears: each 6 dB is twice the amplitude, so a number in dB moves the way loudness feels. [`micLevel`](#micLevel) maps it onto 0 to 1 between the room's noise floor (+6 dB) and −10 dBFS. Use `db` directly when you want your own scale, for example `map(OAVp5.mic.db, -60, -10, 0, 1)`.","zh":"分貝就是耳朵聽到的樣子：每增加 6 dB 就是振幅兩倍，所以用 dB 的數字，變化的方式就跟聽起來的大小聲一致。[`micLevel`](#micLevel) 把它對應到 0 到 1，範圍從房間底噪（再加 6 dB）到 −10 dBFS。想用自己的刻度的話，直接用 `db`，例如 `map(OAVp5.mic.db, -60, -10, 0, 1)`。"}],"value":{"type":"Number","range":"-100..0","en":"`20 * log10(rms)`, measured from [`OAVp5.mic.rms`](#OAVp5.mic.rms). −100 when the level is below about −100 dB and while the microphone is off. Updated once per display frame.","zh":"`20 * log10(rms)`，由 [`OAVp5.mic.rms`](#OAVp5.mic.rms) 算出來。低於約 −100 dB 和麥克風關著的時候是 −100。每個畫面更新一次。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(nf(OAVp5.mic.db, 1, 1) + ' dBFS', 20, 30);\n  const own = constrain(map(OAVp5.mic.db, -60, -10, 0, 1), 0, 1);   // an own scale: -60 dB to -10 dB\n  circle(width / 2, height / 2, 50 + own * 300);\n}","caption":{"en":"The level in dBFS, and a circle on an own scale from −60 to −10 dB. Allow the microphone and talk.","zh":"用 dBFS 表示的音量，加上一個用自己的刻度（−60 到 −10 dB）的圓。允許麥克風，然後說話。"},"run":"sketch","lang":"js"}],"related":["OAVp5.mic.rms","OAVp5.mic.floor","OAVp5.mic.level","micLevel"],"keywords":{"en":["db","decibel","dbfs","loudness in decibels","sound level"],"zh":["分貝","db","dbfs","音量分貝","聲音大小"]},"tbd":[],"claims":[{"before":"startMic()","expr":"OAVp5.mic.db","min":-100,"max":0,"needs":"mic"}]},
{"id":"OAVp5.mic.error","key":"OAVp5.mic.error","name":"OAVp5.mic.error","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.error","source":{"file":"p5/oav-p5.js","line":871,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L871"},"summary":{"en":"Why the microphone did not start, as the browser's error name such as `'NotAllowedError'`; `null` while there is no error.","zh":"麥克風為什麼沒開起來，用瀏覽器的錯誤名稱表示，像 `'NotAllowedError'`，沒有錯誤的時候是 `null`。"},"description":[{"en":"For a sketch that wants to say more than a card does: `if (OAVp5.mic.error === 'NotAllowedError') …`. The card beside the 🎹 already tells the user what to press for each of these, in their language; this is the same fact for your code.","zh":"給想比卡片說得更多的草圖用：`if (OAVp5.mic.error === 'NotAllowedError') …`。🎹 旁邊的卡片已經用使用者的語言，針對每一種告訴他要按什麼了，這是同一件事給你的程式用。"}],"value":{"type":"String | null","en":"`null` at the start and again once the microphone is on. After a failure: the browser's error name (`'NotAllowedError'` or `'SecurityError'` for a refusal, `'NotFoundError'` when there is no microphone, `'NotReadableError'` when another program holds it) or `'no getUserMedia'` on a page that cannot ask.","zh":"一開始是 `null`，麥克風開起來之後又回到 `null`。失敗之後是瀏覽器的錯誤名稱（被拒絕是 `'NotAllowedError'` 或 `'SecurityError'`，沒有麥克風是 `'NotFoundError'`，被別的程式占住是 `'NotReadableError'`），或是在不能詢問的頁面上的 `'no getUserMedia'`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('state: ' + OAVp5.mic.state, 20, 30);\n  text('error: ' + OAVp5.mic.error, 20, 60);   // null while all is well\n}","caption":{"en":"Prints the state and the error. Refuse the microphone to see \"NotAllowedError\".","zh":"印出狀態和錯誤。拒絕麥克風就會看到 \"NotAllowedError\"。"},"run":"sketch","lang":"js"}],"related":["OAVp5.mic.state","startMic","micIsOn"],"keywords":{"en":["mic error","NotAllowedError","why no microphone","permission error","getUserMedia error"],"zh":["麥克風錯誤","NotAllowedError","為什麼沒有麥克風","權限錯誤","getUserMedia 錯誤"]},"tbd":[],"claims":[{"expr":"OAVp5.mic.error === null","equals":true}]},
{"id":"OAVp5.mic.floor","key":"OAVp5.mic.floor","name":"OAVp5.mic.floor","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.floor","source":{"file":"p5/oav-p5.js","line":872,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L872"},"summary":{"en":"The room's own noise as oav-p5 has measured it, in dBFS: the level that `micLevel` treats as silence (plus 6 dB).","zh":"oav-p5 量到的房間本身的底噪，單位是 dBFS：`micLevel` 把它（再加 6 dB）當成安靜的那個音量。"},"description":[{"en":"A humming classroom should not read as loud, so [`micLevel`](#micLevel) is measured from the room itself: this floor follows the quietest recent readings. It drops within a second when the room gets quieter and rises at most 1.5 dB a second when it gets noisier, so a voice never becomes the new silence. 6 dB above the floor reads 0 on `micLevel`.","zh":"嗡嗡響的教室不該被讀成很大聲，所以 [`micLevel`](#micLevel) 是從房間本身量起的：這個底噪跟著最近最安靜的讀數走。房間變安靜時，一秒內就會降下去，變吵時每秒最多往上爬 1.5 dB，所以人聲不會變成新的安靜。比底噪高 6 dB 的音量，在 `micLevel` 上讀成 0。"}],"value":{"type":"Number","range":"-70..-45","en":"Starts at −60 and is always held between −70 and −45. Updated once per display frame while the microphone is on.","zh":"一開始是 −60，永遠限制在 −70 到 −45 之間。麥克風開著時每個畫面更新一次。"},"params":[],"notes":[{"en":"Stopping the microphone does not reset it: the next start continues from the last floor.","zh":"停掉麥克風不會重設它：下次開始會從上一次的底噪接著走。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('level ' + nf(OAVp5.mic.db, 1, 1) + ' dBFS', 20, 30);\n  text('room noise ' + nf(OAVp5.mic.floor, 1, 1) + ' dBFS', 20, 60);\n  text('micLevel ' + nf(OAVp5.mic.level, 1, 2), 20, 90);\n}","caption":{"en":"Shows the level, the room's noise floor and the resulting micLevel. Keep quiet for a few seconds and watch the floor settle.","zh":"顯示音量、房間的底噪，和算出來的 micLevel。安靜個幾秒，看底噪穩定下來。"},"run":"sketch","lang":"js"}],"related":["micLevel","OAVp5.mic.db","OAVp5.mic.rms","OAVp5.mic.level"],"keywords":{"en":["noise floor","room noise","background noise","silence level","floor"],"zh":["底噪","房間噪音","背景噪音","安靜的音量","地板值"]},"tbd":[],"claims":[{"before":"startMic()","expr":"OAVp5.mic.floor","min":-70,"max":-45,"needs":"mic"}]},
{"id":"OAVp5.mic.level","key":"OAVp5.mic.level","name":"OAVp5.mic.level","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.level","source":{"file":"p5/oav-p5.js","line":871,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L871"},"summary":{"en":"The same 0 to 1 loudness as `micLevel`, read without ever turning the microphone on.","zh":"跟 `micLevel` 一樣的 0 到 1 音量，讀它不會把麥克風打開。"},"description":[{"en":"[`micLevel`](#micLevel) is a getter that returns this number and, the first time it is read, asks for the microphone. `OAVp5.mic.level` is the number itself, so reading it is safe anywhere: before the user has chosen, with `?oav_mic=click`, or in a library. Start the microphone with [`startMic()`](#startMic) or 🎤, then it moves. How it is measured (decibels, the room as zero, −10 dBFS as 1) is described under [`micLevel`](#micLevel).","zh":"[`micLevel`](#micLevel) 是一個 getter，回傳這個數字，而且第一次被讀的時候會去要麥克風。`OAVp5.mic.level` 就是數字本身，所以在哪裡讀都安全：使用者還沒決定之前、設了 `?oav_mic=click`，或是在函式庫裡。用 [`startMic()`](#startMic) 或 🎤 啟動麥克風，它才會動。它怎麼量（分貝、以房間為 0、−10 dBFS 為 1）寫在 [`micLevel`](#micLevel) 底下。"}],"value":{"type":"Number","range":"0..1","en":"Updated once per display frame while the microphone is on; 0 while it is off, asking, blocked or missing.","zh":"麥克風開著的時候，每個畫面更新一次。關著、詢問中、被擋下或不存在的時候是 0。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2, height / 2, 50 + OAVp5.mic.level * 300);\n}","caption":{"en":"The loudness circle, written with the raw number instead of micLevel. Allow the microphone and talk.","zh":"音量圓，這次用原始的數字，不用 micLevel。允許麥克風，然後說話。"},"run":"sketch","lang":"js"}],"related":["micLevel","OAVp5.mic.rms","OAVp5.mic.db","OAVp5.mic.floor","startMic"],"keywords":{"en":["mic level","loudness","volume","read without starting","raw level"],"zh":["麥克風音量","音量","大小聲","讀取但不啟動","原始音量"]},"tbd":[],"claims":[{"before":"startMic()","expr":"OAVp5.mic.level","min":0,"max":1,"needs":"mic"}]},
{"id":"OAVp5.mic.on","key":"OAVp5.mic.on","name":"OAVp5.mic.on","aliases":[],"kind":"function","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.on(name, fn)","source":{"file":"p5/oav-p5.js","line":875,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L875"},"summary":{"en":"Listens to a microphone event (`'beat'`, `'kick'`, `'snare'` or `'hat'`) without defining a global function, and returns a function that stops listening.","zh":"監聽麥克風的事件（`'beat'`、`'kick'`、`'snare'` 或 `'hat'`），不必定義全域函式，並回傳一個停止監聽的函式。"},"description":[{"en":"The listener version of [`beatDetected()`](#beatDetected), [`kickDetected()`](#kickDetected), `snareDetected()` and `hatDetected()`: any number of them, added and removed while the sketch runs, with no global names taken. An error inside `fn` shows in the console and on a card.","zh":"[`beatDetected()`](#beatDetected)、[`kickDetected()`](#kickDetected)、`snareDetected()`、`hatDetected()` 的監聽者版本：想要幾個都行，草圖在跑的時候可以加也可以拿掉，也不占用任何全域名稱。`fn` 裡面出錯會顯示在 console 和卡片上。"}],"params":[{"name":"name","type":"String","en":"`'beat'`, `'kick'`, `'snare'` or `'hat'`. Anything else throws an error that lists them.","zh":"`'beat'`、`'kick'`、`'snare'` 或 `'hat'`。其他的名稱會丟出一個錯誤，並列出這幾個。"},{"name":"fn","type":"Function","en":"Called as `fn(level)` with how strong the hit was, 0 to 1: the same number [`beatDetected(level)`](#beatDetected) and [`kickDetected(level)`](#kickDetected) get.","zh":"用 `fn(level)` 的方式呼叫，帶著這一下有多強，0 到 1：跟 [`beatDetected(level)`](#beatDetected) 和 [`kickDetected(level)`](#kickDetected) 拿到的數字一樣。"}],"returns":{"type":"Function","en":"A function that takes `fn` off again: `const off = OAVp5.mic.on(…); off();`.","zh":"一個把 `fn` 取消的函式：`const off = OAVp5.mic.on(…); off();`。"},"needs":{"en":"Listening does not turn the microphone on: call [`startMic()`](#startMic), press 🎤, or read [`micLevel`](#micLevel) too, or nothing will ever fire. (Defining the global `beatDetected()` is different: oav-p5 sees that function and starts the microphone at the first click.) These events come from the microphone only, not from the drum machine.","zh":"監聽並不會把麥克風打開：要呼叫 [`startMic()`](#startMic)、按 🎤，或是也讀一下 [`micLevel`](#micLevel)，不然什麼事件都不會發生。（定義全域的 `beatDetected()` 就不一樣：oav-p5 看到那個函式，會在第一次點擊時啟動麥克風。）這些事件只來自麥克風，不來自鼓機。"},"notes":[{"en":"When the drum machine plays, the global `beatDetected()`, `kickDetected()` and the others are also called for its hits. A listener added with `OAVp5.mic.on()` hears the microphone only.","zh":"鼓機在播的時候，全域的 `beatDetected()`、`kickDetected()` 這些也會為鼓機的每一下被呼叫。用 `OAVp5.mic.on()` 加的監聽者只聽得到麥克風。"}],"examples":[{"code":"let flash = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startMic();\n  OAVp5.mic.on('beat', (level) => { flash = 0.4 + level * 0.6; });   // louder beat, brighter flash\n}\n\nfunction draw() {\n  background(20 + flash * 200);\n  flash *= 0.9;\n}","caption":{"en":"A flash on every beat the microphone hears, brighter for a louder one. Clap or tap the desk.","zh":"麥克風每聽到一個拍子就閃一下，拍子越大聲閃得越亮。拍手或敲桌子。"},"run":"sketch","lang":"js"}],"related":["beatDetected","kickDetected","OAVp5.on","beat","startMic"],"keywords":{"en":["mic events","beat listener","kick listener","subscribe","onset","callback without global"],"zh":["麥克風事件","拍子監聽","大鼓監聽","訂閱","節拍偵測","不用全域函式"]},"tbd":[],"claims":[{"expr":"(() => { try { OAVp5.mic.on('nope', () => {}); return 'no error'; } catch (e) { return e.message.includes('unknown mic event'); } })()","equals":true}]},
{"id":"OAVp5.mic.rms","key":"OAVp5.mic.rms","name":"OAVp5.mic.rms","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.rms","source":{"file":"p5/oav-p5.js","line":872,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L872"},"summary":{"en":"The raw loudness of the microphone: a smoothed RMS, 0 to 1, before oav-p5 turns it into the decibel scale of `micLevel`.","zh":"麥克風原始的音量：平滑過的 RMS，0 到 1，是 oav-p5 把它換成 `micLevel` 的分貝刻度之前的值。"},"description":[{"en":"The root mean square of the samples in the latest frame, smoothed a little. It is linear, the way a waveform is drawn, so it suits a quick check or your own scale. [`micLevel`](#micLevel) is built from it: [`OAVp5.mic.db`](#OAVp5.mic.db) is the same number in dBFS, and [`OAVp5.mic.floor`](#OAVp5.mic.floor) is the room's own noise, in dBFS too.","zh":"最新一個畫面裡所有取樣的均方根，稍微平滑過。它是線性的，就像波形畫出來的樣子，所以適合快速檢查，或是用在你自己的刻度上。[`micLevel`](#micLevel) 就是用它算出來的：[`OAVp5.mic.db`](#OAVp5.mic.db) 是同一個數字換成 dBFS，[`OAVp5.mic.floor`](#OAVp5.mic.floor) 是房間本身的底噪，單位也是 dBFS。"}],"value":{"type":"Number","range":"0..1","en":"Updated once per display frame while the microphone is on; 0 when it is off. A speaking voice into a laptop is a small number, in the range 0.01 to 0.05 according to the comment in the code, which is why `micLevel` works in decibels.","zh":"麥克風開著的時候，每個畫面更新一次，關著是 0。對著筆電說話是個很小的數字，程式碼裡的註解說大約在 0.01 到 0.05，所以 `micLevel` 才改用分貝來算。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('rms ' + nf(OAVp5.mic.rms, 1, 4), 20, 30);\n  rect(20, 60, min(OAVp5.mic.rms * 10, 1) * (width - 40), 20);   // magnified 10 times: a voice is small\n}","caption":{"en":"The raw RMS as a number and a bar, magnified ten times because speech is a small number. Allow the microphone and talk.","zh":"原始的 RMS，用數字和長條表示，放大十倍，因為說話的數值很小。允許麥克風，然後說話。"},"run":"sketch","lang":"js"}],"related":["OAVp5.mic.db","OAVp5.mic.floor","OAVp5.mic.level","micLevel"],"keywords":{"en":["rms","raw loudness","amplitude","signal level","linear volume"],"zh":["rms","原始音量","振幅","訊號大小","線性音量"]},"tbd":[{"en":"Typical values for speech and music depend on the microphone and the room. The 0.01 to 0.05 for a voice is the code comment's figure and was not measured here.","zh":"說話和音樂的典型數值要看麥克風和房間。人聲的 0.01 到 0.05 是程式碼註解裡的數字，這裡沒有量過。"}],"claims":[{"before":"startMic()","expr":"OAVp5.mic.rms","min":0,"max":1,"needs":"mic"}]},
{"id":"OAVp5.mic.sensitivity","key":"OAVp5.mic.sensitivity","name":"OAVp5.mic.sensitivity","aliases":["OAVp5.mic.gain"],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.sensitivity","source":{"file":"p5/oav-p5.js","line":869,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L869"},"summary":{"en":"The microphone's sensitivity: 1 normally, 2 for twice as sensitive. [`micSensitivity()`](#micSensitivity) sets it; `OAVp5.mic.gain` is the old name.","zh":"麥克風的靈敏度：平常是 1，2 是兩倍靈敏。用 [`micSensitivity()`](#micSensitivity) 來設定，`OAVp5.mic.gain` 是舊名字。"},"description":[{"en":"The number multiplies what the microphone hears: on [`micLevel`](#micLevel) 2 adds 6 dB, on [`bass`](#bass), `mid` and `treble` it multiplies by 2. Read it to show the current setting, and set it with [`micSensitivity()`](#micSensitivity), which checks the number.","zh":"這個數字乘在麥克風聽到的東西上：對 [`micLevel`](#micLevel) 來說 2 等於加 6 dB，對 [`bass`](#bass)、`mid`、`treble` 是乘以 2。讀它來顯示目前的設定，要設定的話用 [`micSensitivity()`](#micSensitivity)，它會檢查數字。"}],"value":{"type":"Number","range":"0.05..20","en":"Starts at 1. [`micSensitivity(v)`](#micSensitivity) keeps what you give it between 0.05 and 20.","zh":"一開始是 1。[`micSensitivity(v)`](#micSensitivity) 會把你給的值限制在 0.05 到 20 之間。"},"params":[],"notes":[{"en":"`OAVp5.mic.gain` is the same value under the name oav-p5 0.3.0 used. Assigning to either works (`OAVp5.mic.gain = 2`) but is not held between 0.05 and 20, and `gain` falls back to 1 for a number that is not above 0.","zh":"`OAVp5.mic.gain` 是同一個值，用的是 oav-p5 0.3.0 的名字。直接指定給任何一個都行（`OAVp5.mic.gain = 2`），但不會被限制在 0.05 到 20 之間，而且 `gain` 遇到不大於 0 的數字會退回 1。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  micSensitivity(3);   // a quiet or far-away microphone\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('sensitivity ' + OAVp5.mic.sensitivity, 20, 30);   // 3\n  circle(width / 2, height / 2, 50 + micLevel * 300);\n}","caption":{"en":"micSensitivity(3) sets the number, and OAVp5.mic.sensitivity reads it back.","zh":"micSensitivity(3) 設定這個數字，OAVp5.mic.sensitivity 把它讀回來。"},"run":"sketch","lang":"js"}],"related":["micSensitivity","micLevel","bass"],"keywords":{"en":["sensitivity","gain","boost","quiet microphone","mic gain"],"zh":["靈敏度","增益","放大","麥克風很小聲","麥克風增益"]},"tbd":[],"claims":[{"expr":"OAVp5.mic.sensitivity","equals":1},{"expr":"OAVp5.mic.gain","equals":1}]},
{"id":"OAVp5.mic.start","key":"OAVp5.mic.start","name":"OAVp5.mic.start","aliases":[],"kind":"function","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.start()","source":{"file":"p5/oav-p5.js","line":873,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L873"},"summary":{"en":"Turns the microphone on and returns a promise that says whether it worked: the same as `startMic()`.","zh":"開啟麥克風，並回傳一個 promise，告訴你有沒有成功：跟 `startMic()` 一樣。"},"description":[{"en":"[`startMic()`](#startMic) is this function: see it for what must happen first (a click, `https` or `localhost`). `OAVp5.mic.start()` also lifts the \"stopped by you\" mark that [`stopMic()`](#stopMic) leaves, which is what lets a later read of [`micLevel`](#micLevel) switch the microphone on again.","zh":"[`startMic()`](#startMic) 就是這個函式：要先發生什麼（一次點擊、`https` 或 `localhost`）請看那一條。`OAVp5.mic.start()` 也會解除 [`stopMic()`](#stopMic) 留下的「你停掉的」記號，這樣之後再讀 [`micLevel`](#micLevel) 就又能把麥克風打開。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"Resolves to `true` once the microphone is on, `false` if it was blocked, is missing, or the `mic` option is `off`.","zh":"麥克風開起來之後 resolve 成 `true`，被擋下、不存在、或 `mic` 選項是 `off` 的時候 resolve 成 `false`。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.mic.start().then((ok) => console.log(ok ? 'microphone on' : 'no microphone'));\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.mic.state === 'on' ? 'listening' : 'click the canvas to start the microphone', 20, 30);\n}","caption":{"en":"Nothing happens until you click: then the browser asks, and the text changes when the microphone is on.","zh":"點下去之前什麼都不會發生：點了之後瀏覽器才詢問，麥克風開起來時文字就會改變。"},"run":"sketch","lang":"js"}],"related":["startMic","OAVp5.mic.stop","OAVp5.mic.state","option:mic"],"keywords":{"en":["mic start","start microphone","enable microphone","turn on microphone"],"zh":["啟動麥克風","開啟麥克風","打開麥克風","mic start"]},"tbd":[],"claims":[]},
{"id":"OAVp5.mic.state","key":"OAVp5.mic.state","name":"OAVp5.mic.state","aliases":[],"kind":"variable","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.state","source":{"file":"p5/oav-p5.js","line":868,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L868"},"summary":{"en":"Where the microphone is: `'off'`, `'asking'`, `'on'`, `'denied'` or `'unavailable'`.","zh":"麥克風現在的情形：`'off'`、`'asking'`、`'on'`、`'denied'` 或 `'unavailable'`。"},"description":[{"en":"`'off'`: not started, or stopped. `'asking'`: the browser is waiting for the user's answer. `'on'`: sound is coming in, and [`micIsOn`](#micIsOn) is true. `'denied'`: the permission was refused (by the user, by the computer's settings, or by an iframe without `allow=\"microphone\"`). `'unavailable'`: there is no microphone, none the browser can open, or the page is not on `https` or `localhost`. When it is not `'on'`, [`OAVp5.mic.error`](#OAVp5.mic.error) has the browser's reason and a card beside the 🎹 says what to press.","zh":"`'off'`：還沒啟動，或已經停了。`'asking'`：瀏覽器在等使用者回答。`'on'`：聲音進來了，[`micIsOn`](#micIsOn) 是 true。`'denied'`：權限被拒絕（被使用者、被電腦的設定，或被沒有 `allow=\"microphone\"` 的 iframe）。`'unavailable'`：沒有麥克風、瀏覽器打不開，或網頁不在 `https` 或 `localhost` 底下。不是 `'on'` 的時候，[`OAVp5.mic.error`](#OAVp5.mic.error) 有瀏覽器給的原因，🎹 旁邊也會有卡片說明要按什麼。"}],"value":{"type":"String","range":"'off' | 'asking' | 'on' | 'denied' | 'unavailable'","en":"Changes when you start or stop the microphone and when the browser answers. `'off'` until something starts it. Reading it never starts it.","zh":"你開始或停止麥克風、瀏覽器回答的時候會變。在有東西啟動它之前是 `'off'`。讀它不會啟動它。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction draw() {\n  background(OAVp5.mic.state === 'on' ? 20 : color(90, 30, 30));\n  fill(255);\n  text('microphone: ' + OAVp5.mic.state, 20, 30);\n  if (OAVp5.mic.state === 'denied') text('click the lock left of the address and allow it', 20, 60);\n}","caption":{"en":"The background is red until the microphone is on. The state goes \"asking\", then \"on\" when you allow it, or \"denied\" when you refuse.","zh":"麥克風開起來之前背景是紅的。狀態先是 \"asking\"，你允許之後變成 \"on\"，拒絕的話變成 \"denied\"。"},"run":"sketch","lang":"js"}],"related":["micIsOn","OAVp5.mic.error","startMic","option:mic","OAVp5.check"],"keywords":{"en":["mic state","permission state","microphone denied","asking","is the microphone on"],"zh":["麥克風狀態","權限狀態","麥克風被拒絕","詢問中","麥克風開了嗎"]},"tbd":[],"claims":[{"expr":"['off', 'asking', 'on', 'denied', 'unavailable'].includes(OAVp5.mic.state)","equals":true}]},
{"id":"OAVp5.mic.stop","key":"OAVp5.mic.stop","name":"OAVp5.mic.stop","aliases":[],"kind":"function","package":"oav-p5","category":"mic","documented":true,"signature":"OAVp5.mic.stop()","source":{"file":"p5/oav-p5.js","line":873,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L873"},"summary":{"en":"Turns the microphone off and sets every microphone value back to 0: the same as `stopMic()`.","zh":"關掉麥克風，所有麥克風的值都歸零：跟 `stopMic()` 一樣。"},"description":[{"en":"[`stopMic()`](#stopMic) is this function. It stops the microphone's tracks and closes its audio context, so the browser's \"recording\" indicator goes away, and [`OAVp5.mic.level`](#OAVp5.mic.level), `bass`, `mid`, `treble`, `rms` read 0 and `db` reads −100. After it, only [`OAVp5.mic.start()`](#OAVp5.mic.start) or 🎤 turns the microphone back on: reading `micLevel` does not.","zh":"[`stopMic()`](#stopMic) 就是這個函式。它停掉麥克風的音軌並關掉它的音訊環境，所以瀏覽器上「正在錄音」的指示會消失，[`OAVp5.mic.level`](#OAVp5.mic.level)、`bass`、`mid`、`treble`、`rms` 都讀到 0，`db` 讀到 −100。之後只有 [`OAVp5.mic.start()`](#OAVp5.mic.start) 或 🎤 能再把麥克風打開：讀 `micLevel` 不行。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"Resolves to `false`, the new state of the microphone.","zh":"resolve 成 `false`，也就是麥克風的新狀態。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startMic();\n}\n\nfunction mousePressed() {\n  OAVp5.mic.stop();   // the browser's recording indicator goes away\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('microphone: ' + OAVp5.mic.state + ' (click the canvas to stop it)', 20, 30);\n  circle(width / 2, height / 2, 50 + OAVp5.mic.level * 300);\n}","caption":{"en":"Click the canvas: the microphone stops, the state says \"off\" and the circle drops to its smallest size.","zh":"點一下畫布：麥克風停止，狀態變成 \"off\"，圓縮到最小。"},"run":"sketch","lang":"js"}],"related":["stopMic","OAVp5.mic.start","OAVp5.mic.state","micIsOn"],"keywords":{"en":["mic stop","stop microphone","mute","turn off microphone","disable microphone"],"zh":["停止麥克風","關閉麥克風","靜音","mic stop"]},"tbd":[],"claims":[]},
{"id":"AudioAnalyzer","key":"AudioAnalyzer","name":"AudioAnalyzer","aliases":[],"kind":"class","package":"@openav/audio","category":"mic","documented":true,"signature":"new AudioAnalyzer({ signals = null, fftSize = 2048, smooth = 0.7 } = {})","import":"import { AudioAnalyzer } from '@openav/audio';","source":{"file":"packages/audio/index.js","line":24,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/audio/index.js#L24"},"summary":{"en":"Listens to the microphone, an audio element or any Web Audio node, and turns it into loudness, three bands, onsets and kick, snare and hat hits.","zh":"聽麥克風、audio 元素或任何 Web Audio 節點，把它變成音量、三個頻段、起音，以及大鼓、小鼓、鈸的打點。"},"description":[{"en":"An `AudioAnalyzer` wraps one Web Audio `AnalyserNode`. You give it a sound (`enableMic()`, `enableElement(el)`, or connect your own node into `input()`), call `update()` once per frame, and it returns a plain object of numbers, which it also publishes as `audio/*` signals if you gave it a `signals` bus. This is the analysis under the microphone of a p5 sketch: [`micLevel`](#micLevel) is its `rms` turned into decibels, [`bass`](#bass), `mid` and `treble` are its `low`, `mid` and `high`, and [`beat`](#beat) and [`kickDetected()`](#kickDetected) come from its `onset` and `kick`.","zh":"`AudioAnalyzer` 包著一個 Web Audio 的 `AnalyserNode`。你給它一個聲音來源（`enableMic()`、`enableElement(el)`，或把自己的節點接到 `input()`），每個畫面呼叫一次 `update()`，它就回傳一個只有數字的物件，如果你給了 `signals` 匯流排，也會同時發布成 `audio/*` 訊號。這就是 p5 草圖的麥克風底下的分析：[`micLevel`](#micLevel) 是它的 `rms` 換成分貝，[`bass`](#bass)、`mid`、`treble` 是它的 `low`、`mid`、`high`，[`beat`](#beat) 和 [`kickDetected()`](#kickDetected) 來自它的 `onset` 和 `kick`。"},{"en":"`update()` returns `null` until a source is enabled. After that it returns `{ rms, peak, low, mid, high, centroid, onset, kick, snare, hat }`. `rms` is the smoothed loudness and `peak` the loudest sample of the moment, both 0..1 of full scale (from float samples where the browser has them, so a quiet room is measured, not rounded to one 8-bit step). A voice is a small `rms`: scale it, or map it through a window. `low`, `mid` and `high` are the average strength of the spectrum in 20–250 Hz, 250–2000 Hz and 2000–8000 Hz, 0..1, taken from the analyser's byte spectrum, so they are not decibels and a faint band stays near 0. `centroid` is the spectrum's centre of mass, 0..1 of the way from 0 Hz to half the sample rate (the brightness). `onset` is true for the one `update()` in which the loudness jumped over its adaptive floor, with at least 100 ms between two onsets.","zh":"來源開起來之前，`update()` 回傳 `null`。開起來之後，它回傳 `{ rms, peak, low, mid, high, centroid, onset, kick, snare, hat }`。`rms` 是平滑後的音量，`peak` 是這一刻最大的取樣，兩個都是滿刻度的 0..1（瀏覽器有浮點取樣的話就用浮點，所以安靜的房間也量得到，不會被四捨五入到 8 位元的一格）。人聲的 `rms` 很小，要乘上倍數，或用範圍視窗對應。`low`、`mid`、`high` 是頻譜在 20–250 Hz、250–2000 Hz、2000–8000 Hz 的平均強度，0..1，取自分析器的位元組頻譜，所以不是分貝，微弱的頻段會貼近 0。`centroid` 是頻譜的重心，從 0 Hz 到取樣率的一半，0..1（亮度）。`onset` 只在音量跳過它自己適應的底線的那一次 `update()` 是 true，兩個起音之間至少隔 100 毫秒。"},{"en":"`kick`, `snare` and `hat` are `{ hit, level, env }`, found by looking for a sudden rise in one band of the spectrum, with no machine learning: kick 20–120 Hz, snare 150–800 Hz, hat 6000–14000 Hz. `hit` is true on the update that found one (at least 90, 90 and 60 ms apart), `level` is the band's strength now, and `env` is 0..1: it jumps to `min(1, level × 2)` on a hit and then shrinks by 12% every `update()`, about 120 ms at 60 frames a second. Because it decays per update, it falls faster if you call `update()` less often.","zh":"`kick`、`snare`、`hat` 是 `{ hit, level, env }`，做法是在頻譜的某一段找突然的上升，沒有用機器學習：大鼓 20–120 Hz、小鼓 150–800 Hz、鈸 6000–14000 Hz。`hit` 在找到一下的那次 `update()` 是 true（互相至少隔 90、90、60 毫秒），`level` 是那一段現在的強度，`env` 是 0..1：打到的時候跳到 `min(1, level × 2)`，之後每次 `update()` 縮小 12%，每秒 60 個畫面的話大約 120 毫秒。因為它是每次 `update()` 衰減，所以你呼叫 `update()` 的頻率越低，它掉得越快。"},{"en":"With a `signals` bus, these are published: `audio/rms`, `audio/peak`, `audio/band/low`, `audio/band/mid`, `audio/band/high`, `audio/centroid`, `audio/kick/env`, `audio/snare/env` and `audio/hat/env` (continuous, 0..1), and the pulses `audio/onset` `{ rms }`, `audio/kick` `{ level }`, `audio/snare` `{ level }` and `audio/hat` `{ level }`. Pulses fire only when something was found.","zh":"有 `signals` 匯流排的話，會發布這些：`audio/rms`、`audio/peak`、`audio/band/low`、`audio/band/mid`、`audio/band/high`、`audio/centroid`、`audio/kick/env`、`audio/snare/env`、`audio/hat/env`（連續值，0..1），以及脈衝 `audio/onset` `{ rms }`、`audio/kick` `{ level }`、`audio/snare` `{ level }`、`audio/hat` `{ level }`。脈衝只在找到東西的時候才觸發。"}],"params":[{"name":"signals","type":"Signals","optional":true,"default":"null","en":"A [`Signals`](#Signals) bus to publish into. With one, the `audio/*` signals are defined when you construct the analyzer and set on every `update()`. Without one, `update()` only returns the numbers.","zh":"要發布訊號的 [`Signals`](#Signals) 匯流排。有給的話，建構分析器時就會定義 `audio/*` 訊號，每次 `update()` 都會更新它們。沒給的話，`update()` 只回傳數字。"},{"name":"fftSize","type":"Number","optional":true,"default":"2048","en":"The size of the analyser's FFT, a power of two from 32 to 32768. The spectrum has half as many bins.","zh":"分析器 FFT 的大小，32 到 32768 之間的 2 的次方。頻譜的格數是它的一半。"},{"name":"smooth","type":"Number","optional":true,"default":"0.7","en":"How much of the previous `rms` is kept each update, 0..1. Higher is smoother and slower: `rms = rms × smooth + now × (1 − smooth)`.","zh":"每次更新要留下多少上一次的 `rms`，0..1。越高越平滑也越慢：`rms = rms × smooth + 現在的值 × (1 − smooth)`。"}],"needs":{"en":"Browsers start audio only after a user gesture. `enableMic()` asks for the microphone, and the analyser's `AudioContext` stays suspended until the page has had a click or a key: `update()` calls `resume()` on every frame, so the numbers start by themselves after the first click. Until then they are all 0. The page needs `https` or `localhost` for the microphone.","zh":"瀏覽器要使用者做過一個動作之後才讓聲音開始。`enableMic()` 會去要麥克風，而分析器的 `AudioContext` 在頁面被點一下或按過鍵之前都是暫停的：`update()` 每個畫面都會呼叫 `resume()`，所以第一次點擊之後數字會自己開始動。在那之前全部都是 0。麥克風要求網頁在 `https` 或 `localhost` 底下。"},"notes":[{"en":"It makes its own `AudioContext`, so p5.sound or Tone.js can run beside it. To analyse a sound that lives in another context, connect a node of `audio.ctx` into `input()`, or use `enableElement()`.","zh":"它自己建一個 `AudioContext`，所以 p5.sound 或 Tone.js 可以在旁邊照常跑。要分析在別的 context 裡的聲音，就把 `audio.ctx` 的節點接到 `input()`，或用 `enableElement()`。"},{"en":"`enableMic()` turns off echo cancellation, noise suppression and automatic gain, because they change the loudness you want to measure. On a laptop that plays the sound it listens to, expect the speakers to leak into the microphone.","zh":"`enableMic()` 會關掉回音消除、噪音抑制和自動增益，因為它們會改變你想量的音量。筆電如果一邊播放一邊聽，喇叭的聲音會漏進麥克風。"}],"examples":[{"code":"import { AudioAnalyzer } from '@openav/audio';\n\nconst audio = new AudioAnalyzer();\nawait audio.enableMic();                      // the browser asks for the microphone\nconst g = document.getElementById('c').getContext('2d');\n\n(function frame() {\n  const f = audio.update();                   // { rms, peak, low, mid, high, ... }\n  g.fillStyle = '#111'; g.fillRect(0, 0, 400, 300);\n  g.fillStyle = '#ffd166';\n  g.fillRect(20, 130, Math.min(1, f.rms * 6) * 360, 40);   // a voice is a small rms: scale it\n  requestAnimationFrame(frame);\n})();","caption":{"en":"Allow the microphone, click the page once, and talk: a bar grows with the loudness.","zh":"允許麥克風、在頁面上點一下，然後說話：一條橫條跟著音量變長。"},"run":"module","lang":"js"},{"code":"import { AudioAnalyzer } from '@openav/audio';\n\nconst audio = new AudioAnalyzer();             // no signals bus: update() just returns the numbers\nconst input = audio.input();                    // makes the AudioContext (audio.ctx) and the AnalyserNode\nconst osc = new OscillatorNode(audio.ctx, { frequency: 100 });\nosc.connect(input);                             // any node of audio.ctx can feed the analyser\nosc.start();\n\nlet frames = 0;\n(function frame() {\n  const f = audio.update();                     // also resumes the context once the page has had a click\n  if (++frames < 40) return requestAnimationFrame(frame);\n  console.log(audio.ctx.state, f.low > f.high);  // running true: a 100 Hz tone sits in the low band\n  console.log(f.rms > 0.1, f.centroid < 0.1);    // true true\n})();","caption":{"en":"No microphone needed: a 100 Hz tone is fed into the analyser through `input()`. After 40 frames the log says the low band is louder than the high one.","zh":"不需要麥克風：一個 100 Hz 的音從 `input()` 送進分析器。40 個畫面之後，log 會告訴你低頻段比高頻段大。"},"run":"module","lang":"js"},{"code":"import { AudioAnalyzer } from '@openav/audio';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst audio = new AudioAnalyzer({ signals });         // defines the audio/* signals at once\nconsole.log(signals.list().length);                    // 13\nsignals.on('audio/onset', ({ rms }) => console.log('onset', rms.toFixed(2)));\nsignals.on('audio/kick', ({ level }) => console.log('kick', level.toFixed(2)));\nconsole.log(signals.get('audio/rms'));                 // 0, nothing heard yet\nconsole.log(audio.update());                           // null until a source is enabled","caption":{"en":"With a signals bus the analyzer defines thirteen `audio/*` signals up front. Listen to a pulse with `signals.on`; the handlers run once a source is enabled and a hit is found.","zh":"有匯流排的話，分析器一開始就定義好十三個 `audio/*` 訊號。用 `signals.on` 聽脈衝。來源開起來、找到打點的時候，處理函式就會被呼叫。"},"run":"module","lang":"js"}],"related":["micLevel","bass","beat","kickDetected","micSensitivity","Signals"],"keywords":{"en":["audio","microphone","analysis","fft","loudness","rms","bands","onset","snare","hat"],"zh":["音訊","麥克風","分析","頻譜","音量","低中高音","起音","大鼓","小鼓","鈸"]},"tbd":[{"en":"Typical `rms` values for speech differ between the notes of this project: the handbook (docs/inputs.md) says speech often sits around 0.05 to 0.2, a comment in oav-p5 says 0.01 to 0.05. They depend on the microphone and the room, and have not been measured here, so no typical number is given.","zh":"這個專案的各處對說話的 `rms` 典型值說法不一：手冊（docs/inputs.md）說說話常在 0.05 到 0.2 附近，oav-p5 裡的註解說是 0.01 到 0.05。它要看麥克風和房間，這裡沒有實際量過，所以不給典型數字。"}],"claims":[],"members":[{"name":"enableMic","member":"method","signature":"","doc":"","line":54},{"name":"enableElement","member":"method","signature":"el","doc":"Analyze an <audio>/<video> element (it keeps playing to speakers).","line":63},{"name":"input","member":"method","signature":"","doc":"Bring your own node (Tone.js etc.): node.connect(analyzer.input()).","line":72},{"name":"update","member":"method","signature":"","doc":"Call once per frame.","line":88}]},
{"id":"soundIsOn","key":"soundIsOn","name":"soundIsOn","aliases":[],"kind":"variable","package":"oav-p5","category":"sound","documented":true,"signature":"soundIsOn","source":{"file":"p5/oav-p5.js","line":316,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L316"},"summary":{"en":"Whether notes are being heard right now: true once `startSound()` has finished, false before and after.","zh":"現在彈的音有沒有被聽到：`startSound()` 完成之後是 true，之前和之後都是 false。"},"description":[{"en":"Read it in `draw()` like `mouseIsPressed`: `if (!soundIsOn) text('click to hear', 20, 30);`. It changes only when the state of the sound changes, not every frame. Reading it never starts anything (unlike [`micLevel`](#micLevel)), so it is safe to check anywhere.","zh":"像 `mouseIsPressed` 一樣在 `draw()` 裡讀：`if (!soundIsOn) text('click to hear', 20, 30);`。它只有在聲音的狀態改變的時候才會變，不是每個畫面都更新。讀它不會啟動任何東西（跟 [`micLevel`](#micLevel) 不一樣），所以在哪裡檢查都安全。"},{"en":"`false` does not mean `startSound()` was never called. To tell \"loading\" from \"waiting for the click\" from \"failed\", read [`OAVp5.sound.state`](#OAVp5.sound.state), and [`OAVp5.sound.error`](#OAVp5.sound.error) for the reason of a failure.","zh":"`false` 不代表沒呼叫過 `startSound()`。要分辨「載入中」、「等點擊」和「失敗」，請讀 [`OAVp5.sound.state`](#OAVp5.sound.state)，失敗的原因在 [`OAVp5.sound.error`](#OAVp5.sound.error)。"}],"value":{"type":"Boolean","en":"True only while [`OAVp5.sound.state`](#OAVp5.sound.state) is `'on'`. It is false at the start, while sound waits for a click, while it loads, after [`stopSound()`](#stopSound) and after an error.","zh":"只有 [`OAVp5.sound.state`](#OAVp5.sound.state) 是 `'on'` 的時候才是 true。一開始、等待點擊、載入中、呼叫 [`stopSound()`](#stopSound) 之後、以及出錯的時候都是 false。"},"params":[],"needs":{"en":"It becomes true only after [`startSound()`](#startSound) (or 🔊 in the 🎹 panel, or `?oav_sound=piano`), and after the click the browser wants.","zh":"要先呼叫 [`startSound()`](#startSound)（或按 🎹 面板的 🔊、或網址加 `?oav_sound=piano`），而且過了瀏覽器要求的那次點擊，它才會變成 true。"},"notes":[{"en":"A sketch that already declares its own variable called `soundIsOn` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.sound.state === 'on'` instead.","zh":"如果草圖本來就宣告了自己的 `soundIsOn` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.sound.state === 'on'`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('organ');\n}\n\nfunction draw() {\n  background(20);\n  fill(soundIsOn ? color(120, 220, 140) : color(220, 160, 60));\n  circle(width / 2, height / 2, 80);\n  fill(255);\n  textAlign(CENTER);\n  text(soundIsOn ? 'sound is on' : 'click the canvas to turn sound on', width / 2, height - 40);\n}","caption":{"en":"An amber circle until you click, then a green one: that is the moment [`soundIsOn`](#soundIsOn) turns true.","zh":"點擊之前是琥珀色的圓，點了之後變綠色：那一刻就是 `soundIsOn` 變成 true 的時候。"},"run":"sketch","lang":"js"}],"related":["startSound","stopSound","OAVp5.sound.state","micIsOn","drumsAreOn","recordingIsOn"],"keywords":{"en":["sound","on","status","audio","enabled","ready","playing"],"zh":["聲音","開著","狀態","準備好","有沒有出聲"]},"tbd":[],"claims":[{"expr":"soundIsOn","equals":false}]},
{"id":"startSound","key":"startSound","name":"startSound","aliases":["OAVp5.sound.start"],"kind":"function","package":"oav-p5","category":"sound","documented":true,"signature":"startSound([id])","source":{"file":"p5/oav-p5.js","line":1413,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1413"},"summary":{"en":"Makes the keys you play heard through an instrument you name, and returns a promise that says whether sound is on.","zh":"讓你彈的鍵都聽得到，用你指定的樂器發聲，並回傳一個 promise，告訴你聲音有沒有開起來。"},"description":[{"en":"Call it once in `setup()`: `startSound('organ')`. From then on every note you play is heard: a MIDI keyboard, the 🎹 on screen, a phone, `midi.play(60, 0.8)`. Your own [`notePressed()`](#notePressed) still gets the note, and the sound is made before the sketch is told, so a slow callback never delays it. Pads on MIDI channel 10 (the General MIDI drum channel) play the drum kit instead of the instrument, and the sustain pedal holds notes.","zh":"在 `setup()` 裡呼叫一次：`startSound('organ')`。從這時候起，你彈的每個音都聽得到：MIDI 鍵盤、螢幕上的 🎹、手機、`midi.play(60, 0.8)` 都算。你自己寫的 [`notePressed()`](#notePressed) 一樣會收到音符，而且是先發聲、再通知草圖，所以 callback 寫得慢也不會拖慢聲音。MIDI 第 10 軌（General MIDI 的鼓軌）的打擊墊打的是鼓組，不是樂器，延音踏板則會讓音延續。"},{"en":"The instrument ids are `piano`, `epiano`, `organ`, `strings`, `choir`, `harp`, `marimba`, `vibraphone`, `xylophone`, `glockenspiel`, `music-box` and the synth presets `synth/pad`, `synth/saw-lead`, `synth/square-lead`, `synth/pluck`, `synth/supersaw`, `synth/bass`, `synth/acid`, `synth/brass`, `synth/bell`, `synth/sub` and `synth/chip`. Calling `startSound(id)` while sound plays switches the instrument. An id that does not exist is listed in the console together with the real ones: an instrument that is already playing stays, and at the first call the warm pad `synth/pad` plays.","zh":"樂器代號有 `piano`、`epiano`、`organ`、`strings`、`choir`、`harp`、`marimba`、`vibraphone`、`xylophone`、`glockenspiel`、`music-box`，以及合成器音色 `synth/pad`、`synth/saw-lead`、`synth/square-lead`、`synth/pluck`、`synth/supersaw`、`synth/bass`、`synth/acid`、`synth/brass`、`synth/bell`、`synth/sub`、`synth/chip`。聲音在播的時候再呼叫 `startSound(id)`，就是換樂器。給了不存在的代號，主控台會連同真正的代號清單一起列出來：已經在播的樂器不變，第一次呼叫就給錯的話，會是溫暖的 `synth/pad`。"},{"en":"Nothing is loaded before the first call (or before 🔊 in the 🎹 panel): [@openav/sound](#Sound) comes from next to `oav-p5.js` and Tone.js 15 from jsDelivr (the `tone` option points it somewhere else). Only the `piano` plays recorded samples, about 2 MB the first time; the electric piano sounds meanwhile (or, when you switch from another instrument, that one keeps sounding), and [`OAVp5.sound.instrument`](#OAVp5.sound.instrument) changes to `piano` when the samples are in. Every other instrument is made by Tone.js and has no files to load.","zh":"在第一次呼叫之前（或按 🎹 面板裡的 🔊 之前）什麼都不會載入：[@openav/sound](#Sound) 從 `oav-p5.js` 旁邊載入，Tone.js 15 從 jsDelivr 載入（載入選項 `tone` 可以改成別的地方）。只有 `piano` 播的是錄好的取樣，第一次大約 2 MB，載好之前由電鋼琴先頂著（如果是從別的樂器切過來，就由原本那個繼續響），取樣到齊的時候 [`OAVp5.sound.instrument`](#OAVp5.sound.instrument) 會變成 `piano`。其他樂器都是 Tone.js 即時合成的，沒有檔案要載。"}],"params":[{"name":"id","type":"String","optional":true,"default":"'piano'","en":"An instrument id, for example `'organ'` or `'synth/pluck'` (the list is in the description). Leave it out to keep the instrument chosen before; at the very first call that is the piano. A value that is not a non-empty string counts as left out.","zh":"樂器的代號，例如 `'organ'` 或 `'synth/pluck'`（完整清單在說明裡）。不傳就沿用之前選的樂器，第一次呼叫時就是鋼琴。不是非空字串的值，視同沒傳。"}],"returns":{"type":"Promise<Boolean>","en":"Resolves to `true` once sound is on. When the browser wants a click first, that is after the first click or key press on the page. Resolves to `false` when the loading option `sound` is `off`, when [`stopSound()`](#stopSound) came first, or when Tone.js could not be loaded.","zh":"聲音開起來之後 resolve 成 `true`。瀏覽器要先有一次點擊的話，就是在頁面上第一次點擊或按鍵之後。載入選項 `sound` 是 `off`、[`stopSound()`](#stopSound) 先被呼叫、或 Tone.js 載入失敗的時候，resolve 成 `false`。"},"needs":{"en":"A browser lets a page make sound only after a click or a key press on the page, and a key on a MIDI keyboard does not count. Calling `startSound()` in `setup()` is fine: [`OAVp5.sound.state`](#OAVp5.sound.state) is `'waiting'` and a card beside the 🎹 says \"Sound is ready but silent\"; the first click or key press turns sound on. [`soundIsOn`](#soundIsOn) tells when it has.","zh":"瀏覽器要頁面上先有一次點擊或按鍵才讓頁面出聲，MIDI 鍵盤上的鍵不算。在 `setup()` 裡呼叫 `startSound()` 沒有問題：[`OAVp5.sound.state`](#OAVp5.sound.state) 會是 `'waiting'`，🎹 旁邊有一張卡片說「聲音準備好了，還沒響」，第一次點擊或按鍵就會把聲音打開。[`soundIsOn`](#soundIsOn) 會告訴你什麼時候開了。"},"notes":[{"en":"No code at all: `?oav_sound=piano` on the page address (any instrument id; on OpenProcessing `?sketch_oav_sound=piano`) makes any sketch heard from the first click, and [`?oav_sound=off`](#option-sound) keeps it silent: `startSound()` then does nothing and resolves to `false`. In the 🎹 panel, 🔊 turns sound on and off and shows a picker with every instrument.","zh":"完全不寫程式也行：在網址後面加 `?oav_sound=piano`（任何樂器代號都可以，在 OpenProcessing 上是 `?sketch_oav_sound=piano`），任何草圖從第一次點擊起就聽得到，[`?oav_sound=off`](#option-sound) 則讓它保持安靜，這時 `startSound()` 什麼都不做，並 resolve 成 `false`。🎹 面板裡的 🔊 可以開關聲音，並列出所有樂器讓你挑。"},{"en":"Sound is separate from the drum machine: [`startDrums()`](#startDrums) has its own kit and is heard from the first click without `startSound()`.","zh":"聲音和鼓機是分開的：[`startDrums()`](#startDrums) 有自己的鼓組，不用 `startSound()`，第一次點擊之後就聽得到。"}],"examples":[{"code":"let playing = 60;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startSound('synth/pluck');   // every key you play is heard\n}\n\nfunction mousePressed() {\n  playing = 48 + floor(mouseX / 25);   // low on the left, high on the right\n  midi.play(playing, 0.8);\n}\n\nfunction mouseReleased() {\n  midi.play(playing, 0);\n}\n\nfunction draw() {\n  background(soundIsOn ? 30 : 90);\n  fill(255);\n  textAlign(CENTER, CENTER);\n  text(soundIsOn ? 'click to play a note' : 'click once to turn sound on', width / 2, height / 2);\n}","caption":{"en":"The first click turns sound on and the background darkens; the clicks after that play a note, higher to the right.","zh":"第一次點擊把聲音打開，背景變暗，之後每點一下就彈一個音，越右邊音越高。"},"run":"sketch","lang":"js"},{"code":"const KEYS = { a: 60, s: 62, d: 64, f: 65, g: 67, h: 69, j: 71, k: 72 };\nlet dots = [];\n\nfunction setup() {\n  createCanvas(400, 300);\n  noStroke();\n  startSound('epiano');\n}\n\nfunction keyPressed() {\n  if (KEYS[key]) midi.play(KEYS[key], 0.8);\n}\n\nfunction keyReleased() {\n  if (KEYS[key]) midi.play(KEYS[key], 0);\n}\n\nfunction notePressed(n, v) {\n  dots.push({ x: map(n, 60, 72, 30, width - 30), y: height, v });\n}\n\nfunction draw() {\n  background(20);\n  for (const d of dots) {\n    fill(255, 160, 80);\n    circle(d.x, d.y, 20 + d.v * 30);\n    d.y -= 2;\n  }\n  dots = dots.filter((d) => d.y > -40);\n  fill(255);\n  text(soundIsOn ? 'play with a s d f g h j k' : 'press a key once to turn sound on', 20, 24);\n}","caption":{"en":"Press a, s, d, f, g, h, j, k: each key sounds and a dot floats up from the matching place.","zh":"按 a、s、d、f、g、h、j、k：每個鍵都會發聲，並從對應的位置飄起一個圓點。"},"run":"sketch","lang":"js"},{"code":"const IDS = ['synth/pluck', 'organ', 'marimba', 'piano'];\n\nfunction setup() {\n  createCanvas(400, 300);\n  startSound(IDS[0]);\n}\n\nfunction keyPressed() {\n  const i = Number(key) - 1;\n  if (IDS[i]) startSound(IDS[i]);   // switches while it plays\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const s = OAVp5.sound.status;   // the instrument being loaded\n  text('keys 1-4 pick an instrument', 20, 30);\n  text('playing: ' + OAVp5.sound.instrument, 20, 60);\n  text('loading: ' + s.id + ' (' + s.state + ', ' + nf(s.progress * 100, 1, 0) + '%)', 20, 90);\n}","caption":{"en":"Press 4 for the piano: \"loading\" shows its samples arriving while \"playing\" still names the instrument you hear, and flips to piano when it is ready.","zh":"按 4 換鋼琴：「loading」會顯示取樣載入的進度，「playing」仍是你現在聽到的樂器，等準備好才換成 piano。"},"run":"sketch","lang":"js"}],"related":["stopSound","soundIsOn","OAVp5.sound.state","OAVp5.sound.instrument","notePressed","startDrums"],"keywords":{"en":["sound","audio","instrument","piano","organ","synth","play","tone","speaker","hear","make noise"],"zh":["聲音","發聲","樂器","鋼琴","風琴","合成器","彈奏","喇叭","出聲","音色"]},"tbd":[],"claims":[{"expr":"typeof startSound","equals":"function"}]},
{"id":"stopSound","key":"stopSound","name":"stopSound","aliases":["OAVp5.sound.stop"],"kind":"function","package":"oav-p5","category":"sound","documented":true,"signature":"stopSound()","source":{"file":"p5/oav-p5.js","line":1414,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1414"},"summary":{"en":"Turns sound off and lets every ringing note go; the instrument stays loaded, so `startSound()` brings it back at once.","zh":"關掉聲音，讓正在響的音全部放開，樂器還留在記憶體裡，所以再呼叫 `startSound()` 馬上就回來。"},"description":[{"en":"It releases every note that is ringing, sets [`soundIsOn`](#soundIsOn) to `false` and [`OAVp5.sound.state`](#OAVp5.sound.state) to `'off'`. Notes you play afterwards are not heard (the sketch's [`notePressed()`](#notePressed) is still called). Tone.js and the instrument stay loaded: [`startSound()`](#startSound) afterwards is on again at once, without a new download and without waiting for another click.","zh":"它會放開所有正在響的音，把 [`soundIsOn`](#soundIsOn) 設成 `false`，[`OAVp5.sound.state`](#OAVp5.sound.state) 設成 `'off'`。之後彈的音聽不到（草圖的 [`notePressed()`](#notePressed) 照樣會被呼叫）。Tone.js 和樂器都還載著，所以之後再呼叫 [`startSound()`](#startSound) 會馬上開，不用重新下載，也不用再等一次點擊。"},{"en":"It also works while sound is still loading or waiting for the first click: the pending [`startSound()`](#startSound) then resolves to `false`.","zh":"聲音還在載入、或還在等第一次點擊的時候呼叫也可以：還沒結束的 [`startSound()`](#startSound) 會 resolve 成 `false`。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"A promise that is already resolved to `false`, the new state of the sound.","zh":"一個已經 resolve 成 `false` 的 promise，`false` 就是聲音的新狀態。"},"notes":[{"en":"It does not touch the drum machine (use [`stopDrums()`](#stopDrums)) or the microphone ([`stopMic()`](#stopMic)).","zh":"它不會動到鼓機（要用 [`stopDrums()`](#stopDrums)），也不會動到麥克風（[`stopMic()`](#stopMic)）。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('synth/pluck');\n  textAlign(CENTER, CENTER);\n}\n\nfunction keyPressed() {\n  if (key === 's') {\n    if (soundIsOn) stopSound(); else startSound();\n  }\n}\n\nfunction draw() {\n  background(soundIsOn ? 40 : 100);\n  fill(255);\n  text('press s: sound is ' + (soundIsOn ? 'on' : 'off'), width / 2, height / 2);\n}","caption":{"en":"Press s to switch sound off and on; the background is dark while sound is on.","zh":"按 s 把聲音關掉再打開，聲音開著的時候背景是暗的。"},"run":"sketch","lang":"js"}],"related":["startSound","soundIsOn","OAVp5.sound.state","stopDrums","stopMic"],"keywords":{"en":["sound","stop","off","mute","silence","audio","release notes"],"zh":["聲音","停止","關閉","靜音","關掉聲音","放開"]},"tbd":[],"claims":[{"before":"stopSound()","expr":"OAVp5.sound.state","equals":"off"}]},
{"id":"OAVp5.sound","key":"OAVp5.sound","name":"OAVp5.sound","aliases":[],"kind":"object","package":"oav-p5","category":"sound","documented":true,"signature":"OAVp5.sound","source":{"file":"p5/oav-p5.js","line":1325,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1325"},"summary":{"en":"The object behind the sound names: the state, the instrument, the error, the loading progress and the loaded parts of @openav/sound.","zh":"聲音相關名稱背後的物件：狀態、樂器、錯誤、載入進度，以及載入進來的 @openav/sound 各部分。"},"description":[{"en":"`OAVp5.sound` (also reachable as `midi.sound`) is what [`startSound()`](#startSound), [`stopSound()`](#stopSound) and [`soundIsOn`](#soundIsOn) are built on. Read [`state`](#OAVp5.sound.state) to know where sound is, [`instrument`](#OAVp5.sound.instrument) for what you hear, [`status`](#OAVp5.sound.status) for the loading progress and [`error`](#OAVp5.sound.error) for a failure. `start(id)` and `stop()` are the same as the two global functions. [`shell`](#OAVp5.sound.shell), `engine` and `module` hand you the loaded @openav/sound parts for the rare case that you need them.","zh":"`OAVp5.sound`（也可以用 `midi.sound` 取得）是 [`startSound()`](#startSound)、[`stopSound()`](#stopSound) 和 [`soundIsOn`](#soundIsOn) 的底層。讀 [`state`](#OAVp5.sound.state) 知道聲音在哪個階段，[`instrument`](#OAVp5.sound.instrument) 是你聽到的樂器，[`status`](#OAVp5.sound.status) 是載入進度，[`error`](#OAVp5.sound.error) 是失敗的原因。`start(id)` 和 `stop()` 跟那兩個全域函式一模一樣。[`shell`](#OAVp5.sound.shell)、`engine`、`module` 把載入進來的 @openav/sound 各部分交給你，給少數真的需要的情況。"}],"params":[],"notes":[{"en":"For a sketch the global names are enough. This object is for tools, the console and debugging: `OAVp5.sound.state` is the first thing to look at when nothing is heard.","zh":"一般草圖用全域名稱就夠了。這個物件是給工具、主控台和除錯用的：聽不到聲音的時候，先看 `OAVp5.sound.state`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('harp');\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const s = OAVp5.sound;\n  text('state: ' + s.state, 20, 30);\n  text('instrument: ' + s.instrument, 20, 60);\n  text('error: ' + s.error, 20, 90);\n}","caption":{"en":"Three lines of the sound object: the state goes from \"loading\" or \"waiting\" to \"on\" after the first click.","zh":"聲音物件的三個欄位：狀態會從 \"loading\" 或 \"waiting\"，在第一次點擊之後變成 \"on\"。"},"run":"sketch","lang":"js"}],"related":["startSound","stopSound","soundIsOn","OAVp5.sound.state","OAVp5.sound.status","OAVp5.drums","OAVp5.record"],"keywords":{"en":["OAVp5.sound","sound object","midi.sound","sound state","debug","instrument","status"],"zh":["聲音物件","聲音狀態","除錯","樂器","載入進度"]},"tbd":[],"claims":[]},
{"id":"OAVp5.sound.error","key":"OAVp5.sound.error","name":"OAVp5.sound.error","aliases":[],"kind":"variable","package":"oav-p5","category":"sound","documented":true,"signature":"OAVp5.sound.error","source":{"file":"p5/oav-p5.js","line":1318,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1318"},"summary":{"en":"The message of the last thing that went wrong while loading sound, or `null` when nothing has.","zh":"載入聲音的時候，最近一次出錯的訊息，沒出過錯就是 `null`。"},"description":[{"en":"It is the error the browser reported when @openav/sound or Tone.js could not be loaded. A card beside the 🎹 already tells the person at the keyboard what to do (try another network, or point the `tone` option at another copy of Tone.js); read this value to log it or to show your own message. To know whether sound is failing right now, use [`OAVp5.sound.state`](#OAVp5.sound.state), not this.","zh":"這是 @openav/sound 或 Tone.js 載入失敗的時候，瀏覽器回報的錯誤。🎹 旁邊的卡片已經告訴現場的人該怎麼辦（換一個網路，或把 `tone` 選項指到另一份 Tone.js）。讀這個值是為了記錄下來，或顯示你自己的訊息。要知道聲音現在是不是在出錯，看 [`OAVp5.sound.state`](#OAVp5.sound.state)，不要看這個。"}],"value":{"type":"String | null","en":"Set when the state becomes `'error'`. It stays until the page is reloaded: a later try that works does not clear it.","zh":"狀態變成 `'error'` 的時候設定。它會一直留到頁面重新載入：之後重試成功也不會清掉。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('strings');\n  textAlign(CENTER, CENTER);\n}\n\nfunction draw() {\n  background(OAVp5.sound.error ? color(120, 30, 30) : 20);\n  fill(255);\n  text(OAVp5.sound.error ? OAVp5.sound.error : 'no error', width / 2, height / 2);\n}","caption":{"en":"Stays on \"no error\" when sound loads; if Tone.js cannot be reached the background turns red and the message is printed.","zh":"聲音載入正常的時候一直是 \"no error\"，如果連不到 Tone.js，背景會變紅並印出訊息。"},"run":"sketch","lang":"js"}],"related":["OAVp5.sound.state","startSound","soundIsOn"],"keywords":{"en":["sound error","failed","could not load","tone.js","network","debug"],"zh":["聲音錯誤","載入失敗","連不到","網路","除錯"]},"tbd":[],"claims":[{"expr":"OAVp5.sound.error","equals":"null"}]},
{"id":"OAVp5.sound.instrument","key":"OAVp5.sound.instrument","name":"OAVp5.sound.instrument","aliases":[],"kind":"variable","package":"oav-p5","category":"sound","documented":true,"signature":"OAVp5.sound.instrument","source":{"file":"p5/oav-p5.js","line":1318,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1318"},"summary":{"en":"The id of the instrument you hear right now, or `null` before sound has loaded.","zh":"你現在聽到的樂器代號，聲音還沒載入的時候是 `null`。"},"description":[{"en":"After a first [`startSound('piano')`](#startSound) the piano's samples (about 2 MB) download first, and the electric piano plays meanwhile, so the first key is never silent: `instrument` says `'epiano'` until the piano is ready and `'piano'` after. When you switch to the piano from another instrument, that one keeps sounding and `instrument` names it until the samples are in. How far the loading is, and which instrument it is on, is in [`OAVp5.sound.status`](#OAVp5.sound.status).","zh":"第一次呼叫 [`startSound('piano')`](#startSound) 的時候，鋼琴的取樣（約 2 MB）要先下載，這段時間由電鋼琴頂著，所以第一個鍵不會沒聲音：鋼琴準備好之前 `instrument` 是 `'epiano'`，之後才是 `'piano'`。如果是從別的樂器切到鋼琴，取樣到齊之前原本那個樂器繼續響，`instrument` 也還是它的名字。載到哪裡、載的是哪個樂器，看 [`OAVp5.sound.status`](#OAVp5.sound.status)。"}],"value":{"type":"String | null","en":"Changes when the instrument changes, which for a sampled instrument is when its samples are in.","zh":"樂器換了才會變，取樣型的樂器則是等取樣到齊的時候才換。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('piano');\n  textAlign(CENTER, CENTER);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.sound.instrument || 'nothing loaded yet', width / 2, height / 2);\n}","caption":{"en":"Shows the instrument you hear: epiano while the piano samples arrive, then piano.","zh":"顯示你聽到的樂器：鋼琴取樣下載的時候是 epiano，之後變成 piano。"},"run":"sketch","lang":"js"}],"related":["startSound","OAVp5.sound.status","OAVp5.sound.state"],"keywords":{"en":["instrument","current instrument","piano","epiano","preset","sound"],"zh":["樂器","目前樂器","音色","鋼琴","電鋼琴"]},"tbd":[],"claims":[{"expr":"OAVp5.sound.instrument","equals":"null"}]},
{"id":"OAVp5.sound.state","key":"OAVp5.sound.state","name":"OAVp5.sound.state","aliases":[],"kind":"variable","package":"oav-p5","category":"sound","documented":true,"signature":"OAVp5.sound.state","source":{"file":"p5/oav-p5.js","line":1317,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1317"},"summary":{"en":"Where sound is: `'off'`, `'waiting'` (for a click), `'loading'`, `'on'` or `'error'`.","zh":"聲音現在在哪個階段：`'off'`、`'waiting'`（等點擊）、`'loading'`、`'on'` 或 `'error'`。"},"description":[{"en":"`'off'` is where it starts and where [`stopSound()`](#stopSound) leaves it. [`startSound()`](#startSound) moves it to `'loading'` while @openav/sound and Tone.js arrive, to `'waiting'` when everything is ready but the browser has not yet had its click, and to `'on'` when notes are heard. `'error'` means the sound parts could not be loaded (a network, a school firewall or an ad blocker); the message is in [`OAVp5.sound.error`](#OAVp5.sound.error), and calling `startSound()` again tries once more.","zh":"`'off'` 是起點，也是 [`stopSound()`](#stopSound) 之後的狀態。呼叫 [`startSound()`](#startSound) 之後，@openav/sound 和 Tone.js 還在下載的時候是 `'loading'`，東西都到齊但瀏覽器還沒等到那次點擊的時候是 `'waiting'`，聽得到音符的時候是 `'on'`。`'error'` 表示聲音的部分載入失敗（網路、學校的防火牆或廣告攔截器）。訊息在 [`OAVp5.sound.error`](#OAVp5.sound.error)，再呼叫一次 `startSound()` 會重試。"},{"en":"[`soundIsOn`](#soundIsOn) is the same thing reduced to `state === 'on'`.","zh":"[`soundIsOn`](#soundIsOn) 就是把它縮成 `state === 'on'` 的版本。"}],"value":{"type":"String","range":"off | waiting | loading | on | error","en":"Starts as `'off'` and changes whenever the sound moves to another stage.","zh":"一開始是 `'off'`，聲音換到另一個階段的時候就跟著變。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('marimba');\n  textAlign(CENTER, CENTER);\n  textSize(28);\n}\n\nfunction draw() {\n  background(20);\n  const hints = { off: 'off', loading: 'loading…', waiting: 'click to hear', on: 'listening', error: 'failed' };\n  fill(255);\n  text(hints[OAVp5.sound.state], width / 2, height / 2);\n}","caption":{"en":"One word for each stage; \"click to hear\" is what the sketch shows between a ready sound and the first click.","zh":"每個階段一句話，聲音準備好、但還沒點擊的那段時間，畫面會顯示 \"click to hear\"。"},"run":"sketch","lang":"js"}],"related":["soundIsOn","startSound","stopSound","OAVp5.sound.error","OAVp5.sound.status"],"keywords":{"en":["sound state","waiting","loading","error","off","on","status"],"zh":["聲音狀態","等待","載入中","錯誤","開關"]},"tbd":[],"claims":[{"expr":"OAVp5.sound.state","equals":"off"}]},
{"id":"OAVp5.sound.status","key":"OAVp5.sound.status","name":"OAVp5.sound.status","aliases":[],"kind":"object","package":"oav-p5","category":"sound","documented":true,"signature":"OAVp5.sound.status","source":{"file":"p5/oav-p5.js","line":1322,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1322"},"summary":{"en":"How the instrument is loading: `{ id, state, progress, error }`, with `progress` from 0 to 1.","zh":"樂器載入得怎麼樣了：`{ id, state, progress, error }`，其中 `progress` 從 0 到 1。"},"description":[{"en":"It is about the instrument, not about the whole sound: [`OAVp5.sound.state`](#OAVp5.sound.state) is the whole sound. Only the `piano` has files to load, so for every other instrument `progress` is 1 almost at once. While a sampler loads, [`OAVp5.sound.instrument`](#OAVp5.sound.instrument) is still the one you hear and `status.id` is the one on its way (at the very first call it may name the electric piano that stands in, and then the piano).","zh":"它講的是樂器，不是整個聲音：整個聲音的狀態是 [`OAVp5.sound.state`](#OAVp5.sound.state)。只有 `piano` 有檔案要載，所以其他樂器的 `progress` 幾乎馬上就是 1。取樣型樂器載入的時候，[`OAVp5.sound.instrument`](#OAVp5.sound.instrument) 仍是你現在聽到的那個，`status.id` 才是正在路上的那個（第一次呼叫的時候，一開始可能先是頂替用的電鋼琴，之後才是鋼琴）。"}],"value":{"type":"Object","range":"{ id, state: idle | loading | ready | error, progress: 0..1, error }","en":"A new object on every read. `id` is the instrument the loader is working on, `state` is `'idle'` before anything loads, `'loading'`, `'ready'` or `'error'`, and `progress` is the fraction of the files loaded.","zh":"每次讀都是一個新的物件。`id` 是載入器現在處理的樂器，`state` 在還沒載任何東西時是 `'idle'`，之後是 `'loading'`、`'ready'` 或 `'error'`，`progress` 是檔案載入的比例。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startSound('piano');\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const s = OAVp5.sound.status;\n  fill(60);\n  rect(40, 140, 320, 16);\n  fill(120, 220, 140);\n  rect(40, 140, 320 * s.progress, 16);\n  fill(255);\n  text(s.id + ': ' + s.state, 40, 120);\n}","caption":{"en":"A progress bar for the piano samples; it fills within a few seconds and the label changes to \"ready\".","zh":"鋼琴取樣的進度條，幾秒內會填滿，標籤變成 \"ready\"。"},"run":"sketch","lang":"js"}],"related":["OAVp5.sound.instrument","OAVp5.sound.state","startSound"],"keywords":{"en":["loading progress","progress","samples","piano loading","status","ready"],"zh":["載入進度","進度條","取樣","鋼琴載入","準備好"]},"tbd":[],"claims":[]},
{"id":"BUILTIN_INSTRUMENTS","key":"BUILTIN_INSTRUMENTS","name":"BUILTIN_INSTRUMENTS","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"BUILTIN_INSTRUMENTS = array, 22 items","import":"import { BUILTIN_INSTRUMENTS } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":61,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L61"},"summary":{"en":"The 22 instruments that come with the package, as definitions: a grand piano, mallets, strings, harp, organ, choir and 11 synth presets.","zh":"套件內建的 22 個樂器的定義：平台鋼琴、敲擊琴、弦樂、豎琴、風琴、合唱和 11 個合成器預設。"},"description":[{"en":"```text\nid                 category  name\npiano              keys      Grand piano (Salamander), 30 samples, CC-BY 3.0\nepiano             keys      Electric piano (two FM pairs and a tremolo)\nxylophone          mallets   Xylophone (sounds an octave up)\nmarimba            mallets   Marimba\nvibraphone         mallets   Vibraphone (motor tremolo, key-up damps)\nglockenspiel       mallets   Glockenspiel (two octaves up)\nmusic-box          mallets   Music box (two octaves up)\nstrings            strings   String ensemble\nharp               plucked   Harp (Karplus-Strong)\norgan              organ     Drawbar organ (rotary tremolo)\nchoir              voice     Choir pad (aah)\nsynth/pad          synth     Warm pad (the default)\nsynth/saw-lead     synth     Saw lead\nsynth/square-lead  synth     Square lead\nsynth/pluck        synth     Pluck\nsynth/supersaw     synth     Supersaw\nsynth/bass         synth     Analog bass\nsynth/acid         synth     Acid\nsynth/brass        synth     Synth brass\nsynth/bell         synth     FM bell\nsynth/sub          synth     Sub bass\nsynth/chip         synth     Chip (8-bit)\n```","zh":"```text\nid                 類別      名稱\npiano              keys      平台鋼琴（Salamander），30 個取樣，CC-BY 3.0\nepiano             keys      電鋼琴（兩組 FM 加顫音）\nxylophone          mallets   木琴（高一個八度）\nmarimba            mallets   馬林巴\nvibraphone         mallets   顫音琴（馬達顫音，放開鍵會止音）\nglockenspiel       mallets   鐵琴（高兩個八度）\nmusic-box          mallets   音樂盒（高兩個八度）\nstrings            strings   弦樂合奏\nharp               plucked   豎琴（Karplus-Strong）\norgan              organ     音栓風琴（旋轉喇叭顫音）\nchoir              voice     合唱襯底（啊）\nsynth/pad          synth     溫暖襯底（預設）\nsynth/saw-lead     synth     鋸齒波主奏\nsynth/square-lead  synth     方波主奏\nsynth/pluck        synth     撥奏\nsynth/supersaw     synth     超級鋸齒波\nsynth/bass         synth     類比貝斯\nsynth/acid         synth     酸性 303\nsynth/brass        synth     合成銅管\nsynth/bell         synth     FM 鐘聲\nsynth/sub          synth     超低音\nsynth/chip         synth     晶片音（8-bit）\n```"},{"en":"Loading the package registers all of them, except those whose id you registered first. Each one is a small file in the package's `instruments/` folder. The piano loads its samples only when it is picked, and falls back to the electric piano if they cannot be fetched. The organ, the strings, the choir, the electric piano and the vibraphone bring params of their own.","zh":"載入套件就會登記全部，除非你先登記了同樣 id 的樂器。每一個都是套件的 `instruments/` 資料夾裡的一個小檔案。鋼琴只有在被選到的時候才載入取樣，取樣抓不到就退回電鋼琴。風琴、弦樂、合唱、電鋼琴和顫音琴有自己的參數。"}],"value":{"type":"Object[]","en":"An array of 22 instrument definitions, in the order of the table.","zh":"一個有 22 個樂器定義的陣列，順序跟下面的表一樣。"},"params":[],"notes":[],"examples":[{"code":"import { BUILTIN_INSTRUMENTS, getInstrument } from '@openav/sound';\n\nconsole.log(BUILTIN_INSTRUMENTS.length);                                       // 22\nconsole.log(BUILTIN_INSTRUMENTS.slice(0, 3).map((d) => d.id).join(' '));      // piano epiano xylophone\nconsole.log(getInstrument('piano') === BUILTIN_INSTRUMENTS[0]);                // true: they are the registered ones\nconsole.log(getInstrument('organ').params.map((p) => p.key).join(' '));        // drawbars rotor","caption":{"en":"How many built-in instruments there are, the first three, and that the registry holds these very objects. The organ lists the two params of its own.","zh":"有幾個內建樂器、前三個，以及登記表裡放的就是這些物件。風琴列出它自己的兩個參數。"},"run":"module","lang":"js"}],"related":["listInstruments","SYNTH_PRESETS","DEFAULT_INSTRUMENT","registerInstrument","getInstrument"],"keywords":{"en":["built-in","instruments","piano","organ","strings","marimba","vibraphone","choir","harp","what sounds are there"],"zh":["內建","樂器","鋼琴","風琴","弦樂","馬林巴","顫音琴","合唱","豎琴","有哪些聲音"]},"tbd":[],"claims":[]},
{"id":"CATEGORIES","key":"CATEGORIES","name":"CATEGORIES","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"CATEGORIES = array, 9 items","import":"import { CATEGORIES } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":32,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L32"},"summary":{"en":"The instrument categories in picker order: keys, mallets, strings, plucked, organ, voice, synth, drums and other.","zh":"樂器的類別，照選單的順序：keys、mallets、strings、plucked、organ、voice、synth、drums、other。"},"description":[{"en":"The `category` of an instrument definition picks its group in the picker. A category that is not in this list is allowed: it sorts after these, and its label is its id. Every category also has a Chinese name, which the picker uses on a Chinese page.","zh":"樂器定義的 `category` 決定它在選單裡的組別。不在這份清單裡的類別也可以，它排在這些後面，標籤就是它的 id。中文名稱是 鍵盤、敲擊琴、弦樂、撥弦、風琴、人聲、合成器、鼓、其他。"}],"value":{"type":"Object[]","en":"Nine items `{ id, name: { en, zh } }`, such as `{ id: 'mallets', name: { en: 'Mallets', zh: … } }`.","zh":"九個項目 `{ id, name: { en, zh } }`，例如 `{ id: 'mallets', name: { en: 'Mallets', zh: '敲擊琴' } }`。"},"params":[],"notes":[],"examples":[{"code":"import { CATEGORIES } from '@openav/sound';\n\nconsole.log(CATEGORIES.map((c) => c.id).join(' '));   // keys mallets strings plucked organ voice synth drums other\nconsole.log(CATEGORIES[1].name.en, CATEGORIES[1].name.zh);","caption":{"en":"Logs the nine ids in order and the English and Chinese name of the second.","zh":"依序印出九個 id，以及第二個的英文和中文名稱。"},"run":"module","lang":"js"}],"related":["instrumentGroups","registerInstrument","listInstruments"],"keywords":{"en":["categories","groups","keys","mallets","strings","synth","picker order"],"zh":["類別","分組","鍵盤","敲擊琴","弦樂","合成器","選單順序"]},"tbd":[],"claims":[]},
{"id":"DEFAULT_INSTRUMENT","key":"DEFAULT_INSTRUMENT","name":"DEFAULT_INSTRUMENT","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"DEFAULT_INSTRUMENT = \"synth/pad\"","import":"import { DEFAULT_INSTRUMENT } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":59,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L59"},"summary":{"en":"The instrument used when none is named: 'synth/pad', the warm three-saw pad that was the framework's first voice.","zh":"沒指定樂器時用的那個：'synth/pad'，由三個鋸齒波組成的溫暖襯底，是這個框架最早的聲音。"},"description":[{"en":"[`toneEngine()`](#toneEngine) starts on it, and falls back to it when you name an instrument that is not registered. Its sound has not changed since the first version.","zh":"[`toneEngine()`](#toneEngine) 一開始用它，你指定了沒登記的樂器時也退回它。它的聲音從第一版以來沒有變過。"}],"value":{"type":"String","en":"`'synth/pad'`.","zh":"字串 `'synth/pad'`。"},"params":[],"notes":[],"examples":[{"code":"import { DEFAULT_INSTRUMENT, getInstrument, instrumentName, toneEngine } from '@openav/sound';\n\nconsole.log(DEFAULT_INSTRUMENT, '-', instrumentName(getInstrument(DEFAULT_INSTRUMENT)));   // synth/pad - Warm pad\nconsole.log(toneEngine().instrument === DEFAULT_INSTRUMENT);                                 // true","caption":{"en":"Logs the default instrument and its name, and that an engine with no options starts on it.","zh":"印出預設樂器和它的名字，以及沒給任何選項的引擎一開始用的就是它。"},"run":"module","lang":"js"}],"related":["toneEngine","BUILTIN_INSTRUMENTS","SYNTH_PRESETS"],"keywords":{"en":["default instrument","pad","warm pad","synth/pad","start sound"],"zh":["預設樂器","襯底","溫暖襯底","synth/pad","起始聲音"]},"tbd":[],"claims":[]},
{"id":"getInstrument","key":"getInstrument","name":"getInstrument","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"getInstrument(id)","import":"import { getInstrument } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":102,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L102"},"summary":{"en":"Finds a registered instrument definition by its id, or null.","zh":"用 id 找出一個登記過的樂器定義，找不到就是 null。"},"description":[{"en":"Use it to check that an id exists before you pass it to [`toneEngine()`](#toneEngine) or `setInstrument()`, or to read an instrument's `credit`, `defaults` or `params`.","zh":"在把 id 交給 [`toneEngine()`](#toneEngine) 或 `setInstrument()` 之前，用它確認 id 存在，或讀一個樂器的 `credit`、`defaults`、`params`。"}],"params":[{"name":"id","type":"String","en":"The id, for example `'piano'` or `'synth/pluck'`.","zh":"id，例如 `'piano'` 或 `'synth/pluck'`。"}],"returns":{"type":"Object | null","en":"The definition you registered (`{ id, name, category, create, … }`), or `null`.","zh":"你登記的那個定義（`{ id, name, category, create, … }`），或 `null`。"},"notes":[],"examples":[{"code":"import { getInstrument, instrumentName } from '@openav/sound';\n\nconst piano = getInstrument('piano');\nconsole.log(instrumentName(piano), '/', piano.category);   // Grand piano (Salamander) / keys\nconsole.log(piano.credit.text);                             // Salamander Grand Piano · Alexander Holm · CC-BY 3.0\nconsole.log(getInstrument('theremin'));                     // null","caption":{"en":"Reads the name, category and credit of the built-in piano, and shows that an id that is not registered gives `null`.","zh":"讀內建鋼琴的名字、類別和出處說明，並顯示沒登記過的 id 會得到 `null`。"},"run":"module","lang":"js"}],"related":["listInstruments","registerInstrument","instrumentName"],"keywords":{"en":["get instrument","lookup","find","definition","by id","credit"],"zh":["取得樂器","查詢","尋找","定義","用 id","出處說明"]},"tbd":[],"claims":[]},
{"id":"instrumentGroups","key":"instrumentGroups","name":"instrumentGroups","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"instrumentGroups(lang = 'en', list = listInstruments())","import":"import { instrumentGroups } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":118,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L118"},"summary":{"en":"Groups the instruments by category in picker order: [{ id, label, items }], with empty groups left out.","zh":"把樂器依類別分組，照選單的順序：[{ id, label, items }]，沒有樂器的組會省略。"},"description":[{"en":"It is the data behind the picker, so you can build your own menu, a list or a grid of buttons. A category that is not in `CATEGORIES` is allowed; it sorts after the known ones and its `label` is its id.","zh":"這就是選單背後的資料，所以你可以自己做選單、清單或一排按鈕。不在 `CATEGORIES` 裡的類別也可以，它排在已知的類別後面，`label` 就是它的 id。"}],"params":[{"name":"lang","type":"String","optional":true,"default":"'en'","en":"The language of the group labels.","zh":"組別標籤的語言。"},{"name":"list","type":"Object[]","optional":true,"default":"listInstruments()","en":"The instruments to group. Left out, all the registered ones.","zh":"要分組的樂器。沒給的話，就是所有登記過的。"}],"returns":{"type":"Object[]","en":"`[{ id, label, items: [definition…] }]`: the groups in the order of [`CATEGORIES`](#CATEGORIES), then any categories of your own, sorted by name.","zh":"`[{ id, label, items: [定義…] }]`：各組依 [`CATEGORIES`](#CATEGORIES) 的順序，接著是你自己的類別，依名稱排序。"},"notes":[],"examples":[{"code":"import { instrumentGroups } from '@openav/sound';\n\nfor (const g of instrumentGroups('en')) console.log(g.label + ' (' + g.items.length + ')');\n// Keys (2), Mallets (5), Strings (1), Plucked (1), Organ (1), Voice (1), Synth (11)\nconsole.log(instrumentGroups('zh')[1].label);   // the Chinese label of the mallets group","caption":{"en":"The seven groups the built-in instruments make, with how many each holds (no drums yet, so that group is left out), and a group label in Chinese.","zh":"內建樂器組成的七個組，各有幾個樂器（還沒有鼓，所以那一組省略），以及一個中文的組別標籤。"},"run":"module","lang":"js"}],"related":["listInstruments","CATEGORIES","instrumentName","mountSoundPicker"],"keywords":{"en":["groups","categories","optgroup","menu data","picker data","organize"],"zh":["分組","類別","選單資料","整理"]},"tbd":[],"claims":[]},
{"id":"instrumentName","key":"instrumentName","name":"instrumentName","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"instrumentName(def, lang = 'en')","import":"import { instrumentName } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":111,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L111"},"summary":{"en":"Gives an instrument's display name in a language, falling back to English.","zh":"給一個樂器在某種語言下的顯示名稱，沒有就退回英文。"},"description":[{"en":"Instruments carry `name` as a string or as `{ en, zh }`. This hides which, so your own menu can just call `instrumentName(def, lang)`.","zh":"樂器的 `name` 可以是字串，也可以是 `{ en, zh }`。這個函式把差別藏起來，所以你自己的選單只要呼叫 `instrumentName(def, lang)`。"}],"params":[{"name":"def","type":"Object","en":"An instrument definition. `null` or `undefined` gives `''`.","zh":"樂器的定義。`null` 或 `undefined` 會得到 `''`。"},{"name":"lang","type":"String","optional":true,"default":"'en'","en":"`'en'`, `'zh'`… A longer tag such as `'zh-TW'` is tried by its first two letters.","zh":"`'en'`、`'zh'`…。比較長的標記，像 `'zh-TW'`，會用它前兩個字母去找。"}],"returns":{"type":"String","en":"The name in that language, else the English one. A definition whose `name` is a plain string gives that string.","zh":"該語言的名稱，沒有就是英文的。`name` 只是一個字串的定義就回傳那個字串。"},"notes":[],"examples":[{"code":"import { getInstrument, instrumentName } from '@openav/sound';\n\nconst bar = getInstrument('glockenspiel');\nconsole.log(instrumentName(bar), instrumentName(bar, 'zh'));   // Glockenspiel, then its Chinese name\nconsole.log(instrumentName(bar, 'fr'));                         // Glockenspiel, English as the fallback\nconsole.log(JSON.stringify(instrumentName(null)));              // \"\"","caption":{"en":"The name in English, in Chinese, in a language that has no entry (English again), and for nothing at all.","zh":"英文的名字、中文的名字、沒有登記的語言（退回英文），以及什麼都沒有的情況。"},"run":"module","lang":"js"}],"related":["instrumentGroups","getInstrument","mountSoundPicker"],"keywords":{"en":["name","display name","translate","language","localized","label"],"zh":["名稱","顯示名稱","翻譯","語言","在地化","標籤"]},"tbd":[],"claims":[]},
{"id":"listInstruments","key":"listInstruments","name":"listInstruments","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"listInstruments({ category = null } = {})","import":"import { listInstruments } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":105,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L105"},"summary":{"en":"Lists every registered instrument, in the order they were registered, or only the ones of one category.","zh":"列出所有登記過的樂器，照登記的順序，或只列某一個類別的。"},"description":[{"en":"The 22 built-in instruments are registered when `@openav/sound` loads, so a fresh page lists those. For a list shaped for a menu, with the groups in order, use [`instrumentGroups()`](#instrumentGroups).","zh":"22 個內建樂器在 `@openav/sound` 載入的時候就登記了，所以剛打開的頁面列出的就是這些。要一份適合做選單的清單，各組已排好序，用 [`instrumentGroups()`](#instrumentGroups)。"}],"params":[{"name":"category","type":"String","optional":true,"default":"null","en":"In the options `{ category }`: only the instruments of this category (`'mallets'`).","zh":"在選項 `{ category }` 裡：只列這個類別的樂器（`'mallets'`）。"}],"returns":{"type":"Object[]","en":"A new array of instrument definitions.","zh":"一個新的樂器定義陣列。"},"notes":[],"examples":[{"code":"import { listInstruments } from '@openav/sound';\n\nconsole.log(listInstruments().length);                                       // 22\nconsole.log(listInstruments({ category: 'mallets' }).map((d) => d.id).join(' '));\n// xylophone marimba vibraphone glockenspiel music-box\nconsole.log(listInstruments({ category: 'keys' }).map((d) => d.id).join(' '));    // piano epiano","caption":{"en":"How many instruments there are, and which ones are mallets and keys.","zh":"一共有幾個樂器，以及哪些是敲擊琴和鍵盤。"},"run":"module","lang":"js"}],"related":["instrumentGroups","getInstrument","BUILTIN_INSTRUMENTS","CATEGORIES"],"keywords":{"en":["list","all instruments","category","filter","registry","available sounds"],"zh":["列出","所有樂器","類別","篩選","登記表","有哪些聲音"]},"tbd":[],"claims":[]},
{"id":"modalInstrument","key":"modalInstrument","name":"modalInstrument","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"modalInstrument({ id, name, category = 'mallets', credit = null, params, defaults = { cutoff: 8000 }, partials, strike = null, damp = 0, pitchDecay = 0.6, transpose = 0, gain = 0, attack = 0.0015, tremolo = null, maxVoices = 40, set = null, })","import":"import { modalInstrument } from '@openav/sound';","source":{"file":"packages/sound/instruments/modal.js","line":20,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/modal.js#L20"},"summary":{"en":"Makes a struck-bar or tine instrument from data: a list of partials that ring and die away, and a short noise for the mallet.","zh":"用資料做出敲擊的音條或音叉類樂器：一串會響然後衰減的泛音，加上一小段代表槌子的雜訊。"},"description":[{"en":"A xylophone bar, a glockenspiel bar and a music-box tine are the same physics with different numbers: a strike excites a few modes, each rings at its own multiple of the pitch and dies at its own rate. So a mallet instrument here is data, not code. The built-in xylophone is this short:","zh":"木琴的音條、鐵琴的音條和音樂盒的音叉是同樣的物理，只是數字不同：敲一下激發出幾個振動模式，每個模式以自己的音高倍數響，用自己的速度消失。所以這裡的敲擊類樂器是資料，不是程式。內建的木琴就這麼短："},{"en":"```js\nmodalInstrument({\n  id: 'xylophone', name: { en: 'Xylophone', zh: '…' }, transpose: 12,\n  partials: [{ ratio: 1, gain: 1, decay: 0.9 }, { ratio: 3, gain: 0.32, decay: 0.32 }, { ratio: 6.1, gain: 0.1, decay: 0.1 }],\n  strike: { ratio: 5, q: 1.1, gain: 0.35, decay: 0.012 },\n  pitchDecay: 0.75,\n});\n```","zh":"```js\nmodalInstrument({\n  id: 'xylophone', name: { en: 'Xylophone', zh: '木琴' }, transpose: 12,\n  partials: [{ ratio: 1, gain: 1, decay: 0.9 }, { ratio: 3, gain: 0.32, decay: 0.32 }, { ratio: 6.1, gain: 0.1, decay: 0.1 }],\n  strike: { ratio: 5, q: 1.1, gain: 0.35, decay: 0.012 },\n  pitchDecay: 0.75,\n});\n```"},{"en":"Harder strikes brighten: the upper partials grow faster with velocity, as a real mallet does. The voices are raw Web Audio nodes built for each strike and left to the garbage collector.","zh":"打得越用力越亮：高的泛音會隨力度長得比較快，真的槌子就是這樣。每個聲部都是每敲一下就建一組的原生 Web Audio 節點，用完交給垃圾回收。"}],"params":[{"name":"id","type":"String","optional":false,"en":"The instrument id (see [`registerInstrument()`](#registerInstrument)).","zh":"樂器的 id（見 [`registerInstrument()`](#registerInstrument)）。"},{"name":"name","type":"String | Object","optional":false,"en":"The name: a string, or `{ en, zh }`.","zh":"名稱：字串，或 `{ en, zh }`。"},{"name":"category","type":"String","optional":true,"default":"'mallets'","en":"The picker group.","zh":"選單的組別。"},{"name":"credit","type":"String | Object","optional":true,"default":"null","en":"A credit line, a string or `{ text, url }`.","zh":"出處說明，字串或 `{ text, url }`。"},{"name":"params","type":"Object[]","optional":true,"en":"Params of its own: `[{ key, label, min, max, def }]`. Handled by the `set` function.","zh":"它自己的參數：`[{ key, label, min, max, def }]`。由 `set` 函式處理。"},{"name":"defaults","type":"Object","optional":true,"default":"{ cutoff: 8000 }","en":"Starting values for the shared params. The default opens the filter, because bars are bright.","zh":"共用參數的起始值。預設把濾波器打開，因為敲擊的音條聲音是亮的。"},{"name":"partials","type":"Object[]","optional":false,"en":"The modes of the bar: `[{ ratio, gain, decay }]`. `ratio` is the frequency as a multiple of the note (1 is the note itself, 3 a twelfth above it), `gain` the level relative to the others and `decay` the seconds to near silence at middle C. High notes die faster than low ones. A partial above 45% of the sample rate is skipped. Required: without it this throws.","zh":"音條的各個振動模式：`[{ ratio, gain, decay }]`。`ratio` 是頻率相對於音符的倍數（1 是音符本身，3 是高一個十二度），`gain` 是相對於其他泛音的音量，`decay` 是在中央 C 衰減到幾乎無聲要幾秒。高音比低音消失得快。高於取樣率 45% 的泛音會被略過。必須給：沒給的話會丟出錯誤。"},{"name":"strike","type":"Object","optional":true,"default":"null","en":"The mallet, a few milliseconds of band-passed noise: `{ ratio, q, gain, decay }`, where `ratio` sets its pitch as a multiple of the note, `q` its width, `gain` its level and `decay` its length in seconds.","zh":"槌子，幾毫秒的帶通雜訊：`{ ratio, q, gain, decay }`，`ratio` 是它的音高相對於音符的倍數，`q` 是寬度，`gain` 是音量，`decay` 是長度，單位是秒。"},{"name":"damp","type":"Number","optional":true,"default":"0","en":"Seconds in which letting go of a key mutes the bar. 0 lets the bars ring free (xylophone, glockenspiel); a vibraphone has a damper.","zh":"放開鍵之後幾秒內把音條止音。0 是讓音條自由響完（木琴、鐵琴）。顫音琴有止音器。"},{"name":"pitchDecay","type":"Number","optional":true,"default":"0.6","en":"How much faster high notes die than low ones. The decay is scaled by `(261.63 / frequency) ^ pitchDecay`, held between 0.2 and 3. 0 makes every note last the same.","zh":"高音比低音消失得快多少。衰減時間會乘上 `(261.63 / 頻率) ^ pitchDecay`，限制在 0.2 到 3 之間。0 讓每個音一樣長。"},{"name":"transpose","type":"Number","optional":true,"default":"0","en":"Semitones to shift what is played (the xylophone sounds an octave up: 12).","zh":"把演奏的音移幾個半音（木琴高一個八度：12）。"},{"name":"gain","type":"Number","optional":true,"default":"0","en":"The instrument's level, in dB.","zh":"樂器的音量，單位是 dB。"},{"name":"attack","type":"Number","optional":true,"default":"0.0015","en":"Seconds the partials take to reach full level.","zh":"泛音達到全音量要幾秒。"},{"name":"tremolo","type":"Object","optional":true,"default":"null","en":"A level wobble over all voices, the vibraphone's motor: `{ frequency, depth, spread }`.","zh":"所有聲部一起的音量起伏，也就是顫音琴的馬達：`{ frequency, depth, spread }`。"},{"name":"maxVoices","type":"Number","optional":true,"default":"40","en":"How many notes can ring at once. The oldest is faded out in 30 ms when it is exceeded.","zh":"最多能同時響幾個音。超過的時候，最舊的會在 30 毫秒內淡出。"},{"name":"set","type":"Function","optional":true,"default":"null","en":"`set({ key, value, tremolo, Tone })`, called when one of the instrument's own params changes, so it can change the tremolo or anything else.","zh":"`set({ key, value, tremolo, Tone })`，樂器自己的參數改變時呼叫，好讓它改顫音或別的東西。"}],"returns":{"type":"Object","en":"An instrument definition, not yet registered: pass it to [`registerInstrument()`](#registerInstrument). `tail` is set to `min(8, longest decay × 1.5 + 0.2)` seconds.","zh":"一個還沒登記的樂器定義：交給 [`registerInstrument()`](#registerInstrument)。`tail` 設成 `min(8, 最長的衰減 × 1.5 + 0.2)` 秒。"},"notes":[],"examples":[{"code":"import { modalInstrument, registerInstrument, validateInstrument, getInstrument, unregisterInstrument } from '@openav/sound';\n\nconst bar = modalInstrument({\n  id: 'me/steel-bar', name: { en: 'Steel bar', zh: '鋼片' },\n  partials: [{ ratio: 1, gain: 1, decay: 2.5 }, { ratio: 2.76, gain: 0.4, decay: 1.2 }],\n  strike: { ratio: 6, q: 1, gain: 0.3, decay: 0.01 },\n});\nconsole.log(bar.id, bar.category, bar.tail, validateInstrument(bar));   // me/steel-bar mallets 3.95 []\nregisterInstrument(bar);                                                  // now it is in every picker, under Mallets\nconsole.log(getInstrument('me/steel-bar') === bar);                       // true\nunregisterInstrument('me/steel-bar');","caption":{"en":"Two partials and a mallet make a steel-bar instrument that validates and registers. It is only built, and heard, once the sound engine plays it.","zh":"兩個泛音加一個槌子，就做出一個鋼片樂器，驗證通過也能登記。要等發聲引擎真的播它的時候，它才會被建出來、被聽見。"},"run":"module","lang":"js"}],"related":["registerInstrument","registerSynthPreset","validateInstrument","BUILTIN_INSTRUMENTS"],"keywords":{"en":["modal synthesis","mallet","xylophone","marimba","vibraphone","glockenspiel","bar","partials","music box","percussion"],"zh":["模態合成","敲擊","木琴","馬林巴","顫音琴","鐵琴","音條","泛音","音樂盒","打擊樂器"]},"tbd":[],"claims":[]},
{"id":"mountSoundPicker","key":"mountSoundPicker","name":"mountSoundPicker","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"mountSoundPicker(container, sound, { lang = (globalThis.document?.documentElement?.lang || 'en').toLowerCase().startsWith('zh') ? 'zh' : 'en', className = 'oav-sound-pick', label = null, enableOnPick = true, } = {})","import":"import { mountSoundPicker } from '@openav/sound';","source":{"file":"packages/sound/picker.js","line":18,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/picker.js#L18"},"summary":{"en":"Puts an instrument menu on the page: one group per category, a loading bar for samplers and the credit line an instrument asks for.","zh":"在頁面上放一個樂器選單：每個類別一組，取樣樂器有載入進度條，也會顯示樂器要求的出處說明。"},"description":[{"en":"A native `<select>` with one `<optgroup>` per category ([`CATEGORIES`](#CATEGORIES)), so it works with a keyboard and on a phone and needs no styling to be usable. It lists whatever is registered, and it refreshes itself when an instrument is registered or removed ([`onInstrumentsChange()`](#onInstrumentsChange)). Choosing one calls `sound.setInstrument(id, { remember: true })`. While a sampler loads it shows a bar and a percentage; if the load fails it says so, and says which instrument plays instead. A credit line appears under the menu for an instrument that has one: the Salamander piano is CC-BY, and its credit is always shown.","zh":"這是原生的 `<select>`，每個類別（[`CATEGORIES`](#CATEGORIES)）一個 `<optgroup>`，所以用鍵盤、在手機上都能用，不用做任何樣式就能用。它列出所有已登記的樂器，而且在有樂器被登記或移除時自動更新（[`onInstrumentsChange()`](#onInstrumentsChange)）。選了一個就呼叫 `sound.setInstrument(id, { remember: true })`。取樣樂器載入的時候顯示進度條和百分比。載入失敗會說出來，並說明改播哪個樂器。有出處說明的樂器，選單底下會出現一行：Salamander 鋼琴是 CC-BY，它的出處說明一定會顯示。"}],"params":[{"name":"container","type":"HTMLElement","en":"The element to put the picker in.","zh":"要放選單的元素。"},{"name":"sound","type":"Sound","en":"The [`Sound`](#Sound) to control. The menu follows its instrument and its loading.","zh":"要控制的 [`Sound`](#Sound)。選單會跟著它的樂器和載入狀態走。"},{"name":"lang","type":"'en' | 'zh'","optional":true,"default":"from <html lang>","en":"The language of the labels and instrument names. The default is `'zh'` when the page's `<html lang>` starts with `zh`, else `'en'`.","zh":"標籤和樂器名稱的語言。預設是頁面的 `<html lang>` 以 `zh` 開頭就用 `'zh'`，否則用 `'en'`。"},{"name":"className","type":"String","optional":true,"default":"'oav-sound-pick'","en":"The class of the picker. The parts get `-row`, `-label`, `-status`, `-meter` and `-credit` added to it, for your own styling. The picker brings no CSS.","zh":"選單的 class。各個部分會在它後面加上 `-row`、`-label`、`-status`、`-meter`、`-credit`，讓你自己寫樣式。選單本身不帶 CSS。"},{"name":"label","type":"String","optional":true,"default":"null","en":"The label in front of the menu. Left out, it is the word \"instrument\" (in Chinese when `lang` is `'zh'`).","zh":"選單前面的標籤。沒給的話，依語言是 \"instrument\" 或「樂器」。"},{"name":"enableOnPick","type":"Boolean","optional":true,"default":"true","en":"Choosing an instrument also calls `sound.enable()` if the sound is off. A choice is a click, which is the gesture a browser wants before it plays audio.","zh":"選了樂器之後，如果聲音還沒開，就順便呼叫 `sound.enable()`。選擇本身就是一次點擊，正是瀏覽器要的那個動作。"}],"returns":{"type":"Object","en":"`{ el, select, refresh(), dispose() }`: the element it made, its `<select>`, a function that rebuilds the list, and a function that takes it away.","zh":"`{ el, select, refresh(), dispose() }`：它做出來的元素、裡面的 `<select>`、重建清單的函式，和把它拆掉的函式。"},"notes":[],"examples":[{"code":"import { mountSoundPicker, registerInstrument, unregisterInstrument, Sound } from '@openav/sound';\nimport { Signals } from '@openav/core';\n\nconst engine = {                                     // a stand-in engine with instruments, so nothing is loaded\n  instrument: 'synth/pad', params: [], enable() {}, noteOn() {}, noteOff() {}, set() {},\n  setInstrument: async (id) => { engine.instrument = id; return true; }, subscribe: () => () => {},\n};\nconst sound = new Sound({ signals: new Signals(), engine, remember: false });\nconst picker = mountSoundPicker(document.getElementById('app'), sound, { lang: 'en' });\nconsole.log(picker.select.options.length, picker.select.value);    // 22 synth/pad\n\nregisterInstrument({ id: 'me/kalimba', name: 'Kalimba', category: 'mallets', create: () => ({ noteOn() {}, noteOff() {}, dispose() {} }) });\nconsole.log(picker.select.options.length);                          // 23: the menu refreshed itself\nunregisterInstrument('me/kalimba');","caption":{"en":"A menu of the 22 built-in instruments, in groups, appears on the page. Registering one more adds it to the menu at once.","zh":"頁面上出現一個選單，22 個內建樂器分組列在裡面。再登記一個，它馬上就加進選單。"},"run":"module","lang":"js"}],"related":["Sound","instrumentGroups","onInstrumentsChange","registerInstrument","CATEGORIES"],"keywords":{"en":["picker","instrument menu","select","choose instrument","dropdown","ui","credit","loading progress"],"zh":["選單","樂器選單","選擇樂器","下拉","介面","出處說明","載入進度"]},"tbd":[],"claims":[]},
{"id":"onInstrumentsChange","key":"onInstrumentsChange","name":"onInstrumentsChange","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"onInstrumentsChange(cb)","import":"import { onInstrumentsChange } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":129,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L129"},"summary":{"en":"Calls your function whenever an instrument is registered or removed, so a menu can refresh. Returns a function that stops listening.","zh":"每當有樂器被登記或移除，就呼叫你的函式，讓選單可以更新。回傳一個停止監聽的函式。"},"description":[{"en":"[`mountSoundPicker()`](#mountSoundPicker) listens this way, which is why an instrument registered after the picker was built still shows up. A replacement with `{ replace: true }` also reports `register`. A call to `unregisterInstrument()` for an id that is not there reports nothing.","zh":"[`mountSoundPicker()`](#mountSoundPicker) 就是這樣在聽，所以選單蓋好之後才登記的樂器也會出現。用 `{ replace: true }` 取代也會通報 `register`。對不存在的 id 呼叫 `unregisterInstrument()` 不會通報。"}],"params":[{"name":"cb","type":"Function","en":"`cb({ type, id })` with `type` `'register'` or `'unregister'`. An error inside it is logged and does not stop the others.","zh":"`cb({ type, id })`，`type` 是 `'register'` 或 `'unregister'`。裡面出錯只會被記在 console，不會影響其他監聽者。"}],"returns":{"type":"Function","en":"Call it to stop listening.","zh":"呼叫它就停止監聽。"},"notes":[],"examples":[{"code":"import { onInstrumentsChange, registerInstrument, unregisterInstrument } from '@openav/sound';\n\nconst seen = [];\nconst stop = onInstrumentsChange((e) => seen.push(e.type + ' ' + e.id));\nconst def = { id: 'me/x', name: 'X', category: 'other', create: () => ({ noteOn() {}, noteOff() {}, dispose() {} }) };\nregisterInstrument(def);\nunregisterInstrument('me/x');\nstop();\nregisterInstrument(def); unregisterInstrument('me/x');        // nobody is listening now\nconsole.log(seen.join(' | '));                                  // register me/x | unregister me/x","caption":{"en":"Records two changes while listening, then stops listening and shows that the next two are not reported.","zh":"聽的時候記下兩次變化，然後停止監聽，顯示接下來的兩次不會被通報。"},"run":"module","lang":"js"}],"related":["registerInstrument","unregisterInstrument","mountSoundPicker"],"keywords":{"en":["listen","subscribe","change event","register event","refresh","observer"],"zh":["監聽","訂閱","變化事件","登記事件","更新","觀察者"]},"tbd":[],"claims":[]},
{"id":"registerInstrument","key":"registerInstrument","name":"registerInstrument","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"registerInstrument(def, { replace = false } = {})","import":"import { registerInstrument } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":86,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L86"},"summary":{"en":"Adds a sound to every picker on the page: an id, a name, a category and one create() function that builds the instrument.","zh":"把一個聲音加到頁面上每個選單：一個 id、一個名字、一個類別，和一個建立樂器的 create() 函式。"},"description":[{"en":"A sound you can pick is plain data plus one factory. Registering it is all it takes: import the file before you start, and every [`mountSoundPicker()`](#mountSoundPicker) on the page lists it under its category, with no change to the package. The registry lives on `globalThis`, so two copies of this module on one page share one list. Built-in instruments only fill empty slots: register your own `'piano'` first and you keep it. The definition:","zh":"能選的聲音就是一份資料加一個工廠函式。登記它就夠了：在開始之前匯入那個檔案，頁面上每個 [`mountSoundPicker()`](#mountSoundPicker) 就會把它列在它的類別底下，不用改套件。登記表放在 `globalThis` 上，所以同一個頁面上有兩份這個模組也共用同一份清單。內建的樂器只會填空位：先登記你自己的 `'piano'`，就會保留你的。定義如下："},{"en":"```text\nid          lowercase letters, digits and \"-\", at most one \"/\": 'kalimba', 'me/kalimba'\nname        a string, or { en, zh }\ncategory    keys · mallets · strings · plucked · organ · voice · synth · drums · other, or your own\ncreate      create(Tone, ctx) → { noteOn(note, vel01, time), noteOff(note, time), set?(key, v),\n                                  releaseAll?(time), ready?: Promise, dispose() }\nparams      your own knobs [{ key, label, min, max, def }], shown as sound/<key> while it plays\ndefaults    starting values for the shared params: { cutoff, space, volume }\ncredit      a string, or { text, url }, shown in the picker\ngain        level in dB      transpose   semitones      tail   seconds the old one may ring after a switch\nfallback    the id of an instrument to play if `ready` rejects\n```","zh":"```text\nid          小寫字母、數字和 \"-\"，最多一個 \"/\"：'kalimba'、'me/kalimba'\nname        一個字串，或 { en, zh }\ncategory    keys · mallets · strings · plucked · organ · voice · synth · drums · other，或你自己的\ncreate      create(Tone, ctx) → { noteOn(note, vel01, time), noteOff(note, time), set?(key, v),\n                                  releaseAll?(time), ready?: Promise, dispose() }\nparams      你自己的旋鈕 [{ key, label, min, max, def }]，播放時顯示成 sound/<key>\ndefaults    共用參數的起始值：{ cutoff, space, volume }\ncredit      一個字串，或 { text, url }，顯示在選單裡\ngain        音量，單位 dB      transpose   半音數      tail   換樂器之後舊的可以再響幾秒\nfallback    如果 `ready` 失敗，要改播的樂器 id\n```"},{"en":"`create(Tone, ctx)` runs on enable and on every switch to the instrument, with Tone.js already loaded. `ctx` is `{ output, rawOutput, rawContext, baseUrl, options, onProgress }`: connect Tone nodes to `output` (raw Web Audio nodes to `rawOutput`), `baseUrl` is where sample folders live, `options` is what was passed in `toneEngine({ instruments: { <id>: … } })`, and `onProgress(0..1)` shows a loading bar. If `ready` is a promise, the picker shows progress and the old sound keeps playing until it resolves. An instrument that makes mallet sounds from data can use [`modalInstrument()`](#modalInstrument), and one that is a Tone synth with settings, [`registerSynthPreset()`](#registerSynthPreset).","zh":"`create(Tone, ctx)` 在啟用的時候、以及每次切換到這個樂器的時候執行，那時 Tone.js 已經載入。`ctx` 是 `{ output, rawOutput, rawContext, baseUrl, options, onProgress }`：把 Tone 的節點接到 `output`（原生 Web Audio 節點接到 `rawOutput`），`baseUrl` 是取樣資料夾的位置，`options` 是 `toneEngine({ instruments: { <id>: … } })` 傳進來的東西，`onProgress(0..1)` 用來顯示載入進度。如果 `ready` 是 promise，選單會顯示進度，而且在它 resolve 之前舊的聲音繼續播。從資料做敲擊聲的樂器可以用 [`modalInstrument()`](#modalInstrument)，是有設定的 Tone 合成器就用 [`registerSynthPreset()`](#registerSynthPreset)。"}],"params":[{"name":"def","type":"Object","en":"The instrument definition, see the table below.","zh":"樂器的定義，見下面的表。"},{"name":"replace","type":"Boolean","optional":true,"default":"false","en":"In the options `{ replace }`: allow replacing an instrument that has the same id. Without it a duplicate id throws.","zh":"在選項 `{ replace }` 裡：允許取代同樣 id 的樂器。沒有它的話，重複的 id 會丟出錯誤。"}],"returns":{"type":"Object","en":"The definition you gave. It throws a `TypeError` listing everything wrong if the definition is not valid (see [`validateInstrument()`](#validateInstrument)), and an `Error` for a duplicate id.","zh":"你給的那個定義。定義不合格的話會丟出 `TypeError`，列出所有的問題（見 [`validateInstrument()`](#validateInstrument))，id 重複則丟出 `Error`。"},"notes":[],"examples":[{"code":"import { registerInstrument, getInstrument, listInstruments, unregisterInstrument } from '@openav/sound';\n\nconst def = {\n  id: 'me/kalimba', name: { en: 'Kalimba', zh: '拇指琴' }, category: 'mallets',\n  create: () => ({ noteOn() {}, noteOff() {}, dispose() {} }),     // a silent stand-in for the example\n};\nregisterInstrument(def);\nconsole.log(getInstrument('me/kalimba') === def);                   // true\nconsole.log(listInstruments({ category: 'mallets' }).map((d) => d.id).join(' '));\n// xylophone marimba vibraphone glockenspiel music-box me/kalimba\n\ntry { registerInstrument(def); } catch (e) { console.log(e.message); }\n// [sound] instrument \"me/kalimba\" is already registered; pass { replace: true } to swap it on purpose\nregisterInstrument({ ...def, name: 'Kalimba 2' }, { replace: true });   // on purpose\nunregisterInstrument('me/kalimba');","caption":{"en":"Registers an instrument, finds it in its category, is refused when it registers the same id twice, and swaps it on purpose with `replace`.","zh":"登記一個樂器、在它的類別裡找到它、重複登記同一個 id 會被拒絕，再用 `replace` 刻意換掉。"},"run":"module","lang":"js"},{"code":"import { registerInstrument } from '@openav/sound';\n\nregisterInstrument({\n  id: 'kalimba',\n  name: { en: 'Kalimba', zh: '拇指琴' },\n  category: 'mallets',\n  params: [{ key: 'decay', label: 'Decay (s)', min: 0.2, max: 4, def: 1.2 }],   // becomes sound/decay\n  create(Tone, { output }) {\n    const synth = new Tone.PolySynth(Tone.FMSynth, {\n      harmonicity: 5.1, modulationIndex: 2,\n      envelope: { attack: 0.001, decay: 1.2, sustain: 0, release: 1 },\n    }).connect(output);\n    const hz = (note) => 440 * 2 ** ((note - 69) / 12);\n    return {\n      noteOn: (note, vel, time) => synth.triggerAttack(hz(note), time, vel),\n      noteOff: (note, time) => synth.triggerRelease(hz(note), time),\n      set: (key, value) => key === 'decay' && synth.set({ envelope: { decay: value } }),\n      dispose: () => synth.dispose(),\n    };\n  },\n});","caption":{"en":"A whole instrument in one file: an FM synth with a knob of its own. Import it before the sound starts, and \"Kalimba\" appears under Mallets in every picker.","zh":"一個完整的樂器放在一個檔案裡：一個有自己旋鈕的 FM 合成器。在聲音開始之前匯入它，每個選單的「敲擊琴」底下就會出現 Kalimba。"},"run":"static","lang":"js"}],"related":["validateInstrument","unregisterInstrument","getInstrument","modalInstrument","registerSynthPreset","toneEngine","CATEGORIES"],"keywords":{"en":["register","instrument","add a sound","custom instrument","plugin","create","definition","kalimba","extend"],"zh":["登記","樂器","加一個聲音","自訂樂器","外掛","建立","定義","拇指琴","擴充"]},"tbd":[],"claims":[]},
{"id":"registerSynthPreset","key":"registerSynthPreset","name":"registerSynthPreset","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"registerSynthPreset(preset, opts)","import":"import { registerSynthPreset } from '@openav/sound';","source":{"file":"packages/sound/instruments/synth.js","line":180,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/synth.js#L180"},"summary":{"en":"Adds a synth preset to every picker as synth/ plus its id: describe the voice, the envelope and the effects as data.","zh":"把一個合成器預設加到每個選單，名稱是 synth/ 加上它的 id：把聲部、包絡和效果寫成資料就好。"},"description":[{"en":"A preset names a `voice`, its settings in Tone.js's vocabulary, and an optional effects chain, and nothing else. `id` is lowercase letters, digits and `-` (it becomes `synth/<id>`), `name` is a string or `{ en, zh }`. `voice` is `'synth'` (oscillator and envelope), `'mono'` (adds a resonant filter with its own envelope), `'fm'` or `'am'`. The Tone settings are `oscillator`, `envelope`, `filter`, `filterEnvelope`, `harmonicity`, `modulationIndex`, `modulation`, `modulationEnvelope`, `detune` and `portamento`. The rest: `octave` (a whole number of octaves, `-1` for a bass), `gain` (dB, to level it against the others), `polyphony`, `defaults` (starting `cutoff`, `space`, `volume`), `params`, `category` and `credit`, as in [`registerInstrument()`](#registerInstrument).","zh":"一個預設只有這些：一個 `voice`、用 Tone.js 的詞彙寫的設定，和選用的效果鏈。`id` 是小寫字母、數字和 `-`（會變成 `synth/<id>`），`name` 是字串或 `{ en, zh }`。`voice` 是 `'synth'`（振盪器加包絡）、`'mono'`（多一個有自己包絡的共振濾波器）、`'fm'` 或 `'am'`。Tone 的設定有 `oscillator`、`envelope`、`filter`、`filterEnvelope`、`harmonicity`、`modulationIndex`、`modulation`、`modulationEnvelope`、`detune`、`portamento`。其他的：`octave`（整數個八度，貝斯用 `-1`）、`gain`（dB，用來跟別的樂器配音量）、`polyphony`、`defaults`（起始的 `cutoff`、`space`、`volume`）、`params`、`category`、`credit`，跟 [`registerInstrument()`](#registerInstrument) 一樣。"},{"en":"`effects` is a list of `{ type, …settings }` that the sound passes through in order. The types are `chorus`, `delay`, `pingpong`, `distortion`, `vibrato`, `tremolo`, `phaser`, `autofilter`, `filter`, `eq`, `compressor`, `chebyshev` and `widener`; the other keys are the effect's own Tone settings, including `wet`. A preset that is wrong throws a `TypeError` with the reasons.","zh":"`effects` 是一串 `{ type, …設定 }`，聲音依序通過它們。種類有 `chorus`、`delay`、`pingpong`、`distortion`、`vibrato`、`tremolo`、`phaser`、`autofilter`、`filter`、`eq`、`compressor`、`chebyshev`、`widener`。其他的鍵是那個效果自己的 Tone 設定，包括 `wet`。不合格的預設會丟出 `TypeError` 並說明原因。"}],"params":[{"name":"preset","type":"Object","en":"The preset: `{ id, name, voice, … }`, see below.","zh":"預設：`{ id, name, voice, … }`，見下面。"},{"name":"opts","type":"Object","optional":true,"en":"The options of [`registerInstrument()`](#registerInstrument): `{ replace: true }` swaps an existing one.","zh":"[`registerInstrument()`](#registerInstrument) 的選項：`{ replace: true }` 會換掉現有的。"}],"returns":{"type":"Object","en":"The instrument definition that was registered ([`synthInstrument()`](#synthInstrument) of the preset).","zh":"被登記的那個樂器定義（這個預設的 [`synthInstrument()`](#synthInstrument)）。"},"notes":[],"examples":[{"code":"import { registerSynthPreset, getInstrument, unregisterInstrument } from '@openav/sound';\n\nconst def = registerSynthPreset({\n  id: 'glass', name: { en: 'Glass', zh: '玻璃' },\n  voice: 'fm', harmonicity: 3.01, modulationIndex: 14,\n  envelope: { attack: 0.002, decay: 1.4, sustain: 0, release: 1.2 },\n  effects: [{ type: 'chorus', frequency: 1.5, depth: 0.4, wet: 0.3 }],\n});\nconsole.log(def.id, getInstrument('synth/glass') === def);   // synth/glass true: it is in every picker now\nunregisterInstrument('synth/glass');","caption":{"en":"Registers an FM bell-like sound as plain data. It appears under Synth in the picker as \"Glass\".","zh":"把一個像 FM 鐘的聲音當作單純的資料登記。它會以「Glass」出現在選單的 Synth 組裡。"},"run":"module","lang":"js"}],"related":["synthInstrument","SYNTH_PRESETS","validateSynthPreset","registerInstrument","modalInstrument"],"keywords":{"en":["synth preset","add a synth","fm","envelope","effects","chorus","delay","custom synth","tone.js"],"zh":["合成器預設","加合成器","fm","包絡","效果","合唱效果","延遲","自訂合成器","tone.js"]},"tbd":[],"claims":[]},
{"id":"SALAMANDER_NOTES","key":"SALAMANDER_NOTES","name":"SALAMANDER_NOTES","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"SALAMANDER_NOTES = array, 30 items","import":"import { SALAMANDER_NOTES } from '@openav/sound';","source":{"file":"packages/sound/instruments/piano.js","line":20,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/piano.js#L20"},"summary":{"en":"The 30 notes the Salamander piano has samples for: every minor third from A0 (MIDI 21) to C8 (108).","zh":"Salamander 鋼琴有取樣的 30 個音：從 A0（MIDI 21）到 C8（108），每隔一個小三度一個。"},"description":[{"en":"The sampler shifts the pitch of the notes in between by at most a semitone and a half, which is inaudible on a piano. There is only one velocity layer, so loud against soft is a gain curve, not a different hammer.","zh":"取樣器把中間的音升降最多一個半音半，在鋼琴上聽不出來。只有一個力度層，所以大聲和小聲是音量曲線，不是不同的琴槌。"}],"value":{"type":"Number[]","en":"`[21, 24, 27, …, 108]`: 30 MIDI note numbers, three semitones apart (the A, C, D♯ and F♯ of every octave).","zh":"`[21, 24, 27, …, 108]`：30 個 MIDI 音符號碼，彼此相隔三個半音（每個八度的 A、C、D♯、F♯）。"},"params":[],"notes":[],"examples":[{"code":"import { SALAMANDER_NOTES } from '@openav/sound';\n\nconsole.log(SALAMANDER_NOTES.length, SALAMANDER_NOTES[0], SALAMANDER_NOTES.at(-1));   // 30 21 108\nconsole.log(SALAMANDER_NOTES.slice(0, 4).join(' '));                                    // 21 24 27 30","caption":{"en":"The count, the lowest and highest notes, and the first four.","zh":"數量、最低和最高的音，以及最前面四個。"},"run":"module","lang":"js"}],"related":["salamanderMap","SAMPLES_URL"],"keywords":{"en":["salamander","sampled notes","a0","c8","minor third","piano range"],"zh":["salamander","取樣的音","a0","c8","小三度","鋼琴音域"]},"tbd":[],"claims":[]},
{"id":"salamanderMap","key":"salamanderMap","name":"salamanderMap","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"salamanderMap(notes = SALAMANDER_NOTES)","import":"import { salamanderMap } from '@openav/sound';","source":{"file":"packages/sound/instruments/piano.js","line":23,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/piano.js#L23"},"summary":{"en":"Builds the sample map of the Salamander piano for Tone.Sampler: { 'A0': 'A0.mp3', 'D#1': 'Ds1.mp3', … }.","zh":"做出 Salamander 鋼琴給 Tone.Sampler 用的取樣對照表：{ 'A0': 'A0.mp3', 'D#1': 'Ds1.mp3', … }。"},"description":[{"en":"The piano instrument uses it to fetch its files from `baseUrl + 'salamander/'`. Use it if you want to load the same files into a sampler of your own.","zh":"鋼琴樂器用它從 `baseUrl + 'salamander/'` 抓檔案。想把同樣的檔案載入你自己的取樣器時可以用它。"}],"params":[{"name":"notes","type":"Number[]","optional":true,"default":"SALAMANDER_NOTES","en":"Which MIDI notes to include. The default is [`SALAMANDER_NOTES`](#SALAMANDER_NOTES), all 30.","zh":"要包含哪些 MIDI 音符。預設是 [`SALAMANDER_NOTES`](#SALAMANDER_NOTES)，全部 30 個。"}],"returns":{"type":"Object","en":"An object from Tone note names to file names. The files spell a sharp as \"s\": `D#1` is `Ds1.mp3`.","zh":"一個物件，從 Tone 的音名對應到檔名。檔名把升記號寫成 \"s\"：`D#1` 是 `Ds1.mp3`。"},"notes":[],"examples":[{"code":"import { salamanderMap, SAMPLES_URL } from '@openav/sound';\n\nconst map = salamanderMap();\nconsole.log(Object.keys(map).length, map['A0'], map['D#1']);   // 30 A0.mp3 Ds1.mp3\nconsole.log(salamanderMap([60, 63]));                            // { C4: 'C4.mp3', 'D#4': 'Ds4.mp3' }\nconsole.log(SAMPLES_URL + 'salamander/' + map['C4']);            // the address of one file","caption":{"en":"The full map has 30 files; a map of two notes; and the full address of one sample.","zh":"完整的對照表有 30 個檔案。只有兩個音的對照表。以及一個取樣的完整位址。"},"run":"module","lang":"js"}],"related":["SALAMANDER_NOTES","SAMPLES_URL","BUILTIN_INSTRUMENTS"],"keywords":{"en":["salamander","piano samples","sample map","sampler","mp3","tone.sampler"],"zh":["salamander","鋼琴取樣","取樣對照表","取樣器","mp3","tone.sampler"]},"tbd":[],"claims":[]},
{"id":"SAMPLES_URL","key":"SAMPLES_URL","name":"SAMPLES_URL","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"SAMPLES_URL = \"https://openaudiovisual.com/packages/sound/samples/\"","import":"import { SAMPLES_URL } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":57,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L57"},"summary":{"en":"The address of the folder with the built-in sample sets, when nobody says otherwise: openaudiovisual.com/packages/sound/samples/.","zh":"沒有人指定時，內建取樣組所在資料夾的位址：openaudiovisual.com/packages/sound/samples/。"},"description":[{"en":"It is the default `baseUrl` of [`toneEngine()`](#toneEngine). The piano reads its 30 mp3 files from the `salamander/` folder under it. The server sends CORS headers, so any page can load them. `createShow()` points the engine at the copy next to the page instead. To use your own copy, give `baseUrl`, or `instruments: { piano: { baseUrl } }` for the piano alone.","zh":"它是 [`toneEngine()`](#toneEngine) 的 `baseUrl` 預設值。鋼琴從它底下的 `salamander/` 資料夾讀 30 個 mp3 檔。伺服器有送 CORS 標頭，所以任何頁面都能載入。`createShow()` 會改指向頁面旁邊的那一份。要用自己的備份，就給 `baseUrl`，只想換鋼琴的話給 `instruments: { piano: { baseUrl } }`。"}],"value":{"type":"String","en":"`'https://openaudiovisual.com/packages/sound/samples/'`, ending in a slash.","zh":"`'https://openaudiovisual.com/packages/sound/samples/'`，以斜線結尾。"},"params":[],"notes":[],"examples":[{"code":"import { SAMPLES_URL, toneEngine } from '@openav/sound';\n\nconsole.log(SAMPLES_URL);   // https://openaudiovisual.com/packages/sound/samples/\nconst local = toneEngine({ instrument: 'piano', baseUrl: '/my/samples/' });   // the piano then reads /my/samples/salamander/…","caption":{"en":"Logs the default folder and shows how to point the engine at your own.","zh":"印出預設資料夾，並示範怎麼讓引擎指向你自己的。"},"run":"module","lang":"js"}],"related":["toneEngine","TONE_CDN","salamanderMap"],"keywords":{"en":["samples","base url","salamander","piano samples","mp3","cors","host samples"],"zh":["取樣","基底網址","salamander","鋼琴取樣","mp3","cors","自己放取樣"]},"tbd":[],"claims":[]},
{"id":"Sound","key":"Sound","name":"Sound","aliases":[],"kind":"class","package":"@openav/sound","category":"sound","documented":true,"signature":"new Sound({ signals, params = null, engine, instrument = null, remember = true, picker = true })","import":"import { Sound } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":90,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L90"},"summary":{"en":"Makes every midi/note/on and midi/note/off on a bus audible through a sound engine, and puts the engine's params on the bus as sound/*.","zh":"讓匯流排上每個 midi/note/on 和 midi/note/off 都透過發聲引擎聽得見，並把引擎的參數放到匯流排上，名稱是 sound/*。"},"description":[{"en":"`Sound` is the shell that wires an engine into an app. Once `await sound.enable()` has run, every `midi/note/on` and `midi/note/off` on the bus is played, whatever sent it: a MIDI keyboard, the on-screen piano ([`mountKeys()`](#mountKeys)), a [`SimPlayer`](#SimPlayer), a [`TakePlayer`](#TakePlayer) or a phone. Notes on a channel in the engine's `skipChannels` are left out (the Tone engine skips channel 10, which belongs to the drum machine).","zh":"`Sound` 是把引擎接進應用程式的外殼。`await sound.enable()` 之後，匯流排上每個 `midi/note/on` 和 `midi/note/off` 都會被播出來，不管是誰送的：MIDI 鍵盤、螢幕鋼琴（[`mountKeys()`](#mountKeys)）、[`SimPlayer`](#SimPlayer)、[`TakePlayer`](#TakePlayer) 或手機。在引擎的 `skipChannels` 裡的頻道的音符不會播（Tone 引擎略過頻道 10，那是鼓機的）。"},{"en":"The engine's params become `sound/*` params, so a knob, the timeline or a hand can play the filter the same way it plays the visuals: sound is performable state, not a side effect. Every frame, call `sound.update(state)` with the resolved param values, and the `sound/*` ones are pushed into the engine. The instrument is a discrete choice (`setInstrument()` or the picker); what you perform continuously stays a param.","zh":"引擎的參數會變成 `sound/*` 參數，所以旋鈕、時間軸或一隻手可以像操作畫面那樣操作濾波器：聲音是可以演奏的狀態，不是附帶的效果。每個畫面用解析好的參數值呼叫 `sound.update(state)`，其中 `sound/*` 的就會被送進引擎。樂器是離散的選擇（`setInstrument()` 或選單），要連續演奏的東西則維持是參數。"}],"params":[{"name":"signals","type":"Signals","optional":false,"en":"The [`Signals`](#Signals) bus. `Sound` listens to `midi/note/on` `{ note, vel, ch }`, `midi/note/off` `{ note, ch }` and `midi/cc/64` (the sustain pedal, down at 0.5 or more), and pulses `sound/instrument` `{ id, name }` when the instrument changes.","zh":"[`Signals`](#Signals) 匯流排。`Sound` 聽 `midi/note/on` `{ note, vel, ch }`、`midi/note/off` `{ note, ch }` 和 `midi/cc/64`（延音踏板，0.5 以上算踩下），並在樂器改變時發出 `sound/instrument` `{ id, name }` 脈衝。"},{"name":"params","type":"Params","optional":true,"default":"null","en":"A `Params` registry to add the engine's params to, named `sound/<key>` (`sound/cutoff`, `sound/space`, `sound/volume` and the instrument's own). Left out, no params are registered.","zh":"要把引擎的參數加進去的 `Params`，名稱是 `sound/<key>`（`sound/cutoff`、`sound/space`、`sound/volume`，和樂器自己的）。沒給的話，不註冊任何參數。"},{"name":"engine","type":"Object","optional":false,"en":"The sound engine, such as [`toneEngine()`](#toneEngine) or [`drumEngine()`](#drumEngine). The contract is `{ params, enable(), noteOn(note, vel01), noteOff(note), set(key, value), dispose() }`, and optionally `setInstrument`, `instrument`, `status`, `subscribe`, `sustain(down)` and `skipChannels`.","zh":"發聲引擎，例如 [`toneEngine()`](#toneEngine) 或 [`drumEngine()`](#drumEngine)。約定是 `{ params, enable(), noteOn(note, vel01), noteOff(note), set(key, value), dispose() }`，選用的有 `setInstrument`、`instrument`、`status`、`subscribe`、`sustain(down)`、`skipChannels`。"},{"name":"instrument","type":"String","optional":true,"default":"null","en":"The instrument to start with, for an engine that has instruments (`'piano'`, `'synth/pluck'`). An unknown id is warned about in the console and ignored.","zh":"一開始用哪個樂器，給有樂器的引擎用（`'piano'`、`'synth/pluck'`）。不認識的 id 會在 console 警告，並被忽略。"},{"name":"remember","type":"Boolean | String","optional":true,"default":"true","en":"Keep the instrument the picker last chose, per page, in `localStorage`, and let it win over `instrument` the next time. A string is your own storage key; the default key is `openav:sound:<page path>`. `false` turns it off. Choices made from code are not remembered unless you ask (`setInstrument(id, { remember: true })`).","zh":"把選單最後選的樂器依頁面記在 `localStorage`，下一次它會贏過 `instrument`。給字串就是你自己的儲存鍵。預設的鍵是 `openav:sound:<頁面路徑>`。`false` 是關掉。用程式做的選擇不會被記住，除非你要求（`setInstrument(id, { remember: true })`）。"},{"name":"picker","type":"Boolean","optional":true,"default":"true","en":"Whether a console should show the instrument picker. It is stored as `sound.picker` for whoever builds the interface; `Sound` itself draws nothing.","zh":"控台要不要顯示樂器選單。它被存成 `sound.picker`，給建介面的人讀。`Sound` 自己不畫任何東西。"}],"needs":{"en":"`sound.enable()` from a click: browsers start audio only after a user gesture. With [`toneEngine()`](#toneEngine) it also loads Tone.js from the network (jsDelivr) the first time. Before `enable()` nothing is heard, and the bus is not listened to.","zh":"要在點擊裡呼叫 `sound.enable()`：瀏覽器要使用者做過一個動作才讓聲音開始。用 [`toneEngine()`](#toneEngine) 的話，第一次還會從網路（jsDelivr）載入 Tone.js。`enable()` 之前什麼都聽不到，匯流排也沒有被聽。"},"notes":[{"en":"From a p5 sketch you do not build this: [`startSound()`](#startSound) does it, loading this package only when it is called.","zh":"在 p5 草圖裡不用自己建這個：[`startSound()`](#startSound) 會做，而且只在被呼叫的時候才載入這個套件。"}],"examples":[{"code":"import { Sound } from '@openav/sound';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst heard = [];\nconst engine = {                                    // the smallest engine: it only writes down what it is told\n  params: [{ key: 'cutoff', label: 'Cutoff', min: 100, max: 8000, def: 2500 }],\n  async enable() {},\n  noteOn: (note, vel) => heard.push('on ' + note + ' ' + vel),\n  noteOff: (note) => heard.push('off ' + note),\n  set: (key, value) => heard.push(key + '=' + value),\n  dispose() {},\n};\nconst sound = new Sound({ signals, engine });\nawait sound.enable();\nsignals.pulse('midi/note/on', { note: 60, vel: 0.8, ch: 1 });\nsignals.pulse('midi/note/off', { note: 60, ch: 1 });\nsound.update({ 'sound/cutoff': 900, hue: 0.3 });     // only the sound/* keys go to the engine\nconsole.log(heard.join(', '));                        // on 60 0.8, off 60, cutoff=900\nconsole.log(sound.enabled, sound.choosable, sound.instrument);   // true false null","caption":{"en":"A bus with a stand-in engine: notes on the bus reach the engine, and of the state only `sound/cutoff` does. The engine has no instruments, so `choosable` is false.","zh":"一個匯流排加一個替身引擎：匯流排上的音符傳到引擎，state 裡只有 `sound/cutoff` 會傳到。這個引擎沒有樂器，所以 `choosable` 是 false。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { mountKeys } from '@openav/keys';\nimport { Sound, toneEngine, mountSoundPicker } from '@openav/sound';\n\nconst signals = new Signals();\nconst sound = new Sound({ signals, engine: toneEngine({ instrument: 'piano' }) });\nmountKeys(document.getElementById('app'), { signals });        // an on-screen piano that publishes midi/note/on\nmountSoundPicker(document.getElementById('app'), sound);       // choosing an instrument also turns the sound on\ndocument.body.addEventListener('click', () => sound.enable(), { once: true });\nsound.onChange((e) => console.log(e.type, e.id));","caption":{"en":"Click anywhere to start the sound, then click the piano keys: you hear the Salamander grand piano. Pick another instrument from the menu while you play.","zh":"在任何地方點一下開始發聲，然後點鋼琴的琴鍵：你會聽到 Salamander 平台鋼琴。彈的時候可以從選單換別的樂器。"},"run":"static","lang":"js"}],"related":["toneEngine","mountSoundPicker","registerInstrument","SOUND_PARAMS","startSound","Signals"],"keywords":{"en":["sound","audio output","play notes","instrument","synth","engine","midi to sound","tone.js","enable sound"],"zh":["聲音","音訊輸出","彈出聲音","樂器","合成器","引擎","midi 變成聲音","tone.js","啟用聲音"]},"tbd":[{"en":"The Tone.js engine was not run for this page: there was no network and no speakers. What is written here comes from the code and from the package tests, which use a stand-in for Tone.js, and the examples that run use a stand-in engine.","zh":"這一頁沒有實際跑過 Tone.js 引擎：這裡沒有網路也沒有喇叭。寫在這裡的內容來自程式碼，和套件的測試（測試用一個替身代替 Tone.js），能跑的範例用的是替身引擎。"}],"claims":[],"members":[{"name":"enable","member":"method","signature":"","doc":"Start the engine (must be called from a user gesture — browser audio policy).","line":123},{"name":"update","member":"method","signature":"state","doc":"Per frame: push resolved sound/* params into the engine.","line":145},{"name":"instrument","member":"getter","signature":"","doc":"Current instrument id (null for engines without instruments, e.g.","line":153},{"name":"status","member":"getter","signature":"","doc":"{ id, state: 'idle'|'loading'|'ready'|'error', progress 0..1, error }","line":155},{"name":"choosable","member":"getter","signature":"","doc":"Whether this engine has instruments to choose from.","line":157},{"name":"setInstrument","member":"method","signature":"id, { remember = false } = {}","doc":"Switch instruments, live.","line":165},{"name":"onChange","member":"method","signature":"cb","doc":"Listen to { type: 'instrument' | 'loading' | 'ready' | 'error' | 'enabled', id, progress, error }.","line":172},{"name":"dispose","member":"method","signature":"","doc":"","line":174}]},
{"id":"SOUND_PARAMS","key":"SOUND_PARAMS","name":"SOUND_PARAMS","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"SOUND_PARAMS = array, 3 items","import":"import { SOUND_PARAMS } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":24,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L24"},"summary":{"en":"The three params every instrument shares: cutoff (the filter), space (the reverb) and volume.","zh":"每個樂器共用的三個參數：cutoff（濾波器）、space（殘響）和 volume（音量）。"},"description":[{"en":"`cutoff` is the low-pass filter's frequency in Hz, `space` is how much reverb is mixed in (the wet amount, 0..1), `volume` is the level in dB. They sit after the instrument on the output chain, so the timeline, a knob or a hand keep performing them across instrument switches. In a show they are the params `sound/cutoff`, `sound/space` and `sound/volume`. An instrument may suggest other starting values through its `defaults`, and automation and overrides always win over a suggestion. An instrument's own `params` cannot use these keys.","zh":"`cutoff` 是低通濾波器的頻率，單位是 Hz，`space` 是混進去多少殘響（濕度，0..1），`volume` 是音量，單位是 dB。它們在輸出鏈上排在樂器之後，所以換樂器的時候，時間軸、旋鈕或一隻手可以繼續演奏它們。在演出裡它們是參數 `sound/cutoff`、`sound/space`、`sound/volume`。樂器可以用它的 `defaults` 建議不同的起始值，自動化和覆寫永遠贏過建議。樂器自己的 `params` 不能用這幾個鍵。"}],"value":{"type":"Object[]","en":"`[{ key: 'cutoff', min: 100, max: 8000, def: 2500 }, { key: 'space', min: 0, max: 1, def: 0.3 }, { key: 'volume', min: -36, max: 0, def: -8 }]`, each also with a `label`.","zh":"`[{ key: 'cutoff', min: 100, max: 8000, def: 2500 }, { key: 'space', min: 0, max: 1, def: 0.3 }, { key: 'volume', min: -36, max: 0, def: -8 }]`，每個還有一個 `label`。"},"params":[],"notes":[],"examples":[{"code":"import { SOUND_PARAMS } from '@openav/sound';\n\nconsole.log(SOUND_PARAMS.map((p) => p.key + ' ' + p.min + '..' + p.max + ' (' + p.def + ')').join(' | '));\n// cutoff 100..8000 (2500) | space 0..1 (0.3) | volume -36..0 (-8)","caption":{"en":"Logs the range and the default of each shared param.","zh":"印出每個共用參數的範圍和預設值。"},"run":"module","lang":"js"}],"related":["toneEngine","Sound","registerInstrument"],"keywords":{"en":["params","cutoff","space","reverb","volume","filter","shared","sound/cutoff"],"zh":["參數","cutoff","space","殘響","音量","濾波器","共用","sound/cutoff"]},"tbd":[],"claims":[]},
{"id":"soundOptions","key":"soundOptions","name":"soundOptions","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"soundOptions(value)","import":"import { soundOptions } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":74,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L74"},"summary":{"en":"Reads the modules.sound option of createShow() (true, an instrument id, or an object) into one normalized object.","zh":"把 createShow() 的 modules.sound 選項（true、樂器 id 或物件）讀成一個整理好的物件。"},"description":[{"en":"It is how `createShow({ modules: { sound: … } })` understands its option, so `sound: true`, `sound: 'piano'` and `sound: { instrument: 'organ', remember: false, picker: false }` all work. You only need it if you write something that takes the same option.","zh":"`createShow({ modules: { sound: … } })` 就是靠它理解選項，所以 `sound: true`、`sound: 'piano'` 和 `sound: { instrument: 'organ', remember: false, picker: false }` 都行。只有你要寫收同樣選項的東西時才需要它。"}],"params":[{"name":"value","type":"Boolean | String | Object","en":"`true` for the defaults, an instrument id (`'piano'`), or `{ instrument, remember, picker, engine, …toneEngine options }`. Anything falsy gives `null`.","zh":"`true` 是預設，樂器 id（`'piano'`），或 `{ instrument, remember, picker, engine, …toneEngine 的選項 }`。任何假值會得到 `null`。"}],"returns":{"type":"Object | null","en":"`{ engine, instrument, remember, picker, engineOptions }` (`engine` and `instrument` are `null` when not given, `remember` and `picker` are `true`), or `null` for a falsy value. `engineOptions` holds the rest of an object: `baseUrl`, `cdn`, `instruments`…. An `instrument` that is not a string, or a value of another kind, throws a `TypeError`.","zh":"`{ engine, instrument, remember, picker, engineOptions }`（沒給的 `engine` 和 `instrument` 是 `null`，`remember` 和 `picker` 是 `true`），假值則是 `null`。`engineOptions` 放物件裡其餘的東西：`baseUrl`、`cdn`、`instruments`…。`instrument` 不是字串，或給了別種值，會丟出 `TypeError`。"},"notes":[],"examples":[{"code":"import { soundOptions } from '@openav/sound';\n\nconsole.log(soundOptions('piano').instrument);                                  // piano\nconsole.log(soundOptions({ instrument: 'organ', remember: false, baseUrl: 'x/' }));\n// { engine: null, instrument: 'organ', remember: false, picker: true, engineOptions: { baseUrl: 'x/' } }\nconsole.log(soundOptions(false));                                               // null\ntry { soundOptions([]); } catch (e) { console.log(e.message); }\n// modules.sound: true, an instrument id, or { instrument, remember, picker, engine, baseUrl, cdn, instruments }","caption":{"en":"The three accepted shapes, a falsy value that turns the sound off, and the error for something that is none of them.","zh":"三種接受的形式、關掉聲音的假值，以及什麼都不是的東西會得到的錯誤。"},"run":"module","lang":"js"}],"related":["Sound","toneEngine","createShow"],"keywords":{"en":["createShow","modules.sound","options","normalize","config"],"zh":["createShow","modules.sound","選項","正規化","設定"]},"tbd":[],"claims":[]},
{"id":"SYNTH_PRESETS","key":"SYNTH_PRESETS","name":"SYNTH_PRESETS","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"SYNTH_PRESETS = array, 11 items","import":"import { SYNTH_PRESETS } from '@openav/sound';","source":{"file":"packages/sound/instruments/synth.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/synth.js#L30"},"summary":{"en":"The 11 synth presets as plain data: pad, saw lead, square lead, pluck, supersaw, bass, acid, brass, bell, sub and chip.","zh":"11 個合成器預設，是單純的資料：pad、saw lead、square lead、pluck、supersaw、bass、acid、brass、bell、sub、chip。"},"description":[{"en":"Each preset is what [`synthInstrument()`](#synthInstrument) turns into an instrument `synth/<id>`. They are data in Tone.js's own vocabulary (`oscillator`, `envelope`, `filter`, `filterEnvelope`, `harmonicity`…), a good place to look when you write a preset of your own for [`registerSynthPreset()`](#registerSynthPreset). The `pad` is the framework's original voice: three detuned sawtooth waves with a quick attack and a long release.","zh":"每個預設都會被 [`synthInstrument()`](#synthInstrument) 變成一個樂器 `synth/<id>`。它們是用 Tone.js 自己的詞彙寫的資料（`oscillator`、`envelope`、`filter`、`filterEnvelope`、`harmonicity`…），要為 [`registerSynthPreset()`](#registerSynthPreset) 寫自己的預設時，是個好的參考。`pad` 是框架最早的聲音：三個失諧的鋸齒波，起音快、釋放長。"}],"value":{"type":"Object[]","en":"An array of 11 presets `{ id, name: { en, zh }, voice, … }`: `pad`, `saw-lead`, `square-lead`, `pluck`, `supersaw`, `bass`, `acid`, `brass`, `bell`, `sub`, `chip`.","zh":"一個有 11 個預設的陣列 `{ id, name: { en, zh }, voice, … }`：`pad`、`saw-lead`、`square-lead`、`pluck`、`supersaw`、`bass`、`acid`、`brass`、`bell`、`sub`、`chip`。"},"params":[],"notes":[],"examples":[{"code":"import { SYNTH_PRESETS } from '@openav/sound';\n\nconsole.log(SYNTH_PRESETS.map((p) => p.id).join(' '));\n// pad saw-lead square-lead pluck supersaw bass acid brass bell sub chip\nconst bass = SYNTH_PRESETS.find((p) => p.id === 'bass');\nconsole.log(bass.voice, bass.octave, bass.oscillator.type);   // mono -1 fatsawtooth","caption":{"en":"Lists the preset ids and reads a few settings of the bass: a mono synth with a filter, an octave down.","zh":"列出預設的 id，並讀貝斯的幾個設定：有濾波器的單音合成器，降一個八度。"},"run":"module","lang":"js"}],"related":["synthInstrument","registerSynthPreset","validateSynthPreset","BUILTIN_INSTRUMENTS"],"keywords":{"en":["synth presets","pad","lead","bass","acid","supersaw","pluck","bell","chip","data"],"zh":["合成器預設","襯底","主奏","貝斯","酸性","超級鋸齒波","撥奏","鐘聲","晶片音","資料"]},"tbd":[],"claims":[]},
{"id":"synthInstrument","key":"synthInstrument","name":"synthInstrument","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"synthInstrument(preset)","import":"import { synthInstrument } from '@openav/sound';","source":{"file":"packages/sound/instruments/synth.js","line":141,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/synth.js#L141"},"summary":{"en":"Turns a synth preset into an instrument definition whose id is synth/ followed by the preset id, without registering it.","zh":"把一個合成器預設變成樂器定義，id 是 synth/ 加上預設的 id，但不登記它。"},"description":[{"en":"The definition's `create()` builds one `Tone.PolySynth` of the preset's `voice` (`synth`, `mono`, `fm` or `am`), with the Tone settings the preset carries, a chain of its `effects`, and its `polyphony` (16 voices if left out). Use it when you want the definition for yourself (to adjust it before registering); otherwise [`registerSynthPreset()`](#registerSynthPreset) does both steps.","zh":"這個定義的 `create()` 會建一個 `Tone.PolySynth`，聲部是預設的 `voice`（`synth`、`mono`、`fm` 或 `am`），帶著預設的 Tone 設定、一串 `effects` 效果，以及它的 `polyphony`（沒給就是 16 個聲部）。想自己拿到這個定義（登記前先調整）時才用它。否則 [`registerSynthPreset()`](#registerSynthPreset) 兩步一起做完。"}],"params":[{"name":"preset","type":"Object","en":"The preset, see [`registerSynthPreset()`](#registerSynthPreset). A preset that [`validateSynthPreset()`](#validateSynthPreset) refuses throws a `TypeError`.","zh":"預設，見 [`registerSynthPreset()`](#registerSynthPreset)。[`validateSynthPreset()`](#validateSynthPreset) 不接受的預設會丟出 `TypeError`。"}],"returns":{"type":"Object","en":"An instrument definition: `id` is `'synth/' + preset.id`, `name` the preset's (or its id), `category` the preset's or `'synth'`, `transpose` is `octave × 12`, `gain`, `defaults`, `params` and `credit` come from the preset, `tail` is `min(8, release × 1.5 + 0.5)` seconds and `preset` is the preset itself.","zh":"一個樂器定義：`id` 是 `'synth/' + preset.id`，`name` 用預設的（沒有就用它的 id），`category` 用預設的或 `'synth'`，`transpose` 是 `octave × 12`，`gain`、`defaults`、`params`、`credit` 來自預設，`tail` 是 `min(8, release × 1.5 + 0.5)` 秒，`preset` 是預設本身。"},"notes":[],"examples":[{"code":"import { synthInstrument, validateInstrument } from '@openav/sound';\n\nconst def = synthInstrument({ id: 'glass', name: 'Glass', voice: 'fm', octave: -1, gain: -3, envelope: { release: 2 } });\nconsole.log(def.id, def.category, def.transpose, def.gain, def.tail);   // synth/glass synth -12 -3 3.5\nconsole.log(validateInstrument(def));                                    // []\ntry { synthInstrument({ id: 'Glass!' }); } catch (e) { console.log(e instanceof TypeError); }   // true","caption":{"en":"A preset becomes a valid instrument definition: an octave down is 12 semitones, and the tail follows the release. A bad id throws.","zh":"一個預設變成合格的樂器定義：降一個八度是 12 個半音，尾巴長度跟著釋放時間走。壞的 id 會丟出錯誤。"},"run":"module","lang":"js"}],"related":["registerSynthPreset","SYNTH_PRESETS","validateSynthPreset","registerInstrument"],"keywords":{"en":["synth","preset to instrument","polysynth","convert","definition","fm","mono"],"zh":["合成器","預設轉樂器","複音合成器","轉換","定義","fm","單音"]},"tbd":[],"claims":[]},
{"id":"TONE_CDN","key":"TONE_CDN","name":"TONE_CDN","aliases":[],"kind":"constant","package":"@openav/sound","category":"sound","documented":true,"signature":"TONE_CDN = \"https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm\"","import":"import { TONE_CDN } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":54,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L54"},"summary":{"en":"The address Tone.js is loaded from when the engine is enabled: Tone 15.0.4 on jsDelivr.","zh":"引擎啟用時載入 Tone.js 的位址：jsDelivr 上的 Tone 15.0.4。"},"description":[{"en":"A module URL that `toneEngine()` imports on `enable()`. To host Tone.js yourself, pass your own as the `cdn` option of [`toneEngine()`](#toneEngine).","zh":"`toneEngine()` 在 `enable()` 時匯入的模組網址。想自己放 Tone.js 的話，把你的網址當作 [`toneEngine()`](#toneEngine) 的 `cdn` 選項。"}],"value":{"type":"String","en":"`'https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm'`.","zh":"字串 `'https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm'`。"},"params":[],"notes":[],"examples":[{"code":"import { TONE_CDN, toneEngine } from '@openav/sound';\n\nconsole.log(TONE_CDN);   // https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm\nconst own = toneEngine({ cdn: '/vendor/tone.mjs' });   // your own copy, used when enable() runs","caption":{"en":"Logs the default address and shows where an own copy would be given.","zh":"印出預設位址，並示範自己的備份要寫在哪裡。"},"run":"module","lang":"js"}],"related":["toneEngine","SAMPLES_URL"],"keywords":{"en":["tone.js","cdn","jsdelivr","url","version","host yourself"],"zh":["tone.js","cdn","jsdelivr","網址","版本","自己放"]},"tbd":[],"claims":[]},
{"id":"toneEngine","key":"toneEngine","name":"toneEngine","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"toneEngine({ cdn = TONE_CDN, instrument = DEFAULT_INSTRUMENT, baseUrl = SAMPLES_URL, instruments: perInstrument = {}, drumChannel = 10, } = {})","import":"import { toneEngine } from '@openav/sound';","source":{"file":"packages/sound/index.js","line":226,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/index.js#L226"},"summary":{"en":"The sound engine that plays the registered instruments with Tone.js through a filter, a reverb and a limiter.","zh":"發聲引擎，用 Tone.js 播放登記好的樂器，聲音依序經過濾波器、殘響和限幅器。"},"description":[{"en":"Tone.js stays a neighbour, not a dependency: it is loaded from the network the first time `enable()` runs, so an app that never turns the sound on pays nothing. After `enable()` the instrument's output goes through a low-pass filter (`cutoff`, starting at 2500 Hz), a reverb (`space`, the wet amount, starting at 0.3), a volume (`volume`, in dB, starting at -8) and a limiter at -1 dB, so a loud organ never clips the room. These three are the [`SOUND_PARAMS`](#SOUND_PARAMS) shared by every instrument. Their starting values can follow the instrument (a piano wants the filter open), and the instrument may add params of its own.","zh":"Tone.js 只是鄰居，不是相依套件：第一次 `enable()` 的時候才從網路載入，所以從不開聲音的應用程式什麼代價都不用付。`enable()` 之後，樂器的輸出會經過低通濾波器（`cutoff`，起始 2500 Hz）、殘響（`space`，濕度，起始 0.3）、音量（`volume`，單位 dB，起始 -8）和 -1 dB 的限幅器，所以很大聲的風琴也不會把房間削波。這三個就是每個樂器共用的 [`SOUND_PARAMS`](#SOUND_PARAMS)。它們的起始值可以跟著樂器走（鋼琴想要濾波器全開），樂器也可以再加自己的參數。"},{"en":"`setInstrument(id)` switches live and returns a promise of whether it worked. Before `enable()` it only remembers the choice. Held notes are released on the old instrument and its tail is left to ring for a few seconds. A sampler (the piano) keeps the old sound until its files are in; if they cannot load, the engine plays the instrument's `fallback` (the piano falls back to the electric piano), so a performance never goes silent because of one failed download. `subscribe(cb)` tells you about `instrument`, `loading`, `ready` and `error` events. `sustain(down)` is the pedal: note-offs wait until it comes up. `releaseAll()` is a panic button.","zh":"`setInstrument(id)` 現場換樂器，回傳換成功與否的 promise。在 `enable()` 之前它只是記住這個選擇。舊樂器按著的音會被放開，它的尾巴讓它再響幾秒。取樣樂器（鋼琴）在檔案進來之前會繼續用舊的聲音。如果載入失敗，引擎會播這個樂器的 `fallback`（鋼琴退回電鋼琴），所以演出不會因為一次下載失敗就沒聲音。`subscribe(cb)` 會告訴你 `instrument`、`loading`、`ready`、`error` 事件。`sustain(down)` 是踏板：放開音的動作要等到踏板抬起來才做。`releaseAll()` 是緊急停止鈕。"},{"en":"You usually pass the engine to a [`Sound`](#Sound), which is what listens to the bus. Used on its own, `noteOn(60, 0.8)` plays a note.","zh":"通常你會把引擎交給 [`Sound`](#Sound)，由它去聽匯流排。單獨使用的話，`noteOn(60, 0.8)` 就是彈一個音。"}],"params":[{"name":"cdn","type":"String","optional":true,"default":"TONE_CDN","en":"Where Tone.js is imported from when the engine is enabled. The default is [`TONE_CDN`](#TONE_CDN). A module URL, so you can host your own copy.","zh":"引擎啟用時從哪裡匯入 Tone.js。預設是 [`TONE_CDN`](#TONE_CDN)。是一個模組網址，所以你可以放自己的備份。"},{"name":"instrument","type":"String","optional":true,"default":"'synth/pad'","en":"The instrument to start with. An unknown id is warned about and `synth/pad` is used.","zh":"一開始用的樂器。不認識的 id 會警告，改用 `synth/pad`。"},{"name":"baseUrl","type":"String","optional":true,"default":"SAMPLES_URL","en":"The folder that holds the built-in sample sets (`salamander/…`). The default is [`SAMPLES_URL`](#SAMPLES_URL).","zh":"放內建取樣組（`salamander/…`）的資料夾。預設是 [`SAMPLES_URL`](#SAMPLES_URL)。"},{"name":"instruments","type":"Object","optional":true,"default":"{}","en":"Options for single instruments, handed to their `create()` as `ctx.options`: `{ piano: { baseUrl: '/my/salamander/' } }`.","zh":"給單一樂器的選項，會當作 `ctx.options` 交給它的 `create()`：`{ piano: { baseUrl: '/my/salamander/' } }`。"},{"name":"drumChannel","type":"Number | null","optional":true,"default":"10","en":"Notes on this MIDI channel belong to a drum engine (the drum machine sends General MIDI channel 10) and are not played here. `null` plays everything.","zh":"這個 MIDI 頻道的音符屬於鼓的引擎（鼓機送的是 General MIDI 的頻道 10），這裡不播。`null` 是全部都播。"}],"returns":{"type":"Object","en":"An engine: `{ params, sharedParams, instrument, status, skipChannels, hasInstrument(id), subscribe(cb), setInstrument(id), enable(), noteOn(note, vel01, time), noteOff(note, time), sustain(down), releaseAll(), set(key, value), dispose() }`.","zh":"一個引擎：`{ params, sharedParams, instrument, status, skipChannels, hasInstrument(id), subscribe(cb), setInstrument(id), enable(), noteOn(note, vel01, time), noteOff(note, time), sustain(down), releaseAll(), set(key, value), dispose() }`。"},"needs":{"en":"`await engine.enable()` from a click: it loads Tone.js from the network, then starts its audio. Before that, `noteOn()` does nothing and `setInstrument()` only remembers the choice.","zh":"要在點擊裡 `await engine.enable()`：它從網路載入 Tone.js，然後啟動音訊。在那之前 `noteOn()` 什麼都不做，`setInstrument()` 只是記住選擇。"},"notes":[],"examples":[{"code":"import { toneEngine } from '@openav/sound';\n\nconst engine = toneEngine({ instrument: 'strings' });      // nothing is loaded yet\nconsole.log(engine.instrument, engine.status.state);        // strings idle\nconsole.log(engine.params.map((p) => p.key + ' ' + p.def).join(', '));\n// cutoff 6000, space 0.45, volume -8, attack 0.35, vibrato 0.35   (the shared three, then the strings' own)\nconsole.log(engine.skipChannels, engine.hasInstrument('piano'));   // [10] true\nconsole.log(toneEngine({ drumChannel: null }).skipChannels);       // []","caption":{"en":"Looks at an engine before it is enabled: its instrument, the params it will offer (the strings suggest a different starting cutoff and space, and add two of their own), and that channel 10 is left to the drums.","zh":"在引擎啟用之前看它：它的樂器、它會提供的參數（弦樂建議了不同的起始 cutoff 和 space，還多加了兩個自己的），以及頻道 10 是留給鼓的。"},"run":"module","lang":"js"},{"code":"import { toneEngine } from '@openav/sound';\n\nconst engine = toneEngine({ instrument: 'epiano' });\ndocument.body.addEventListener('click', async () => {\n  await engine.enable();                                // loads Tone.js from the network, starts the audio\n  engine.set('space', 0.6);                             // more reverb\n  [60, 64, 67].forEach((note) => engine.noteOn(note, 0.8));   // a C major chord\n  setTimeout(() => engine.releaseAll(), 1500);\n}, { once: true });","caption":{"en":"Click the page: an electric piano plays a C major chord, with more reverb than usual, and lets go after a second and a half.","zh":"在頁面上點一下：電鋼琴彈一個 C 大調和弦，殘響比平常多，一秒半之後放開。"},"run":"static","lang":"js"}],"related":["Sound","registerInstrument","SOUND_PARAMS","TONE_CDN","SAMPLES_URL","drumEngine"],"keywords":{"en":["tone.js","engine","synth","instrument player","reverb","filter","limiter","samples","enable","noteOn"],"zh":["tone.js","引擎","合成器","樂器播放","殘響","濾波器","限幅器","取樣","啟用","noteOn"]},"tbd":[{"en":"The Tone.js engine was not run for this page: there was no network and no speakers. What is written here comes from the code and from the package tests, which use a stand-in for Tone.js, and the examples that run use a stand-in engine.","zh":"這一頁沒有實際跑過 Tone.js 引擎：這裡沒有網路也沒有喇叭。寫在這裡的內容來自程式碼，和套件的測試（測試用一個替身代替 Tone.js），能跑的範例用的是替身引擎。"}],"claims":[]},
{"id":"unregisterInstrument","key":"unregisterInstrument","name":"unregisterInstrument","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"unregisterInstrument(id)","import":"import { unregisterInstrument } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":96,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L96"},"summary":{"en":"Removes a registered sound, for tests and hot reload. Returns whether it was there.","zh":"移除一個登記過的聲音，給測試和熱重載用。回傳它原本在不在。"},"description":[{"en":"A picker refreshes itself. It does not stop an engine that is playing the instrument: switch away first.","zh":"選單會自己更新。它不會停掉正在播這個樂器的引擎：要先換掉。"}],"params":[{"name":"id","type":"String","en":"The id of the instrument.","zh":"樂器的 id。"}],"returns":{"type":"Boolean","en":"True if it existed. The listeners of [`onInstrumentsChange()`](#onInstrumentsChange) hear `{ type: 'unregister', id }` only then.","zh":"原本有就是 true。只有這時候，[`onInstrumentsChange()`](#onInstrumentsChange) 的監聽者才會收到 `{ type: 'unregister', id }`。"},"notes":[],"examples":[{"code":"import { registerInstrument, unregisterInstrument, getInstrument } from '@openav/sound';\n\nregisterInstrument({ id: 'me/temp', name: 'Temp', category: 'other', create: () => ({ noteOn() {}, noteOff() {}, dispose() {} }) });\nconsole.log(unregisterInstrument('me/temp'), getInstrument('me/temp'));   // true null\nconsole.log(unregisterInstrument('me/temp'));                              // false","caption":{"en":"Removes an instrument; the second time there is nothing to remove.","zh":"移除一個樂器。第二次就沒有東西可以移除了。"},"run":"module","lang":"js"}],"related":["registerInstrument","getInstrument","onInstrumentsChange"],"keywords":{"en":["remove","unregister","delete instrument","hot reload"],"zh":["移除","取消登記","刪除樂器","熱重載"]},"tbd":[],"claims":[]},
{"id":"validateInstrument","key":"validateInstrument","name":"validateInstrument","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"validateInstrument(def)","import":"import { validateInstrument } from '@openav/sound';","source":{"file":"packages/sound/registry.js","line":49,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/registry.js#L49"},"summary":{"en":"Checks an instrument definition and lists everything wrong with it as readable sentences. An empty list means it is valid.","zh":"檢查一個樂器定義，把有問題的地方都列成看得懂的句子。空的清單表示沒問題。"},"description":[{"en":"It checks: `id` (lowercase letters, digits and `-`, at most one `/`), `name` (a string, or `{ en, zh }` with at least `en`), `category` (a string), `create` (a function), `params` (each with a word as `key`, numbers `min < max`, a `def` within them, and a key that is not one of the shared three), `defaults` (only `cutoff`, `space` or `volume`, each within its range), `credit` (a string, or `{ text, url }`), `gain`, `transpose` and `tail` (numbers) and `fallback` (another instrument's id). [`registerInstrument()`](#registerInstrument) runs it and throws with these sentences.","zh":"它檢查：`id`（小寫字母、數字和 `-`，最多一個 `/`）、`name`（字串，或至少有 `en` 的 `{ en, zh }`）、`category`（字串）、`create`（函式）、`params`（每個的 `key` 是一個單字，`min < max` 是數字，`def` 在兩者之間，而且 key 不是共用的那三個之一）、`defaults`（只能是 `cutoff`、`space`、`volume`，且各自在範圍內）、`credit`（字串，或 `{ text, url }`）、`gain`、`transpose`、`tail`（數字），以及 `fallback`（另一個樂器的 id）。[`registerInstrument()`](#registerInstrument) 會執行它，並用這些句子丟出錯誤。"}],"params":[{"name":"def","type":"any","en":"The definition to check.","zh":"要檢查的定義。"}],"returns":{"type":"String[]","en":"Sentences, one per problem. `[]` when valid. It never throws.","zh":"句子，每個問題一句。沒問題就是 `[]`。它不會丟出錯誤。"},"notes":[],"examples":[{"code":"import { validateInstrument } from '@openav/sound';\n\nconst ok = { id: 'me/ok', name: 'OK', category: 'other', create() {} };\nconsole.log(validateInstrument(ok));                          // []\nconsole.log(validateInstrument({ ...ok, id: 'Piano', params: [{ key: 'cutoff', min: 0, max: 1, def: 0 }] }));\n// [ 'id \"Piano\": lowercase letters, digits and \"-\", at most one \"/\" (e.g. \"synth/pad\")',\n//   'params[0] (cutoff): \"cutoff\" is a shared sound param; set its starting value with defaults: { cutoff: … }' ]\nconsole.log(validateInstrument(null));                        // [ 'an instrument definition is an object' ]","caption":{"en":"A good definition gives an empty list; a definition with a capital letter in the id and a param that is already shared gets two sentences.","zh":"好的定義得到空清單。id 有大寫字母、又有一個已經是共用參數的定義，得到兩句話。"},"run":"module","lang":"js"}],"related":["registerInstrument","validateSynthPreset","SOUND_PARAMS"],"keywords":{"en":["validate","check definition","errors","lint","instrument rules"],"zh":["驗證","檢查定義","錯誤","樂器規則","問題清單"]},"tbd":[],"claims":[]},
{"id":"validateSynthPreset","key":"validateSynthPreset","name":"validateSynthPreset","aliases":[],"kind":"function","package":"@openav/sound","category":"sound","documented":true,"signature":"validateSynthPreset(p)","import":"import { validateSynthPreset } from '@openav/sound';","source":{"file":"packages/sound/instruments/synth.js","line":128,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/sound/instruments/synth.js#L128"},"summary":{"en":"Checks a synth preset and lists what is wrong with it. An empty list means it is valid.","zh":"檢查一個合成器預設，列出有問題的地方。空的清單表示沒問題。"},"description":[{"en":"It checks the `id` (lowercase letters, digits and `-`), the `voice` (`synth`, `mono`, `fm` or `am`), that every effect has a `type` the package knows, and that `octave` is a whole number. It does not check the Tone settings, which are Tone.js's to judge.","zh":"它檢查 `id`（小寫字母、數字和 `-`）、`voice`（`synth`、`mono`、`fm` 或 `am`）、每個效果的 `type` 是不是套件認得的，以及 `octave` 是不是整數。Tone 的設定它不檢查，那是 Tone.js 的事。"}],"params":[{"name":"p","type":"Object","en":"The preset to check.","zh":"要檢查的預設。"}],"returns":{"type":"String[]","en":"Sentences, one per problem. `[]` when valid.","zh":"句子，每個問題一句。沒問題就是 `[]`。"},"notes":[],"examples":[{"code":"import { validateSynthPreset } from '@openav/sound';\n\nconsole.log(validateSynthPreset({ id: 'glass', voice: 'fm', effects: [{ type: 'chorus' }] }));   // []\nconsole.log(validateSynthPreset({ id: 'Glass', voice: 'laser', effects: [{ type: 'zz' }], octave: 1.5 }));\n// [ 'id \"Glass\": lowercase letters, digits and \"-\" (it becomes \"synth/<id>\")',\n//   'voice \"laser\": one of synth, mono, fm, am',\n//   'effects[0].type \"zz\": one of chorus, delay, pingpong, distortion, vibrato, tremolo, phaser, autofilter, filter, eq, compressor, chebyshev, widener',\n//   'octave: a whole number' ]","caption":{"en":"A good preset gives an empty list; a preset with four mistakes gets one sentence for each.","zh":"好的預設得到空清單。有四個錯誤的預設，每個錯誤得到一句話。"},"run":"module","lang":"js"}],"related":["registerSynthPreset","synthInstrument","validateInstrument"],"keywords":{"en":["validate preset","check","errors","synth rules"],"zh":["驗證預設","檢查","錯誤","合成器規則"]},"tbd":[],"claims":[]},
{"id":"beatPhase","key":"beatPhase","name":"beatPhase","aliases":["OAVp5.drums.beatPhase"],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"beatPhase","source":{"file":"p5/oav-p5.js","line":1447,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1447"},"summary":{"en":"How far through the current beat the drum machine is, smoothly from 0 up to just below 1, four times per bar.","zh":"鼓機走到這一拍的哪裡，從 0 平滑地增加到快要 1，一小節會跑四次。"},"description":[{"en":"A beat is a quarter note (four steps). `1 - beatPhase` is biggest right on the beat and shrinks until the next one, so `circle(width / 2, height / 2, 60 + (1 - beatPhase) * 80)` pulses with the tempo, with no callback needed. [`drumPhase`](#drumPhase) is the same thing across a whole bar.","zh":"一拍是一個四分音符（四格）。`1 - beatPhase` 在正拍上最大，然後縮小到下一拍，所以 `circle(width / 2, height / 2, 60 + (1 - beatPhase) * 80)` 會跟著速度脈動，不用任何 callback。[`drumPhase`](#drumPhase) 是同樣的東西，範圍是整個小節。"}],"value":{"type":"Number","range":"0..1","en":"Read fresh each time you look at it. 0 on the beat, close to 1 just before the next one, then 0 again. It is 0 before the machine has played and holds still while it is stopped.","zh":"每次讀都是當下的值。正拍上是 0，下一拍之前接近 1，然後回到 0。鼓機還沒播之前是 0，停下來的時候停在原地。"},"params":[],"needs":{"en":"Start the machine with [`startDrums()`](#startDrums); until it plays, `beatPhase` is 0.","zh":"先用 [`startDrums()`](#startDrums) 啟動鼓機，在它開始播之前，`beatPhase` 是 0。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  fill(255, 120, 90);\n  circle(width / 2, height / 2, 60 + (1 - beatPhase) * 100);   // biggest on the beat\n}","caption":{"en":"A circle that pulses four times per bar: biggest on each beat, then shrinking until the next one.","zh":"一個圓，一小節脈動四次：每一拍最大，然後縮小到下一拍。"},"run":"sketch","lang":"js"}],"related":["drumPhase","drumStep","startDrums","beat"],"keywords":{"en":["beat phase","pulse","beat progress","quarter note","smooth","drums"],"zh":["拍子進度","脈動","一拍","平滑","鼓機"]},"tbd":[],"claims":[{"before":"startDrums('four on floor', 120)","expr":"beatPhase","min":0,"max":1,"moves":true}]},
{"id":"drawRhythm","key":"drawRhythm","name":"drawRhythm","aliases":["OAVp5.drums.draw"],"kind":"function","package":"oav-p5","category":"drums","documented":true,"signature":"drawRhythm([x], [y], [w], [h], [style], [o])","source":{"file":"p5/oav-p5.js","line":1871,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1871"},"summary":{"en":"Draws the rhythm on the canvas as a grid, a circle or a spiral, with a playhead and the hits lighting up, so a recording has it too.","zh":"把節奏畫在畫布上，樣式可以是格子、圓或螺旋，有播放頭，打擊的時候會亮起來，所以錄影也錄得到。"},"description":[{"en":"Call it in `draw()` after `background()`: `drawRhythm(20, 100, 360, 100)`. Each row is one drum that plays in the pattern, a hit lights up the moment it is heard (the clock [`drumPhase`](#drumPhase) reads), and a hit between two steps, such as a triplet, sits where it falls. The grid card is part of the page and a recording never shows it, but the canvas is what a recording takes, so this is how the rhythm gets into the video.","zh":"在 `draw()` 裡、`background()` 之後呼叫：`drawRhythm(20, 100, 360, 100)`。每一列是節奏裡有用到的一種鼓，打擊在你聽到的那一刻亮起來（用的就是 [`drumPhase`](#drumPhase) 讀的那個時鐘），兩格之間的打擊（例如三連音）會畫在它實際落下的位置。鼓機的卡片是頁面的一部分，錄影錄不到，但錄影錄的是畫布，所以要讓節奏進影片，就用這個。"},{"en":"With no box it draws a strip along the bottom for `'grid'` and a square in the middle for the other two. It draws with p5's own shapes between `push()` and `pop()`, so it leaves your styles alone and works in 2D and in `WEBGL`, in p5 1.x and 2.x. In `WEBGL` the origin is the middle of the canvas: `drawRhythm(-width / 2, -height / 2, width, height, 'circle')` fills it.","zh":"不給方框的話，`'grid'` 畫在畫布下方的一條帶子，另外兩種畫在中間的正方形。它用 p5 自己的圖形，包在 `push()` 和 `pop()` 之間，所以不會弄亂你設定的樣式，2D 和 `WEBGL`、p5 1.x 和 2.x 都能用。在 `WEBGL` 裡原點是畫布中心：`drawRhythm(-width / 2, -height / 2, width, height, 'circle')` 會鋪滿整個畫布。"}],"params":[{"name":"x","type":"Number","optional":true,"en":"Left edge of the box to draw in. In `WEBGL` the middle of the canvas is (0, 0). If any of `x`, `y`, `w`, `h` is left out, the default box is used.","zh":"要畫的方框的左邊。在 `WEBGL` 裡，畫布中心是 (0, 0)。`x`、`y`、`w`、`h` 只要有一個沒傳，就用預設的方框。"},{"name":"y","type":"Number","optional":true,"en":"Top edge of the box.","zh":"方框的上邊。"},{"name":"w","type":"Number","optional":true,"en":"Width of the box.","zh":"方框的寬。"},{"name":"h","type":"Number","optional":true,"en":"Height of the box.","zh":"方框的高。"},{"name":"style","type":"String | Object","optional":true,"default":"'grid'","en":"`'grid'` (the 16 steps with a playhead), `'circle'` (a ring of 16 per drum with a hand turned by [`drumPhase`](#drumPhase)) or `'spiral'` (the last few bars wound outwards, now at the tip). An object here is taken as `o`, with `style` inside it.","zh":"`'grid'`（16 格加一條播放頭）、`'circle'`（每種鼓一圈 16 格，指針照 [`drumPhase`](#drumPhase) 轉）或 `'spiral'`（最近幾小節往外繞，現在在最外面的尖端）。這裡如果給的是物件，就當成 `o`，`style` 寫在物件裡面。"},{"name":"o","type":"Object","optional":true,"en":"Options: `colors` (`{ kick: '#f66', … }`, any p5 colour per drum), `color` (one colour for all), `off` (the empty steps), `playhead` (a colour, or `false` for none), `bg` (a background for the box) and `bars` (spiral only, 1 to 16, 4 if left out).","zh":"選項：`colors`（`{ kick: '#f66', … }`，每種鼓一個 p5 看得懂的顏色）、`color`（全部同一個顏色）、`off`（空格子的顏色）、`playhead`（顏色，或 `false` 表示不畫）、`bg`（方框的底色）、`bars`（只用在螺旋，1 到 16，沒寫就是 4）。"}],"returns":{"type":"Boolean","en":"`true` if it drew something, `false` when the box is empty (`w` or `h` is not above 0) or there is no p5 global mode to draw with.","zh":"有畫東西就是 `true`，方框是空的（`w` 或 `h` 不大於 0），或沒有 p5 全域模式可以畫的時候是 `false`。"},"needs":{"en":"Start the machine with [`startDrums()`](#startDrums) first. Before it plays you get an empty four-lane grid.","zh":"要先用 [`startDrums()`](#startDrums) 啟動鼓機。在它開始播之前，畫出來的是一個空的四軌格子。"},"notes":[{"en":"`OAVp5.drums.draw(x, y, w, h, style, o)` is the same function as a method.","zh":"`OAVp5.drums.draw(x, y, w, h, style, o)` 是同一個函式，寫成方法的樣子。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);\n}\n\nfunction draw() {\n  background(14, 16, 26);\n  drawRhythm(20, 100, 360, 100);   // the 16 steps with a playhead\n}","caption":{"en":"The pattern as a grid on the canvas itself: a white playhead crosses it, and every hit lights up as it sounds.","zh":"節奏以格子的樣子畫在畫布上：一條白色的播放頭掃過去，每一下打擊發聲的時候那一格亮起來。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd(3,8), [~ sd]*2, hh*8, ~ ~ ~ cp', 104);\n}\n\nfunction draw() {\n  background(14, 16, 26);\n  drawRhythm(10, 10, 180, 180, 'circle');\n  drawRhythm(210, 10, 180, 180, 'spiral', { bars: 4, colors: { kick: '#ff7b54' } });\n  drawRhythm(20, 220, 360, 60, 'grid', { bg: 'rgba(255, 255, 255, 0.05)' });\n}","caption":{"en":"The same rhythm three ways: a ring per drum with a hand, the last four bars as a spiral, and the grid along the bottom.","zh":"同一個節奏的三種畫法：每種鼓一圈、有指針的圓，最近四小節繞成的螺旋，以及下方的格子。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300, WEBGL);\n  startDrums('four on floor', 120);\n}\n\nfunction draw() {\n  background(14, 16, 26);\n  drawRhythm(-width / 2, -height / 2, width, height, 'circle');   // (0, 0) is the middle in WEBGL\n}","caption":{"en":"In a WEBGL canvas the box starts at (-width / 2, -height / 2), because (0, 0) is the middle.","zh":"在 WEBGL 畫布裡方框要從 (-width / 2, -height / 2) 開始，因為 (0, 0) 是畫布中心。"},"run":"sketch","lang":"js"}],"related":["startDrums","drumPhase","drumStep","startRecording","rhythmChanged"],"keywords":{"en":["draw rhythm","visualize","grid","circle","spiral","playhead","drum visual","sequencer view","recording"],"zh":["畫節奏","視覺化","格子","圓形","螺旋","播放頭","鼓機畫面","錄影"]},"tbd":[],"claims":[]},
{"id":"drumBpm","key":"drumBpm","name":"drumBpm","aliases":["OAVp5.drums.bpm"],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"drumBpm","source":{"file":"p5/oav-p5.js","line":317,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L317"},"summary":{"en":"The tempo of the drum machine in beats per minute.","zh":"鼓機的速度，單位是每分鐘幾拍。"},"description":[{"en":"A beat is a quarter note, four steps, so one step lasts `60 / drumBpm / 4` seconds and one bar `240 / drumBpm`: at 120 a bar takes 2 seconds. To change the tempo call [`startDrums(bpm)`](#startDrums): `startDrums(drumBpm + 5)` speeds up by five. Writing to `drumBpm` yourself changes nothing.","zh":"一拍是一個四分音符，也就是四格，所以一格長 `60 / drumBpm / 4` 秒，一小節長 `240 / drumBpm` 秒：速度 120 的時候一小節是 2 秒。要改速度就呼叫 [`startDrums(bpm)`](#startDrums)：`startDrums(drumBpm + 5)` 就是加快 5。自己對 `drumBpm` 賦值不會有任何作用。"},{"en":"[`startDrums()`](#startDrums) keeps the tempo between 30 and 300; the slider on the grid goes from 60 to 180.","zh":"[`startDrums()`](#startDrums) 會把速度限制在 30 到 300 之間，格子上的滑桿則是從 60 到 180。"}],"value":{"type":"Number","range":"30..300","en":"112 until you set another one. It follows [`startDrums()`](#startDrums) and the tempo slider on the grid.","zh":"在你設定別的之前是 112。它會跟著 [`startDrums()`](#startDrums) 和格子上的速度滑桿走。"},"params":[],"notes":[{"en":"A sketch that declares its own variable called `drumBpm` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.drums.bpm` instead.","zh":"如果草圖本來就宣告了自己的 `drumBpm` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.drums.bpm`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 100);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction keyPressed() {\n  if (keyCode === UP_ARROW) startDrums(drumBpm + 5);\n  if (keyCode === DOWN_ARROW) startDrums(drumBpm - 5);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(round(drumBpm) + ' bpm', width / 2, height / 2);\n}","caption":{"en":"Press the up and down arrow keys to change the tempo by 5; the number shows it.","zh":"按上、下方向鍵，每次把速度加減 5，數字會顯示出來。"},"run":"sketch","lang":"js"}],"related":["startDrums","drumPhase","beatPhase","drumBar"],"keywords":{"en":["bpm","tempo","speed","beats per minute","drums","drum machine"],"zh":["速度","節拍速度","每分鐘幾拍","鼓機","bpm"]},"tbd":[],"claims":[{"expr":"drumBpm","equals":112}]},
{"id":"drumHit","key":"drumHit","name":"drumHit","aliases":[],"kind":"event","package":"oav-p5","category":"drums","documented":true,"signature":"function drumHit(lane, velocity) {}","source":{"file":"p5/oav-p5.js","line":1498,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1498"},"summary":{"en":"An event you define: it is called for every hit of the drum machine, with the drum that sounded and how hard.","zh":"一個由你定義的事件函式：鼓機每打一下就會呼叫它，告訴你是哪一種鼓、打得多用力。"},"description":[{"en":"Define `function drumHit(lane, velocity) { … }` and oav-p5 calls it for every hit of every drum, the way it calls [`notePressed()`](#notePressed) for a key. It is called in the frame in which the hit is heard, right before `draw()`.","zh":"定義 `function drumHit(lane, velocity) { … }`，鼓機每打一下、不論哪一種鼓，oav-p5 都會呼叫它，就像按一個鍵會呼叫 [`notePressed()`](#notePressed) 一樣。它是在你聽到那一下的那個畫面、`draw()` 之前被呼叫的。"},{"en":"The same hit also calls the microphone's functions, so you can pick the grain you want: [`kickDetected()`](#kickDetected) for the kick, `snareDetected()` for snare, clap and rim, `hatDetected()` for the three hats, and [`beatDetected()`](#beatDetected) (with [`beat`](#beat) true for a frame) for kick, snare, clap and rim, once per step. Toms, crash, ride and cowbell reach only `drumHit()`. On one step the order is: [`cueFired()`](#cueFired), [`barStarted()`](#barStarted), [`stepStarted()`](#stepStarted), then the hits.","zh":"同一下打擊也會呼叫麥克風用的那些函式，你可以挑想要的粒度：[`kickDetected()`](#kickDetected) 是大鼓，`snareDetected()` 是小鼓、拍手和鼓邊，`hatDetected()` 是三種鈸，[`beatDetected()`](#beatDetected)（同時 [`beat`](#beat) 在那個畫面是 true）是大鼓、小鼓、拍手和鼓邊，每一格最多一次。中鼓、碎音鈸、疊音鈸和牛鈴只會呼叫 `drumHit()`。同一格裡的順序是：[`cueFired()`](#cueFired)、[`barStarted()`](#barStarted)、[`stepStarted()`](#stepStarted)，最後才是各個打擊。"}],"params":[{"name":"lane","type":"String","en":"The drum that sounded: `'kick'`, `'snare'`, `'hat'` or `'clap'` for the grid, and also `'openhat'`, `'pedalhat'`, `'rim'`, `'lowtom'`, `'tom'`, `'hightom'`, `'crash'`, `'ride'` or `'cowbell'` for a line of rhythm.","zh":"發聲的鼓：格子上是 `'kick'`、`'snare'`、`'hat'`、`'clap'`，一行字寫的節奏還會有 `'openhat'`、`'pedalhat'`、`'rim'`、`'lowtom'`、`'tom'`、`'hightom'`、`'crash'`、`'ride'`、`'cowbell'`。"},{"name":"velocity","type":"Number","en":"How hard, from 0.15 to 1. It varies a little from hit to hit, because the machine plays slightly softer or louder on purpose.","zh":"打得多用力，從 0.15 到 1。每一下會有一點點不同，因為鼓機故意打得時輕時重。"}],"notes":[{"en":"While the tab is hidden nobody sees a frame: the clock keeps counting, but no hit is told, and nothing is told twice when the tab comes back. An error thrown inside `drumHit()` is printed, with its file and line, and the next hit still comes.","zh":"分頁被藏起來的時候沒有人看得到畫面：時鐘照樣在數，但不會通知任何一下打擊，分頁回來時也不會把漏掉的補通知一次。`drumHit()` 裡丟出的錯誤會連同檔案和行號印出來，下一下打擊照樣會來。"}],"examples":[{"code":"const COLORS = { kick: '#ff6b6b', snare: '#ffd166', hat: '#6fd3ff', clap: '#c792ff' };\nconst lit = { kick: 0, snare: 0, hat: 0, clap: 0 };\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);\n  noStroke();\n}\n\nfunction drumHit(lane, velocity) {\n  lit[lane] = velocity;\n}\n\nfunction draw() {\n  background(20);\n  Object.keys(lit).forEach((lane, i) => {\n    fill(COLORS[lane]);\n    circle(60 + i * 93, height / 2, 30 + lit[lane] * 70);\n    lit[lane] *= 0.85;\n  });\n}","caption":{"en":"Four circles, one per drum: each swells with the hit and shrinks back, and a harder hit swells more.","zh":"四個圓，一種鼓一個：每打一下就鼓起來再縮回去，打得越用力鼓得越大。"},"run":"sketch","lang":"js"},{"code":"let last = '';\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, cr ~ ~ ~, lt(3,8), hh*8', 100);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction drumHit(lane, velocity) {\n  last = lane;\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(last, width / 2, height / 2);\n}","caption":{"en":"The name of the latest drum that sounded: a line of rhythm brings crash and low tom, which only drumHit() reports.","zh":"顯示最近一下是哪種鼓：一行字寫的節奏會有碎音鈸和落地鼓，這些只有 drumHit() 會通知。"},"run":"sketch","lang":"js"}],"related":["startDrums","kickDetected","beatDetected","stepStarted","barStarted","OAVp5.drums.on","notePressed"],"keywords":{"en":["drum hit","hit","drum event","kick","snare","hat","clap","callback","velocity","trigger"],"zh":["打擊","鼓點","鼓的事件","大鼓","小鼓","力度","觸發","callback"]},"tbd":[],"claims":[]},
{"id":"drumPhase","key":"drumPhase","name":"drumPhase","aliases":["OAVp5.drums.phase"],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"drumPhase","source":{"file":"p5/oav-p5.js","line":728,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L728"},"summary":{"en":"How far through the current bar the drum machine is, smoothly from 0 up to just below 1, as it is heard.","zh":"鼓機走到這一小節的哪裡，從 0 平滑地增加到快要 1，是你耳朵聽到的位置。"},"description":[{"en":"Use it for anything that should move continuously with the music: `rotate(drumPhase * TWO_PI)` turns once per bar. [`drumStep`](#drumStep) says the same thing in 16 jumps; `drumPhase` is the smooth version. The value is what the listener hears, which is the machine's position corrected for the latency of the speakers, so a drawing driven by it lands on the beat you hear.","zh":"任何要跟著音樂連續移動的東西都用它：`rotate(drumPhase * TWO_PI)` 一小節轉一圈。[`drumStep`](#drumStep) 講的是同一件事，只是分成 16 次跳動，`drumPhase` 是平滑的版本。它的值是聽的人聽到的位置，也就是鼓機的位置再扣掉喇叭的延遲，所以用它驅動的畫面會落在你聽到的拍子上。"},{"en":"A bar has four beats; [`beatPhase`](#beatPhase) is the same idea for one beat.","zh":"一小節有四拍，[`beatPhase`](#beatPhase) 是同樣的概念，單位是一拍。"}],"value":{"type":"Number","range":"0..1","en":"Read fresh each time you look at it, so it is never a frame old. 0 at the start of a bar, close to 1 at its end, then 0 again. It is 0 before the machine has played and holds still while it is stopped.","zh":"每次讀都是當下的值，不會慢一個畫面。小節開頭是 0，結尾接近 1，然後回到 0。鼓機還沒播之前是 0，停下來的時候停在原地。"},"params":[],"needs":{"en":"Start the machine with [`startDrums()`](#startDrums); until it plays, `drumPhase` is 0.","zh":"先用 [`startDrums()`](#startDrums) 啟動鼓機，在它開始播之前，`drumPhase` 是 0。"},"notes":[{"en":"A sketch that declares its own `drumPhase` keeps it, and oav-p5 does not write that name. `OAVp5.drums.phase` is the same number under a name nothing else can take.","zh":"如果草圖本來就宣告了自己的 `drumPhase`，就用你的，oav-p5 不會去寫這個名稱。`OAVp5.drums.phase` 是同一個數字，用一個不會跟別人衝突的名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  strokeWeight(4);\n}\n\nfunction draw() {\n  background(20);\n  stroke(255, 211, 107);\n  translate(width / 2, height / 2);\n  rotate(drumPhase * TWO_PI - HALF_PI);\n  line(0, 0, 110, 0);   // a hand that turns once per bar\n}","caption":{"en":"A clock hand that turns exactly once every bar, smoothly.","zh":"一根時鐘的指針，每一小節剛好轉一圈，轉得很平滑。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 110);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const a = drumStep / 16 * TWO_PI - HALF_PI;   // hops from step to step\n  const b = drumPhase * TWO_PI - HALF_PI;       // moves all the time\n  fill(255, 120, 90);\n  circle(200 + cos(a) * 100, 150 + sin(a) * 100, 20);\n  fill(120, 200, 255);\n  circle(200 + cos(b) * 70, 150 + sin(b) * 70, 20);\n}","caption":{"en":"Two dots on two circles: the orange one hops from step to step, the blue one runs smoothly.","zh":"兩個圓上各有一個點：橘色的一格一格跳，藍色的連續不斷地跑。"},"run":"sketch","lang":"js"}],"related":["beatPhase","drumStep","drumBar","startDrums","drawRhythm"],"keywords":{"en":["phase","bar phase","progress","smooth","rotate","cycle","drums","position in bar"],"zh":["相位","小節進度","平滑","轉一圈","週期","鼓機","走到哪"]},"tbd":[],"claims":[{"before":"startDrums('four on floor', 120)","expr":"drumPhase","min":0,"max":1,"moves":true}]},
{"id":"drumsAreOn","key":"drumsAreOn","name":"drumsAreOn","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"drumsAreOn","source":{"file":"p5/oav-p5.js","line":316,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L316"},"summary":{"en":"Whether the drum machine is playing right now.","zh":"鼓機現在有沒有在播。"},"description":[{"en":"Use it to tell \"the drums are not running\" from \"the drums are at step 0\": [`drumStep`](#drumStep) is 0 in both cases. `if (drumsAreOn) { … }` is also the right guard before you read [`drumPhase`](#drumPhase). It changes only when the machine starts or stops, and reading it never starts anything.","zh":"用它來分辨「鼓機沒在跑」和「鼓機剛好在第 0 格」：這兩種情況下 [`drumStep`](#drumStep) 都是 0。在讀 [`drumPhase`](#drumPhase) 之前用 `if (drumsAreOn) { … }` 把關也很合適。它只有在鼓機開始或停止的時候才會變，讀它不會啟動任何東西。"},{"en":"For the reason behind a `false` read `OAVp5.drums.state`: [`'loading'`](#OAVp5.drums.state) while the module arrives, `'stopped'` when the grid is up but not playing, `'error'` when it could not load.","zh":"`false` 的原因看 `OAVp5.drums.state`：模組還在下載是 [`'loading'`](#OAVp5.drums.state)，格子在但沒在播是 `'stopped'`，載入失敗是 `'error'`。"}],"value":{"type":"Boolean","en":"True while the machine plays, false at the start, after [`stopDrums()`](#stopDrums), after an error and while the grid's own checkbox is unticked.","zh":"鼓機在播的時候是 true，一開始、呼叫 [`stopDrums()`](#stopDrums) 之後、出錯的時候、以及格子上自己的勾選框取消勾選的時候是 false。"},"params":[],"needs":{"en":"It becomes true after [`startDrums()`](#startDrums) (or 🥁 in the 🎹 panel) has loaded the machine, usually within a fraction of a second.","zh":"要先呼叫 [`startDrums()`](#startDrums)（或按 🎹 面板的 🥁），等鼓機載入完才會變成 true，通常不到一秒。"},"notes":[{"en":"A sketch that declares its own variable called `drumsAreOn` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.drums.state === 'on'` instead.","zh":"如果草圖本來就宣告了自己的 `drumsAreOn` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.drums.state === 'on'`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('half time', 100);\n  textAlign(CENTER, CENTER);\n}\n\nfunction draw() {\n  background(20);\n  fill(drumsAreOn ? color(120, 220, 140) : color(220, 160, 60));\n  circle(width / 2, height / 2, 80);\n  fill(255);\n  text(drumsAreOn ? 'drums are on' : 'drums are not running', width / 2, height - 40);\n}","caption":{"en":"An amber circle for a moment while the machine loads, then green.","zh":"鼓機載入的那一瞬間是琥珀色的圓，然後變成綠色。"},"run":"sketch","lang":"js"}],"related":["startDrums","stopDrums","drumStep","soundIsOn","micIsOn","recordingIsOn"],"keywords":{"en":["drums","on","playing","status","running","drum machine"],"zh":["鼓機","開著","播放中","狀態","有沒有在播"]},"tbd":[{"en":"After [`stopDrums()`](#stopDrums) and a second [`startDrums()`](#startDrums) it stayed `false` while the machine played again (Chromium, oav-p5 0.8.0). Not confirmed whether that is intended.","zh":"呼叫 [`stopDrums()`](#stopDrums) 之後再呼叫一次 [`startDrums()`](#startDrums)，鼓機明明又在播，它卻仍是 `false`（Chromium，oav-p5 0.8.0）。這是不是預期的行為，還沒有確認。"}],"claims":[{"expr":"drumsAreOn","equals":false}]},
{"id":"rhythmChanged","key":"rhythmChanged","name":"rhythmChanged","aliases":[],"kind":"event","package":"oav-p5","category":"drums","documented":true,"signature":"function rhythmChanged(src, how) {}","source":{"file":"p5/oav-p5.js","line":1637,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1637"},"summary":{"en":"An event you define: it is called with the new line of rhythm every time the rhythm changes.","zh":"一個由你定義的事件函式：節奏每次改變，就會帶著新的那一行節奏被呼叫。"},"description":[{"en":"The line over the grid and the cells edit each other, so the rhythm can change without your code: a click on a cell, a line typed into the card, ↶. `rhythmChanged(src, how)` tells the sketch, so it can show the line on the canvas (where a recording has it) or store it. It is also called once after [`startDrums()`](#startDrums), a moment after the rhythm module has loaded, with `how` equal to `'code'`.","zh":"格子上方的那一行字和格子本身會互相改寫，所以節奏可以在你的程式碼之外改變：點格子、在卡片上打字、按 ↶。`rhythmChanged(src, how)` 把這件事告訴草圖，讓它可以把那一行字畫在畫布上（錄影就錄得到），或是存起來。[`startDrums()`](#startDrums) 之後，節奏模組載入完的那一刻也會呼叫一次，這時 `how` 是 `'code'`。"}],"params":[{"name":"src","type":"String","en":"The line of rhythm that plays now, for example `'bd*4, [~ sd]*2, hh*8'`. A preset or a grid you drew comes as the line it is written as.","zh":"現在播的那一行節奏，例如 `'bd*4, [~ sd]*2, hh*8'`。預設節奏或你自己畫的格子，會以它對應的那一行字給你。"},{"name":"how","type":"String","en":"What changed it: `'code'` ([`startDrums()`](#startDrums)), `'cell'` (a click on a cell of the grid), `'type'` (typed into the line on the card, or [`OAVp5.drums.edit()`](#OAVp5.drums.edit)), `'undo'` (↶ or [`OAVp5.drums.undo()`](#OAVp5.drums.undo)) or `'menu'` (a preset picked on the card).","zh":"是什麼改變了它：`'code'`（[`startDrums()`](#startDrums)）、`'cell'`（點了格子上的一格）、`'type'`（在卡片的那行字上打字，或用 [`OAVp5.drums.edit()`](#OAVp5.drums.edit)）、`'undo'`（↶ 或 [`OAVp5.drums.undo()`](#OAVp5.drums.undo)）、`'menu'`（在卡片上挑了一個預設節奏）。"}],"notes":[],"examples":[{"code":"let current = '';\nlet how = '';\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, [~ sd]*2, hh*8', 110);\n  textAlign(CENTER, CENTER);\n}\n\nfunction rhythmChanged(src, mode) {\n  current = src;\n  how = mode;\n}\n\nfunction mousePressed() {\n  OAVp5.drums.edit('bd(3,8), [~ sd]*2, hh*16');   // as if typed on the card\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(current, width / 2, height / 2 - 10);\n  text('changed by: ' + how, width / 2, height / 2 + 20);\n  text('click the canvas, or click a cell of the grid', width / 2, height - 30);\n}","caption":{"en":"The line that plays and what changed it: \"code\" at the start, \"type\" after a click on the canvas, \"cell\" when you click a cell of the grid.","zh":"顯示正在播的那一行節奏和是誰改的：一開始是 \"code\"，點畫布之後是 \"type\"，點格子上的一格就是 \"cell\"。"},"run":"sketch","lang":"js"}],"related":["startDrums","OAVp5.drums.src","OAVp5.drums.edit","OAVp5.drums.undo","OAVp5.drums.history","OAVp5.drums.on","drawRhythm"],"keywords":{"en":["rhythm changed","line of rhythm","edit","callback","event","undo","strudel","pattern changed"],"zh":["節奏改變","一行節奏","編輯","事件","復原","節奏型改變"]},"tbd":[],"claims":[]},
{"id":"startDrums","key":"startDrums","name":"startDrums","aliases":["OAVp5.drums.start"],"kind":"function","package":"oav-p5","category":"drums","documented":true,"signature":"startDrums([pattern], [bpm])","source":{"file":"p5/oav-p5.js","line":1731,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1731"},"summary":{"en":"Starts a drum machine: a 16-step grid on the page that plays a preset, your own grid or one line of rhythm, and counts the bars for your sketch.","zh":"啟動鼓機：頁面上出現一個 16 格的鼓機，播放預設節奏、你自己畫的格子或一行字寫的節奏，並替草圖數小節。"},"description":[{"en":"Call it once in `setup()`: `startDrums('breakbeat', 120)`. A grid of 16 steps by 4 lanes (kick, snare, hat, clap) appears at the top of the page, with the tempo and the bar it is in, and plays a synthesized drum kit (no sample files). The grid is part of the page, not of the canvas: a recording never shows it, so use [`drawRhythm()`](#drawRhythm) to draw the rhythm on the canvas. A click on a cell changes the beat, and 🥁 in the 🎹 panel starts and stops the machine with no code at all.","zh":"在 `setup()` 裡呼叫一次：`startDrums('breakbeat', 120)`。頁面上方會出現 16 格 × 4 軌（大鼓、小鼓、鈸、拍手）的鼓機，旁邊有速度和目前的小節，用合成的鼓組發聲（沒有取樣檔）。這個鼓機是頁面的一部分，不在畫布上，所以錄影錄不到它，要把節奏畫進畫布請用 [`drawRhythm()`](#drawRhythm)。點格子可以改節奏，🎹 面板裡的 🥁 不用寫程式就能開關鼓機。"},{"en":"The machine is also a clock and a source of events. Every hit calls [`drumHit()`](#drumHit) and the microphone's own functions ([`kickDetected()`](#kickDetected), [`snareDetected()`](#kickDetected), [`hatDetected()`](#kickDetected), [`beatDetected()`](#beatDetected)), so a sketch written for the microphone moves over unchanged. [`drumStep`](#drumStep), [`drumBar`](#drumBar), [`drumPhase`](#drumPhase) and [`beatPhase`](#beatPhase) say where the machine is, and [`cue()`](#cue) lets you plan what happens at which bar.","zh":"鼓機同時也是時鐘和事件的來源。每一下打擊都會呼叫 [`drumHit()`](#drumHit) 和麥克風用的那幾個函式（[`kickDetected()`](#kickDetected)、[`snareDetected()`](#kickDetected)、[`hatDetected()`](#kickDetected)、[`beatDetected()`](#beatDetected)），所以為麥克風寫的草圖不用改就能換成鼓機。[`drumStep`](#drumStep)、[`drumBar`](#drumBar)、[`drumPhase`](#drumPhase)、[`beatPhase`](#beatPhase) 告訴你鼓機走到哪，[`cue()`](#cue) 讓你事先安排哪一小節發生什麼。"},{"en":"Calling it again while it plays changes the pattern or the tempo without restarting: `startDrums(90)` only changes the tempo, `startDrums('latin')` only the pattern. A line of rhythm is any text that is not a preset name. A word is a drum (`bd` kick, `sd` snare, `hh` hat, `oh` open hat, `cp` clap, `rim`, `lt` `mt` `ht` toms, `cr` crash, `rd` ride, `cb` cowbell), `~` is a rest, words separated by spaces share the bar evenly, `[ ]` groups, `*4` repeats, `,` puts another line on top, `< >` takes one per bar and `bd(3,8)` spreads 3 hits over 8 steps. A mistake shows a card that points at the character and says how to fix it, and `'four on floor'` plays meanwhile.","zh":"機器播的時候再呼叫，是換節奏或換速度，不會重新開始：`startDrums(90)` 只改速度，`startDrums('latin')` 只換節奏。不是預設節奏名字的文字，都當成一行節奏來讀。一個字就是一種鼓（`bd` 大鼓、`sd` 小鼓、`hh` 鈸、`oh` 開鈸、`cp` 拍手、`rim` 鼓邊、`lt` `mt` `ht` 三種中鼓、`cr` 碎音鈸、`rd` 疊音鈸、`cb` 牛鈴），`~` 是休息，用空白隔開的字平分一小節，`[ ]` 是一組，`*4` 是重複四次，`,` 是疊上另一行，`< >` 是每小節輪流一個，`bd(3,8)` 是把 3 下打擊平均分在 8 格裡。寫錯的話會跳出一張卡片，指出是哪個字、怎麼改，這時先播 `'four on floor'`。"},{"en":"A line copied from strudel.cc works too: `startDrums('s(\"bd*4, [~ sd]*2\").fast(2)')`. `s(\"…\")` and `sound(\"…\")` with `.fast(n)` and `.slow(n)` are read, `setcpm(110/4)` sets the tempo if you give none, `.bank(…)` is left out (oav-p5 plays its own kit), and any other call in the chain gets a card that says it is not supported yet. The rest plays.","zh":"從 strudel.cc 複製來的程式也可以：`startDrums('s(\"bd*4, [~ sd]*2\").fast(2)')`。`s(\"…\")` 和 `sound(\"…\")` 加上 `.fast(n)`、`.slow(n)` 會被讀進來，沒給速度的話 `setcpm(110/4)` 會設定速度，`.bank(…)` 會略過（oav-p5 用自己的鼓組），鏈裡其他的呼叫會跳一張卡片說目前還不支援，其餘照播。"}],"params":[{"name":"pattern","type":"String | Object | Number","optional":true,"en":"What to play. A preset name (`'four on floor'`, `'breakbeat'`, `'half time'`, `'latin'`, `'sparse'`). A line of rhythm such as `'bd*4, [~ sd]*2, hh*8'`, or one copied from strudel.cc. Your own grid `{ kick: 'x...x...x...x...', snare, hat, clap }` (`x` or `1` is a hit, a lane you leave out is silent, `{}` is a silent clock). A number alone is the tempo. Leave it out to keep what plays now (`'four on floor'` at the start).","zh":"要播什麼。預設節奏的名字（`'four on floor'`、`'breakbeat'`、`'half time'`、`'latin'`、`'sparse'`）。一行字寫的節奏，例如 `'bd*4, [~ sd]*2, hh*8'`，或從 strudel.cc 複製來的。你自己畫的格子 `{ kick: 'x...x...x...x...', snare, hat, clap }`（`x` 或 `1` 是打擊，沒寫的軌道是靜音，`{}` 是只走時間、不出聲的時鐘）。只給一個數字，那個數字就是速度。不傳就維持現在播的（一開始是 `'four on floor'`）。"},{"name":"bpm","type":"Number","optional":true,"en":"The tempo in beats per minute (a beat is four steps). Kept between 30 and 300. Leave it out to keep the current tempo: 112 at the start, or the one a copied Strudel line sets with `setcpm(…)`.","zh":"速度，單位是每分鐘幾拍（一拍是四格）。會被限制在 30 到 300 之間。不傳就維持現在的速度：一開始是 112，或是複製來的 Strudel 程式用 `setcpm(…)` 設的那個。"}],"returns":{"type":"Promise<Boolean>","en":"Resolves to `true` once the machine plays (or when a later `startDrums()` took over). Resolves to `false` when [`stopDrums()`](#stopDrums) came first, when there is no page to put the grid on, or when @openav/drums could not be loaded.","zh":"鼓機開始播之後 resolve 成 `true`（或者後面又有一次 `startDrums()` 接手的時候）。[`stopDrums()`](#stopDrums) 先被呼叫、沒有頁面可以放鼓機、或 @openav/drums 載入失敗的時候，resolve 成 `false`。"},"needs":{"en":"Nothing before the call. The counting, [`drumHit()`](#drumHit) and the other callbacks start at once, but a browser lets the page make sound only after a click or key press on it, so the drums are silent until the first one. A card at the top says so after a moment.","zh":"呼叫之前不用準備什麼。計數、[`drumHit()`](#drumHit) 和其他 callback 馬上就開始，但瀏覽器要頁面上先有一次點擊或按鍵才讓頁面出聲，所以在那之前鼓是沒聲音的，過一會兒上方會出現卡片提醒你。"},"notes":[{"en":"Drum sound does not need [`startSound()`](#startSound): the machine has its own kit. Pads on MIDI channel 10 play the same kit once `startSound()` is on.","zh":"鼓的聲音不需要 [`startSound()`](#startSound)：鼓機有自己的鼓組。MIDI 第 10 軌的打擊墊，在 `startSound()` 開著的時候打的就是同一組鼓。"},{"en":"The card has – to fold the grid away and × to stop the machine. A tab in the background keeps counting and playing. The loading options [`drums`](#option-drums) (`lookahead` or `frame`) and [`drumlatency`](#option-drumlatency) tune the clock.","zh":"卡片上的 – 可以把格子收起來，× 會停掉鼓機。分頁在背景的時候，鼓機照樣數、照樣播。載入選項 [`drums`](#option-drums)（`lookahead` 或 `frame`）和 [`drumlatency`](#option-drumlatency) 可以調整時鐘。"}],"examples":[{"code":"let swell = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);   // the grid appears at the top of the page\n}\n\nfunction kickDetected(level) {\n  swell = 1;\n}\n\nfunction draw() {\n  background(20);\n  swell *= 0.9;\n  circle(width / 2, height / 2, 60 + swell * 120);\n}","caption":{"en":"A grid appears at the top of the page. Click the canvas once to hear it: the circle jumps on every kick.","zh":"頁面上方出現鼓機。在畫布上點一下就聽得到鼓聲，每一下大鼓圓都會跳一下。"},"run":"sketch","lang":"js"},{"code":"const LINES = ['bd*4, [~ sd]*2, hh*8', 'bd(3,8), [~ sd]*2, hh*16', 'bd ~ ~ bd, ~ sd ~ ~, [hh hh]*4'];\nlet n = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums(LINES[0], 110);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  n = (n + 1) % LINES.length;\n  startDrums(LINES[n]);   // changes the rhythm while it plays\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('click to change the rhythm', width / 2, height / 2 - 20);\n  text(OAVp5.drums.src, width / 2, height / 2 + 20);\n}","caption":{"en":"Click the canvas to step through three rhythms: the line of text under the caption is the one that plays now, and the grid at the top follows it.","zh":"在畫布上點一下就換下一個節奏：說明文字下面那一行就是現在播的節奏，上方的鼓機格子也跟著變。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums({}, 90);   // no lane has a hit: nothing to hear, the clock still counts\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  for (let i = 0; i < 16; i++) {\n    fill(i === drumStep ? 255 : 90);\n    circle(30 + i * 22.5, height / 2, i === drumStep ? 18 : 8);\n  }\n}","caption":{"en":"A silent clock: sixteen dots with the current step enlarged, running at 90 bpm with no sound at all.","zh":"一個沒有聲音的時鐘：十六個點，現在這一格的點比較大，以每分鐘 90 拍的速度走，完全沒有聲音。"},"run":"sketch","lang":"js"}],"related":["stopDrums","drumsAreOn","drumHit","drumStep","drumPhase","drawRhythm","cue","rhythmChanged","beat"],"keywords":{"en":["drums","drum machine","rhythm","beat","tempo","bpm","pattern","sequencer","strudel","tidal","kick","snare","hi-hat","metronome","clock"],"zh":["鼓機","鼓","節奏","拍子","速度","節拍器","節奏型","大鼓","小鼓","時鐘","一行節奏"]},"tbd":[],"claims":[{"before":"startDrums('latin', 1000)","expr":"OAVp5.drums.bpm","equals":300},{"before":"startDrums('latin', 5)","expr":"OAVp5.drums.bpm","equals":30},{"before":"startDrums(90)","expr":"OAVp5.drums.bpm","equals":90}]},
{"id":"stopDrums","key":"stopDrums","name":"stopDrums","aliases":["OAVp5.drums.stop"],"kind":"function","package":"oav-p5","category":"drums","documented":true,"signature":"stopDrums()","source":{"file":"p5/oav-p5.js","line":1732,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1732"},"summary":{"en":"Stops the drum machine and hides its card.","zh":"停掉鼓機，並把它的卡片收起來。"},"description":[{"en":"The machine stops playing, the grid disappears and [`drumsAreOn`](#drumsAreOn) becomes `false`. The numbers keep the last value they had: [`drumStep`](#drumStep), [`drumBar`](#drumBar) and [`cueName`](#cueName) stay where they were, and [`drumPhase`](#drumPhase) and [`beatPhase`](#beatPhase) hold still. [`startDrums()`](#startDrums) afterwards begins again from bar 1.","zh":"鼓機停止播放，格子消失，[`drumsAreOn`](#drumsAreOn) 變成 `false`。各個數字停在最後的值：[`drumStep`](#drumStep)、[`drumBar`](#drumBar) 和 [`cueName`](#cueName) 留在原來的地方，[`drumPhase`](#drumPhase) 和 [`beatPhase`](#beatPhase) 也停住不動。之後再呼叫 [`startDrums()`](#startDrums) 會從第 1 小節重新開始。"},{"en":"It also works while the machine is still loading: the pending [`startDrums()`](#startDrums) then resolves to `false`. The × on the card does the same thing.","zh":"機器還在載入的時候呼叫也可以：還沒結束的 [`startDrums()`](#startDrums) 會 resolve 成 `false`。卡片上的 × 做的是同一件事。"}],"params":[],"returns":{"type":"Promise<Boolean>","en":"A promise that is already resolved to `false`, the new state of the machine.","zh":"一個已經 resolve 成 `false` 的 promise，`false` 就是鼓機的新狀態。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  stopDrums();\n}\n\nfunction draw() {\n  background(drumsAreOn ? 40 : 100);\n  fill(255);\n  text(drumsAreOn ? 'click to stop the drums' : 'stopped at step ' + drumStep, width / 2, height / 2);\n}","caption":{"en":"Click the canvas: the grid disappears, the background turns light and the text shows the step it stopped on.","zh":"在畫布上點一下：鼓機消失，背景變亮，文字顯示它停在第幾格。"},"run":"sketch","lang":"js"}],"related":["startDrums","drumsAreOn","stopSound"],"keywords":{"en":["drums","stop","off","halt","silence","drum machine"],"zh":["鼓機","停止","關閉","停掉","靜音"]},"tbd":[{"en":"After `stopDrums()` and a second `startDrums()` on the same page the machine plays and counts again, but [`drumsAreOn`](#drumsAreOn) and `OAVp5.drums.state` stayed `false` and `'off'` when this was tried (Chromium, oav-p5 0.8.0). Whether that is intended is not confirmed.","zh":"在同一個頁面呼叫 `stopDrums()` 之後再呼叫一次 `startDrums()`，鼓機會再度播放、計數，但實際試的時候（Chromium，oav-p5 0.8.0），[`drumsAreOn`](#drumsAreOn) 和 `OAVp5.drums.state` 仍然是 `false` 和 `'off'`。這是不是預期的行為，還沒有確認。"}],"claims":[]},
{"id":"OAVp5.drums","key":"OAVp5.drums","name":"OAVp5.drums","aliases":[],"kind":"object","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums","source":{"file":"p5/oav-p5.js","line":1471,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1471"},"summary":{"en":"The object behind the drum names: the state of the machine, its counters, the line of rhythm, its history and the loaded machine.","zh":"鼓機相關名稱背後的物件：機器的狀態、各種計數、那一行節奏、它的歷史，以及載入進來的鼓機。"},"description":[{"en":"`OAVp5.drums` (also reachable as `midi.drums`) is what [`startDrums()`](#startDrums), [`drumStep`](#drumStep) and the rest are built on. It holds the numbers ([`state`](#OAVp5.drums.state), [`step`](#drumStep), [`bar`](#drumBar), [`count`](#OAVp5.drums.count), [`bpm`](#drumBpm), [`phase`](#drumPhase), `beatPhase`), the rhythm ([`pattern`](#OAVp5.drums.pattern), [`src`](#OAVp5.drums.src), [`copyText`](#OAVp5.drums.copyText), [`strudelUrl`](#OAVp5.drums.strudelUrl), [`history`](#OAVp5.drums.history), [`edit()`](#OAVp5.drums.edit), [`undo()`](#OAVp5.drums.undo)), the cues ([`cue()`](#OAVp5.drums.cue), [`cues`](#OAVp5.drums.cues), [`lastCue`](#cueName)), a way to listen ([`on()`](#OAVp5.drums.on)), what the machine hears right now ([`now`](#OAVp5.drums.now)) and handles for the curious ([`machine`](#OAVp5.drums.machine), [`clock`](#OAVp5.drums.clock)).","zh":"`OAVp5.drums`（也可以用 `midi.drums` 取得）是 [`startDrums()`](#startDrums)、[`drumStep`](#drumStep) 這些名稱的底層。它裡面有數字（[`state`](#OAVp5.drums.state)、[`step`](#drumStep)、[`bar`](#drumBar)、[`count`](#OAVp5.drums.count)、[`bpm`](#drumBpm)、[`phase`](#drumPhase)、`beatPhase`）、節奏（[`pattern`](#OAVp5.drums.pattern)、[`src`](#OAVp5.drums.src)、[`copyText`](#OAVp5.drums.copyText)、[`strudelUrl`](#OAVp5.drums.strudelUrl)、[`history`](#OAVp5.drums.history)、[`edit()`](#OAVp5.drums.edit)、[`undo()`](#OAVp5.drums.undo)）、cue（[`cue()`](#OAVp5.drums.cue)、[`cues`](#OAVp5.drums.cues)、[`lastCue`](#cueName)）、監聽的方法（[`on()`](#OAVp5.drums.on)）、鼓機此刻聽到的樣子（[`now`](#OAVp5.drums.now)），以及給好奇的人用的把手（[`machine`](#OAVp5.drums.machine)、[`clock`](#OAVp5.drums.clock)）。"},{"en":"`start`, `stop` and `draw` are the same as [`startDrums()`](#startDrums), [`stopDrums()`](#stopDrums) and [`drawRhythm()`](#drawRhythm). For a sketch the global names are enough. This object is for tools, libraries (Hydra, for one), the console and debugging.","zh":"`start`、`stop`、`draw` 跟 [`startDrums()`](#startDrums)、[`stopDrums()`](#stopDrums)、[`drawRhythm()`](#drawRhythm) 是同一回事。一般草圖用全域名稱就夠了。這個物件是給工具、函式庫（例如 Hydra）、主控台和除錯用的。"}],"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('latin', 110);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const d = OAVp5.drums;\n  text('state: ' + d.state, 20, 30);\n  text('pattern: ' + d.pattern, 20, 60);\n  text('bar ' + d.bar + ', step ' + d.step + ', count ' + d.count, 20, 90);\n  text('bpm: ' + d.bpm, 20, 120);\n}","caption":{"en":"Four lines read from the drums object while the machine plays.","zh":"機器在播的時候，從鼓機物件讀出來的四行資料。"},"run":"sketch","lang":"js"}],"related":["startDrums","OAVp5.drums.state","OAVp5.drums.on","OAVp5.sound","OAVp5.record"],"keywords":{"en":["OAVp5.drums","drums object","midi.drums","drum state","debug","drum machine"],"zh":["鼓機物件","鼓機狀態","除錯","midi.drums"]},"tbd":[],"claims":[]},
{"id":"OAVp5.drums.copyText","key":"OAVp5.drums.copyText","name":"OAVp5.drums.copyText","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.copyText","source":{"file":"p5/oav-p5.js","line":1467,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1467"},"summary":{"en":"The text the Copy button on the card puts on the clipboard: the rhythm that plays now as a ready `startDrums(\"…\", bpm)` call.","zh":"卡片上「複製」按鈕放進剪貼簿的文字：把現在播的節奏寫成可以直接貼上的 `startDrums(\"…\", bpm)`。"},"description":[{"en":"Click cells, type a line, find a rhythm you like, and paste this into `setup()` to keep it. The tempo is the current one, rounded to two decimals.","zh":"點格子、打字，找到喜歡的節奏之後，把這一行貼進 `setup()` 就能留住它。速度是現在的速度，四捨五入到小數點後兩位。"}],"value":{"type":"String","en":"For example `startDrums(\"bd*4, [~ sd]*2, hh*8\", 110)`. Read fresh each time; `''` before the machine has loaded.","zh":"例如 `startDrums(\"bd*4, [~ sd]*2, hh*8\", 110)`。每次讀都是當下的值，鼓機載入之前是 `''`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);\n  textSize(12);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.drums.copyText, 20, 20, width - 40, 200);\n}","caption":{"en":"The line to paste back into setup(); click cells on the grid and it follows.","zh":"可以貼回 setup() 的那一行，在格子上點一點，它也會跟著變。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums.src","OAVp5.drums.strudelUrl","startDrums"],"keywords":{"en":["copy","clipboard","copy code","startDrums call","save rhythm"],"zh":["複製","剪貼簿","複製程式碼","存下節奏"]},"tbd":[],"claims":[]},
{"id":"OAVp5.drums.edit","key":"OAVp5.drums.edit","name":"OAVp5.drums.edit","aliases":[],"kind":"function","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.edit(text)","source":{"file":"p5/oav-p5.js","line":1465,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1465"},"summary":{"en":"Plays a line of rhythm as if it was typed on the card, and returns whether it worked and, if not, what is wrong with it.","zh":"把一行節奏當成在卡片上打的字來播，並回傳有沒有成功，不成功的話告訴你哪裡有問題。"},"description":[{"en":"Unlike [`startDrums(text)`](#startDrums), which plays 'four on floor' when the line has a mistake, `edit()` keeps the line that was playing and tells you why the new one failed, so a live-coding sketch can show the message. The line goes into [`OAVp5.drums.history`](#OAVp5.drums.history) and [`rhythmChanged()`](#rhythmChanged) is called with `how` equal to `'type'`. A mistake also shows the card that points at the character.","zh":"跟 [`startDrums(text)`](#startDrums) 不同：那一行有錯的時候，`startDrums` 會改播 'four on floor'，而 `edit()` 會讓原本那一行繼續播，並告訴你新的為什麼失敗，所以即時編碼的草圖可以把訊息顯示出來。這一行會進到 [`OAVp5.drums.history`](#OAVp5.drums.history)，[`rhythmChanged()`](#rhythmChanged) 也會被呼叫，`how` 是 `'type'`。有錯的時候也會跳出卡片，指出是哪個字。"}],"params":[{"name":"text","type":"String","en":"The line of rhythm, for example `'bd*4, [~ sd]*2, hh*8'`, or a line copied from strudel.cc.","zh":"一行節奏，例如 `'bd*4, [~ sd]*2, hh*8'`，或是從 strudel.cc 複製來的程式。"}],"returns":{"type":"Object","en":"`{ ok: true, error: null, text }` when the line plays. `{ ok: false, error, text }` when it has a mistake: `error` has `code`, `index` and `length` (where the bad word is in `text`), a `message` and a `hint`, and the last line that worked keeps playing. Before the machine has loaded: `{ ok: false, error: null }`.","zh":"這行節奏播起來了就回傳 `{ ok: true, error: null, text }`。有錯的話回傳 `{ ok: false, error, text }`：`error` 裡有 `code`、`index` 和 `length`（錯的那個字在 `text` 的位置）、`message` 和 `hint`，這時上一行能播的節奏繼續播。鼓機還沒載入的時候回傳 `{ ok: false, error: null }`。"},"needs":{"en":"The machine and the rhythm module must have loaded: call [`startDrums()`](#startDrums) first and give it a moment.","zh":"鼓機和節奏模組必須已經載入完成：先呼叫 [`startDrums()`](#startDrums)，再等一下。"},"notes":[],"examples":[{"code":"const TRIES = ['bd*4, hh*8', 'bd(3,8), [~ sd]*2', 'bd*4, xyz*8'];\nlet n = 0;\nlet result = '';\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, [~ sd]*2, hh*8', 110);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  const r = OAVp5.drums.edit(TRIES[n % TRIES.length]);\n  result = r.ok ? 'ok: ' + r.text : 'wrong: ' + r.error.message;\n  n++;\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('click to try the next line', width / 2, 40);\n  text(result, width / 2, height / 2 - 10);\n  text('playing: ' + OAVp5.drums.src, width / 2, height / 2 + 30);\n}","caption":{"en":"Click three times: the first two lines play; the third has a mistake, so the message is shown and the line before it keeps playing.","zh":"連點三下：前兩行會播，第三行有錯，所以顯示錯誤訊息，並讓前一行繼續播。"},"run":"sketch","lang":"js"}],"related":["startDrums","OAVp5.drums.undo","OAVp5.drums.history","rhythmChanged","OAVp5.drums.src"],"keywords":{"en":["edit","type a line","live coding","change rhythm","error message","mistake","drums"],"zh":["編輯","改節奏","即時編碼","錯誤訊息","打字","鼓機"]},"tbd":[],"claims":[]},
{"id":"OAVp5.drums.history","key":"OAVp5.drums.history","name":"OAVp5.drums.history","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.history","source":{"file":"p5/oav-p5.js","line":1461,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1461"},"summary":{"en":"The earlier lines of rhythm, oldest first: every click on a cell, typed line or menu pick leaves the line before it here.","zh":"之前播過的節奏，最舊的在最前面：每點一次格子、每改一次文字、每從選單挑一次，前一行就會存到這裡。"},"description":[{"en":"[`OAVp5.drums.undo()`](#OAVp5.drums.undo) takes the last line off this list and plays it. Changes made by [`startDrums()`](#startDrums) are not recorded here, only the ones made on the card or with [`OAVp5.drums.edit()`](#OAVp5.drums.edit). The array is the machine's own, so treat it as read-only.","zh":"[`OAVp5.drums.undo()`](#OAVp5.drums.undo) 會把這份清單最後一行拿出來播。用 [`startDrums()`](#startDrums) 做的改變不會記在這裡，只有在卡片上做的，或用 [`OAVp5.drums.edit()`](#OAVp5.drums.edit) 做的才會。這個陣列是鼓機自己的，請當成唯讀。"}],"value":{"type":"Array<String>","en":"Empty at the start, and at most 50 lines long. The line that plays now is not in it: that is [`OAVp5.drums.src`](#OAVp5.drums.src).","zh":"一開始是空的，最多保留 50 行。現在在播的那一行不在裡面，那一行是 [`OAVp5.drums.src`](#OAVp5.drums.src)。"},"params":[],"notes":[],"examples":[{"code":"const LINES = ['bd*4, hh*8', 'bd(3,8), hh*8', 'bd ~ ~ bd, [~ sd]*2, hh*16'];\nlet n = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums(LINES[0], 110);\n  textSize(14);\n}\n\nfunction mousePressed() {\n  n = (n + 1) % LINES.length;\n  OAVp5.drums.edit(LINES[n]);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('click to edit the line', 20, 30);\n  text('history: ' + OAVp5.drums.history.length + ' earlier lines', 20, 60);\n  text(OAVp5.drums.history.join('\\n'), 20, 90);\n}","caption":{"en":"Each click edits the line and adds the one before it to the history list.","zh":"每點一下就改一次那一行，前一行會加進歷史清單。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums.undo","OAVp5.drums.edit","OAVp5.drums.src","rhythmChanged"],"keywords":{"en":["history","undo list","earlier rhythms","previous lines"],"zh":["歷史","復原清單","之前的節奏","上一行"]},"tbd":[],"claims":[{"expr":"OAVp5.drums.history.length","equals":0}]},
{"id":"OAVp5.drums.now","key":"OAVp5.drums.now","name":"OAVp5.drums.now","aliases":[],"kind":"object","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.now","source":{"file":"p5/oav-p5.js","line":1449,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1449"},"summary":{"en":"A snapshot of what the machine hears right now: the bar, the step, both phases and which words of the line are lit.","zh":"鼓機此刻聽到的樣子，一次拿齊：小節、格、兩個相位，以及那一行字裡哪些字亮著。"},"description":[{"en":"It is what the card uses to light the word that sounds. Read it when you want the line and its lit words in one go, for example to draw the line on the canvas with the sounding word marked. Before the machine has loaded it answers with zeros and an empty `text`.","zh":"卡片就是用它讓正在響的字亮起來。想一次拿到那一行字和亮著的字的時候讀它，例如把那一行字畫在畫布上，並標出正在響的字。鼓機載入之前，它給的是零和空的 `text`。"}],"value":{"type":"Object","range":"{ bar, step, phase, beatPhase, tokens, mode, text }","en":"A new object on every read. `phase` and `beatPhase` are [`drumPhase`](#drumPhase) and [`beatPhase`](#beatPhase). `tokens` is a list of `{ start, end }`: the places in `text` of the words that sound now. `mode` is `'string'` for a line of rhythm and `'grid'` for a preset or a grid you drew. `text` is the line.","zh":"每次讀都是一個新的物件。`phase` 和 `beatPhase` 就是 [`drumPhase`](#drumPhase) 和 [`beatPhase`](#beatPhase)。`tokens` 是 `{ start, end }` 的清單，標出 `text` 裡現在正在響的那幾個字的位置。`mode` 在播一行節奏時是 `'string'`，預設節奏或你自己畫的格子是 `'grid'`。`text` 是那一行字。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, [~ sd]*2, hh*8', 80);\n  textFont('monospace');\n  textSize(18);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const n = OAVp5.drums.now;\n  fill(255, 211, 107);\n  for (const t of n.tokens) {\n    rect(20 + textWidth(n.text.slice(0, t.start)), 150, textWidth(n.text.slice(t.start, t.end)), 3);   // underline the sounding words\n  }\n  fill(255);\n  text(n.text, 20, 140);\n}","caption":{"en":"The line of rhythm with a yellow underline that follows the words as they sound.","zh":"那一行節奏，下面有一條黃色底線，跟著正在響的字移動。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums.src","drumPhase","beatPhase","drumStep","drumBar"],"keywords":{"en":["now","snapshot","current state","tokens","lit words","highlight","drums"],"zh":["現在","快照","目前狀態","亮著的字","標示","鼓機"]},"tbd":[],"claims":[]},
{"id":"OAVp5.drums.on","key":"OAVp5.drums.on","name":"OAVp5.drums.on","aliases":[],"kind":"function","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.on(name, fn)","source":{"file":"p5/oav-p5.js","line":1441,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1441"},"summary":{"en":"Listens to the drum machine from anywhere: `OAVp5.drums.on('hit', fn)` calls `fn` like `drumHit` does, and returns a function that stops listening.","zh":"在任何地方監聽鼓機：`OAVp5.drums.on('hit', fn)` 會像 `drumHit` 一樣呼叫 `fn`，並回傳一個可以取消監聽的函式。"},"description":[{"en":"The functions you define (`drumHit`, `barStarted` …) are one listener each. `on()` is for the cases where that does not fit: several listeners for one event, a library or a Hydra sketch that has no `drumHit()` of its own, a listener you want to remove again. It is the same idea as `midi.on()`. An error thrown inside `fn` is printed and the other listeners still run.","zh":"你定義的函式（`drumHit`、`barStarted` ……）每種事件只能有一個。`on()` 是給不適用的情況：同一個事件要有好幾個監聽者、函式庫或 Hydra 草圖沒有自己的 `drumHit()`、或是想把監聽者再拿掉。概念跟 `midi.on()` 一樣。`fn` 裡丟出的錯誤會被印出來，其他監聽者照樣執行。"}],"params":[{"name":"name","type":"String","en":"What to listen to: `'hit'` (like [`drumHit()`](#drumHit)), `'step'` (like [`stepStarted()`](#stepStarted)), `'bar'` (like [`barStarted()`](#barStarted)), `'cue'` (like [`cueFired()`](#cueFired)) or `'rhythm'` (like [`rhythmChanged()`](#rhythmChanged)). Any other name throws an error that lists them.","zh":"要監聽什麼：`'hit'`（像 [`drumHit()`](#drumHit)）、`'step'`（像 [`stepStarted()`](#stepStarted)）、`'bar'`（像 [`barStarted()`](#barStarted)）、`'cue'`（像 [`cueFired()`](#cueFired)）、`'rhythm'`（像 [`rhythmChanged()`](#rhythmChanged)）。其他的名字會丟出錯誤，並列出這幾個。"},{"name":"fn","type":"Function","en":"Called with the same arguments as the function it matches: `(lane, velocity)`, `(step, bar)`, `(bar)`, `(name, bar)` or `(src, how)`.","zh":"被呼叫的時候帶的參數跟對應的函式一樣：`(lane, velocity)`、`(step, bar)`、`(bar)`、`(name, bar)` 或 `(src, how)`。"}],"returns":{"type":"Function","en":"A function: call it to stop listening.","zh":"一個函式，呼叫它就停止監聽。"},"notes":[],"examples":[{"code":"let bars = 0;\nlet off = null;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 160);\n  off = OAVp5.drums.on('bar', (bar) => { bars++; });   // returns a function that stops it\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  if (off) { off(); off = null; }   // stop counting\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('bars counted: ' + bars, width / 2, height / 2);\n  text(off ? 'click to stop counting' : 'stopped', width / 2, height / 2 + 30);\n}","caption":{"en":"A counter that goes up once per bar; a click removes the listener and the number stops.","zh":"一個每小節加一的計數器，點一下就拿掉監聽者，數字就停住。"},"run":"sketch","lang":"js"}],"related":["drumHit","stepStarted","barStarted","cueFired","rhythmChanged","OAVp5.drums"],"keywords":{"en":["listen","subscribe","event","listener","on","hydra","drums","off"],"zh":["監聽","訂閱","事件","聽事件","取消監聽","鼓機"]},"tbd":[],"claims":[{"expr":"typeof OAVp5.drums.on('bar', function () {})","equals":"function"}]},
{"id":"OAVp5.drums.pattern","key":"OAVp5.drums.pattern","name":"OAVp5.drums.pattern","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.pattern","source":{"file":"p5/oav-p5.js","line":1437,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1437"},"summary":{"en":"What the drum machine plays, as a name: the preset's name, `'custom'` for a grid, or the line of rhythm itself.","zh":"鼓機正在播的東西，用名字表示：預設節奏的名字、自己畫的格子是 `'custom'`，一行節奏就是那一行字本身。"},"description":[{"en":"After `startDrums('breakbeat')` it is `'breakbeat'`, after `startDrums({ kick: 'x...x...x...x...' })` it is `'custom'`, and after `startDrums('bd*4, hh*8')` it is that line. A click on a cell of a preset makes it `'custom'`. To get the line of rhythm in every case, read [`OAVp5.drums.src`](#OAVp5.drums.src): a preset also has one.","zh":"`startDrums('breakbeat')` 之後是 `'breakbeat'`，`startDrums({ kick: 'x...x...x...x...' })` 之後是 `'custom'`，`startDrums('bd*4, hh*8')` 之後就是那一行字。在預設節奏的格子上點一下，它就變成 `'custom'`。想在每一種情況下都拿到那一行節奏，請讀 [`OAVp5.drums.src`](#OAVp5.drums.src)：預設節奏也有對應的那一行。"}],"value":{"type":"String","en":"`'four on floor'` until you choose another. It follows what plays, including clicks on the grid.","zh":"在你選別的之前是 `'four on floor'`。它跟著正在播的東西走，包括在格子上點的修改。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 110);\n  textAlign(CENTER, CENTER);\n  textSize(24);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.drums.pattern, width / 2, height / 2);\n}","caption":{"en":"Shows \"breakbeat\"; click a cell of the grid and it turns into \"custom\".","zh":"顯示 \"breakbeat\"，在鼓機的格子上點一下，就變成 \"custom\"。"},"run":"sketch","lang":"js"}],"related":["startDrums","OAVp5.drums.src","rhythmChanged"],"keywords":{"en":["pattern","preset","current pattern","custom","rhythm name","drums"],"zh":["節奏型","預設節奏","目前節奏","自訂","鼓機"]},"tbd":[],"claims":[{"expr":"OAVp5.drums.pattern","equals":"four on floor"}]},
{"id":"OAVp5.drums.src","key":"OAVp5.drums.src","name":"OAVp5.drums.src","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.src","source":{"file":"p5/oav-p5.js","line":1457,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1457"},"summary":{"en":"The line of rhythm that plays now, such as `'bd*4, [~ sd]*2, hh*8'`; an empty string until the machine has loaded.","zh":"現在播的那一行節奏，例如 `'bd*4, [~ sd]*2, hh*8'`。鼓機載入之前是空字串。"},"description":[{"en":"Show it on the canvas so a recording has it (`text(OAVp5.drums.src, 20, 30)`), or keep it to come back to the rhythm later with [`startDrums(src)`](#startDrums). [`rhythmChanged()`](#rhythmChanged) tells you when it changes, and [`OAVp5.drums.copyText`](#OAVp5.drums.copyText) is the same line wrapped in a `startDrums(\"…\")` call.","zh":"把它畫在畫布上，錄影就錄得到（`text(OAVp5.drums.src, 20, 30)`），或是存起來，之後用 [`startDrums(src)`](#startDrums) 回到這個節奏。它改變的時候 [`rhythmChanged()`](#rhythmChanged) 會通知你，[`OAVp5.drums.copyText`](#OAVp5.drums.copyText) 則是同一行字外面包上 `startDrums(\"…\")`。"}],"value":{"type":"String","en":"Read fresh each time. A preset or a grid you drew shows as the line it is written as, and it changes with every click on a cell and every typed edit.","zh":"每次讀都是當下的值。預設節奏或你自己畫的格子，會顯示成它對應的那一行字，每點一次格子、每改一次文字，它就跟著變。"},"params":[],"needs":{"en":"It is `''` until [`startDrums()`](#startDrums) has loaded the machine and the rhythm module, a fraction of a second.","zh":"在 [`startDrums()`](#startDrums) 載入鼓機和節奏模組之前都是 `''`，大約不到一秒。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('half time', 100);\n  textAlign(CENTER, CENTER);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.drums.src || 'loading…', width / 2, height / 2);\n}","caption":{"en":"The preset \"half time\" written as the line of rhythm it is; click cells on the grid and the line is rewritten.","zh":"預設節奏 \"half time\" 對應的那一行字，在格子上點一點，這一行就會被改寫。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums.pattern","OAVp5.drums.copyText","OAVp5.drums.edit","rhythmChanged","startDrums"],"keywords":{"en":["line of rhythm","source","rhythm string","current line","mini notation","drums"],"zh":["一行節奏","節奏文字","目前這一行","鼓機","寫法"]},"tbd":[],"claims":[]},
{"id":"OAVp5.drums.state","key":"OAVp5.drums.state","name":"OAVp5.drums.state","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.state","source":{"file":"p5/oav-p5.js","line":1436,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1436"},"summary":{"en":"Where the drum machine is: `'off'`, `'loading'`, `'on'`, `'stopped'` or `'error'`.","zh":"鼓機現在在哪個階段：`'off'`、`'loading'`、`'on'`、`'stopped'` 或 `'error'`。"},"description":[{"en":"`'loading'` is the moment after [`startDrums()`](#startDrums) while the module arrives, `'on'` is playing, `'stopped'` is a grid that is up but not playing (its checkbox is unticked), `'off'` is where it starts and where [`stopDrums()`](#stopDrums) leaves it, and `'error'` means @openav/drums could not be loaded. [`drumsAreOn`](#drumsAreOn) is `state === 'on'`.","zh":"`'loading'` 是呼叫 [`startDrums()`](#startDrums) 之後，模組還在下載的那段時間，`'on'` 是正在播，`'stopped'` 是格子還在但沒在播（它的勾選框取消勾選了），`'off'` 是起點，也是 [`stopDrums()`](#stopDrums) 之後的狀態，`'error'` 表示 @openav/drums 載入失敗。[`drumsAreOn`](#drumsAreOn) 就是 `state === 'on'`。"}],"value":{"type":"String","range":"off | loading | on | stopped | error","en":"Starts as `'off'` and changes when the machine loads, plays, is stopped or fails.","zh":"一開始是 `'off'`，鼓機載入、播放、停止或失敗的時候就跟著變。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('sparse', 90);\n  textAlign(CENTER, CENTER);\n  textSize(28);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.drums.state, width / 2, height / 2);\n}","caption":{"en":"Shows \"loading\" for a moment, then \"on\". Untick the checkbox on the grid and it reads \"stopped\".","zh":"先短暫顯示 \"loading\"，然後是 \"on\"。把鼓機上的勾選框取消勾選，就變成 \"stopped\"。"},"run":"sketch","lang":"js"}],"related":["drumsAreOn","startDrums","stopDrums","OAVp5.drums"],"keywords":{"en":["drum state","status","loading","stopped","drums","machine state"],"zh":["鼓機狀態","載入中","已停止","狀態"]},"tbd":[{"en":"After [`stopDrums()`](#stopDrums) and a second [`startDrums()`](#startDrums) the state stayed `'off'` while the machine played again (Chromium, oav-p5 0.8.0). Not confirmed whether that is intended.","zh":"呼叫 [`stopDrums()`](#stopDrums) 之後再呼叫一次 [`startDrums()`](#startDrums)，鼓機明明又在播，狀態卻仍是 `'off'`（Chromium，oav-p5 0.8.0）。這是不是預期的行為，還沒有確認。"}],"claims":[{"expr":"OAVp5.drums.state","equals":"off"}]},
{"id":"OAVp5.drums.strudelUrl","key":"OAVp5.drums.strudelUrl","name":"OAVp5.drums.strudelUrl","aliases":[],"kind":"variable","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.strudelUrl","source":{"file":"p5/oav-p5.js","line":1459,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1459"},"summary":{"en":"The rhythm that plays now as a link that opens it at strudel.cc, or an empty string when there is none.","zh":"現在播的節奏做成的連結，打開就會在 strudel.cc 裡開啟同一個節奏，沒有的時候是空字串。"},"description":[{"en":"It holds the line as `s(\"…\")` with `setcpm(bpm/4)` (one bar here is one Strudel cycle of four beats), the same thing the \"Open in Strudel\" link on the card opens. Open it in a new tab: `window.open(OAVp5.drums.strudelUrl)` from a click handler.","zh":"裡面是那一行節奏，寫成 `s(\"…\")`，再加上 `setcpm(bpm/4)`（這裡的一小節就是 Strudel 的一個週期，四拍），跟卡片上「在 Strudel 打開」連結開啟的是同一個東西。用新分頁打開它：在點擊的處理函式裡寫 `window.open(OAVp5.drums.strudelUrl)`。"}],"value":{"type":"String","en":"A `https://strudel.cc/#…` address, read fresh each time. `''` before the machine has loaded.","zh":"一個 `https://strudel.cc/#…` 的網址，每次讀都是當下的。鼓機載入之前是 `''`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, [~ sd]*2, hh*8', 110);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  if (OAVp5.drums.strudelUrl) window.open(OAVp5.drums.strudelUrl, '_blank');\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('click to open this rhythm in strudel.cc', width / 2, height / 2 - 15);\n  text(OAVp5.drums.strudelUrl.slice(0, 40) + '…', width / 2, height / 2 + 15);\n}","caption":{"en":"Shows the start of the link; a click opens it at strudel.cc in a new tab.","zh":"顯示連結的開頭，點一下就在新分頁的 strudel.cc 裡打開。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums.src","OAVp5.drums.copyText","startDrums"],"keywords":{"en":["strudel","strudel.cc","open in strudel","link","url","live coding"],"zh":["Strudel","連結","網址","在 Strudel 打開","即時編碼"]},"tbd":[{"en":"Not tried: whether the sketch frame of OpenProcessing lets `window.open` open the link.","zh":"還沒試過：OpenProcessing 的草圖框是否允許 `window.open` 開啟這個連結。"}],"claims":[]},
{"id":"OAVp5.drums.undo","key":"OAVp5.drums.undo","name":"OAVp5.drums.undo","aliases":[],"kind":"function","package":"oav-p5","category":"drums","documented":true,"signature":"OAVp5.drums.undo()","source":{"file":"p5/oav-p5.js","line":1463,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1463"},"summary":{"en":"Goes back one line of rhythm, like ↶ on the card; returns `true` if there was an earlier line.","zh":"回到上一行節奏，跟卡片上的 ↶ 一樣，如果有上一行就回傳 `true`。"},"description":[{"en":"The line before the current one is played again and removed from the history. [`rhythmChanged()`](#rhythmChanged) is called with `how` equal to `'undo'`. On the card the same thing is ⌘Z or Ctrl+Z after touching it.","zh":"上一行會再播一次，並從歷史清單裡拿掉。[`rhythmChanged()`](#rhythmChanged) 會被呼叫，`how` 是 `'undo'`。在卡片上，點一下卡片之後按 ⌘Z 或 Ctrl+Z 也是同樣的事。"}],"params":[],"returns":{"type":"Boolean","en":"`true` if it went back one line, `false` if [`OAVp5.drums.history`](#OAVp5.drums.history) was empty or the machine is not loaded.","zh":"回到上一行就是 `true`，[`OAVp5.drums.history`](#OAVp5.drums.history) 是空的、或鼓機還沒載入就是 `false`。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, hh*8', 110);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  OAVp5.drums.edit('bd(3,8), hh*16');\n}\n\nfunction keyPressed() {\n  if (key === 'z') OAVp5.drums.undo();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.drums.src, width / 2, height / 2 - 10);\n  text('click: change the line   z: undo', width / 2, height / 2 + 20);\n}","caption":{"en":"Click to change the line, press z to go back to the one before.","zh":"點一下換一行節奏，按 z 回到前一行。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums.history","OAVp5.drums.edit","rhythmChanged"],"keywords":{"en":["undo","back","revert","previous rhythm","drums"],"zh":["復原","上一步","回上一行","鼓機"]},"tbd":[],"claims":[]},
{"id":"drumEngine","key":"drumEngine","name":"drumEngine","aliases":[],"kind":"function","package":"@openav/drums","category":"drums","documented":true,"signature":"drumEngine({ samples = null } = {})","import":"import { drumEngine } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L38"},"summary":{"en":"A drum kit made of Web Audio synthesis, with no sample files: General MIDI drum notes go in, drum sounds come out.","zh":"一組用 Web Audio 合成的鼓組，沒有取樣檔：送進 General MIDI 的鼓音符，就打出鼓聲。"},"description":[{"en":"It follows the small engine contract of [`@openav/sound`](#Sound), so the same shell and the same signals drive it: `enable()` makes the `AudioContext`, `noteOn(note, vel01, time)` plays a drum, `set(key, value)` sets a param. The kit has thirteen voices (see [`GM`](#GM)): kick, rim, snare, clap, low tom, hat, pedal hat, tom, open hat, high tom, crash, ride and cowbell. A note the kit does not know (any melodic note) plays nothing and returns `null`.","zh":"它遵守 [`@openav/sound`](#Sound) 的小小引擎約定，所以同一個外殼、同樣的訊號都能驅動它：`enable()` 建立 `AudioContext`，`noteOn(note, vel01, time)` 打一下鼓，`set(key, value)` 設定參數。鼓組有十三種鼓聲（見 [`GM`](#GM)）：大鼓、鼓邊、小鼓、拍手、落地鼓、鈸、踩鈸、中鼓、開鈸、高音中鼓、碎音鈸、疊音鈸、牛鈴。鼓組不認得的音符（任何旋律音符）不會發聲，並回傳 `null`。"},{"en":"`noteOn(note, vel01, time)` returns `{ time, cancel() }`, or `null` before `enable()` or for a note it does not play. `time` is on the kit's own clock (`engine.context.currentTime`); left out, or already past, the hit sounds now. `cancel()` silences a hit that has not started, which is how [`DrumTransport`](#DrumTransport) takes back what it scheduled when the tempo changes. `hit('crash', 0.9)` plays a voice by name (or a note number) for code that is not a MIDI pad. There is one param, `kitVolume`, from −30 to 0 dB (default −6).","zh":"`noteOn(note, vel01, time)` 回傳 `{ time, cancel() }`，在 `enable()` 之前，或遇到它不打的音符時回傳 `null`。`time` 是鼓組自己的時鐘（`engine.context.currentTime`）上的時間。沒給，或已經過了，這一下就馬上響。`cancel()` 能讓還沒開始的鼓靜音，[`DrumTransport`](#DrumTransport) 在速度改變時就是用它把排好的收回來。`hit('crash', 0.9)` 用名字（或音符號碼）打一種鼓，給不是 MIDI 墊子的程式用。它有一個參數 `kitVolume`，從 −30 到 0 dB（預設 −6）。"},{"en":"Once enabled, `engine.context` is its `AudioContext` and `engine.output` is the node that goes to the speakers, which a recorder can tap. Before that both are `null`. `noteOff()` does nothing (drums ring out) and `dispose()` closes the context.","zh":"啟用之後，`engine.context` 是它的 `AudioContext`，`engine.output` 是接到喇叭的節點，錄音器可以從這裡接。在那之前兩個都是 `null`。`noteOff()` 什麼都不做（鼓聲自己響完），`dispose()` 會關掉 context。"}],"params":[{"name":"samples","type":"Object","optional":true,"default":"null","en":"Play audio files instead of the synthesis: `{ 36: 'kick.wav', 38: 'snare.wav' }`, MIDI note number to URL. They are fetched and decoded by `enable()`; a file that fails to load is skipped without an error, and a note of the kit then plays its synthesized sound instead.","zh":"用音檔取代合成：`{ 36: 'kick.wav', 38: 'snare.wav' }`，MIDI 音符號碼對應網址。`enable()` 會去下載並解碼。載入失敗的檔案會被略過、不報錯，屬於鼓組的音符就改用合成的聲音打。"}],"returns":{"type":"Object","en":"An engine: `{ params, context, output, enable(), noteOn(note, vel01, time), hit(what, vel01, time), noteOff(), set(key, value), dispose() }`.","zh":"一個引擎：`{ params, context, output, enable(), noteOn(note, vel01, time), hit(what, vel01, time), noteOff(), set(key, value), dispose() }`。"},"needs":{"en":"`await kit.enable()` from a click: browsers start audio only after a user gesture. [`mountDrums()`](#mountDrums) and [`DrumTransport`](#DrumTransport) with `autoEnable` do that for you on the first hit.","zh":"要在點擊裡 `await kit.enable()`：瀏覽器要使用者做過一個動作才讓聲音開始。[`mountDrums()`](#mountDrums) 和 `autoEnable` 的 [`DrumTransport`](#DrumTransport) 會在第一下鼓的時候替你做。"},"notes":[],"examples":[{"code":"import { drumEngine, GM } from '@openav/drums';\n\nconst kit = drumEngine();\nconsole.log(kit.params.map((p) => p.key + ' ' + p.min + '..' + p.max + ' (' + p.def + ')').join());\n// kitVolume -30..0 (-6)\nconsole.log(kit.noteOn(GM.kick, 1), kit.context);   // null null: not enabled yet","caption":{"en":"Before `enable()` the kit has a param but no sound and no `AudioContext`: `noteOn()` returns `null`.","zh":"在 `enable()` 之前，鼓組有參數但沒有聲音也沒有 `AudioContext`：`noteOn()` 回傳 `null`。"},"run":"module","lang":"js"},{"code":"import { drumEngine } from '@openav/drums';\n\nconst kit = drumEngine();\ndocument.body.addEventListener('click', async () => {\n  await kit.enable();                                          // from a click: audio may start\n  kit.hit('kick');\n  kit.hit('snare', 0.9, kit.context.currentTime + 0.5);        // half a second from now\n  kit.hit('crash', 0.8, kit.context.currentTime + 1);\n});","caption":{"en":"Click the page: a kick, then a snare half a second later and a crash after a second. The times are on the kit's own audio clock.","zh":"在頁面上點一下：先是大鼓，半秒後小鼓，一秒後碎音鈸。時間是鼓組自己的音訊時鐘上的時間。"},"run":"static","lang":"js"}],"related":["DrumTransport","mountDrums","GM","voiceForNote","Sound","toneEngine"],"keywords":{"en":["drum kit","drum sounds","synth drums","web audio","engine","noteOn","kick","snare","general midi","samples"],"zh":["鼓組","鼓聲","合成鼓","引擎","大鼓","小鼓","取樣","音量","general midi"]},"tbd":[],"claims":[]},
{"id":"drumName","key":"drumName","name":"drumName","aliases":[],"kind":"function","package":"@openav/drums","category":"drums","documented":true,"signature":"drumName(voice, lang = 'en')","import":"import { drumName } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":809,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L809"},"summary":{"en":"The name of a drum for the screen, in English or in Chinese: `drumName('lowtom')` is `'low tom'`.","zh":"給人讀的鼓的名字，英文或中文：`drumName('lowtom', 'zh')` 是 `'落地鼓'`。"},"description":[{"en":"The drum names inside code stay English everywhere (`'kick'`, `'snare'`), and this is for the screen: the row labels of the drum machine card use it, and with `'zh'` they are written in Chinese.","zh":"程式裡的鼓的名字到處都保持英文（`'kick'`、`'snare'`），這個函式是給畫面顯示用的：鼓機卡的列標籤就是用它，中文的瀏覽器裡是 大鼓 小鼓 鈸 拍手。"}],"params":[{"name":"voice","type":"String","en":"The drum: `kick`, `snare`, `hat`, `clap`, `openhat`, `pedalhat`, `rim`, `lowtom`, `tom`, `hightom`, `crash`, `ride` or `cowbell`.","zh":"哪種鼓：`kick`、`snare`、`hat`、`clap`、`openhat`、`pedalhat`、`rim`、`lowtom`、`tom`、`hightom`、`crash`、`ride`、`cowbell`。"},{"name":"lang","type":"'en' | 'zh'","optional":true,"default":"'en'","en":"`'zh'` for Chinese; anything else is English.","zh":"`'zh'` 是中文。其他都是英文。"}],"returns":{"type":"String","en":"The name, or `voice` itself if it is not a drum of the kit.","zh":"名字。如果 `voice` 不是鼓組的鼓，就原樣回傳 `voice`。"},"notes":[],"examples":[{"code":"import { drumName } from '@openav/drums';\n\nconsole.log(drumName('kick'), drumName('kick', 'zh'));         // kick, then the Chinese name of the kick\nconsole.log(drumName('openhat', 'zh'), drumName('lowtom', 'zh')); // Chinese names of the open hat and the low tom\nconsole.log(drumName('banjo', 'zh'));                           // banjo, not a drum: returned as is","caption":{"en":"Logs a few drums in English and in Chinese, and what a name that is not a drum gives back.","zh":"印出幾種鼓的英文和中文名字，以及不是鼓的名字會得到什麼。"},"run":"module","lang":"js"}],"related":["GM","mountDrums","WORDS"],"keywords":{"en":["drum names","labels","translate","chinese","display name","localization"],"zh":["鼓的名字","標籤","翻譯","中文","顯示名稱","在地化"]},"tbd":[],"claims":[]},
{"id":"DrumScheduler","key":"DrumScheduler","name":"DrumScheduler","aliases":[],"kind":"class","package":"@openav/drums","category":"drums","documented":true,"signature":"new DrumScheduler({ bpm = 112, swing = 0, humanize = 0.35, lookahead = 0.1, pattern = 'four on floor', seed = 0, random = Math.random } = {})","import":"import { DrumScheduler } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":296,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L296"},"summary":{"en":"Pure drum logic with no timers and no audio: a grid or a line of rhythm goes in, timed events come out, ahead of time, on any clock you pass.","zh":"純粹的鼓的邏輯，沒有計時器也沒有聲音：放進一張格子圖或一行節奏，就提前吐出有時間的事件，時鐘由你傳進來。"},"description":[{"en":"A `DrumScheduler` knows what plays in which bar, and when a bar happens, but it owns no clock: every time you give it is in seconds on one clock of your choosing, so it runs the same under a fake clock in a test as under the audio clock in a page. [`DrumTransport`](#DrumTransport) is the runtime that feeds it the real one. Use the scheduler on its own when you want drum events as data, for your own sound engine or a test.","zh":"`DrumScheduler` 知道哪個小節要打什麼，也知道一個小節在什麼時候發生，但它沒有自己的時鐘：你給它的每個時間，都是你選的某一個時鐘上的秒數，所以它在測試裡用假時鐘跑、在頁面裡用音訊時鐘跑，結果一樣。[`DrumTransport`](#DrumTransport) 是把真的時鐘餵給它的執行環境。想把鼓的事件當資料用，例如給自己的發聲引擎或測試，就單獨用排程器。"},{"en":"The loop is: `start(at)` puts the downbeat of bar 1 at time `at`. Then, again and again, `advance(now)` schedules everything up to `now + lookahead` and returns the new events, in order. `due(t)` hands out the events whose time has come (they stay in `queue` until then). Two kinds of event come out: a step `{ kind: 'step', i, n, bar, pos, time }` for every 16th, and a hit `{ kind: 'hit', voice, vel, pos, n, bar, token, word, layer, dur, time }`. `pos` counts bars from the start (0 is the downbeat of bar 1, 1.5 is half way through bar 2), `time` is in your seconds, `i` is the step 0..15 and `n` counts steps since the start. For a grid, the step comes before its hits. `token`, `word`, `layer` and `dur` come from a line of rhythm and are `null` or `undefined` for a grid.","zh":"流程是這樣：`start(at)` 讓第 1 小節的強拍落在時間 `at`。然後一再地呼叫 `advance(now)`，它排好直到 `now + lookahead` 的全部，並依序回傳新的事件。`due(t)` 把時間到了的事件交出來（在那之前它們留在 `queue` 裡）。事件有兩種：每個十六分音符一個步 `{ kind: 'step', i, n, bar, pos, time }`，和一下鼓 `{ kind: 'hit', voice, vel, pos, n, bar, token, word, layer, dur, time }`。`pos` 從開始算起以小節為單位（0 是第 1 小節的強拍，1.5 是第 2 小節的一半），`time` 是你的時鐘的秒數，`i` 是步 0..15，`n` 是從開始算起的步數。格子圖的話，步在它的鼓之前。`token`、`word`、`layer`、`dur` 來自一行節奏，格子圖的話是 `null` 或 `undefined`。"},{"en":"Positions turn into seconds by multiplication from the last tempo anchor, never by adding up frame times, so a thousand bars later nothing has drifted (the test measures under a microsecond). A tempo change adds an anchor, so the past keeps the tempo it had.","zh":"位置是從最近一個速度錨點用乘法換成秒的，不是把畫面時間加起來，所以一千個小節之後也沒有漂移（測試量到的誤差不到一微秒）。速度改變會多加一個錨點，所以過去保持它當時的速度。"}],"params":[{"name":"bpm","type":"Number","optional":true,"default":"112","en":"The tempo, in beats per minute. A value that is not above 0 becomes 112.","zh":"速度，每分鐘幾拍。不大於 0 的值會變成 112。"},{"name":"swing","type":"Number","optional":true,"default":"0","en":"0 to 0.5: every odd 16th is that share of a 16th late. 0 is straight.","zh":"0 到 0.5：每個奇數的十六分音符晚那麼多個十六分音符。0 是直的。"},{"name":"humanize","type":"Number","optional":true,"default":"0.35","en":"0..1: a few milliseconds late and a little softer or louder, so it grooves instead of ticking. Use 0 for exact, repeatable times.","zh":"0..1：晚幾毫秒、力道有一點大小變化，讓它有律動而不是機械的滴答。要精準、可重複的時間就用 0。"},{"name":"lookahead","type":"Number","optional":true,"default":"0.1","en":"Seconds `advance()` schedules beyond `now`.","zh":"`advance()` 在 `now` 之後多排幾秒。"},{"name":"pattern","type":"String | Object","optional":true,"default":"'four on floor'","en":"A preset name from [`PATTERNS`](#PATTERNS), your own grid, or a compiled line of rhythm (see `setPattern()`).","zh":"[`PATTERNS`](#PATTERNS) 裡的預設名稱、你自己的格子圖，或編譯過的一行節奏（見 `setPattern()`）。"},{"name":"seed","type":"Number","optional":true,"default":"0","en":"Passed to the rhythm line as `eventsInBar(bar, { seed })`, so each `?` decides the same way every time.","zh":"當作 `eventsInBar(bar, { seed })` 交給那一行節奏，讓每個 `?` 每次都做同樣的決定。"},{"name":"random","type":"Function","optional":true,"default":"Math.random","en":"Where `humanize` gets its randomness: a function that returns a number from 0 up to 1. Pass your own for repeatable output.","zh":"`humanize` 的隨機從哪裡來：回傳 0 到 1 之間數字的函式。傳自己的就能得到可重複的結果。"}],"notes":[],"examples":[{"code":"import { DrumScheduler } from '@openav/drums';\n\nconst s = new DrumScheduler({ bpm: 120, humanize: 0 });\ns.start(0);                                   // the downbeat of bar 1 is at time 0\ns.advance(1.9);                               // schedule up to 1.9 + 0.1 seconds: one whole bar\nconst heard = s.due(0.5);                     // what is heard by 0.5 s\nconsole.log(heard.filter((e) => e.kind === 'hit').map((e) => e.voice + '@' + e.time).join(' '));\n// kick@0 hat@0 hat@0.25 kick@0.5 snare@0.5 hat@0.5\nconsole.log(s.phaseAt(1).bar, s.posAt(2), s.timeAt(1));   // 0.5 1 2","caption":{"en":"No clock, no audio: the scheduler is asked for one bar of four on the floor at 120 bpm and logs the hits heard in the first half second, with their times in seconds.","zh":"沒有時鐘、沒有聲音：向排程器要 120 bpm 四四拍的一個小節，印出前半秒聽到的鼓，連同以秒為單位的時間。"},"run":"module","lang":"js"},{"code":"import { DrumScheduler } from '@openav/drums';\nimport { compile } from '@openav/pattern';\n\nconst s = new DrumScheduler({ bpm: 120, humanize: 0 });\ns.setPattern(compile('bd [~ sd]*2'), { text: 'bd [~ sd]*2' });\nconsole.log(s.mode, s.pattern);                          // string bd [~ sd]*2\nconsole.log(s.barEvents(1).map((e) => e.voice + '@' + e.pos).join(' '));   // kick@0 snare@0.625 snare@0.875\nconst g = s.gridOf(1);\nconsole.log(g.grid.snare.join(''), g.offGrid);           // 0000000000100010 0\n\ns.start(10);\ns.advance(10);\ns.setBpm(60, 12);                                        // half the tempo from time 12\nconsole.log(s.posAt(12), s.posAt(14));                   // 1 1.5","caption":{"en":"A line of rhythm goes in: its first bar is read as hits, then as a 16-step grid, and a tempo change halves the speed at time 12 without moving what came before.","zh":"放進一行節奏：先把它的第一小節讀成鼓點，再讀成 16 格的格子圖，然後在時間 12 把速度減半，在那之前的不受影響。"},"run":"module","lang":"js"}],"related":["DrumTransport","DrumSequencer","compile","toGrid","PATTERNS","GM"],"keywords":{"en":["scheduler","schedule","lookahead","events","fake clock","tempo map","bar","position","swing","humanize"],"zh":["排程器","預讀","事件","假時鐘","速度","小節","位置","搖擺","擬人化","佇列"]},"tbd":[],"claims":[],"members":[{"name":"bpm","member":"getter","signature":"","doc":"","line":308},{"name":"stepDur","member":"method","signature":"","doc":"","line":310},{"name":"setPattern","member":"method","signature":"p, { text } = {}","doc":"A preset name, your own grid { kick: 'x...x...', … }, or a compiled rhythm (anything with eventsInBar(bar, { seed }), i.e.","line":314},{"name":"toggleCell","member":"method","signature":"lane, i","doc":"","line":333},{"name":"gridOf","member":"method","signature":"bar = 1","doc":"bar `bar` of what plays, on the 16 steps of the four lanes: { grid, offGrid (hits moved to the nearest step), dropped (voices the grid has no lane for) }","line":340},{"name":"takeover","member":"method","signature":"bar = 1","doc":"the rhythm string becomes the grid it plays in `bar` (a click on a cell: the cells take over)","line":353},{"name":"barEvents","member":"method","signature":"bar","doc":"the hits of bar `bar` (1 = the first): [{ voice, pos 0..1, vel?, token?, word?, layer?","line":359},{"name":"posAt","member":"method","signature":"t","doc":"where the music is at time t, in bars from the start (bar 1's downbeat = 0; negative before it)","line":383},{"name":"timeAt","member":"method","signature":"p","doc":"when position p sounds (before swing and humanize)","line":385},{"name":"bpmAt","member":"method","signature":"t","doc":"","line":386},{"name":"phaseAt","member":"method","signature":"t","doc":"{ pos, bar 0..1 through the bar, beat 0..1 through the beat } at time t (0 before the first downbeat)","line":388},{"name":"start","member":"method","signature":"at","doc":"","line":390},{"name":"stop","member":"method","signature":"","doc":"stop: what was scheduled and not yet heard (to take back from the kit)","line":392},{"name":"setBpm","member":"method","signature":"bpm, at","doc":"the tempo from time `at` on (left out: from the end of what is already scheduled, so nothing is taken back) → dropped","line":394},{"name":"rewind","member":"method","signature":"at","doc":"take back everything scheduled at or after time `at` (the next advance makes it again, from the pattern and the tempo as they are then) → the events taken back.","line":409},{"name":"advance","member":"method","signature":"now","doc":"schedule up to now + lookahead → the new events, in order (they are in `queue` too, until due() hands them out)","line":419},{"name":"due","member":"method","signature":"t","doc":"the events heard by time t, in order (removed from the queue)","line":452}]},
{"id":"DrumSequencer","key":"DrumSequencer","name":"DrumSequencer","aliases":[],"kind":"class","package":"@openav/drums","category":"drums","documented":true,"signature":"new DrumSequencer({ onHit, onStep = null, bpm = 112, swing = 0.12, humanize = 0.35, pattern = 'four on floor' } = {})","import":"import { DrumSequencer } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":210,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L210"},"summary":{"en":"A 16-step, four-lane drum pattern clock that you move forward with `update(dt)` every frame and that calls your function for each hit.","zh":"一個 16 步、四條軌道的鼓節奏時鐘，每個畫面用 `update(dt)` 往前推，每一下鼓就呼叫你的函式。"},"description":[{"en":"It is the simplest clock: no audio, no timers, no DOM. Make it with an `onHit` that does something, call `toggle(true)`, then call `update(dt)` once per frame with the seconds since the last frame. It walks through the 16 steps of a bar (a 16th lasts `60 / bpm / 4` seconds) and calls `onHit(lane, velocity)` for each hit in the four lanes `kick`, `snare`, `hat` and `clap`. A hit is told in the frame in which it is due, so it can be late by up to a frame, and nothing happens in a hidden tab. For a steady beat under a heavy frame, use [`DrumTransport`](#DrumTransport).","zh":"這是最簡單的時鐘：沒有聲音、沒有計時器、沒有 DOM。做一個會做事的 `onHit`，呼叫 `toggle(true)`，然後每個畫面用距離上一個畫面過了幾秒呼叫 `update(dt)`。它會走過一個小節的 16 步（一個十六分音符是 `60 / bpm / 4` 秒），每一下 `kick`、`snare`、`hat`、`clap` 四條軌道上的鼓就呼叫 `onHit(lane, velocity)`。鼓是在到時間的那個畫面才被告知，所以最多會晚一個畫面，分頁被藏起來的時候什麼都不會發生。要在畫面很重的時候也有穩的節拍，用 [`DrumTransport`](#DrumTransport)。"},{"en":"The grid is `seq.grid`: `{ kick: [0|1 × 16], snare, hat, clap }`. `seq.pattern` is the preset name, or `'custom'` once you changed a cell or gave your own grid. `seq.playing`, `seq.bpm`, `seq.swing` and `seq.humanize` are plain properties you can read and set.","zh":"格子圖是 `seq.grid`：`{ kick: [0|1 × 16], snare, hat, clap }`。`seq.pattern` 是預設的名稱，改過格子或給了自己的格子圖之後就是 `'custom'`。`seq.playing`、`seq.bpm`、`seq.swing`、`seq.humanize` 都是可以直接讀和設的普通屬性。"}],"params":[{"name":"onHit","type":"Function","optional":false,"en":"`onHit(lane, velocity)` for every hit: `lane` is `'kick'`, `'snare'`, `'hat'` or `'clap'`, `velocity` is 0.15 to 1. You decide what a hit does: play a sound, draw, publish a signal.","zh":"每一下鼓呼叫 `onHit(lane, velocity)`：`lane` 是 `'kick'`、`'snare'`、`'hat'`、`'clap'`，`velocity` 是 0.15 到 1。一下鼓要做什麼由你決定：發聲、畫圖、發布訊號。"},{"name":"onStep","type":"Function","optional":true,"default":"null","en":"`onStep(i, n)` for every 16th step: `i` is 0..15, `n` counts steps since the start. It comes before that step's hits. A timeline counts bars on it.","zh":"每個十六分音符步呼叫 `onStep(i, n)`：`i` 是 0..15，`n` 是從開始算起的步數。它在那一步的鼓之前。時間軸靠它數小節。"},{"name":"bpm","type":"Number","optional":true,"default":"112","en":"The tempo, in beats per minute. It can be changed later: `seq.bpm = 90`.","zh":"速度，每分鐘幾拍。之後可以改：`seq.bpm = 90`。"},{"name":"swing","type":"Number","optional":true,"default":"0.12","en":"0 to 0.5: every odd 16th is that share of a 16th late. 0 is straight.","zh":"0 到 0.5：每個奇數的十六分音符晚那麼多個十六分音符。0 是直的。"},{"name":"humanize","type":"Number","optional":true,"default":"0.35","en":"0..1: some hits a few milliseconds late and a little softer or louder. Use 0 for exact, repeatable velocities (0.9 on the beat, 0.6 between).","zh":"0..1：有些鼓晚幾毫秒、力道有一點大小。要精準、可重複的力道就用 0（拍點上 0.9，其他 0.6）。"},{"name":"pattern","type":"String | Object","optional":true,"default":"'four on floor'","en":"A preset name from [`PATTERNS`](#PATTERNS) or your own grid.","zh":"[`PATTERNS`](#PATTERNS) 裡的預設名稱，或你自己的格子圖。"}],"notes":[],"examples":[{"code":"import { DrumSequencer } from '@openav/drums';\n\nconst hits = [];\nconst seq = new DrumSequencer({\n  onHit: (lane, vel) => hits.push(lane + ' ' + vel.toFixed(1)),\n  bpm: 120, swing: 0, humanize: 0,\n});\nseq.toggle(true);\nfor (let i = 0; i < 32; i++) seq.update(1 / 60);   // about half a second, in 60 fps frames\nconsole.log(hits.join(', '));\n// kick 0.9, hat 0.9, hat 0.6, kick 0.9, snare 0.9, hat 0.9\nconsole.log(seq.pattern, seq.grid.kick.join(''));   // four on floor 1000100010001000","caption":{"en":"Runs half a second of four on the floor with no sound and no screen, and logs the hits: a kick and a hat on the downbeat, the off-beat hat, then kick, snare and hat on beat 2.","zh":"不用聲音也不用畫面，跑半秒的四四拍，印出鼓點：強拍上的大鼓和鈸、反拍的鈸，然後第二拍的大鼓、小鼓和鈸。"},"run":"module","lang":"js"},{"code":"import { DrumSequencer } from '@openav/drums';\n\nconst g = document.getElementById('c').getContext('2d');\nlet lit = 0;\nconst seq = new DrumSequencer({\n  bpm: 110, pattern: { kick: 'x...x...x...x...', snare: '....x.......x...', hat: 'x.x.x.x.x.x.x.x.' },\n  onStep: (i) => { lit = i; },          // onHit would be the place to play a sound\n});\nseq.toggle(true);\nlet last = performance.now();\n(function frame(now) {\n  seq.update((now - last) / 1000); last = now;\n  g.fillStyle = '#111'; g.fillRect(0, 0, 400, 300);\n  for (let i = 0; i < 16; i++) { g.fillStyle = i === lit ? '#ffd166' : '#2a3348'; g.fillRect(12 + i * 23, 120, 18, 60); }\n  requestAnimationFrame(frame);\n})(last);","caption":{"en":"Sixteen blocks on the canvas, and a bright one runs along them in time with the beat. There is no sound: `onHit` is where you would play one.","zh":"畫布上有十六個方塊，亮的那一個跟著節拍跑。這裡沒有聲音：`onHit` 就是你放發聲程式的地方。"},"run":"module","lang":"js"}],"related":["DrumTransport","DrumScheduler","PATTERNS","laneSteps","mountDrums","GM"],"keywords":{"en":["sequencer","step sequencer","16 steps","pattern clock","tempo","swing","humanize","lanes","kick","snare"],"zh":["音序器","步進音序器","16 步","節奏時鐘","速度","搖擺","擬人化","軌道","大鼓","小鼓"]},"tbd":[],"claims":[],"members":[{"name":"setPattern","member":"method","signature":"p","doc":"A preset name ('breakbeat'), or your own grid { kick: 'x...x...x...x...', snare: …, hat: …, clap: … } (= 'custom'; a lane left out is silent).","line":222},{"name":"toggleCell","member":"method","signature":"lane, i","doc":"","line":232},{"name":"toggle","member":"method","signature":"on = !this.playing","doc":"","line":233},{"name":"stepDur","member":"method","signature":"","doc":"","line":239},{"name":"update","member":"method","signature":"dt","doc":"","line":240}]},
{"id":"DrumTransport","key":"DrumTransport","name":"DrumTransport","aliases":[],"kind":"class","package":"@openav/drums","category":"drums","documented":true,"signature":"new DrumTransport({ scheduler = new DrumScheduler(), engine = null, onStep = null, onHit = null, onLate = null, autoEnable = false, now = perfNow, hidden = pageHidden, heartbeat = 'auto', tick = 0.025, latency = 'auto', stale = 0.25, startDelay = 0.05 } = {})","import":"import { DrumTransport } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":473,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L473"},"summary":{"en":"The runtime around a DrumScheduler: it hands every hit to the kit ahead of time on its audio clock, and tells the page each step and hit when it is heard.","zh":"包在 DrumScheduler 外面的執行環境：它用鼓組的音訊時鐘，把每一下鼓提前交給鼓組，並在聽到的時候才告訴頁面每一步和每一下。"},"description":[{"en":"A page that plays a hit in the frame it is due is late by up to a frame, and silent when its tab is hidden. `DrumTransport` fixes both. A timer (the heartbeat, from a Web Worker, 25 ms by default) looks 0.1 seconds ahead on the kit's own audio clock and hands the kit every hit with its exact start time. The page is told separately, in the frame in which each step and hit is **heard**: its time plus the output latency the browser reports. So the beat stays steady under a heavy `draw()` and in a hidden tab, and the callbacks still arrive in order: a step before its hits, hits in time order.","zh":"在到時間的那個畫面才打的鼓，最多會晚一個畫面，分頁被藏起來的時候還會停。`DrumTransport` 兩個都解決。一個計時器（心跳，預設用 Web Worker，每 25 毫秒一次）在鼓組自己的音訊時鐘上往前看 0.1 秒，把每一下鼓連同確切的開始時間交給鼓組。頁面則是另外被通知，在每一步、每一下**被聽到**的那個畫面：它的時間加上瀏覽器回報的輸出延遲。所以畫面很重或分頁被藏起來，節拍還是穩的，回呼也依然按順序到：一步在它的鼓之前，鼓依時間排序。"},{"en":"Call `start()`, then `frame()` once per animation frame. The heartbeat calls `pump()` for you. Tempo and pattern changes (`setBpm()`, `setPattern()`, `toggleCell()`) take back what was already handed over after now and make it again, so no hit is doubled or lost. Before the kit's `AudioContext` runs (no click yet) the clock runs on `now()` and the callbacks run but nothing sounds; once it runs, the hits from then on sound, and none from before.","zh":"呼叫 `start()`，然後每個動畫畫面呼叫一次 `frame()`。心跳會替你呼叫 `pump()`。速度和節奏的改變（`setBpm()`、`setPattern()`、`toggleCell()`）會把現在之後已經交出去的收回來重做，所以不會有鼓重複或漏掉。鼓組的 `AudioContext` 還沒跑（還沒點擊）的時候，時鐘用 `now()` 跑，回呼照常執行但沒有聲音。開始跑之後，從那之後的鼓才會響，之前的不補。"},{"en":"A hidden page, or a frame loop that catches up more than `stale` seconds late, tells its steps quietly: `ev.quiet` is true, the clock moves and cues fire, but `onHit` is not called, and of several bar starts at once only the last has `ev.announce`. Nothing floods the page when the tab comes back. If a `draw()` ever holds the thread longer than the lookahead, the lookahead grows to 0.3 seconds (`info().extended`).","zh":"頁面被藏起來，或畫面迴圈追趕時已經晚了超過 `stale` 秒的時候，步會被安靜地告知：`ev.quiet` 是 true，時鐘照走、cue 照發，但 `onHit` 不會被呼叫，而且同時到的好幾個小節開頭，只有最後一個有 `ev.announce`。分頁回來的時候不會一下子湧進一大堆。如果某個 `draw()` 佔住執行緒的時間比預讀還長，預讀會增加到 0.3 秒（`info().extended`）。"},{"en":"It has the getters `playing`, `pattern`, `grid`, `mode`, `text`, `bpm`, `swing`, `humanize` and `lookahead` of its scheduler; `bpm`, `swing` and `humanize` can also be set. You usually get one through [`mountDrums()`](#mountDrums) with `clock: 'lookahead'`; build your own when you want the clock without the card.","zh":"它有它的排程器的 getter：`playing`、`pattern`、`grid`、`mode`、`text`、`bpm`、`swing`、`humanize`、`lookahead`。`bpm`、`swing`、`humanize` 也可以設定。通常是透過 `clock: 'lookahead'` 的 [`mountDrums()`](#mountDrums) 拿到一個。想要時鐘而不要卡片的時候再自己建。"}],"params":[{"name":"scheduler","type":"DrumScheduler","optional":true,"default":"new DrumScheduler()","en":"The [`DrumScheduler`](#DrumScheduler) that decides what is played when.","zh":"決定什麼時候打什麼的 [`DrumScheduler`](#DrumScheduler)。"},{"name":"engine","type":"Object","optional":true,"default":"null","en":"The sound engine, usually a [`drumEngine()`](#drumEngine). If it has a `context` property (an `AudioContext`, or `null` before it is enabled), hits are scheduled ahead on that clock. If not, each hit is played by calling `engine.noteOn(note, vel)` at the moment it is heard.","zh":"發聲的引擎，通常是 [`drumEngine()`](#drumEngine)。它有 `context` 這個屬性（一個 `AudioContext`，啟用之前是 `null`）的話，鼓點就用那個時鐘提前排程。沒有的話，每一下鼓就在聽到的那一刻呼叫 `engine.noteOn(note, vel)` 來打。"},{"name":"onStep","type":"Function","optional":true,"default":"null","en":"`onStep(i, n, ev)` for every 16th step when it is heard. `ev.quiet` and `ev.announce` say whether it was told quietly (below).","zh":"`onStep(i, n, ev)`，每個十六分音符步在聽到的時候呼叫。`ev.quiet` 和 `ev.announce` 說明它是不是被安靜地告知（見下面）。"},{"name":"onHit","type":"Function","optional":true,"default":"null","en":"`onHit(voice, vel, ev)` for every drum hit when it is heard.","zh":"`onHit(voice, vel, ev)`，每一下鼓在聽到的時候呼叫。"},{"name":"onLate","type":"Function","optional":true,"default":"null","en":"`onLate(margin, transport)`: a hit was handed to the kit after its time, because the page held the main thread longer than the lookahead. `margin` is how many seconds early it was (negative or tiny).","zh":"`onLate(margin, transport)`：因為頁面佔住主執行緒的時間比預讀還長，有一下鼓交給鼓組的時候已經過了它的時間。`margin` 是它早了幾秒（負的或很小）。"},{"name":"autoEnable","type":"Boolean","optional":true,"default":"false","en":"Call `engine.enable()` by `start()` when the engine has no `AudioContext` yet.","zh":"引擎還沒有 `AudioContext` 的話，在 `start()` 時呼叫 `engine.enable()`。"},{"name":"now","type":"Function","optional":true,"default":"performance.now() / 1000","en":"A function that returns the time in seconds. Pass your own to drive the transport from a fake clock in a test.","zh":"回傳目前時間（秒）的函式。測試時傳自己的，就能用假的時鐘帶動傳輸控制。"},{"name":"hidden","type":"Function","optional":true,"default":"checks document.visibilityState","en":"A function that returns true while nobody can see the page.","zh":"沒有人看得到頁面時回傳 true 的函式。"},{"name":"heartbeat","type":"'auto' | 'interval' | false","optional":true,"default":"'auto'","en":"Who calls `pump()`: `'auto'` a Web Worker timer (a hidden tab does not slow a worker), falling back to `setInterval`; `'interval'` always `setInterval`; `false` (or `'none'`) nobody, and you call `pump()` or `frame()` yourself.","zh":"誰來呼叫 `pump()`：`'auto'` 是 Web Worker 的計時器（被藏起來的分頁不會讓 worker 變慢），不行就退回 `setInterval`。`'interval'` 一律用 `setInterval`。`false`（或 `'none'`）是沒人叫，你自己呼叫 `pump()` 或 `frame()`。"},{"name":"tick","type":"Number","optional":true,"default":"0.025","en":"Seconds between two heartbeats.","zh":"兩次心跳之間隔幾秒。"},{"name":"latency","type":"'auto' | Number","optional":true,"default":"'auto'","en":"Seconds between a hit being scheduled and being heard. `'auto'` asks the browser (`getOutputTimestamp()`, `outputLatency`); a number sets it by hand.","zh":"一下鼓從排程到聽到之間隔幾秒。`'auto'` 向瀏覽器問（`getOutputTimestamp()`、`outputLatency`）。給數字就是手動設定。"},{"name":"stale","type":"Number","optional":true,"default":"0.25","en":"A step that is told more than this many seconds late (a frame loop catching up) is told quietly.","zh":"一步如果被告知的時候已經晚了超過這麼多秒（畫面迴圈在追趕），就會被安靜地告知。"},{"name":"startDelay","type":"Number","optional":true,"default":"0.05","en":"When the audio clock is running, the first downbeat is put this many seconds after `start()`, so the first hit is not already late.","zh":"音訊時鐘在跑的時候，第一個強拍會放在 `start()` 之後這麼多秒，這樣第一下才不會已經晚了。"}],"notes":[],"examples":[{"code":"import { DrumScheduler, DrumTransport } from '@openav/drums';\n\nlet T = 100;                                         // a clock we move by hand, in seconds\nconst log = [];\nconst transport = new DrumTransport({\n  scheduler: new DrumScheduler({ bpm: 120, humanize: 0 }),\n  heartbeat: false, now: () => T,                    // no timer, our own clock\n  onStep: (i) => { if (i % 4 === 0) log.push('step ' + i); },\n  onHit: (voice, vel) => { if (voice !== 'hat') log.push(voice); },\n});\ntransport.start();\nfor (let k = 0; k < 40; k++) { T += 1 / 20; transport.frame(); }   // two seconds in 20 fps frames\nconsole.log(log.join(', '));\n// step 0, kick, step 4, kick, snare, step 8, kick, step 12, kick, snare\nconsole.log(transport.playing, transport.phase().bar.toFixed(2));  // true 1.00","caption":{"en":"Runs a whole bar of four on the floor on a clock we move by hand, and logs what the page is told: the downbeats of each beat, the kicks and the snares on 2 and 4.","zh":"在我們手動撥動的時鐘上跑完一整個小節的四四拍，印出頁面被告知的事：每一拍的強拍、大鼓，和第 2、4 拍的小鼓。"},"run":"module","lang":"js"},{"code":"import { DrumScheduler, DrumTransport, drumEngine } from '@openav/drums';\n\nconst kit = drumEngine();\nconst transport = new DrumTransport({\n  scheduler: new DrumScheduler({ bpm: 100, swing: 0.1, pattern: 'breakbeat' }),\n  engine: kit, autoEnable: true,                      // the click that starts it also starts the audio\n  onHit: (voice, vel) => console.log(voice, vel.toFixed(2)),\n});\ndocument.body.addEventListener('click', () => transport.start(), { once: true });\n(function frame() { transport.frame(); requestAnimationFrame(frame); })();","caption":{"en":"Click the page and a breakbeat starts on the kit's audio clock; the console logs each hit when it is heard. Switch to another tab: the beat keeps going.","zh":"在頁面上點一下，breakbeat 就用鼓組的音訊時鐘開始打。console 在聽到每一下的時候印出來。切到別的分頁：節拍照樣繼續。"},"run":"static","lang":"js"}],"related":["DrumScheduler","mountDrums","drumEngine","DrumSequencer","startDrums","drumPhase"],"keywords":{"en":["transport","lookahead","audio clock","heartbeat","scheduling","steady tempo","web worker","hidden tab","latency","follow"],"zh":["傳輸控制","預讀","音訊時鐘","心跳","排程","穩定速度","分頁被藏起來","延遲","跟隨","外部時鐘"]},"tbd":[],"claims":[],"members":[{"name":"playing","member":"getter","signature":"","doc":"","line":484},{"name":"pattern","member":"getter","signature":"","doc":"","line":485},{"name":"grid","member":"getter","signature":"","doc":"","line":486},{"name":"mode","member":"getter","signature":"","doc":"","line":487},{"name":"text","member":"getter","signature":"","doc":"","line":488},{"name":"bpm","member":"getter","signature":"","doc":"","line":489},{"name":"swing","member":"getter","signature":"","doc":"","line":491},{"name":"humanize","member":"getter","signature":"","doc":"","line":493},{"name":"lookahead","member":"getter","signature":"","doc":"","line":495},{"name":"stepDur","member":"method","signature":"","doc":"","line":496},{"name":"times","member":"method","signature":"","doc":"{ sched, heard, live }: `sched` is the earliest time a hit can still go to the kit, `heard` what the listener hears right now.","line":501},{"name":"start","member":"method","signature":"","doc":"","line":538},{"name":"stop","member":"method","signature":"","doc":"","line":550},{"name":"toggle","member":"method","signature":"on = !this.playing","doc":"on: start from bar 1 (again, if it was playing), as the grid's checkbox does; off: stop","line":557},{"name":"setPattern","member":"method","signature":"p, o","doc":"","line":558},{"name":"toggleCell","member":"method","signature":"lane, i","doc":"","line":559},{"name":"takeover","member":"method","signature":"bar","doc":"","line":560},{"name":"setBpm","member":"method","signature":"v","doc":"a new tempo from now on: what was scheduled after now is taken back and made again, so no hit doubles or goes missing","line":562},{"name":"rewind","member":"method","signature":"","doc":"a change is heard at once: what was scheduled after now (10 ms of margin: never a hit already sounding) is made again","line":572},{"name":"pump","member":"method","signature":"","doc":"the heartbeat: schedule ahead; and while no frame runs (a hidden tab, a sketch stuck for a moment) tell the page too","line":579},{"name":"frame","member":"method","signature":"","doc":"once a frame (and right before p5 draws): schedule, then tell the page every step and hit heard by now → those events","line":586},{"name":"phase","member":"method","signature":"","doc":"where the listener is: { bar 0..1 through the bar, beat 0..1 through the beat, pos bars since the start }; it holds still while the machine is stopped","line":595},{"name":"info","member":"method","signature":"","doc":"","line":599},{"name":"dispose","member":"method","signature":"","doc":"","line":603},{"name":"heardAt","member":"method","signature":"at","doc":"the scheduler's time line (what is heard) at a moment of the `now` clock (performance.now() / 1000 in a page)","line":610},{"name":"follow","member":"method","signature":"{ pos = 0, at, bpm } = {}, { catchUp = 0.5, jump = 0.125, pull = 0.08, still = 0.0005 } = {}","doc":"play on an external clock: position `pos` (bars from its start; 0 = the downbeat of bar 1) is heard at `at` (the `now` clock: performance.now() / 1000, the MIDI timestamp / 1000), the tempo is `bpm`.","line":630},{"name":"followInfo","member":"method","signature":"","doc":"{ bpm, err (s, the last correction), jumps, steers, at } while following; null when it never did","line":654},{"name":"unfollow","member":"method","signature":"","doc":"stop following (the machine keeps the tempo it has, on its own clock)","line":656}]},
{"id":"GM","key":"GM","name":"GM","aliases":[],"kind":"constant","package":"@openav/drums","category":"drums","documented":true,"signature":"GM = object, 13 keys","import":"import { GM } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":23,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L23"},"summary":{"en":"The General MIDI drum note each drum of the kit plays: `GM.kick` is 36, `GM.snare` is 38.","zh":"鼓組裡每種鼓對應的 General MIDI 鼓音符：`GM.kick` 是 36，`GM.snare` 是 38。"},"description":[{"en":"Drum pads and drum machines send these notes on MIDI channel 10. The drum machine sends them as `midi/note/on` `{ note, vel, ch: 10 }` for every hit, so anything that listens to MIDI hears the drums. The reverse, from a note to a drum, is [`voiceForNote()`](#voiceForNote), which also knows the neighbouring General MIDI notes (35 is also a kick, 40 is also a snare).","zh":"打擊墊和鼓機在 MIDI 頻道 10 送這些音符。鼓機每打一下就送一個 `midi/note/on` `{ note, vel, ch: 10 }`，所以任何聽 MIDI 的東西都聽得到鼓。從音符反查是哪種鼓用 [`voiceForNote()`](#voiceForNote)，它也認得 General MIDI 相鄰的音符（35 也是大鼓，40 也是小鼓）。"}],"value":{"type":"Object","en":"`{ kick: 36, rim: 37, snare: 38, clap: 39, lowtom: 41, hat: 42, pedalhat: 44, tom: 45, openhat: 46, hightom: 48, crash: 49, ride: 51, cowbell: 56 }`: drum name to MIDI note number.","zh":"`{ kick: 36, rim: 37, snare: 38, clap: 39, lowtom: 41, hat: 42, pedalhat: 44, tom: 45, openhat: 46, hightom: 48, crash: 49, ride: 51, cowbell: 56 }`：鼓的名字對應 MIDI 音符號碼。"},"params":[],"notes":[],"examples":[{"code":"import { GM, voiceForNote } from '@openav/drums';\n\nconsole.log(GM.kick, GM.snare, GM.hat);        // 36 38 42\nconsole.log(Object.keys(GM).length);            // 13\nconsole.log(voiceForNote(GM.crash));            // crash","caption":{"en":"Logs the notes of three drums, how many drums the kit has, and that the note of a drum maps back to the drum.","zh":"印出三種鼓的音符、鼓組有幾種鼓，以及鼓的音符可以反查回那種鼓。"},"run":"module","lang":"js"}],"related":["voiceForNote","drumEngine","WORDS","drumHit"],"keywords":{"en":["general midi","gm","drum notes","channel 10","midi drum map","note numbers","kick 36"],"zh":["general midi","gm","鼓音符","頻道 10","音符號碼","鼓對照表","大鼓 36"]},"tbd":[],"claims":[]},
{"id":"laneSteps","key":"laneSteps","name":"laneSteps","aliases":[],"kind":"function","package":"@openav/drums","category":"drums","documented":true,"signature":"laneSteps(v)","import":"import { laneSteps } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":199,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L199"},"summary":{"en":"Turns a lane written as text, like 'x...x...x...x...', or as a list into 16 steps of 0 and 1.","zh":"把寫成文字的軌道，像 'x...x...x...x...'，或寫成陣列的軌道，變成 16 格 0 和 1。"},"description":[{"en":"It is how every grid you write is read: `{ kick: 'x...x...x...x...' }` in [`DrumSequencer`](#DrumSequencer), [`DrumScheduler`](#DrumScheduler) and [`startDrums()`](#startDrums). `'x... x... x... x...'` and `'1000100010001000'` are the same lane. Note that the dot is a rest here, and only here: in a line of rhythm (see [`compile()`](#compile)) a rest is `~`.","zh":"你寫的每一張格子圖都是這樣被讀的：[`DrumSequencer`](#DrumSequencer)、[`DrumScheduler`](#DrumScheduler) 和 [`startDrums()`](#startDrums) 裡的 `{ kick: 'x...x...x...x...' }`。`'x... x... x... x...'` 和 `'1000100010001000'` 是同一條軌道。注意點在這裡是休止，只有在這裡：一行節奏（見 [`compile()`](#compile)）裡的休止是 `~`。"}],"params":[{"name":"v","type":"String | Array","en":"The lane. In text, `x`, `X` and `1` are hits and everything else is a rest; spaces and `|` are ignored, so they can help the eye. In a list, `1` and `true` are hits.","zh":"軌道。文字的話，`x`、`X`、`1` 是鼓點，其他都是休止。空白和 `|` 會被忽略，可以用來幫助眼睛。陣列的話，`1` 和 `true` 是鼓點。"}],"returns":{"type":"Number[]","en":"An array of 16 numbers, 1 for a hit and 0 for a rest. A short lane is padded with rests, a long one is cut at 16.","zh":"一個 16 個數字的陣列，1 是鼓點、0 是休止。太短的軌道用休止補滿，太長的在第 16 格截掉。"},"notes":[],"examples":[{"code":"import { laneSteps } from '@openav/drums';\n\nconsole.log(laneSteps('x... x... x... x...').join(''));   // 1000100010001000\nconsole.log(laneSteps([1, 0, true]).join(''));            // 1010000000000000\nconsole.log(laneSteps('x').length, laneSteps('x'.repeat(40)).length);   // 16 16","caption":{"en":"Logs a lane written with spaces, a lane written as a list, and that any length comes out as 16 steps.","zh":"印出用空白寫的軌道、用陣列寫的軌道，以及不管多長出來都是 16 格。"},"run":"module","lang":"js"}],"related":["PATTERNS","DrumSequencer","DrumScheduler","fromGrid"],"keywords":{"en":["lane","steps","grid","x...","pattern string","parse lane","16 steps"],"zh":["軌道","步進","格子","格子字串","解析軌道","16 格"]},"tbd":[],"claims":[]},
{"id":"mountDrums","key":"mountDrums","name":"mountDrums","aliases":[],"kind":"function","package":"@openav/drums","category":"drums","documented":true,"signature":"mountDrums(container, { signals, engine = null, autoEnableEngine = true, onStep = null, onHit = null, onTakeover = null, bpm = 112, pattern = 'four on floor', clock = 'frame', lang = 'en', heartbeat = 'auto', latency = 'auto', now, hidden, rhythm = null, onRhythm = null, onLossy = null, onEditError = null, copyText = null, strudelComment = '' } = {})","import":"import { mountDrums } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":830,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L830"},"summary":{"en":"Puts a drum machine on the page: a step grid, a transport and a line of rhythm you can type into. It publishes every hit as `drum/*` signals.","zh":"在頁面上放一台鼓機：步進格子、播放控制，和一行可以直接打字修改的節奏。它把每一下鼓發布成 `drum/*` 訊號。"},"description":[{"en":"`mountDrums(el, { signals })` builds a drum machine card inside `el`: a play checkbox, a preset menu, a tempo slider, the bar it is in, and a grid with a lane per drum (kick, snare, hat, clap, and a row of their own for the crash, ride, toms and open hat that a line of rhythm uses). A click on a cell toggles it. The returned object lets you do from code what the card does with the mouse: `play(true)`, `setPattern('breakbeat')`, `setBpm(96)`.","zh":"`mountDrums(el, { signals })` 在 `el` 裡蓋一張鼓機卡：播放勾選框、預設選單、速度滑桿、現在在第幾小節，和一個每種鼓一條軌道的格子（大鼓、小鼓、鈸、拍手，一行節奏用到的碎音鈸、疊音鈸、中鼓、開鈸各有自己的一列）。點一個格子就切換它。回傳的物件讓你用程式做卡片上用滑鼠做的事：`play(true)`、`setPattern('breakbeat')`、`setBpm(96)`。"},{"en":"**You must call `drums.update(dt)` every frame**, with the seconds since the last one. It moves the machine (with the frame clock), tells your `onStep` and `onHit` what is heard, paints the grid and lets the `drum/*/env` signals decay. A machine that is never updated does not move.","zh":"**每個畫面都要呼叫 `drums.update(dt)`**，參數是距離上一個畫面過了幾秒。它讓鼓機前進（用 frame 時鐘的時候）、把聽到的事告訴你的 `onStep` 和 `onHit`、畫格子，並讓 `drum/*/env` 訊號衰減。從來不更新的鼓機不會動。"},{"en":"Hits travel the same road as a drummer heard through the microphone, so a sketch written for the microphone moves over unchanged: `drum/kick`, `drum/snare`, `drum/hat`, `drum/clap` as pulses with `{ level }`, `drum/<voice>/env` as continuous 0..1 values, and `midi/note/on` `{ note, vel, ch: 10 }` with the General MIDI drum note (see [`GM`](#GM)). A voice a line of rhythm adds, like `drum/crash`, is defined the first time it is hit.","zh":"鼓點走的路跟麥克風聽到的鼓手一樣，所以為麥克風寫的草圖不用改就能換成它：`drum/kick`、`drum/snare`、`drum/hat`、`drum/clap` 是帶 `{ level }` 的脈衝，`drum/<voice>/env` 是 0..1 的連續值，還有 `midi/note/on` `{ note, vel, ch: 10 }`，音符是 General MIDI 的鼓音符（見 [`GM`](#GM)）。一行節奏多出來的鼓，像 `drum/crash`，會在第一次被打到的時候才定義。"},{"en":"Give it the pattern module (`rhythm`) and the line of rhythm is always shown over the grid: a preset shows the line its grid is (`'four on floor'` is `bd*4, [~ sd]*2, hh*8`), and the word that sounds lights up. Then the two edit each other. A click on a cell rewrites the line (with [`fromGrid()`](#fromGrid)). Typing in the line changes the cells: Enter, or 400 ms without a key, plays what was typed. A mistake is marked red, `onEditError` is told, and the last line that worked keeps playing. Escape drops what was not played yet. Every change is kept in `history`; **↶** on the card, or `Cmd/Ctrl+Z` after touching the card, goes back one. **Copy** puts the line on the clipboard and **Open in Strudel** opens it at strudel.cc in a new tab ([`strudelUrl()`](#strudelUrl)).","zh":"把鼓的模組（`rhythm`）交給它，格子上面就一直有那一行節奏：預設會顯示它的格子圖是哪一行（`'four on floor'` 就是 `bd*4, [~ sd]*2, hh*8`），正在響的字會亮起來。接著兩邊互相修改。點一個格子會改寫那一行（用 [`fromGrid()`](#fromGrid)）。在那一行打字會改變格子：按 Enter，或停手 400 毫秒，就播放打進去的內容。有錯的地方標紅、通知 `onEditError`，上一行能用的繼續打。按 Escape 丟掉還沒播的。每次改動都存在 `history`。卡上的 **↶**，或碰過卡片之後按 `Cmd/Ctrl+Z`，退一步。**Copy** 把那一行放進剪貼簿，**Open in Strudel** 在新分頁的 strudel.cc 打開它（[`strudelUrl()`](#strudelUrl)）。"},{"en":"What the returned object offers:","zh":"回傳的物件提供這些："},{"en":"```text\nplay(on)               start (true) or stop (false) like the checkbox; no argument flips; returns whether it plays\nsetPattern(p, {text})  a preset name, your own grid, or a compiled line (text: the line it came from); the card follows\nsetBpm(bpm)            the tempo, like the slider; returns the tempo\nupdate(dt)             call every frame\ndeliver()              tell onStep/onHit what is heard by now without painting (call it right before you draw)\nphase()                { bar, beat, pos }: how far through the bar and the beat the listener is, 0..1\nnow()                  { bar, step, phase, beatPhase, tokens, mode, text } for the display\ninfo()                 { clock, heartbeat, lookahead, late, extended, audio, latency, ... } the health of the clock\nuseRhythm(mod)         hand over @openav/pattern later (it can load on its own)\nsrc()                  the line that plays now ('' with a grid and no rhythm module)\nstrudelUrl()           the line as a strudel.cc link, or ''\nedit(text)             play a line as if typed: { ok, error }; a mistake keeps the last line playing\nhistory, undo()        the lines before, oldest first; undo goes back one and returns whether it did\ncopy()                 put copyText() on the clipboard: Promise<{ ok, text }>\nbarEvents(bar)         the hits of a bar: [{ voice, pos, ... }]\nvoices()               the rows on the grid now\nseq, engine, root      the DrumSequencer or DrumTransport, the sound engine, the card's element\ndispose()              stop, release the kit and take the card out\n```","zh":"```text\nplay(on)               像勾選框那樣開始（true）或停止（false）。不給參數就是反過來。回傳現在有沒有在打\nsetPattern(p, {text})  預設名稱、你自己的格子圖，或編譯好的一行（text：它原本的那行字）。卡片會跟著變\nsetBpm(bpm)            速度，像滑桿。回傳速度\nupdate(dt)             每個畫面都要呼叫\ndeliver()              把現在聽到的告訴 onStep/onHit，但不畫圖（在你畫圖之前呼叫）\nphase()                { bar, beat, pos }：聽的人在小節和拍子的哪裡，0..1\nnow()                  { bar, step, phase, beatPhase, tokens, mode, text }，給畫面顯示用\ninfo()                 { clock, heartbeat, lookahead, late, extended, audio, latency, ... } 時鐘的健康狀況\nuseRhythm(mod)         之後再把 @openav/pattern 交給它（它可以自己載入）\nsrc()                  現在在打的那一行（只有格子又沒有節奏模組的時候是 ''）\nstrudelUrl()           那一行變成 strudel.cc 連結，沒有就是 ''\nedit(text)             像打字一樣播放一行：{ ok, error }。有錯就繼續打上一行\nhistory, undo()        之前的幾行，舊的在前。undo 退一步，回傳有沒有退成\ncopy()                 把 copyText() 放進剪貼簿：Promise<{ ok, text }>\nbarEvents(bar)         某一小節的鼓點：[{ voice, pos, ... }]\nvoices()               現在格子上有哪幾列\nseq, engine, root      DrumSequencer 或 DrumTransport、發聲引擎、卡片的元素\ndispose()              停止、釋放鼓組並把卡片拆掉\n```"}],"params":[{"name":"container","type":"HTMLElement","en":"The element to put the card in. `dispose()` takes it out again.","zh":"要放鼓機卡的元素。`dispose()` 會把它拆掉。"},{"name":"signals","type":"Signals","optional":true,"en":"A [`Signals`](#Signals) bus. Every hit is published as `drum/<voice>` (a pulse `{ level }`), `drum/<voice>/env` (0..1, decays) and `midi/note/on` `{ note, vel, ch: 10 }`. Without one the machine plays and calls your functions, and publishes nothing.","zh":"一個 [`Signals`](#Signals) 匯流排。每一下鼓都會發布成 `drum/<voice>`（脈衝 `{ level }`）、`drum/<voice>/env`（0..1，會衰減）和 `midi/note/on` `{ note, vel, ch: 10 }`。沒給的話鼓機照樣會打、會呼叫你的函式，但什麼都不發布。"},{"name":"engine","type":"Object","optional":true,"default":"null","en":"The sound engine. Left out, the machine makes its own [`drumEngine()`](#drumEngine). Anything with `enable()` and `noteOn(note, vel01, time)` works.","zh":"發聲的引擎。沒給的話，鼓機自己做一個 [`drumEngine()`](#drumEngine)。任何有 `enable()` 和 `noteOn(note, vel01, time)` 的東西都可以。"},{"name":"autoEnableEngine","type":"Boolean","optional":true,"default":"true","en":"Switch the engine on by the first hit (or by `play()` with the lookahead clock), so no extra click is needed beyond the one the browser wants.","zh":"讓第一下鼓（用 lookahead 時鐘的話是 `play()`）自己把引擎打開，所以除了瀏覽器要的那一次點擊，不用再多一次點擊。"},{"name":"onStep","type":"Function","optional":true,"default":"null","en":"`onStep(i, n, ev)`, once for every 16th step when it is heard. `i` is 0..15 in the bar, `n` counts steps since the start, before that step's hits. It is the clock a timeline counts bars on.","zh":"`onStep(i, n, ev)`，每個十六分音符步在聽到的時候呼叫一次。`i` 是小節裡的 0..15，`n` 是從開始算起的步數，在那一步的鼓之前呼叫。時間軸就是靠它數小節。"},{"name":"onHit","type":"Function","optional":true,"default":"null","en":"`onHit(voice, vel, ev)`, once for every drum hit when it is heard. `voice` is a name like `'kick'`, `vel` is 0..1.","zh":"`onHit(voice, vel, ev)`，每一下鼓在聽到的時候呼叫一次。`voice` 是像 `'kick'` 的名字，`vel` 是 0..1。"},{"name":"onTakeover","type":"Function","optional":true,"default":"null","en":"`onTakeover({ text, bar, offGrid, dropped, varied })`: without a `rhythm` module, a click on a cell turned a line of rhythm into the grid it was playing. `offGrid` hits moved to the nearest step and `dropped` drums had no row.","zh":"`onTakeover({ text, bar, offGrid, dropped, varied })`：沒有 `rhythm` 模組的時候，點一個格子把一行節奏換成它正在打的格子圖。`offGrid` 個鼓點被移到最近的一格，`dropped` 是沒有對應列的鼓。"},{"name":"bpm","type":"Number","optional":true,"default":"112","en":"The starting tempo, in beats per minute. The slider on the card goes from 60 to 180.","zh":"一開始的速度，每分鐘幾拍。卡上的滑桿範圍是 60 到 180。"},{"name":"pattern","type":"String | Object","optional":true,"default":"'four on floor'","en":"A preset name from [`PATTERNS`](#PATTERNS) or your own grid `{ kick: 'x...x...', … }`. Not a line of rhythm: to start with one, call `drums.edit(line)` or `drums.setPattern(compile(line), { text: line })`.","zh":"[`PATTERNS`](#PATTERNS) 裡的預設名稱，或你自己的格子圖 `{ kick: 'x...x...', … }`。不是一行節奏：要用一行節奏開始，就呼叫 `drums.edit(line)` 或 `drums.setPattern(compile(line), { text: line })`。"},{"name":"clock","type":"'frame' | 'lookahead'","optional":true,"default":"'frame'","en":"`'frame'`: the machine moves in `update(dt)` and each hit sounds in the frame it is due. `'lookahead'`: hits are handed to the kit ahead of time on its audio clock, so the beat is steady under a heavy frame and in a hidden tab (`'audio'` means the same). See [`DrumTransport`](#DrumTransport).","zh":"`'frame'`：鼓機在 `update(dt)` 裡前進，每一下在到時間的那個畫面響。`'lookahead'`：鼓點提前交給鼓組，用它的音訊時鐘排程，所以畫面很重或分頁被藏起來時節拍還是穩的（`'audio'` 是同一個意思）。見 [`DrumTransport`](#DrumTransport)。"},{"name":"lang","type":"'en' | 'zh'","optional":true,"default":"'en'","en":"`'zh'` names the lanes and the buttons in Chinese. Anything else is English.","zh":"`'zh'` 會用中文為軌道和按鈕命名（大鼓 小鼓 鈸 拍手）。其他的值都是英文。"},{"name":"heartbeat","type":"'auto' | 'interval' | false","optional":true,"default":"'auto'","en":"Only for `clock: 'lookahead'`. Who wakes the scheduler: `'auto'` a Web Worker timer (falls back to `setInterval` where a worker cannot start), `'interval'` always `setInterval`, `false` nobody (you call `drums.update()` often enough).","zh":"只在 `clock: 'lookahead'` 時有用。誰來叫醒排程器：`'auto'` 是 Web Worker 的計時器（worker 起不來的地方退回 `setInterval`），`'interval'` 一律用 `setInterval`，`false` 是沒人叫（你要自己夠頻繁地呼叫 `drums.update()`）。"},{"name":"latency","type":"'auto' | Number","optional":true,"default":"'auto'","en":"Only for `clock: 'lookahead'`. How long after a hit is scheduled it is heard, in seconds. `'auto'` asks the browser for its output latency; a number sets it by hand (a Bluetooth speaker is 0.15 to 0.25).","zh":"只在 `clock: 'lookahead'` 時有用。一下鼓排程之後多久才聽到，單位是秒。`'auto'` 向瀏覽器問輸出延遲。給數字就是手動設定（藍牙喇叭是 0.15 到 0.25）。"},{"name":"now","type":"Function","optional":true,"en":"Only for `clock: 'lookahead'`. A function that returns the time in seconds. The default is `performance.now() / 1000`. Replace it to run the machine on a fake clock in a test.","zh":"只在 `clock: 'lookahead'` 時有用。回傳目前時間（秒）的函式。預設是 `performance.now() / 1000`。測試時換掉它，就能讓鼓機跑在假的時鐘上。"},{"name":"hidden","type":"Function","optional":true,"en":"Only for `clock: 'lookahead'`. A function that returns true while nobody can see the page. The default looks at `document.visibilityState`.","zh":"只在 `clock: 'lookahead'` 時有用。沒有人看得到頁面時回傳 true 的函式。預設看的是 `document.visibilityState`。"},{"name":"rhythm","type":"Module","optional":true,"default":"null","en":"The [`@openav/pattern`](#compile) module (`import * as rhythm from '@openav/pattern'`, anything with `compile` and `fromGrid`). With it the line of rhythm over the grid is always there and editable, and the cells and the line rewrite each other. It can also be handed over later with `drums.useRhythm(mod)`.","zh":"[`@openav/pattern`](#compile) 模組（`import * as rhythm from '@openav/pattern'`，只要有 `compile` 和 `fromGrid` 就行）。有了它，格子上面那一行節奏就一直在、可以編輯，格子和那一行會互相改寫。也可以之後再用 `drums.useRhythm(mod)` 交給它。"},{"name":"onRhythm","type":"Function","optional":true,"default":"null","en":"`onRhythm(line, { how, prev })`, once for every new line. `how` is `'code'`, `'cell'`, `'type'`, `'undo'` or `'menu'`.","zh":"`onRhythm(line, { how, prev })`，每一行新的節奏呼叫一次。`how` 是 `'code'`、`'cell'`、`'type'`、`'undo'`、`'menu'` 其中之一。"},{"name":"onLossy","type":"Function","optional":true,"default":"null","en":"`onLossy({ text, bar, lost })`: a click on a cell would lose something the 16 steps cannot hold (triplets, `< >`, `?`, `bd:2`). `lost` is the same as in [`toGrid()`](#toGrid). Left out, a note appears on the card. The next click rewrites the line anyway.","zh":"`onLossy({ text, bar, lost })`：點一個格子會損失 16 格放不下的東西（三連音、`< >`、`?`、`bd:2`）。`lost` 跟 [`toGrid()`](#toGrid) 的一樣。沒給的話，卡上會出現一行提示。再點一次就會照樣改寫那一行。"},{"name":"onEditError","type":"Function","optional":true,"default":"null","en":"`onEditError(error, text)` when what was typed is not a line of rhythm (the mistake is marked in red and the last line that worked keeps playing), and `onEditError(null)` once it is fixed.","zh":"打進去的不是一行節奏的時候呼叫 `onEditError(error, text)`（錯的地方標紅，上一行能用的繼續打），修好之後呼叫 `onEditError(null)`。"},{"name":"copyText","type":"Function","optional":true,"default":"null","en":"`copyText(line, bpm)` returns what the Copy button puts on the clipboard. The default is the line itself.","zh":"`copyText(line, bpm)` 回傳「複製」按鈕要放進剪貼簿的文字。預設就是那一行本身。"},{"name":"strudelComment","type":"String","optional":true,"default":"''","en":"A comment to put at the head of the code that **Open in Strudel** sends.","zh":"要放在「在 Strudel 打開」送出去的程式碼最前面的註解。"}],"returns":{"type":"Object","en":"`{ seq, engine, root, clock, play(on), setPattern(p, { text }), setBpm(bpm), update(dt), deliver(), phase(), now(), info(), dispose(), useRhythm(mod), src(), strudelUrl(), history, undo(), edit(text), copy(), copyText(), barEvents(bar), voices() }` (and the getters `rhythm` and `copied`).","zh":"`{ seq, engine, root, clock, play(on), setPattern(p, { text }), setBpm(bpm), update(dt), deliver(), phase(), now(), info(), dispose(), useRhythm(mod), src(), strudelUrl(), history, undo(), edit(text), copy(), copyText(), barEvents(bar), voices() }`（還有 getter `rhythm` 和 `copied`）。"},"needs":{"en":"A page with a DOM, and `update(dt)` called every frame. Sound waits for the browser: the kit's `AudioContext` is made by the first hit (or by `play()` with the lookahead clock), and until the page has had a click it stays suspended. Callbacks and signals run from the start; the sound comes in on the first click, and nothing from before it is played late.","zh":"需要有 DOM 的頁面，而且每個畫面都要呼叫 `update(dt)`。聲音要等瀏覽器：鼓組的 `AudioContext` 在第一下鼓（用 lookahead 時鐘的話是 `play()`）的時候才建立，頁面被點一下之前它都是暫停的。回呼和訊號從一開始就會跑。聲音在第一次點擊之後才進來，點擊之前的鼓不會補打。"},"notes":[{"en":"With `clock: 'frame'` the first bar of a compiled line is played on the 16-step grid of four lanes: hits between steps move to the nearest step, and drums with no lane (crash, ride, toms, open hat) are left out. The console says so. `clock: 'lookahead'` plays a line exactly as written.","zh":"用 `clock: 'frame'` 的時候，編譯過的一行只有第一小節會在四條軌道、十六格的格子上打：落在格子之間的鼓點會移到最近的一格，沒有軌道的鼓（碎音鈸、疊音鈸、中鼓、開鈸）會被略過，console 會說一聲。`clock: 'lookahead'` 則照寫的樣子打。"},{"en":"In a p5 sketch you do not call it: [`startDrums()`](#startDrums) builds this card for you, with the lookahead clock, and calls `update()` every frame.","zh":"在 p5 草圖裡不用自己呼叫：[`startDrums()`](#startDrums) 會替你蓋這張卡，用 lookahead 時鐘，並且每個畫面幫你呼叫 `update()`。"}],"examples":[{"code":"import { mountDrums } from '@openav/drums';\n\nconst drums = mountDrums(document.getElementById('app'), { bpm: 120, pattern: 'breakbeat' });\ndrums.play(true);                          // start, like ticking the checkbox\nlet last = performance.now();\n(function frame(now) {\n  drums.update((now - last) / 1000);       // the machine only moves when you call this\n  last = now;\n  requestAnimationFrame(frame);\n})(last);","caption":{"en":"A drum machine card appears and plays a breakbeat; the yellow column runs along the grid. Click the page once to hear the kit; click the cells to change the beat.","zh":"頁面上出現一張鼓機卡，打著 breakbeat，黃色的那一欄沿著格子跑。在頁面上點一下才聽得到鼓組。點格子就能改節奏。"},"run":"module","lang":"js"},{"code":"import * as rhythm from '@openav/pattern';\nimport { mountDrums } from '@openav/drums';\n\nconst told = [];\nconst drums = mountDrums(document.getElementById('app'), {\n  rhythm, bpm: 110, clock: 'lookahead',\n  onRhythm: (line, { how }) => told.push(how + ': ' + line),\n  onEditError: (error) => told.push(error ? 'error ' + error.code : 'fixed'),\n});\nconsole.log(drums.src());                         // bd*4, [~ sd]*2, hh*8\nconsole.log(drums.edit('bd(3,8), cr ~ ~ ~').ok);  // true: the crash gets a row of its own\nconsole.log(drums.edit('bd [sd').ok);              // false: the last line that worked keeps playing\nconsole.log(drums.src());                          // bd(3,8), cr ~ ~ ~\nconsole.log(drums.undo(), drums.src());            // true bd*4, [~ sd]*2, hh*8\nconsole.log(told.join(' | '));\n// code: bd*4, [~ sd]*2, hh*8 | type: bd(3,8), cr ~ ~ ~ | fixed | error unclosed | undo: bd*4, [~ sd]*2, hh*8 | fixed\nlet last = performance.now();\n(function frame(now) { drums.update((now - last) / 1000); last = now; requestAnimationFrame(frame); })(last);","caption":{"en":"With the pattern module the line is shown over the grid. The code types two lines for you, one of them wrong, then goes back; type into the line yourself and the cells follow.","zh":"有了節奏模組，格子上面就會顯示那一行。程式替你打了兩行，其中一行是錯的，然後退回去。你自己在那一行打字，格子會跟著變。"},"run":"module","lang":"js"},{"code":"import { mountDrums } from '@openav/drums';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst g = document.getElementById('c').getContext('2d');\nconst drums = mountDrums(document.getElementById('app'), { signals, clock: 'lookahead', bpm: 100 });\ndrums.play(true);\n\nlet last = performance.now();\n(function frame(now) {\n  drums.update((now - last) / 1000); last = now;\n  g.fillStyle = '#111'; g.fillRect(0, 0, 400, 300);\n  g.fillStyle = '#ff7b54';\n  g.beginPath();\n  g.arc(200, 150, 30 + signals.get('drum/kick/env') * 90, 0, Math.PI * 2);   // 0..1, decays after each hit\n  g.fill();\n  requestAnimationFrame(frame);\n})(last);","caption":{"en":"A circle on the canvas swells with every kick: the drum machine publishes `drum/kick/env`, and the sketch only reads the signal.","zh":"畫布上的圓隨著每一下大鼓脹大：鼓機發布 `drum/kick/env`，草圖只要讀這個訊號。"},"run":"module","lang":"js"}],"related":["DrumTransport","DrumSequencer","drumEngine","compile","startDrums","drumHit","Signals"],"keywords":{"en":["drum machine","drums","sequencer","step grid","tr-808","beat","rhythm","transport","play","tempo"],"zh":["鼓機","鼓","音序器","步進格子","節奏","播放","速度","格子","掛上頁面","strudel"]},"tbd":[{"en":"The figure of 0.15 to 0.25 seconds for a Bluetooth speaker (the `latency` option) is from the handbook (docs/p5.md); it was not measured here.","zh":"藍牙喇叭的 0.15 到 0.25 秒（`latency` 選項）這個數字來自手冊（docs/p5.md），這裡沒有實際量過。"}],"claims":[]},
{"id":"PATTERNS","key":"PATTERNS","name":"PATTERNS","aliases":[],"kind":"constant","package":"@openav/drums","category":"drums","documented":true,"signature":"PATTERNS = object, 5 keys","import":"import { PATTERNS } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":173,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L173"},"summary":{"en":"The five preset grids of the drum machine: 'four on floor', breakbeat, 'half time', latin and sparse.","zh":"鼓機內建的五個預設格子圖：'four on floor'、breakbeat、'half time'、latin、sparse。"},"description":[{"en":"The preset names are what [`startDrums()`](#startDrums), [`DrumSequencer`](#DrumSequencer) and [`mountDrums()`](#mountDrums) accept as a `pattern`. A preset is just data, so you can start from one: `{ ...PATTERNS.latin, clap: 'x...x...x...x...' }` is `latin` with a clap on every beat.","zh":"預設的名稱就是 [`startDrums()`](#startDrums)、[`DrumSequencer`](#DrumSequencer)、[`mountDrums()`](#mountDrums) 的 `pattern` 收的值。預設就只是資料，所以你可以從其中一個開始改：`{ ...PATTERNS.latin, clap: 'x...x...x...x...' }` 就是在 `latin` 上每一拍加一下拍手。"},{"en":"The five presets as text, one lane per column:\n\n```text\n                  kick              snare             hat               clap\nfour on floor     1000100010001000  0000100000001000  1010101010101010  0000000000000000\nbreakbeat         1000000110100000  0000100000001001  1010101010101010  0000100000000000\nhalf time         1000000000001000  0000000010000000  1010101010101010  0000000010000000\nlatin             1000001000100000  0010010000100100  1011101110111011  0000000000000000\nsparse            1000000000100000  0000000000000000  0010001000100010  0000000010000000\n```","zh":"五個預設的內容，一欄一條軌道：\n\n```text\n                  kick              snare             hat               clap\nfour on floor     1000100010001000  0000100000001000  1010101010101010  0000000000000000\nbreakbeat         1000000110100000  0000100000001001  1010101010101010  0000100000000000\nhalf time         1000000000001000  0000000010000000  1010101010101010  0000000010000000\nlatin             1000001000100000  0010010000100100  1011101110111011  0000000000000000\nsparse            1000000000100000  0000000000000000  0010001000100010  0000000010000000\n```"}],"value":{"type":"Object","en":"Five presets, each `{ kick, snare, hat, clap }` as 16-character strings of `1` and `0`.","zh":"五個預設，每個都是 `{ kick, snare, hat, clap }`，各是 16 個字的 `1` 和 `0`。"},"params":[],"notes":[],"examples":[{"code":"import { PATTERNS, DrumSequencer } from '@openav/drums';\n\nconsole.log(Object.keys(PATTERNS).join(' | '));   // four on floor | breakbeat | half time | latin | sparse\nconsole.log(PATTERNS['half time'].snare);          // 0000000010000000\n\nconst seq = new DrumSequencer({ onHit() {} });\nseq.setPattern({ ...PATTERNS.latin, clap: 'x...x...x...x...' });   // latin plus a clap on every beat\nconsole.log(seq.pattern, seq.grid.clap.join(''));  // custom 1000100010001000","caption":{"en":"Lists the presets, reads one lane, and builds a variation of `latin` by spreading it into a new grid.","zh":"列出預設、讀其中一條軌道，並把 `latin` 展開成新的格子圖，做出一個變化版。"},"run":"module","lang":"js"}],"related":["laneSteps","DrumSequencer","DrumScheduler","mountDrums","startDrums"],"keywords":{"en":["presets","patterns","four on floor","breakbeat","half time","latin","sparse","beats","grids"],"zh":["預設","節奏","four on floor","breakbeat","half time","latin","sparse","格子圖"]},"tbd":[],"claims":[]},
{"id":"voiceForNote","key":"voiceForNote","name":"voiceForNote","aliases":[],"kind":"function","package":"@openav/drums","category":"drums","documented":true,"signature":"voiceForNote(note)","import":"import { voiceForNote } from '@openav/drums';","source":{"file":"packages/drums/index.js","line":34,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/drums/index.js#L34"},"summary":{"en":"Tells which drum of the kit a General MIDI drum note plays, or `null` for a note it does not play.","zh":"告訴你一個 General MIDI 鼓音符會打鼓組裡的哪一種鼓，不打的音符回傳 `null`。"},"description":[{"en":"Pads and drum machines send notes on channel 10, and several notes map to the same drum: 35 and 36 are kicks, 38 and 40 snares, 41 and 43 low toms, 45 and 47 toms, 48 and 50 high toms, 49, 52, 55 and 57 crashes, 51, 53 and 59 rides. A note that is not in the table, such as a melodic 60, gives `null`: the kit stays silent instead of guessing, because a wrong cymbal on a kick pad is worse than nothing.","zh":"打擊墊和鼓機在頻道 10 送音符，好幾個音符會對到同一種鼓：35 和 36 是大鼓，38 和 40 是小鼓，41 和 43 是落地鼓，45 和 47 是中鼓，48 和 50 是高音中鼓，49、52、55、57 是碎音鈸，51、53、59 是疊音鈸。不在表裡的音符，像旋律用的 60，會回傳 `null`：鼓組寧可不出聲也不亂猜，因為大鼓的打擊墊打出錯的鈸，比沒聲音還糟。"}],"params":[{"name":"note","type":"Number","en":"A MIDI note number.","zh":"MIDI 音符號碼。"}],"returns":{"type":"String | null","en":"The drum name (`'kick'`, `'crash'`…), or `null`.","zh":"鼓的名字（`'kick'`、`'crash'`…），或 `null`。"},"notes":[],"examples":[{"code":"import { voiceForNote } from '@openav/drums';\n\nconsole.log(voiceForNote(36), voiceForNote(35));   // kick kick\nconsole.log(voiceForNote(49), voiceForNote(57));   // crash crash\nconsole.log(voiceForNote(60));                      // null, a melodic note","caption":{"en":"Logs which drum a few notes play: two notes for one drum, and a melodic note that plays none.","zh":"印出幾個音符打哪一種鼓：兩個音符對同一種鼓，以及一個不打任何鼓的旋律音符。"},"run":"module","lang":"js"}],"related":["GM","drumEngine","WORDS"],"keywords":{"en":["general midi","note to drum","channel 10","pad","drum map","reverse lookup"],"zh":["general midi","音符對應鼓","頻道 10","打擊墊","鼓對照表","反查"]},"tbd":[],"claims":[]},
{"id":"compile","key":"compile","name":"compile","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"compile(src, { words = WORDS } = {})","import":"import { compile } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":498,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L498"},"summary":{"en":"Reads one line of rhythm text, like 'bd*4, [~ sd]*2, hh*8', and gives back the drum hits of any bar you ask for. It never throws.","zh":"把一行節奏文字，像 'bd*4, [~ sd]*2, hh*8'，讀成任何一個小節要打哪些鼓。它不會丟出錯誤。"},"description":[{"en":"`compile()` is the one function you need to turn a line of rhythm into hits. `compile('bd sd hh').eventsInBar(1)` answers \"what happens in bar 1?\" with a list of hits, sorted by `pos`. Bars count from 1, and one bar is one cycle of the line: the words share the bar equally, so `bd sd hh` puts a kick at 0, a snare at 1/3 and a hat at 2/3.","zh":"要把一行節奏變成鼓點，用 `compile()` 就夠了。`compile('bd sd hh').eventsInBar(1)` 回答「第 1 小節發生什麼事」，給你一串按 `pos` 排好的鼓點。小節從 1 開始數，一個小節就是這一行的一個循環：字平分這個小節，所以 `bd sd hh` 是大鼓在 0、小鼓在 1/3、鈸在 2/3。"},{"en":"Each hit from `eventsInBar(bar, { seed })` is `{ word, voice, n, pos, dur, vel, layer, token }`. `word` is what was written, `voice` the drum it plays (`'kick'`), `pos` where in the bar, 0 up to (not including) 1. `dur` is how long it lasts, in bars, and may run past the bar (`bd/2` lasts two). `vel` is always 1. `layer` is the index of the comma part it came from. `token` is `{ start, end }`, the character range of the word in `src`, so you can light the word while it sounds. `n` is the sample number from `bd:2`, or `null`. `n` is kept for you but does not change the sound.","zh":"`eventsInBar(bar, { seed })` 給的每個鼓點是 `{ word, voice, n, pos, dur, vel, layer, token }`。`word` 是寫的字，`voice` 是它打的鼓（`'kick'`），`pos` 是在小節的哪裡，從 0 到（不含）1。`dur` 是持續多久，單位是小節，可以超過這個小節（`bd/2` 長兩個小節）。`vel` 永遠是 1。`layer` 是它來自第幾個逗號層。`token` 是 `{ start, end }`，這個字在 `src` 裡的字元範圍，字在響的時候可以拿來點亮它。`n` 是 `bd:2` 的取樣編號，沒寫就是 `null`。`n` 會保留給你，但不會改變聲音。"},{"en":"It never throws. A line it cannot read comes back as `{ ok: false, error }`, and `eventsInBar()` returns `[]` for every bar, so check `ok` before you trust the hits. `error.index` and `error.length` mark the characters that are wrong, `error.message` and `error.hint` are English sentences, and `error.code` is one of `empty`, `unclosed`, `unexpected`, `bad-number`, `bad-euclid`, `unknown-word` or `unsupported`. An `unknown-word` error also carries `suggestion`, the closest word or `null`. The words and the symbols you can write are listed below.","zh":"它不會丟出錯誤。讀不懂的一行會回傳 `{ ok: false, error }`，而且每個小節的 `eventsInBar()` 都回傳 `[]`，所以相信鼓點之前先檢查 `ok`。`error.index` 和 `error.length` 標出錯的字元，`error.message` 和 `error.hint` 是英文句子，`error.code` 是 `empty`、`unclosed`、`unexpected`、`bad-number`、`bad-euclid`、`unknown-word`、`unsupported` 其中之一。`unknown-word` 錯誤還會帶 `suggestion`，也就是最接近的字，沒有就是 `null`。可以寫的字和符號列在下面。"},{"en":"```text\nbd sd hh      steps share the bar equally\n~  or  -       a rest\n[bd sd]        a group takes one step and splits it again\nbd*4           faster: 4 times in its step (decimals work: bd*1.5)\nbd/2           slower: once every 2 bars\n<bd sd>        one per bar, in turn\nbd, hh*8       a comma stacks layers\nbd(3,8)        Euclidean: 3 hits spread over 8 steps\nbd(3,8,2)      the same, starting 2 steps later\nbd!2  bd !     repeat the step (each copy takes a step)\nbd@3  bd _ _   make the step 3 times as long\nhh*8?  hh*8?0.3   drop each hit by chance (1/2, or 0.3)\nbd:2           sample number, kept in n\n```","zh":"```text\nbd sd hh      字平分一個小節\n~  或  -       休止\n[bd sd]        一組佔一格，再平分那一格\nbd*4           加快：在它那一格裡打 4 次（小數也行：bd*1.5）\nbd/2           放慢：每 2 個小節打一次\n<bd sd>        每小節輪一個\nbd, hh*8       逗號把幾層疊在一起\nbd(3,8)        歐幾里得節奏：3 下平均分在 8 格\nbd(3,8,2)      一樣，但晚 2 格開始\nbd!2  bd !     重複這一格（每個複本各佔一格）\nbd@3  bd _ _   這一格拉長成 3 倍\nhh*8?  hh*8?0.3   每一下用機率省略（1/2 或 0.3）\nbd:2           取樣編號，存在 n\n```"},{"en":"Modifiers chain in the order written (`bd(3,8)*2`, `hh*8?`). Word case does not matter (`BD` is `bd`). `?` is decided by `seed` and the time of the hit, so the same seed and bar always give the same hits, while other bars and other seeds differ. Not supported, and refused with an `unsupported` error and a hint: `.` (Strudel's grouping), `|`, `{ }`, `%`, `..` and `^`. A dot matters most: lane strings like `'x...x...'` are not a line of rhythm, and a rest is `~`.","zh":"修飾符號照寫的順序一個接一個（`bd(3,8)*2`、`hh*8?`）。字的大小寫不影響（`BD` 就是 `bd`）。`?` 由 `seed` 和鼓點的時間決定，所以同一個 seed、同一個小節永遠打出一樣的鼓點，換一個小節或換一個 seed 就不同。不支援，並會用 `unsupported` 錯誤和提示拒絕的有：`.`（Strudel 的分組）、`|`、`{ }`、`%`、`..`、`^`。其中最重要的是點：像 `'x...x...'` 這種軌道字串不是一行節奏，休止要寫 `~`。"}],"params":[{"name":"src","type":"String","en":"The line of rhythm, for example `'bd sd hh'`. Anything that is not a string is turned into one (`null` and `undefined` become an empty line, which is an error).","zh":"一行節奏，例如 `'bd sd hh'`。不是字串的東西會被轉成字串（`null` 和 `undefined` 變成空的一行，那是錯誤）。"},{"name":"words","type":"Object","optional":true,"default":"WORDS","en":"Which words mean which drum: `{ word: voice }`. The default is [`WORDS`](#WORDS). To add a word, spread the default: `{ ...WORDS, boom: 'kick' }`. The voice should be one the drum kit plays, see [`GM`](#GM).","zh":"哪個字代表哪一種鼓：`{ 字: 鼓聲 }`。預設是 [`WORDS`](#WORDS)。要多加一個字，就把預設展開：`{ ...WORDS, boom: 'kick' }`。鼓聲要是鼓組會打的，見 [`GM`](#GM)。"}],"returns":{"type":"Object","en":"`{ ok, error, src, voices, layers, period, eventsInBar(bar, { seed }) }`. `ok` is true when the line was read. `error` is `null`, or `{ code, index, length, message, hint }`. `voices` lists the drums the line uses, in the order they first appear. `layers` counts the comma parts. `period` is the number of bars until the written rhythm repeats.","zh":"`{ ok, error, src, voices, layers, period, eventsInBar(bar, { seed }) }`。`ok` 是 true 表示這一行讀懂了。`error` 是 `null`，或 `{ code, index, length, message, hint }`。`voices` 列出這一行用到的鼓，依第一次出現的順序。`layers` 是逗號分出來的層數。`period` 是寫出來的節奏要幾個小節才會重複。"},"notes":[{"en":"Time inside is exact fractions, and each bar is computed on its own. Bar 1 000 000 of `[bd sd sn]*3` lands on the same ninths as bar 1, and asking for bar 500 is as cheap as asking for bar 2.","zh":"內部的時間用精確的分數，每個小節各自計算。`[bd sd sn]*3` 的第 1 000 000 小節跟第 1 小節落在一模一樣的九分之幾，問第 500 小節跟問第 2 小節一樣便宜。"},{"en":"From a p5 sketch you do not call it: [`startDrums()`](#startDrums) takes the line and compiles it for you. Use `compile()` yourself when you want the hits as data, for a score, a drawing or a test.","zh":"在 p5 草圖裡不用自己呼叫：[`startDrums()`](#startDrums) 收下這一行，會替你編譯。想把鼓點當資料用，例如畫譜、畫圖或寫測試的時候，才自己呼叫 `compile()`。"},{"en":"The way of writing comes from TidalCycles and Strudel. This is an independent implementation written from their public documentation. Where it differs from strudel.cc on purpose (a rotation of `n` steps or more wraps around, `*0` and `(9,8)` are errors, unknown names are errors), the package README says so.","zh":"這種寫法來自 TidalCycles 和 Strudel，這個套件是依他們的公開文件獨立實作的。刻意跟 strudel.cc 不同的地方（旋轉 `n` 格以上會繞回來、`*0` 和 `(9,8)` 是錯誤、不認識的字是錯誤），套件的 README 都有寫。"}],"examples":[{"code":"import { compile } from '@openav/pattern';\n\nconst p = compile('bd sd hh');\nfor (const e of p.eventsInBar(1)) console.log(e.word, e.voice, e.pos.toFixed(2));\n// bd kick 0.00\n// sd snare 0.33\n// hh hat 0.67","caption":{"en":"Logs the three hits of bar 1: the word, the drum it plays, and where in the bar. Open the console to see them.","zh":"印出第 1 小節的三個鼓點：字、它打的鼓、在小節的哪裡。打開 console 就看得到。"},"run":"module","lang":"js"},{"code":"import { compile } from '@openav/pattern';\n\nconst p = compile('<bd sd>, hh*4');\nconsole.log(p.period);                                   // 2\nfor (const bar of [1, 2, 3]) {\n  const words = p.eventsInBar(bar).filter((e) => e.layer === 0).map((e) => e.word);\n  console.log('bar', bar, words.join(' '));\n}\n// bar 1 bd\n// bar 2 sd\n// bar 3 bd\n\nconst bad = compile('bd [sd');\nconsole.log(bad.ok, bad.error.code, bad.error.index);   // false unclosed 3\nconsole.log(bad.eventsInBar(1));                         // []","caption":{"en":"A line that changes every bar: `<bd sd>` alternates, so `period` is 2. A line with a mistake comes back with `ok: false`, the error code and the character it starts at.","zh":"每個小節都不一樣的一行：`<bd sd>` 輪流出現，所以 `period` 是 2。有錯的一行會回傳 `ok: false`、錯誤代碼，和錯誤從哪個字元開始。"},"run":"module","lang":"js"},{"code":"import { compile } from '@openav/pattern';\n\nconst p = compile('bd(3,8), [~ sd]*2, hh*8');\nconst g = document.getElementById('c').getContext('2d');\ng.fillStyle = '#111'; g.fillRect(0, 0, 400, 300);\nconst colors = ['#ff7b54', '#ffd166', '#6f9bff'];\nfor (const e of p.eventsInBar(1)) {\n  g.fillStyle = colors[e.layer];\n  g.fillRect(20 + e.pos * 360, 40 + e.layer * 70, e.dur * 360 - 4, 40);   // dur is in bars\n}","caption":{"en":"Draws bar 1 on the canvas, one row per comma part: each hit is a block at its position, as long as it lasts.","zh":"把第 1 小節畫在畫布上，每個逗號層一排：每個鼓點是在它位置上的一塊，長度就是它持續的時間。"},"run":"module","lang":"js"}],"related":["parse","WORDS","euclid","toGrid","DrumScheduler","startDrums"],"keywords":{"en":["rhythm","mini-notation","tidal","strudel","pattern","drum","parse","events","bar","eventsInBar"],"zh":["節奏","一行節奏","鼓點","小節","編譯","解析","strudel","歐幾里得","鼓機"]},"tbd":[],"claims":[]},
{"id":"euclid","key":"euclid","name":"euclid","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"euclid(pulses, steps, rotate = 0)","import":"import { euclid } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":69,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L69"},"summary":{"en":"Spreads a number of hits as evenly as possible over a number of steps, the Euclidean rhythm. `euclid(3, 8)` is the tresillo.","zh":"把一些鼓點盡量平均地分在一些格子上，也就是歐幾里得節奏。`euclid(3, 8)` 就是 tresillo。"},"description":[{"en":"It is the function behind `bd(3,8)` in a line of rhythm: [`compile()`](#compile) calls it for every `(k,n)`. Use it on its own when you want the pattern as data, for a drawing or your own sequencer. `euclid(3, 8)` is `x..x..x.`, and `euclid(5, 8)` is `x.xx.xx.`.","zh":"一行節奏裡的 `bd(3,8)` 就是靠它：[`compile()`](#compile) 每遇到一個 `(k,n)` 就會呼叫它。想把這個節奏當資料用，例如畫圖或接到自己的音序器，就單獨呼叫它。`euclid(3, 8)` 是 `x..x..x.`，`euclid(5, 8)` 是 `x.xx.xx.`。"},{"en":"It follows Bjorklund's procedure as Toussaint (2005) describes it. `rotate` moves the pattern later, the direction strudel.cc plays: `euclid(3, 8, 2)` is `x.x..x..`, the hits of `euclid(3, 8)` moved two steps to the right.","zh":"它照 Toussaint（2005）說明的 Bjorklund 方法做。`rotate` 把節奏往後移，方向跟 strudel.cc 一樣：`euclid(3, 8, 2)` 是 `x.x..x..`，也就是 `euclid(3, 8)` 的鼓點往右移兩格。"}],"params":[{"name":"pulses","type":"Number","en":"How many hits. A negative number plays the gaps instead: `euclid(-3, 8)` is the five steps `euclid(3, 8)` leaves out. More hits than steps fills every step. The number is cut to a whole number.","zh":"有幾下。負數會打空隙：`euclid(-3, 8)` 是 `euclid(3, 8)` 沒打的那五格。比格數多的話，每一格都會打。小數會被去掉。"},{"name":"steps","type":"Number","en":"How many steps. Fewer than 1 gives an empty array.","zh":"有幾格。小於 1 會得到空陣列。"},{"name":"rotate","type":"Number","optional":true,"default":"0","en":"Start the pattern this many steps later. Negative starts earlier, and a number of `steps` or more wraps around.","zh":"讓節奏晚這麼多格開始。負數是提早，`steps` 以上的數字會繞回來。"}],"returns":{"type":"Boolean[]","en":"An array of `steps` booleans, true where there is a hit.","zh":"一個長度 `steps` 的布林陣列，有鼓點的地方是 true。"},"notes":[],"examples":[{"code":"import { euclid } from '@openav/pattern';\n\nconst show = (bits) => bits.map((b) => (b ? 'x' : '.')).join('');\nconsole.log(show(euclid(3, 8)));      // x..x..x.\nconsole.log(show(euclid(5, 8)));      // x.xx.xx.\nconsole.log(show(euclid(3, 8, 2)));   // x.x..x..","caption":{"en":"Logs three rhythms as text: `x` is a hit and `.` is a rest. The third is the first one moved two steps later.","zh":"把三個節奏印成文字：`x` 是鼓點、`.` 是休止。第三個是第一個往後移兩格。"},"run":"module","lang":"js"},{"code":"import { euclid } from '@openav/pattern';\n\nconst g = document.getElementById('c').getContext('2d');\ng.fillStyle = '#111'; g.fillRect(0, 0, 400, 300);\ng.fillStyle = '#ffd166';\neuclid(5, 16).forEach((hit, i) => {          // 5 hits on 16 steps\n  g.globalAlpha = hit ? 1 : 0.2;\n  g.fillRect(12 + i * 23, 120, 18, 60);\n});\nconsole.log(euclid(5, 16).filter(Boolean).length);   // 5","caption":{"en":"Draws sixteen blocks and lights five of them, spread as evenly as sixteen steps allow.","zh":"畫十六個方塊，點亮其中五個，分得跟十六格能做到的一樣平均。"},"run":"module","lang":"js"}],"related":["compile","toGrid","fromGrid"],"keywords":{"en":["euclidean","euclid","bjorklund","toussaint","tresillo","cinquillo","spread hits","rhythm generator","polyrhythm"],"zh":["歐幾里得","歐氏節奏","bjorklund","平均分配","節奏產生器","旋轉","tresillo"]},"tbd":[],"claims":[]},
{"id":"fromGrid","key":"fromGrid","name":"fromGrid","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"fromGrid(grid, { steps = 16, words = null, order = null } = {})","import":"import { fromGrid } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":628,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L628"},"summary":{"en":"Turns a grid of steps, like the drum machine card, into the one line of rhythm a person would write for it.","zh":"把一張格子圖，像鼓機卡上的那種，轉成一個人會為它寫的那一行節奏。"},"description":[{"en":"It is what happens when you click a cell of the drum machine card: the grid has to come back as text. Each lane becomes one layer, joined with `, `, and it picks the shortest honest spelling. Steady hits become `bd*4` or `hh*8`, one hit per group `[~ sd]*2`, a Euclidean rhythm `bd(3,8)`, and anything else is written beat by beat, like `bd [~ ~ ~ bd] bd*2 ~`. It never throws.","zh":"按下鼓機卡上的一個格子，就是在做這件事：格子圖要變回文字。每條軌道變成一層，用 `, ` 接起來，並選最短又準確的寫法。平穩的鼓點寫成 `bd*4` 或 `hh*8`，每組一下寫成 `[~ sd]*2`，歐幾里得節奏寫成 `bd(3,8)`，其他的一拍一拍寫，像 `bd [~ ~ ~ bd] bd*2 ~`。它不會丟出錯誤。"},{"en":"The line is exact: [`compile()`](#compile) of the result lands on the same steps, in every bar. The rules are in the package README, and the tests check a thousand random grids.","zh":"這一行是精確的：把結果交給 [`compile()`](#compile)，每個小節都會落在同樣的格子上。規則寫在套件的 README，測試用一千張隨機的格子圖驗證過。"}],"params":[{"name":"grid","type":"Object","en":"The steps: `{ lane: 'x...x...' }` or `{ lane: [1, 0, 0, 0, …] }`. A lane is named by its drum (`kick`) or its word (`bd`). `x`, `X`, `1` and `true` are hits, `|` and spaces are ignored. A lane with no name the kit knows is left out.","zh":"格子：`{ 軌道: 'x...x...' }` 或 `{ 軌道: [1, 0, 0, 0, …] }`。軌道可以用鼓的名字（`kick`）或字（`bd`）命名。`x`、`X`、`1`、`true` 是鼓點，`|` 和空白會被忽略。名字不是鼓組認得的軌道會被丟掉。"},{"name":"steps","type":"Number","optional":true,"default":"16","en":"How many steps each lane has.","zh":"每條軌道有幾格。"},{"name":"words","type":"Object","optional":true,"default":"null","en":"Keep your own spelling: `{ kick: 'kick' }` writes `kick*4` instead of `bd*4`.","zh":"保留你自己的寫法：`{ kick: 'kick' }` 會寫成 `kick*4` 而不是 `bd*4`。"},{"name":"order","type":"String[]","optional":true,"default":"null","en":"Drums to put first, in this order, for example `['kick', 'snare']`. The rest follow in the order of the grid's keys.","zh":"要放在最前面的鼓，照這個順序，例如 `['kick', 'snare']`。其他的接在後面，照格子圖的鍵的順序。"}],"returns":{"type":"String","en":"One line of rhythm that plays exactly the grid. A grid with no hit anywhere gives `'~'`.","zh":"一行剛好打出這張格子圖的節奏。完全沒有鼓點的格子圖會得到 `'~'`。"},"notes":[],"examples":[{"code":"import { fromGrid } from '@openav/pattern';\n\nconsole.log(fromGrid({\n  kick: 'x...x...x...x...',\n  snare: '....x.......x...',\n  hat: 'x.x.x.x.x.x.x.x.',\n}));\n// bd*4, [~ sd]*2, hh*8","caption":{"en":"Logs the line for a classic four-on-the-floor grid: a kick on every beat, a snare on 2 and 4, eight hats.","zh":"印出經典四四拍格子圖的那一行：每一拍大鼓、第 2 和第 4 拍小鼓、八下鈸。"},"run":"module","lang":"js"},{"code":"import { fromGrid, compile } from '@openav/pattern';\n\nconst grid = { kick: [1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0], hat: 'x.x.x.x.x.x.x.x.' };\nconst line = fromGrid(grid, { order: ['kick'] });\nconsole.log(line);                          // bd(3,8), hh*8\nconsole.log(compile(line).ok);              // true\nconsole.log(fromGrid(grid, { words: { kick: 'kick' } }));   // kick(3,8), hh*8\nconsole.log(fromGrid({}));                  // ~","caption":{"en":"A lane can be an array of 0 and 1. The hits at steps 0, 6 and 12 are recognised as a Euclidean rhythm. `words` keeps your own spelling, and an empty grid is one rest.","zh":"一條軌道也可以是 0 和 1 的陣列。第 0、6、12 格的鼓點被認成歐幾里得節奏。`words` 保留你自己的寫法，空的格子圖就是一個休止。"},"run":"module","lang":"js"}],"related":["toGrid","compile","euclid","toStrudel"],"keywords":{"en":["grid","steps","step sequencer","cells","lanes","to text","write rhythm","drum machine card"],"zh":["格子","步進","步進音序器","軌道","轉成文字","鼓機卡","一行節奏"]},"tbd":[],"claims":[]},
{"id":"fromStrudel","key":"fromStrudel","name":"fromStrudel","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"fromStrudel(code)","import":"import { fromStrudel } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":727,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L727"},"summary":{"en":"Reads code copied from strudel.cc, like `s(\"bd*4\").fast(2)`, into the line of rhythm OAV plays, plus the tempo it found.","zh":"把從 strudel.cc 複製來的程式碼，像 `s(\"bd*4\").fast(2)`，讀成 OAV 會打的那一行節奏，並附上找到的速度。"},"description":[{"en":"It lets someone paste what they made in Strudel. It reads `s(\"…\")` and `sound(\"…\")` with `\"`, `'` or backtick quotes. Several of them (also with a label like `$:` or `name:` in front) become layers. A `.fast(n)` or `.slow(n)` is folded into the line as `[…]*n` or `[…]/n`. `setcpm(x)` gives `bpm` as x times 4, `setcps(x)` as x times 240. `.bank(…)` goes to `ignored`, because OAV plays its own kit. Any other call or statement goes to `unsupported`, and the rest still plays. It never throws.","zh":"它讓人可以貼上自己在 Strudel 做的東西。它讀 `s(\"…\")` 和 `sound(\"…\")`，引號可以是 `\"`、`'` 或反引號。好幾個（前面也可以有 `$:` 或 `name:` 這種標籤）會變成好幾層。`.fast(n)` 或 `.slow(n)` 會折進那一行，變成 `[…]*n` 或 `[…]/n`。`setcpm(x)` 的 `bpm` 是 x 乘 4，`setcps(x)` 是 x 乘 240。`.bank(…)` 進 `ignored`，因為 OAV 打的是自己的鼓組。其他的呼叫或敘述都進 `unsupported`，剩下的照樣會打。它不會丟出錯誤。"},{"en":"It only reshapes text: it does not check the words. `fromStrudel('s(\"bd kik\")')` gives `ok: true` with `src: 'bd kik'`, so pass `src` to [`compile()`](#compile) and look at its `ok`. [`startDrums()`](#startDrums) accepts Strudel code directly and does all of this for you.","zh":"它只是把文字重新整理，不會檢查字。`fromStrudel('s(\"bd kik\")')` 會回 `ok: true` 和 `src: 'bd kik'`，所以要把 `src` 交給 [`compile()`](#compile)，看它的 `ok`。[`startDrums()`](#startDrums) 可以直接收 Strudel 程式碼，這些事都替你做了。"}],"params":[{"name":"code","type":"String","en":"The Strudel code, as copied from the strudel.cc editor.","zh":"從 strudel.cc 編輯器複製來的 Strudel 程式碼。"}],"returns":{"type":"Object","en":"`{ strudel, ok, src, bpm, ignored, unsupported }`. `strudel` is false when the text has no `s(…)` or `sound(…)` in it. `ok` is true when there is a line of rhythm in `src`. `bpm` is the tempo from `setcpm()` or `setcps()`, else `null`. `ignored` lists what was left out on purpose (`.bank(…)`), `unsupported` what OAV does not do (`.jux(rev)`, `.gain(…)`).","zh":"`{ strudel, ok, src, bpm, ignored, unsupported }`。文字裡沒有 `s(…)` 或 `sound(…)` 的話，`strudel` 是 false。`src` 裡有一行節奏的話，`ok` 是 true。`bpm` 是 `setcpm()` 或 `setcps()` 給的速度，沒有就是 `null`。`ignored` 列出刻意略過的（`.bank(…)`），`unsupported` 列出 OAV 不做的（`.jux(rev)`、`.gain(…)`）。"},"notes":[],"examples":[{"code":"import { fromStrudel, compile } from '@openav/pattern';\n\nconst r = fromStrudel('setcpm(110/4)\\ns(\"bd*4, [~ sd]*2\").bank(\"RolandTR909\").fast(2).jux(rev)');\nconsole.log(r.src);            // [bd*4, [~ sd]*2]*2\nconsole.log(r.bpm);            // 110\nconsole.log(r.ignored);        // ['.bank(\"RolandTR909\")']\nconsole.log(r.unsupported);    // ['.jux(rev)']\nconsole.log(compile(r.src).ok); // true","caption":{"en":"Reads a typical Strudel snippet: `.fast(2)` is folded into the line, the tempo is found, `.bank()` is ignored and `.jux()` is reported as unsupported.","zh":"讀一段常見的 Strudel 程式：`.fast(2)` 折進那一行，找到速度，`.bank()` 被略過，`.jux()` 被回報為不支援。"},"run":"module","lang":"js"},{"code":"import { fromStrudel } from '@openav/pattern';\n\nconsole.log(fromStrudel('note(\"c e g\")').strudel);   // false, no s(\"…\") in it\nconsole.log(fromStrudel('s(\"bd kik\")').ok);          // true: the words are not checked\nconsole.log(fromStrudel('setcps(0.5)\\ns(\"bd\")').bpm); // 120","caption":{"en":"The edges: text with no `s(…)` is not Strudel for OAV, a misspelled word still passes, and `setcps(0.5)` is 120 bpm.","zh":"邊界情況：沒有 `s(…)` 的文字對 OAV 來說不是 Strudel，拼錯的字還是會通過，`setcps(0.5)` 是 120 bpm。"},"run":"module","lang":"js"}],"related":["toStrudel","strudelUrl","compile","startDrums"],"keywords":{"en":["strudel","strudel.cc","import","paste","tidal","live coding","setcpm","fast","slow"],"zh":["strudel","匯入","貼上","直播編碼","live coding","速度","setcpm"]},"tbd":[],"claims":[]},
{"id":"parse","key":"parse","name":"parse","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"parse(src)","import":"import { parse } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":123,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L123"},"summary":{"en":"Reads a line of rhythm into a tree of nodes, each with its character range. It does not look words up in the drum kit.","zh":"把一行節奏讀成一棵節點樹，每個節點都帶著它的字元範圍。它不會去查鼓組有沒有這個字。"},"description":[{"en":"`parse()` is the first half of [`compile()`](#compile): it checks the grammar and builds the tree, and stops there. Use it when you want to look at the structure of a line, for example to colour its words or find a mistake. For the hits, call `compile()`.","zh":"`parse()` 是 [`compile()`](#compile) 的前半段：只檢查文法、建出樹，到此為止。想看一行的結構，例如幫字上色或找錯誤的時候用它。要鼓點的話，呼叫 `compile()`。"},{"en":"The root is always a `layer` whose `layers` are the top-level comma parts, and each of those is a `seq`. The node types are `step` `{ word, n }`, `rest`, `seq` `{ steps: [{ node, weight }] }`, `layer` `{ layers }`, `alt` `{ steps }` (one per bar), `fast` and `slow` `{ factor, node }`, `euclid` `{ pulses, slots, rotate, node }` and `maybe` `{ chance, id, node }`. Every node has `start` and `end`, its character range in `src`. Weights and factors are fractions `[numerator, denominator]`, so `bd*1.5` has the factor `[3, 2]`.","zh":"樹根永遠是 `layer`，它的 `layers` 是最上層用逗號分開的幾部分，每一部分是一個 `seq`。節點的種類有 `step` `{ word, n }`、`rest`、`seq` `{ steps: [{ node, weight }] }`、`layer` `{ layers }`、`alt` `{ steps }`（每小節一個）、`fast` 和 `slow` `{ factor, node }`、`euclid` `{ pulses, slots, rotate, node }`、`maybe` `{ chance, id, node }`。每個節點都有 `start` 和 `end`，也就是它在 `src` 裡的字元範圍。權重和倍數是分數 `[分子, 分母]`，所以 `bd*1.5` 的倍數是 `[3, 2]`。"}],"params":[{"name":"src","type":"String","en":"The line of rhythm, for example `'bd [sd sd]'`.","zh":"一行節奏，例如 `'bd [sd sd]'`。"}],"returns":{"type":"Object","en":"`{ ok: true, tree, error: null }`, or `{ ok: false, tree: null, error }` with the same `{ code, index, length, message, hint }` error as [`compile()`](#compile).","zh":"`{ ok: true, tree, error: null }`，或 `{ ok: false, tree: null, error }`，`error` 跟 [`compile()`](#compile) 的一樣，是 `{ code, index, length, message, hint }`。"},"notes":[{"en":"Because `parse()` never looks at the drum kit, `parse('kik')` succeeds while `compile('kik')` fails with `unknown-word`. Everything else (an unclosed bracket, `*0`, `(9,8)`) fails in both.","zh":"因為 `parse()` 不看鼓組，`parse('kik')` 會成功，`compile('kik')` 卻會失敗，錯誤是 `unknown-word`。其他的錯（括號沒關、`*0`、`(9,8)`）兩個都會失敗。"}],"examples":[{"code":"import { parse } from '@openav/pattern';\n\nconst r = parse('bd [sd hh]*2');\nconsole.log(r.ok);                                          // true\nconsole.log(r.tree.layers[0].steps.map((s) => s.node.type)); // ['step', 'fast']\nconsole.log(parse('bd [sd').error.code);                    // unclosed","caption":{"en":"Logs the type of each step of the first layer (a plain step, then a group made faster), and the error code of a line with an unclosed bracket.","zh":"印出第一層每一格的種類（一個普通的字，接著一組被加快的），以及括號沒關的那一行的錯誤代碼。"},"run":"module","lang":"js"},{"code":"import { parse } from '@openav/pattern';\n\n// light every word of the line: each step node knows its characters\nconst src = 'bd*4, [~ sd]*2, hh*8';\nconst words = [];\nconst walk = (node) => {\n  if (node.type === 'step') words.push(src.slice(node.start, node.end) + '@' + node.start);\n  for (const s of node.steps || []) walk(s.node);\n  for (const l of node.layers || []) walk(l);\n  if (node.node) walk(node.node);\n};\nwalk(parse(src).tree);\nconsole.log(words.join(' '));   // bd@0 sd@9 hh@16","caption":{"en":"Walks the tree and logs each word with the character it starts at, the data you need to colour words in an editor.","zh":"走過整棵樹，印出每個字和它開始的字元位置，在編輯器裡幫字上色就需要這份資料。"},"run":"module","lang":"js"}],"related":["compile","toGrid","toStrudel"],"keywords":{"en":["parse","tree","syntax","grammar","ast","mini-notation","nodes","tokens"],"zh":["解析","語法樹","文法","節點","一行節奏","字元範圍"]},"tbd":[],"claims":[]},
{"id":"SHORT","key":"SHORT","name":"SHORT","aliases":[],"kind":"constant","package":"@openav/pattern","category":"drums","documented":true,"signature":"SHORT = object, 13 keys","import":"import { SHORT } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":548,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L548"},"summary":{"en":"The short word Tidal and Strudel write for each drum: `kick` is `bd`, `snare` is `sd`. The other direction from `WORDS`.","zh":"Tidal 和 Strudel 為每種鼓寫的短字：`kick` 是 `bd`，`snare` 是 `sd`。跟 `WORDS` 方向相反。"},"description":[{"en":"[`fromGrid()`](#fromGrid) writes with these words when you do not give it your own spelling, and [`toStrudel()`](#toStrudel) uses them (with `hh` for `pedalhat`) to speak Strudel.","zh":"[`fromGrid()`](#fromGrid) 沒給你自己的寫法時，就是用這些字寫。[`toStrudel()`](#toStrudel) 也用它們（`pedalhat` 用 `hh`）來說 Strudel 的話。"}],"value":{"type":"Object","en":"A frozen object of 13 `{ voice: word }` pairs, one per drum of the kit. `pedalhat` has no short word, so it maps to `'pedalhat'`, OAV's own.","zh":"一個凍結的物件，有 13 組 `{ 鼓聲: 字 }`，鼓組每種鼓一組。`pedalhat` 沒有短字，所以對到 OAV 自己的 `'pedalhat'`。"},"params":[],"notes":[],"examples":[{"code":"import { SHORT } from '@openav/pattern';\n\nconsole.log(SHORT.kick, SHORT.snare, SHORT.hat, SHORT.hightom);   // bd sd hh ht\nconsole.log(SHORT.pedalhat);                                       // pedalhat","caption":{"en":"Logs the short word for a few drums, and the one drum that has none.","zh":"印出幾種鼓的短字，以及唯一沒有短字的那一種。"},"run":"module","lang":"js"}],"related":["WORDS","fromGrid","toStrudel"],"keywords":{"en":["short names","bd","sd","hh","reverse","words","voice to word"],"zh":["短字","縮寫","鼓的名字","反查","bd sd hh"]},"tbd":[],"claims":[]},
{"id":"strudelUrl","key":"strudelUrl","name":"strudelUrl","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"strudelUrl(src, opts = {})","import":"import { strudelUrl } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":872,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L872"},"summary":{"en":"Makes a strudel.cc link that opens the line of rhythm in the Strudel editor.","zh":"做出一個 strudel.cc 的連結，打開後 Strudel 編輯器裡就是這一行節奏。"},"description":[{"en":"The format is the one strudel.cc shows in its own address bar. It is what the drum machine card's **Open in Strudel** link uses. Open it in a **new tab**: a strudel.cc tab that is already open does not read a changed `#`.","zh":"格式就是 strudel.cc 自己的網址列顯示的那種，鼓機卡上的 **在 Strudel 打開** 連結用的就是它。要在**新分頁**打開：已經開著的 strudel.cc 分頁不會重讀改變過的 `#`。"}],"params":[{"name":"src","type":"String","en":"The line of rhythm.","zh":"一行節奏。"},{"name":"opts","type":"Object","optional":true,"default":"{}","en":"The options of [`toStrudel()`](#toStrudel): `{ bpm, comment, words }`.","zh":"[`toStrudel()`](#toStrudel) 的選項：`{ bpm, comment, words }`。"}],"returns":{"type":"String","en":"A URL: `'https://strudel.cc/#'` and the code from [`toStrudel()`](#toStrudel), as UTF-8 base64, URL-encoded.","zh":"一個網址：`'https://strudel.cc/#'` 加上 [`toStrudel()`](#toStrudel) 的程式碼，轉成 UTF-8 的 base64，再做網址編碼。"},"notes":[{"en":"OAV only builds the link. No Strudel code runs in, or ships with, OAV.","zh":"OAV 只是做出連結，沒有任何 Strudel 的程式碼在 OAV 裡執行，也沒有隨 OAV 一起發佈。"}],"examples":[{"code":"import { strudelUrl } from '@openav/pattern';\n\nconst url = strudelUrl('bd*4, [~ sd]*2, hh*8', { bpm: 110 });\nconsole.log(url.startsWith('https://strudel.cc/#'));            // true\nconst code = atob(decodeURIComponent(url.split('#')[1]));\nconsole.log(code);                                              // setcpm(110/4)\n                                                                // s(\"bd*4, [~ sd]*2, hh*8\")\n\nconst a = document.getElementById('app').appendChild(document.createElement('a'));\na.href = url; a.target = '_blank'; a.textContent = 'Open in Strudel';","caption":{"en":"Builds the link, decodes it again to show the code inside, and puts it on the page as a link that opens in a new tab.","zh":"做出連結，再解碼一次讓你看到裡面的程式碼，並放在頁面上，做成會在新分頁打開的連結。"},"run":"module","lang":"js"}],"related":["toStrudel","fromStrudel","mountDrums"],"keywords":{"en":["strudel","link","url","open","share","strudel.cc","base64"],"zh":["strudel","連結","網址","打開","分享","base64"]},"tbd":[{"en":"Not opened in strudel.cc from this build: the link format was read off strudel.cc's address bar on 2026-10-15 (README), and a strudel.cc change could break it.","zh":"這個版本沒有實際在 strudel.cc 打開過：連結格式是 2026-10-15 從 strudel.cc 的網址列讀出來的（見 README），strudel.cc 改版的話可能會失效。"}],"claims":[]},
{"id":"toGrid","key":"toGrid","name":"toGrid","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"toGrid(p, { bar = 1, steps = 16, seed = 0, bars = 16, words = WORDS } = {})","import":"import { toGrid } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":656,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L656"},"summary":{"en":"Shows one bar of a line of rhythm on a grid of steps, and tells you what the grid cannot hold.","zh":"把一行節奏的某一個小節畫成格子圖，並告訴你格子放不下哪些東西。"},"description":[{"en":"It is the way back from [`fromGrid()`](#fromGrid), and what the drum machine card uses to draw the cells of a line. Each hit goes to the step it is nearest to. The `lost` fields say what that cost: `offGrid` is the number of hits that sit between steps (triplets) and were moved to the nearest one. `stacked` counts two hits of one drum on one step. `varies` is true when other bars differ (`< >`, `/2`). `random` is true when the line has a `?`. `samples` is true when it has a sample number like `bd:2`. `exact` is true only when none of these happened.","zh":"它是 [`fromGrid()`](#fromGrid) 的反方向，鼓機卡就是用它畫出一行節奏的格子。每個鼓點會放到離它最近的那一格。`lost` 的欄位告訴你這樣損失了什麼：`offGrid` 是落在格子之間（三連音）而被移到最近一格的鼓點數量。`stacked` 是同一種鼓在同一格有兩下。`varies` 為 true 表示其他小節不一樣（`< >`、`/2`）。`random` 為 true 表示這一行有 `?`。`samples` 為 true 表示有像 `bd:2` 的取樣編號。只有這些都沒發生，`exact` 才是 true。"},{"en":"A line that fails to compile does not throw: you get `ok: false`.","zh":"編譯失敗的一行不會丟出錯誤，你會得到 `ok: false`。"}],"params":[{"name":"p","type":"String | Object","en":"A line of rhythm, or the result of [`compile()`](#compile).","zh":"一行節奏，或 [`compile()`](#compile) 的結果。"},{"name":"bar","type":"Number","optional":true,"default":"1","en":"Which bar to show, counting from 1.","zh":"要看哪個小節，從 1 開始數。"},{"name":"steps","type":"Number","optional":true,"default":"16","en":"How many steps the grid has.","zh":"格子有幾格。"},{"name":"seed","type":"Number","optional":true,"default":"0","en":"Decides each `?`, the same way as in `eventsInBar()`.","zh":"決定每一個 `?`，跟 `eventsInBar()` 裡的用法一樣。"},{"name":"bars","type":"Number","optional":true,"default":"16","en":"How many bars to look through to find out whether the other bars differ (`lost.varies`). At most the line's `period`, and never more than 64.","zh":"要看幾個小節，用來判斷其他小節有沒有不同（`lost.varies`）。最多看到這一行的 `period`，而且不會超過 64。"},{"name":"words","type":"Object","optional":true,"default":"WORDS","en":"Which words mean which drum, the same as in [`compile()`](#compile).","zh":"哪個字代表哪一種鼓，跟 [`compile()`](#compile) 一樣。"}],"returns":{"type":"Object","en":"`{ ok, grid, words, voices, exact, lost }`. `grid` is `{ voice: [0 or 1 × steps] }`. `words` is the word the line used for each drum. `voices` lists the drums. `exact` is true when the grid holds the bar completely. `lost` counts what it cannot: `{ offGrid, stacked, varies, random, samples }`. If the line cannot be read, `ok` is false and the rest is empty.","zh":"`{ ok, grid, words, voices, exact, lost }`。`grid` 是 `{ 鼓聲: [0 或 1，共 steps 個] }`。`words` 是這一行為每種鼓用的字。`voices` 列出鼓。`exact` 是 true 表示格子完整放得下這個小節。`lost` 數出放不下的東西：`{ offGrid, stacked, varies, random, samples }`。這一行讀不懂的話，`ok` 是 false，其他都是空的。"},"notes":[],"examples":[{"code":"import { toGrid } from '@openav/pattern';\n\nconst g = toGrid('bd*4, [~ sd]*2, hh*8');\nconsole.log(g.exact);                        // true\nconsole.log(g.grid.snare.join(''));          // 0000100000001000\nconsole.log(g.words);                        // { kick: 'bd', snare: 'sd', hat: 'hh' }","caption":{"en":"A line that fits the sixteen steps: `exact` is true, and each drum has its row of 0 and 1.","zh":"一行放得進十六格：`exact` 是 true，每種鼓有它那一排 0 和 1。"},"run":"module","lang":"js"},{"code":"import { toGrid } from '@openav/pattern';\n\nconsole.log(toGrid('bd [sd sd sd]').lost.offGrid);      // 2, the triplet sits between steps\nconsole.log(toGrid('<bd sd>', { bar: 2 }).lost.varies); // true\nconsole.log(toGrid('hh*8?').lost.random);               // true\nconsole.log(toGrid('bd:2').exact);                      // false, the sample number is lost\nconsole.log(toGrid('bd . sd').ok);                      // false, not a line of rhythm","caption":{"en":"Five lines and what each costs on a grid: triplets move, alternating bars and chance cannot be shown, and a line that does not compile has no grid at all.","zh":"五行節奏，以及各自放進格子要付出的代價：三連音會移位，輪流的小節和機率顯示不出來，編譯失敗的一行根本沒有格子。"},"run":"module","lang":"js"}],"related":["fromGrid","compile","DrumScheduler","mountDrums"],"keywords":{"en":["grid","steps","cells","to grid","step sequencer","quantize","triplet","off grid","lossy"],"zh":["格子","步進","轉成格子","三連音","對齊格子","鼓機卡","放不下"]},"tbd":[],"claims":[]},
{"id":"toStrudel","key":"toStrudel","name":"toStrudel","aliases":[],"kind":"function","package":"@openav/pattern","category":"drums","documented":true,"signature":"toStrudel(src, { bpm = null, comment = '', words = WORDS } = {})","import":"import { toStrudel } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":832,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L832"},"summary":{"en":"Turns a line of rhythm into Strudel code: OAV's drum words become Strudel's, with a tempo line and a comment if you give them.","zh":"把一行節奏轉成 Strudel 程式碼：OAV 的鼓的字換成 Strudel 的字，有給的話還會加上速度那行和註解。"},"description":[{"en":"The words become the names Strudel's drum sounds use: `kick` becomes `bd`, `pedalhat` becomes `hh`. A rotation of `n` steps or more in a Euclidean part, like `bd(3,8,10)`, is written as the rotation OAV plays (`bd(3,8,2)`), because strudel.cc would play it unrotated. A line that does not read is passed through unchanged, so the result may not play in Strudel: check it with [`compile()`](#compile) first. It never throws.","zh":"字會換成 Strudel 的鼓的名字：`kick` 變 `bd`，`pedalhat` 變 `hh`。歐幾里得的旋轉在 `n` 格以上的，像 `bd(3,8,10)`，會寫成 OAV 實際打的旋轉（`bd(3,8,2)`），因為 strudel.cc 會把它當作沒旋轉。讀不懂的一行會原樣放過去，所以結果在 Strudel 裡可能打不出來，先用 [`compile()`](#compile) 檢查。它不會丟出錯誤。"},{"en":"To open the result in the Strudel editor, use [`strudelUrl()`](#strudelUrl).","zh":"要在 Strudel 編輯器裡打開結果，用 [`strudelUrl()`](#strudelUrl)。"}],"params":[{"name":"src","type":"String","en":"The line of rhythm.","zh":"一行節奏。"},{"name":"bpm","type":"Number","optional":true,"default":"null","en":"The tempo. When it is above 0 a `setcpm(bpm/4)` line is written, because one OAV bar is one Strudel cycle of four beats.","zh":"速度。大於 0 的話會寫一行 `setcpm(bpm/4)`，因為 OAV 的一個小節就是 Strudel 的一個四拍的循環。"},{"name":"comment","type":"String","optional":true,"default":"''","en":"A line to put first as a `//` comment.","zh":"要放在最前面的一行 `//` 註解。"},{"name":"words","type":"Object","optional":true,"default":"WORDS","en":"Which words mean which drum, the same as in [`compile()`](#compile).","zh":"哪個字代表哪一種鼓，跟 [`compile()`](#compile) 一樣。"}],"returns":{"type":"String","en":"The Strudel code, one to three lines, ending in `s(\"…\")`.","zh":"Strudel 程式碼，一到三行，最後一行是 `s(\"…\")`。"},"notes":[],"examples":[{"code":"import { toStrudel } from '@openav/pattern';\n\nconsole.log(toStrudel('kick*4, pedalhat*8', { bpm: 110, comment: 'from OAV' }));\n// // from OAV\n// setcpm(110/4)\n// s(\"bd*4, hh*8\")","caption":{"en":"Logs three lines of Strudel code: the comment, the tempo, and the pattern with Strudel's names for the drums.","zh":"印出三行 Strudel 程式碼：註解、速度，和用 Strudel 的鼓名寫的節奏。"},"run":"module","lang":"js"},{"code":"import { toStrudel } from '@openav/pattern';\n\nconsole.log(toStrudel('bd(3,8,10)'));   // s(\"bd(3,8,2)\")\nconsole.log(toStrudel('bd . sd'));      // s(\"bd . sd\"), a line that does not read passes through","caption":{"en":"A rotation past the number of steps is written the way OAV plays it, and a line with a mistake is not touched.","zh":"超過格數的旋轉，會寫成 OAV 實際打的樣子，有錯的一行則不會被動到。"},"run":"module","lang":"js"}],"related":["strudelUrl","fromStrudel","compile"],"keywords":{"en":["strudel","export","strudel.cc","open in strudel","setcpm","bd","convert"],"zh":["strudel","匯出","轉成 strudel","速度","setcpm","轉換"]},"tbd":[],"claims":[]},
{"id":"WORDS","key":"WORDS","name":"WORDS","aliases":[],"kind":"constant","package":"@openav/pattern","category":"drums","documented":true,"signature":"WORDS = object, 26 keys","import":"import { WORDS } from '@openav/pattern';","source":{"file":"packages/pattern/index.js","line":31,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pattern/index.js#L31"},"summary":{"en":"The drum words a line of rhythm may use and the drum each one plays: `bd` is `kick`, `sd` is `snare`, `hh` is `hat`.","zh":"一行節奏可以用的鼓的字，以及每個字打哪一種鼓：`bd` 是 `kick`，`sd` 是 `snare`，`hh` 是 `hat`。"},"description":[{"en":"The short words are the ones TidalCycles and Strudel use: `bd` kick, `sd` and `sn` snare, `hh` hat, `oh` open hat, `cp` clap, `rim` and `rs` rim, `lt` low tom, `mt` tom, `ht` high tom, `cr` crash, `rd` ride, `cb` cowbell. Each drum's own name works too: `kick`, `snare`, `hat`, `openhat`, `pedalhat`, `clap`, `lowtom`, `tom`, `hightom`, `crash`, `ride`, `cowbell`. The values are the drum names of [`GM`](#GM).","zh":"短的字是 TidalCycles 和 Strudel 用的：`bd` 大鼓、`sd` 和 `sn` 小鼓、`hh` 鈸、`oh` 開鈸、`cp` 拍手、`rim` 和 `rs` 鼓邊、`lt` 落地鼓、`mt` 中鼓、`ht` 高音中鼓、`cr` 碎音鈸、`rd` 疊音鈸、`cb` 牛鈴。每種鼓自己的名字也能用：`kick`、`snare`、`hat`、`openhat`、`pedalhat`、`clap`、`lowtom`、`tom`、`hightom`、`crash`、`ride`、`cowbell`。值就是 [`GM`](#GM) 裡的鼓的名字。"},{"en":"It is the default of the `words` option of [`compile()`](#compile), [`toGrid()`](#toGrid) and [`toStrudel()`](#toStrudel). To teach a line a new word, pass a copy with your word added.","zh":"它是 [`compile()`](#compile)、[`toGrid()`](#toGrid)、[`toStrudel()`](#toStrudel) 的 `words` 選項的預設值。要讓一行節奏認得新的字，就傳一份加了你的字的複本。"}],"value":{"type":"Object","en":"A frozen object of 26 `{ word: voice }` pairs. Keys are lowercase, and [`compile()`](#compile) ignores the case of what you write.","zh":"一個凍結的物件，有 26 組 `{ 字: 鼓聲 }`。鍵是小寫，[`compile()`](#compile) 不管你寫的大小寫。"},"params":[],"notes":[],"examples":[{"code":"import { WORDS, compile } from '@openav/pattern';\n\nconsole.log(WORDS.bd, WORDS.sn, WORDS.rs, WORDS.mt);   // kick snare rim tom\nconsole.log(Object.keys(WORDS).length);                 // 26\n\nconst mine = compile('boom tss boom tss', { words: { ...WORDS, boom: 'kick', tss: 'hat' } });\nconsole.log(mine.ok, mine.voices.join(' '));            // true kick hat","caption":{"en":"Looks up a few words, counts them, and teaches a line two words of your own by spreading `WORDS` into a new object.","zh":"查幾個字、數有幾個，再把 `WORDS` 展開到新物件裡，讓一行節奏認得兩個你自己的字。"},"run":"module","lang":"js"}],"related":["SHORT","compile","GM","voiceForNote"],"keywords":{"en":["words","drum names","bd","sd","hh","vocabulary","kick","snare","hat","sound names"],"zh":["字","鼓的名字","詞彙","大鼓","小鼓","鈸","bd sd hh"]},"tbd":[],"claims":[]},
{"id":"barStarted","key":"barStarted","name":"barStarted","aliases":[],"kind":"event","package":"oav-p5","category":"timeline","documented":true,"signature":"function barStarted(bar) {}","source":{"file":"p5/oav-p5.js","line":1490,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1490"},"summary":{"en":"An event you define: it is called at the first step of every bar of the drum machine, with the bar number.","zh":"一個由你定義的事件函式：鼓機每一小節的第一格就會呼叫它，帶著小節的號碼。"},"description":[{"en":"Define `function barStarted(bar) { … }` to do something once per bar: `function barStarted(bar) { drift += 9; }`. It is called in the frame where the first step of the bar is heard, right before `draw()`, and the first call is `barStarted(1)` when the machine starts. Within a step, [`cueFired()`](#cueFired) comes before it and [`stepStarted()`](#stepStarted) after.","zh":"定義 `function barStarted(bar) { … }`，就可以每小節做一次事：`function barStarted(bar) { drift += 9; }`。它是在你聽到那一小節第一格的那個畫面、`draw()` 之前被呼叫的，鼓機啟動時的第一次呼叫是 `barStarted(1)`。在同一格裡，[`cueFired()`](#cueFired) 在它之前，[`stepStarted()`](#stepStarted) 在它之後。"}],"params":[{"name":"bar","type":"Number","en":"The bar that starts, counted from 1.","zh":"剛開始的小節，從 1 算起。"}],"notes":[{"en":"While the tab is hidden it is still called once per bar, but nothing is told twice when the tab comes back. [`stepStarted()`](#stepStarted) and the hits are not told in that time.","zh":"分頁被藏起來的時候，每一小節仍然會呼叫一次，分頁回來的時候也不會補呼叫漏掉的。這段時間不會呼叫 [`stepStarted()`](#stepStarted)，也不會通知打擊。"}],"examples":[{"code":"let shade = 200;\n\nfunction setup() {\n  createCanvas(400, 300);\n  colorMode(HSB, 360, 100, 100);\n  startDrums('four on floor', 150);\n}\n\nfunction barStarted(bar) {\n  shade = (shade + 40) % 360;   // a new colour every bar\n}\n\nfunction draw() {\n  background(shade, 60, 80);\n  fill(0, 0, 100);\n  textAlign(CENTER, CENTER);\n  textSize(48);\n  text(drumBar, width / 2, height / 2);\n}","caption":{"en":"The background changes colour at the start of every bar, while the number counts the bars.","zh":"每一小節的開頭背景換一個顏色，數字數著現在是第幾小節。"},"run":"sketch","lang":"js"}],"related":["stepStarted","cueFired","drumBar","drumHit","OAVp5.drums.on"],"keywords":{"en":["bar started","new bar","bar event","downbeat","measure","callback"],"zh":["小節開始","新的一小節","小節事件","強拍","callback"]},"tbd":[],"claims":[]},
{"id":"cue","key":"cue","name":"cue","aliases":[],"kind":"function","package":"oav-p5","category":"timeline","documented":true,"signature":"cue(bar, name, [every])","source":{"file":"p5/oav-p5.js","line":1733,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1733"},"summary":{"en":"Plans something for a bar: at bar `bar` the function `cueFired(name, bar)` is called and `cueName` becomes `name`; with `every`, again every that many bars.","zh":"替某一小節安排事情：到了第 `bar` 小節，就呼叫 `cueFired(name, bar)`，`cueName` 變成 `name`，給了 `every`，之後每隔那麼多小節再來一次。"},"description":[{"en":"The drum machine counts the bars, so it can run a plan: `cue(1, 'calm', 12); cue(5, 'bloom', 12); cue(9, 'storm', 12);` in `setup()` makes three scenes that come round every 12 bars. When a cue is due, [`cueFired(name, bar)`](#cueFired) is called and [`cueName`](#cueName) holds the name; read it in `draw()` if you prefer that to a callback. Several cues on one step fire in the order you declared them, before [`barStarted()`](#barStarted), [`stepStarted()`](#stepStarted) and the hits of that step.","zh":"鼓機會數小節，所以可以照著計畫走：在 `setup()` 裡寫 `cue(1, 'calm', 12); cue(5, 'bloom', 12); cue(9, 'storm', 12);`，就是三個場景，每 12 小節輪一圈。cue 到的時候會呼叫 [`cueFired(name, bar)`](#cueFired)，[`cueName`](#cueName) 裡也會是它的名字，不想用 callback 的話，在 `draw()` 裡讀它就好。同一格有好幾個 cue 的時候，會依你宣告的順序發動，並且在 [`barStarted()`](#barStarted)、[`stepStarted()`](#stepStarted) 和那一格的打擊之前。"},{"en":"Calling `cue()` twice with the same values adds nothing, so it is safe in `draw()` too. Counting starts with [`startDrums()`](#startDrums), and a cue only fires when the machine passes its place: declare the cues right after `startDrums()` in `setup()` so that a cue at bar 1 is not missed. After the machine is stopped and started again, the cues fire again from bar 1.","zh":"用同樣的值呼叫兩次 `cue()` 不會重複加，所以放在 `draw()` 裡也安全。計數從 [`startDrums()`](#startDrums) 開始，cue 只在鼓機走過它的位置時才發動：請在 `setup()` 裡緊接著 `startDrums()` 宣告，第 1 小節的 cue 才不會漏掉。鼓機停止再啟動之後，cue 會從第 1 小節重新發動。"}],"params":[{"name":"bar","type":"Number","en":"The bar, counted from 1 (the first bar). A fraction is a place inside a bar: 4.5 is halfway through bar 4 (the nearest of the 16 steps is used). A value below 1, or one that is not a number, prints a warning in the console and adds nothing.","zh":"第幾小節，第一小節是 1。小數表示小節裡的位置：4.5 是第 4 小節的一半（會取最接近的那一格，共 16 格）。小於 1 或不是數字的值，會在主控台印一個警告，什麼都不會加。"},{"name":"name","type":"Any","en":"What to call it, usually a string. It is passed to [`cueFired()`](#cueFired) and becomes [`cueName`](#cueName).","zh":"給它取的名字，通常是字串。會傳給 [`cueFired()`](#cueFired)，並成為 [`cueName`](#cueName)。"},{"name":"every","type":"Number","optional":true,"default":"0","en":"Repeat every this many bars after the first time (fractions work, at least one step). 0 or left out: only once.","zh":"第一次之後，每隔這麼多小節再來一次（小數可以，至少一格）。0 或不傳：只發動一次。"}],"returns":{"type":"undefined","en":"Nothing. [`OAVp5.drums.cue()`](#OAVp5.drums.cue) is the same function and returns the cue it made.","zh":"沒有回傳值。[`OAVp5.drums.cue()`](#OAVp5.drums.cue) 是同一個函式，它會回傳建立出來的 cue。"},"notes":[{"en":"A cue declared after its bar has passed fires at its next repeat if it has an `every`, otherwise never until the machine is started again. Without a running machine no cue fires at all.","zh":"如果 cue 是在它的那一小節已經過了之後才宣告的，有 `every` 就會在下一次重複時發動，沒有的話，要等到鼓機重新啟動才會發動。鼓機沒在跑的時候，任何 cue 都不會發動。"}],"examples":[{"code":"let scene = 'calm';\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 140);\n  cue(1, 'calm', 4);    // bars 1, 5, 9 ...\n  cue(3, 'storm', 4);   // bars 3, 7, 11 ...\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction cueFired(name, bar) {\n  scene = name;\n}\n\nfunction draw() {\n  background(scene === 'storm' ? color(130, 30, 40) : color(20, 40, 100));\n  fill(255);\n  text(scene + ' (bar ' + drumBar + ')', width / 2, height / 2);\n}","caption":{"en":"The picture swaps between \"calm\" (blue) and \"storm\" (red) every two bars, and the text names the scene and the bar.","zh":"畫面每兩小節在 \"calm\"（藍色）和 \"storm\"（紅色）之間切換，文字說明現在的場景和小節。"},"run":"sketch","lang":"js"},{"code":"let flash = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 130);\n  cue(2.5, 'flash', 2);   // halfway through bar 2, then every 2 bars\n  textAlign(CENTER, CENTER);\n}\n\nfunction cueFired(name, bar) {\n  if (name === 'flash') flash = 1;\n}\n\nfunction draw() {\n  flash *= 0.92;\n  background(20 + flash * 200);\n  fill(255 - flash * 255);\n  text('a flash halfway through every second bar (bar ' + drumBar + ')', width / 2, height / 2);\n}","caption":{"en":"The screen flashes in the middle of bar 2 and again in the middle of every second bar after it.","zh":"畫面在第 2 小節的中間閃一下，之後每隔一小節的中間又閃一次。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('latin', 120);\n  textAlign(CENTER, CENTER);\n  textSize(28);\n}\n\nfunction draw() {\n  cue(2, 'drop', 4);   // the same call every frame adds nothing\n  background(20);\n  fill(255);\n  text(cueName === '' ? 'waiting for the first cue' : 'last cue: ' + cueName, width / 2, height / 2);\n}","caption":{"en":"cue() inside draw() is safe: the text changes to \"last cue: drop\" at bar 2 and stays that way.","zh":"在 draw() 裡呼叫 cue() 也沒問題：到了第 2 小節，文字變成 \"last cue: drop\" 就一直是這樣。"},"run":"sketch","lang":"js"}],"related":["cueFired","cueName","barStarted","drumBar","startDrums","OAVp5.drums.cue"],"keywords":{"en":["cue","schedule","timeline","scene","at bar","plan","automation","sections","repeat"],"zh":["cue","時間軸","場景","排程","第幾小節","安排","段落","重複"]},"tbd":[],"claims":[{"before":"cue(1, 'a'); cue(1, 'a'); cue(3, 'b', 4)","expr":"OAVp5.drums.cues.length","equals":2},{"before":"cue(0, 'never')","expr":"OAVp5.drums.cues.length","equals":0}]},
{"id":"cueFired","key":"cueFired","name":"cueFired","aliases":[],"kind":"event","package":"oav-p5","category":"timeline","documented":true,"signature":"function cueFired(name, bar) {}","source":{"file":"p5/oav-p5.js","line":1487,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1487"},"summary":{"en":"An event you define: it is called when the bar of a cue declared with `cue()` has come, with the cue's name and the bar.","zh":"一個由你定義的事件函式：用 `cue()` 宣告的 cue，到了它的那一小節就會呼叫它，帶著 cue 的名字和小節。"},"description":[{"en":"Define `function cueFired(name, bar) { … }` and it is called at the step where a cue is due, before [`barStarted()`](#barStarted), [`stepStarted()`](#stepStarted) and the hits of that step. A good place to change a scene: `function cueFired(name) { look = SCENES[name]; }`. If you only need the latest name, read [`cueName`](#cueName) in `draw()` instead.","zh":"定義 `function cueFired(name, bar) { … }`，cue 該發動的那一格就會呼叫它，順序在 [`barStarted()`](#barStarted)、[`stepStarted()`](#stepStarted) 和那一格的打擊之前。很適合用來換場景：`function cueFired(name) { look = SCENES[name]; }`。如果你只需要最近的名字，在 `draw()` 裡讀 [`cueName`](#cueName) 就好。"}],"params":[{"name":"name","type":"Any","en":"The name you gave to [`cue()`](#cue).","zh":"你給 [`cue()`](#cue) 的名字。"},{"name":"bar","type":"Number","en":"The bar the cue fired in, counted from 1. For a cue in the middle of a bar (4.5) it is the whole bar it falls in (4).","zh":"cue 發動時所在的小節，從 1 算起。小節中間的 cue（4.5）給的是它落在的那一整小節（4）。"}],"notes":[{"en":"Cues keep firing while the tab is hidden, so the scene is right when you come back. Several cues on one step call it once each, in the order they were declared.","zh":"分頁被藏起來的時候，cue 照樣會發動，所以回來的時候場景是對的。同一格有好幾個 cue，會依宣告的順序各呼叫一次。"}],"examples":[{"code":"let scene = '-';\nlet when = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 150);\n  cue(1, 'intro');\n  cue(3, 'verse', 4);\n  cue(5, 'chorus', 4);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction cueFired(name, bar) {\n  scene = name + ' (bar ' + bar + ')';\n  when = millis();\n}\n\nfunction draw() {\n  background(20 + max(0, 1 - (millis() - when) / 400) * 120);   // a flash on every cue\n  fill(255);\n  text(scene, width / 2, height / 2);\n}","caption":{"en":"The screen flashes and names the scene each time a cue fires: intro once, then verse and chorus take turns.","zh":"每次 cue 發動，畫面閃一下並顯示場景名稱：intro 只有一次，之後 verse 和 chorus 輪流。"},"run":"sketch","lang":"js"}],"related":["cue","cueName","barStarted","stepStarted","OAVp5.drums.on"],"keywords":{"en":["cue fired","cue event","scene change","callback","timeline event"],"zh":["cue 發動","cue 事件","換場景","callback","時間軸事件"]},"tbd":[],"claims":[]},
{"id":"cueName","key":"cueName","name":"cueName","aliases":["OAVp5.drums.lastCue"],"kind":"variable","package":"oav-p5","category":"timeline","documented":true,"signature":"cueName","source":{"file":"p5/oav-p5.js","line":317,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L317"},"summary":{"en":"The name of the last cue that fired, or an empty string before the first one.","zh":"最近一個發動的 cue 的名字，第一個發動之前是空字串。"},"description":[{"en":"Read it in `draw()` instead of waiting for [`cueFired()`](#cueFired): `look = SCENES[cueName];`. It is the name of the last cue only, so it is the right thing for \"which scene am I in\", and `cueFired()` is the right thing for \"something just began\". Cues are declared with [`cue()`](#cue).","zh":"可以在 `draw()` 裡直接讀，不用等 [`cueFired()`](#cueFired)：`look = SCENES[cueName];`。它只是最近一個 cue 的名字，所以回答「我現在在哪個場景」用它最適合，「某件事剛剛開始」則適合用 `cueFired()`。cue 用 [`cue()`](#cue) 宣告。"}],"value":{"type":"String","range":"'' or a cue name","en":"Changes when a cue fires, and keeps the name until the next one. It is `''` at the start.","zh":"cue 發動的時候改變，之後一直是那個名字，直到下一個。一開始是 `''`。"},"params":[],"notes":[{"en":"A sketch that declares its own variable called `cueName` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.drums.lastCue` instead.","zh":"如果草圖本來就宣告了自己的 `cueName` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.drums.lastCue`。"}],"examples":[{"code":"const SCENES = { calm: '#3b5bdb', bloom: '#e64980', storm: '#495057' };\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 150);\n  cue(1, 'calm', 6);\n  cue(3, 'bloom', 6);\n  cue(5, 'storm', 6);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction draw() {\n  background(SCENES[cueName] || '#111');   // nothing yet: dark\n  fill(255);\n  text(cueName, width / 2, height / 2);\n}","caption":{"en":"The background and the word follow the last cue: blue \"calm\", pink \"bloom\", grey \"storm\", every six bars.","zh":"背景和文字跟著最近一個 cue 走：藍色 \"calm\"、粉紅色 \"bloom\"、灰色 \"storm\"，每六小節輪一圈。"},"run":"sketch","lang":"js"}],"related":["cue","cueFired","drumBar","startDrums"],"keywords":{"en":["cue name","scene","current scene","last cue","section name"],"zh":["cue 名字","場景","目前場景","最近的 cue","段落名稱"]},"tbd":[],"claims":[{"expr":"cueName","equals":""}]},
{"id":"drumBar","key":"drumBar","name":"drumBar","aliases":["OAVp5.drums.bar"],"kind":"variable","package":"oav-p5","category":"timeline","documented":true,"signature":"drumBar","source":{"file":"p5/oav-p5.js","line":316,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L316"},"summary":{"en":"Which bar the drum machine is in, counted from 1 when it started.","zh":"鼓機現在在第幾小節，從啟動的時候起算，第一小節是 1。"},"description":[{"en":"A bar is 16 steps, four beats. Use it to give a piece a shape: `floor((drumBar - 1) / 4) % 3` is the section number when you change every four bars. To make something happen at an exact bar, [`cue()`](#cue) is neater than testing `drumBar` in `draw()`. [`barStarted()`](#barStarted) is the function that is called at the start of each bar.","zh":"一小節是 16 格、四拍。用它替作品安排結構：每四小節換一段的話，`floor((drumBar - 1) / 4) % 3` 就是現在是第幾段。要讓某件事剛好發生在某一小節，[`cue()`](#cue) 比在 `draw()` 裡判斷 `drumBar` 更簡潔。[`barStarted()`](#barStarted) 則是每小節開頭會被呼叫的函式。"}],"value":{"type":"Number","range":"0, 1, 2, 3 …","en":"0 before the machine has played its first step, then 1, 2, 3 … with no upper limit. It keeps its last value when the machine stops.","zh":"鼓機播第一格之前是 0，之後是 1、2、3 ……，沒有上限。鼓機停止之後停在最後的值。"},"params":[],"needs":{"en":"Start the machine with [`startDrums()`](#startDrums). Stopping and starting again counts from bar 1 again.","zh":"先用 [`startDrums()`](#startDrums) 啟動鼓機。停止之後再啟動，會從第 1 小節重新算起。"},"notes":[{"en":"A sketch that declares its own variable called `drumBar` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.drums.bar` instead.","zh":"如果草圖本來就宣告了自己的 `drumBar` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.drums.bar`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 150);\n  textAlign(CENTER, CENTER);\n  textSize(64);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(drumBar, width / 2, height / 2);\n}","caption":{"en":"A big number that goes up by one at the start of every bar.","zh":"一個大數字，每一小節開頭加一。"},"run":"sketch","lang":"js"},{"code":"const SECTIONS = ['#3b5bdb', '#e8590c', '#2f9e44'];\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 160);\n  noStroke();\n}\n\nfunction draw() {\n  const section = floor((max(drumBar, 1) - 1) / 2) % SECTIONS.length;   // a new section every 2 bars\n  background(SECTIONS[section]);\n  fill(255);\n  text('bar ' + drumBar + ', section ' + (section + 1), 20, 30);\n}","caption":{"en":"The background changes colour every two bars and the text names the bar and the section.","zh":"背景每兩小節換一個顏色，文字說明現在是第幾小節、第幾段。"},"run":"sketch","lang":"js"}],"related":["barStarted","drumStep","cue","OAVp5.drums.count","startDrums"],"keywords":{"en":["bar","measure","bar number","section","count bars","timeline","drums"],"zh":["小節","第幾小節","段落","計數小節","時間軸","鼓機"]},"tbd":[],"claims":[{"expr":"drumBar","equals":0},{"before":"startDrums('four on floor', 300)","expr":"drumBar","min":0,"moves":true}]},
{"id":"drumStep","key":"drumStep","name":"drumStep","aliases":["OAVp5.drums.step"],"kind":"variable","package":"oav-p5","category":"timeline","documented":true,"signature":"drumStep","source":{"file":"p5/oav-p5.js","line":316,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L316"},"summary":{"en":"Which of the 16 steps of the bar the drum machine just played, from 0 to 15.","zh":"鼓機剛剛播到這一小節 16 格裡的第幾格，從 0 到 15。"},"description":[{"en":"A bar has 16 steps (sixteenth notes) and a beat has 4, so `drumStep % 4 === 0` is a beat and `drumStep === 0` the start of a bar. At 120 bpm a step lasts 125 ms. Use it to light a cell or pick a frame: `fill(i === drumStep ? 255 : 80)`. If you would rather be called on each step, define [`stepStarted()`](#stepStarted). For something that moves smoothly instead of in 16 jumps, use [`drumPhase`](#drumPhase).","zh":"一小節有 16 格（十六分音符），一拍有 4 格，所以 `drumStep % 4 === 0` 就是正拍，`drumStep === 0` 是小節的開頭。速度 120 的時候一格是 125 毫秒。可以用它讓某一格亮起來或挑選畫面：`fill(i === drumStep ? 255 : 80)`。如果你比較想在每一格被呼叫，就定義 [`stepStarted()`](#stepStarted)。想要平滑移動，不要 16 次跳動的話，用 [`drumPhase`](#drumPhase)。"}],"value":{"type":"Number","range":"0..15","en":"Changes in the frame in which a step is heard, right before `draw()`. 0 at the start, and it keeps its last value when the machine stops.","zh":"在你聽到那一格的那個畫面、`draw()` 之前改變。一開始是 0，鼓機停止之後停在最後的值。"},"params":[],"needs":{"en":"Start the machine with [`startDrums()`](#startDrums). Before that, and when the machine is stopped, the value is 0 or the last one, so check [`drumsAreOn`](#drumsAreOn) if the difference matters.","zh":"先用 [`startDrums()`](#startDrums) 啟動鼓機。在那之前、以及鼓機停止之後，值是 0 或最後的值，這個差別重要的話，要檢查 [`drumsAreOn`](#drumsAreOn)。"},"notes":[{"en":"A sketch that declares its own variable called `drumStep` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.drums.step` instead. While the tab is hidden the clock keeps counting but nothing else is told.","zh":"如果草圖本來就宣告了自己的 `drumStep` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.drums.step`。分頁被藏起來的時候，時鐘照樣在數，但不會通知任何事件。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  for (let i = 0; i < 16; i++) {\n    fill(drumsAreOn && i === drumStep ? 255 : 70);\n    rect(20 + i * 22, 130, 18, 40);   // sixteen cells, the current one lights up\n  }\n}","caption":{"en":"Sixteen cells in a row with the current step lit, one cell at a time, from left to right.","zh":"一排十六個格子，現在的那一格亮著，從左到右一格一格走。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 110);\n  textAlign(CENTER, CENTER);\n  textSize(18);\n}\n\nfunction draw() {\n  background(drumStep % 4 === 0 ? 70 : 20);   // a flash on every beat\n  fill(255);\n  text('step ' + drumStep + ' of 16' + (drumStep === 0 ? '  (bar starts)' : ''), width / 2, height / 2);\n}","caption":{"en":"The background lights up on every beat (steps 0, 4, 8, 12) and the text counts the steps.","zh":"每一拍（第 0、4、8、12 格）背景亮一下，文字數著現在是第幾格。"},"run":"sketch","lang":"js"}],"related":["stepStarted","drumBar","drumPhase","beatPhase","drumsAreOn","startDrums"],"keywords":{"en":["step","drum step","sixteenth","16 steps","sequencer step","current step","counter","beat"],"zh":["格","第幾格","十六分音符","16 格","目前這一格","計數","鼓機"]},"tbd":[],"claims":[{"expr":"drumStep","equals":0},{"before":"startDrums('four on floor', 120)","expr":"drumStep","min":0,"max":15,"moves":true}]},
{"id":"stepStarted","key":"stepStarted","name":"stepStarted","aliases":[],"kind":"event","package":"oav-p5","category":"timeline","documented":true,"signature":"function stepStarted(step, bar) {}","source":{"file":"p5/oav-p5.js","line":1491,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1491"},"summary":{"en":"An event you define: it is called at every one of the 16 steps of the drum machine, with the step and the bar.","zh":"一個由你定義的事件函式：鼓機 16 格裡的每一格都會呼叫它，帶著這一格和小節。"},"description":[{"en":"Define `function stepStarted(step, bar) { … }` for something on every step: `if (step % 4 === 0) ping = 1;` is a pulse on the beat. It is called in the frame where the step is heard, right before `draw()`, after [`barStarted()`](#barStarted) and before the hits of that step. [`drumStep`](#drumStep) holds the same number if you would rather read than be called.","zh":"定義 `function stepStarted(step, bar) { … }`，每一格都能做事：`if (step % 4 === 0) ping = 1;` 就是每一拍脈動一下。它是在你聽到那一格的那個畫面、`draw()` 之前被呼叫的，在 [`barStarted()`](#barStarted) 之後、那一格的打擊之前。如果你比較想讀值而不是被呼叫，[`drumStep`](#drumStep) 裡是同一個數字。"}],"params":[{"name":"step","type":"Number","en":"The step that starts, 0 to 15. Every 4 steps is a beat.","zh":"剛開始的那一格，0 到 15。每 4 格是一拍。"},{"name":"bar","type":"Number","en":"The bar it is in, counted from 1.","zh":"它所在的小節，從 1 算起。"}],"notes":[{"en":"It is not called while the tab is hidden, so a sketch that adds a particle per step does not burst when you come back.","zh":"分頁被藏起來的時候不會呼叫它，所以每一格就加一個粒子的草圖，在你回來的時候不會一口氣冒出一大堆。"}],"examples":[{"code":"let ping = 0;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);\n  noStroke();\n}\n\nfunction stepStarted(step, bar) {\n  if (step % 4 === 0) ping = 1;   // on every beat\n}\n\nfunction draw() {\n  background(20);\n  ping *= 0.9;\n  fill(255, 160, 80);\n  circle(width / 2, height / 2, 40 + ping * 120);\n}","caption":{"en":"A circle that pulses on every beat (every fourth step) and shrinks back in between.","zh":"一個圓，每一拍（每四格）脹一下，中間縮回去。"},"run":"sketch","lang":"js"}],"related":["drumStep","barStarted","drumHit","cueFired","OAVp5.drums.on"],"keywords":{"en":["step started","step event","every step","sixteenth","tick","callback","beat"],"zh":["每一格","格開始","步進事件","十六分音符","callback","拍子"]},"tbd":[],"claims":[]},
{"id":"OAVp5.drums.count","key":"OAVp5.drums.count","name":"OAVp5.drums.count","aliases":[],"kind":"variable","package":"oav-p5","category":"timeline","documented":true,"signature":"OAVp5.drums.count","source":{"file":"p5/oav-p5.js","line":1437,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1437"},"summary":{"en":"How many steps the drum machine has played since it started, from 0; -1 before the first one.","zh":"鼓機從啟動到現在一共播了幾格，從 0 開始算，第一格之前是 -1。"},"description":[{"en":"It is the single clock the others are cut from: `drumBar` is `floor(count / 16) + 1`, `drumStep` is `count % 16`, and [`cue()`](#cue) fires when `count` reaches the step of its bar. It is handy when you want one number that only grows: a position on a very long timeline, or an index into a list.","zh":"其他的數字都是從它切出來的：`drumBar` 是 `floor(count / 16) + 1`，`drumStep` 是 `count % 16`，[`cue()`](#cue) 是在 `count` 走到它那一小節的位置時發動。想要一個只會一直變大的數字的時候很好用，例如很長的時間軸上的位置，或是清單的索引。"}],"value":{"type":"Number","range":"-1, 0, 1, 2 …","en":"Goes up by one at every step and never goes back to 0 at a new bar. It starts again from 0 when the machine is started again.","zh":"每一格加一，到了新的小節也不會歸零。機器重新啟動的時候，才會從 0 重新算。"},"params":[],"needs":{"en":"Start the machine with [`startDrums()`](#startDrums); it is -1 until the first step has played.","zh":"先用 [`startDrums()`](#startDrums) 啟動鼓機，第一格播出來之前都是 -1。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('half time', 140);\n  textAlign(CENTER, CENTER);\n  textSize(28);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const n = OAVp5.drums.count;\n  text('step ' + n + '  =  bar ' + (floor(n / 16) + 1) + ', step ' + (n % 16), width / 2, height / 2);\n}","caption":{"en":"The running step count and the bar and step it is made of.","zh":"一路累加的格數，以及它換算成的小節和格。"},"run":"sketch","lang":"js"}],"related":["drumBar","drumStep","cue","startDrums"],"keywords":{"en":["count","step count","running count","total steps","ticks","timeline position"],"zh":["計數","累計格數","總格數","時間軸位置"]},"tbd":[],"claims":[{"expr":"OAVp5.drums.count","equals":-1}]},
{"id":"OAVp5.drums.cue","key":"OAVp5.drums.cue","name":"OAVp5.drums.cue","aliases":[],"kind":"function","package":"oav-p5","category":"timeline","documented":true,"signature":"OAVp5.drums.cue(bar, name, [every])","source":{"file":"p5/oav-p5.js","line":1439,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1439"},"summary":{"en":"The same as `cue(bar, name, every)`, but it returns the cue it made (or the one that was already there), and `null` when `bar` is not valid.","zh":"跟 `cue(bar, name, every)` 一樣，但會回傳建立出來的 cue（或原本就有的那個），`bar` 無效的時候回傳 `null`。"},"description":[{"en":"Use it when you want to hold on to the cue, for example to find out which step it fires at. For a plain plan, [`cue()`](#cue) is shorter. The list of every cue is [`OAVp5.drums.cues`](#OAVp5.drums.cues).","zh":"想拿著這個 cue 的時候用它，例如想知道它在第幾格發動。只是安排計畫的話，[`cue()`](#cue) 比較短。所有 cue 的清單在 [`OAVp5.drums.cues`](#OAVp5.drums.cues)。"}],"params":[{"name":"bar","type":"Number","en":"The bar, counted from 1; 4.5 is halfway through bar 4. See [`cue()`](#cue).","zh":"第幾小節，從 1 算起，4.5 是第 4 小節的一半。見 [`cue()`](#cue)。"},{"name":"name","type":"Any","en":"What to call the cue.","zh":"這個 cue 的名字。"},{"name":"every","type":"Number","optional":true,"en":"Repeat every this many bars; 0 or left out: once.","zh":"每隔幾小節再來一次，0 或不傳：只一次。"}],"returns":{"type":"Object | null","en":"The cue: `{ bar, name, every, at, period, key }` with `at` and `period` counted in steps. `null` if `bar` is below 1 or not a number. A repeat of an earlier call gives back the cue that exists.","zh":"這個 cue：`{ bar, name, every, at, period, key }`，其中 `at` 和 `period` 以格為單位。`bar` 小於 1 或不是數字就是 `null`。重複前面呼叫過的，會回傳已經存在的那個 cue。"},"notes":[],"examples":[{"code":"let c = null;\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  c = OAVp5.drums.cue(3.5, 'half', 4);\n  textAlign(CENTER, CENTER);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(c ? 'cue \"' + c.name + '\" at step ' + c.at + ', every ' + c.period + ' steps' : 'no cue', width / 2, height / 2);\n}","caption":{"en":"Bar 3.5 is step 40 of the count, and \"every 4 bars\" is 64 steps.","zh":"第 3.5 小節是累計的第 40 格，「每 4 小節」是 64 格。"},"run":"sketch","lang":"js"}],"related":["cue","OAVp5.drums.cues","cueFired"],"keywords":{"en":["cue","cue object","schedule","timeline","return value"],"zh":["cue","回傳 cue","時間軸","排程"]},"tbd":[],"claims":[{"before":"OAVp5.drums.cue(1, 'a')","expr":"OAVp5.drums.cues.length","equals":1}]},
{"id":"OAVp5.drums.cues","key":"OAVp5.drums.cues","name":"OAVp5.drums.cues","aliases":[],"kind":"variable","package":"oav-p5","category":"timeline","documented":true,"signature":"OAVp5.drums.cues","source":{"file":"p5/oav-p5.js","line":1437,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1437"},"summary":{"en":"The list of every cue declared so far, in the order they were declared.","zh":"到目前為止宣告過的所有 cue，照宣告的順序排。"},"description":[{"en":"Read it to show a plan on screen or to check what is declared. It is the machine's own list, so treat it as read-only; declare cues with [`cue()`](#cue).","zh":"想把計畫顯示在畫面上，或檢查宣告了哪些 cue 的時候讀它。它是鼓機自己的清單，請當成唯讀，要宣告 cue 請用 [`cue()`](#cue)。"}],"value":{"type":"Array<Object>","en":"Each item is `{ bar, name, every, at, period, key }`: the values you passed, and `at` and `period` counted in steps. An empty list at the start.","zh":"每一項是 `{ bar, name, every, at, period, key }`：你傳進去的值，以及以格為單位的 `at` 和 `period`。一開始是空的。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  cue(1, 'calm', 8);\n  cue(5, 'storm', 8);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  OAVp5.drums.cues.forEach((c, i) => {\n    text('bar ' + c.bar + (c.every ? ' (every ' + c.every + ')' : '') + ': ' + c.name, 20, 30 + i * 24);\n  });\n}","caption":{"en":"The plan as a list: one line per cue.","zh":"把計畫列成清單：每個 cue 一行。"},"run":"sketch","lang":"js"}],"related":["cue","OAVp5.drums.cue","cueName"],"keywords":{"en":["cues","cue list","plan","all cues","timeline"],"zh":["cue 清單","所有 cue","計畫","時間軸"]},"tbd":[],"claims":[{"expr":"OAVp5.drums.cues.length","equals":0}]},
{"id":"Director","key":"Director","name":"Director","aliases":[],"kind":"class","package":"@openav/score","category":"timeline","documented":true,"signature":"new Director({ score, modules = [], api = {}, onStatus = null, onCue = null, maxStep = 2 } = {})","import":"import { Director } from '@openav/score';","source":{"file":"packages/score/director.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/score/director.js#L38"},"summary":{"en":"Runs a score's segment modules: enters each once, updates it every frame, exits it once, fires cues, and switches off a module that crashes without stopping the show.","zh":"執行總譜的段落模組：每個模組進場一次、每個畫面更新、退場一次，觸發 cue，而且某個模組當掉時只關掉它，不會讓演出停下來。"},"description":[{"en":"A **segment module** is the code that performs one segment (the dawn, the storm, the finale). The director calls its hooks as the show moves through the score:\n\n```js\n{\n  id: 'dusk',                                  // the id of the segment it performs\n  enter(api, ctx)  {},                         // the segment began, or a seek landed inside it\n  update(api, ctx) {},                         // every frame while the segment (and its linger) is current\n  exit(api, ctx)   {},                         // it ended, or a seek left it: give back what you took\n  param(key, value, ctx) { return value; },    // optional: rewrite a timeline param meanwhile\n  cue(name, ctx)   {},                         // optional: one of its cues was just passed\n  reset(api)       {},                         // optional: R was pressed, forget everything\n}\n```","zh":"**段落模組**就是負責表演一個段落（黎明、暴風雨、終曲）的程式碼。演出走過總譜的時候，導演會呼叫它的掛鉤：\n\n```js\n{\n  id: 'dusk',                                  // the id of the segment it performs\n  enter(api, ctx)  {},                         // the segment began, or a seek landed inside it\n  update(api, ctx) {},                         // every frame while the segment (and its linger) is current\n  exit(api, ctx)   {},                         // it ended, or a seek left it: give back what you took\n  param(key, value, ctx) { return value; },    // optional: rewrite a timeline param meanwhile\n  cue(name, ctx)   {},                         // optional: one of its cues was just passed\n  reset(api)       {},                         // optional: R was pressed, forget everything\n}\n```"},{"en":"Every hook gets a `ctx`: `p` (progress through the segment, 0 to 1; above 1 during `linger`; exactly 1 while holding), `t` (seconds into the segment), `dt`, `dur`, `T` (show time), `segment`, `cue(name)` and `cueT(name)` (where a cue sits, as a progress and in show seconds), `phase` (`'active'`, `'linger'`, or `'failed'` inside `exit()` after a crash), `holding`, `held` (seconds waited at a hold: a clock for ambient motion while `T` is frozen), `playing` and `api`.","zh":"每個掛鉤都會收到一個 `ctx`：`p`（這一段走了多少，0 到 1，餘韻期間大於 1，hold 的時候剛好是 1）、`t`（段內第幾秒）、`dt`、`dur`、`T`（演出時間）、`segment`、`cue(name)` 和 `cueT(name)`（cue 的位置，一個是進度、一個是演出秒數）、`phase`（`'active'`、`'linger'`，崩潰後的 `exit()` 裡是 `'failed'`）、`holding`、`held`（在 hold 上等了幾秒，演出時間 `T` 凍結時可以拿它當環境動態的時鐘）、`playing` 和 `api`。"},{"en":"A module is entered once and exited once, however the show got there: a seek into the middle of a segment calls `enter` with the right `p`, so a module must be able to start from the middle. A module that throws is switched off alone: the director calls its `exit()` once with `phase: 'failed'`, logs one `console.error`, and never calls it again; the other modules and the show carry on. Reset brings it back.","zh":"一個模組只進場一次、只退場一次，不管演出是怎麼走到那裡的：seek 到段落中間，會用正確的 `p` 呼叫 `enter`，所以模組必須能從中間開始。丟出錯誤的模組只有它自己會被關掉：導演用 `phase: 'failed'` 呼叫一次它的 `exit()`，印出一行 `console.error`，之後不再呼叫它，其他模組和演出照常進行。reset 可以把它請回來。"},{"en":"In a show, [`createShow()`](#createShow) builds the director from its `score` option and calls `update()` every frame after the timeline has advanced; the director's `param()` plugs into `timeline.layer`. Without `createShow()` you do that wiring yourself: `director.update(timeline.t, dt, { holding: timeline.holding, playing: timeline.playing })`.","zh":"在演出裡，[`createShow()`](#createShow) 依 `score` 選項建好導演，每個畫面在時間軸前進之後呼叫 `update()`，導演的 `param()` 接到 `timeline.layer` 上。不用 `createShow()` 的話，這些接線要自己做：`director.update(timeline.t, dt, { holding: timeline.holding, playing: timeline.playing })`。"}],"params":[{"name":"score","type":"Score","optional":false,"en":"The [`Score`](#Score) to perform. Required: without it the constructor throws \"Director needs a score\".","zh":"要表演的 [`Score`](#Score)。必填，沒有的話建構函式會丟出 \"Director needs a score\"。"},{"name":"modules","type":"Array | Object","optional":true,"default":"[]","en":"The segment modules: an array (matched to segments by each module's `id`) or an object keyed by segment id. A segment with no module is simply not performed.","zh":"段落模組：一個陣列（依每個模組的 `id` 對應到段落），或以段落 id 為 key 的物件。沒有模組的段落就單純不被表演。"},{"name":"api","type":"Object | Function","optional":true,"default":"{}","en":"What every module hook receives as its first argument (also `ctx.api`). A function is called once, the first time it is needed, and its result is used.","zh":"每個模組的掛鉤收到的第一個參數（也是 `ctx.api`）。如果是函式，會在第一次需要時呼叫一次，然後使用它回傳的結果。"},{"name":"onStatus","type":"Function | null","optional":true,"default":"null","en":"Called with `status()` when the segment, the holding state or the list of switched-off modules changes, not every frame.","zh":"當段落、hold 的狀態或被關掉的模組清單改變的時候，以 `status()` 呼叫，不是每個畫面都呼叫。"},{"name":"onCue","type":"Function | null","optional":true,"default":"null","en":"Called for each cue the playhead passes: `{ name, cue, T, n, segment }`.","zh":"播放頭每經過一個 cue 就呼叫一次：`{ name, cue, T, n, segment }`。"},{"name":"maxStep","type":"Number","optional":true,"default":"2","en":"A jump bigger than this many seconds between two `update()` calls counts as a seek, so the cues in between are not fired. Raise it if you rehearse at a very high `timeline.rate`.","zh":"兩次 `update()` 之間跳超過這麼多秒，就算是 seek，中間的 cue 不會觸發。如果你用很高的 `timeline.rate` 排練，把它調大。"}],"notes":[{"en":"The last segment is never exited by time: while the show sits at its end (or past it), the last module stays entered. It exits on a seek that leaves it, or on `reset()`.","zh":"最後一個段落不會因為時間而退場：演出停在尾端（或超過尾端）時，最後一個模組仍維持進場狀態。它只在 seek 離開或 `reset()` 時退場。"},{"en":"A new segment's module is entered before the old one exits, and during `linger` both run at the same time.","zh":"新段落的模組會先進場、舊的才退場，在 `linger` 期間兩個會同時在跑。"},{"en":"A module whose `module` or `id` matches no segment, or a segment that names a module nobody registered (listed in `status().missing`), is simply not performed. The show keeps going.","zh":"`module` 或 `id` 對不到任何段落的模組，或段落指定了沒人登記的模組（會列在 `status().missing`），都只是不被表演，演出照常進行。"}],"examples":[{"code":"import { Score, Director } from '@openav/score';\n\nconst score = new Score({ cuts: [{ id: 'full', segments: [\n  { id: 'dawn', dur: 4, cues: { birds: 0.5 } },\n  { id: 'noon', dur: 4 },\n] }] });\nconst say = (id) => ({\n  id,\n  enter(api, ctx) { console.log(id, 'enter, p =', ctx.p); },\n  exit() { console.log(id, 'exit'); },\n  cue(name) { console.log(id, 'cue', name); },\n});\nconst director = new Director({ score, modules: [say('dawn'), say('noon')] });\nfor (let T = 0; T <= 8; T += 1) director.update(T, 1, { playing: true });   // one call per frame","caption":{"en":"The console prints `dawn enter, p = 0`, `dawn cue birds` (second 2), then `noon enter, p = 0` and `dawn exit` at second 4. The last segment stays entered to the end.","zh":"主控台印出 `dawn enter, p = 0`、`dawn cue birds`（第 2 秒），然後第 4 秒是 `noon enter, p = 0` 和 `dawn exit`。最後一個段落會一直維持進場到結束。"},"run":"module","lang":"js"},{"code":"import { Score, Director } from '@openav/score';\n\nconst score = new Score({ cuts: [{ id: 'full', segments: [{ id: 'dawn', dur: 4 }, { id: 'noon', dur: 4 }] }] });\nconst sun = { id: 'dawn', param: (key, value, ctx) => (key === 'sun' ? ctx.p : value) };\nconst director = new Director({ score, modules: [sun] });\n\ndirector.update(1, 1, { playing: true });\nconsole.log(director.param('sun', 0.9, 1), director.param('moon', 0.9, 1));   // 0.25 0.9\ndirector.update(2, 1, { playing: true });\nconsole.log(director.param('sun', 0.9, 2));                                     // 0.5","caption":{"en":"While `dawn` is current its module bends only the `sun` param, following its own progress: the console prints `0.25 0.9`, then `0.5`.","zh":"`dawn` 在進行的時候，它的模組只改動 `sun` 這個參數，跟著自己的進度走：主控台印出 `0.25 0.9`，然後是 `0.5`。"},"run":"module","lang":"js"},{"code":"import { Score, Director } from '@openav/score';\n\nconsole.error = (message) => console.log('logged:', String(message).slice(0, 62));   // show the director's error line here\n\nconst score = new Score({ cuts: [{ id: 'full', segments: [{ id: 'dawn', dur: 4 }, { id: 'noon', dur: 4 }] }] });\nconst dawn = { id: 'dawn', update() { throw new Error('boom'); } };\nconst noon = { id: 'noon', enter() { console.log('noon is still performed'); } };\nconst director = new Director({ score, modules: [dawn, noon] });\n\ndirector.update(1, 1, { playing: true });     // dawn throws: it is switched off, once\ndirector.update(5, 1, { playing: true });     // the show goes on\nconsole.log(director.status().failed, director.status().errors);\ndirector.reset();                              // brings dawn back\nconsole.log(director.status().failed);","caption":{"en":"A crash is contained: the console prints `logged: [score] segment module \"dawn\" crashed in update: boom and was` (normally a red error line), then `noon is still performed`, `[ 'dawn' ] { dawn: 'update: boom' }`, and after the reset `[]`.","zh":"崩潰被關在原地：主控台印出 `logged: [score] segment module \"dawn\" crashed in update: boom and was`（平常那是一行紅色的錯誤訊息），接著是 `noon is still performed`、`[ 'dawn' ] { dawn: 'update: boom' }`，reset 之後是 `[]`。"},"run":"module","lang":"js"}],"related":["Score","ScoreError","Timeline","createShow","mountConsole"],"keywords":{"en":["director","segment module","module","enter","exit","update","cue","linger","hold","param layer","crash","status","score player"],"zh":["導演","段落模組","模組","進場","退場","cue","餘韻","hold","參數圖層","崩潰","狀態"]},"tbd":[],"claims":[],"members":[{"name":"api","member":"getter","signature":"","doc":"","line":69},{"name":"register","member":"method","signature":"mod","doc":"Add a segment module (replaces one with the same id).","line":75},{"name":"moduleFor","member":"method","signature":"seg","doc":"The module that performs a segment: its `module` name if the cut gives one, else its own id.","line":82},{"name":"holding","member":"getter","signature":"","doc":"","line":85},{"name":"update","member":"method","signature":"T, dt = 0, state = {}","doc":"Call once per frame.","line":92},{"name":"param","member":"method","signature":"key, value, T = this._T","doc":"The timeline's param layer: let the modules of the current segments rewrite a param.","line":136},{"name":"seek","member":"method","signature":"T","doc":"The playhead jumped to T: modules whose segment no longer covers T exit now; the next update() enters the new ones with the right p.","line":150},{"name":"reset","member":"method","signature":"","doc":"Back to the start: every active module exits, every module hears reset(), disabled modules come back.","line":165},{"name":"status","member":"method","signature":"","doc":"For the desk and the backstage: the current segment, the next one and how long until it, who is off.","line":182}]},
{"id":"Score","key":"Score","name":"Score","aliases":[],"kind":"class","package":"@openav/score","category":"timeline","documented":true,"signature":"new Score({ cuts, cut } = {})","import":"import { Score } from '@openav/score';","source":{"file":"packages/score/score.js","line":48,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/score/score.js#L48"},"summary":{"en":"A show written as segments with lengths, never as absolute seconds: it works out every start, end, cue and countdown, for every version (cut) of the show.","zh":"把一場演出寫成「每一段有多長」，而不是絕對秒數：每個起點、終點、cue 和倒數，以及每一個版本（cut）的時間，都由它算出來。"},"description":[{"en":"A timeline written as a table of absolute seconds cannot grow: make one scene longer and every later cue, curve and `if (t > 466)` is wrong. In a score you write only how **long** each segment is, and `Score` computes every start, end, cue time, countdown and scene from those lengths. Change one `dur` and everything after it moves; a second version of the show is a few lines of data.","zh":"把時間軸寫成一張絕對秒數的表，它就長不大：某個場景拉長一點，後面每個 cue、曲線和 `if (t > 466)` 全都錯了。在總譜裡你只寫每一段「多**長**」，`Score` 從這些長度算出每個起點、終點、cue 的時間、倒數和場景。改一個 `dur`，它後面的一切都跟著挪。第二個版本只是幾行資料。"},{"en":"A cut is one version of the show; a segment is one stretch of it:\n\n```js\n{ id: 'full', title: 'A day', segments: [\n  { id: 'standby', dur: 6, hold: true, acts: ['Press Space when the room is ready'] },\n  { id: 'dawn',    dur: 24, cues: { birds: 0.3 } },          // a ratio of the segment\n  { id: 'noon',    dur: 30, cues: { flare: { s: 8 } } },     // or seconds into it\n  { id: 'dusk',    dur: 24, linger: 8 },\n] }\n{ id: 'short', from: 'full', scale: 0.5 }                     // the same show at half the length\n```\n\n`dur` is the only time a segment has. `cues` are named moments (a ratio `0 <= r < 1`, or `{ s: seconds }`), `acts` is what the human operator does (strings shown on the desk), `linger` keeps a segment's module running for some seconds after it ends, `hold` stops playback at the end of the segment until the operator releases it, and `module` names the module that performs it when that is not the segment's own id. A derived cut scales `dur`, `linger` and `{ s }` cues, so a cue keeps its place in the music.","zh":"一個 cut 是演出的一個版本，一個 segment（段落）是其中的一段：\n\n```js\n{ id: 'full', title: 'A day', segments: [\n  { id: 'standby', dur: 6, hold: true, acts: ['Press Space when the room is ready'] },\n  { id: 'dawn',    dur: 24, cues: { birds: 0.3 } },          // a ratio of the segment\n  { id: 'noon',    dur: 30, cues: { flare: { s: 8 } } },     // or seconds into it\n  { id: 'dusk',    dur: 24, linger: 8 },\n] }\n{ id: 'short', from: 'full', scale: 0.5 }                     // the same show at half the length\n```\n\n`dur` 是一個段落唯一擁有的時間。`cues` 是有名字的時刻（比例 `0 <= r < 1`，或 `{ s: 秒數 }`），`acts` 是操作者要做的事（顯示在控台上的字串），`linger` 讓段落的模組在段落結束後再多跑幾秒，`hold` 讓播放停在段落尾端，等操作者放行，`module` 在負責表演的模組不是段落自己的 id 時，指定模組名稱。衍生的 cut 會縮放 `dur`、`linger` 和 `{ s }` 形式的 cue，所以 cue 在音樂裡的位置不變。"},{"en":"Everything is checked when the score is built. Duplicate ids, a `dur` of 0 or below, a `from` that is not a cut, a cut derived from itself, a cue outside its segment: each throws a [`ScoreError`](#ScoreError) that lists every problem at once. In a show, [`createShow()`](#createShow) builds the score from its `score` option and hands it to a [`Timeline`](#Timeline) and a [`Director`](#Director); `?cut=<id>` in the page address picks the cut.","zh":"總譜在建立的時候就會全部檢查一遍。id 重複、`dur` 等於或小於 0、`from` 指到不存在的 cut、cut 衍生自己、cue 落在段落外面，每一種都會丟出 [`ScoreError`](#ScoreError)，一次把所有問題列出來。在演出裡，[`createShow()`](#createShow) 依 `score` 選項建好總譜，交給 [`Timeline`](#Timeline) 和 [`Director`](#Director)，網址上的 `?cut=<id>` 可以選 cut。"}],"params":[{"name":"cuts","type":"Array | Object","optional":false,"en":"The cuts this show can be performed as: an array of cuts, or an object keyed by cut id. A cut is `{ id, title?, start?, segments: [...] }`, or a derived one, `{ id, title?, from, scale }`. Required: without a cut it throws.","zh":"這場演出可以表演成哪些 cut：一個 cut 的陣列，或以 cut id 為 key 的物件。一個 cut 是 `{ id, title?, start?, segments: [...] }`，或衍生的 `{ id, title?, from, scale }`。必填，沒有 cut 就會丟出錯誤。"},{"name":"cut","type":"String","optional":true,"default":"the first cut","en":"The id of the cut to perform. Defaults to the first one; an id that is not in `cuts` is an error.","zh":"要表演的 cut 的 id。預設是第一個，不在 `cuts` 裡的 id 是錯誤。"}],"notes":[{"en":"Plain fields: `cut` (the id being performed), `title`, `segments` (each with `start`, `end`, `dur`, `index`, `cues` …), `total` (the length of the show), `startT` (where `reset` goes), `scale` and `base`.","zh":"普通欄位：`cut`（正在表演的 id）、`title`、`segments`（每一段都有 `start`、`end`、`dur`、`index`、`cues` ……）、`total`（演出總長）、`startT`（`reset` 回到哪裡）、`scale` 和 `base`。"},{"en":"Segment and cue ids must not contain a dot: the dot is how `cueTime('dusk.glow')` separates a segment from its cue. A cue cannot be called `end`.","zh":"段落和 cue 的 id 不能含有句點：`cueTime('dusk.glow')` 就是靠句點把段落和它的 cue 分開。cue 也不能叫 `end`。"},{"en":"If two cuts differ in structure and not only in length, do not share automation between them. Drive the params from the segment modules instead, where `ctx.p` already scales for free.","zh":"如果兩個 cut 不只是長度不同、連結構都不同，就不要讓它們共用自動化曲線，改由段落模組來驅動參數，那裡的 `ctx.p` 本來就會跟著縮放。"}],"examples":[{"code":"import { Score } from '@openav/score';\n\nconst score = new Score({ cuts: [{ id: 'full', segments: [\n  { id: 'dawn', dur: 24, cues: { birds: 0.25 } },\n  { id: 'noon', dur: 30, cues: { flare: { s: 8 } } },\n  { id: 'dusk', dur: 26 },\n] }] });\nconsole.table(score.table());\nconsole.log(score.total, score.segmentAt(40).id, score.cueTime('noon.flare'));","caption":{"en":"The console shows a table of the three segments (`dawn` 0 to 24, `noon` 24 to 54, `dusk` 54 to 80), then `80 noon 32`: the show is 80 seconds long, second 40 is in `noon`, and the flare cue sits at 32.","zh":"主控台會顯示三個段落的表格（`dawn` 0 到 24、`noon` 24 到 54、`dusk` 54 到 80），然後印出 `80 noon 32`：演出長 80 秒，第 40 秒在 `noon`，flare 這個 cue 在第 32 秒。"},"run":"module","lang":"js"},{"code":"import { Score } from '@openav/score';\n\nconst cuts = [\n  { id: 'full', segments: [{ id: 'dawn', dur: 24, cues: { birds: 0.25 } }, { id: 'noon', dur: 30, cues: { flare: { s: 8 } } }] },\n  { id: 'short', from: 'full', scale: 0.5 },\n];\nfor (const cut of ['full', 'short']) {\n  const score = new Score({ cuts, cut });\n  console.log(cut, score.total, score.cueTime('dawn.birds'), score.cueTime('noon.flare'));\n}","caption":{"en":"The same show at two lengths: `full 54 6 32`, then `short 27 3 16`. Every time is halved and the cues stay in the same place in the music.","zh":"同一場演出的兩種長度：`full 54 6 32`，然後 `short 27 3 16`。所有時間都減半，cue 在音樂裡的位置不變。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\nimport { Score } from '@openav/score';\nimport { Timeline } from '@openav/timeline';\n\nconst cuts = [\n  { id: 'full', segments: [{ id: 'a', dur: 20 }, { id: 'b', dur: 40, hold: true }, { id: 'c', dur: 20 }] },\n  { id: 'short', from: 'full', scale: 0.5 },\n];\nconst params = new Params([{ key: 'haze', min: 0, max: 1, def: 0 }]);\nfor (const cut of ['full', 'short']) {\n  const timeline = new Timeline({ params, score: new Score({ cuts, cut }), automation: { haze: [[0, 0], [80, 1]] } });\n  console.log(cut, timeline.total, timeline.scenes.map((s) => s.id + '@' + s.t).join(' '), timeline.valueAt('haze', 20));\n}","caption":{"en":"The timeline takes its scenes and total from the score, and reads the haze curve (written once for the full cut) stretched: `full 80 a@0 b@20 c@60 0.25`, then `short 40 a@0 b@10 c@30 0.5`.","zh":"時間軸的場景和總長都從總譜來，而且讀的是被拉伸的 haze 曲線（只對完整版寫過一次）：`full 80 a@0 b@20 c@60 0.25`，然後 `short 40 a@0 b@10 c@30 0.5`。"},"run":"module","lang":"js"}],"related":["ScoreError","Director","Timeline","createShow"],"keywords":{"en":["score","cut","cuts","segment","segments","cue","cues","hold","linger","dur","duration","structure","version","show structure"],"zh":["總譜","樂譜","cut","段落","版本","cue","長度","結構","倒數","場景","hold"]},"tbd":[],"claims":[],"members":[{"name":"cuts","member":"getter","signature":"","doc":"Every cut, for a version picker: [{ id, title, total, from?, scale }]","line":77},{"name":"validate","member":"method","signature":"","doc":"Re-check everything and throw a ScoreError listing every problem.","line":179},{"name":"table","member":"method","signature":"","doc":"One row per segment, for console.table and docs.","line":195},{"name":"segment","member":"method","signature":"id","doc":"The segment with this id (undefined when there is none).","line":200},{"name":"indexAt","member":"method","signature":"T","doc":"Index of the segment playing at show time T.","line":203},{"name":"segmentAt","member":"method","signature":"T","doc":"","line":208},{"name":"cueTime","member":"method","signature":"name","doc":"A named moment in show seconds.","line":214},{"name":"cues","member":"method","signature":"","doc":"Every cue of the cut in time order: [{ name, cue, T, p, n, segment }] (n = its place in that order).","line":227},{"name":"cuesBetween","member":"method","signature":"T0, T1","doc":"The cues a playhead passed going from T0 to T1: those with T0 < time ≤ T1, in order.","line":230},{"name":"baseTime","member":"method","signature":"T","doc":"Time on the cut this one was derived from.","line":239},{"name":"scenes","member":"method","signature":"","doc":"The segments as a","line":242}]},
{"id":"ScoreError","key":"ScoreError","name":"ScoreError","aliases":[],"kind":"class","package":"@openav/score","category":"timeline","documented":true,"signature":"new ScoreError(problems)","import":"import { ScoreError } from '@openav/score';","source":{"file":"packages/score/score.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/score/score.js#L30"},"summary":{"en":"The error a Score throws when it cannot be built: its `problems` array lists every reason at once, and its message lists them too.","zh":"總譜建不起來的時候丟出的錯誤：它的 `problems` 陣列一次列出所有原因，錯誤訊息裡也會一起列出來。"},"description":[{"en":"You rarely build one yourself: [`new Score()`](#Score) throws it. `error.problems` is an array of strings and `error.message` is the same list as one text, headed \"Score is invalid (3 problems)\". `error.name` is `'ScoreError'`, and it is a normal `Error`, so `instanceof ScoreError` and `instanceof Error` both work.","zh":"通常不用自己建：[`new Score()`](#Score) 會丟出它。`error.problems` 是字串陣列，`error.message` 是同一份清單串成的文字，開頭是「Score is invalid (3 problems)」。`error.name` 是 `'ScoreError'`，它是一般的 `Error`，所以 `instanceof ScoreError` 和 `instanceof Error` 都成立。"},{"en":"It lists every problem it found, not only the first, so one run of the show shows you everything to fix. Nothing half-valid can reach the stage.","zh":"它會列出找到的所有問題，不是只列第一個，所以跑一次就能看到所有要修的地方。有問題的總譜上不了台。"}],"params":[{"name":"problems","type":"Array<String>","optional":false,"en":"The reasons, one string each. Each one names the cut, the segment and what is wrong.","zh":"所有原因，每個一個字串。每一個都會指出是哪個 cut、哪個段落、哪裡不對。"}],"notes":[],"examples":[{"code":"import { Score, ScoreError } from '@openav/score';\n\ntry {\n  new Score({ cuts: [\n    { id: 'a', segments: [{ id: 'x', dur: 0 }, { id: 'x', dur: 5 }] },\n    { id: 'b', from: 'missing', scale: 1 },\n  ] });\n} catch (error) {\n  console.log(error instanceof ScoreError, error.problems.length);\n  for (const problem of error.problems) console.log('-', problem);\n}","caption":{"en":"Three mistakes, all reported at once: the console prints `true 3` and then one line for each (a `dur` of 0, a repeated id, a `from` that is not a cut).","zh":"三個錯誤一次全部報出來：主控台印出 `true 3`，然後每個錯誤一行（`dur` 是 0、id 重複、`from` 指到不存在的 cut）。"},"run":"module","lang":"js"}],"related":["Score","Director"],"keywords":{"en":["score error","ScoreError","validation","invalid score","problems","error","throws"],"zh":["總譜錯誤","驗證","檢查","錯誤","問題清單","例外"]},"tbd":[],"claims":[]},
{"id":"CURVES","key":"CURVES","name":"CURVES","aliases":[],"kind":"constant","package":"@openav/timeline","category":"timeline","documented":true,"signature":"CURVES = object, 5 keys","import":"import { CURVES } from '@openav/timeline';","source":{"file":"packages/timeline/index.js","line":28,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/timeline/index.js#L28"},"summary":{"en":"The curve names a keyframe can use to say how its segment gets there: `linear`, `ease`, `in`, `out`, `step`, each a function from 0..1 to 0..1.","zh":"關鍵影格可以用來說明「這一段怎麼抵達」的曲線名稱：`linear`、`ease`、`in`、`out`、`step`，每個都是 0..1 對應到 0..1 的函式。"},"description":[{"en":"In a [`Timeline`](#Timeline), the third item of a keyframe names the curve of the segment that **ends** at that keyframe: `[30, 320, 'ease']` means \"arrive at 320 at second 30 the `ease` way\". `linear` is the default, and an unknown name or a typo also falls back to `linear`, so a mistake never stops a show. You can pass your own function instead of a name.","zh":"在 [`Timeline`](#Timeline) 裡，關鍵影格的第三項是「**抵達**這個影格的那一段」的曲線名稱：`[30, 320, 'ease']` 的意思是「用 `ease` 的方式，在第 30 秒抵達 320」。預設是 `linear`，名稱不認得或打錯也會退回 `linear`，所以打錯字不會讓演出停下來。你也可以傳自己的函式，不用名稱。"},{"en":"`linear` goes straight. `ease` is a smoothstep: slow, fast, slow. `in` starts slowly (f²). `out` starts fast and lands softly (1 − (1 − f)²). `step` is special: the segment holds the **old** value until the keyframe, then jumps. The `step` function stored here returns 0 and is never called to interpolate; it is listed so that the object is the whole vocabulary.","zh":"`linear` 走直線。`ease` 是 smoothstep：慢、快、慢。`in` 起頭慢（f²）。`out` 起頭快、輕輕落地（1 − (1 − f)²）。`step` 比較特別：這一段會一直守著**舊**值，到了關鍵影格才跳過去。放在這裡的 `step` 函式永遠回傳 0，內插的時候不會用它來算，列在這裡只是為了讓這個物件涵蓋整套名稱。"},{"en":"A param declared with `step` in its [`Params`](#Params) schema holds each keyframe whatever curve you name.","zh":"在 [`Params`](#Params) 的 schema 裡宣告了 `step` 的參數，不管你寫哪種曲線，都會停在每個關鍵影格。"}],"params":[],"notes":[],"examples":[{"code":"import { CURVES } from '@openav/timeline';\n\nfor (const name of ['linear', 'ease', 'in', 'out']) {\n  console.log(name, CURVES[name](0.5));        // halfway through the segment\n}","caption":{"en":"The console prints how far each curve has got at the halfway point: `linear 0.5`, `ease 0.5`, `in 0.25`, `out 0.75`.","zh":"主控台印出每條曲線在一半的時間點走到了哪裡：`linear 0.5`、`ease 0.5`、`in 0.25`、`out 0.75`。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\nimport { Timeline } from '@openav/timeline';\n\nconst params = new Params([{ key: 'speed', min: 0, max: 4, def: 0 }]);\nconst timeline = new Timeline({ params, automation: {\n  speed: [[0, 0.3], [16, 1.6, 'in'], [20, 3.4, 'step'], [30, 0.8, 'out']],\n} });\nfor (const t of [8, 16, 19, 20, 25, 30]) console.log(t, timeline.valueAt('speed', t).toFixed(2));","caption":{"en":"One lane, three curves: the console prints a slow start (`8 0.63`), the arrival at `16 1.60`, a long hold (`19 1.60`) until the step jumps at 20 (`20 3.40`), then an `out` curve down to `30 0.80`.","zh":"一條軌道，三種曲線：主控台印出緩慢的起頭（`8 0.63`）、第 16 秒抵達 `16 1.60`、一路守著（`19 1.60`）到第 20 秒 step 才跳（`20 3.40`），最後用 `out` 曲線降到 `30 0.80`。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\nimport { Timeline } from '@openav/timeline';\n\nconst params = new Params([{ key: 'speed', min: 0, max: 4, def: 0 }]);\nconst timeline = new Timeline({ params, automation: {\n  speed: [[0, 0], [10, 4, (f) => f * f * f]],      // your own curve: a function of 0..1\n} });\nconsole.log(timeline.valueAt('speed', 5));","caption":{"en":"A function works wherever a name does. Halfway through, a cubic curve is only an eighth of the way: the console prints `0.5` (4 × 0.125).","zh":"函式可以用在任何可以寫名稱的地方。走到一半的時候，三次方曲線只走了八分之一：主控台印出 `0.5`（4 × 0.125）。"},"run":"module","lang":"js"}],"related":["Timeline","Params","approach"],"keywords":{"en":["curve","curves","easing","ease","ease in","ease out","step","linear","interpolation","smoothstep","keyframe"],"zh":["曲線","緩動","緩入","緩出","階梯","內插","關鍵影格","線性"]},"tbd":[],"claims":[]},
{"id":"HOLD_EPS","key":"HOLD_EPS","name":"HOLD_EPS","aliases":[],"kind":"constant","package":"@openav/timeline","category":"timeline","documented":true,"signature":"HOLD_EPS = 0.000001","import":"import { HOLD_EPS } from '@openav/timeline';","source":{"file":"packages/timeline/index.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/timeline/index.js#L38"},"summary":{"en":"How far before the boundary the playhead parks when a hold stops the show: one microsecond, so it still belongs to the scene that is waiting.","zh":"hold 讓演出停住的時候，播放頭停在邊界前多遠的地方：一微秒，這樣它仍然屬於正在等候的那個場景。"},"description":[{"en":"A scene with `hold: true` stops playback at its end. The [`Timeline`](#Timeline) parks `HOLD_EPS` (0.000001 seconds) **before** the boundary rather than on it, so `sceneIndexAt(t)` and the director still say \"the held scene\", `timeline.t` reads 5.999999 for a hold at 6, and the next scene has not started. `release()` then moves the playhead onto the boundary itself.","zh":"`hold: true` 的場景會在尾端讓播放停住。[`Timeline`](#Timeline) 把播放頭停在邊界**前面** `HOLD_EPS`（0.000001 秒）的地方，而不是正好停在邊界上，所以 `sceneIndexAt(t)` 和導演仍然說「現在是被 hold 的那個場景」，在 6 秒的 hold 上 `timeline.t` 讀到的是 5.999999，下一個場景還沒開始。之後 `release()` 才把播放頭移到邊界本身。"},{"en":"You only need it when you compare the playhead with a boundary yourself: while holding, `timeline.t` is `boundary - HOLD_EPS`, not exactly `boundary`.","zh":"只有在你自己拿播放頭跟邊界做比較的時候才會用到：hold 的時候，`timeline.t` 是 `邊界 - HOLD_EPS`，不是剛好等於邊界。"}],"params":[],"notes":[],"examples":[{"code":"import { Params } from '@openav/core';\nimport { Timeline, HOLD_EPS } from '@openav/timeline';\n\nconst params = new Params([]);\nconst timeline = new Timeline({ params, total: 20, scenes: [{ id: 'wait', t: 0, hold: true }, { id: 'go', t: 6 }] });\ntimeline.play();\nfor (let i = 0; i < 10; i++) timeline.advance(1);\nconsole.log(timeline.holding, timeline.currentScene().id, Math.abs(6 - timeline.t - HOLD_EPS) < 1e-9);","caption":{"en":"The show parks just before second 6, still inside the held scene: the console prints `true wait true` (`6 - timeline.t` is `HOLD_EPS`, apart from rounding).","zh":"演出停在第 6 秒的稍前方，仍然在被 hold 的場景裡：主控台印出 `true wait true`（`6 - timeline.t` 在捨入誤差內等於 `HOLD_EPS`）。"},"run":"module","lang":"js"}],"related":["Timeline","Director","Score"],"keywords":{"en":["hold","eps","epsilon","park","standby","boundary","release"],"zh":["hold","停住","待機","邊界","放行","微秒"]},"tbd":[],"claims":[]},
{"id":"Timeline","key":"Timeline","name":"Timeline","aliases":[],"kind":"class","package":"@openav/timeline","category":"timeline","documented":true,"signature":"new Timeline({ params, automation = {}, scenes = [], total = 600, score = null, start = null, loop = false, onCue = null })","import":"import { Timeline } from '@openav/timeline';","source":{"file":"packages/timeline/index.js","line":40,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/timeline/index.js#L40"},"summary":{"en":"Answers \"what is every param at time t?\" from keyframes, and runs the transport around it: play, seek, scenes, holds, loops, cues and recording.","zh":"用關鍵影格回答「時間 t 的時候，每個參數是多少？」，並在外面包一層播放控制：播放、跳轉、場景、hold、循環、cue 和錄製。"},"description":[{"en":"`timeline.state(t)` returns `{ key: value }` for every param at time `t`, interpolated from `automation`. That is the whole job: the timeline owns time, and a world never reads the clock. In a show, [`createShow()`](#createShow) builds the timeline from its `timeline` option, [`Loop`](#Loop) calls `advance(dt)` every frame, and what `state()` returns goes into [`params.resolve()`](#Params), where a hand or a knob that has overridden a param still wins.","zh":"`timeline.state(t)` 回傳時間 `t` 的時候每個參數的 `{ key: value }`，由 `automation` 內插得來。它的工作就這一件：時間歸時間軸管，世界從來不讀時鐘。在演出裡，[`createShow()`](#createShow) 依 `timeline` 選項建好時間軸，[`Loop`](#Loop) 每個畫面呼叫 `advance(dt)`，`state()` 的結果再交給 [`params.resolve()`](#Params)，被手或旋鈕覆寫的參數仍然以覆寫為準。"},{"en":"Between two keyframes the value moves in a straight line. The optional third item of a keyframe says how the segment that **ends** at that keyframe gets there: `'linear'`, `'ease'`, `'in'`, `'out'`, `'step'`, or your own function of 0..1 (see [`CURVES`](#CURVES)). Before the first keyframe the value is the first one, after the last it is the last one, and a param with no keyframes stays at its `def`. A param declared with `step` holds each keyframe instead of interpolating.","zh":"兩個關鍵影格之間，值走直線。關鍵影格選填的第三項，說的是「**抵達**這個影格的那一段」怎麼走：`'linear'`、`'ease'`、`'in'`、`'out'`、`'step'`，或你自己寫的 0..1 函式（見 [`CURVES`](#CURVES)）。第一個影格之前，值就是第一個影格的值。最後一個影格之後，就是最後一個的值。沒有任何關鍵影格的參數維持在 `def`。宣告了 `step` 的參數會停在每個關鍵影格，不做內插。"},{"en":"Scenes give the show its shape: `{ id: 'storm', t: 90, title: 'Storm', note: 'clusters welcome' }`. `onSceneChange` is the hook to fire sound, lights or prompts, and the console shows the title and note to the performer. A scene with `hold: true` stops playback at its end until someone calls `release()`. Scene `cues` are `{ name, t }` with `t` in show seconds (not seconds into the scene), and they fire only when **playback** passes them: a seek, a jump or a reset never does.","zh":"場景給了演出一個結構：`{ id: 'storm', t: 90, title: 'Storm', note: 'clusters welcome' }`。`onSceneChange` 是用來觸發聲音、燈光或提詞的掛鉤，主控台會把標題和備註顯示給表演者。`hold: true` 的場景，播放到它的尾端就停住，直到有人呼叫 `release()`。場景的 `cues` 是 `{ name, t }`，`t` 是演出秒數（不是場景內的第幾秒），而且只有**播放**經過它們的時候才會觸發：seek、跳轉或 reset 都不會。"}],"params":[{"name":"params","type":"Params | Array","optional":false,"en":"The [`Params`](#Params) of the show, or a plain schema array. The timeline reads the schema to know which params to resolve. Required.","zh":"演出的 [`Params`](#Params)，或一個單純的 schema 陣列。時間軸讀 schema 來知道要解析哪些參數。必填。"},{"name":"automation","type":"Object","optional":true,"default":"{}","en":"The keyframes of each param: `{ hue: [[0, 200], [30, 320, 'ease'], [60, 40]] }`. A keyframe is `[seconds, value, curve?]`, sorted by time.","zh":"每個參數的關鍵影格：`{ hue: [[0, 200], [30, 320, 'ease'], [60, 40]] }`。一個關鍵影格是 `[秒數, 值, 曲線?]`，依時間排序。"},{"name":"scenes","type":"Array","optional":true,"default":"[]","en":"The scenes: `{ id, t, title?, note?, acts?, hold?, cues? }`, sorted by `t`. A negative `t` is a pre-show standby. Ignored when a `score` is given.","zh":"場景：`{ id, t, title?, note?, acts?, hold?, cues? }`，依 `t` 排序。`t` 是負的就是開演前的待機。有給 `score` 的時候會被忽略。"},{"name":"total","type":"Number","optional":true,"default":"600","en":"The length of the show in seconds. Playback stops by itself there. (`createShow()` passes 120 unless you say otherwise.) Ignored when a `score` is given.","zh":"演出的長度，單位是秒。播放到這裡會自己停下來。（`createShow()` 沒特別指定的時候傳 120。）有給 `score` 的時候會被忽略。"},{"name":"score","type":"Score | null","optional":true,"default":"null","en":"A [`Score`](#Score): the scenes, the total and the start then come from it, and automation is read at `score.baseTime(t)`, so a derived cut stretches the curves with it.","zh":"一份 [`Score`](#Score)：場景、總長和起點都改從它來，自動化曲線讀的是 `score.baseTime(t)`，所以衍生的 cut 會把曲線一起拉伸。"},{"name":"start","type":"Number | 'first' | null","optional":true,"default":"null","en":"Where `reset()` goes and where the playhead starts: a number of seconds, or `'first'` for the start of the first scene (a standby at −30, say). `null` means 0, or the score's start.","zh":"`reset()` 要回到哪裡，播放頭一開始也在這裡：一個秒數，或 `'first'` 代表第一個場景的起點（例如 −30 的待機）。`null` 表示 0，或總譜的起點。"},{"name":"loop","type":"Boolean | String | Object","optional":true,"default":"false","en":"`true` wraps the show at its end and plays on; `'scene'` repeats the scene the playhead is in; a scene id repeats that scene; `{ from, to }` repeats a range (seconds or scene ids). It is a plain property: set `timeline.loop` any time.","zh":"`true` 是演出到尾端就繞回去繼續播，`'scene'` 是反覆播目前所在的場景，給場景 id 就反覆播那個場景，`{ from, to }` 反覆播一段範圍（秒數或場景 id）。它是個普通屬性，隨時可以設定 `timeline.loop`。"},{"name":"onCue","type":"Function | null","optional":true,"default":"null","en":"The same as calling `timeline.onCue(cb)` once: `cb(cue, { cause })`.","zh":"等於呼叫一次 `timeline.onCue(cb)`：`cb(cue, { cause })`。"}],"needs":{"en":"Time only moves when you call `play()` and then `advance(dt)` every frame (a show does this for you). `state(t)` and `valueAt(key, t)` work at any moment, playing or not. The callbacks do not fire for the starting scene until the first `advance()` or `seek()`.","zh":"時間只在你呼叫 `play()`、並且每個畫面呼叫 `advance(dt)` 的時候才會往前走（演出會替你做）。`state(t)` 和 `valueAt(key, t)` 隨時都能用，不論有沒有在播放。起始場景的回呼，要等到第一次 `advance()` 或 `seek()` 才會觸發。"},"notes":[{"en":"These are plain fields you can read and set: `t`, `playing`, `holding`, `rate` (1 is real time; 4 rehearses at four times the speed), `total`, `start`, `scenes`, `automation`, `score`, `loop`, `loops` (laps since the last reset), and `layer`.","zh":"這些是可以直接讀、也可以直接設定的普通欄位：`t`、`playing`、`holding`、`rate`（1 是真實速度，4 是四倍速排練）、`total`、`start`、`scenes`、`automation`、`score`、`loop`、`loops`（上次 reset 以來繞了幾圈）和 `layer`。"},{"en":"`layer` is `(key, value, t) => value`. A [`Director`](#Director) plugs in here so segment modules can bend a param; the performer's override is still applied afterwards and wins.","zh":"`layer` 是 `(key, value, t) => value`。[`Director`](#Director) 會接在這裡，讓段落模組可以改動參數，表演者的覆寫之後才套用，所以仍然以覆寫為準。"},{"en":"With a `score`, anything you pass as `scenes` or `total` is ignored, and `reset()` goes to the score's start.","zh":"有 `score` 的時候，你傳的 `scenes` 和 `total` 都會被忽略，`reset()` 回到的是總譜的起點。"}],"examples":[{"code":"import { Params } from '@openav/core';\nimport { Timeline } from '@openav/timeline';\n\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 200 }]);\nconst timeline = new Timeline({\n  params,\n  total: 60,\n  automation: { hue: [[0, 200], [30, 320], [60, 40, 'ease']] },\n});\nfor (const t of [0, 15, 30, 45]) console.log(t, timeline.state(t).hue);","caption":{"en":"The console prints the hue at four moments: `0 200`, `15 260`, `30 320`, `45 180`. Nothing plays; `state(t)` just looks the answer up.","zh":"主控台印出四個時刻的色相：`0 200`、`15 260`、`30 320`、`45 180`。什麼都沒有在播放，`state(t)` 只是把答案查出來。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\nimport { Timeline } from '@openav/timeline';\n\nconst params = new Params([{ key: 'energy', min: 0, max: 1, def: 0.2 }]);\nconst timeline = new Timeline({\n  params,\n  total: 20,\n  scenes: [\n    { id: 'standby', t: 0, hold: true },\n    { id: 'go', t: 6, cues: [{ name: 'flash', t: 10 }] },\n  ],\n});\ntimeline.onSceneChange((i, scene, { cause }) => console.log('scene', scene.id, cause));\ntimeline.onCue((cue) => console.log('cue', cue.name, 'at', cue.t));\n\ntimeline.play();\nfor (let i = 0; i < 8; i++) timeline.advance(1);   // one second per call\nconsole.log('holding:', timeline.holding, 'at', timeline.t.toFixed(3));\ntimeline.release();                                  // the operator says go\nfor (let i = 0; i < 6; i++) timeline.advance(1);","caption":{"en":"The console prints `scene standby play`, then `holding: true at 6.000` (the show waits at the end of the standby), then `scene go release`, then `cue flash at 10`.","zh":"主控台依序印出 `scene standby play`、`holding: true at 6.000`（演出停在待機場景的尾端等候），然後是 `scene go release`，最後是 `cue flash at 10`。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\nimport { Timeline } from '@openav/timeline';\n\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 100 }]);\nconst timeline = new Timeline({ params, total: 30, automation: { hue: [[0, 100], [30, 100]] } });\n\nlet knob = null;                                  // a number while a hand holds the knob\ntimeline.record('hue', () => knob);\ntimeline.onRecord((take) => console.log('take', take.from, 'to', take.to, take.cause));\n\ntimeline.play();\nfor (let i = 0; i < 20; i++) {\n  knob = i >= 5 && i < 12 ? 100 + (i - 5) * 20 : null;   // turned from second 5 to 11\n  timeline.advance(1);\n}\nconsole.log(JSON.stringify(timeline.automation.hue));","caption":{"en":"The knob turns for seven seconds; the console prints `take 6 to 12 release`, then the new keyframes: the take replaced the middle of the curve and the old ends stayed.","zh":"旋鈕轉了七秒，主控台印出 `take 6 to 12 release`，接著是新的關鍵影格：這段錄製取代了曲線的中段，原本的兩端保留下來。"},"run":"module","lang":"js"}],"related":["Params","Loop","Score","Director","CURVES","HOLD_EPS","createShow","mountConsole"],"keywords":{"en":["timeline","automation","keyframe","keyframes","scene","scenes","cue","hold","loop","seek","play","transport","record","state","playhead"],"zh":["時間軸","自動化","關鍵影格","場景","cue","hold","循環","跳轉","播放","播放頭","錄製","曲線"]},"tbd":[],"claims":[],"members":[{"name":"valueAt","member":"method","signature":"key, t = this.t","doc":"","line":71},{"name":"state","member":"method","signature":"t = this.t","doc":"Base state for all params at time t (no overrides — Params.resolve applies those).","line":91},{"name":"sceneIndexAt","member":"method","signature":"t = this.t","doc":"","line":100},{"name":"sceneEnd","member":"method","signature":"i","doc":"","line":101},{"name":"currentScene","member":"method","signature":"t = this.t","doc":"","line":102},{"name":"onSceneChange","member":"method","signature":"cb","doc":"cb(index, scene, { cause }) — cause: 'play' | 'release' | 'seek' | 'jump' | 'reset' | 'loop'","line":105},{"name":"onSeek","member":"method","signature":"cb","doc":"cb(t, kind) when the playhead is moved by hand — kind: 'seek' | 'jump' | 'reset'.","line":107},{"name":"play","member":"method","signature":"","doc":"Play.","line":115},{"name":"pause","member":"method","signature":"","doc":"","line":116},{"name":"toggle","member":"method","signature":"","doc":"","line":117},{"name":"seek","member":"method","signature":"t, cause = 'seek'","doc":"Move the playhead to t (clamped).","line":120},{"name":"jumpScene","member":"method","signature":"d","doc":"","line":129},{"name":"next","member":"method","signature":"","doc":"→ : release a hold (and keep playing), otherwise jump to the next scene.","line":131},{"name":"prev","member":"method","signature":"","doc":"← : the previous scene.","line":133},{"name":"release","member":"method","signature":"","doc":"Let a hold go: the next scene starts now and playback continues.","line":135},{"name":"reset","member":"method","signature":"","doc":"Back to the start (default 0, or `start`): stopped, not holding.","line":146},{"name":"advance","member":"method","signature":"dt","doc":"Call per frame with clamped dt.","line":164},{"name":"loopRange","member":"method","signature":"t = this.t","doc":"The range being looped right now, { a, b } in seconds, or null.","line":208},{"name":"onLoop","member":"method","signature":"cb","doc":"cb({ t, from, to, count }) each time the playhead wraps round the loop.","line":221},{"name":"cues","member":"method","signature":"","doc":"Every cue of every scene, in time order: { …cue, scene: sceneId, sceneIndex }.","line":225},{"name":"onCue","member":"method","signature":"cb","doc":"cb(cue, { cause }) when PLAYBACK passes a scene cue (cause 'play') or fireCue() sets it off by hand ('manual').","line":228},{"name":"fireCue","member":"method","signature":"c","doc":"Set a cue off by hand (the scrubber's ◆ click).","line":230},{"name":"record","member":"method","signature":"key, source, opts = {}","doc":"Arm a lane: while the timeline PLAYS, whatever `source()` returns is written into automation[key] as keyframes.","line":255},{"name":"recording","member":"getter","signature":"","doc":"","line":263},{"name":"stopRecording","member":"method","signature":"","doc":"Close every armed recorder (committing what each holds).","line":265},{"name":"onRecord","member":"method","signature":"cb","doc":"cb({ key, from, to, count, cause, keyframes }) when a take closes.","line":268},{"name":"mergeTake","member":"method","signature":"key, raw, opts = {}","doc":"Write a take (an array of [t, value], t in automation time) into automation[key]: · thinned: the first and last points stay; a point in between stays when its value moved more than `epsilon` (default 0.6 % of the…","line":283}]},
{"id":"count","key":"count","name":"count","aliases":["OAVp5.chop.count"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"count(on, [limit], [name])","source":{"file":"p5/oav-p5.js","line":3057,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3057"},"summary":{"en":"Counts how often `on` has turned on, or how many `'kick'` (or any other event) have happened; with a limit it loops 0, 1, … limit − 1.","zh":"數 `on` 打開過幾次，或某個事件（像 `'kick'`）發生過幾次。給了上限就會循環：0、1、…、上限 − 1（TouchDesigner 的 Count CHOP）。"},"description":[{"en":"A counter you do not have to keep yourself: `count(mouseIsPressed)` is the number of clicks so far, `count(beat, 4)` walks 0 1 2 3 0 1 … on the beat, and `count('kick')` counts the kicks of the drum machine. A switch has to go off and on again to be counted twice.","zh":"不用自己維護的計數器：`count(mouseIsPressed)` 是到現在的點擊次數，`count(beat, 4)` 跟著拍子數 0 1 2 3 0 1 …，`count('kick')` 數鼓機的大鼓。開關要關掉再打開，才會算第二次。"},{"en":"Event counts start when `count('kick')` is first called, so earlier kicks are not counted. An event of your own fired with `trigger()` is counted from its first firing, whenever `count()` is first called.","zh":"事件的計數從第一次呼叫 `count('kick')` 才開始，之前的大鼓不算。你自己用 `trigger()` 發出的事件，從它第一次發出就開始算，不管 `count()` 什麼時候第一次被呼叫。"}],"params":[{"name":"on","type":"Boolean | Number | String","en":"A switch: a boolean, or a number above 0.5, and each time it goes from off to on counts one. Or an event name (`'kick'`, `'beat'`, `'note'`, or one of your own fired with [`trigger()`](#trigger)): how many of those events have happened.","zh":"可以是開關：布林值，或大於 0.5 的數字，每次從關變成開就算一次。也可以是事件名字（`'kick'`、`'beat'`、`'note'`，或你自己用 [`trigger()`](#trigger) 發出的）：那種事件到現在發生了幾次。"},{"name":"limit","type":"Number","optional":true,"default":"0","en":"Loop at this number: 4 gives 0 1 2 3 0 1 … Left out, or 0 or less, it keeps counting up.","zh":"數到這個數字就繞回去：4 會得到 0 1 2 3 0 1 …。不寫、或是 0 以下，就一直往上數。"},{"name":"name","type":"String | Number","optional":true,"en":"Only for the switch form (`count(beat, 4, 'a')`): a name for its memory, as the last argument. The event-name form needs none.","zh":"只用在開關的寫法（`count(beat, 4, 'a')`）：給它的記憶取的名字，寫在最後一個參數。事件名字的寫法不需要。"}],"returns":{"type":"Number","en":"The count, a whole number from 0. It is 0 until @openav/chop has loaded (a few frames), and for an unnamed switch outside `draw()`.","zh":"計數，從 0 開始的整數。@openav/chop 還沒載好之前（幾個畫面），以及在 `draw()` 外面沒有名字的開關，都回傳 0。"},"needs":{"en":"The event names work like [`env()`](#env): `'kick'`, `'snare'`, `'hat'` and `'beat'` come from the microphone, or from the drum machine once [`startDrums()`](#startDrums) has been called, and the first `count('kick')` asks for the microphone if the drum machine is not on. The first call loads @openav/chop, which takes a few frames.","zh":"事件名字的規則跟 [`env()`](#env) 一樣：`'kick'`、`'snare'`、`'hat'`、`'beat'` 來自麥克風，或是呼叫過 [`startDrums()`](#startDrums) 之後來自鼓機，而鼓機沒開的時候，第一次 `count('kick')` 會去要麥克風。第一次呼叫會載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"A sketch with its own variable or function called `count` keeps it. `OAVp5.chop.count(…)` is this function under another name.","zh":"草圖如果自己有叫 `count` 的變數或函式，就由草圖保留。`OAVp5.chop.count(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(96);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(count(mouseIsPressed), width / 2, height / 2);   // clicks so far\n}","caption":{"en":"A counter that goes up by one on every click. Holding the button down counts once.","zh":"每點一下加一的計數器。按住不放只算一次。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n  startDrums('bd*4, [~ sd]*2, hh*8', 108);\n}\n\nfunction draw() {\n  background(20);\n  const nth = count('kick', 4);   // 0 1 2 3 0 1 ...\n  for (let i = 0; i < 4; i++) {\n    fill(i === nth ? color(255, 90, 31) : 70);\n    circle(100 + i * 67, 150, i === nth ? 50 : 30);\n  }\n}","caption":{"en":"Four dots light up one after another, one step per kick of the drum machine. Click the canvas once if you hear nothing.","zh":"四個圓點依序亮起，鼓機每一下大鼓前進一格。如果沒聽到聲音，點一下畫布。"},"run":"sketch","lang":"js"},{"code":"const colors = ['#ff5a1f', '#78beff', '#ffd36b'];\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() { trigger('boom'); }\n\nfunction draw() {\n  background(colors[count('boom', 3)]);   // 0 1 2 0 1 2 ...\n  fill(20);\n  text('clicks: ' + count('boom'), 20, 30);\n}","caption":{"en":"Each click cycles the background through three colours, and the text counts the clicks without end.","zh":"每點一下，背景就在三種顏色之間輪流換，文字則不停地數點擊次數。"},"run":"sketch","lang":"js"}],"related":["env","trigger","beat","startDrums","OAVp5.chop"],"keywords":{"en":["count","counter","tally","loop","modulo","step","sequence","Count CHOP","TouchDesigner"],"zh":["計數","計數器","次數","循環","步進","序列","TouchDesigner"]},"tbd":[],"claims":[{"before":"trigger('claim-cycle'); trigger('claim-cycle'); trigger('claim-cycle'); trigger('claim-cycle'); trigger('claim-cycle'); count('claim-cycle')","expr":"count('claim-cycle', 4)","equals":1}]},
{"id":"drawTrail","key":"drawTrail","name":"drawTrail","aliases":["OAVp5.chop.drawTrail"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"drawTrail(values, [x], [y], [w], [h], [lo], [hi], [name])","source":{"file":"p5/oav-p5.js","line":3152,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3152"},"summary":{"en":"Draws a line through `values` on the canvas, so it is in the work and in a recording: the oldest at `x`, `lo` at the bottom of the box, `hi` at the top.","zh":"在畫布上畫一條穿過 `values` 的線，所以它會在作品裡、也會被錄進影片：最舊的在 `x`，`lo` 在方框底，`hi` 在方框頂。"},"description":[{"en":"The picture [`scope()`](#scope) shows in the corner, but drawn on the canvas: `stroke(255); drawTrail(trail(micLevel, 3), 0, height - 100, width, 100)`. The line is one shape in the sketch's own `stroke()` colour and weight (white while `noStroke()` is in effect), with no fill, and the style you had before is put back afterwards.","zh":"就是 [`scope()`](#scope) 顯示在角落的那張圖，只是畫在畫布上：`stroke(255); drawTrail(trail(micLevel, 3), 0, height - 100, width, 100)`。這條線是草圖自己的 `stroke()` 顏色和粗細畫出的一個形狀（`noStroke()` 生效時是白色），不填色，畫完之後會把你原本的樣式還原。"},{"en":"The oldest value is at the left edge `x`, the newest at `x + w`. A number instead of an array is the shortcut: `drawTrail(micLevel, 0, 0, width, 100)` draws the last 2 seconds without a separate [`trail()`](#trail) call.","zh":"最舊的值在左邊的 `x`，最新的在 `x + w`。傳數字而不傳陣列是捷徑：`drawTrail(micLevel, 0, 0, width, 100)` 不用另外呼叫 [`trail()`](#trail) 就能畫出最近 2 秒。"}],"params":[{"name":"values","type":"Array<Number> | Number","en":"An array such as the one [`trail()`](#trail) returns. Or a single number: then drawTrail keeps its own trail of the last 2 seconds.","zh":"像 [`trail()`](#trail) 回傳的那種陣列。或是單一個數字：這時 drawTrail 會自己保留最近 2 秒的軌跡。"},{"name":"x","type":"Number","optional":true,"default":"0","en":"Left edge of the box.","zh":"方框左邊的位置。"},{"name":"y","type":"Number","optional":true,"default":"0","en":"Top edge of the box.","zh":"方框上緣的位置。"},{"name":"w","type":"Number","optional":true,"default":"width","en":"Width of the box. Left out, it is the canvas width.","zh":"方框的寬度。不寫就是畫布的寬度。"},{"name":"h","type":"Number","optional":true,"default":"height","en":"Height of the box. Left out, it is the canvas height.","zh":"方框的高度。不寫就是畫布的高度。"},{"name":"lo","type":"Number","optional":true,"default":"0","en":"The value drawn at the bottom of the box.","zh":"畫在方框底部的那個值。"},{"name":"hi","type":"Number","optional":true,"default":"1","en":"The value drawn at the top of the box. Values outside `lo`..`hi` sit on the edge of the box. If `hi` equals `lo`, the line runs through the middle.","zh":"畫在方框頂部的那個值。超出 `lo` 到 `hi` 的值會貼在方框的邊上。`hi` 等於 `lo` 的話，線會從中間穿過。"},{"name":"name","type":"String | Number","optional":true,"en":"Only when `values` is a single number: a name for its own trail, as the 8th argument (after `hi`).","zh":"只有 `values` 是單一個數字的時候才用：給它自己的軌跡取的名字，寫在第 8 個參數（`hi` 後面）。"}],"returns":{"type":"Array<Number>","en":"The array that was drawn: the one you passed, or the number's own trail. Nothing is drawn when it has fewer than 2 values.","zh":"畫出來的那個陣列：你傳進來的，或是那個數字自己的軌跡。少於 2 個值的時候不會畫。"},"needs":{"en":"Call it in `draw()` after `createCanvas()`. With a number, the first call loads @openav/chop, which takes a few frames, and nothing is drawn until then.","zh":"在 `createCanvas()` 之後的 `draw()` 裡呼叫。傳數字的話，第一次呼叫會載入 @openav/chop，要幾個畫面，那之前不會畫任何東西。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"`OAVp5.chop.drawTrail(…)` is this function under another name.","zh":"`OAVp5.chop.drawTrail(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const wave = 0.5 + 0.5 * sin(frameCount * 0.05);\n  stroke(255, 211, 107);\n  strokeWeight(2);\n  drawTrail(wave, 20, 100, width - 40, 100);   // a number: keeps its own 2 seconds\n}","caption":{"en":"A moving sine line drawn on the canvas itself, so it would appear in a recording.","zh":"一條移動的正弦線，直接畫在畫布上，所以錄影也會錄到。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noFill();\n  strokeWeight(2);\n}\n\nfunction draw() {\n  background(20);\n  const raw = frameCount % 120 < 60 ? 1 : 0;\n  const smooth = lag(raw, 0.3, 0.6);\n  stroke(95, 224, 138);\n  drawTrail(trail(raw, 3), 20, 60, width - 40, 80);       // 0 at the bottom, 1 at the top\n  stroke(255, 211, 107);\n  drawTrail(trail(smooth, 3), 20, 60, width - 40, 80);    // same box: drawn on top\n}","caption":{"en":"The raw square wave in green and its lagged version in yellow, in the same box so you can see the delay.","zh":"綠色是原始的方波，黃色是加了 lag 的版本，畫在同一個方框裡，看得出延遲。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noFill();\n  stroke(255);\n}\n\nfunction draw() {\n  background(20);\n  const wave = sin(frameCount * 0.05);   // -1..1\n  drawTrail(trail(wave, 3), 20, 100, width - 40, 100, -1, 1);   // lo and hi say what the box shows\n}","caption":{"en":"A wave that goes below 0: lo = -1 and hi = 1 put the bottom of the box at -1 and the top at 1, so the whole wave fits.","zh":"一條會掉到 0 以下的波：`lo` = -1、`hi` = 1 讓方框底是 -1、頂是 1，整條波就放得下。"},"run":"sketch","lang":"js"}],"related":["trail","scope","lag","OAVp5.chop"],"keywords":{"en":["drawTrail","draw a trail","plot","graph","line chart","history line","waveform","record the plot"],"zh":["畫軌跡","圖表","折線圖","歷史曲線","波形","畫在畫布上","錄進影片"]},"tbd":[],"claims":[]},
{"id":"env","key":"env","name":"env","aliases":["OAVp5.chop.env"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"env([event], [attack], [decay], [name])","source":{"file":"p5/oav-p5.js","line":3016,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3016"},"summary":{"en":"A 0..1 pulse that jumps up after each event (a kick, a beat, a key, a press) and then fades out: TouchDesigner's Trigger CHOP.","zh":"一個 0 到 1 的脈衝：每次事件（大鼓、拍子、琴鍵、按下）之後就跳起來，然後慢慢退下去（TouchDesigner 的 Trigger CHOP）。"},"description":[{"en":"Call it every frame and use the number like a size or a brightness: `circle(width / 2, height / 2, 60 + env('kick', 0.01, 0.3) * 200)` swells on every kick. The number rises to 1 in `attack` seconds after an event and falls back to 0 in `decay` seconds, quick at first and landing softly, the way a bass drum's swell looks right.","zh":"每個畫面都呼叫它，把這個數字當成大小或亮度來用：`circle(width / 2, height / 2, 60 + env('kick', 0.01, 0.3) * 200)` 讓圓在每一下大鼓都脹大。事件之後，數字在 `attack` 秒內升到 1，再用 `decay` 秒落回 0，一開始退得快，最後輕輕落地，就像大鼓的那一下看起來該有的樣子。"},{"en":"An event that arrives while the last pulse is still sounding restarts from where the pulse is, never from zero, so fast hits pile up into a plateau instead of flickering. The attack is measured from 0: a retrigger from halfway up takes half the attack.","zh":"上一個脈衝還沒退完時又來一個事件，會從脈衝現在的位置重新開始，不會回到 0，所以很快的連打會堆成一個平台，不會閃爍。attack 是從 0 算起的：從一半的地方再觸發，只花一半的 attack。"},{"en":"The events: `'kick'`, `'snare'` and `'hat'` are the hits [`kickDetected()`](#kickDetected) hears (from the microphone, or from the drum machine when it plays), `'beat'` is [`beat`](#beat), `'note'` is any key going down, `'bar'` and `'step'` come from the drum machine, a cue's name fires when [`cue()`](#cue) does, and any other name is yours to fire with [`trigger()`](#trigger). The switch form, `env(mouseIsPressed, 0.02, 0.5)`, fires on every press.","zh":"事件有這些：`'kick'`、`'snare'`、`'hat'` 就是 [`kickDetected()`](#kickDetected) 聽到的那些打擊（來自麥克風，或鼓機在播的時候來自鼓機），`'beat'` 是 [`beat`](#beat)，`'note'` 是任何一個琴鍵按下，`'bar'` 和 `'step'` 來自鼓機，cue 的名字在 [`cue()`](#cue) 觸發時發出，其他的名字由你用 [`trigger()`](#trigger) 發出。開關的寫法 `env(mouseIsPressed, 0.02, 0.5)` 在每次按下時發出事件。"}],"params":[{"name":"event","type":"String | Boolean | Number","optional":true,"default":"'beat'","en":"What to react to. A name: `'kick'`, `'snare'`, `'hat'`, `'beat'` (from the microphone or the drum machine), `'note'` (any key), `'bar'`, `'step'` (the drum machine), a cue's name, or an event of your own fired with [`trigger()`](#trigger). Or a switch: a boolean, or a number above 0.5, and each time it turns on counts as an event. Left out: `'beat'`.","zh":"要對什麼起反應。可以是名字：`'kick'`、`'snare'`、`'hat'`、`'beat'`（來自麥克風或鼓機）、`'note'`（任何琴鍵）、`'bar'`、`'step'`（鼓機）、某個 cue 的名字，或你自己用 [`trigger()`](#trigger) 發出的事件。也可以是開關：布林值，或大於 0.5 的數字，每次從關變成開就算一次事件。不寫就是 `'beat'`。"},{"name":"attack","type":"Number","optional":true,"default":"0.01","en":"Seconds the rise from 0 to 1 takes.","zh":"從 0 升到 1 要花的秒數。"},{"name":"decay","type":"Number","optional":true,"default":"0.3","en":"Seconds the fall from 1 back to 0 takes.","zh":"從 1 落回 0 要花的秒數。"},{"name":"name","type":"String | Number","optional":true,"en":"Only for the switch form (`env(mouseIsPressed, …, 'a')`): a name for its memory, as the last argument. A named event such as `'kick'` needs none.","zh":"只用在開關的寫法（`env(mouseIsPressed, …, 'a')`）：給它的記憶取的名字，寫在最後一個參數。像 `'kick'` 這種有名字的事件不需要。"}],"returns":{"type":"Number","en":"0..1: 0 when nothing has happened, 1 at the top of the pulse. Until @openav/chop has loaded (a few frames) it returns 0.","zh":"0 到 1：什麼都沒發生是 0，脈衝的頂點是 1。@openav/chop 還沒載好之前（幾個畫面）回傳 0。"},"needs":{"en":"`'kick'`, `'snare'`, `'hat'` and `'beat'` need something that makes them. If the sketch has not started the drum machine ([`startDrums()`](#startDrums)), the first `env('kick')` asks for the microphone, like reading [`micLevel`](#micLevel), and `?oav_mic=off` means nothing ever fires. Once the drum machine is started, the microphone is not opened for it. `'note'` needs a key: play one, press 🎹, or call [`OAVp5.play()`](#OAVp5.play). A name of your own needs [`trigger()`](#trigger): if nothing fires it within 4 seconds, a card says so.","zh":"`'kick'`、`'snare'`、`'hat'`、`'beat'` 要有東西去產生它們。草圖還沒啟動鼓機（[`startDrums()`](#startDrums)）的話，第一次 `env('kick')` 就會像讀 [`micLevel`](#micLevel) 一樣去要麥克風，而 `?oav_mic=off` 表示永遠不會有事件。鼓機一啟動，就不會為它去開麥克風。`'note'` 要有琴鍵：彈一個、按 🎹，或呼叫 [`OAVp5.play()`](#OAVp5.play)。你自己取的名字要靠 [`trigger()`](#trigger)：4 秒內沒有任何東西觸發它，會跳出一張卡片告訴你。"},"notes":[{"en":"A named event needs no place in `draw()`: `env('kick')` works in `setup()` or `mousePressed()` too. Only the switch form remembers anything between frames, and an unnamed switch outside `draw()` returns 0. Calls in `draw()` are matched by order, like [`lag()`](#lag).","zh":"有名字的事件不需要待在 `draw()` 裡：`env('kick')` 在 `setup()` 或 `mousePressed()` 裡也能用。只有開關的寫法會在畫面之間記東西，沒有名字的開關在 `draw()` 外面回傳 0。在 `draw()` 裡的呼叫跟 [`lag()`](#lag) 一樣，靠順序來對應。"},{"en":"`OAVp5.chop.env(…)` is this function under another name.","zh":"`OAVp5.chop.env(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const k = env(mouseIsPressed, 0.02, 0.5);   // 1 right after each press, then it fades\n  circle(width / 2, height / 2, 60 + k * 200);\n}","caption":{"en":"Click the canvas: the circle jumps open and fades back each time.","zh":"點一下畫布：圓每次都彈開，再慢慢退回去。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n  startDrums('bd*4, [~ sd]*2, hh*8', 108);   // a drum machine, so no microphone is needed\n}\n\nfunction draw() {\n  background(20);\n  const kick = env('kick', 0.01, 0.3);       // 1 on every kick, 0 again 0.3 s later\n  const snare = env('snare', 0.005, 0.15);\n  fill(255, 90, 31);\n  circle(120, 150, 40 + kick * 160);\n  fill(120, 190, 255);\n  circle(280, 150, 40 + snare * 160);\n}","caption":{"en":"Two circles swell on the drum machine's kick and snare. Click the canvas once if you hear nothing: browsers wait for a click before they play sound.","zh":"兩個圓分別在鼓機的大鼓和小鼓時脹大。如果沒聽到聲音，點一下畫布：瀏覽器要先有一次點擊才會播聲音。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.play(48 + floor(random(24)), 0.8);   // play a key, as a controller would\n}\n\nfunction draw() {\n  background(20);\n  const k = env('note', 0.005, 0.8);         // every key makes it flash\n  fill(255, 211, 107);\n  circle(width / 2, height / 2, 40 + k * 240);\n}","caption":{"en":"Click the canvas, or play a key on a controller or the 🎹: every key pulses the circle.","zh":"點一下畫布，或在控制器或 🎹 上彈一個鍵：每個鍵都讓圓跳一下。"},"run":"sketch","lang":"js"}],"related":["trigger","count","lag","beat","kickDetected","startDrums","OAVp5.chop"],"keywords":{"en":["envelope","env","trigger","pulse","attack","decay","flash","kick","beat","Trigger CHOP","TouchDesigner"],"zh":["包絡","脈衝","觸發","起音","衰減","閃一下","大鼓","拍子","TouchDesigner"]},"tbd":[],"claims":[{"before":"trigger('claim-boom'); env('claim-boom')","expr":"env('claim-boom', 0.01, 5)","min":0.5,"max":1}]},
{"id":"lag","key":"lag","name":"lag","aliases":["OAVp5.chop.lag"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"lag(value, [up], [down], [name])","source":{"file":"p5/oav-p5.js","line":3008,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3008"},"summary":{"en":"Follows `value` smoothly instead of jumping: up in `up` seconds, down in `down` seconds (TouchDesigner's Lag CHOP).","zh":"讓數值平順地跟上 `value`，不要跳：上升花 `up` 秒，下降花 `down` 秒（TouchDesigner 的 Lag CHOP）。"},"description":[{"en":"Wrap any number that jumps: `circle(width / 2, height / 2, 50 + lag(micLevel, 0.03, 0.4) * 300)` is a VU meter. It opens within 0.03 seconds when someone speaks and takes 0.4 seconds to close. `up` is the time to follow a rise, `down` the time to follow a fall. Leave `down` out and both are `up`. Leave both out and they are 0.1.","zh":"包住任何會跳的數字：`circle(width / 2, height / 2, 50 + lag(micLevel, 0.03, 0.4) * 300)` 就是一個音量表。有人說話時 0.03 秒內張開，要 0.4 秒才收回去。`up` 是跟上升的時間，`down` 是跟下降的時間。不寫 `down`，兩個都是 `up`。兩個都不寫，就都是 0.1。"},{"en":"The seconds are TouchDesigner's: `lag(x, 0.1)` moves like a Lag CHOP set to 0.1, so the numbers you learn here carry over. That is about the time the output takes to cover 90% of a jump (underneath, a one-pole lag whose time constant is the seconds divided by 2.3). The curve is the same at 30 and at 144 frames per second.","zh":"這裡的秒數跟 TouchDesigner 一樣：`lag(x, 0.1)` 的動法就像把 Lag CHOP 設成 0.1，所以在這裡學的數字可以直接帶到 TouchDesigner。意思大約是輸出走完一次跳動的 90% 所花的時間（底層是一階的 lag，時間常數是秒數除以 2.3）。每秒 30 格和每秒 144 格畫出來的曲線一樣。"},{"en":"The first value it sees is taken as it is, so nothing glides in from 0. A boolean counts as 0 or 1: `lag(mouseIsPressed, 0.2)` is a soft switch.","zh":"它看到的第一個值照單全收，所以不會從 0 慢慢滑進來。布林值算成 0 或 1：`lag(mouseIsPressed, 0.2)` 就是一個柔軟的開關。"}],"params":[{"name":"value","type":"Number | Boolean","en":"The number to follow. A boolean counts as 0 or 1. Anything that is not a finite number (a string, `NaN`) goes straight through and the lag keeps what it had.","zh":"要跟隨的數字。布林值算成 0 或 1。不是有限數字的東西（字串、`NaN`）會直接穿過去，lag 保留原來的狀態。"},{"name":"up","type":"Number","optional":true,"default":"0.1","en":"Seconds to follow a rise: about how long the output takes to cover 90% of a jump upward. Negative numbers count as 0.","zh":"跟上升的秒數：輸出大約花這麼久走完往上跳的 90%。負數算 0。"},{"name":"down","type":"Number","optional":true,"default":"up","en":"Seconds to follow a fall. Left out, it is the same as `up`.","zh":"跟下降的秒數。不寫的話跟 `up` 一樣。"},{"name":"name","type":"String | Number","optional":true,"en":"A name for this call's memory, written as the last argument. Optional. Without one, oav-p5 tells calls apart by their order in `draw()`; with one, the call is the same wherever it is made.","zh":"給這次呼叫的記憶取的名字，寫在最後一個參數。可以不寫。不寫的話，oav-p5 用 `draw()` 裡呼叫的先後順序來區分。有名字的呼叫，在哪裡呼叫都是同一個。"}],"returns":{"type":"Number","en":"The smoothed value. It returns `value` as it is until @openav/chop has loaded (a few frames), and for an unnamed call outside `draw()`.","zh":"平順過的數值。@openav/chop 還沒載好之前（幾個畫面），以及在 `draw()` 外面沒有名字的呼叫，原樣回傳 `value`。"},"needs":{"en":"Nothing to call or declare: write it in `draw()`. The first call loads @openav/chop from next to oav-p5.js, which takes a few frames.","zh":"不用呼叫別的東西，也不用宣告：直接寫在 `draw()` 裡。第一次呼叫時會從 oav-p5.js 旁邊載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"A sketch with its own variable or function called `lag` keeps it. `OAVp5.chop.lag(…)` is this function under another name.","zh":"草圖如果自己有叫 `lag` 的變數或函式，就由草圖保留。`OAVp5.chop.lag(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const x = lag(mouseX, 0.5);   // follows the mouse, half a second behind\n  circle(x, height / 2, 40);\n}","caption":{"en":"A circle that chases the mouse: it never jumps, it glides, and takes about half a second to catch up.","zh":"一個追著滑鼠跑的圓：它不會跳，而是滑過去，大約半秒追上。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const open = lag(mouseIsPressed, 0.15, 0.8);   // a boolean counts as 0 or 1\n  fill(255, 211, 107);\n  circle(width / 2, height / 2, 40 + open * 240);\n}","caption":{"en":"Hold the mouse down: the circle swells quickly and, when you let go, shrinks back slowly.","zh":"按住滑鼠：圓很快脹大，放開之後慢慢縮回去。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  for (let i = 0; i < 6; i++) {\n    const seconds = 0.1 + i * 0.12;\n    const x = lag(mouseX, seconds, seconds, 'dot' + i);   // a name per dot\n    fill(255, 211, 107);\n    circle(x, 40 + i * 44, 24);\n  }\n}","caption":{"en":"Six dots follow the mouse, each a little slower than the one above. The names keep each dot's memory its own even if you later add or remove dots.","zh":"六個圓點跟著滑鼠，每一個都比上面那個慢一點。有了名字，之後就算增減圓點，每一個記得的還是自己的位置。"},"run":"sketch","lang":"js"}],"related":["spring","env","micLevel","knob1","trail","scope","OAVp5.chop"],"keywords":{"en":["lag","smooth","smoothing","ease","follow","low pass","damping","inertia","vu meter","Lag CHOP","TouchDesigner"],"zh":["平滑","緩動","延遲","跟隨","慣性","抖動","不要抖","音量表","TouchDesigner"]},"tbd":[],"claims":[{"expr":"lag(7, 0.5, 0.5, 'claim-first')","equals":7}]},
{"id":"scope","key":"scope","name":"scope","aliases":["OAVp5.chop.scope"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"scope(value, [label])","source":{"file":"p5/oav-p5.js","line":3072,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3072"},"summary":{"en":"Shows `value` in a small oscilloscope in the corner of the page and gives it back, so you can wrap any expression to watch it.","zh":"把 `value` 顯示在網頁角落的小示波器上，並原樣還給你，所以可以包住任何運算式來觀察它。"},"description":[{"en":"It is a CHOP's viewer: each label is one line in a small plot across the page from the 🎹, with the last 4 seconds of the value and its number now. Call it twice in a frame with two labels and you get two lines to compare, raw against smoothed. The scale is at least 0 to 1 and grows to hold whatever the values reach.","zh":"它就是 CHOP 的檢視器：每個標籤是一條線，畫在跟 🎹 對角的網頁角落一個小圖上，顯示這個值最近 4 秒的樣子和現在的數字。在一個畫面裡用兩個標籤呼叫兩次，就有兩條線可以比較，例如原始的和平滑過的。刻度至少是 0 到 1，值跑出去就跟著放大。"},{"en":"It lives on the page, not on the canvas: never in a recording or in 📷, and it never takes a click from the sketch. To draw the same line inside the work, use [`drawTrail()`](#drawTrail).","zh":"它在網頁上，不在畫布上：不會錄進影片，也不會進 📷，而且不會搶走草圖的點擊。要把同樣的線畫進作品裡，用 [`drawTrail()`](#drawTrail)。"}],"params":[{"name":"value","type":"Number | Boolean","en":"The number to watch. A boolean is drawn as 0 or 1. Anything that is not a finite number is returned as it is and not drawn.","zh":"要觀察的數字。布林值畫成 0 或 1。不是有限數字的東西原樣回傳，也不會畫。"},{"name":"label","type":"String","optional":true,"en":"The name of this line in the legend. Each label is one line, and a label longer than 9 characters is cut short there. Without one the line is numbered.","zh":"這條線在圖例上的名字。每個標籤是一條線，超過 9 個字元的標籤在圖例上會被截短。不寫的話，這條線用編號。"}],"returns":{"type":"Number | Boolean","en":"Exactly `value`, so `circle(x, y, scope(lag(micLevel, 0.03, 0.4), 'lag') * 300)` works.","zh":"原封不動的 `value`，所以 `circle(x, y, scope(lag(micLevel, 0.03, 0.4), 'lag') * 300)` 這樣寫就行。"},"needs":{"en":"Only inside `draw()`: anywhere else it just returns `value`. `?oav_scope=off` hides it, and so does `?oav_hints=off`, which is the one thing to know if it does not appear. See [`scope`](#option-scope). The first call loads @openav/chop, which takes a few frames.","zh":"只在 `draw()` 裡有作用：其他地方它只會原樣回傳 `value`。`?oav_scope=off` 會把它藏起來，`?oav_hints=off` 也會，這是它沒出現時唯一要知道的事。見 [`scope`](#option-scope)。第一次呼叫會載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"`OAVp5.chop.scope(…)` is this function under another name. The lines keep coming from the last second: stop calling it for a second and its line leaves the plot.","zh":"`OAVp5.chop.scope(…)` 是同一個函式的另一個名字。線條只保留最近一秒內還有呼叫的：停止呼叫一秒之後，那條線就會從圖上消失。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const wave = 0.5 + 0.5 * sin(frameCount * 0.05);\n  circle(width / 2, height / 2, 40 + scope(wave, 'wave') * 200);   // watch it in the corner\n}","caption":{"en":"The circle breathes, and a small plot in the corner of the page draws the same number as a moving line.","zh":"圓一漲一縮，網頁角落的小圖把同一個數字畫成一條移動的線。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const raw = frameCount % 120 < 60 ? 1 : 0;   // a square wave\n  scope(raw, 'raw');\n  const smooth = scope(lag(raw, 0.3, 0.6), 'lag');\n  fill(255, 211, 107);\n  circle(width / 2, height / 2, 40 + smooth * 200);\n}","caption":{"en":"The corner plot shows two lines: the square wave and what lag() makes of it, rising in 0.3 seconds and falling in 0.6.","zh":"角落的圖有兩條線：方波，以及 lag() 把它變成的樣子，上升 0.3 秒、下降 0.6 秒。"},"run":"sketch","lang":"js"}],"related":["drawTrail","trail","lag","option:scope","option:hints"],"keywords":{"en":["scope","oscilloscope","plot","graph","debug","watch a value","monitor","viewer"],"zh":["示波器","圖表","除錯","觀察數值","監看","檢視器","scope"]},"tbd":[],"claims":[]},
{"id":"slope","key":"slope","name":"slope","aliases":["OAVp5.chop.slope"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"slope(value, [name])","source":{"file":"p5/oav-p5.js","line":3052,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3052"},"summary":{"en":"How fast `value` is changing, per second: positive while it rises, negative while it falls, 0 while it holds still (TouchDesigner's Slope CHOP).","zh":"`value` 每秒變化多快：上升時是正的，下降時是負的，靜止時是 0（TouchDesigner 的 Slope CHOP）。"},"description":[{"en":"Ask how hard something is moving rather than where it is: `abs(slope(mouseX))` is the mouse speed in pixels per second, and `slope(micLevel)` jumps up at the start of a word. It is as noisy as its input, so smooth it when it flickers: `lag(abs(slope(x)), 0.05, 0.3)`.","zh":"問的是東西動得多用力，而不是在哪裡：`abs(slope(mouseX))` 是滑鼠每秒移動幾像素，`slope(micLevel)` 在一個字開頭的時候會往上衝。它的雜訊跟輸入一樣多，閃爍的時候先平滑一下：`lag(abs(slope(x)), 0.05, 0.3)`。"}],"params":[{"name":"value","type":"Number | Boolean","en":"The number to watch. A boolean counts as 0 or 1. Anything that is not a finite number keeps the last slope.","zh":"要觀察的數字。布林值算成 0 或 1。不是有限數字的東西，就保留上一個斜率。"},{"name":"name","type":"String | Number","optional":true,"en":"A name for this call's memory, written as the last argument. Optional. Without one, oav-p5 tells calls apart by their order in `draw()`; with one, the call is the same wherever it is made.","zh":"給這次呼叫的記憶取的名字，寫在最後一個參數。可以不寫。不寫的話，oav-p5 用 `draw()` 裡呼叫的先後順序來區分。有名字的呼叫，在哪裡呼叫都是同一個。"}],"returns":{"type":"Number","en":"The change per second, in the units of `value` (pixels per second for a pixel position). 0 on the first call, and 0 until @openav/chop has loaded or for an unnamed call outside `draw()`.","zh":"每秒的變化量，單位跟 `value` 一樣（像素位置就是每秒幾像素）。第一次呼叫是 0。@openav/chop 還沒載好之前，以及在 `draw()` 外面沒有名字的呼叫，也是 0。"},"needs":{"en":"Nothing to call or declare: write it in `draw()`. The first call loads @openav/chop, which takes a few frames.","zh":"不用呼叫別的東西，也不用宣告：直接寫在 `draw()` 裡。第一次呼叫會載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"`OAVp5.chop.slope(…)` is this function under another name.","zh":"`OAVp5.chop.slope(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(16);\n}\n\nfunction draw() {\n  background(20);\n  const v = abs(slope(mouseX));   // pixels per second\n  fill(255);\n  text(nf(v, 1, 0) + ' px/s', 20, 120);\n  rect(20, 140, min(v / 10, width - 40), 24);\n}","caption":{"en":"Move the mouse sideways: the bar and the number show how fast you are going, and drop to 0 when you stop.","zh":"左右移動滑鼠：長條和數字顯示你動得多快，停下來就回到 0。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const r = spring(mouseIsPressed ? 140 : 60, 100, 6);   // a radius that bounces\n  const moving = lag(abs(slope(r)), 0.05, 0.3);          // how fast it is changing, smoothed\n  noFill();\n  stroke(255, 211, 107);\n  strokeWeight(2 + min(14, moving / 40));\n  circle(width / 2, height / 2, r * 2);\n}","caption":{"en":"Hold the mouse: the ring grows and bounces, and its outline is thickest while it is changing fastest.","zh":"按住滑鼠：圓環長大、彈跳，變化最快的時候外框最粗。"},"run":"sketch","lang":"js"}],"related":["lag","speed","spring","trail","OAVp5.chop"],"keywords":{"en":["slope","derivative","velocity","rate of change","how fast","delta","Slope CHOP","TouchDesigner"],"zh":["斜率","變化率","速度","微分","動得多快","TouchDesigner"]},"tbd":[],"claims":[]},
{"id":"speed","key":"speed","name":"speed","aliases":["OAVp5.chop.speed"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"speed(rate, [name])","source":{"file":"p5/oav-p5.js","line":3034,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3034"},"summary":{"en":"Adds `rate` to a running total every second, so `rotate(speed(TWO_PI))` turns once a second and the rate can change without the total ever jumping.","zh":"每秒把 `rate` 加進一個累計值，所以 `rotate(speed(TWO_PI))` 每秒轉一圈，而且速率隨時可以變，累計值不會跳（TouchDesigner 的 Speed CHOP）。"},"description":[{"en":"Use the total to turn or move something: `rotate(speed(TWO_PI))` is one turn a second, `x = speed(60) % width` slides 60 pixels a second and wraps around. You pass the rate, not the position, so the rate can follow a knob or the microphone (`speed(micLevel * TWO_PI)` spins faster the louder it gets) and the angle never jumps, which `frameCount * rate` would do.","zh":"用累計值來轉或移動東西：`rotate(speed(TWO_PI))` 是每秒轉一圈，`x = speed(60) % width` 是每秒滑 60 像素，到邊界再繞回來。你傳的是速率，不是位置，所以速率可以跟著旋鈕或麥克風走（`speed(micLevel * TWO_PI)` 越大聲轉越快），角度也不會跳，換成 `frameCount * rate` 就會跳。"},{"en":"Time is in seconds, the same at any frame rate. One frame counts for at most a quarter of a second, so coming back to a hidden tab does not make the shape leap.","zh":"時間是以秒計，任何畫面更新率都一樣。一個畫面最多只算四分之一秒，所以從被藏起來的分頁回來時，圖形不會一下子衝出去。"}],"params":[{"name":"rate","type":"Number | Boolean","en":"How much to add per second, in the unit you use the result in (radians for `rotate()`, pixels for a position). It may change every frame. A boolean counts as 0 or 1.","zh":"每秒要加多少，單位看你怎麼用結果（給 `rotate()` 就是弧度，給位置就是像素）。每個畫面都可以不一樣。布林值算成 0 或 1。"},{"name":"name","type":"String | Number","optional":true,"en":"A name for this call's memory, written as the last argument. Optional. Without one, oav-p5 tells calls apart by their order in `draw()`; with one, the call is the same wherever it is made.","zh":"給這次呼叫的記憶取的名字，寫在最後一個參數。可以不寫。不寫的話，oav-p5 用 `draw()` 裡呼叫的先後順序來區分。有名字的呼叫，在哪裡呼叫都是同一個。"}],"returns":{"type":"Number","en":"The running total, 0 at the start. It is 0 until @openav/chop has loaded (a few frames), and for an unnamed call outside `draw()`.","zh":"累計值，一開始是 0。@openav/chop 還沒載好之前（幾個畫面），以及在 `draw()` 外面沒有名字的呼叫，都回傳 0。"},"needs":{"en":"Nothing to call or declare: write it in `draw()`. The first call loads @openav/chop, which takes a few frames.","zh":"不用呼叫別的東西，也不用宣告：直接寫在 `draw()` 裡。第一次呼叫會載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"Plenty of sketches have a variable called `speed`. If yours does, it keeps the name, and `OAVp5.chop.speed(…)` is this function under another name (third example).","zh":"很多草圖都有一個叫 `speed` 的變數。你的草圖有的話，名稱就歸你的草圖，而 `OAVp5.chop.speed(…)` 是同一個函式的另一個名字（第三個範例）。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  rectMode(CENTER);\n}\n\nfunction draw() {\n  background(20);\n  translate(width / 2, height / 2);\n  rotate(speed(TWO_PI));   // one turn a second\n  rect(0, 0, 120, 120);\n}","caption":{"en":"A square that turns once a second, steadily.","zh":"一個方塊，每秒平穩地轉一圈。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  rectMode(CENTER);\n}\n\nfunction draw() {\n  background(20);\n  const rate = map(mouseX, 0, width, -TWO_PI, TWO_PI);   // turns per second, left to right\n  translate(width / 2, height / 2);\n  rotate(speed(rate));\n  rect(0, 0, 120, 120);\n}","caption":{"en":"Move the mouse: the left side spins the square one way, the middle stops it, the right side spins it the other way, and it never jumps.","zh":"移動滑鼠：左邊讓方塊往一個方向轉，中間讓它停下，右邊往另一個方向轉，而且不會跳。"},"run":"sketch","lang":"js"},{"code":"let speed = 2;   // the sketch's own variable called speed\n\nfunction setup() {\n  createCanvas(400, 300);\n  rectMode(CENTER);\n}\n\nfunction draw() {\n  background(20);\n  translate(width / 2, height / 2);\n  rotate(OAVp5.chop.speed(speed));   // the same function, under another name\n  rect(0, 0, 120, 120);\n}","caption":{"en":"The sketch keeps its own `speed` (2 radians a second) and reaches the function as OAVp5.chop.speed.","zh":"草圖保留自己的 `speed`（每秒 2 弧度），並用 OAVp5.chop.speed 呼叫這個函式。"},"run":"sketch","lang":"js"}],"related":["lag","spring","slope","OAVp5.chop"],"keywords":{"en":["speed","rotate","spin","integrate","accumulate","rate","angle","Speed CHOP","TouchDesigner"],"zh":["速度","旋轉","轉動","累計","積分","角度","TouchDesigner"]},"tbd":[],"claims":[]},
{"id":"spring","key":"spring","name":"spring","aliases":["OAVp5.chop.spring"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"spring(value, [stiffness], [damping], [name])","source":{"file":"p5/oav-p5.js","line":3039,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3039"},"summary":{"en":"Follows `value` like a mass on a spring: it overshoots, bounces and settles (TouchDesigner's Spring CHOP).","zh":"像彈簧上的重物那樣跟上 `value`：會衝過頭、彈幾下、然後穩住（TouchDesigner 的 Spring CHOP）。"},"description":[{"en":"Wrap a target that jumps: `circle(width / 2, height / 2, spring(mouseIsPressed ? 300 : 100, 100, 6))`. The size does not just glide to the new target (that is [`lag()`](#lag)): it swings past it, comes back and settles.","zh":"包住一個會跳的目標：`circle(width / 2, height / 2, spring(mouseIsPressed ? 300 : 100, 100, 6))`。大小會衝過頭、回來、再穩住，跟單純滑向新目標的 [`lag()`](#lag) 不一樣。"},{"en":"Stiffness and damping are the two knobs. A stiff, lightly damped spring rings like a ruler on a table. A soft, heavily damped one just eases. A big jump or a low frame rate does not break it: the motion is the same at any frame rate.","zh":"剛性（stiffness）和阻尼（damping）是兩個旋鈕。很硬、阻尼很小的彈簧像桌邊的尺一樣一直顫。很軟、阻尼很大的就只是緩緩靠近。目標跳很大，或畫面更新率很低，都不會弄壞它：任何更新率的動作都一樣。"}],"params":[{"name":"value","type":"Number | Boolean","en":"The target the spring is pulled toward. A boolean counts as 0 or 1.","zh":"彈簧被拉向的目標。布林值算成 0 或 1。"},{"name":"stiffness","type":"Number","optional":true,"default":"100","en":"How fast it swings: 100 swings about 1.6 times a second. Higher is snappier. Negative numbers count as 0.","zh":"擺動得多快：100 大約每秒擺 1.6 次。越大越俐落。負數算 0。"},{"name":"damping","type":"Number","optional":true,"default":"6","en":"How soon it settles: 6 gives two or three bounces, 20 and up none. The edge where it stops bouncing is `2 * sqrt(stiffness)`.","zh":"多快穩下來：6 會彈兩三下，20 以上就不彈了。剛好不再彈的界線是 `2 * sqrt(stiffness)`。"},{"name":"name","type":"String | Number","optional":true,"en":"A name for this call's memory, written as the last argument. Optional. Without one, oav-p5 tells calls apart by their order in `draw()`; with one, the call is the same wherever it is made.","zh":"給這次呼叫的記憶取的名字，寫在最後一個參數。可以不寫。不寫的話，oav-p5 用 `draw()` 裡呼叫的先後順序來區分。有名字的呼叫，在哪裡呼叫都是同一個。"}],"returns":{"type":"Number","en":"The spring's position now. The first value it sees is taken as it is. It returns `value` as it is until @openav/chop has loaded (a few frames), and for an unnamed call outside `draw()`.","zh":"彈簧現在的位置。看到的第一個值照單全收。@openav/chop 還沒載好之前（幾個畫面），以及在 `draw()` 外面沒有名字的呼叫，原樣回傳 `value`。"},"needs":{"en":"Nothing to call or declare: write it in `draw()`. The first call loads @openav/chop, which takes a few frames.","zh":"不用呼叫別的東西，也不用宣告：直接寫在 `draw()` 裡。第一次呼叫會載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"`OAVp5.chop.spring(…)` is this function under another name.","zh":"`OAVp5.chop.spring(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const size = spring(mouseIsPressed ? 300 : 100, 100, 6);   // overshoots, then settles\n  circle(width / 2, height / 2, size);\n}","caption":{"en":"Hold the mouse: the circle springs open, overshoots and settles. Let go and it bounces back down.","zh":"按住滑鼠：圓彈開、衝過頭、再穩住。放開就彈回去。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const x = spring(mouseX, 60, 4);   // soft and bouncy\n  const y = spring(mouseY, 60, 4);\n  circle(x, y, 40);\n}","caption":{"en":"A circle hung on the mouse by a soft spring: it swings around the pointer instead of sticking to it.","zh":"一個用軟彈簧掛在滑鼠上的圓：它繞著滑鼠擺動，不會黏在上面。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const target = mouseIsPressed ? 340 : 60;\n  const dampings = [2, 6, 20];\n  for (let i = 0; i < 3; i++) {\n    const x = spring(target, 100, dampings[i]);   // same pull, three dampings\n    circle(x, 60 + i * 90, 36);\n  }\n}","caption":{"en":"Hold the mouse: three circles are pulled right by the same spring with different damping. The top one bounces many times, the middle one a few, the bottom one not at all.","zh":"按住滑鼠：三個圓被同一個彈簧、不同的阻尼往右拉。最上面的彈很多下，中間的彈幾下，最下面的完全不彈。"},"run":"sketch","lang":"js"}],"related":["lag","speed","slope","OAVp5.chop"],"keywords":{"en":["spring","bounce","overshoot","elastic","damping","stiffness","physics","Spring CHOP","TouchDesigner"],"zh":["彈簧","彈跳","過衝","彈性","阻尼","剛性","物理","TouchDesigner"]},"tbd":[],"claims":[]},
{"id":"trail","key":"trail","name":"trail","aliases":["OAVp5.chop.trail"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"trail(value, [seconds], [name])","source":{"file":"p5/oav-p5.js","line":3050,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3050"},"summary":{"en":"The last few seconds of `value` as an array, 60 values a second, oldest first and newest last (TouchDesigner's Trail CHOP).","zh":"`value` 最近幾秒的歷史，做成一個陣列，每秒 60 個值，最舊的在前、最新的在最後（TouchDesigner 的 Trail CHOP）。"},"description":[{"en":"A history to draw or measure: `drawTrail(trail(micLevel, 3), 20, 20, width - 40, 100)` plots the last three seconds of the microphone level, and `Math.max(...trail(x, 2))` is a two-second peak. The values are sampled 60 times a second whatever the frame rate, and the gaps between two frames are interpolated, so 30 fps and 144 fps give the same shape.","zh":"要畫出來或量一量的歷史：`drawTrail(trail(micLevel, 3), 20, 20, width - 40, 100)` 畫出麥克風音量最近三秒的樣子，`Math.max(...trail(x, 2))` 是兩秒內的最高值。不管畫面更新率是多少，都是每秒取 60 個值，兩個畫面之間的空隙用內插補起來，所以每秒 30 格和 144 格得到同樣的形狀。"},{"en":"Change `seconds` between frames and the history is resized, keeping the newest values. The array you got last frame is never changed afterwards.","zh":"在畫面之間改 `seconds`，歷史會跟著調整大小，保留最新的值。上一個畫面拿到的陣列之後不會再被改動。"}],"params":[{"name":"value","type":"Number | Boolean","en":"The number to record. A boolean counts as 0 or 1. Anything that is not a finite number is skipped.","zh":"要記錄的數字。布林值算成 0 或 1。不是有限數字的東西會被略過。"},{"name":"seconds","type":"Number","optional":true,"default":"2","en":"How many seconds of history to keep, from 0.05 to 600.","zh":"要保留幾秒的歷史，範圍是 0.05 到 600。"},{"name":"name","type":"String | Number","optional":true,"en":"A name for this call's memory, written as the last argument. Optional. Without one, oav-p5 tells calls apart by their order in `draw()`; with one, the call is the same wherever it is made.","zh":"給這次呼叫的記憶取的名字，寫在最後一個參數。可以不寫。不寫的話，oav-p5 用 `draw()` 裡呼叫的先後順序來區分。有名字的呼叫，在哪裡呼叫都是同一個。"}],"returns":{"type":"Array<Number>","en":"About `seconds * 60 + 1` numbers (121 for 2 seconds), a new array every call. The length is fixed from the first call and filled with the first value, so you can draw it from frame one. It is an empty array until @openav/chop has loaded, and for an unnamed call outside `draw()`.","zh":"大約 `seconds * 60 + 1` 個數字（2 秒是 121 個），每次呼叫都是一個新的陣列。長度從第一次呼叫就固定，並且用第一個值填滿，所以從第一個畫面就能畫。@openav/chop 還沒載好之前，以及在 `draw()` 外面沒有名字的呼叫，是空陣列。"},"needs":{"en":"Nothing to call or declare: write it in `draw()`. The first call loads @openav/chop, which takes a few frames.","zh":"不用呼叫別的東西，也不用宣告：直接寫在 `draw()` 裡。第一次呼叫會載入 @openav/chop，要幾個畫面。"},"notes":[{"en":"Unnamed calls find their memory by their order in `draw()`: the first `lag()` is the first lag, the second is the second. Put one inside an `if`, or in a loop whose length changes, and the order slips: a card says so, and from then on oav-p5 matches each call by the line it is written on. A name at the end is the sure way. Outside `draw()` an unnamed call has nothing to remember by: it returns what Returns says and a card says so.","zh":"沒有名字的呼叫靠它在 `draw()` 裡的順序找回自己的記憶：第一個 `lag()` 是第一個 lag，第二個是第二個。放進 `if`，或放進長度會變的迴圈，順序就會對不上。這時會跳出一張卡片，從那之後 oav-p5 改成照「寫在哪一行」來對應。最穩的做法是在最後加一個名字。在 `draw()` 外面，沒有名字的呼叫沒有東西可以記，會回傳「回傳」欄寫的值，也會跳出卡片。"},{"en":"`OAVp5.chop.trail(…)` is this function under another name. To see the line on the canvas use [`drawTrail()`](#drawTrail), to see it in the corner of the page use [`scope()`](#scope).","zh":"`OAVp5.chop.trail(…)` 是同一個函式的另一個名字。要在畫布上看這條線用 [`drawTrail()`](#drawTrail)，要在網頁角落看就用 [`scope()`](#scope)。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const wave = 0.5 + 0.5 * sin(frameCount * 0.05);   // 0..1\n  stroke(255);\n  drawTrail(trail(wave, 3), 20, 100, width - 40, 100);   // the last 3 seconds\n}","caption":{"en":"A sine wave drawn from its own history: it scrolls to the left as new values arrive at the right.","zh":"用自己的歷史畫出一條正弦波：新的值從右邊進來，波形往左捲動。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction draw() {\n  background(20);\n  const t = trail(mouseX, 1);   // where the mouse was during the last second\n  for (let i = 0; i < t.length; i += 6) {\n    fill(255, 211, 107, 10 + (i / t.length) * 200);\n    circle(t[i], mouseY, 24);\n  }\n}","caption":{"en":"A ghost trail behind the pointer: the older the copy, the fainter it is.","zh":"滑鼠後面拖著一串殘影：越舊的越淡。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const t = trail(mouseX, 3);   // the last 3 seconds of the mouse position\n  const left = Math.min(...t), right = Math.max(...t);\n  noStroke();\n  fill(255, 211, 107, 90);\n  rect(left, 0, right - left, height);   // the band the mouse covered\n  stroke(255);\n  drawTrail(t, 0, 20, width, 60, 0, width);\n}","caption":{"en":"A band shows how far left and right the mouse went in the last three seconds, with the trail itself drawn across the top.","zh":"一條色帶顯示滑鼠最近三秒往左往右走了多遠，上方畫出軌跡本身。"},"run":"sketch","lang":"js"}],"related":["drawTrail","scope","lag","slope","OAVp5.chop"],"keywords":{"en":["trail","history","buffer","record values","past","oscilloscope","waveform","Trail CHOP","TouchDesigner"],"zh":["軌跡","歷史","緩衝","殘影","波形","示波器","TouchDesigner"]},"tbd":[],"claims":[{"before":"trail(0.5, 2, 'claim-trail')","expr":"trail(0.5, 2, 'claim-trail').length","equals":121}]},
{"id":"trigger","key":"trigger","name":"trigger","aliases":["OAVp5.chop.trigger"],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"trigger([name])","source":{"file":"p5/oav-p5.js","line":3032,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3032"},"summary":{"en":"Fires an event of your own, by name, for [`env()`](#env) and [`count()`](#count) to react to.","zh":"用名字發出一個你自己的事件，讓 [`env()`](#env) 和 [`count()`](#count) 去對它起反應。"},"description":[{"en":"Call it where the thing happens: `function mousePressed() { trigger('boom'); }`. Then read it anywhere in `draw()` with `env('boom', 0.01, 0.6)` (a pulse that fades) or `count('boom')` (how many times so far). It returns nothing.","zh":"在事情發生的地方呼叫它：`function mousePressed() { trigger('boom'); }`。然後在 `draw()` 的任何地方，用 `env('boom', 0.01, 0.6)`（會退下去的脈衝）或 `count('boom')`（到現在發生幾次）去讀它。它沒有回傳值。"},{"en":"It works anywhere, not only in `draw()`, and each call is one event, remembered with the time it happened. You can fire the built-in names by hand as well: `trigger('kick')` pulses `env('kick')` as a real kick would.","zh":"它在哪裡都能用，不只是 `draw()`，每呼叫一次就是一個事件，連同發生的時間一起被記下來。內建的名字也可以用手發出：`trigger('kick')` 會讓 `env('kick')` 跳一下，就像真的大鼓一樣。"}],"params":[{"name":"name","type":"String","optional":true,"default":"'trigger'","en":"The name of the event. Anything is fine except the built-in meanings: `'kick'`, `'snare'`, `'hat'`, `'beat'`, `'note'`, `'bar'` and `'step'` are also fired by the microphone, the keys and the drum machine. Left out: `'trigger'`.","zh":"事件的名字。隨便取，只是要留意內建的意思：`'kick'`、`'snare'`、`'hat'`、`'beat'`、`'note'`、`'bar'`、`'step'` 也會被麥克風、琴鍵和鼓機發出。不寫就是 `'trigger'`。"}],"notes":[{"en":"`OAVp5.chop.trigger(…)` is this function under another name.","zh":"`OAVp5.chop.trigger(…)` 是同一個函式的另一個名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  trigger('boom');   // an event of our own\n}\n\nfunction draw() {\n  background(20);\n  const k = env('boom', 0.01, 0.6);\n  circle(width / 2, height / 2, 60 + k * 220);\n}","caption":{"en":"Click the canvas: each click fires \"boom\" and the circle pulses once.","zh":"點一下畫布：每點一下就發出一次 \"boom\"，圓跳一下。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  noStroke();\n}\n\nfunction mousePressed() { trigger('boom'); }\nfunction keyPressed() { trigger('spark'); }\n\nfunction draw() {\n  background(20);\n  fill(255, 90, 31);\n  circle(120, 150, 40 + env('boom', 0.01, 0.6) * 160);\n  fill(120, 190, 255);\n  circle(280, 150, 40 + env('spark', 0.005, 0.2) * 160);\n}","caption":{"en":"Click for the orange circle, press any key for the blue one: each reacts only to its own event.","zh":"點一下讓橘色的圓跳，按任何一個鍵讓藍色的圓跳：各自只對自己的事件起反應。"},"run":"sketch","lang":"js"}],"related":["env","count","OAVp5.chop"],"keywords":{"en":["trigger","fire","event","custom event","emit","signal","Trigger CHOP"],"zh":["觸發","事件","自訂事件","發出","訊號","TouchDesigner"]},"tbd":[],"claims":[{"before":"trigger('claim-count'); trigger('claim-count'); trigger('claim-count'); count('claim-count')","expr":"count('claim-count')","equals":3}]},
{"id":"OAVp5.chop","key":"OAVp5.chop","name":"OAVp5.chop","aliases":[],"kind":"object","package":"oav-p5","category":"chop","documented":true,"signature":"OAVp5.chop","source":{"file":"p5/oav-p5.js","line":2861,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2861"},"summary":{"en":"The CHOP functions under one roof, plus the state of the module behind them: `OAVp5.chop.lag(…)`, `.state`, `.slots()`, `.reset()`.","zh":"CHOP 函式放在同一個地方，加上它們背後模組的狀態：`OAVp5.chop.lag(…)`、`.state`、`.slots()`、`.reset()`。"},"description":[{"en":"Every function of this group is also here: `OAVp5.chop.lag`, `.env`, `.trigger`, `.speed`, `.spring`, `.trail`, `.slope`, `.count`, `.scope` and `.drawTrail` are the same functions as the plain names, for a sketch whose own `speed` or `count` keeps the plain name.","zh":"這一組的每個函式這裡都有：`OAVp5.chop.lag`、`.env`、`.trigger`、`.speed`、`.spring`、`.trail`、`.slope`、`.count`、`.scope`、`.drawTrail` 跟一般的名稱是同一批函式，給那些自己的 `speed` 或 `count` 保留了一般名稱的草圖用。"},{"en":"The rest is for looking inside: [`state`](#OAVp5.chop.state) says whether the module is loaded, [`module`](#OAVp5.chop.module) is the module itself, [`slots()`](#OAVp5.chop.slots) lists every memory in use and [`reset()`](#OAVp5.chop.reset) forgets them.","zh":"其餘的是用來看裡面的：[`state`](#OAVp5.chop.state) 說模組載好了沒，[`module`](#OAVp5.chop.module) 就是模組本身，[`slots()`](#OAVp5.chop.slots) 列出正在用的每一個記憶，[`reset()`](#OAVp5.chop.reset) 把它們忘掉。"}],"params":[],"notes":[{"en":"This is advanced: a sketch that only calls `lag()` and friends never needs it.","zh":"這是進階用法：只呼叫 `lag()` 這些函式的草圖用不到它。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const x = OAVp5.chop.lag(mouseX, 0.4);   // the same as lag(mouseX, 0.4)\n  fill(255);\n  text('chop ' + OAVp5.chop.state + ', ' + OAVp5.chop.slots().length + ' memory in use', 20, 30);\n  circle(x, height / 2, 40);\n}","caption":{"en":"A circle chasing the mouse through OAVp5.chop.lag, with the module's state and the number of memories in use printed above it.","zh":"一個透過 OAVp5.chop.lag 追著滑鼠的圓，上方印出模組的狀態和正在用的記憶數量。"},"run":"sketch","lang":"js"}],"related":["lag","env","OAVp5.chop.state","OAVp5.chop.slots","OAVp5.chop.reset"],"keywords":{"en":["OAVp5.chop","chop object","chop module","values over time","TouchDesigner"],"zh":["chop 物件","chop 模組","數值隨時間變化","TouchDesigner"]},"tbd":[],"claims":[]},
{"id":"OAVp5.chop.module","key":"OAVp5.chop.module","name":"OAVp5.chop.module","aliases":[],"kind":"variable","package":"oav-p5","category":"chop","documented":true,"signature":"OAVp5.chop.module","source":{"file":"p5/oav-p5.js","line":2842,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2842"},"summary":{"en":"The @openav/chop module itself once it has loaded, otherwise `null`: the classes and functions the CHOP functions are built on.","zh":"@openav/chop 模組本身，載好之後才有，之前是 `null`：CHOP 函式底下用的那些類別和函式。"},"description":[{"en":"For reaching below the p5-style calls: `OAVp5.chop.module.DEFAULTS` lists the defaults the functions use, and `new OAVp5.chop.module.Lag({ up: 0.1 })` makes a lag you keep yourself. Check it is not `null` first.","zh":"要用到比 p5 風格呼叫更底層的東西時才需要：`OAVp5.chop.module.DEFAULTS` 列出這些函式用的預設值，`new OAVp5.chop.module.Lag({ up: 0.1 })` 做出一個你自己保管的 lag。先確認它不是 `null`。"}],"value":{"type":"Object | null","en":"`null` until the first CHOP function has been called and the module has arrived, then the module (`Lag`, `Spring`, `Trail`, `envelope()`, `DEFAULTS` and the rest).","zh":"第一次呼叫 CHOP 函式、而且模組載好之前是 `null`，之後就是那個模組（`Lag`、`Spring`、`Trail`、`envelope()`、`DEFAULTS` 等等）。"},"params":[],"notes":[{"en":"This is advanced. The module is also published as `@openav/chop` for sketches and shows that do not go through oav-p5.","zh":"這是進階用法。這個模組也以 `@openav/chop` 發布，給不經過 oav-p5 的草圖和演出使用。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  lag(mouseX, 0.3, 0.3, 'warm-up');   // the first call loads the module\n  const m = OAVp5.chop.module;\n  fill(255);\n  text(m ? 'spring defaults: ' + JSON.stringify(m.DEFAULTS.spring) : 'not loaded yet', 20, 30);\n}","caption":{"en":"Prints the spring defaults from the module as soon as it has loaded.","zh":"模組一載好，就印出它裡面 spring 的預設值。"},"run":"sketch","lang":"js"}],"related":["OAVp5.chop.state","OAVp5.chop"],"keywords":{"en":["chop module","@openav/chop","Lag class","DEFAULTS"],"zh":["chop 模組","@openav/chop","預設值"]},"tbd":[],"claims":[{"expr":"OAVp5.chop.module === null","equals":true}]},
{"id":"OAVp5.chop.reset","key":"OAVp5.chop.reset","name":"OAVp5.chop.reset","aliases":[],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"OAVp5.chop.reset([name])","source":{"file":"p5/oav-p5.js","line":2855,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2855"},"summary":{"en":"Forgets the memory named `name` (a lag, a speed total, a count …), or every memory when you give no name: the next call starts fresh.","zh":"忘掉叫 `name` 的那個記憶（lag、speed 的累計、計數…），不給名字就全部忘掉：下一次呼叫會重新開始。"},"description":[{"en":"The call that follows starts from nothing, as if it were the first one: a `speed()` total is 0 again, a `lag()` takes its input as it is, a `count()` is back at 0. Use it to restart a named effect, for example on a key press.","zh":"接下來的那次呼叫會從零開始，就像是第一次：`speed()` 的累計值回到 0，`lag()` 照單全收輸入，`count()` 回到 0。想讓某個有名字的效果重新開始時用，例如按一個鍵的時候。"},{"en":"Calling it with no name also drops the unnamed memories. They are made again by order on the next frame, so nothing breaks, but every unnamed value starts over too. Event counts from `trigger()` are not memories of a call and are not touched.","zh":"不給名字的時候，沒有名字的記憶也一起丟掉。下一個畫面會重新照順序建立，所以不會壞掉，但每一個沒有名字的值也都重新開始。`trigger()` 的事件次數不是某個呼叫的記憶，不受影響。"}],"params":[{"name":"name","type":"String","optional":true,"en":"The name you gave a call as its last argument (`'spin'` in `speed(1, 'spin')`). The memory of that name is forgotten in every function that has one. Left out, every memory is forgotten, named or not.","zh":"你在呼叫的最後一個參數給的名字（`speed(1, 'spin')` 裡的 `'spin'`）。每個函式裡那個名字的記憶都會被忘掉。不寫的話，不管有沒有名字，全部忘掉。"}],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.chop.reset('spin');   // forget the named speed\n}\n\nfunction draw() {\n  background(20);\n  translate(width / 2, height / 2);\n  rotate(speed(1, 'spin'));\n  rect(-40, -40, 80, 80);\n}","caption":{"en":"The square turns steadily. Click: it jumps back to its starting angle and carries on from there.","zh":"方塊平穩地轉。點一下：它跳回起始的角度，再從那裡繼續轉。"},"run":"sketch","lang":"js"}],"related":["OAVp5.chop.slots","OAVp5.chop","speed","count"],"keywords":{"en":["reset","restart","forget","clear memory","start over"],"zh":["重設","重新開始","忘掉","清除記憶","歸零"]},"tbd":[],"claims":[]},
{"id":"OAVp5.chop.slots","key":"OAVp5.chop.slots","name":"OAVp5.chop.slots","aliases":[],"kind":"function","package":"oav-p5","category":"chop","documented":true,"signature":"OAVp5.chop.slots()","source":{"file":"p5/oav-p5.js","line":2844,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2844"},"summary":{"en":"Lists every memory the CHOP functions are using right now: its kind, how it is found, and its latest value.","zh":"列出 CHOP 函式現在用的每一個記憶：是哪一種、怎麼找到它、最新的值是多少。"},"description":[{"en":"For finding out why two calls seem to share a memory, or whether a loop made hundreds: `OAVp5.chop.slots().length` is how many there are. It reads the state and changes nothing.","zh":"用來查為什麼兩個呼叫好像共用同一個記憶，或迴圈是不是做出了好幾百個：`OAVp5.chop.slots().length` 就是現在有幾個。它只讀取狀態，不會改任何東西。"}],"params":[],"returns":{"type":"Array<Object>","en":"One `{ kind, key, mode, value }` per memory. `kind` is the function (`'lag'`, `'spring'`, …). `key` is `'#1'`, `'#2'` for calls found by order, a name for named ones, or the line of code for calls matched by line. `mode` says which: `'order'`, `'name'` or `'site'`. `value` is its latest output (missing for a trail).","zh":"每一個記憶一筆 `{ kind, key, mode, value }`。`kind` 是哪個函式（`'lag'`、`'spring'` …）。`key` 是靠順序找到的呼叫的 `'#1'`、`'#2'`，有名字的就是名字，靠行號對應的就是那一行程式。`mode` 說明是哪一種：`'order'`、`'name'` 或 `'site'`。`value` 是它最近一次的輸出（trail 沒有）。"},"notes":[{"en":"This is advanced. 🩺 and tests use it; a sketch rarely needs to.","zh":"這是進階用法。🩺 和測試會用到，草圖很少需要。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  lag(mouseX, 0.3);\n  lag(mouseY, 0.3);\n  spring(mouseX, 100, 6, 'ball');\n  fill(255);\n  const lines = OAVp5.chop.slots().map((s) => s.kind + ' ' + s.key + ' (' + s.mode + ')');\n  text(lines.join('\\n'), 20, 30);\n}","caption":{"en":"Lists the memories the three calls are using: two lags found by order and one spring found by its name.","zh":"列出這三個呼叫在用的記憶：兩個靠順序找到的 lag，和一個靠名字找到的 spring。"},"run":"sketch","lang":"js"}],"related":["OAVp5.chop.reset","OAVp5.chop","lag"],"keywords":{"en":["chop slots","memory","list","debug","inspect"],"zh":["chop 記憶","列出","除錯","檢查"]},"tbd":[],"claims":[]},
{"id":"OAVp5.chop.state","key":"OAVp5.chop.state","name":"OAVp5.chop.state","aliases":[],"kind":"variable","package":"oav-p5","category":"chop","documented":true,"signature":"OAVp5.chop.state","source":{"file":"p5/oav-p5.js","line":2841,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2841"},"summary":{"en":"Where the @openav/chop module is: `'off'`, `'loading'`, `'on'` or `'error'`.","zh":"@openav/chop 模組目前的狀態：`'off'`、`'loading'`、`'on'` 或 `'error'`。"},"description":[{"en":"`off` until one of these functions is first called, `loading` while @openav/chop arrives (a few frames), `on` once it is in, `error` if it could not be loaded: a card says so and every function then passes its input through. [`trigger()`](#trigger) on its own never loads it, because it only records that something happened.","zh":"在任何一個函式第一次被呼叫之前是 `off`，@openav/chop 在載入的時候（幾個畫面）是 `loading`，載好之後是 `on`，載不到是 `error`：這時會跳出卡片，所有函式也都改成把輸入直接穿過去。單獨呼叫 [`trigger()`](#trigger) 不會載入它，因為它只記錄「發生了一件事」。"}],"value":{"type":"String","range":"'off' | 'loading' | 'on' | 'error'","en":"Changes the first time a CHOP function is called and once more when the module has arrived. `off` before any call.","zh":"第一次呼叫 CHOP 函式的時候會變，模組載好時再變一次。在任何呼叫之前是 `off`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('chop: ' + OAVp5.chop.state, 20, 30);   // off, then loading, then on\n  if (frameCount > 30) circle(lag(mouseX, 0.3), height / 2, 30);   // the first lag() loads the module\n}","caption":{"en":"For half a second it says off. The first lag() call then loads the module: loading, then on.","zh":"前半秒它顯示 off。接著第一次呼叫 lag() 就載入模組：loading，然後 on。"},"run":"sketch","lang":"js"}],"related":["OAVp5.chop.module","OAVp5.chop","lag"],"keywords":{"en":["chop state","loaded","loading","module ready"],"zh":["chop 狀態","載入","載好了沒","模組"]},"tbd":[],"claims":[{"expr":"OAVp5.chop.state","equals":"off"}]},
{"id":"approach","key":"approach","name":"approach","aliases":[],"kind":"function","package":"@openav/chop","category":"chop","documented":true,"signature":"approach(current, target, seconds, dt)","import":"import { approach } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L38"},"summary":{"en":"One step of a one-pole lag: moves `current` toward `target`, closing 63% of the gap after `seconds`.","zh":"一階低通跟隨的一步：把 `current` 往 `target` 推近，經過 `seconds` 秒走完 63% 的差距。"},"description":[{"en":"The smallest piece of smoothing. Call it once per frame, `value = approach(value, target, 0.2, dt)`, and `value` chases `target`: 63% of the way after `seconds`, 90% after 2.3 × `seconds`, 99% after 4.6 × `seconds`. It is exact for any `dt`, so a sketch at 30 fps and one at 144 fps draw the same curve.","zh":"平滑裡最小的一塊。每個畫面呼叫一次，`value = approach(value, target, 0.2, dt)`，`value` 就會追著 `target` 跑：經過 `seconds` 走完 63%，2.3 × `seconds` 走完 90%，4.6 × `seconds` 走完 99%。不管 `dt` 多大都是精確解，所以 30 fps 和 144 fps 畫出同一條曲線。"},{"en":"`seconds` here is a time constant, which is shorter than the \"seconds\" of TouchDesigner's Lag CHOP. [`Lag`](#Lag) takes the TouchDesigner kind and divides it by [`TD_LAG_RATIO`](#TD_LAG_RATIO) before calling this function. `@openav/mapping`'s `smooth` steps with `approach` and means the time constant too.","zh":"這裡的 `seconds` 是時間常數，比 TouchDesigner 的 Lag CHOP 的「秒數」短。[`Lag`](#Lag) 收的是 TouchDesigner 那種秒數，會先除以 [`TD_LAG_RATIO`](#TD_LAG_RATIO) 再呼叫這個函式。`@openav/mapping` 的 `smooth` 也是用 `approach` 一步一步走，意思同樣是時間常數。"}],"params":[{"name":"current","type":"Number","en":"Where the value is now.","zh":"數值現在的位置。"},{"name":"target","type":"Number","en":"Where it is heading.","zh":"它要去的地方。"},{"name":"seconds","type":"Number","en":"The time constant: after this many seconds the gap to `target` has closed by 63% (1 − 1/e). 0, a negative number or NaN jumps straight to `target`, whatever `dt` is.","zh":"時間常數：經過這麼多秒，與 `target` 的差距縮小 63%（1 − 1/e）。0、負數或 NaN 會直接跳到 `target`，不管 `dt` 是多少。"},{"name":"dt","type":"Number","en":"Seconds since the last step. 0, a negative number or NaN leaves `current` where it is.","zh":"距離上一步過了幾秒。0、負數或 NaN 時 `current` 原地不動。"}],"returns":{"type":"Number","en":"The new value, between `current` and `target`.","zh":"新的數值，落在 `current` 與 `target` 之間。"},"needs":{"en":"Nothing. It is pure arithmetic with no memory: you keep `current` yourself and pass in the seconds since the last call. For a value with its own memory use [`Lag`](#Lag).","zh":"不需要先做什麼。它只是算術，沒有記憶：`current` 由你自己保管，並把距離上次呼叫的秒數傳進來。想要自帶記憶的版本，用 [`Lag`](#Lag)。"},"notes":[],"examples":[{"code":"import { approach } from '@openav/chop';\n\nlet value = 0;\nconst seen = [];\nfor (let frame = 0; frame < 6; frame++) {\n  value = approach(value, 1, 0.1, 1 / 60);   // chase 1, time constant 0.1 s\n  seen.push(value.toFixed(3));\n}\ndocument.getElementById('app').textContent = seen.join('  ');","caption":{"en":"Six frames of a value chasing 1: it covers about 15% of the remaining gap each frame (0.154, 0.283, 0.393 …).","zh":"一個數值追著 1 跑的六個畫面：每個畫面大約走完剩下差距的 15%（0.154、0.283、0.393 …）。"},"run":"module","lang":"js"},{"code":"import { approach } from '@openav/chop';\n\n// one second of time constant: the gap is 63% closed after 1 s, 90% after 2.3 s\nconst seconds = 1;\nconst after = (t) => approach(0, 1, seconds, t).toFixed(3);\ndocument.getElementById('app').textContent =\n  `after 1 s: ${after(1)}   after 2.3 s: ${after(2.3026)}   after 4.6 s: ${after(4.6052)}`;","caption":{"en":"Prints 0.632, 0.900 and 0.990: the three numbers worth remembering.","zh":"印出 0.632、0.900、0.990：值得記住的三個數字。"},"run":"module","lang":"js"}],"related":["Lag","TD_LAG_RATIO","lag"],"keywords":{"en":["lag","smooth","smoothing","ease","easing","time constant","one-pole","low-pass","follow","chase","exponential"],"zh":["平滑","緩動","跟隨","時間常數","低通","延遲","追趕"]},"tbd":[],"claims":[{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"OAVp5.chop.module.approach(0, 1, 1, 1)","min":0.6321,"max":0.6322},{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"OAVp5.chop.module.approach(0, 1, 1, 2.302585092994046)","min":0.8999,"max":0.9001}]},
{"id":"Count","key":"Count","name":"Count","aliases":[],"kind":"class","package":"@openav/chop","category":"chop","documented":true,"signature":"new Count({ threshold = 0.5 } = {})","import":"import { Count } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":225,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L225"},"summary":{"en":"Counts how many times its input turned on (true, or a number above `threshold`); with a limit it loops 0 … limit − 1. The Count CHOP.","zh":"數輸入打開了幾次（true，或超過 `threshold` 的數字），給上限就會在 0 … 上限 − 1 之間循環。就是 Count CHOP。"},"description":[{"en":"`count.step(input, limit)` counts a rising edge, the moment the input goes from off to on, not the time it stays on. Hold a button for ten seconds and that is one. With `limit` 4 the result walks 0 1 2 3 0 1 …, which is how a beat becomes \"beat 1 of 4\".","zh":"`count.step(input, limit)` 數的是上升的那一下，也就是輸入從關變成開的瞬間，不是開著的時間。按住按鈕十秒也只算一次。給 `limit` 4，結果會是 0 1 2 3 0 1 …，一個拍子就這樣變成「四拍裡的第幾拍」。"},{"en":"The counter keeps counting underneath even when you pass a limit, so you can change the limit later without losing your place. A value that is not a number counts as off.","zh":"就算給了上限，底下的計數器還是一直累加，所以之後改上限也不會失去進度。不是數字的值算成關。"},{"en":"In a p5 sketch use [`count()`](#count): `count(beat, 4)`, or `count('kick')` to count the drum machine's kicks.","zh":"在 p5 草圖裡請用 [`count()`](#count)：`count(beat, 4)`，或用 `count('kick')` 數鼓機的大鼓。"}],"params":[{"name":"threshold","type":"Number","en":"A number counts as \"on\" when it is above this. A boolean true is 1, false is 0.","zh":"數字超過這個值才算「打開」。布林值 true 是 1，false 是 0。"}],"needs":{"en":"Nothing to load. Call `step` every frame, even while the input is off: it has to see the \"off\" to notice the next \"on\". It has no `dt`, because a count does not depend on time.","zh":"不用載入任何東西。每個畫面都呼叫 `step`，連輸入關著的時候也要：它得看到「關」，才認得出下一次的「開」。它沒有 `dt`，因為計數跟時間無關。"},"notes":[{"en":"Public fields: `threshold`, `on` (is the input on right now) and `total` (the count before looping).","zh":"公開的欄位有 `threshold`、`on`（輸入現在是不是開著）和 `total`（循環之前的計數）。"}],"examples":[{"code":"import { Count } from '@openav/chop';\n\nconst count = new Count();\nconst inputs = [0, 1, 1, 0, 1, true, false, true];\ndocument.getElementById('app').textContent =\n  inputs.map((x) => count.step(x)).join('  ');","caption":{"en":"Prints 0  1  1  1  2  2  2  3: it counts each time the input goes from off to on, and holding it on adds nothing.","zh":"印出 0  1  1  1  2  2  2  3：輸入每次從關變開就加一，一直開著不會再加。"},"run":"module","lang":"js"},{"code":"import { Count } from '@openav/chop';\n\nconst count = new Count();\nconst beats = [];\nfor (let i = 0; i < 12; i++) beats.push(count.step(i % 2, 4));   // off, on, off, on …\ndocument.getElementById('app').textContent = beats.join('  ');","caption":{"en":"With a limit of 4 the count loops: 0  1  1  2  2  3  3  0  0  1  1  2.","zh":"給上限 4，計數就會循環：0  1  1  2  2  3  3  0  0  1  1  2。"},"run":"module","lang":"js"}],"related":["count","wrapCount","envelope"],"keywords":{"en":["count","count chop","counter","beat counter","rising edge","loop","modulo","bar","step sequencer"],"zh":["計數","count chop","計數器","拍子","上升緣","循環","取餘數"]},"tbd":[],"claims":[],"members":[{"name":"step","member":"method","signature":"input, limit = 0","doc":"","line":227},{"name":"reset","member":"method","signature":"value = 0","doc":"","line":233}]},
{"id":"DEFAULTS","key":"DEFAULTS","name":"DEFAULTS","aliases":[],"kind":"constant","package":"@openav/chop","category":"chop","documented":true,"signature":"DEFAULTS = object, 5 keys","import":"import { DEFAULTS } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":243,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L243"},"summary":{"en":"The default settings of every CHOP in the package, as one frozen object: lag, envelope, spring, trail and count.","zh":"這個套件裡每個 CHOP 的預設設定，收在一個凍結的物件裡：lag、envelope、spring、trail 和 count。"},"description":[{"en":"For documentation and for interfaces that show the defaults, such as the placeholder text of a field. The classes and [`envelope()`](#envelope) have these same numbers written in their own signatures; changing this object changes nothing about how they behave.","zh":"給文件和要顯示預設值的介面用，例如輸入欄的提示文字。這些類別和 [`envelope()`](#envelope) 在自己的簽名裡已經寫了同樣的數字。改這個物件不會改變它們的行為。"}],"value":{"type":"Object","en":"`{ lag: { up: 0.1, down: 0.1 }, envelope: { attack: 0.01, decay: 0.3 }, spring: { stiffness: 100, damping: 6 }, trail: { seconds: 2, rate: 60 }, count: { threshold: 0.5 } }`. Frozen: assigning to it does nothing.","zh":"`{ lag: { up: 0.1, down: 0.1 }, envelope: { attack: 0.01, decay: 0.3 }, spring: { stiffness: 100, damping: 6 }, trail: { seconds: 2, rate: 60 }, count: { threshold: 0.5 } }`。已凍結：指定新值不會有作用。"},"params":[],"notes":[],"examples":[{"code":"import { DEFAULTS } from '@openav/chop';\n\ndocument.getElementById('app').textContent = JSON.stringify(DEFAULTS.spring);","caption":{"en":"Prints {\"stiffness\":100,\"damping\":6}: what a Spring does when you give it no settings.","zh":"印出 {\"stiffness\":100,\"damping\":6}：沒給設定時 Spring 用的值。"},"run":"module","lang":"js"}],"related":["Lag","Spring","Trail","Count","envelope"],"keywords":{"en":["defaults","default values","settings","constants","options"],"zh":["預設值","預設","設定","常數","選項"]},"tbd":[],"claims":[]},
{"id":"envelope","key":"envelope","name":"envelope","aliases":[],"kind":"function","package":"@openav/chop","category":"chop","documented":true,"signature":"envelope(times, t, attack = 0.01, decay = 0.3, peak = 1)","import":"import { envelope } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":86,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L86"},"summary":{"en":"The level, 0 to `peak`, of an attack-and-decay envelope at time `t`, for events that happened at the given `times`; the Trigger CHOP.","zh":"給定事件發生的時間 `times`，算出起音加衰減的包絡在時間 `t` 的高度（0 到 `peak`），就是 Trigger CHOP。"},"description":[{"en":"Keep a list of the times something happened (a kick, a key press, a cue) and ask for the level at the time you are drawing: `scale(1 + envelope(kicks, now, 0.01, 0.3))`. It rises in a straight line to 1 in `attack` seconds, then falls to 0 in `decay` seconds, quick at first and landing softly.","zh":"把某件事發生的時間（大鼓、按鍵、cue）存成一個清單，畫的時候問它「現在多高」：`scale(1 + envelope(kicks, now, 0.01, 0.3))`。它在 `attack` 秒內直線升到 1，再用 `decay` 秒落回 0，一開始快、收尾柔。"},{"en":"It is a pure function of the times and of `t`: ask twice, or at 30 fps and at 144 fps, and the same moment gives the same level; `attack` and `decay` may change every frame (a knob on the decay). A new event while the last one still sounds restarts from where the envelope is, never from 0, so fast hits pile up into a plateau instead of flickering. The attack counts from 0 to the peak, so a retrigger from halfway up takes half the attack.","zh":"它是 `times` 和 `t` 的純函式：問兩次、或在 30 fps 與 144 fps 問同一個時間點，答案一樣。`attack` 和 `decay` 每個畫面都可以改（例如用旋鈕調 decay）。上一個事件還沒消散時又來一個，包絡從目前的高度重新出發，不會先掉到 0，所以快速的連擊會堆成一個高原，不會閃爍。起音以 0 到峰值計算，所以從一半高度重新觸發只要一半的起音時間。"},{"en":"In a p5 sketch you do not import it: [`env()`](#env) and `trigger()` keep the event list for you and read the drum machine and the microphone.","zh":"在 p5 草圖裡不用 import：[`env()`](#env) 和 `trigger()` 會替你保管事件清單，也會讀鼓機和麥克風。"}],"params":[{"name":"times","type":"Number[]","en":"When the events happened, in seconds on any clock you like, oldest first. An empty array (or none) gives 0. Events later than `t` have not happened yet and are ignored. Only the last few matter, so you can trim the list.","zh":"事件發生的時間，單位是秒，用哪個時鐘都行，最舊的排前面。空陣列（或沒給）回傳 0。比 `t` 晚的事件還沒發生，會被忽略。只有最後幾筆有影響，所以可以把舊的剪掉。"},{"name":"t","type":"Number","en":"The moment to read, on the same clock as `times`.","zh":"要讀取的時間點，跟 `times` 用同一個時鐘。"},{"name":"attack","type":"Number","optional":true,"en":"Seconds to rise from 0 to the peak. 0 starts at the peak at once.","zh":"從 0 升到峰值要幾秒。0 就一開始直接在峰值。"},{"name":"decay","type":"Number","optional":true,"en":"Seconds to fall back to 0 after the peak: it is exactly 0 from `attack + decay` seconds after an event.","zh":"到達峰值後落回 0 要幾秒：事件之後 `attack + decay` 秒起正好是 0。"},{"name":"peak","type":"Number","optional":true,"en":"The top of the envelope.","zh":"包絡的最高點。"}],"returns":{"type":"Number","en":"0 to `peak`: `peak` just after an event, 0 once it has died away.","zh":"0 到 `peak`：事件剛發生時是 `peak`，消散之後是 0。"},"needs":{"en":"Nothing to load. You record the event times yourself (for example `kicks.push(performance.now() / 1000)` when a kick arrives) and pass the same clock as `t`.","zh":"不用載入任何東西。事件的時間由你自己記（例如大鼓來的時候 `kicks.push(performance.now() / 1000)`），`t` 要用同一個時鐘。"},"notes":[],"examples":[{"code":"import { envelope } from '@openav/chop';\n\nconst kicks = [1];   // one kick, at 1 s\nconst times = [0.9, 1, 1.005, 1.01, 1.1, 1.2, 1.31];\ndocument.getElementById('app').innerHTML = times\n  .map((t) => `${t} s: ${envelope(kicks, t).toFixed(2)}`)\n  .join('<br>');","caption":{"en":"Before the kick 0.00, half way up the attack 0.50 at 1.005 s, the peak 1.00 at 1.01 s, then 0.49, 0.13 and 0.00 at 1.31 s.","zh":"大鼓之前是 0.00，1.005 秒到起音一半 0.50，1.01 秒到峰值 1.00，然後是 0.49、0.13，1.31 秒回到 0.00。"},"run":"module","lang":"js"},{"code":"import { envelope } from '@openav/chop';\n\nconst ctx = document.getElementById('c').getContext('2d');\nconst kicks = [];\nlet nextKick = 0;\n\nfunction frame(ms) {\n  const t = ms / 1000;\n  if (t >= nextKick) { kicks.push(nextKick); nextKick += 0.5; }   // a kick every half second\n  const level = envelope(kicks, t, 0.01, 0.3);\n  ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\n  ctx.fillStyle = '#fc4';\n  ctx.beginPath(); ctx.arc(200, 150, 40 + level * 80, 0, Math.PI * 2); ctx.fill();\n  requestAnimationFrame(frame);\n}\nrequestAnimationFrame(frame);","caption":{"en":"A disc that swells on every kick (twice a second) and shrinks back within 0.3 s.","zh":"一個圓盤，每次大鼓（每秒兩次）都脹大，0.3 秒內縮回去。"},"run":"module","lang":"js"},{"code":"import { envelope } from '@openav/chop';\n\n// three hits 50 ms apart pile up; one lone hit at the same moment is much lower\nconst pile = envelope([0, 0.05, 0.1], 0.11);\nconst lone = envelope([0], 0.11);\ndocument.getElementById('app').textContent =\n  `pile-up: ${pile.toFixed(2)}   lone hit: ${lone.toFixed(2)}`;","caption":{"en":"Prints \"pile-up: 0.95   lone hit: 0.44\": each new hit restarts from where the envelope is, so quick hits hold it high.","zh":"印出「pile-up: 0.95   lone hit: 0.44」：每一擊都從目前高度重新出發，所以密集的擊打會把它撐在高處。"},"run":"module","lang":"js"}],"related":["envelopeStage","env","Lag","Count"],"keywords":{"en":["envelope","trigger","trigger chop","attack","decay","kick","pulse","swell","adsr","on every beat"],"zh":["包絡","觸發","trigger","起音","衰減","大鼓","脈衝","每個拍子"]},"tbd":[],"claims":[{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"OAVp5.chop.module.envelope([1], 1.1)","min":0.489,"max":0.491},{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"OAVp5.chop.module.envelope([1], 1.005)","min":0.499,"max":0.501},{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"OAVp5.chop.module.envelope([1], 1.31)","equals":0}]},
{"id":"envelopeStage","key":"envelopeStage","name":"envelopeStage","aliases":[],"kind":"function","package":"@openav/chop","category":"chop","documented":true,"signature":"envelopeStage(elapsed, attack, decay, from = 0, peak = 1)","import":"import { envelopeStage } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":70,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L70"},"summary":{"en":"The level of one envelope `elapsed` seconds after its trigger: a straight rise to `peak`, then a fall to 0 in `decay` seconds.","zh":"一個包絡在被觸發 `elapsed` 秒之後的高度：先直線升到 `peak`，再用 `decay` 秒落回 0。"},"description":[{"en":"The building block of [`envelope()`](#envelope), which strings several of these together for a list of events. On its own it answers: \"how high is a kick that happened `elapsed` seconds ago?\" With the defaults of `envelope()` (attack 0.01, decay 0.3) it is 0.5 at 5 ms, 1 at 10 ms, 0.49 at 100 ms and 0 from 310 ms on.","zh":"[`envelope()`](#envelope) 的基本零件，它把好幾個這樣的片段串起來處理一串事件。單獨使用時，它回答的是：「`elapsed` 秒前發生的一次大鼓，現在有多高？」用 `envelope()` 的預設值（attack 0.01、decay 0.3），5 毫秒時是 0.5，10 毫秒是 1，100 毫秒是 0.49，310 毫秒起是 0。"},{"en":"The fall is quick at first and lands softly: that is the shape a kick drum swell looks right with. It reaches 0 in exactly `decay` seconds, not asymptotically.","zh":"下降一開始很快、收尾很柔：大鼓的律動配這個形狀看起來最對。它剛好在 `decay` 秒落到 0，不是無限接近。"}],"params":[{"name":"elapsed","type":"Number","en":"Seconds since the trigger. Below 0 (or NaN) gives `from`.","zh":"觸發之後過了幾秒。小於 0（或 NaN）時回傳 `from`。"},{"name":"attack","type":"Number","en":"Seconds to rise from 0 to `peak`. 0 or less starts at the peak at once.","zh":"從 0 升到 `peak` 要幾秒。0 或更小就一開始直接在峰值。"},{"name":"decay","type":"Number","en":"Seconds to fall from `peak` to 0, along (1 − u)² where u runs 0 to 1. 0 or less drops to 0 right after the attack.","zh":"從 `peak` 落到 0 要幾秒，走 (1 − u)² 的曲線（u 從 0 跑到 1）。0 或更小就在起音結束後立刻變 0。"},{"name":"from","type":"Number","optional":true,"en":"The level at the moment of the trigger. The attack still counts from 0 to the peak, so starting from halfway up takes half the attack.","zh":"觸發當下的高度。起音仍然以 0 到峰值計算，所以從一半高度開始只要一半的起音時間。"},{"name":"peak","type":"Number","optional":true,"en":"The top of the envelope.","zh":"包絡的最高點。"}],"returns":{"type":"Number","en":"The level, from 0 to `peak`.","zh":"高度，從 0 到 `peak`。"},"notes":[{"en":"Advanced. Most sketches never call it: use [`envelope()`](#envelope), or `env()` in p5.","zh":"進階用法。大部分草圖不會直接呼叫它：請用 [`envelope()`](#envelope)，p5 裡用 `env()`。"}],"examples":[{"code":"import { envelopeStage } from '@openav/chop';\n\nconst row = [0, 0.005, 0.01, 0.1, 0.16, 0.31].map(\n  (t) => `${t} s: ${envelopeStage(t, 0.01, 0.3).toFixed(2)}`\n);\ndocument.getElementById('app').textContent = row.join('   ');","caption":{"en":"Prints the level at six moments after a trigger: 0.00, 0.50, 1.00, 0.49, 0.25, 0.00.","zh":"印出觸發後六個時間點的高度：0.00、0.50、1.00、0.49、0.25、0.00。"},"run":"module","lang":"js"}],"related":["envelope","env","Lag"],"keywords":{"en":["envelope","attack","decay","trigger","adsr","kick","pulse","swell"],"zh":["包絡","起音","衰減","觸發","大鼓","脈衝"]},"tbd":[],"claims":[]},
{"id":"Lag","key":"Lag","name":"Lag","aliases":[],"kind":"class","package":"@openav/chop","category":"chop","documented":true,"signature":"new Lag({ up = 0.1, down = up, value = null } = {})","import":"import { Lag } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":49,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L49"},"summary":{"en":"Follows its input with one speed while the input is above the output and another while it is below; the TouchDesigner Lag CHOP.","zh":"輸入比輸出高的時候用一種速度跟上去，比輸出低的時候用另一種速度，就是 TouchDesigner 的 Lag CHOP。"},"description":[{"en":"Make one `Lag` per value you want to smooth, then call `step(input, dt)` once per frame. `new Lag({ up: 0.03, down: 0.4 })` is a VU meter: it opens almost at once and closes slowly. `up` and `down` are in TouchDesigner's seconds: with `up: 1`, a jump from 0 to 1 is at 0.9 one second later, at 30, 60 or 144 fps alike.","zh":"每一個要平滑的數值做一個 `Lag`，每個畫面呼叫一次 `step(input, dt)`。`new Lag({ up: 0.03, down: 0.4 })` 就是音量表：幾乎立刻張開，慢慢收回。`up` 和 `down` 用 TouchDesigner 的秒數：`up: 1` 的話，從 0 跳到 1 之後一秒是 0.9，30、60 或 144 fps 都一樣。"},{"en":"Each step is the exact answer for an input held at its new value over `dt` seconds, so the frame rate does not change the curve. A boolean counts as 0 or 1 (`lag.step(mouseIsPressed, dt)` is a soft switch). A value that is not a number, such as `NaN`, comes back untouched and leaves the memory as it was.","zh":"每一步都是「輸入在這 `dt` 秒內維持新值」的精確解，所以影格率不會改變曲線。布林值算成 0 或 1（`lag.step(mouseIsPressed, dt)` 是柔和的開關）。不是數字的值（像 `NaN`）原樣傳回，記憶不受影響。"},{"en":"In a p5 sketch you do not import this: [`lag()`](#lag) keeps one `Lag` per call for you.","zh":"在 p5 草圖裡不用 import 它：[`lag()`](#lag) 會替每一個呼叫保管一個 `Lag`。"}],"params":[{"name":"up","type":"Number","en":"Seconds to follow a rise, while the input is above the output. TouchDesigner's seconds: about the time to cover 90% of a jump. 0 follows at once.","zh":"追上升高的輸入要幾秒（輸入比輸出高的時候）。用 TouchDesigner 的秒數：大約是走完 90% 跳躍的時間。0 表示立刻跟上。"},{"name":"down","type":"Number","en":"Seconds to follow a fall, while the input is below the output. Leave it out and it is the same as `up`.","zh":"追上降低的輸入要幾秒（輸入比輸出低的時候）。不給就跟 `up` 一樣。"},{"name":"value","type":"Number","en":"A starting value. Leave it out and the first input is taken as it is, so nothing glides in from 0.","zh":"起始值。不給的話，第一個輸入原樣當作輸出，所以不會從 0 慢慢滑進來。"}],"needs":{"en":"Nothing to load or start. Pass the seconds since the previous frame as `dt` (in a browser: the difference of two `performance.now() / 1000`). The first `step` only stores the input, so the output equals the input on frame one.","zh":"不用載入或啟動任何東西。把距離上一個畫面的秒數當作 `dt` 傳進來（瀏覽器裡：兩次 `performance.now() / 1000` 的差）。第一次 `step` 只是把輸入記下來，所以第一個畫面輸出等於輸入。"},"notes":[{"en":"Public fields: `up`, `down` and `value`, the last output (`null` until the first step). You can change `up` and `down` any time.","zh":"公開的欄位有 `up`、`down` 和 `value`（最後一次的輸出，第一次 `step` 之前是 `null`）。`up`、`down` 隨時可以改。"},{"en":"The time constant underneath is `seconds / TD_LAG_RATIO` (see [`TD_LAG_RATIO`](#TD_LAG_RATIO)), not `seconds`. If you want the plain 63% kind, call [`approach()`](#approach).","zh":"底層的時間常數是 `seconds / TD_LAG_RATIO`（見 [`TD_LAG_RATIO`](#TD_LAG_RATIO)），不是 `seconds`。想要單純 63% 那一種，直接呼叫 [`approach()`](#approach)。"}],"examples":[{"code":"import { Lag } from '@openav/chop';\n\nconst lag = new Lag({ up: 0.03, down: 0.4 });   // opens fast, closes slowly\nconst seen = [];\nfor (let frame = 0; frame < 12; frame++) {\n  const input = frame >= 2 && frame < 6 ? 1 : 0;   // on for four frames\n  seen.push(lag.step(input, 1 / 60).toFixed(2));\n}\ndocument.getElementById('app').textContent = seen.join('  ');","caption":{"en":"The output shoots up within two frames when the input turns on, then sags back slowly after it turns off (0.00 0.00 0.72 0.92 0.98 0.99 0.90 0.82 …).","zh":"輸入打開時，輸出兩個畫面內就衝上去，輸入關掉之後才慢慢掉下來（0.00 0.00 0.72 0.92 0.98 0.99 0.90 0.82 …）。"},"run":"module","lang":"js"},{"code":"import { Lag } from '@openav/chop';\n\nconst ctx = document.getElementById('c').getContext('2d');\nctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\nctx.lineWidth = 2;\nconst input = (t) => (t < 0.25 || t > 1.25 ? 0 : 1);   // a pulse, 1 s long\n\nfor (const [fps, color] of [[30, '#555'], [30, '#fa4'], [144, '#4af']]) {\n  const lag = new Lag({ up: 0.1, down: 0.4 });\n  ctx.strokeStyle = color; ctx.beginPath();\n  for (let k = 0; k <= fps * 2; k++) {\n    const t = k / fps;\n    const y = color === '#555' ? input(t) : lag.step(input(t), k ? 1 / fps : 0);\n    ctx.lineTo(20 + t * 180, 260 - y * 220);\n  }\n  ctx.stroke();\n}","caption":{"en":"A one-second pulse (grey) through a Lag at 30 fps (orange) and at 144 fps (blue): it rises in about 0.1 s, falls over about 0.4 s, and the two curves lie on top of each other.","zh":"一個一秒長的脈衝（灰）分別用 30 fps（橘）和 144 fps（藍）走過 Lag：大約 0.1 秒升上去、0.4 秒左右落下來，兩條曲線疊在一起。"},"run":"module","lang":"js"},{"code":"import { Lag } from '@openav/chop';\n\nconst lag = new Lag();                       // up and down are both 0.1\nlag.step(0.5, 0);                            // the first input is taken as it is\nconst nan = lag.step(NaN, 1 / 60);           // not a number: passed through, memory kept\nconst kept = lag.value;                      // still 0.5\nconst toward = lag.step(true, 1 / 60);       // a boolean counts as 1, so it moves up from 0.5\ndocument.getElementById('app').textContent =\n  `NaN in: ${nan}, memory still ${kept}, true in: ${toward.toFixed(2)}`;","caption":{"en":"Prints \"NaN in: NaN, memory still 0.5, true in: 0.66\": a non-number leaves the memory alone, and a boolean counts as 0 or 1.","zh":"印出「NaN in: NaN, memory still 0.5, true in: 0.66」：不是數字就不動記憶，布林值算成 0 或 1。"},"run":"module","lang":"js"}],"related":["approach","TD_LAG_RATIO","lag","Spring","envelope"],"keywords":{"en":["lag","smooth","smoothing","follow","vu meter","attack release","ease","low-pass","touchdesigner","lag chop","up down"],"zh":["平滑","緩動","跟隨","音量表","延遲","低通","touchdesigner","上升下降"]},"tbd":[],"claims":[{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"(() => { const l = new OAVp5.chop.module.Lag({ up: 1 }); l.step(0, 0); let v; for (let i = 0; i < 100; i++) v = l.step(1, 0.01); return v; })()","min":0.8999,"max":0.9001}],"members":[{"name":"step","member":"method","signature":"input, dt, up = this.up, down = this.down","doc":"","line":55},{"name":"reset","member":"method","signature":"value = null","doc":"","line":62}]},
{"id":"Slope","key":"Slope","name":"Slope","aliases":[],"kind":"class","package":"@openav/chop","category":"chop","documented":true,"signature":"new Slope()","import":"import { Slope } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":209,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L209"},"summary":{"en":"How fast the input is changing, in units per second; 0 on the first step. The Slope CHOP.","zh":"輸入變化得多快，單位是每秒多少，第一步是 0。就是 Slope CHOP。"},"description":[{"en":"`slope.step(value, dt)` returns `(value − previous value) / dt`. A value rising by 3 every second gives 3 at any frame rate; a value that stands still gives 0; a falling one gives a negative number. Use it for \"how hard did the knob just move\": `scale(1 + abs(slope.step(knob, dt)))`.","zh":"`slope.step(value, dt)` 回傳 `(value − 上一個值) / dt`。每秒升 3 的數值，在任何影格率下都回傳 3，沒動的數值是 0，往下掉的是負數。用來表達「旋鈕剛剛轉得多用力」：`scale(1 + abs(slope.step(knob, dt)))`。"},{"en":"It has no smoothing: a single jump of the input is one big spike for one step. Put the result through a [`Lag`](#Lag) if you want it to fade. A `dt` of 0 or less changes nothing and returns the last slope, and a value that is not a number returns the last slope too.","zh":"它不做任何平滑：輸入跳一下，就是一步裡的一根大尖峰。想讓它淡出，把結果交給 [`Lag`](#Lag)。`dt` 是 0 或更小，什麼都不改，回傳上一次的斜率。值不是數字時也回傳上一次的斜率。"},{"en":"In a p5 sketch use [`slope()`](#slope).","zh":"在 p5 草圖裡請用 [`slope()`](#slope)。"}],"params":[],"needs":{"en":"Nothing to load. The first `step` only stores the value and returns 0, because there is no earlier value to compare with.","zh":"不用載入任何東西。第一次 `step` 只是把值記下來並回傳 0，因為沒有更早的值可以比。"},"notes":[{"en":"The last slope is the public field `value`.","zh":"最近一次的斜率是公開欄位 `value`。"}],"examples":[{"code":"import { Slope } from '@openav/chop';\n\nconst slope = new Slope();\nconst seen = [];\nfor (const [value, dt] of [[0, 1 / 60], [0.05, 1 / 60], [0.1, 1 / 60], [0.1, 1 / 60]]) {\n  seen.push(slope.step(value, dt).toFixed(1));\n}\ndocument.getElementById('app').textContent = seen.join('  ');","caption":{"en":"Prints 0.0  3.0  3.0  0.0: nothing on the first step, then 3 units a second while it rises, then 0 when it stops.","zh":"印出 0.0  3.0  3.0  0.0：第一步沒有東西，上升時每秒 3 單位，停下來就是 0。"},"run":"module","lang":"js"}],"related":["slope","Speed","Lag"],"keywords":{"en":["slope","slope chop","derivative","rate of change","velocity","how fast","delta per second"],"zh":["斜率","slope chop","微分","變化率","速度","多快","每秒變化"]},"tbd":[],"claims":[],"members":[{"name":"step","member":"method","signature":"input, dt","doc":"","line":211},{"name":"reset","member":"method","signature":"","doc":"","line":218}]},
{"id":"Speed","key":"Speed","name":"Speed","aliases":[],"kind":"class","package":"@openav/chop","category":"chop","documented":true,"signature":"new Speed({ value = 0 } = {})","import":"import { Speed } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":100,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L100"},"summary":{"en":"Adds `rate` every second to a running total: a rate in units per second becomes a position; the Speed CHOP.","zh":"每秒把 `rate` 加進一個累計值：每秒多少單位的速率變成位置，就是 Speed CHOP。"},"description":[{"en":"`speed.step(rate, dt)` adds `rate * dt` to the total and returns it. With `rate` 1 the total grows by 1 every second; `rotate(speed.step(TWO_PI, dt))` is one turn a second. Feed it a rate that changes (a knob, `micLevel`) and the motion speeds up and slows down smoothly, without the jump you get from multiplying `frameCount` by it.","zh":"`speed.step(rate, dt)` 把 `rate * dt` 加到累計值裡並回傳。`rate` 是 1 的話，累計值每秒增加 1。`rotate(speed.step(TWO_PI, dt))` 就是每秒轉一圈。餵給它會變動的速率（旋鈕、`micLevel`），動作會平順地變快變慢，不會像把 `frameCount` 乘上速率那樣一跳一跳。"},{"en":"It is the integral of the rate, so it is the same at any frame rate. A boolean rate counts as 1 per second while true. A rate that is not a number, or a `dt` of 0 or less, adds nothing.","zh":"它是速率的積分，所以不論影格率都一樣。布林值的速率在 true 的時候算每秒 1。速率不是數字、或 `dt` 是 0 或更小，就什麼都不加。"},{"en":"In a p5 sketch use [`speed()`](#speed), which keeps one for each call.","zh":"在 p5 草圖裡請用 [`speed()`](#speed)，它會替每個呼叫保管一個。"}],"params":[{"name":"value","type":"Number","en":"The starting total. Anything that is not a finite number starts at 0.","zh":"累計值的起點。不是有限數字的話就從 0 開始。"}],"needs":{"en":"Nothing to load. Pass the seconds since the previous frame as `dt`.","zh":"不用載入任何東西。把距離上一個畫面的秒數當作 `dt` 傳進來。"},"notes":[{"en":"The total is the public field `value`.","zh":"累計值就是公開欄位 `value`。"}],"examples":[{"code":"import { Speed } from '@openav/chop';\n\nconst speed = new Speed();\nconst seen = [];\nfor (let i = 0; i < 4; i++) seen.push(speed.step(2, 0.5).toFixed(1));   // 2 units a second\ndocument.getElementById('app').textContent = seen.join('  ');","caption":{"en":"Prints 1.0  2.0  3.0  4.0: two units a second, half a second per step.","zh":"印出 1.0  2.0  3.0  4.0：每秒 2 單位，每一步半秒。"},"run":"module","lang":"js"},{"code":"import { Speed } from '@openav/chop';\n\nconst ctx = document.getElementById('c').getContext('2d');\nconst spin = new Speed();\nlet last = performance.now();\n\nfunction frame(now) {\n  const dt = (now - last) / 1000; last = now;\n  const angle = spin.step(Math.PI * 2, dt);        // one turn a second\n  ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\n  ctx.strokeStyle = '#fc4'; ctx.lineWidth = 6;\n  ctx.beginPath(); ctx.moveTo(200, 150);\n  ctx.lineTo(200 + Math.cos(angle) * 90, 150 + Math.sin(angle) * 90); ctx.stroke();\n  requestAnimationFrame(frame);\n}\nrequestAnimationFrame(frame);","caption":{"en":"A hand that turns once a second, at any frame rate. Change the rate to a changing number (a level, a knob) and it speeds up and slows down smoothly.","zh":"一根每秒轉一圈的指針，不論影格率。把速率換成會變動的數字（音量、旋鈕），它就會平順地加速減速。"},"run":"module","lang":"js"}],"related":["speed","Slope","Lag"],"keywords":{"en":["speed","speed chop","integrate","rate","per second","rotate","accumulate","position from velocity"],"zh":["速度","speed chop","積分","速率","每秒","旋轉","累計"]},"tbd":[],"claims":[],"members":[{"name":"step","member":"method","signature":"rate, dt","doc":"","line":102},{"name":"reset","member":"method","signature":"value = 0","doc":"","line":107}]},
{"id":"Spring","key":"Spring","name":"Spring","aliases":[],"kind":"class","package":"@openav/chop","category":"chop","documented":true,"signature":"new Spring({ stiffness = 100, damping = 6, value = null } = {})","import":"import { Spring } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":149,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L149"},"summary":{"en":"Follows its input like a mass on a spring: it overshoots, swings back and settles; the Spring CHOP with the input as position.","zh":"像吊在彈簧上的重物一樣追著輸入：會衝過頭、彈回來、再安定下來，就是輸入當作位置的 Spring CHOP。"},"description":[{"en":"Make one `Spring` per value, call `step(target, dt)` once per frame and draw what it returns. With the defaults (stiffness 100, damping 6), a jump of the input from 0 to 1 sends the output to 1.37 after a third of a second, and it is within 1% of 1 from about 1.4 s on. Raise the damping to 20 and it glides to the target without overshooting.","zh":"每個數值做一個 `Spring`，每個畫面呼叫一次 `step(target, dt)`，畫它回傳的值。用預設值（stiffness 100、damping 6）的話，輸入從 0 跳到 1，輸出在約三分之一秒時衝到 1.37，大約 1.4 秒之後就落在 1 的 1% 以內。把 damping 調到 20，它就會滑到目標上，不衝過頭。"},{"en":"Each step is the exact answer from [`springStep()`](#springStep), so 30 fps and 144 fps give the same motion and a long frame cannot blow it up. The input is the position it is pulled toward; a boolean counts as 0 or 1; a value that is not a number is returned as it is and changes nothing.","zh":"每一步都是 [`springStep()`](#springStep) 的精確解，所以 30 fps 和 144 fps 動得一樣，一個很長的畫面也不會讓它發散。輸入是它被拉向的位置，布林值算成 0 或 1，不是數字的值原樣傳回，什麼都不改。"},{"en":"In a p5 sketch use [`spring()`](#spring).","zh":"在 p5 草圖裡請用 [`spring()`](#spring)。"}],"params":[{"name":"stiffness","type":"Number","en":"How hard the spring pulls, so how fast it swings. With nothing damping it, it swings about `√stiffness / 2π` times a second (100 is about 1.6).","zh":"彈簧拉得多用力，也就是擺動多快。沒有阻尼的時候，每秒大約擺 `√stiffness / 2π` 次（100 大約是 1.6 次）。"},{"name":"damping","type":"Number","en":"How soon it settles. 6 with a stiffness of 100 bounces a few times; `2 * √stiffness` (20 for 100) and above never bounces.","zh":"多快安定下來。stiffness 100 配 damping 6 會彈幾下。`2 * √stiffness`（100 的話是 20）以上就完全不彈。"},{"name":"value","type":"Number","en":"A starting position. Leave it out and the first input is taken as it is, so it does not swing in from 0.","zh":"起始位置。不給的話，第一個輸入原樣當作位置，所以不會從 0 擺進來。"}],"needs":{"en":"Nothing to load. Pass the seconds since the previous frame as `dt`. The first `step` only stores the input (the spring starts at rest on it).","zh":"不用載入任何東西。把距離上一個畫面的秒數當作 `dt` 傳進來。第一次 `step` 只是把輸入記下來（彈簧就靜止在那裡開始）。"},"notes":[{"en":"Public fields: `stiffness`, `damping`, `value` (the position; `null` before the first step) and `velocity`.","zh":"公開的欄位有 `stiffness`、`damping`、`value`（位置，第一次 `step` 之前是 `null`）和 `velocity`。"}],"examples":[{"code":"import { Spring } from '@openav/chop';\n\nconst spring = new Spring();             // stiffness 100, damping 6\nspring.step(0, 0);                        // rest at 0\nconst seen = [];\nfor (let frame = 0; frame < 24; frame += 3) {\n  for (let i = 0; i < 3; i++) spring.step(1, 1 / 60);   // the input jumps to 1\n  seen.push(spring.value.toFixed(2));\n}\ndocument.getElementById('app').textContent = seen.join('  ');","caption":{"en":"Every third frame after the input jumps to 1: the value overshoots past 1 (to about 1.37), comes back and settles.","zh":"輸入跳到 1 之後每隔三個畫面看一次：數值衝過 1（約 1.37）、彈回來、再安定。"},"run":"module","lang":"js"},{"code":"import { Spring } from '@openav/chop';\n\nconst ctx = document.getElementById('c').getContext('2d');\nctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\nctx.lineWidth = 2;\nconst target = (t) => (t < 0.2 ? 0 : 1);   // the input jumps at 0.2 s\n\nfor (const [damping, color] of [[null, '#555'], [6, '#fa4'], [20, '#4af']]) {\n  const s = new Spring({ stiffness: 100, damping: damping ?? 6 });\n  ctx.strokeStyle = color; ctx.beginPath();\n  for (let k = 0; k <= 180; k++) {\n    const t = k / 60;\n    const y = damping === null ? target(t) : s.step(target(t), k ? 1 / 60 : 0);\n    ctx.lineTo(20 + t * 120, 250 - y * 150);\n  }\n  ctx.stroke();\n}","caption":{"en":"Two springs follow the same jump (grey): damping 6 (orange) overshoots and wobbles, damping 20 (blue) glides straight to the target.","zh":"兩個彈簧追同一個跳躍（灰）：damping 6（橘）衝過頭再晃幾下，damping 20（藍）直接滑到目標。"},"run":"module","lang":"js"}],"related":["springStep","spring","Lag","approach"],"keywords":{"en":["spring","spring chop","overshoot","bounce","wobble","elastic","damping","stiffness","physics","follow"],"zh":["彈簧","spring chop","過衝","彈跳","晃動","彈性","阻尼","勁度"]},"tbd":[],"claims":[{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"(() => { const s = new OAVp5.chop.module.Spring(); s.step(0, 0); let peak = 0; for (let i = 0; i < 180; i++) peak = Math.max(peak, s.step(1, 1 / 60)); return peak; })()","min":1.37,"max":1.375}],"members":[{"name":"step","member":"method","signature":"target, dt, stiffness = this.stiffness, damping = this.damping","doc":"","line":154},{"name":"reset","member":"method","signature":"value = null","doc":"","line":161}]},
{"id":"springStep","key":"springStep","name":"springStep","aliases":[],"kind":"function","package":"@openav/chop","category":"chop","documented":true,"signature":"springStep(x, v, target, stiffness, damping, dt)","import":"import { springStep } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":116,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L116"},"summary":{"en":"One exact step of a damped spring of mass 1 pulled toward `target`; returns `[position, velocity]`.","zh":"質量為 1 的阻尼彈簧朝 `target` 被拉的一個精確步驟，回傳 `[位置, 速度]`。"},"description":[{"en":"Solves x'' = −stiffness·(x − target) − damping·x' exactly over `dt` for a `target` that stays still during the step. It handles all three cases (under-damped: it bounces; critically damped; over-damped: it creeps) and a huge `dt` never blows up, which is why a sketch can fall behind for a whole second and the spring just lands on the target.","zh":"對 `dt` 這段時間內 `target` 不動的情況，精確解 x'' = −stiffness·(x − target) − damping·x'。欠阻尼（會彈）、臨界阻尼、過阻尼（慢慢爬）三種情況都處理，而且 `dt` 再大也不會發散，所以草圖卡了一整秒，彈簧也只是落在目標上。"},{"en":"`damping = 2 * Math.sqrt(stiffness)` is the edge where it stops bouncing: with `stiffness` 100 that is a damping of 20. With no spring (`stiffness` 0) only the damping acts and the velocity just fades.","zh":"`damping = 2 * Math.sqrt(stiffness)` 是剛好不再彈的臨界點：`stiffness` 是 100 的話，就是 damping 20。沒有彈簧（`stiffness` 為 0）的時候只剩阻尼作用，速度慢慢消失。"}],"params":[{"name":"x","type":"Number","en":"The position now.","zh":"現在的位置。"},{"name":"v","type":"Number","en":"The velocity now, in units per second.","zh":"現在的速度，單位是每秒多少單位。"},{"name":"target","type":"Number","en":"Where the other end of the spring is held.","zh":"彈簧另一端固定的位置。"},{"name":"stiffness","type":"Number","en":"How hard it pulls. Negative counts as 0.","zh":"拉得多用力。負數當作 0。"},{"name":"damping","type":"Number","en":"How much friction slows it. Negative counts as 0.","zh":"摩擦力多大。負數當作 0。"},{"name":"dt","type":"Number","en":"Seconds to advance. 0 or less (or NaN) returns `[x, v]` unchanged.","zh":"要前進幾秒。0 或更小（或 NaN）原樣回傳 `[x, v]`。"}],"returns":{"type":"[Number, Number]","en":"The new position and velocity.","zh":"新的位置與速度。"},"notes":[{"en":"Advanced. The [`Spring`](#Spring) class holds the position and the velocity for you and calls this.","zh":"進階用法。[`Spring`](#Spring) 類別會替你保管位置與速度，並呼叫這個函式。"}],"examples":[{"code":"import { springStep } from '@openav/chop';\n\nlet x = 0, v = 0;\nconst seen = [];\nfor (let frame = 0; frame < 8; frame++) {\n  [x, v] = springStep(x, v, 1, 100, 6, 1 / 60);   // pulled toward 1\n  seen.push(x.toFixed(2));\n}\ndocument.getElementById('app').textContent = seen.join('  ');","caption":{"en":"Eight frames of a spring released at 0 and pulled toward 1: it accelerates (0.01, 0.05, 0.11 …).","zh":"從 0 放開、朝 1 拉的彈簧的八個畫面：愈來愈快（0.01、0.05、0.11 …）。"},"run":"module","lang":"js"},{"code":"import { springStep } from '@openav/chop';\n\nconst [x, v] = springStep(0, 0, 1, 100, 6, 10);   // a ten-second step in one go\ndocument.getElementById('app').textContent = `after 10 s: position ${x.toFixed(3)}, velocity ${v.toFixed(3)}`;","caption":{"en":"A ten-second step lands on the target (position 1.000, velocity 0.000) instead of exploding.","zh":"一次走十秒也只是落在目標上（位置 1.000、速度 0.000），不會發散。"},"run":"module","lang":"js"}],"related":["Spring","spring","approach"],"keywords":{"en":["spring","damped","oscillator","physics","bounce","overshoot","integrator","step"],"zh":["彈簧","阻尼","振盪","物理","彈跳","過衝"]},"tbd":[],"claims":[]},
{"id":"TD_LAG_RATIO","key":"TD_LAG_RATIO","name":"TD_LAG_RATIO","aliases":[],"kind":"constant","package":"@openav/chop","category":"chop","documented":true,"signature":"TD_LAG_RATIO = 2.302585092994046","import":"import { TD_LAG_RATIO } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":29,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L29"},"summary":{"en":"`Math.LN10`, about 2.303: how many time constants a lag needs to cover 90% of a jump, which is what TouchDesigner's Lag seconds mean.","zh":"`Math.LN10`，約 2.303：一個 lag 走完 90% 要幾個時間常數，也就是 TouchDesigner 的 Lag 秒數的意思。"},"description":[{"en":"TouchDesigner's Lag CHOP quotes its seconds as roughly the time the output takes to follow 90% of a change. A one-pole lag reaches 90% after ln 10 time constants, so `seconds / TD_LAG_RATIO` is the time constant to hand to [`approach()`](#approach). [`Lag`](#Lag) does exactly that, which is why `lag(x, 0.1)` in a p5 sketch moves like a Lag CHOP set to 0.1.","zh":"TouchDesigner 的 Lag CHOP 把秒數定義成大約是輸出追上 90% 變化所花的時間。一階低通要走 ln 10 個時間常數才到 90%，所以 `seconds / TD_LAG_RATIO` 就是該交給 [`approach()`](#approach) 的時間常數。[`Lag`](#Lag) 做的正是這件事，所以 p5 草圖裡的 `lag(x, 0.1)` 動起來就像把 Lag CHOP 設成 0.1。"},{"en":"You only need it when you mix the two kinds of seconds: a time constant `tau` is `tdSeconds / TD_LAG_RATIO`, and `tdSeconds` is `tau * TD_LAG_RATIO`. Attack and decay of an [`envelope()`](#envelope) are plain lengths and are not converted.","zh":"只有在兩種秒數混著用的時候才會用到它：時間常數 `tau` 等於 `tdSeconds / TD_LAG_RATIO`，`tdSeconds` 等於 `tau * TD_LAG_RATIO`。[`envelope()`](#envelope) 的 attack 和 decay 是單純的長度，不用換算。"}],"value":{"type":"Number","en":"Exactly `Math.LN10`, 2.302585092994046. A fixed number: it never changes.","zh":"就是 `Math.LN10`，2.302585092994046。固定的數字，不會變。"},"params":[],"notes":[],"examples":[{"code":"import { approach, TD_LAG_RATIO } from '@openav/chop';\n\nconst tdSeconds = 0.5;                       // as you would type it into a TouchDesigner Lag CHOP\nconst tau = tdSeconds / TD_LAG_RATIO;        // the time constant approach() wants\nconst reached = approach(0, 1, tau, tdSeconds);\ndocument.getElementById('app').textContent =\n  `tau = ${tau.toFixed(3)} s, after ${tdSeconds} s the value is ${reached.toFixed(2)}`;","caption":{"en":"Prints \"tau = 0.217 s, after 0.5 s the value is 0.90\": half a second of Lag CHOP seconds is 90% of the way.","zh":"印出「tau = 0.217 s, after 0.5 s the value is 0.90」：Lag CHOP 的 0.5 秒就是走完 90%。"},"run":"module","lang":"js"}],"related":["approach","Lag","lag"],"keywords":{"en":["ln 10","touchdesigner","lag chop","seconds","time constant","90%","ratio","convert"],"zh":["時間常數","touchdesigner","lag chop","秒數","換算","比例","90%"]},"tbd":[],"claims":[{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"OAVp5.chop.module.TD_LAG_RATIO","equals":2.302585092994046}]},
{"id":"Trail","key":"Trail","name":"Trail","aliases":[],"kind":"class","package":"@openav/chop","category":"chop","documented":true,"signature":"new Trail({ seconds = 2, rate = 60 } = {})","import":"import { Trail } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":169,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L169"},"summary":{"en":"Remembers the input over the last few seconds as a list of values, oldest first, at a fixed 60 values a second; the Trail CHOP.","zh":"把輸入最近幾秒的變化記成一串數值，最舊的在前，固定每秒 60 個，就是 Trail CHOP。"},"description":[{"en":"`trail.step(value, dt)` returns an array of `seconds * rate` past values followed by this step's input at the end, so the array is `seconds * rate + 1` long (121 for the defaults). Draw it across the canvas as a line: that is a scope of the value.","zh":"`trail.step(value, dt)` 回傳一個陣列：`seconds * rate` 個過去的值，最後接上這一步的輸入，所以長度是 `seconds * rate + 1`（預設是 121）。把它橫跨畫布畫成一條線，就是這個數值的示波器。"},{"en":"The length is fixed from the first step, filled with the first value, so a sketch can draw the whole line from frame one. The values are interpolated between steps, so 30 fps and 144 fps give the same line. A gap longer than the whole window (a tab that was hidden) restarts it filled with the new value.","zh":"長度從第一步就固定，用第一個值填滿，所以草圖從第一個畫面就能畫出整條線。步與步之間會內插，所以 30 fps 和 144 fps 得到同一條線。如果有一段空檔比整個視窗還長（例如分頁被藏起來），就用新的值填滿重新開始。"},{"en":"In a p5 sketch use [`trail()`](#trail) and [`drawTrail()`](#drawTrail).","zh":"在 p5 草圖裡請用 [`trail()`](#trail) 和 [`drawTrail()`](#drawTrail)。"}],"params":[{"name":"seconds","type":"Number","en":"How many seconds of history to keep. 0 or less falls back to 2.","zh":"要保留幾秒的歷史。0 或更小的話改用 2。"},{"name":"rate","type":"Number","en":"Values per second in the list. 0 or less falls back to 60.","zh":"清單裡每秒幾個值。0 或更小的話改用 60。"}],"needs":{"en":"Nothing to load. Pass the seconds since the previous frame as `dt`. A value that is not a number leaves the history as it is and returns the current list.","zh":"不用載入任何東西。把距離上一個畫面的秒數當作 `dt` 傳進來。不是數字的值不會改動歷史，只是回傳目前的清單。"},"notes":[],"examples":[{"code":"import { Trail } from '@openav/chop';\n\nconst trail = new Trail({ seconds: 0.1 });   // 6 values a tenth of a second\nlet list;\nfor (let i = 0; i < 10; i++) list = trail.step(i, 1 / 60);\ndocument.getElementById('app').textContent =\n  `${list.length} values: ${list.join(' ')}`;","caption":{"en":"Prints \"7 values: 4 5 6 7 8 9 9\": the last six steps of the input, then the newest input once more at the end.","zh":"印出「7 values: 4 5 6 7 8 9 9」：輸入最近的六步，最後再接一次最新的輸入。"},"run":"module","lang":"js"},{"code":"import { Trail } from '@openav/chop';\n\nconst ctx = document.getElementById('c').getContext('2d');\nconst trail = new Trail({ seconds: 3 });\nlet last = performance.now();\n\nfunction frame(now) {\n  const dt = (now - last) / 1000; last = now;\n  const list = trail.step(0.5 + 0.5 * Math.sin(now / 400), dt);   // a slow sine, 0..1\n  ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\n  ctx.strokeStyle = '#4af'; ctx.beginPath();\n  list.forEach((v, i) => ctx.lineTo((i / (list.length - 1)) * 400, 260 - v * 220));\n  ctx.stroke();\n  requestAnimationFrame(frame);\n}\nrequestAnimationFrame(frame);","caption":{"en":"A scope of a sine wave that scrolls across the canvas: the newest value is at the right edge, with three seconds of history behind it (flat on the left until that much has passed).","zh":"一個正弦波的示波器，從畫布上捲過去：最新的值在右邊緣，後面是三秒的歷史（過滿三秒之前，左邊是平的）。"},"run":"module","lang":"js"}],"related":["trail","drawTrail","scope","Slope"],"keywords":{"en":["trail","trail chop","history","buffer","scope","oscilloscope","last seconds","ring buffer","plot"],"zh":["軌跡","trail chop","歷史","緩衝","示波器","最近幾秒","繪圖"]},"tbd":[],"claims":[{"before":"(lag(0), new Promise((done) => { const t = setInterval(() => { if (OAVp5.chop.state === 'on') { clearInterval(t); done(); } }, 20); setTimeout(done, 5000); }))","expr":"new OAVp5.chop.module.Trail().step(0, 0).length","equals":121}],"members":[{"name":"size","member":"getter","signature":"","doc":"","line":175},{"name":"ordered","member":"method","signature":"","doc":"","line":183},{"name":"step","member":"method","signature":"value, dt, seconds = this.seconds","doc":"","line":184},{"name":"values","member":"method","signature":"","doc":"oldest → newest, the newest being the last input","line":204},{"name":"reset","member":"method","signature":"","doc":"","line":205}]},
{"id":"wrapCount","key":"wrapCount","name":"wrapCount","aliases":[],"kind":"function","package":"@openav/chop","category":"chop","documented":true,"signature":"wrapCount(total, limit)","import":"import { wrapCount } from '@openav/chop';","source":{"file":"packages/chop/index.js","line":237,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chop/index.js#L237"},"summary":{"en":"Loops a count to 0 … limit − 1: `wrapCount(5, 4)` is 1. A limit of 0 or less leaves the count as it is.","zh":"把計數循環到 0 … 上限 − 1：`wrapCount(5, 4)` 是 1。上限是 0 或更小就維持原樣。"},"description":[{"en":"The modulo that [`Count`](#Count) uses, made safe for negative numbers: `wrapCount(-1, 4)` is 3, where `-1 % 4` is -1. It is useful on its own for anything that loops, such as a step number: `wrapCount(frameCount, 16)`.","zh":"[`Count`](#Count) 使用的取餘數，並且對負數也安全：`wrapCount(-1, 4)` 是 3，而 `-1 % 4` 是 -1。拿來處理任何要循環的東西也很方便，例如步序編號：`wrapCount(frameCount, 16)`。"}],"params":[{"name":"total","type":"Number","en":"The count so far.","zh":"目前累計的次數。"},{"name":"limit","type":"Number","en":"How many values to loop through. A fraction is rounded down. 0, a negative number, or no value at all returns `total` unchanged.","zh":"要循環幾個值。小數會無條件捨去。0、負數或沒給，就原樣回傳 `total`。"}],"returns":{"type":"Number","en":"`total` looped into 0 … `limit` − 1 (never negative, even for a negative `total`).","zh":"循環到 0 … `limit` − 1 的 `total`（就算 `total` 是負的，結果也不會是負的）。"},"notes":[],"examples":[{"code":"import { wrapCount } from '@openav/chop';\n\nconst row = [0, 3, 4, 5, 9, -1].map((n) => wrapCount(n, 4));\ndocument.getElementById('app').textContent =\n  `0 3 4 5 9 -1 in a loop of 4: ${row.join(' ')}   no limit: ${wrapCount(5, 0)}`;","caption":{"en":"Prints 0 3 0 1 1 3, then 5 when there is no limit.","zh":"印出 0 3 0 1 1 3。沒有上限的話則是 5。"},"run":"module","lang":"js"}],"related":["Count","count"],"keywords":{"en":["wrap","modulo","mod","loop","cycle","wrap around","step number"],"zh":["循環","取餘數","包裹","繞回來","步序"]},"tbd":[],"claims":[]},
{"id":"recordingIsOn","key":"recordingIsOn","name":"recordingIsOn","aliases":[],"kind":"variable","package":"oav-p5","category":"record","documented":true,"signature":"recordingIsOn","source":{"file":"p5/oav-p5.js","line":317,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L317"},"summary":{"en":"Whether a recording is running right now.","zh":"現在有沒有在錄影。"},"description":[{"en":"Use it to keep things out of the video: `if (!recordingIsOn) text('press r to record', 20, 30);`, because everything on the canvas is recorded. Use it for a key that toggles: `recordingIsOn ? stopRecording() : startRecording()`. It changes when a take starts and when it ends, and reading it never starts anything. For the other stages (`starting`, `saving`) read [`OAVp5.record.state`](#OAVp5.record.state).","zh":"用它把東西留在影片之外：`if (!recordingIsOn) text('press r to record', 20, 30);`，因為畫布上的所有東西都會被錄進去。也可以用在開關鍵上：`recordingIsOn ? stopRecording() : startRecording()`。它只在錄影開始和結束的時候改變，讀它不會啟動任何東西。想知道其他階段（`starting`、`saving`），請讀 [`OAVp5.record.state`](#OAVp5.record.state)。"}],"value":{"type":"Boolean","en":"True while a take runs. It is false at the start, while the recorder is still loading, from the moment [`stopRecording()`](#stopRecording) is called (the file is then being made) and after the take.","zh":"錄影進行中是 true。一開始、錄影程式還在載入、從呼叫 [`stopRecording()`](#stopRecording) 的那一刻起（檔案這時還在製作）、以及錄完之後，都是 false。"},"params":[],"notes":[{"en":"A sketch that declares its own variable called `recordingIsOn` keeps it: oav-p5 leaves the name alone and a card says to read `OAVp5.record.state === 'on'` instead.","zh":"如果草圖本來就宣告了自己的 `recordingIsOn` 變數，就用你的：oav-p5 不動這個名稱，並用一張卡片提醒你改讀 `OAVp5.record.state === 'on'`。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(16);\n}\n\nfunction keyPressed() {\n  if (key === 'r') recordingIsOn ? stopRecording() : startRecording();\n}\n\nfunction draw() {\n  background(20);\n  fill(255, 120, 90);\n  circle(200 + sin(frameCount * 0.05) * 120, 150, 50);\n  if (!recordingIsOn) {   // the guide never reaches the video\n    fill(255);\n    text('r: start / stop recording', 20, 30);\n  }\n}","caption":{"en":"The guide text is on the canvas until you press r, and gone while recording, so the video has only the circle.","zh":"按 r 之前畫布上有說明文字，錄影的時候消失，所以影片裡只有那個圓。"},"run":"sketch","lang":"js"}],"related":["startRecording","stopRecording","OAVp5.record.state","soundIsOn","drumsAreOn"],"keywords":{"en":["recording","is recording","status","record state","on"],"zh":["錄影中","有沒有在錄","狀態","錄影狀態"]},"tbd":[],"claims":[{"expr":"recordingIsOn","equals":false}]},
{"id":"recordingSaved","key":"recordingSaved","name":"recordingSaved","aliases":[],"kind":"event","package":"oav-p5","category":"record","documented":true,"signature":"function recordingSaved(info) {}","source":{"file":"p5/oav-p5.js","line":1911,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1911"},"summary":{"en":"An event you define: it is called when a recording has been saved and checked, with an `info` object about the file.","zh":"一個由你定義的事件函式：錄影存好、檢查完之後呼叫它，帶著一個說明這支檔案的 `info` 物件。"},"description":[{"en":"Define `function recordingSaved(info) { … }` to know when the file is done: [`stopRecording()`](#stopRecording) or the end of `seconds` leads to it. By then the file has already been handed to the browser to download. The same `info` is given to the promise of `stopRecording()` and to `OAVp5.record.on('saved', fn)`, and stays in [`OAVp5.record.last`](#OAVp5.record.last).","zh":"定義 `function recordingSaved(info) { … }`，就知道檔案什麼時候好了：[`stopRecording()`](#stopRecording) 或 `seconds` 時間到之後會走到這裡。那時檔案已經交給瀏覽器下載了。同一份 `info` 也會交給 `stopRecording()` 回傳的 promise 和 `OAVp5.record.on('saved', fn)`，並且留在 [`OAVp5.record.last`](#OAVp5.record.last)。"},{"en":"`info.url` works as a link to the file (`createA(info.url, 'play')` or a `<video>`) until a minute after the next take is saved. `warnings` is what the card after the take says in words; an empty list means nothing looked wrong.","zh":"`info.url` 可以當成檔案的連結用（`createA(info.url, 'play')` 或 `<video>`），直到下一支錄好之後一分鐘為止。`warnings` 是錄完之後卡片用文字說的那些事，空的清單表示沒有哪裡不對。"}],"params":[{"name":"info","type":"Object","en":"`name` (the file name), `format` (`'mp4'` or `'webm'`), `type` (the exact MIME type), `seconds` (length, to a tenth), `bytes`, `width` and `height` (even numbers for MP4), `fps`, `audio` (the video has a sound track), `heard` (something sounded), `sources` (which sounds were in it: `'sound'`, `'drums'`, `'mic'`, `'p5sound'`), `warnings` (what looked wrong: `'recEmpty'`, `'recWebm'`, `'recNoAudio'`, `'recSilent'`, `'recBlack'`, usually an empty list), `blob` (the file) and `url` (an address for the blob).","zh":"`name`（檔名）、`format`（`'mp4'` 或 `'webm'`）、`type`（精確的 MIME 類型）、`seconds`（長度，精確到 0.1 秒）、`bytes`、`width` 和 `height`（MP4 會是偶數）、`fps`、`audio`（影片有沒有音軌）、`heard`（有沒有真的出過聲）、`sources`（錄進去的聲音來源：`'sound'`、`'drums'`、`'mic'`、`'p5sound'`）、`warnings`（看起來哪裡不對：`'recEmpty'`、`'recWebm'`、`'recNoAudio'`、`'recSilent'`、`'recBlack'`，通常是空的）、`blob`（檔案本身）、`url`（blob 的位址）。"}],"notes":[],"examples":[{"code":"let saved = '';\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(3);\n}\n\nfunction recordingSaved(info) {\n  saved = info.name + '  ' + info.format + '  ' + info.seconds + ' s  ' + round(info.bytes / 1024) + ' KB';\n}\n\nfunction draw() {\n  background(20);\n  circle(200 + sin(frameCount * 0.05) * 120, 150, 50);\n  if (!recordingIsOn) {\n    fill(255);\n    text(saved || 'press r to record 3 seconds', 20, 30);\n  }\n}","caption":{"en":"Press r: three seconds later the file is downloaded and its name, format, length and size appear on the canvas.","zh":"按 r：三秒後檔案下載完成，檔名、格式、長度和大小會顯示在畫布上。"},"run":"sketch","lang":"js"},{"code":"let note = '';\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(4);\n}\n\nfunction recordingSaved(info) {\n  note = info.heard ? 'with sound: ' + info.sources.join(', ') : 'silent: click the page before you record';\n  if (info.warnings.length) note += '  (' + info.warnings.join(', ') + ')';\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  if (!recordingIsOn) text(note || 'click once, then press r', 20, 30);\n}","caption":{"en":"Press r (after one click on the page): the text says whether the video has sound, from which sources, and lists anything that looked wrong.","zh":"（先在頁面上點一下）再按 r：文字會說影片有沒有聲音、來自哪些來源，以及有沒有哪裡不對。"},"run":"sketch","lang":"js"}],"related":["startRecording","stopRecording","OAVp5.record.last","OAVp5.record.on","recordingIsOn"],"keywords":{"en":["recording saved","file saved","recording done","info","callback","blob","download","file name"],"zh":["錄影存好","檔案完成","錄完","檔案資訊","callback","檔名"]},"tbd":[],"claims":[]},
{"id":"startRecording","key":"startRecording","name":"startRecording","aliases":["OAVp5.record.start"],"kind":"function","package":"oav-p5","category":"record","documented":true,"signature":"startRecording([o])","source":{"file":"p5/oav-p5.js","line":2306,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2306"},"summary":{"en":"Records the canvas and the sound your sketch makes into one video file, and returns a promise that says whether the recording began.","zh":"把畫布和草圖發出的聲音錄成一支影片檔，並回傳一個 promise，告訴你錄影有沒有開始。"},"description":[{"en":"Start it from a key or a click: `if (key === 'r') startRecording();`. What goes into the video is the canvas alone, at its own pixel size (a retina screen doubles it), and everything your sketch sounds like: the instrument of [`startSound()`](#startSound), the drum machine, the pads, the microphone while it is on, and p5.sound 1.x, mixed into one track while the speakers play as before. The 🎹, the drum grid and the cards are part of the page, not of the canvas, so they are never in the video. A sound that comes on during the take joins it.","zh":"從按鍵或點擊啟動：`if (key === 'r') startRecording();`。進影片的只有畫布本身（用畫布自己的像素大小，視網膜螢幕會是兩倍），加上你的草圖發出的所有聲音：[`startSound()`](#startSound) 的樂器、鼓機、打擊墊、開著的麥克風、p5.sound 1.x，混成一條音軌，喇叭照常播放。🎹、鼓機格子和提示卡片是頁面的一部分，不在畫布上，所以不會出現在影片裡。錄到一半才開起來的聲音也會加入。"},{"en":"The file is MP4 (H.264 + AAC, which Instagram, YouTube and every editor take) where the browser can write it, and WebM where not; a card says Instagram will not take a WebM. It is named `oav-20261015-183005.mp4`, with `name` in the middle when you give one. When the take is done it is downloaded, and on a phone that can share files a Share button appears on the card. After [`stopRecording()`](#stopRecording), or when `seconds` has run out, [`recordingSaved(info)`](#recordingSaved) is called with the details. Without `seconds` a take stops by itself at the longest length (300 seconds unless the `reclimit` option says otherwise).","zh":"瀏覽器寫得出 MP4 的話，檔案就是 MP4（H.264 + AAC，Instagram、YouTube 和所有剪輯軟體都吃），不然就是 WebM，這時會有卡片說 Instagram 不收 WebM。檔名像 `oav-20261015-183005.mp4`，有給 `name` 的話會放在中間。錄完會自動下載，在能分享檔案的手機上，卡片會出現「分享」按鈕。呼叫 [`stopRecording()`](#stopRecording) 之後，或 `seconds` 時間到了，會呼叫 [`recordingSaved(info)`](#recordingSaved) 並把細節交給它。沒給 `seconds` 的話，錄到最長長度就會自己停（預設 300 秒，載入選項 `reclimit` 可以改）。"},{"en":"Without `bitrate` the quality follows the canvas: 0.16 bits per pixel per frame, kept between 4 and 16 megabits a second (24 above 30 frames a second). Calling `startRecording()` while a take is running starts nothing new. After every take a card says what was recorded and, when something looks wrong, what and why: nothing reached the sound track although a sound was on, WebM, a file that is almost empty, a black video of a canvas that was not black.","zh":"沒給 `bitrate` 的話，畫質跟著畫布大小走：每個像素、每一格 0.16 位元，限制在每秒 4 到 16 百萬位元之間（每秒超過 30 格時是 24）。已經在錄的時候再呼叫 `startRecording()`，不會開新的一支。每支錄完之後，卡片會說錄了什麼，有地方不對的話還會說是什麼、為什麼：有聲音開著、卻沒有任何東西進到音軌、WebM、幾乎是空的檔案、畫布明明不是黑的、影片卻是黑的。"}],"params":[{"name":"o","type":"Number | Object","optional":true,"en":"A number is the length in seconds: `startRecording(10)` stops by itself after ten. An object can hold `seconds` (the same, never more than the longest take, 300 by default), `fps` (frames a second, at most 60, 30 by default), `mic` (`false` keeps the microphone out of the video), `name` (a word for the file name), `canvas` (the canvas to record: a `<canvas>`, a p5 canvas or a `createGraphics()` buffer; the biggest p5 canvas on the page by default) and `bitrate` (bits a second). Leave it out to record until [`stopRecording()`](#stopRecording).","zh":"給數字就是秒數：`startRecording(10)` 錄十秒後自己停。給物件的話，可以放 `seconds`（同樣是秒數，不會超過一支影片的長度上限，預設 300）、`fps`（每秒幾格，最多 60，預設 30）、`mic`（`false` 就不把麥克風錄進影片）、`name`（檔名裡的一個詞）、`canvas`（要錄的畫布：`<canvas>`、p5 畫布或 `createGraphics()` 的緩衝區，預設是頁面上最大的那張 p5 畫布）和 `bitrate`（每秒幾位元）。不傳就一直錄到呼叫 [`stopRecording()`](#stopRecording)。"}],"returns":{"type":"Promise<Boolean>","en":"Resolves to `true` once the recording is running (or when one already is). Resolves to `false` when the loading option `record` is `off`, when there is no canvas to record, when this browser has no `MediaRecorder`, or when it could not start; a card says why.","zh":"錄影開始之後 resolve 成 `true`（已經在錄的話也是 `true`）。載入選項 `record` 是 `off`、找不到要錄的畫布、這個瀏覽器沒有 `MediaRecorder`、或是啟動失敗的時候，resolve 成 `false`，並且會有一張卡片說明原因。"},"needs":{"en":"A browser that has `MediaRecorder`: where it is missing, a card says so and the promise resolves to `false`. The picture is recorded at once, but the page can only make sound after a click or key press on it, so a take that begins before the first one has nothing to hear at first. A canvas that shows an image from another site (no CORS) cannot be recorded, and a card says so.","zh":"瀏覽器要有 `MediaRecorder`，沒有的話會跳出卡片說明，promise 也會 resolve 成 `false`。畫面馬上就錄，但頁面要有一次點擊或按鍵之後才能發聲，所以在第一次點擊之前開始的錄影，一開始是沒有聲音可錄的。畫布上如果有別的網站來的圖片（沒有 CORS），就錄不了，卡片會告訴你。"},"notes":[{"en":"In the 🎹 panel, ⏺ starts and stops a take with no code, and 📷 saves the canvas as a PNG (see [`OAVp5.record.shot()`](#OAVp5.record.shot)). While a take runs the 🎹 button shows a red dot and the time. The loading options [`record`](#option-record) (`on`, `nomic` or `off`), `recfps`, `reclimit` and `title` set the defaults.","zh":"在 🎹 面板裡，⏺ 不用寫程式就能開始和停止錄影，📷 把畫布存成 PNG（見 [`OAVp5.record.shot()`](#OAVp5.record.shot)）。錄影的時候，🎹 按鈕上會有紅點和時間。載入選項 [`record`](#option-record)（`on`、`nomic` 或 `off`）、`recfps`、`reclimit`、`title` 可以設定預設值。"},{"en":"The microphone is in the take while it is on: wear headphones, or the speakers are recorded twice. p5.sound for p5 2.x connects each sound straight to the speakers, so it cannot be recorded (a card says so); use p5 1.11, or [`startSound()`](#startSound) and [`startDrums()`](#startDrums). Sound that your sketch makes with its own Tone.js or Web Audio code is not recorded either.","zh":"麥克風開著的時候會錄進影片：請戴耳機，不然喇叭的聲音會被錄兩次。p5 2.x 用的 p5.sound 把每個聲音直接接到喇叭，沒辦法錄（卡片會說明），請改用 p5 1.11，或用 [`startSound()`](#startSound) 和 [`startDrums()`](#startDrums)。草圖自己用 Tone.js 或 Web Audio 程式發出的聲音，同樣不會被錄進去。"},{"en":"Whatever you draw on the canvas is in the video, so show hints only while [`recordingIsOn`](#recordingIsOn) is false.","zh":"畫在畫布上的東西都會進影片，所以提示文字只在 [`recordingIsOn`](#recordingIsOn) 是 false 的時候畫。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(5);   // five seconds, then it stops and downloads\n}\n\nfunction draw() {\n  background(20);\n  circle(200 + sin(frameCount * 0.05) * 120, 150, 50);\n  if (!recordingIsOn) {\n    fill(255);\n    text('press r to record 5 seconds', 20, 30);   // not drawn while recording\n  }\n}","caption":{"en":"Press r: after five seconds an MP4 of the moving circle is downloaded, without the hint text.","zh":"按 r：五秒後會下載一支圓在移動的 MP4，影片裡沒有那行提示文字。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 110);\n  startSound('synth/pluck');\n}\n\nfunction keyPressed() {\n  if (key === 'r') {\n    if (recordingIsOn) stopRecording();\n    else startRecording({ name: 'my piece', fps: 60 });\n  }\n  if (key === 'a') midi.play(60, 0.8);\n}\n\nfunction kickDetected(level) {\n  background(255 * level);\n}\n\nfunction draw() {\n  background(20);\n  if (!recordingIsOn) {\n    fill(255);\n    text('click once, then: a plays a note, r starts and stops a recording', 20, 30);\n  }\n}","caption":{"en":"Click once so the browser allows sound, then press r to start and r again to stop: the file holds the picture, the drums and the notes you played.","zh":"先點一下讓瀏覽器允許出聲，再按 r 開始、再按 r 停止：檔案裡有畫面、鼓聲和你彈的音。"},"run":"sketch","lang":"js"},{"code":"let layer;\n\nfunction setup() {\n  createCanvas(400, 300);\n  layer = createGraphics(200, 200);\n}\n\nfunction keyPressed() {\n  if (key === 'g') startRecording({ canvas: layer, seconds: 3 });   // record the buffer, not the main canvas\n}\n\nfunction draw() {\n  background(20);\n  layer.background(40);\n  layer.circle(100 + sin(frameCount * 0.1) * 60, 100, 40);\n  image(layer, 100, 50);\n  fill(255);\n  text('press g to record the small square only (3 s)', 20, 30);\n}","caption":{"en":"Press g: the video is 200 by 200, the buffer alone, not the 400 by 300 canvas.","zh":"按 g：影片是 200 × 200，只有那個緩衝區，不是 400 × 300 的整張畫布。"},"run":"sketch","lang":"js"}],"related":["stopRecording","recordingIsOn","recordingSaved","OAVp5.record","OAVp5.record.shot","startSound","startDrums","drawRhythm"],"keywords":{"en":["record","recording","video","capture","mp4","webm","screen recording","export video","instagram","canvas","mediarecorder"],"zh":["錄影","錄製","影片","輸出影片","擷取","mp4","存成影片","畫布","作品影片"]},"tbd":[],"claims":[{"expr":"recordingIsOn","equals":false},{"expr":"OAVp5.record.state","equals":"off"}]},
{"id":"stopRecording","key":"stopRecording","name":"stopRecording","aliases":["OAVp5.record.stop"],"kind":"function","package":"oav-p5","category":"record","documented":true,"signature":"stopRecording()","source":{"file":"p5/oav-p5.js","line":2307,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2307"},"summary":{"en":"Stops the recording; the video is finished, downloaded, and handed to `recordingSaved(info)` and to the returned promise.","zh":"停止錄影，影片完成後會下載，並交給 `recordingSaved(info)` 和回傳的 promise。"},"description":[{"en":"It ends a take that has no `seconds`, or ends one early. [`recordingIsOn`](#recordingIsOn) turns `false` at once, while the file is made (`OAVp5.record.state` is `'saving'` until then), so the promise or [`recordingSaved()`](#recordingSaved) is how you know the file is ready. Both come after a fraction of a second, a little more for a long take.","zh":"用來結束沒有給 `seconds` 的錄影，或是提早結束。[`recordingIsOn`](#recordingIsOn) 馬上變成 `false`，檔案這時還在製作（`OAVp5.record.state` 在那之前是 `'saving'`），所以要用 promise 或 [`recordingSaved()`](#recordingSaved) 才知道檔案好了。兩者都在不到一秒之後，很長的一支會再久一點。"}],"params":[],"returns":{"type":"Promise<Object | null>","en":"Resolves, once the file is saved and checked, to the same `info` that [`recordingSaved(info)`](#recordingSaved) gets. Resolves to `null` at once when nothing is recording, or when the recording was still starting (nothing was recorded then).","zh":"檔案存好、檢查完之後，resolve 成跟 [`recordingSaved(info)`](#recordingSaved) 拿到的同一份 `info`。沒有在錄、或錄影還在啟動中（那時還沒錄到東西）的時候，馬上 resolve 成 `null`。"},"notes":[],"examples":[{"code":"let message = 'press r to record, r again to stop';\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key !== 'r') return;\n  if (recordingIsOn) {\n    stopRecording().then((info) => {\n      if (info) message = 'saved ' + info.name;   // the file is ready\n    });\n  } else {\n    startRecording();\n  }\n}\n\nfunction draw() {\n  background(20);\n  circle(200 + sin(frameCount * 0.05) * 120, 150, 50);\n  if (!recordingIsOn) {\n    fill(255);\n    text(message, 20, 30);\n  }\n}","caption":{"en":"Press r to start and r to stop: once the file is ready the text names it.","zh":"按 r 開始、再按 r 停止：檔案好了之後，文字會顯示它的檔名。"},"run":"sketch","lang":"js"}],"related":["startRecording","recordingIsOn","recordingSaved","OAVp5.record.state"],"keywords":{"en":["stop recording","finish","end recording","save video","download"],"zh":["停止錄影","結束錄影","存成影片","下載"]},"tbd":[],"claims":[{"before":"stopRecording()","expr":"OAVp5.record.state","equals":"off"}]},
{"id":"OAVp5.record","key":"OAVp5.record","name":"OAVp5.record","aliases":[],"kind":"object","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record","source":{"file":"p5/oav-p5.js","line":1906,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1906"},"summary":{"en":"The object behind the recording names: the state, the time, the last file, the formats this browser can write and the screenshot function.","zh":"錄影相關名稱背後的物件：狀態、時間、最後一支檔案、這個瀏覽器能寫的格式，以及存圖的函式。"},"description":[{"en":"`OAVp5.record` (also reachable as `midi.record`) is what [`startRecording()`](#startRecording), [`stopRecording()`](#stopRecording) and [`recordingIsOn`](#recordingIsOn) are built on. It holds the [`state`](#OAVp5.record.state) of the recording, how many [`seconds`](#OAVp5.record.seconds) it has run, the [`last`](#OAVp5.record.last) file's `info`, the [`sources`](#OAVp5.record.sources) in the take, and the [`fps`](#OAVp5.record.fps) and [`limit`](#OAVp5.record.fps) in force. [`formats()`](#OAVp5.record.formats) says what the browser can write, [`shot()`](#OAVp5.record.shot) saves a PNG of the canvas and [`on()`](#OAVp5.record.on) listens to `'start'` and `'saved'`. `start` and `stop` are the same as the two global functions.","zh":"`OAVp5.record`（也可以用 `midi.record` 取得）是 [`startRecording()`](#startRecording)、[`stopRecording()`](#stopRecording) 和 [`recordingIsOn`](#recordingIsOn) 的底層。裡面有錄影的 [`state`](#OAVp5.record.state)、已經錄了多少 [`seconds`](#OAVp5.record.seconds)、[`last`](#OAVp5.record.last) 最後一支檔案的 `info`、這一支裡有哪些聲音來源 [`sources`](#OAVp5.record.sources)，以及生效的 [`fps`](#OAVp5.record.fps) 和 [`limit`](#OAVp5.record.fps)。[`formats()`](#OAVp5.record.formats) 說明瀏覽器能寫什麼格式，[`shot()`](#OAVp5.record.shot) 把畫布存成 PNG，[`on()`](#OAVp5.record.on) 監聽 `'start'` 和 `'saved'`。`start` 和 `stop` 跟那兩個全域函式一模一樣。"}],"params":[],"notes":[{"en":"For a sketch the global names are enough. This object is for tools, the console and debugging.","zh":"一般草圖用全域名稱就夠了。這個物件是給工具、主控台和除錯用的。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const r = OAVp5.record;\n  text('state: ' + r.state, 20, 30);\n  text('seconds: ' + nf(r.seconds, 1, 1), 20, 60);\n  text('limit: ' + r.limit + ' s at ' + r.fps + ' fps', 20, 90);\n}","caption":{"en":"Three fields of the recording object; the state stays \"off\" until a take starts (press ⏺ in the 🎹 panel).","zh":"錄影物件的三個欄位，開始錄影之前狀態一直是 \"off\"（可以按 🎹 面板裡的 ⏺ 試試）。"},"run":"sketch","lang":"js"}],"related":["startRecording","stopRecording","recordingIsOn","OAVp5.sound","OAVp5.drums"],"keywords":{"en":["OAVp5.record","record object","midi.record","recording state","debug"],"zh":["錄影物件","錄影狀態","除錯","midi.record"]},"tbd":[],"claims":[]},
{"id":"OAVp5.record.formats","key":"OAVp5.record.formats","name":"OAVp5.record.formats","aliases":[],"kind":"function","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.formats()","source":{"file":"p5/oav-p5.js","line":1895,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1895"},"summary":{"en":"Says what this browser can record: `{ recorder, mp4, webm, mime }`.","zh":"告訴你這個瀏覽器能錄什麼：`{ recorder, mp4, webm, mime }`。"},"description":[{"en":"Use it before you offer a recording button, or to tell the person which file they will get: MP4 goes to Instagram and every editor, WebM does not. It asks the browser each time you call it.","zh":"在你要提供錄影按鈕之前用它，或是告訴使用的人會拿到哪種檔案：MP4 可以直接上傳 Instagram，也能放進所有剪輯軟體，WebM 不行。每次呼叫都會重新問一次瀏覽器。"}],"params":[],"returns":{"type":"Object","en":"`recorder` is true when the browser has `MediaRecorder` at all, `mp4` and `webm` are true when it can write that format, and `mime` is the MIME type a recording asks for first (MP4 where possible), or `''` when none works.","zh":"`recorder` 表示瀏覽器到底有沒有 `MediaRecorder`，`mp4` 和 `webm` 表示能不能寫那種格式，`mime` 是錄影最先要求的 MIME 類型（可以的話是 MP4），都不行的話是 `''`。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const f = OAVp5.record.formats();\n  text('recorder: ' + f.recorder, 20, 30);\n  text('mp4: ' + f.mp4 + '   webm: ' + f.webm, 20, 60);\n  text('asks for: ' + (f.mime || 'nothing'), 20, 90);\n}","caption":{"en":"Shows what this browser can write: Chrome 126 and later and Safari say mp4 true.","zh":"顯示這個瀏覽器能寫的格式：Chrome 126 之後的版本和 Safari 會顯示 mp4 為 true。"},"run":"sketch","lang":"js"}],"related":["startRecording","OAVp5.record"],"keywords":{"en":["formats","mp4","webm","mime","supported","mediarecorder","browser support"],"zh":["格式","mp4","webm","支援","瀏覽器支援","MIME"]},"tbd":[],"claims":[{"expr":"OAVp5.record.formats().recorder","equals":true}]},
{"id":"OAVp5.record.fps","key":"OAVp5.record.fps","name":"OAVp5.record.fps","aliases":["OAVp5.record.limit"],"kind":"variable","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.fps","source":{"file":"p5/oav-p5.js","line":1887,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1887"},"summary":{"en":"The frame rate (`fps`) and the longest take in seconds (`limit`) that the loading options `recfps` and `reclimit` set.","zh":"載入選項 `recfps` 和 `reclimit` 設定的錄影格率（`fps`）和一支影片最長的秒數（`limit`）。"},"description":[{"en":"They are there to read: `OAVp5.record.limit` tells a sketch how long a take can be. Writing to them changes nothing, because a take reads the options, not these two. To record at another frame rate once, pass `startRecording({ fps: 60 })`; to change the limit, set the `reclimit` option before oav-p5 loads (`window.OAV_P5 = { reclimit: 600 }`).","zh":"它們是拿來讀的：`OAVp5.record.limit` 讓草圖知道一支影片最長能錄多久。對它們賦值沒有作用，因為錄影讀的是載入選項，不是這兩個值。想用別的格率錄一次，就傳 `startRecording({ fps: 60 })`，要改長度上限，在 oav-p5 載入之前設定 `reclimit` 選項（`window.OAV_P5 = { reclimit: 600 }`）。"}],"value":{"type":"Number","range":"fps 1..60, limit 1 …","en":"`fps` is 30 and `limit` is 300 unless the options say otherwise (`fps` is kept between 1 and 60).","zh":"`fps` 預設是 30，`limit` 預設是 300，除非載入選項另外設定（`fps` 會被限制在 1 到 60 之間）。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('a take is ' + OAVp5.record.fps + ' frames a second', 20, 30);\n  text('and at most ' + OAVp5.record.limit + ' seconds (' + OAVp5.record.limit / 60 + ' minutes)', 20, 60);\n}","caption":{"en":"Prints the two defaults: 30 frames a second and 300 seconds.","zh":"印出兩個預設值：每秒 30 格、最長 300 秒。"},"run":"sketch","lang":"js"}],"related":["startRecording","OAVp5.record"],"keywords":{"en":["fps","frame rate","limit","maximum length","recfps","reclimit","longest take"],"zh":["格率","每秒幾格","長度上限","最長","recfps","reclimit"]},"tbd":[],"claims":[{"expr":"OAVp5.record.fps","equals":30},{"expr":"OAVp5.record.limit","equals":300}]},
{"id":"OAVp5.record.last","key":"OAVp5.record.last","name":"OAVp5.record.last","aliases":[],"kind":"variable","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.last","source":{"file":"p5/oav-p5.js","line":1887,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1887"},"summary":{"en":"The `info` of the last recording that was saved (the same object `recordingSaved` got), or `null` before the first one.","zh":"最近一支存好的錄影的 `info`（跟 `recordingSaved` 拿到的是同一個物件），第一支之前是 `null`。"},"description":[{"en":"Handy when you did not keep `info` in `recordingSaved()`: show the last file name, or put `OAVp5.record.last.url` in a link. The address stays valid until a minute after the next take is saved.","zh":"當你沒有在 `recordingSaved()` 裡把 `info` 存下來的時候很方便：顯示最後一支的檔名，或把 `OAVp5.record.last.url` 放進連結。這個位址會一直有效，直到下一支存好之後一分鐘。"}],"value":{"type":"Object | null","en":"Replaced each time a take is saved. See [`recordingSaved()`](#recordingSaved) for its fields.","zh":"每存好一支就換成新的。欄位見 [`recordingSaved()`](#recordingSaved)。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textSize(14);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(2);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const last = OAVp5.record.last;\n  text(last ? 'last file: ' + last.name + ' (' + last.seconds + ' s)' : 'nothing recorded yet, press r', 20, 30);\n}","caption":{"en":"Press r: two seconds later the last file name appears; press again and it changes.","zh":"按 r：兩秒後會顯示最近一支的檔名，再錄一次它就會換掉。"},"run":"sketch","lang":"js"}],"related":["recordingSaved","OAVp5.record.seconds","stopRecording"],"keywords":{"en":["last recording","last take","previous file","info","file name","url"],"zh":["最近一支","上一次錄影","檔名","連結"]},"tbd":[],"claims":[{"expr":"OAVp5.record.last","equals":"null"}]},
{"id":"OAVp5.record.on","key":"OAVp5.record.on","name":"OAVp5.record.on","aliases":[],"kind":"function","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.on(name, fn)","source":{"file":"p5/oav-p5.js","line":1897,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1897"},"summary":{"en":"Listens to the recording from anywhere: `OAVp5.record.on('saved', fn)` or `on('start', fn)`, and it returns a function that stops listening.","zh":"在任何地方監聽錄影：`OAVp5.record.on('saved', fn)` 或 `on('start', fn)`，並回傳一個可以取消監聽的函式。"},"description":[{"en":"[`recordingSaved()`](#recordingSaved) is a single function that you define. `on()` is for the cases where that does not fit: several listeners, a library, a listener you want to remove again. It is the same idea as `OAVp5.drums.on()` and `midi.on()`. An error thrown inside `fn` is printed and the other listeners still run.","zh":"[`recordingSaved()`](#recordingSaved) 是你定義的單一函式。`on()` 是給不適用的情況：好幾個監聽者、函式庫、或是想把監聽者再拿掉。概念跟 `OAVp5.drums.on()` 和 `midi.on()` 一樣。`fn` 裡丟出的錯誤會被印出來，其他監聽者照樣執行。"}],"params":[{"name":"name","type":"String","en":"`'start'` (a take began) or `'saved'` (a file is saved, like [`recordingSaved()`](#recordingSaved)). Any other name throws an error that lists the two.","zh":"`'start'`（一支錄影開始了）或 `'saved'`（檔案存好了，跟 [`recordingSaved()`](#recordingSaved) 一樣）。其他的名字會丟出錯誤，並列出這兩個。"},{"name":"fn","type":"Function","en":"For `'start'` it gets `{ width, height, fps, type, seconds }` (`seconds` is 0 when no length was asked for). For `'saved'` it gets the same `info` as [`recordingSaved()`](#recordingSaved).","zh":"`'start'` 拿到的是 `{ width, height, fps, type, seconds }`（沒指定長度的話 `seconds` 是 0）。`'saved'` 拿到的是跟 [`recordingSaved()`](#recordingSaved) 一樣的 `info`。"}],"returns":{"type":"Function","en":"A function: call it to stop listening.","zh":"一個函式，呼叫它就停止監聽。"},"notes":[],"examples":[{"code":"let status = 'press r to record 2 seconds';\n\nfunction setup() {\n  createCanvas(400, 300);\n  OAVp5.record.on('start', (i) => { status = 'recording ' + i.width + 'x' + i.height + ' at ' + i.fps + ' fps'; });\n  OAVp5.record.on('saved', (i) => { status = 'saved ' + i.name; });\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(2);\n}\n\nfunction draw() {\n  background(20);\n  circle(200 + sin(frameCount * 0.05) * 120, 150, 50);\n  if (!recordingIsOn) {\n    fill(255);\n    text(status, 20, 30);\n  }\n}","caption":{"en":"Press r: the text reports the start (size and frame rate) and then the saved file name, from two listeners.","zh":"按 r：文字先回報開始錄影（大小和格率），再回報存好的檔名，這是由兩個監聽者做的。"},"run":"sketch","lang":"js"}],"related":["recordingSaved","startRecording","OAVp5.drums.on","OAVp5.record"],"keywords":{"en":["listen","subscribe","event","on saved","on start","listener","off"],"zh":["監聽","訂閱","事件","取消監聽","錄影事件"]},"tbd":[],"claims":[{"expr":"typeof OAVp5.record.on('saved', function () {})","equals":"function"}]},
{"id":"OAVp5.record.seconds","key":"OAVp5.record.seconds","name":"OAVp5.record.seconds","aliases":[],"kind":"variable","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.seconds","source":{"file":"p5/oav-p5.js","line":1887,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1887"},"summary":{"en":"How long the running take has been going, in seconds, and the length of the last take after it.","zh":"正在錄的這一支已經錄了幾秒，錄完之後是那一支的長度。"},"description":[{"en":"Use it as a clock that starts with the take: `const t = recordingIsOn ? OAVp5.record.seconds : 0;` makes an animation that always begins at 0 in the video. It is the time since the recording began, not the time of the sketch, so it does not count the moment before `startRecording()`.","zh":"當成跟著這一支錄影一起開始的時鐘：`const t = recordingIsOn ? OAVp5.record.seconds : 0;` 做出來的動畫，在影片裡一定從 0 開始。它是錄影開始之後的時間，不是草圖的時間，所以不包含 `startRecording()` 之前的那段。"}],"value":{"type":"Number","range":"0 …","en":"0 at the start, then refreshed four times a second while recording. When the take is saved it is that take's length, and the next take sets it back to 0.","zh":"一開始是 0，錄影中每秒更新四次。檔案存好之後是那一支的長度，下一支開始的時候又歸零。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(4);\n}\n\nfunction draw() {\n  background(20);\n  const t = recordingIsOn ? OAVp5.record.seconds : 0;   // 0 at the first frame of the video\n  fill(255, 160, 80);\n  circle(40 + t * 80, 150, 40);                         // slides right, 80 px a second\n  if (!recordingIsOn) {\n    fill(255);\n    text('press r: the video starts with the circle at the left', 20, 30);\n  }\n}","caption":{"en":"Press r: the circle jumps to the left edge and slides right for four seconds, so the video always begins the same way.","zh":"按 r：圓跳到最左邊，往右滑四秒，所以影片每次的開頭都一樣。"},"run":"sketch","lang":"js"}],"related":["recordingIsOn","OAVp5.record.state","startRecording","OAVp5.record.last"],"keywords":{"en":["seconds","elapsed","recording time","timer","duration","clock"],"zh":["秒數","已錄時間","錄影時間","計時","長度"]},"tbd":[],"claims":[{"expr":"OAVp5.record.seconds","equals":0}]},
{"id":"OAVp5.record.shot","key":"OAVp5.record.shot","name":"OAVp5.record.shot","aliases":[],"kind":"function","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.shot([o])","source":{"file":"p5/oav-p5.js","line":1893,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1893"},"summary":{"en":"Saves the canvas alone as a PNG picture (not the 🎹, not the cards) and downloads it.","zh":"把畫布本身存成 PNG 圖片（不包含 🎹 和提示卡片）並下載。"},"description":[{"en":"It is the button 📷 in the 🎹 panel as a function. The file is named `oav-20261015-183005.png` (with `name` in the middle when you give one) and is downloaded at once; on a phone that can share files a Share button appears on a card instead. The picture has the size of the canvas in pixels, so a retina screen gives twice the width and height. p5's own `saveCanvas('name', 'png')` does the same thing without the card.","zh":"就是 🎹 面板裡 📷 按鈕的函式版。檔名像 `oav-20261015-183005.png`（有給 `name` 的話會放在中間），馬上下載，在能分享檔案的手機上，會改成在卡片上出現「分享」按鈕。圖片的大小就是畫布的像素大小，所以視網膜螢幕的寬和高會是兩倍。p5 自己的 `saveCanvas('name', 'png')` 做的是同一件事，只是沒有卡片。"}],"params":[{"name":"o","type":"Object","optional":true,"en":"Options: `canvas` (the canvas to save: a `<canvas>`, a p5 canvas or a `createGraphics()` buffer; the biggest p5 canvas on the page by default) and `name` (a word for the file name).","zh":"選項：`canvas`（要存的畫布：`<canvas>`、p5 畫布或 `createGraphics()` 的緩衝區，預設是頁面上最大的 p5 畫布）和 `name`（檔名裡的一個詞）。"}],"returns":{"type":"Promise<Object | null>","en":"Resolves to `{ name, blob, url, width, height, bytes }`, or to `null` when there is no canvas or it cannot be read (an image from another site on it, no CORS).","zh":"resolve 成 `{ name, blob, url, width, height, bytes }`，找不到畫布、或畫布讀不出來（上面有別的網站來的圖片，沒有 CORS）的時候是 `null`。"},"notes":[{"en":"With the loading option `record=off` the 📷 button is gone and [`startRecording()`](#startRecording) does nothing, but this function still saves the PNG (tried).","zh":"載入選項 `record=off` 的時候，📷 按鈕會消失，[`startRecording()`](#startRecording) 也什麼都不做，但這個函式仍然可以存 PNG（實際試過）。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'p') OAVp5.record.shot({ name: 'still' });\n}\n\nfunction draw() {\n  background(20);\n  circle(mouseX, mouseY, 60);\n  fill(255);\n  text('press p to save a PNG of the canvas', 20, 30);\n}","caption":{"en":"Move the circle with the mouse and press p: a PNG of the canvas is downloaded.","zh":"用滑鼠移動圓，按 p：會下載一張畫布的 PNG。"},"run":"sketch","lang":"js"}],"related":["startRecording","OAVp5.record","recordingSaved"],"keywords":{"en":["screenshot","png","still","save image","save canvas","snapshot","picture"],"zh":["截圖","png","存圖","靜態圖","快照","存畫布"]},"tbd":[],"claims":[{"expr":"typeof OAVp5.record.shot","equals":"function"}]},
{"id":"OAVp5.record.sources","key":"OAVp5.record.sources","name":"OAVp5.record.sources","aliases":[],"kind":"variable","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.sources","source":{"file":"p5/oav-p5.js","line":1903,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1903"},"summary":{"en":"Which sounds the running take is hearing right now: a list of `'sound'`, `'drums'`, `'mic'` and `'p5sound'`; empty when no take runs.","zh":"正在錄的這一支現在錄進了哪些聲音：由 `'sound'`、`'drums'`、`'mic'`、`'p5sound'` 組成的清單，沒在錄的時候是空的。"},"description":[{"en":"It is what the take has connected, not whether something is audible at this moment. A source that is switched on halfway joins the list within a quarter of a second. After the take, [`recordingSaved()`](#recordingSaved) gives the final list in `info.sources`.","zh":"它是這一支已經接上的來源，不是此刻有沒有聲音聽得到。錄到一半才打開的來源，會在四分之一秒內加進清單。錄完之後，最終的清單在 [`recordingSaved()`](#recordingSaved) 的 `info.sources` 裡。"}],"value":{"type":"Array<String>","en":"A new list on every read. `'sound'` is the instrument of [`startSound()`](#startSound), `'drums'` the drum machine and the pads, `'mic'` the microphone, `'p5sound'` p5.sound 1.x.","zh":"每次讀都是一份新的清單。`'sound'` 是 [`startSound()`](#startSound) 的樂器，`'drums'` 是鼓機和打擊墊，`'mic'` 是麥克風，`'p5sound'` 是 p5.sound 1.x。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('breakbeat', 120);\n  startSound('synth/pluck');\n  textSize(14);\n}\n\nfunction keyPressed() {\n  if (key === 'r') recordingIsOn ? stopRecording() : startRecording();\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(recordingIsOn ? 'in the take: ' + OAVp5.record.sources.join(', ') : 'press r to start a take', 20, 30);\n}","caption":{"en":"Press r: the text lists what the take has connected, here the drum machine and the instrument.","zh":"按 r：文字列出這一支接上了什麼，這裡是鼓機和樂器。"},"run":"sketch","lang":"js"}],"related":["recordingSaved","startRecording","startSound","startDrums","startMic"],"keywords":{"en":["sources","audio sources","what is recorded","mic in take","tracks"],"zh":["來源","聲音來源","錄進了什麼","音軌"]},"tbd":[],"claims":[{"expr":"JSON.stringify(OAVp5.record.sources)","equals":"[]"}]},
{"id":"OAVp5.record.state","key":"OAVp5.record.state","name":"OAVp5.record.state","aliases":[],"kind":"variable","package":"oav-p5","category":"record","documented":true,"signature":"OAVp5.record.state","source":{"file":"p5/oav-p5.js","line":1886,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1886"},"summary":{"en":"Where the recording is: `'off'`, `'starting'`, `'on'` or `'saving'`.","zh":"錄影現在在哪個階段：`'off'`、`'starting'`、`'on'` 或 `'saving'`。"},"description":[{"en":"[`recordingIsOn`](#recordingIsOn) is `state === 'on'`. Read the state when the other stages matter: a button can show \"starting…\" and \"saving…\" and refuse a new take while one is being saved.","zh":"[`recordingIsOn`](#recordingIsOn) 就是 `state === 'on'`。其他階段有影響的時候才需要讀它：按鈕可以顯示「啟動中……」和「儲存中……」，在檔案還在存的時候不接受新的一支。"}],"value":{"type":"String","range":"off | starting | on | saving","en":"Starts as `'off'`. A take goes `'starting'` (the recorder is loading, the first time), `'on'` (recording), `'saving'` (the file is being made) and back to `'off'`.","zh":"一開始是 `'off'`。一支錄影會依序是 `'starting'`（錄影程式載入中，第一次才會有）、`'on'`（錄影中）、`'saving'`（檔案製作中），然後回到 `'off'`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(28);\n}\n\nfunction keyPressed() {\n  if (key === 'r' && OAVp5.record.state === 'off') startRecording(3);\n}\n\nfunction draw() {\n  background(20);\n  circle(200 + sin(frameCount * 0.05) * 120, 220, 40);\n  fill(255);\n  text(OAVp5.record.state === 'off' ? 'press r' : OAVp5.record.state, width / 2, 70);\n}","caption":{"en":"Press r: the word goes through starting, on and saving and comes back to the prompt; the key does nothing in between.","zh":"按 r：文字依序經過 starting、on、saving，然後回到提示，中間再按 r 沒有作用。"},"run":"sketch","lang":"js"}],"related":["recordingIsOn","startRecording","stopRecording","OAVp5.record"],"keywords":{"en":["record state","status","saving","starting","recording stage"],"zh":["錄影狀態","儲存中","啟動中","錄影階段"]},"tbd":[],"claims":[{"expr":"OAVp5.record.state","equals":"off"}]},
{"id":"AUDIO_MIMES","key":"AUDIO_MIMES","name":"AUDIO_MIMES","aliases":[],"kind":"constant","package":"@openav/record","category":"record","documented":true,"signature":"AUDIO_MIMES = array, 5 items","import":"import { AUDIO_MIMES } from '@openav/record';","source":{"file":"packages/record/mime.js","line":22,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/mime.js#L22"},"summary":{"en":"The audio MIME types the recorder tries for a sound-only file, best first: AAC in .m4a, then Opus.","zh":"只錄聲音的檔案時，錄影器會依序嘗試的音訊 MIME 型別，最好的在前：先是 .m4a 的 AAC，再來是 Opus。"},"description":[{"en":"AAC in an `.m4a` file comes first because it imports into every DAW and editor. This is the list a [`Recorder`](#Recorder) with `audioOnly: true` uses, through [`pickMime()`](#pickMime).","zh":"AAC 的 `.m4a` 檔排第一，因為它每個 DAW 和剪輯軟體都能匯入。`audioOnly: true` 的 [`Recorder`](#Recorder) 用的就是這份清單，透過 [`pickMime()`](#pickMime)。"}],"value":{"type":"String[]","en":"A frozen list of 5 strings: `audio/mp4;codecs=mp4a.40.2`, `audio/webm;codecs=opus`, `audio/ogg;codecs=opus`, `audio/mp4`, `audio/webm`.","zh":"一份凍結的清單，有 5 個字串：`audio/mp4;codecs=mp4a.40.2`、`audio/webm;codecs=opus`、`audio/ogg;codecs=opus`、`audio/mp4`、`audio/webm`。"},"params":[],"notes":[],"examples":[{"code":"import { AUDIO_MIMES, pickMime, extFor } from '@openav/record';\n\nconst mime = pickMime((m) => MediaRecorder.isTypeSupported(m), { audioOnly: true });\nconsole.log(AUDIO_MIMES[0]);        // audio/mp4;codecs=mp4a.40.2\nconsole.log(mime, extFor(mime));    // the one this browser would use, and its file extension","caption":{"en":"Logs the best candidate for a sound file, and the one this browser would pick with its extension.","zh":"印出聲音檔最好的候選，以及這個瀏覽器實際會挑的那一個和它的副檔名。"},"run":"module","lang":"js"}],"related":["VIDEO_MIMES","pickMime","extFor","Recorder"],"keywords":{"en":["mime","m4a","aac","opus","audio format","sound file","candidates"],"zh":["mime","m4a","aac","opus","音訊格式","聲音檔","候選"]},"tbd":[],"claims":[]},
{"id":"AudioTap","key":"AudioTap","name":"AudioTap","aliases":[],"kind":"class","package":"@openav/record","category":"record","documented":true,"signature":"new AudioTap({ context = null, sources = [], captureDestination = false } = {})","import":"import { AudioTap } from '@openav/record';","source":{"file":"packages/record/audio.js","line":44,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/audio.js#L44"},"summary":{"en":"Collects the sound of your work (and, if you add it, a microphone) into one recordable track, without sending anything to the speakers.","zh":"把你的作品的聲音（以及你加進來的麥克風）收集成一條可以錄的音軌，不會把任何東西送到喇叭。"},"description":[{"en":"Recording the speakers with a microphone gives you the room. Tapping the Web Audio graph gives you the instrument exactly as it was synthesized, on the same clock as the canvas. An `AudioTap` has two buses and two outputs. The **work** bus is the sound of the piece; the **extra** bus is for things that belong in the video but not in the sound file, such as a microphone. `tap.stream` and `tap.tracks` are work and extras together (give them to the video [`Recorder`](#Recorder)); `tap.workStream` and `tap.workTracks` are the work alone (for the separate audio file). Both are a single track, on purpose: Chrome records only the first audio track of a stream.","zh":"用麥克風錄喇叭，錄到的是房間。接上 Web Audio 的節點圖，錄到的是樂器合成出來的原樣，而且跟畫布用同一個時鐘。`AudioTap` 有兩條匯流排和兩個輸出。**work** 匯流排是作品的聲音，**extra** 匯流排放的是該在影片裡、卻不該在聲音檔裡的東西，例如麥克風。`tap.stream` 和 `tap.tracks` 是 work 加 extra（交給影片的 [`Recorder`](#Recorder)），`tap.workStream` 和 `tap.workTracks` 只有 work（給另外存的聲音檔）。兩個都只有一條音軌，這是故意的：Chrome 只會錄一個串流的第一條音軌。"},{"en":"The tap never routes anything to the speakers, so adding a microphone cannot feed back. To record a sketch you did not write (p5.sound, Tone.js, [`Sound`](#Sound)), call `captureDestination()` **before** the work starts its sound: from then on every node that connects to a speaker destination is tapped too.","zh":"這個 tap 絕不會把東西送到喇叭，所以加麥克風不會產生回授。要錄不是你寫的草圖（p5.sound、Tone.js、[`Sound`](#Sound)），就在作品開始出聲**之前**呼叫 `captureDestination()`：從那時起，每個接到喇叭的節點也會被接進來。"}],"params":[{"name":"context","type":"AudioContext","optional":true,"default":"null","en":"The `AudioContext` to mix in, usually the work's own. One is made if you leave it out; it needs Web Audio to exist.","zh":"要在哪個 `AudioContext` 裡混音，通常是作品自己的。沒給的話會新建一個，需要瀏覽器有 Web Audio。"},{"name":"sources","type":"Array","optional":true,"default":"[]","en":"Things to add to the work bus at once: `AudioNode`s, `MediaStream`s or `MediaStreamTrack`s (see `add()`).","zh":"要馬上加進作品匯流排的東西：`AudioNode`、`MediaStream` 或 `MediaStreamTrack`（見 `add()`）。"},{"name":"captureDestination","type":"Boolean","optional":true,"default":"false","en":"Also call `captureDestination()` right away.","zh":"同時馬上呼叫 `captureDestination()`。"}],"needs":{"en":"An `AudioContext` that is running. Browsers start audio only after a click, so call `tap.resume()` from a click handler. Without the click the tracks exist but carry silence.","zh":"要有正在跑的 `AudioContext`。瀏覽器要點擊過才讓聲音開始，所以在點擊的處理函式裡呼叫 `tap.resume()`。沒有點擊的話，音軌雖然存在，裡面是靜音。"},"notes":[],"examples":[{"code":"import { AudioTap } from '@openav/record';\n\nconst tap = new AudioTap();                              // makes its own AudioContext\nconst osc = new OscillatorNode(tap.context, { frequency: 440 });\nconst remove = tap.add(osc);                             // the work bus\nosc.start();\nawait tap.resume();                                      // from a click in a real page\nawait new Promise((done) => setTimeout(done, 300));\nconsole.log(tap.tracks.length, tap.workTracks.length);   // 1 1\nconsole.log(tap.level() > 0.1);                          // true: sound is reaching the recording\nremove();","caption":{"en":"A 440 Hz tone is tapped and the meter confirms that sound reaches the recording. The tracks are ready to hand to a Recorder.","zh":"接上一個 440 Hz 的音，指示確認聲音有進到錄影裡。音軌已經可以交給 Recorder。"},"run":"module","lang":"js"},{"code":"import { AudioTap } from '@openav/record';\n\nconst tap = new AudioTap();\nconst uninstall = tap.captureDestination();              // before the work starts its sound\n\nconst ctx = new AudioContext();                          // \"someone else's\" sketch, in its own context\nconst osc = new OscillatorNode(ctx, { frequency: 330 });\nosc.connect(ctx.destination);                            // goes to the speakers, and into the tap\nosc.start();\nawait new Promise((done) => setTimeout(done, 300));\nconsole.log(tap.level() > 0.05);                         // true\nuninstall();\ntap.dispose();","caption":{"en":"A sound made in another AudioContext, connected to the speakers the usual way, is picked up by the tap too: this is how you record a sketch you did not write.","zh":"另一個 AudioContext 裡的聲音，照平常接到喇叭，tap 也收得到：錄不是你寫的草圖就是這樣做。"},"run":"module","lang":"js"}],"related":["Recorder","mixTracks","Compositor","Sound","startRecording"],"keywords":{"en":["audio tap","record sound","web audio","mix","capture","speakers","tone.js","p5.sound","track","stream"],"zh":["收音","錄聲音","web audio","混音","擷取","喇叭","音軌","串流"]},"tbd":[],"claims":[],"members":[{"name":"stream","member":"getter","signature":"","doc":"Work + extras, one track: give it to the video Recorder.","line":77},{"name":"workStream","member":"getter","signature":"","doc":"The work alone, one track: the separate audio file.","line":79},{"name":"tracks","member":"getter","signature":"","doc":"`stream`'s audio tracks (one).","line":81},{"name":"workTracks","member":"getter","signature":"","doc":"`workStream`'s audio tracks (one).","line":83},{"name":"add","member":"method","signature":"source, { bus = 'work', gain = 1 } = {}","doc":"Tap a source.","line":93},{"name":"captureDestination","member":"method","signature":"","doc":"From now on, every node that connects to an AudioDestinationNode (any context) is tapped too.","line":136},{"name":"addMicrophone","member":"method","signature":"{ deviceId = '', gain = 1, echoCancellation = false, noiseSuppression = false, autoGainControl = false } = {}","doc":"Add a microphone (or any audio input) to the video's mix — not to the audio file.","line":161},{"name":"removeMicrophone","member":"method","signature":"{ stop = true } = {}","doc":"Take the microphone out of the mix (and stop it unless `stop: false`).","line":171},{"name":"level","member":"method","signature":"","doc":"Peak level of the mix right now, 0..1 — for a \"sound is reaching the recording\" meter.","line":179},{"name":"resume","member":"method","signature":"","doc":"Resume a suspended context (call from a click).","line":187},{"name":"dispose","member":"method","signature":"","doc":"Let go of everything the tap made (the context itself is left alone).","line":190}]},
{"id":"cameraConstraints","key":"cameraConstraints","name":"cameraConstraints","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"cameraConstraints({ deviceId = '', width = 1920, height = 1080, fps = 30, facingMode = '' } = {})","import":"import { cameraConstraints } from '@openav/record';","source":{"file":"packages/record/camera.js","line":13,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/camera.js#L13"},"summary":{"en":"Builds the getUserMedia options for a camera: it asks for 1920 by 1080 at 30 fps, but only as an ideal, never as a demand.","zh":"做出開鏡頭用的 getUserMedia 選項：它要求 1920 乘 1080、30 fps，但只是理想值，不是硬性要求。"},"description":[{"en":"Size and rate are `ideal`, never `exact`: a camera that cannot do 1920 by 1080 should open at what it can, not throw in front of an audience. It is pure, so you can look at what [`openCamera()`](#openCamera) will ask for. The stream has no audio: a microphone is added through [`AudioTap`](#AudioTap).","zh":"尺寸和畫格率是 `ideal`，不是 `exact`：做不到 1920 乘 1080 的鏡頭應該用它做得到的規格開起來，而不是在觀眾面前丟出錯誤。它是純函式，所以你可以看 [`openCamera()`](#openCamera) 會要求什麼。串流裡沒有聲音：麥克風要透過 [`AudioTap`](#AudioTap) 加。"}],"params":[{"name":"deviceId","type":"String","optional":true,"default":"''","en":"A specific camera, from [`listCameras()`](#listCameras). It is asked for as `exact`: if it is gone, opening fails.","zh":"指定哪一個鏡頭，來自 [`listCameras()`](#listCameras)。用 `exact` 要求：如果它不見了，就會開啟失敗。"},{"name":"width","type":"Number","optional":true,"default":"1920","en":"The width to ask for, as `ideal`.","zh":"想要的寬，用 `ideal` 要求。"},{"name":"height","type":"Number","optional":true,"default":"1080","en":"The height to ask for, as `ideal`.","zh":"想要的高，用 `ideal` 要求。"},{"name":"fps","type":"Number","optional":true,"default":"30","en":"The frame rate to ask for, as `ideal`.","zh":"想要的畫格率，用 `ideal` 要求。"},{"name":"facingMode","type":"String","optional":true,"default":"''","en":"`'user'` or `'environment'` on a phone. Ignored when a `deviceId` is given.","zh":"手機上的 `'user'`（前鏡頭）或 `'environment'`（後鏡頭）。有給 `deviceId` 時會被忽略。"}],"returns":{"type":"Object","en":"A `MediaStreamConstraints`: `{ video: { width: { ideal }, height: { ideal }, frameRate: { ideal }, deviceId?, facingMode? }, audio: false }`.","zh":"一個 `MediaStreamConstraints`：`{ video: { width: { ideal }, height: { ideal }, frameRate: { ideal }, deviceId?, facingMode? }, audio: false }`。"},"notes":[],"examples":[{"code":"import { cameraConstraints } from '@openav/record';\n\nconsole.log(JSON.stringify(cameraConstraints()));\n// {\"video\":{\"width\":{\"ideal\":1920},\"height\":{\"ideal\":1080},\"frameRate\":{\"ideal\":30}},\"audio\":false}\nconsole.log(JSON.stringify(cameraConstraints({ deviceId: 'abc', width: 1280, height: 720, fps: 60 }).video));\n// {\"width\":{\"ideal\":1280},\"height\":{\"ideal\":720},\"frameRate\":{\"ideal\":60},\"deviceId\":{\"exact\":\"abc\"}}","caption":{"en":"The default request, and a request for one specific camera at 720p60.","zh":"預設的要求，以及指定某一個鏡頭、要 720p60 的要求。"},"run":"module","lang":"js"}],"related":["openCamera","listCameras","Compositor"],"keywords":{"en":["getUserMedia","constraints","camera","resolution","ideal","facingMode","device id"],"zh":["getUserMedia","限制條件","鏡頭","解析度","理想值","前後鏡頭","裝置 id"]},"tbd":[],"claims":[]},
{"id":"cleanValue","key":"cleanValue","name":"cleanValue","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"cleanValue(v, depth = 0)","import":"import { cleanValue } from '@openav/record';","source":{"file":"packages/record/events.js","line":19,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/events.js#L19"},"summary":{"en":"Makes a copy of a signal value that is safe to write as JSON: rounded numbers, plain arrays, no functions, nothing too deep.","zh":"做出一份訊號值的複本，可以安全地寫成 JSON：數字捨入、陣列變普通陣列、沒有函式、也不會太深。"},"description":[{"en":"The rules: numbers are rounded to 6 decimals, and `NaN` and infinities become `null`. Typed arrays become plain arrays. A `bigint` becomes a number. Objects and arrays are copied down to depth 4 and anything deeper becomes `null`. A function, symbol or `undefined` becomes `null` in an array and the key is dropped from an object. [`EventLog`](#EventLog) runs every value through it.","zh":"規則：數字捨入到小數第 6 位，`NaN` 和無限大變成 `null`。型別陣列變成普通陣列。`bigint` 變成數字。物件和陣列往下複製到第 4 層，再深的變成 `null`。函式、symbol 和 `undefined` 在陣列裡變成 `null`，在物件裡這個鍵會被丟掉。[`EventLog`](#EventLog) 把每個值都經過它。"}],"params":[{"name":"v","type":"any","en":"The value to clean.","zh":"要整理的值。"},{"name":"depth","type":"Number","optional":true,"default":"0","en":"How deep we already are. Leave it out; it is used by the function calling itself.","zh":"目前已經到第幾層。不用給，是函式呼叫自己的時候用的。"}],"returns":{"type":"any","en":"The cleaned copy.","zh":"整理過的複本。"},"notes":[],"examples":[{"code":"import { cleanValue } from '@openav/record';\n\nconst out = cleanValue({\n  a: 1.23456789, list: [1, NaN, () => 1], pos: new Float32Array([0.5, 0.25]), fn() {}, big: 10n,\n});\nconsole.log(JSON.stringify(out));\n// {\"a\":1.234568,\"list\":[1,null,null],\"pos\":[0.5,0.25],\"big\":10}","caption":{"en":"A messy object goes in and plain data comes out: the number is rounded, the function key is gone, the typed array became a list.","zh":"一個亂七八糟的物件進去，出來的是單純的資料：數字被捨入、函式的鍵不見了、型別陣列變成陣列。"},"run":"module","lang":"js"}],"related":["EventLog","normalizeTake"],"keywords":{"en":["clean","json safe","serialize","sanitize","round","typed array"],"zh":["整理","json 安全","序列化","清理","捨入","型別陣列"]},"tbd":[],"claims":[]},
{"id":"Compositor","key":"Compositor","name":"Compositor","aliases":[],"kind":"class","package":"@openav/record","category":"record","documented":true,"signature":"new Compositor({ size = 'vertical-1080p', layout = 'stack', layoutOptions = {}, getWorkCanvas = null, background = '#000', mirrorCam = true, maxFps = 60, onDraw = null, canvas = null } = {})","import":"import { Compositor } from '@openav/record';","source":{"file":"packages/record/compositor.js","line":33,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/compositor.js#L33"},"summary":{"en":"Draws your work and the performer's camera into one canvas of an exact size, every frame: the canvas that a Recorder records.","zh":"每個畫面把你的作品和表演者的鏡頭畫進一張尺寸精確的畫布：這張畫布就是 Recorder 要錄的那張。"},"description":[{"en":"A performance video is two things in one picture: your work and the performer. The `Compositor` makes that picture. It owns a canvas (`comp.canvas`) of exactly the size of the file you want, whatever your window is, so a vertical 4K video from a laptop really is 4K. It is the canvas you hand to a [`Recorder`](#Recorder). To show it on the page as a preview, put `comp.canvas` in the DOM and shrink it with CSS.","zh":"表演影片是同一個畫面裡的兩件事：你的作品和表演者。`Compositor` 就是做這個畫面的。它擁有一張畫布（`comp.canvas`），大小就是你要的檔案大小，跟你的視窗多大無關，所以在筆電上錄的直式 4K 影片真的是 4K。把這張畫布交給 [`Recorder`](#Recorder)。要在頁面上預覽，就把 `comp.canvas` 放進 DOM，再用 CSS 縮小。"},{"en":"`comp.start()` redraws every animation frame (and, because a hidden tab freezes animation frames, from a Web Worker timer too, so a recording does not freeze with the tab). `comp.draw()` draws one frame now: call it yourself right after your work renders if you have your own loop. `comp.rects` is the last layout, `{ work, cam }` in output pixels, and `comp.fps` is the rate it has been drawing at.","zh":"`comp.start()` 每個動畫畫面重畫一次（而且因為被藏起來的分頁會凍結動畫畫面，所以也用 Web Worker 的計時器畫，這樣錄影不會跟著分頁一起凍住）。`comp.draw()` 馬上畫一個畫面：如果你有自己的迴圈，就在作品畫完之後自己呼叫它。`comp.rects` 是最近一次的版面，`{ work, cam }`，單位是輸出的像素，`comp.fps` 是它實際畫的頻率。"},{"en":"The camera is optional and never owned: `setCamera(stream)` shows it and `setCamera(null)` lets go, and you stop the tracks yourself. The camera's area can show a **panel** instead, anything with `draw(ctx, rect, now)`, for example a drawing of the controller being played. The area for the camera is reserved before the camera arrives, so the picture does not jump.","zh":"鏡頭是選用的，而且合成器不擁有它：`setCamera(stream)` 顯示它，`setCamera(null)` 放手，音軌由你自己停。鏡頭的區域也可以改放一個**面板**，也就是任何有 `draw(ctx, rect, now)` 的東西，例如一張正在彈的控制器的圖。鏡頭的區域會在鏡頭來之前就先留好，所以畫面不會跳動。"}],"params":[{"name":"size","type":"String | Object | Number[]","optional":true,"default":"'vertical-1080p'","en":"The size of the output, which is the size of the file. A preset id from [`PRESETS`](#PRESETS) (`'vertical-4k'`), `'1080x1920'`, `[w, h]` or `{ w, h }`. Each side is rounded up to an even number. See [`resolveSize()`](#resolveSize).","zh":"輸出的尺寸，也就是檔案的尺寸。可以是 [`PRESETS`](#PRESETS) 的 id（`'vertical-4k'`）、`'1080x1920'`、`[w, h]` 或 `{ w, h }`。每一邊都會進位成偶數。見 [`resolveSize()`](#resolveSize)。"},{"name":"layout","type":"String","optional":true,"default":"'stack'","en":"Where the work and the camera go: `stack`, `pip`, `side` or `work` (see [`LAYOUTS`](#LAYOUTS)).","zh":"作品和鏡頭放在哪裡：`stack`、`pip`、`side`、`work`（見 [`LAYOUTS`](#LAYOUTS)）。"},{"name":"layoutOptions","type":"Object","optional":true,"default":"{}","en":"Passed on to [`layoutRects()`](#layoutRects): `fit`, `camFit`, `camAnchor`, `minCam`, `split`, `pip`.","zh":"原封不動交給 [`layoutRects()`](#layoutRects)：`fit`、`camFit`、`camAnchor`、`minCam`、`split`、`pip`。"},{"name":"getWorkCanvas","type":"Function","optional":true,"default":"null","en":"Returns the canvas of your work, or `null`. It is asked every frame, because a p5 sketch may replace its canvas and a WebGL work may resize it. The compositor never owns the work.","zh":"回傳你的作品的畫布，或 `null`。每個畫面都會問一次，因為 p5 草圖可能換掉畫布，WebGL 作品可能改變大小。合成器從來不擁有作品。"},{"name":"background","type":"String","optional":true,"default":"'#000'","en":"The colour behind everything, any canvas colour string.","zh":"所有東西後面的顏色，任何畫布能用的顏色字串。"},{"name":"mirrorCam","type":"Boolean","optional":true,"default":"true","en":"Show the camera as a mirror, as performers expect. Keep it equal to the `mirror` of a hand tracker drawn on top.","zh":"把鏡頭顯示成鏡子，這是表演者習慣的樣子。要跟畫在上面的手部追蹤的 `mirror` 保持一樣。"},{"name":"maxFps","type":"Number","optional":true,"default":"60","en":"Draw at most this often when running. A 120 Hz display does not need 120 composites a second.","zh":"跑起來的時候最多這麼頻繁地畫。120 Hz 的螢幕不需要每秒合成 120 次。"},{"name":"onDraw","type":"Function","optional":true,"default":"null","en":"`onDraw(ctx, rects, comp)`, called after the work and the camera are drawn, to put an overlay on top. A context state is saved and restored around it. An error in it is warned about once and does not stop the recording.","zh":"`onDraw(ctx, rects, comp)`，作品和鏡頭畫好之後呼叫，用來在最上面再蓋一層。呼叫前後會保存和還原繪圖狀態。裡面出錯只會警告一次，不會中斷錄影。"},{"name":"canvas","type":"HTMLCanvasElement","optional":true,"default":"null","en":"Draw into this canvas instead of making a new one. Its width and height are set to the output size.","zh":"畫進這張畫布，不另外做新的。它的寬高會被設成輸出的尺寸。"}],"notes":[{"en":"A WebGL work: the compositor copies its canvas in the same animation frame it was drawn in, which works in Chrome. If you see black, create the WebGL context with `preserveDrawingBuffer: true`, or call `comp.draw()` right after your render and do not call `start()`.","zh":"WebGL 的作品：合成器會在作品畫完的同一個動畫畫面裡複製它的畫布，在 Chrome 上這樣就行。如果看到黑的，就用 `preserveDrawingBuffer: true` 建立 WebGL context，或在你渲染完之後馬上呼叫 `comp.draw()`，並且不要呼叫 `start()`。"},{"en":"Webcams usually stop at 1080p30, so a 4K `stack` layout enlarges the camera about 1.6 times. The work above it stays sharp because it is drawn at the output size, as long as the work's own canvas is at least as large as its area.","zh":"網路攝影機通常只到 1080p30，所以 4K 的 `stack` 版面會把鏡頭放大約 1.6 倍。上面的作品是照輸出尺寸畫的，只要作品自己的畫布至少跟它的區域一樣大，就會保持清楚。"},{"en":"In a p5 sketch you do not need this: [`startRecording()`](#startRecording) records the canvas and its sound for you. The `Compositor` is for a page that records the work together with a camera.","zh":"在 p5 草圖裡用不到這個：[`startRecording()`](#startRecording) 會替你錄畫布和它的聲音。`Compositor` 是給要把作品和鏡頭一起錄的頁面用的。"}],"examples":[{"code":"import { Compositor } from '@openav/record';\n\nconst work = document.createElement('canvas');          // the work: any canvas\nwork.width = 600; work.height = 600;\nconst g = work.getContext('2d');\ng.fillStyle = '#c33'; g.fillRect(0, 0, 600, 600);\n\nconst comp = new Compositor({ size: '540x960', layout: 'stack', getWorkCanvas: () => work });\nconst rects = comp.draw();                               // one frame, now\nconsole.log(comp.size, rects.work.h, rects.cam.box.h);   // { w: 540, h: 960 } 540 420\n\ncomp.canvas.style.width = '135px';                       // a preview, scaled down with CSS\ndocument.getElementById('app').append(comp.canvas);","caption":{"en":"A square work is drawn into a 540 by 960 frame: the work takes the top 540 pixels and 420 are kept for the camera. The result appears on the page as a small preview.","zh":"把一個正方形的作品畫進 540 乘 960 的畫面：作品佔上面 540 像素，下面 420 像素留給鏡頭。結果以小預覽的樣子出現在頁面上。"},"run":"module","lang":"js"},{"code":"import { Compositor, openCamera } from '@openav/record';\n\nconst work = document.getElementById('c');              // the 400 x 300 canvas of the page\nconst wg = work.getContext('2d');\nconst comp = new Compositor({ size: 'landscape-1080p', layout: 'pip', getWorkCanvas: () => work,\n  layoutOptions: { pip: { corner: 'br', size: 0.25 } } });\ncomp.start();                                           // redraws every frame\nconst size = await comp.setCamera(await openCamera());  // the browser asks for the camera\nconsole.log(size, comp.rects.cam.w);                    // { w: 1920, h: 1080 } 480 (what the camera gives may differ)\n\nlet x = 0;\n(function animate() {                                   // the work moves, the composite follows\n  wg.fillStyle = '#111'; wg.fillRect(0, 0, 400, 300);\n  wg.fillStyle = '#ffd166'; wg.fillRect(x = (x + 4) % 380, 120, 20, 60);\n  requestAnimationFrame(animate);\n})();\ncomp.canvas.style.width = '480px';\ndocument.getElementById('app').append(comp.canvas);","caption":{"en":"The animated canvas fills a 1080p frame with the camera in a corner. Allow the camera when the browser asks; the composite appears below the work.","zh":"會動的畫布鋪滿 1080p 的畫面，鏡頭在一個角落。瀏覽器問的時候允許鏡頭。合成的結果會出現在作品下面。"},"run":"module","lang":"js"},{"code":"import { Compositor } from '@openav/record';\n\nconst work = document.getElementById('c');\nconst comp = new Compositor({\n  size: [720, 720], layout: 'work', getWorkCanvas: () => work,\n  onDraw(ctx, rects) {                                   // an overlay, on top of everything\n    ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';\n    ctx.font = '32px sans-serif';\n    ctx.fillText('take 1', rects.work.x + 20, rects.work.y + 48);\n  },\n});\nwork.getContext('2d').fillRect(0, 0, 400, 300);\ncomp.draw();\ndocument.getElementById('app').append(comp.canvas);\ncomp.canvas.style.width = '240px';","caption":{"en":"The work alone in a square frame, with a caption written over it by `onDraw`. The caption is part of the picture, so it is in the recording.","zh":"作品單獨放在正方形畫面裡，上面由 `onDraw` 寫了一行字。這行字是畫面的一部分，所以會被錄進影片。"},"run":"module","lang":"js"}],"related":["Recorder","layoutRects","PRESETS","openCamera","AudioTap","startRecording"],"keywords":{"en":["compositor","composite","camera","webcam","canvas","layout","picture in picture","pip","stack","vertical video"],"zh":["合成器","合成","鏡頭","網路攝影機","畫布","版面","子母畫面","直式影片","錄影","鏡像"]},"tbd":[],"claims":[],"members":[{"name":"size","member":"getter","signature":"","doc":"The output size `{ w, h }`.","line":72},{"name":"setSize","member":"method","signature":"size","doc":"Change the output size (between takes: a recorder running on this canvas keeps its old size).","line":78},{"name":"setLayout","member":"method","signature":"layout, options","doc":"","line":90},{"name":"video","member":"getter","signature":"","doc":"The <video> the camera plays in (null without one) — e.g.","line":99},{"name":"cameraSize","member":"getter","signature":"","doc":"The camera's size once it plays, else null.","line":102},{"name":"setCamera","member":"method","signature":"src","doc":"Show a camera.","line":111},{"name":"panel","member":"getter","signature":"","doc":"The panel drawn in the camera's area (null = the camera).","line":136},{"name":"setPanel","member":"method","signature":"panel","doc":"Draw a panel where the camera goes — e.g.","line":146},{"name":"draw","member":"method","signature":"","doc":"Draw one frame now (hosts with their own loop call this right after the work renders).","line":167},{"name":"drawOnCamera","member":"method","signature":"fn","doc":"Draw over the camera in the camera's own coordinates: `fn(ctx, w, h)` gets a context translated to where the WHOLE camera frame lands and clipped to the visible camera rect — the shape HandTracker / PoseTracker…","line":221},{"name":"start","member":"method","signature":"","doc":"Redraw every animation frame (and from a worker heartbeat while the tab is hidden).","line":244},{"name":"stop","member":"method","signature":"","doc":"","line":259},{"name":"dispose","member":"method","signature":"","doc":"Stop drawing and let go of the camera (its tracks are the host's to stop).","line":267}]},
{"id":"EventLog","key":"EventLog","name":"EventLog","aliases":[],"kind":"class","package":"@openav/record","category":"record","documented":true,"signature":"new EventLog({ now = defaultNow, filter = null, throttleMs = 0 } = {})","import":"import { EventLog } from '@openav/record';","source":{"file":"packages/record/events.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/events.js#L38"},"summary":{"en":"Keeps a timestamped list of what happened during a take, such as notes and parameter changes, on the same clock as the recording.","zh":"把一次錄影過程中發生的事，例如音符和參數變化，連同時間記成一份清單，用的是跟錄影同一個時鐘。"},"description":[{"en":"A video keeps the picture and the sound. The log keeps the **score** of the take: every note, pad hit and (throttled) hand position, in milliseconds from the moment recording started. With it a take can be re-rendered at another size, re-synced to a better audio take, or turned into MIDI later. Start it with the recorder's start time so event times line up with the video's first frame: `log.start(recorder.t0)`.","zh":"影片留下的是畫面和聲音。事件記錄留下的是這次錄影的**樂譜**：每個音符、每一下墊子、以及（有節流的）手的位置，從開始錄的那一刻算起的毫秒數。有了它，一段錄影之後可以用別的尺寸重新渲染、跟更好的聲音檔重新對齊，或轉成 MIDI。用錄影器的開始時間來開始它，事件時間才會跟影片的第一個畫面對齊：`log.start(recorder.t0)`。"},{"en":"`log.attach(signals)` logs every signal a [`Signals`](#Signals) bus publishes; pulses are always kept and continuous signals obey `throttleMs`. Or add events yourself with `log.add(name, value)`. Values are cleaned into plain JSON data by [`cleanValue()`](#cleanValue). `toJSON()` gives `{ format: 'openav-eventlog', version: 1, startedAt, durationMs, count, events }`, and `EventLog.fromJSON(json)` reads it back. `toTake()` turns the log into a MIDI take; [`TakeRecorder`](#TakeRecorder) is a log that does exactly that on `midi/*`.","zh":"`log.attach(signals)` 會記下 [`Signals`](#Signals) 匯流排發布的每個訊號。脈衝一定會留，連續的訊號則受 `throttleMs` 限制。也可以自己用 `log.add(name, value)` 加事件。值會被 [`cleanValue()`](#cleanValue) 整理成單純的 JSON 資料。`toJSON()` 給 `{ format: 'openav-eventlog', version: 1, startedAt, durationMs, count, events }`，`EventLog.fromJSON(json)` 能讀回來。`toTake()` 把記錄轉成 MIDI 的 take，[`TakeRecorder`](#TakeRecorder) 就是在 `midi/*` 上剛好做這件事的記錄。"},{"en":"Properties: `events` is the list `[{ t, name, value }]` (`t` in ms, to a tenth), `t0` the clock time of t = 0, `startedAt` the wall-clock time (ISO text) and `durationMs` is `null` until `stop()`.","zh":"屬性：`events` 是 `[{ t, name, value }]` 的清單（`t` 是毫秒，精確到十分之一），`t0` 是 t = 0 的時鐘時間，`startedAt` 是真實時間（ISO 文字），`durationMs` 在 `stop()` 之前是 `null`。"}],"params":[{"name":"now","type":"Function","optional":true,"default":"performance.now","en":"The clock, in milliseconds. The default is the same clock as `Recorder.t0`. Pass your own in a test.","zh":"時鐘，單位是毫秒。預設跟 `Recorder.t0` 是同一個時鐘。測試時傳自己的。"},{"name":"filter","type":"Function","optional":true,"default":"null","en":"`filter(name, value, meta)`, returns whether to keep an event. It applies to `add()` too.","zh":"`filter(name, value, meta)`，回傳要不要留下這個事件。`add()` 也適用。"},{"name":"throttleMs","type":"Number","optional":true,"default":"0","en":"For continuous signals: keep at most one event per name in this many milliseconds. Pulses (note on, note off…) are never throttled.","zh":"對連續的訊號：每個名字在這麼多毫秒內最多留一個事件。脈衝（音符開、音符關…）永遠不會被節流。"}],"notes":[],"examples":[{"code":"import { EventLog } from '@openav/record';\n\nlet clock = 1000;                                   // a clock we move by hand, in ms\nconst log = new EventLog({ now: () => clock });\nlog.start();\nclock = 1100; log.add('midi/note/on', { note: 60, vel: 0.8 });\nclock = 1200; log.mark('chorus');\nclock = 1500; log.stop();\nconsole.log(log.events.map((e) => e.t + ' ' + e.name).join(' | '));   // 100 midi/note/on | 200 mark\nconsole.log(log.durationMs, log.running, log.toJSON().count);          // 500 false 2\nconsole.log(log.add('late'));                                          // null: the take is over","caption":{"en":"A take of half a second on a clock moved by hand: a note at 100 ms, a marker at 200 ms. After `stop()` nothing more is accepted.","zh":"用手動撥動的時鐘錄半秒：100 毫秒一個音符，200 毫秒一個標記。`stop()` 之後就不再收任何事件。"},"run":"module","lang":"js"},{"code":"import { EventLog } from '@openav/record';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst log = new EventLog({ throttleMs: 100 });        // knobs: at most one event per 100 ms\nlog.start();\nconst detach = log.attach(signals);\nsignals.pulse('midi/note/on', { note: 64, vel: 0.7 }); // a pulse: always kept\nfor (let i = 0; i < 10; i++) signals.set('param/hue', i / 10);   // continuous: throttled\ndetach();\nconsole.log(log.events.length, log.events[0].name);    // 2 midi/note/on\nconsole.log(EventLog.fromJSON(log.toJSON()).events.length);   // 2","caption":{"en":"A bus is attached: the note pulse is kept, and ten changes of a continuous signal in a few milliseconds become one event. The log survives a round trip through JSON.","zh":"接上一個匯流排：音符脈衝被留下，連續訊號在幾毫秒內的十次變化只剩一個事件。記錄經過 JSON 來回之後依然完整。"},"run":"module","lang":"js"}],"related":["TakeRecorder","Recorder","cleanValue","Signals","toMidiFile"],"keywords":{"en":["event log","log","score","timestamps","record events","notes played","signals","json","markers"],"zh":["事件記錄","記錄","樂譜","時間戳記","錄事件","彈過的音","訊號","json","標記"]},"tbd":[],"claims":[],"members":[{"name":"running","member":"getter","signature":"","doc":"","line":58},{"name":"start","member":"method","signature":"at = this.now()","doc":"Start a take: clears the log.","line":65},{"name":"stop","member":"method","signature":"at = this.now()","doc":"End the take; later events are ignored.","line":75},{"name":"add","member":"method","signature":"name, value = null, { at = this.now(), pulse = true, meta } = {}","doc":"Log one event.","line":88},{"name":"mark","member":"method","signature":"label, data = null","doc":"A labelled marker (\"chorus\", \"take 2 starts here\") — shows up as name 'mark'.","line":104},{"name":"attach","member":"method","signature":"signals","doc":"Log every signal a `Signals` registry publishes (`signals.onAny`).","line":112},{"name":"detach","member":"method","signature":"","doc":"Detach from every registry.","line":121},{"name":"between","member":"method","signature":"start, end","doc":"Events with start ≤ t < end (ms).","line":124},{"name":"toJSON","member":"method","signature":"","doc":"","line":127},{"name":"toTake","member":"method","signature":"{ name = 'take', meta = {} } = {}","doc":"A take is an EventLog filtered to midi/* — TakeRecorder is exactly that.","line":141},{"name":"fromJSON","member":"static method","signature":"json","doc":"Rebuild a (stopped) log from toJSON() output or its string.","line":155}]},
{"id":"extFor","key":"extFor","name":"extFor","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"extFor(mime)","import":"import { extFor } from '@openav/record';","source":{"file":"packages/record/mime.js","line":57,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/mime.js#L57"},"summary":{"en":"Gives the file extension for a recorded MIME type: video/mp4 is 'mp4', audio/mp4 is 'm4a', anything unknown is 'bin'.","zh":"給一個錄好的 MIME 型別該用的副檔名：video/mp4 是 'mp4'，audio/mp4 是 'm4a'，不認識的是 'bin'。"},"description":[{"en":"A [`Recorder`](#Recorder) puts this in the `ext` of its result. Use it for a download name: `'take.' + extFor(blob.type)`.","zh":"[`Recorder`](#Recorder) 把它放進結果的 `ext`。拿來取下載檔名：`'take.' + extFor(blob.type)`。"}],"params":[{"name":"mime","type":"String","en":"A MIME type, with or without codecs, in any letter case. Anything that is not text counts as empty.","zh":"MIME 型別，有沒有 codecs 都行，大小寫不拘。不是文字的東西當作空字串。"}],"returns":{"type":"String","en":"`mp4`, `m4a` (audio/mp4), `webm`, `ogg`, `mkv` (video/x-matroska) or `bin`.","zh":"`mp4`、`m4a`（audio/mp4）、`webm`、`ogg`、`mkv`（video/x-matroska）或 `bin`。"},"notes":[],"examples":[{"code":"import { extFor } from '@openav/record';\n\nconsole.log(extFor('video/mp4;codecs=avc1.640033,mp4a.40.2'));   // mp4\nconsole.log(extFor('audio/mp4'), extFor('audio/webm'));           // m4a webm\nconsole.log(extFor('video/x-matroska'), extFor('text/plain'));    // mkv bin","caption":{"en":"Five MIME types and the extension each one gets.","zh":"五個 MIME 型別，和各自拿到的副檔名。"},"run":"module","lang":"js"}],"related":["pickMime","Recorder","VIDEO_MIMES","AUDIO_MIMES"],"keywords":{"en":["extension","file name","mime to extension","download name","mp4","m4a","webm"],"zh":["副檔名","檔名","mime 轉副檔名","下載檔名","mp4","m4a","webm"]},"tbd":[],"claims":[]},
{"id":"framePoint","key":"framePoint","name":"framePoint","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"framePoint(camRect, u, v)","import":"import { framePoint } from '@openav/record';","source":{"file":"packages/record/layout.js","line":219,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L219"},"summary":{"en":"Turns a point in the camera's own 0..1 coordinates, like a hand landmark, into a pixel in the output frame.","zh":"把鏡頭自己的 0..1 座標裡的一個點，像手的關節點，換成輸出畫面裡的像素。"},"description":[{"en":"Landmarks from a hand or pose tracker come as fractions of the camera picture. To draw something on the output at the place a landmark is, you need pixels of the output, where the camera may be enlarged and cropped. `framePoint()` does that. It gives the same point whether or not part of the camera is cropped away, because it uses `frame`, the whole camera picture placed in the output. Use the same `mirror` in the tracker and in the compositor.","zh":"手部或姿態追蹤給的關節點是鏡頭畫面的比例。要在輸出上、關節點所在的位置畫東西，需要的是輸出畫面的像素，而鏡頭可能被放大和裁切。`framePoint()` 就是做這件事。不管鏡頭有沒有被裁掉一部分，它給的點都是對的，因為它用的是 `frame`，也就是整個鏡頭畫面放進輸出之後的位置。追蹤器和合成器要用同樣的 `mirror`。"}],"params":[{"name":"camRect","type":"Object | null","en":"The `cam` of [`layoutRects()`](#layoutRects), or of `compositor.rects`.","zh":"[`layoutRects()`](#layoutRects) 的 `cam`，或 `compositor.rects` 的 `cam`。"},{"name":"u","type":"Number","en":"Across the camera picture, 0 left to 1 right, in the displayed (mirrored if `mirrorCam`) orientation.","zh":"橫跨鏡頭畫面，0 是左、1 是右，用顯示出來的方向（有 `mirrorCam` 就是鏡像後的）。"},{"name":"v","type":"Number","en":"Down the camera picture, 0 top to 1 bottom.","zh":"沿著鏡頭畫面往下，0 是上、1 是下。"}],"returns":{"type":"Object | null","en":"`{ x, y }` in output pixels, or `null` when there is no camera (or its size is not known yet).","zh":"輸出像素的 `{ x, y }`，沒有鏡頭（或還不知道它的尺寸）就是 `null`。"},"notes":[],"examples":[{"code":"import { layoutRects, framePoint } from '@openav/record';\n\nconst r = layoutRects('stack', { w: 1080, h: 1920 }, { workW: 1000, workH: 1000, camW: 1920, camH: 1080 });\nconsole.log(framePoint(r.cam, 0.5, 0.5));    // { x: 540, y: 1500 }: the middle of the camera\nconsole.log(framePoint(r.cam, 0, 0));        // { x: -206.67, y: 1080 }: the left edge is cropped away, off the frame\nconsole.log(framePoint(null, 0.5, 0.5));     // null: no camera","caption":{"en":"The centre of the camera lands in the middle of its area. The camera's left edge is outside the frame because the 16:9 picture is cropped to fit.","zh":"鏡頭的中心落在它的區域中間。鏡頭的左邊緣在畫面外面，因為 16:9 的畫面為了塞進去被裁掉了。"},"run":"module","lang":"js"}],"related":["layoutRects","Compositor"],"keywords":{"en":["landmark","hand tracking","pose","overlay","coordinates","camera to output","skeleton"],"zh":["關節點","手部追蹤","姿態","疊加","座標","鏡頭座標轉輸出","骨架"]},"tbd":[],"claims":[]},
{"id":"fromMidiFile","key":"fromMidiFile","name":"fromMidiFile","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"fromMidiFile(bytes, { name, families = 'both' } = {})","import":"import { fromMidiFile } from '@openav/record';","source":{"file":"packages/record/smf.js","line":85,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/smf.js#L85"},"summary":{"en":"Reads a Standard MIDI File into a take whose events are the signals a keyboard playing that file would publish.","zh":"把標準 MIDI 檔讀成 take，裡面的事件就是鍵盤彈這個檔案時會發布的訊號。"},"description":[{"en":"It reads type 0 and type 1 files (the tracks are merged), running status, tempo changes and SMPTE time division; system exclusive messages are skipped. A file that is not a MIDI file throws a `TypeError` (`fromMidiFile: not a MIDI file (no MThd)`), and one that ends early a `RangeError`. Play the result into a show with [`TakePlayer`](#TakePlayer): it drives a work like someone playing the file on a keyboard.","zh":"它讀 type 0 和 type 1 的檔案（音軌會合併）、running status、速度變化，和 SMPTE 時間單位。系統專用訊息會被略過。不是 MIDI 的檔案會丟出 `TypeError`（`fromMidiFile: not a MIDI file (no MThd)`），中途結束的檔案會丟出 `RangeError`。用 [`TakePlayer`](#TakePlayer) 把結果播進演出：它驅動作品的方式，就像有人在鍵盤上彈這個檔案。"}],"params":[{"name":"bytes","type":"Uint8Array | ArrayBuffer | Number[]","en":"The bytes of the `.mid` file, for example `await file.arrayBuffer()`.","zh":"`.mid` 檔的位元組，例如 `await file.arrayBuffer()`。"},{"name":"name","type":"String","optional":true,"en":"The name of the take. Left out, it is the track name in the file, then `'take'`.","zh":"take 的名字。沒給的話用檔案裡的音軌名稱，再沒有就是 `'take'`。"},{"name":"families","type":"'both' | 'legacy' | 'channel'","optional":true,"default":"'both'","en":"Which signal spellings to produce, as in [`signalsOfMidi()`](#signalsOfMidi). The default is both, like hardware.","zh":"要產生哪幾種訊號寫法，跟 [`signalsOfMidi()`](#signalsOfMidi) 一樣。預設兩種都有，就像硬體。"}],"returns":{"type":"Object","en":"A take. `meta.bpm` is the first tempo in the file (not set for SMPTE time).","zh":"一個 take。`meta.bpm` 是檔案裡第一個速度（SMPTE 時間的檔案不設）。"},"notes":[],"examples":[{"code":"import { toMidiFile, fromMidiFile } from '@openav/record';\n\nconst take = { v: 1, name: 'riff', durationMs: 1500, meta: { bpm: 120 }, events: [\n  { t: 500, name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } },\n  { t: 900, name: 'midi/note/off', value: { note: 60, ch: 1 } },\n] };\nconst back = fromMidiFile(toMidiFile(take));\nconsole.log(back.name, back.durationMs, back.meta.bpm, back.events.length);   // riff 1500 120 4\nconsole.log(fromMidiFile(toMidiFile(take), { families: 'legacy' }).events.map((e) => e.name));\n// ['midi/note/on', 'midi/note/off']\ntry { fromMidiFile(new Uint8Array([1, 2, 3, 4, 5])); } catch (e) { console.log(e.message); }\n// fromMidiFile: not a MIDI file (no MThd)","caption":{"en":"A round trip: the two notes come back as four events (both spellings), or two with `families: 'legacy'`. Bytes that are not a MIDI file are refused.","zh":"來回一趟：兩個音符變回四個事件（兩種寫法），用 `families: 'legacy'` 則是兩個。不是 MIDI 檔的位元組會被拒絕。"},"run":"module","lang":"js"}],"related":["toMidiFile","signalsOfMidi","TakePlayer","normalizeTake"],"keywords":{"en":["midi file","import midi","smf",".mid","load midi","parse midi file","tempo"],"zh":["midi 檔","匯入 midi","smf",".mid","載入 midi","解析 midi 檔","速度"]},"tbd":[],"claims":[]},
{"id":"layoutRects","key":"layoutRects","name":"layoutRects","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"layoutRects(layout, out, src = {}, opts = {})","import":"import { layoutRects } from '@openav/record';","source":{"file":"packages/record/layout.js","line":163,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L163"},"summary":{"en":"Works out where the work and the camera go in an output frame, in whole pixels, for one of the four layouts.","zh":"算出作品和鏡頭在輸出畫面的哪裡，單位是整數像素，支援四種版面。"},"description":[{"en":"It is pure arithmetic: no DOM, no clock. The [`Compositor`](#Compositor) calls it whenever a size changes. A 1080 by 1920 frame with a square work and `stack` splits into exactly 1080 by 1080 and 1080 by 840, with no seam row. The crop of a camera is in the **displayed** orientation, so with the mirror on, an anchor of `x: 0.2` still means \"keep the left of what you see\".","zh":"這是純算術：沒有 DOM、沒有時鐘。[`Compositor`](#Compositor) 在尺寸改變時就呼叫它。1080 乘 1920 的畫面放一個正方形的作品、用 `stack`，會剛好切成 1080 乘 1080 和 1080 乘 840，中間沒有縫。鏡頭的裁切是用**顯示出來的**方向表示的，所以開了鏡像之後，`x: 0.2` 的錨點仍然是「保留你看到的畫面的左邊」。"},{"en":"`stack` puts the work across the full width at the top and the camera below. If that would leave the camera under `minCam` of the height (a square or landscape frame), the work gets `split` of the height and is letterboxed in it. `side` is the same turned sideways. `pip` fills the frame with the work and puts the camera in a corner as a rounded inset. `work` is the work alone.","zh":"`stack` 把作品橫跨整個寬度放在上面，鏡頭在下面。如果這樣留給鏡頭的高度不到 `minCam`（正方形或橫式畫面），作品就只佔高度的 `split`，並且在裡面留黑邊。`side` 是同樣的做法轉九十度。`pip` 讓作品鋪滿畫面，把鏡頭放在角落當圓角小視窗。`work` 只有作品。"}],"params":[{"name":"layout","type":"String","en":"One of [`LAYOUTS`](#LAYOUTS). Any other name throws a `TypeError`.","zh":"[`LAYOUTS`](#LAYOUTS) 其中之一。其他名字會丟出 `TypeError`。"},{"name":"out","type":"Object","en":"The output size `{ w, h }` in pixels. It must be positive, or this throws.","zh":"輸出尺寸 `{ w, h }`，單位是像素。必須是正數，否則會丟出錯誤。"},{"name":"src","type":"Object","optional":true,"default":"{}","en":"The sizes of the sources: `{ workW, workH, camW, camH }`. A missing or zero value means unknown. An unknown work counts as square. With an unknown camera the camera's area is still reserved, with `crop: null` and `frame: null`, so the frame does not jump when the camera arrives.","zh":"來源的尺寸：`{ workW, workH, camW, camH }`。沒給或是 0 就表示不知道。不知道尺寸的作品當作正方形。鏡頭尺寸不知道時，鏡頭的區域還是會先留好，`crop: null`、`frame: null`，所以鏡頭來的時候畫面不會跳動。"},{"name":"opts","type":"Object","optional":true,"default":"{}","en":"Options: `fit` (`'contain'` default, or `'cover'`, for the work), `camFit` (`'cover'` default, or `'contain'`), `camAnchor` (`{ x, y }`, 0..1, default the middle: which part a cover-crop keeps, in what you see), `minCam` (0.2: the share of the height in `stack`, or of the width in `side`, that the camera must keep), `split` (0.6: the work's share when it would leave the camera less than `minCam`) and `pip` (`{ corner: 'br' | 'bl' | 'tr' | 'tl', size: 0.3, margin: 0.03, radius: 0.08, aspect: 0 }`: the inset's width as a share of the frame, its distance from the corner as a share of the short side, its corner radius as a share of the inset's short side, and its shape, 0 being the camera's own).","zh":"選項：`fit`（作品，預設 `'contain'`，或 `'cover'`）、`camFit`（預設 `'cover'`，或 `'contain'`）、`camAnchor`（`{ x, y }`，0..1，預設在中間：滿版裁切時保留哪一部分，是你看到的畫面裡的位置）、`minCam`（0.2：`stack` 裡鏡頭至少要留的高度比例，`side` 裡是寬度比例）、`split`（0.6：留給鏡頭的比 `minCam` 還少時，作品佔的比例）、`pip`（`{ corner: 'br' | 'bl' | 'tr' | 'tl', size: 0.3, margin: 0.03, radius: 0.08, aspect: 0 }`：小視窗寬度佔畫面的比例、離角落的距離佔短邊的比例、圓角半徑佔小視窗短邊的比例，以及形狀，0 表示用鏡頭自己的比例）。"}],"returns":{"type":"Object","en":"`{ layout, out, work, cam }`. `work` and `cam` are `{ x, y, w, h, crop, frame, box }` in output pixels (all whole numbers), and `cam` also has `radius`. `crop` is the part of the source that is drawn (source pixels, may be fractional) or `null` for the whole source. `frame` is where the whole source frame would land, cropped parts falling outside the rect, which is what overlays need. `box` is the area it was placed in. `cam` is `null` for the `work` layout.","zh":"`{ layout, out, work, cam }`。`work` 和 `cam` 是輸出像素的 `{ x, y, w, h, crop, frame, box }`（全是整數），`cam` 還多一個 `radius`。`crop` 是來源被畫出來的那一塊（來源像素，可以有小數），整張都畫就是 `null`。`frame` 是整個來源畫面會落在哪裡，被裁掉的部分在矩形外面，疊圖需要它。`box` 是它被放進的區域。`work` 版面的 `cam` 是 `null`。"},"notes":[],"examples":[{"code":"import { layoutRects } from '@openav/record';\n\nconst r = layoutRects('stack', { w: 1080, h: 1920 }, { workW: 1000, workH: 1000, camW: 1920, camH: 1080 });\nconsole.log(r.work.x, r.work.y, r.work.w, r.work.h);   // 0 0 1080 1080\nconsole.log(r.cam.x, r.cam.y, r.cam.w, r.cam.h);       // 0 1080 1080 840\nconsole.log(r.cam.crop);                                 // the middle of the 16:9 camera that fills 1080x840","caption":{"en":"A square work and a 16:9 camera in a vertical frame: the work is the top 1080 pixels, the camera fills the 840 below it with a middle crop.","zh":"直式畫面裡放正方形的作品和 16:9 的鏡頭：作品是上面 1080 像素，鏡頭用中間的裁切填滿它下面的 840。"},"run":"module","lang":"js"},{"code":"import { layoutRects } from '@openav/record';\n\nconst pip = layoutRects('pip', { w: 1920, h: 1080 }, { workW: 1920, workH: 1080, camW: 1280, camH: 720 },\n  { pip: { corner: 'tl', size: 0.25 } });\nconsole.log(pip.cam.x, pip.cam.y, pip.cam.w, pip.cam.h, pip.cam.radius);   // 32 32 480 270 22\n\nconst noCam = layoutRects('stack', { w: 1080, h: 1920 }, { workW: 1000, workH: 1000 });\nconsole.log(noCam.cam.h, noCam.cam.crop, noCam.cam.frame);                  // 840 null null: the area is reserved\ntry { layoutRects('grid', { w: 10, h: 10 }); } catch (e) { console.log(e.message); }\n// layoutRects: unknown layout \"grid\" (stack, pip, side, work)","caption":{"en":"A picture-in-picture in the top-left corner, the reserved camera area before a camera exists, and the error for a layout that does not exist.","zh":"左上角的子母畫面、鏡頭還不存在時先留好的鏡頭區域，以及不存在的版面會得到的錯誤。"},"run":"module","lang":"js"}],"related":["LAYOUTS","framePoint","Compositor","resolveSize"],"keywords":{"en":["layout","rects","crop","cover","contain","letterbox","pip","stack","side","geometry"],"zh":["版面","矩形","裁切","滿版","黑邊","子母畫面","幾何","畫面","鏡頭區域"]},"tbd":[],"claims":[]},
{"id":"LAYOUTS","key":"LAYOUTS","name":"LAYOUTS","aliases":[],"kind":"constant","package":"@openav/record","category":"record","documented":true,"signature":"LAYOUTS = array, 4 items","import":"import { LAYOUTS } from '@openav/record';","source":{"file":"packages/record/layout.js","line":14,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L14"},"summary":{"en":"The four layout names, in picker order: `'stack'`, `'pip'`, `'side'`, `'work'`.","zh":"四種版面的名字，照選單的順序：`'stack'`、`'pip'`、`'side'`、`'work'`。"},"description":[{"en":"`stack`: the work across the full width at the top, the camera fills the rest below (for vertical video: Reels, Shorts, TikTok). `pip`: the work fills the frame and the camera is a rounded inset in a corner (for square frames, or a work that needs all of the frame). `side`: the work at full height on the left, the camera fills the right (landscape). `work`: the work alone. [`layoutRects()`](#layoutRects) says exactly where everything lands, and the `layout` option of the [`Compositor`](#Compositor) takes one of these names; any other name throws.","zh":"`stack`：作品橫跨整個寬度放在上面，鏡頭填滿下面剩下的（給直式影片：Reels、Shorts、TikTok）。`pip`：作品鋪滿畫面，鏡頭是角落的圓角小視窗（給正方形畫面，或需要整個畫面的作品）。`side`：作品佔滿左邊的整個高度，鏡頭填滿右邊（橫式）。`work`：只有作品。確切的位置由 [`layoutRects()`](#layoutRects) 算出，[`Compositor`](#Compositor) 的 `layout` 選項收的就是這幾個名字，其他名字會丟出錯誤。"}],"value":{"type":"String[]","en":"`['stack', 'pip', 'side', 'work']`, a frozen-by-convention list (do not change it).","zh":"`['stack', 'pip', 'side', 'work']`，一份約定不要修改的清單。"},"params":[],"notes":[],"examples":[{"code":"import { LAYOUTS, layoutRects } from '@openav/record';\n\nconsole.log(LAYOUTS.join(' '));                                    // stack pip side work\nfor (const name of LAYOUTS) {\n  const r = layoutRects(name, { w: 1080, h: 1920 }, { workW: 1000, workH: 1000, camW: 1920, camH: 1080 });\n  console.log(name, r.work.w + 'x' + r.work.h, r.cam ? r.cam.w + 'x' + r.cam.h : 'no camera');\n}\n// stack 1080x1080 1080x840\n// pip 1080x1080 324x182\n// side 648x648 432x1920   (a vertical frame is too narrow for side: the work gets 60% of the width)\n// work 1080x1080 no camera","caption":{"en":"Runs the four layouts on one vertical frame with a square work and a 16:9 camera, and logs the size of each area.","zh":"在同一個直式畫面上，用正方形的作品和 16:9 的鏡頭跑過四種版面，印出每個區域的大小。"},"run":"module","lang":"js"}],"related":["layoutRects","Compositor","PRESETS"],"keywords":{"en":["layout","stack","pip","side","picture in picture","work","arrangement","camera position"],"zh":["版面","堆疊","子母畫面","並排","鏡頭位置","排版"]},"tbd":[],"claims":[]},
{"id":"listCameras","key":"listCameras","name":"listCameras","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"listCameras()","import":"import { listCameras } from '@openav/record';","source":{"file":"packages/record/camera.js","line":36,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/camera.js#L36"},"summary":{"en":"Lists the video inputs of this computer as { deviceId, groupId, label }.","zh":"列出這台電腦的影像輸入，格式是 { deviceId, groupId, label }。"},"description":[{"en":"Browsers hide the labels (and sometimes the ids) until the page has had camera permission once, so call it again after [`openCamera()`](#openCamera) to get the real names. Needs `https` or `localhost`: elsewhere there is no `navigator.mediaDevices` and the promise rejects.","zh":"瀏覽器在頁面拿到過一次鏡頭權限之前，會隱藏名字（有時連 id 也是），所以在 [`openCamera()`](#openCamera) 之後再呼叫一次，才會拿到真正的名字。需要 `https` 或 `localhost`：其他地方沒有 `navigator.mediaDevices`，promise 會被 reject。"}],"params":[],"returns":{"type":"Promise<Object[]>","en":"A promise of a list of `{ deviceId, groupId, label }`. A device without a label is called `Camera 1`, `Camera 2`… by its position.","zh":"一個 promise，內容是 `{ deviceId, groupId, label }` 的清單。沒有名字的裝置會依位置叫作 `Camera 1`、`Camera 2`…。"},"notes":[],"examples":[{"code":"import { listCameras, openCamera } from '@openav/record';\n\nconsole.log((await listCameras()).length > 0);        // true; the labels are generic until permission is given\nconst stream = await openCamera();                      // the browser asks for the camera\nconst cams = await listCameras();                       // now with their real names\nconsole.log(cams.map((c) => c.label).join(', '));\nstream.getTracks().forEach((t) => t.stop());","caption":{"en":"Lists the cameras before and after permission is given; the second list has the real names. Stop the tracks when you are done.","zh":"在拿到權限之前和之後各列一次鏡頭。第二次的清單有真正的名字。用完之後要把音軌停掉。"},"run":"module","lang":"js"}],"related":["listMicrophones","openCamera","cameraConstraints"],"keywords":{"en":["cameras","webcam list","enumerate devices","video inputs","device id","camera picker"],"zh":["鏡頭清單","網路攝影機","列出裝置","影像輸入","裝置 id","選鏡頭"]},"tbd":[],"claims":[]},
{"id":"listMicrophones","key":"listMicrophones","name":"listMicrophones","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"listMicrophones()","import":"import { listMicrophones } from '@openav/record';","source":{"file":"packages/record/camera.js","line":39,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/camera.js#L39"},"summary":{"en":"Lists the audio inputs of this computer as { deviceId, groupId, label }.","zh":"列出這台電腦的音訊輸入，格式是 { deviceId, groupId, label }。"},"description":[{"en":"The same as [`listCameras()`](#listCameras) for audio, with the same rule about labels: they are generic until the page has had microphone permission once. Pass a `deviceId` to [`AudioTap.addMicrophone()`](#AudioTap) to use one.","zh":"跟 [`listCameras()`](#listCameras) 一樣，只是列的是音訊，名字的規則也一樣：頁面拿到過一次麥克風權限之前，名字都是通用的。把 `deviceId` 交給 [`AudioTap.addMicrophone()`](#AudioTap) 就能使用其中一個。"}],"params":[],"returns":{"type":"Promise<Object[]>","en":"A promise of a list of `{ deviceId, groupId, label }`. A device without a label is called `Microphone 1`, `Microphone 2`… by its position.","zh":"一個 promise，內容是 `{ deviceId, groupId, label }` 的清單。沒有名字的裝置會依位置叫作 `Microphone 1`、`Microphone 2`…。"},"notes":[],"examples":[{"code":"import { listMicrophones } from '@openav/record';\n\nconst mics = await listMicrophones();\nconsole.log(mics.length > 0, Object.keys(mics[0]).join(','));   // true deviceId,groupId,label","caption":{"en":"Lists the audio inputs and shows the shape of one item.","zh":"列出音訊輸入，並顯示其中一項的形狀。"},"run":"module","lang":"js"}],"related":["listCameras","AudioTap","openCamera"],"keywords":{"en":["microphones","audio inputs","enumerate devices","mic list","device id","microphone picker"],"zh":["麥克風清單","音訊輸入","列出裝置","裝置 id","選麥克風"]},"tbd":[],"claims":[]},
{"id":"midiEventsOf","key":"midiEventsOf","name":"midiEventsOf","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"midiEventsOf(take)","import":"import { midiEventsOf } from '@openav/record';","source":{"file":"packages/record/take.js","line":144,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L144"},"summary":{"en":"Reads a take as plain MIDI events (note on and off, controllers, pitch bend), picking one naming family so nothing is counted twice.","zh":"把一個 take 讀成單純的 MIDI 事件（音符開關、控制器、彎音），只選一種名稱寫法，所以不會重複算。"},"description":[{"en":"A take made on hardware holds the same note several times, under several names. This picks one family for the notes: `midi/note/*` if present, else `midi/ch/*`, else one device's `midi/<device>/note/*`, else raw `midi/virtual` bytes. Controllers and pitch bend come from `midi/ch/*` if present, else the classic names (as channel 1), else one device's. [`toMidiFile()`](#toMidiFile) and [`takeStats()`](#takeStats) are built on it.","zh":"在硬體上錄的 take，同一個音會用好幾個名字記好幾次。這裡替音符選一種寫法：有 `midi/note/*` 就用它，沒有就用 `midi/ch/*`，再沒有就用某一個裝置的 `midi/<device>/note/*`，最後是 `midi/virtual` 的原始位元組。控制器和彎音：有 `midi/ch/*` 就用，沒有就用傳統名稱（當作頻道 1），再沒有就用某一個裝置的。[`toMidiFile()`](#toMidiFile) 和 [`takeStats()`](#takeStats) 都是建立在它上面。"}],"params":[{"name":"take","type":"Object","en":"The take.","zh":"這個 take。"}],"returns":{"type":"Object[]","en":"A list in the shape of `@openav/midi`: `{ t, type: 'noteon', ch, note, vel }`, `noteoff`, `{ t, type: 'cc', ch, cc, value }`, `{ t, type: 'pitchbend', ch, value }` (0..16383), `chanat` and `polyat`. `vel` and `value` are 0..127.","zh":"`@openav/midi` 的格式：`{ t, type: 'noteon', ch, note, vel }`、`noteoff`、`{ t, type: 'cc', ch, cc, value }`、`{ t, type: 'pitchbend', ch, value }`（0..16383）、`chanat`、`polyat`。`vel` 和 `value` 是 0..127。"},"notes":[],"examples":[{"code":"import { midiEventsOf } from '@openav/record';\n\nconst take = { v: 1, name: 'x', durationMs: 600, events: [\n  { t: 100, name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } },\n  { t: 100, name: 'midi/ch/1/note/60', value: 0.8 },        // the same key, spelled the other way\n  { t: 400, name: 'midi/note/off', value: { note: 60, ch: 1 } },\n  { t: 400, name: 'midi/ch/1/note/60', value: 0 },\n] };\nconsole.log(midiEventsOf(take));\n// [{ t: 100, type: 'noteon', ch: 1, note: 60, vel: 102 }, { t: 400, type: 'noteoff', ch: 1, note: 60, vel: 0 }]","caption":{"en":"Four recorded events are one key pressed and released, spelled twice; the result has just the two MIDI events.","zh":"四個錄下來的事件其實是一個鍵按下又放開，寫了兩次。結果只有兩個 MIDI 事件。"},"run":"module","lang":"js"}],"related":["toMidiFile","takeStats","noteOf","signalsOfMidi"],"keywords":{"en":["midi events","note on","cc","pitch bend","convert take","families","deduplicate"],"zh":["midi 事件","音符開","控制器","彎音","轉換 take","寫法","去重複"]},"tbd":[],"claims":[]},
{"id":"mixTracks","key":"mixTracks","name":"mixTracks","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"mixTracks(tracks)","import":"import { mixTracks } from '@openav/record';","source":{"file":"packages/record/recorder.js","line":23,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/recorder.js#L23"},"summary":{"en":"Mixes several audio tracks into one, because Chrome records only the first audio track of a stream.","zh":"把好幾條音軌混成一條，因為 Chrome 只會錄一個串流的第一條音軌。"},"description":[{"en":"A [`Recorder`](#Recorder) calls this itself when you give it more than one live audio track, and closes the mix on `stop()`. Call it yourself when you want the one track for something else. It makes an `AudioContext`, so the page needs to have had a click. For several sounds that come from Web Audio nodes, [`AudioTap`](#AudioTap) is the better tool: it mixes them without a detour through streams.","zh":"你給 [`Recorder`](#Recorder) 超過一條活著的音軌時，它會自己呼叫這個，並在 `stop()` 時關掉混音。想把那一條音軌拿去做別的事時，才自己呼叫。它會建一個 `AudioContext`，所以頁面需要先被點過一次。如果聲音是來自 Web Audio 節點，用 [`AudioTap`](#AudioTap) 比較好：它直接混，不用繞一圈串流。"}],"params":[{"name":"tracks","type":"MediaStreamTrack[]","en":"The audio tracks to mix.","zh":"要混的音軌。"}],"returns":{"type":"Object","en":"`{ track, close() }`: the one mixed `MediaStreamTrack`, and a function that closes the `AudioContext` that mixes it.","zh":"`{ track, close() }`：混好的那一條 `MediaStreamTrack`，以及關掉混音用的 `AudioContext` 的函式。"},"notes":[],"examples":[{"code":"import { AudioTap, mixTracks } from '@openav/record';\n\nconst tap = new AudioTap();\nconst mix = mixTracks([tap.tracks[0], tap.workTracks[0]]);   // two tracks in, one comes out\nconsole.log(mix.track.kind, mix.track.readyState);            // audio live\nmix.close();                                                  // closes the mixing context\ntap.dispose();","caption":{"en":"Two audio tracks go in and one live audio track comes out; `close()` shuts the mixer down.","zh":"兩條音軌進去，出來一條活的音軌。`close()` 把混音器關掉。"},"run":"module","lang":"js"}],"related":["Recorder","AudioTap"],"keywords":{"en":["mix","merge","audio tracks","combine","chrome first track","mediastream"],"zh":["混音","合併","音軌","合成一條","mediastream"]},"tbd":[],"claims":[]},
{"id":"normalizeTake","key":"normalizeTake","name":"normalizeTake","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"normalizeTake(input)","import":"import { normalizeTake } from '@openav/record';","source":{"file":"packages/record/take.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L30"},"summary":{"en":"Turns anything take-shaped, or its JSON text, into a valid take: events sorted by time, bad events dropped, nothing negative.","zh":"把任何長得像 take 的東西，或它的 JSON 文字，整理成合格的 take：事件依時間排序、壞事件丟掉、沒有負數。"},"description":[{"en":"A take is what an [`EventLog`](#EventLog) filtered to `midi/*` records, as plain data: `t` is milliseconds from the start, `name` is a signal name such as `midi/note/on`, `value` is its payload, and `meta` holds free fields like `{ bpm, device, notes }`. `normalizeTake()` makes any input safe: events that are not objects, have no name or have a `t` that is not a number are dropped; `t` is rounded to a tenth of a millisecond and held at 0 or more; the events are sorted by `t` (events with equal times keep their order); `name` defaults to `'take'`; `durationMs` is at least the time of the last event. A new object is returned.","zh":"take 就是過濾成 `midi/*` 的 [`EventLog`](#EventLog) 記下的東西，是單純的資料：`t` 是從開始算起的毫秒數，`name` 是訊號名稱，像 `midi/note/on`，`value` 是它的內容，`meta` 放自由欄位，像 `{ bpm, device, notes }`。`normalizeTake()` 讓任何輸入都變安全：不是物件、沒有名字、`t` 不是數字的事件會被丟掉，`t` 捨入到十分之一毫秒並限制在 0 以上，事件依 `t` 排序（時間一樣的保持原來的順序），`name` 預設是 `'take'`，`durationMs` 至少是最後一個事件的時間。回傳的是新物件。"},{"en":"Every function in this package that takes a take runs it through here first, so you can pass raw data.","zh":"這個套件裡每個收 take 的函式，都會先把它送進這裡處理，所以你可以直接傳原始資料。"}],"params":[{"name":"input","type":"Object | String","en":"A take, or its JSON text. A string is parsed with `JSON.parse`. Anything that is not an object throws a `TypeError`.","zh":"一個 take，或它的 JSON 文字。字串會用 `JSON.parse` 解析。不是物件的東西會丟出 `TypeError`。"}],"returns":{"type":"Object","en":"A new take: `{ v: 1, name, createdAt, durationMs, events: [{ t, name, value }], meta }`.","zh":"一個新的 take：`{ v: 1, name, createdAt, durationMs, events: [{ t, name, value }], meta }`。"},"notes":[],"examples":[{"code":"import { normalizeTake } from '@openav/record';\n\nconst take = normalizeTake({\n  events: [{ t: 30, name: 'b' }, { t: 10, name: 'a', value: 1 }, { t: 'x', name: 'c' }, { name: 'd' }, null],\n  durationMs: 5,\n});\nconsole.log(take.events.map((e) => e.t + ':' + e.name).join(' '));   // 10:a 30:b\nconsole.log(take.durationMs, take.name, take.v);                     // 30 take 1","caption":{"en":"Three of the five events are junk and go; the rest are sorted, and the length grows to cover the last event.","zh":"五個事件裡三個是垃圾，會被丟掉。剩下的排好序，長度也被拉長到涵蓋最後一個事件。"},"run":"module","lang":"js"}],"related":["validateTake","TAKE_VERSION","trimSilence","EventLog","TakePlayer"],"keywords":{"en":["take","normalize","sanitize","sort events","midi take","json","clean up"],"zh":["take","正規化","整理","排序事件","midi take","json","清理"]},"tbd":[],"claims":[]},
{"id":"noteOf","key":"noteOf","name":"noteOf","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"noteOf(ev)","import":"import { noteOf } from '@openav/record';","source":{"file":"packages/record/take.js","line":99,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L99"},"summary":{"en":"Tells whether a recorded event is a key going down or up, whatever its spelling, and which note on which channel.","zh":"告訴你一個錄下來的事件是不是鍵按下去或放開，不管它用哪一種寫法，以及是哪個頻道的哪個音。"},"description":[{"en":"A hardware keyboard publishes the same note under several names, and this function knows them all: `midi/note/on` and `midi/note/off` (family `legacy`, with `value: { note, vel, ch }`), `midi/ch/<ch>/note/<note>` (family `channel`, a number, 0 for off), `midi/<device>/note/on|off` (family `slug:<device>`) and raw bytes on `midi/virtual` (family `virtual`, `{ data: [status, note, vel] }`). A key-down with velocity 0 in raw bytes counts as off. A channel of 0, as the on-screen piano says, counts as channel 1.","zh":"硬體鍵盤會把同一個音符用好幾種名字發布，這個函式全部認得：`midi/note/on` 和 `midi/note/off`（寫法 `legacy`，內容是 `{ note, vel, ch }`）、`midi/ch/<ch>/note/<note>`（寫法 `channel`，是數字，0 表示放開）、`midi/<device>/note/on|off`（寫法 `slug:<device>`），以及 `midi/virtual` 上的原始位元組（寫法 `virtual`，`{ data: [status, note, vel] }`）。原始位元組裡力度是 0 的按下算放開。頻道是 0 的（螢幕鋼琴會這樣）當作頻道 1。"}],"params":[{"name":"ev","type":"Object","en":"An event `{ name, value }` of a take.","zh":"take 的一個事件 `{ name, value }`。"}],"returns":{"type":"Object | null","en":"`{ key, on, note, ch, family, vel }`, or `null` if it is not a note event. `key` is `'<ch>:<note>'`, `on` is true for a key-down, `family` says which spelling it came in, `vel` is 0..127.","zh":"`{ key, on, note, ch, family, vel }`，不是音符事件就是 `null`。`key` 是 `'<ch>:<note>'`，`on` 是 true 表示按下，`family` 說明它用哪一種寫法，`vel` 是 0..127。"},"notes":[],"examples":[{"code":"import { noteOf } from '@openav/record';\n\nconsole.log(noteOf({ name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } }));\n// { key: '1:60', on: true, note: 60, ch: 1, family: 'legacy', vel: 102 }\nconsole.log(noteOf({ name: 'midi/ch/1/note/60', value: 0 }).on);                   // false\nconsole.log(noteOf({ name: 'midi/virtual', value: { data: [0x90, 60, 100] } }).on);  // true\nconsole.log(noteOf({ name: 'midi/cc/1', value: 1 }));                                // null","caption":{"en":"The same key in three spellings, and a controller that is not a note.","zh":"同一個鍵的三種寫法，和一個不是音符的控制器。"},"run":"module","lang":"js"}],"related":["noteOffFor","midiEventsOf","TakePlayer","TakeRecorder"],"keywords":{"en":["note on","note off","midi event","key down","parse note","families","spellings"],"zh":["音符開","音符關","midi 事件","按下","解析音符","寫法"]},"tbd":[],"claims":[]},
{"id":"noteOffFor","key":"noteOffFor","name":"noteOffFor","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"noteOffFor(n)","import":"import { noteOffFor } from '@openav/record';","source":{"file":"packages/record/take.js","line":126,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L126"},"summary":{"en":"Gives the events that release a sounding note, in the same spelling it was played in.","zh":"給出能放開一個正在響的音的事件，用的是它當初被彈出來的那種寫法。"},"description":[{"en":"This is how a [`TakePlayer`](#TakePlayer) and a [`TakeRecorder`](#TakeRecorder) make sure no note is left sounding: for `legacy` it is `midi/note/off`, for `channel` it is `midi/ch/<ch>/note/<note>` with 0, for `virtual` it is a raw note-off byte triple, for a device family `midi/<device>/note/off`.","zh":"[`TakePlayer`](#TakePlayer) 和 [`TakeRecorder`](#TakeRecorder) 就是這樣確保沒有音一直響著：`legacy` 是 `midi/note/off`，`channel` 是值為 0 的 `midi/ch/<ch>/note/<note>`，`virtual` 是原始的放開位元組，裝置寫法則是 `midi/<device>/note/off`。"}],"params":[{"name":"n","type":"Object","en":"The result of [`noteOf()`](#noteOf): `{ note, ch, family }`.","zh":"[`noteOf()`](#noteOf) 的結果：`{ note, ch, family }`。"}],"returns":{"type":"Object[]","en":"A list of `{ name, value }` events (one, or none for an unknown family).","zh":"`{ name, value }` 事件的清單（一個，不認得的寫法則是空的）。"},"notes":[],"examples":[{"code":"import { noteOf, noteOffFor } from '@openav/record';\n\nconst down = noteOf({ name: 'midi/ch/3/note/60', value: 0.5 });\nconsole.log(noteOffFor(down));      // [{ name: 'midi/ch/3/note/60', value: 0 }]\nconst legacy = noteOf({ name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } });\nconsole.log(noteOffFor(legacy));    // [{ name: 'midi/note/off', value: { note: 60, ch: 1 } }]","caption":{"en":"The release of the same key depends on how it was played: the per-channel spelling releases with a zero, the classic one with a note-off.","zh":"同一個鍵怎麼放開要看它當初怎麼彈：每頻道的寫法用 0 放開，傳統的用 note-off。"},"run":"module","lang":"js"}],"related":["noteOf","TakePlayer","TakeRecorder"],"keywords":{"en":["note off","release","stuck notes","panic","matching off event"],"zh":["音符關","放開","卡音","對應的放開事件"]},"tbd":[],"claims":[]},
{"id":"openCamera","key":"openCamera","name":"openCamera","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"openCamera(opts = {})","import":"import { openCamera } from '@openav/record';","source":{"file":"packages/record/camera.js","line":47,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/camera.js#L47"},"summary":{"en":"Opens a camera and returns its MediaStream: ideally 1920 by 1080 at 30 fps, or whatever the camera can do.","zh":"開啟鏡頭並回傳它的 MediaStream：理想是 1920 乘 1080、30 fps，不行就用鏡頭做得到的。"},"description":[{"en":"Give the stream to [`Compositor.setCamera()`](#Compositor). The caller owns the stream: stop its tracks when you are done, the compositor never does. Webcams usually top out at 1080p30, so a 4K frame enlarges the camera; the work above it stays sharp. It needs `https` or `localhost`, and the user's permission.","zh":"把串流交給 [`Compositor.setCamera()`](#Compositor)。串流由呼叫的人擁有：用完要自己把音軌停掉，合成器不會做。網路攝影機通常最多到 1080p30，所以 4K 的畫面會把鏡頭放大，上面的作品仍然清楚。需要 `https` 或 `localhost`，以及使用者許可。"}],"params":[{"name":"opts","type":"Object","optional":true,"default":"{}","en":"The options of [`cameraConstraints()`](#cameraConstraints): `{ deviceId, width, height, fps, facingMode }`.","zh":"[`cameraConstraints()`](#cameraConstraints) 的選項：`{ deviceId, width, height, fps, facingMode }`。"}],"returns":{"type":"Promise<MediaStream>","en":"A promise of the stream, with one video track and no audio. It rejects with the browser's error when permission is refused or the camera is missing.","zh":"串流的 promise，有一條影像軌，沒有聲音。權限被拒或沒有鏡頭時會用瀏覽器的錯誤 reject。"},"notes":[],"examples":[{"code":"import { openCamera } from '@openav/record';\n\nconst stream = await openCamera();                // the browser asks for the camera\nconst video = document.createElement('video');\nvideo.muted = true; video.srcObject = stream; await video.play();\nconsole.log(video.videoWidth > 0);                // true once frames arrive\ndocument.getElementById('app').append(video);\nvideo.style.width = '240px';","caption":{"en":"Allow the camera and a small live picture of it appears on the page.","zh":"允許鏡頭之後，頁面上會出現一個小小的即時畫面。"},"run":"module","lang":"js"}],"related":["Compositor","cameraConstraints","listCameras"],"keywords":{"en":["camera","webcam","open camera","getUserMedia","stream","video input","permission"],"zh":["鏡頭","網路攝影機","開啟鏡頭","串流","影像輸入","權限"]},"tbd":[],"claims":[]},
{"id":"pickMime","key":"pickMime","name":"pickMime","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"pickMime(isTypeSupported, { audioOnly = false, prefer = null, candidates = null } = {})","import":"import { pickMime } from '@openav/record';","source":{"file":"packages/record/mime.js","line":42,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/mime.js#L42"},"summary":{"en":"Picks the first MIME type from a list that the browser can record, using the browser's own check.","zh":"用瀏覽器自己的檢查，從清單裡挑出第一個瀏覽器錄得了的 MIME 型別。"},"description":[{"en":"A [`Recorder`](#Recorder) calls this for you. It takes the browser's check as an argument so that it is pure and can be tried with a fake one, like the examples below.","zh":"[`Recorder`](#Recorder) 會替你呼叫它。它把瀏覽器的檢查當成參數傳進來，所以是純函式，可以像下面的範例那樣用假的檢查來試。"}],"params":[{"name":"isTypeSupported","type":"Function","en":"The browser's check, for example `(m) => MediaRecorder.isTypeSupported(m)`. Not a function gives `''`. A check that throws counts as \"no\" for that type.","zh":"瀏覽器的檢查函式，例如 `(m) => MediaRecorder.isTypeSupported(m)`。給的不是函式就回傳 `''`。丟出錯誤的檢查，對那個型別算「不行」。"},{"name":"audioOnly","type":"Boolean","optional":true,"default":"false","en":"Use [`AUDIO_MIMES`](#AUDIO_MIMES) instead of [`VIDEO_MIMES`](#VIDEO_MIMES).","zh":"用 [`AUDIO_MIMES`](#AUDIO_MIMES) 而不是 [`VIDEO_MIMES`](#VIDEO_MIMES)。"},{"name":"prefer","type":"'mp4' | 'webm'","optional":true,"default":"null","en":"Move this container to the front, keeping the order inside each group.","zh":"把這種容器移到最前面，兩組各自的內部順序不變。"},{"name":"candidates","type":"String[]","optional":true,"default":"null","en":"Your own list instead of the built-in one.","zh":"用你自己的清單，取代內建的。"}],"returns":{"type":"String","en":"The first supported type, or `''` when none matches (then let `MediaRecorder` choose its default).","zh":"第一個支援的型別，都不符合就回傳 `''`（這時就讓 `MediaRecorder` 用它的預設）。"},"notes":[],"examples":[{"code":"import { pickMime } from '@openav/record';\n\nconst pretend = (...ok) => (mime) => ok.includes(mime);      // a fake browser that supports only these\nconst both = pretend('video/webm;codecs=vp9,opus', 'video/mp4;codecs=avc1.640033,mp4a.40.2');\nconsole.log(pickMime(both));                                  // video/mp4;codecs=avc1.640033,mp4a.40.2\nconsole.log(pickMime(both, { prefer: 'webm' }));              // video/webm;codecs=vp9,opus\nconsole.log(pickMime(pretend('audio/webm;codecs=opus'), { audioOnly: true }));   // audio/webm;codecs=opus\nconsole.log(JSON.stringify(pickMime(() => false)));           // \"\"","caption":{"en":"Four fake browsers: one that has both, the same asked to prefer WebM, one that only records Opus, and one that records nothing.","zh":"四個假的瀏覽器：兩種都支援的、同一個但要求優先 WebM、只錄得了 Opus 的，和什麼都錄不了的。"},"run":"module","lang":"js"}],"related":["VIDEO_MIMES","AUDIO_MIMES","extFor","Recorder"],"keywords":{"en":["mime","supported","isTypeSupported","codec","choose format","mediarecorder"],"zh":["mime","支援","isTypeSupported","編碼","挑格式","mediarecorder"]},"tbd":[],"claims":[]},
{"id":"presetById","key":"presetById","name":"presetById","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"presetById(id)","import":"import { presetById } from '@openav/record';","source":{"file":"packages/record/layout.js","line":69,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L69"},"summary":{"en":"Finds one size preset by its id: `presetById('vertical-4k')`.","zh":"用 id 找出一個尺寸預設：`presetById('vertical-4k')`。"},"description":[{"en":"Use it to fill a size picker with [`PRESETS`](#PRESETS), or to read the suggested frame rate and layout of a size before you build a [`Compositor`](#Compositor).","zh":"拿來為尺寸選單填上 [`PRESETS`](#PRESETS) 的內容，或在建 [`Compositor`](#Compositor) 之前讀一個尺寸建議的畫格率和版面。"}],"params":[{"name":"id","type":"String","en":"The preset id, for example `'square-1080'`.","zh":"預設的 id，例如 `'square-1080'`。"}],"returns":{"type":"Object | null","en":"The preset `{ id, group, w, h, label, labelZh, layout, fps }`, or `null` if there is no such id.","zh":"那個預設 `{ id, group, w, h, label, labelZh, layout, fps }`，沒有這個 id 就是 `null`。"},"notes":[],"examples":[{"code":"import { presetById } from '@openav/record';\n\nconst p = presetById('vertical-4k');\nconsole.log(p.w, p.h, p.fps, p.layout);   // 2160 3840 30 stack\nconsole.log(presetById('nope'));           // null","caption":{"en":"Logs the numbers of one preset and shows that an unknown id gives `null`.","zh":"印出一個預設的各個數字，並顯示不認識的 id 會得到 `null`。"},"run":"module","lang":"js"}],"related":["PRESETS","resolveSize"],"keywords":{"en":["preset","lookup","size by id","find size","output size"],"zh":["預設","查詢","用 id 找尺寸","輸出尺寸"]},"tbd":[],"claims":[]},
{"id":"PRESETS","key":"PRESETS","name":"PRESETS","aliases":[],"kind":"constant","package":"@openav/record","category":"record","documented":true,"signature":"PRESETS = array, 11 items","import":"import { PRESETS } from '@openav/record';","source":{"file":"packages/record/layout.js","line":54,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L54"},"summary":{"en":"The output sizes the compositor knows by name, from `vertical-1080p` (1080 by 1920) to `landscape-4k`, each with its suggested layout and frame rate.","zh":"合成器認得名字的輸出尺寸，從 `vertical-1080p`（1080 乘 1920）到 `landscape-4k`，每個都附建議的版面和畫格率。"},"description":[{"en":"```text\nid                 size        fps   layout\nvertical-1080p     1080x1920   60    stack\nvertical-4x5       1440x1800   60    stack\nvertical-2.7k      1520x2704   30    stack\nvertical-4k        2160x3840   30    stack\nlandscape-1080p    1920x1080   60    side\nlandscape-16x10    1920x1200   60    side\nlandscape-2.7k     2704x1520   30    side\nlandscape-4k       3840x2160   30    side\nsquare-1080        1080x1080   60    pip\nsquare-2048        2048x2048   30    pip\nsquare-2160        2160x2160   30    pip\n```","zh":"```text\nid                 尺寸        fps   版面\nvertical-1080p     1080x1920   60    stack\nvertical-4x5       1440x1800   60    stack\nvertical-2.7k      1520x2704   30    stack\nvertical-4k        2160x3840   30    stack\nlandscape-1080p    1920x1080   60    side\nlandscape-16x10    1920x1200   60    side\nlandscape-2.7k     2704x1520   30    side\nlandscape-4k       3840x2160   30    side\nsquare-1080        1080x1080   60    pip\nsquare-2048        2048x2048   30    pip\nsquare-2160        2160x2160   30    pip\n```"},{"en":"Every side is even, because H.264 and VP9 encode 4:2:0 colour, which needs even sizes. Use an id as the `size` of a [`Compositor`](#Compositor); [`presetById()`](#presetById) looks one up.","zh":"每一邊都是偶數，因為 H.264 和 VP9 用 4:2:0 的色彩編碼，需要偶數的尺寸。可以把 id 當作 [`Compositor`](#Compositor) 的 `size`。用 [`presetById()`](#presetById) 查某一個。"}],"value":{"type":"Object[]","en":"A frozen list of 11 presets. Each is `{ id, group, w, h, label, labelZh, layout, fps }`: `group` is `'vertical'`, `'landscape'` or `'square'`, `label` and `labelZh` are names for a picker, `layout` is the layout suggested for the shape and `fps` is [`recommendedFps()`](#recommendedFps).","zh":"一份凍結的清單，有 11 個預設。每個是 `{ id, group, w, h, label, labelZh, layout, fps }`：`group` 是 `'vertical'`、`'landscape'` 或 `'square'`，`label` 和 `labelZh` 是給選單用的名字，`layout` 是這種形狀建議的版面，`fps` 是 [`recommendedFps()`](#recommendedFps)。"},"params":[],"notes":[],"examples":[{"code":"import { PRESETS } from '@openav/record';\n\nconsole.log(PRESETS.length, PRESETS[0].id);                      // 11 vertical-1080p\nconst big = PRESETS.filter((p) => p.w * p.h > 2.6e6).map((p) => p.id + ' @' + p.fps);\nconsole.log(big.join(', '));\n// vertical-2.7k @30, vertical-4k @30, landscape-2.7k @30, landscape-4k @30, square-2048 @30, square-2160 @30","caption":{"en":"Lists the presets that are bigger than about 2.6 megapixels: all of them are recorded at 30 frames a second.","zh":"列出大於約 260 萬像素的預設：它們全都用每秒 30 個畫面來錄。"},"run":"module","lang":"js"}],"related":["presetById","resolveSize","recommendedFps","Compositor"],"keywords":{"en":["sizes","presets","1080p","4k","vertical","landscape","square","resolution","9:16","reels"],"zh":["尺寸","預設","1080p","4k","直式","橫式","方形","解析度","9:16","輸出尺寸"]},"tbd":[],"claims":[]},
{"id":"quantizeTake","key":"quantizeTake","name":"quantizeTake","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"quantizeTake(take, { bpm, grid = 4, strength = 1, offsetMs = 0 } = {})","import":"import { quantizeTake } from '@openav/record';","source":{"file":"packages/record/take.js","line":259,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L259"},"summary":{"en":"Snaps the key presses of a take to a rhythmic grid, so a loose performance lands on the beat.","zh":"把一個 take 的按鍵對齊到節奏格子上，讓不太準的演奏落在拍點上。"},"description":[{"en":"Each key-down moves to the nearest grid line (`60000 / bpm / grid` milliseconds apart). The key-up of a note moves with it, so the length of every note is kept. Everything that is not a note (controllers, pitch bend) stays where it was. A note written in two spellings at the same time (`midi/note/on` and `midi/ch/1/note/60`) moves by the same amount in both.","zh":"每次按下的鍵會移到最近的格線上（格線之間隔 `60000 / bpm / grid` 毫秒）。音符放開的時間跟著一起移，所以每個音的長度不變。不是音符的東西（控制器、彎音）留在原處。同一個音符用兩種寫法同時記錄的（`midi/note/on` 和 `midi/ch/1/note/60`），兩邊移動的量一樣。"}],"params":[{"name":"take","type":"Object","en":"The take.","zh":"這個 take。"},{"name":"bpm","type":"Number","optional":true,"en":"The tempo. Left out, it is `take.meta.bpm`, and then 120.","zh":"速度。沒給的話用 `take.meta.bpm`，再沒有就是 120。"},{"name":"grid","type":"Number","optional":true,"default":"4","en":"Grid steps per beat: 4 is sixteenths, 2 is eighths, 1 is quarters.","zh":"每一拍幾格：4 是十六分音符，2 是八分音符，1 是四分音符。"},{"name":"strength","type":"Number","optional":true,"default":"1","en":"How far to move, 0..1. 1 is all the way, 0.5 is half way to the grid, 0 changes nothing.","zh":"移動多少，0..1。1 是移到定位，0.5 是移一半，0 是什麼都不變。"},{"name":"offsetMs","type":"Number","optional":true,"default":"0","en":"Shift the grid later by this many milliseconds, for a take that does not start on a beat.","zh":"把格子往後移這麼多毫秒，給不是從拍點開始的 take 用。"}],"returns":{"type":"Object","en":"A new take.","zh":"一個新的 take。"},"notes":[],"examples":[{"code":"import { quantizeTake } from '@openav/record';\n\nconst on = (t, note) => ({ t, name: 'midi/note/on', value: { note, vel: 0.8, ch: 1 } });\nconst off = (t, note) => ({ t, name: 'midi/note/off', value: { note, ch: 1 } });\nconst take = { v: 1, name: 'loose', durationMs: 1500, meta: { bpm: 120 },\n  events: [on(510, 60), off(900, 60), on(1010, 64), off(1400, 64)] };\n\nconst q = quantizeTake(take, { grid: 4 });              // sixteenths at 120 bpm: a grid line every 125 ms\nconsole.log(q.events.map((e) => e.t));                  // [500, 890, 1000, 1390]\nconsole.log(quantizeTake(take, { grid: 4, strength: 0.5 }).events.map((e) => e.t));   // [505, 895, 1005, 1395]","caption":{"en":"Two notes a little off the beat snap to 500 and 1000 ms, and each note-off moves with its note. With a strength of one half they only go halfway.","zh":"兩個稍微偏離拍點的音對齊到 500 和 1000 毫秒，每個放開的時間跟著它的音一起移。強度一半的話，它們只移一半。"},"run":"module","lang":"js"}],"related":["trimSilence","takeStats","toMidiFile","normalizeTake"],"keywords":{"en":["quantize","snap to grid","timing","on the beat","tempo","groove","clean up playing"],"zh":["量化","對齊格子","節奏","對拍","速度","律動","修正演奏"]},"tbd":[],"claims":[]},
{"id":"recommendedBitrate","key":"recommendedBitrate","name":"recommendedBitrate","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"recommendedBitrate(w, h, fps = recommendedFps(w, h))","import":"import { recommendedBitrate } from '@openav/record';","source":{"file":"packages/record/layout.js","line":37,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L37"},"summary":{"en":"A video bitrate that keeps fast generative detail clean: 0.12 bits per pixel per frame, held between 4 and 80 Mb/s.","zh":"能讓快速的生成式細節保持乾淨的影像位元率：每個像素每個畫面 0.12 位元，限制在 4 到 80 Mb/s。"},"description":[{"en":"Particles and thin lines are the first things a low bitrate smears, so this is higher than a streaming bitrate on purpose: the file is a master that will be edited and encoded again. It is the default `videoBitsPerSecond` of a [`Recorder`](#Recorder) that records a canvas.","zh":"粒子和細線是位元率太低時最先被糊掉的東西，所以這個數字刻意比串流的位元率高：這個檔案是要再剪輯、再編碼的母帶。它是錄畫布的 [`Recorder`](#Recorder) 的 `videoBitsPerSecond` 預設值。"}],"params":[{"name":"w","type":"Number","en":"Width in pixels.","zh":"寬，單位是像素。"},{"name":"h","type":"Number","en":"Height in pixels.","zh":"高，單位是像素。"},{"name":"fps","type":"Number","optional":true,"default":"recommendedFps(w, h)","en":"The frame rate. Left out, it is [`recommendedFps()`](#recommendedFps) for the size.","zh":"畫格率。沒給的話，用這個尺寸的 [`recommendedFps()`](#recommendedFps)。"}],"returns":{"type":"Number","en":"Bits per second, rounded to 100 000. 1080 by 1920 at 60 fps is 14 900 000, 4K at 30 fps is 29 900 000.","zh":"每秒位元數，四捨五入到 100 000。1080 乘 1920、60 fps 是 14 900 000，4K、30 fps 是 29 900 000。"},"notes":[],"examples":[{"code":"import { recommendedBitrate } from '@openav/record';\n\nconst mbps = (w, h, fps) => (recommendedBitrate(w, h, fps) / 1e6).toFixed(1) + ' Mb/s';\nconsole.log(mbps(1080, 1920));       // 14.9 Mb/s\nconsole.log(mbps(2160, 3840));       // 29.9 Mb/s\nconsole.log(mbps(320, 240));         // 4.0 Mb/s, the floor\nconsole.log(mbps(3840, 2160, 60));   // 59.7 Mb/s","caption":{"en":"The bitrate for a few sizes, in megabits per second: the floor of 4 holds for a tiny frame, and asking for 60 fps at 4K doubles the number.","zh":"幾種尺寸的位元率，單位是每秒百萬位元：很小的畫面會停在下限 4，4K 要 60 fps 的話數字會加倍。"},"run":"module","lang":"js"}],"related":["recommendedFps","Recorder"],"keywords":{"en":["bitrate","quality","video bitrate","mbps","compression","master"],"zh":["位元率","畫質","影像位元率","壓縮","母帶"]},"tbd":[],"claims":[]},
{"id":"recommendedFps","key":"recommendedFps","name":"recommendedFps","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"recommendedFps(w, h)","import":"import { recommendedFps } from '@openav/record';","source":{"file":"packages/record/layout.js","line":25,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L25"},"summary":{"en":"The frame rate to record a frame size at: 60 up to about 2.6 megapixels, 30 above that.","zh":"一個畫面尺寸該用什麼畫格率來錄：到約 260 萬像素是 60，再大就是 30。"},"description":[{"en":"60 fps holds up to about 1080 by 1920 (and 1440 by 1800, 1920 by 1200). A 4K canvas at 60 means 500 million pixels a second through the compositor and the encoder, which drops frames on a laptop in the middle of a performance, and 30 is what cameras shoot 4K at anyway. Pass the result as `fps` to a [`Recorder`](#Recorder).","zh":"60 fps 可以撐到約 1080 乘 1920（以及 1440 乘 1800、1920 乘 1200）。4K 的畫布用 60 的話，合成器和編碼器每秒要處理 5 億個像素，在筆電上表演到一半會掉格，而且相機拍 4K 本來也是 30。把結果當作 `fps` 交給 [`Recorder`](#Recorder)。"}],"params":[{"name":"w","type":"Number","en":"Width in pixels.","zh":"寬，單位是像素。"},{"name":"h","type":"Number","en":"Height in pixels.","zh":"高，單位是像素。"}],"returns":{"type":"30 | 60","en":"60 when `w × h` is at most 2 600 000 pixels, otherwise 30.","zh":"`w × h` 不超過 2 600 000 像素是 60，否則是 30。"},"notes":[],"examples":[{"code":"import { recommendedFps } from '@openav/record';\n\nconsole.log(recommendedFps(1080, 1920));   // 60\nconsole.log(recommendedFps(2160, 3840));   // 30\nconsole.log(recommendedFps(1920, 1200));   // 60","caption":{"en":"Logs the rate for a 1080p vertical frame, a 4K vertical frame and a 16:10 landscape frame.","zh":"印出 1080p 直式、4K 直式和 16:10 橫式畫面的畫格率。"},"run":"module","lang":"js"}],"related":["recommendedBitrate","Recorder","PRESETS"],"keywords":{"en":["fps","frame rate","60","30","4k","performance","dropped frames"],"zh":["畫格率","fps","每秒幾格","4k","掉格"]},"tbd":[],"claims":[]},
{"id":"Recorder","key":"Recorder","name":"Recorder","aliases":[],"kind":"class","package":"@openav/record","category":"record","documented":true,"signature":"new Recorder({ canvas = null, stream = null, fps = 60, audioTracks = [], audioOnly = false, videoBitsPerSecond, audioBitsPerSecond = 192000, mimeType, prefer = null } = {})","import":"import { Recorder } from '@openav/record';","source":{"file":"packages/record/recorder.js","line":32,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/recorder.js#L32"},"summary":{"en":"Records a canvas, together with the sound you give it, into one video file with one MediaRecorder, so picture and sound stay in sync.","zh":"用一個 MediaRecorder 把一張畫布連同你給它的聲音錄成一個影片檔，所以畫面和聲音保持同步。"},"description":[{"en":"`new Recorder({ canvas, audioTracks })` then `start()` and, later, `await stop()`. The canvas track and the audio track go into the same `MediaRecorder`, which stamps both from one clock: nothing has to be lined up afterwards. `stop()` resolves, after the last chunk, with `{ mimeType, ext, bytes, chunks, durationMs, width, height, fps, audio, blob, error }`: the `blob` is the file, `ext` the extension to give it (`mp4`, `webm`, `m4a`…), `audio` whether it has sound. Give the blob to `URL.createObjectURL()` for a download link.","zh":"`new Recorder({ canvas, audioTracks })`，然後 `start()`，之後 `await stop()`。畫布的影像軌和聲音軌進的是同一個 `MediaRecorder`，它用同一個時鐘替兩者打時間戳記，所以事後不用對齊。`stop()` 在最後一塊資料到了之後 resolve，內容是 `{ mimeType, ext, bytes, chunks, durationMs, width, height, fps, audio, blob, error }`：`blob` 就是檔案，`ext` 是該用的副檔名（`mp4`、`webm`、`m4a`…），`audio` 是有沒有聲音。把 blob 交給 `URL.createObjectURL()` 就能做下載連結。"},{"en":"Which file you get depends on the browser: it records MP4 (H.264 and AAC) when it can, WebM otherwise. A WebM from Chrome has no duration in its header; players cope, editors may want `ffmpeg -i in.webm -c copy out.webm`. The file is a master to edit, so the default bitrate is high.","zh":"拿到什麼檔案看瀏覽器：能錄 MP4（H.264 和 AAC）就錄 MP4，不行就錄 WebM。Chrome 錄出來的 WebM 檔頭沒有長度，播放器沒問題，剪輯軟體可能需要 `ffmpeg -i in.webm -c copy out.webm`。這個檔案是拿來剪輯的母帶，所以預設的位元率偏高。"},{"en":"To keep a long take out of the tab's memory, pass `ondata(blob, seq)` to `start()`: each chunk (every `timeslice` ms) is handed over in order, and a promise you return is awaited before the next chunk and before `stop()` resolves, so uploads stay ordered. With `ondata` the chunks are not kept and `blob` is `null`, unless you pass `keep: true`. To line a take up with an [`EventLog`](#EventLog), start the log with `recorder.t0`, the `performance.now()` of the moment recording started.","zh":"要讓很長的一段錄影不佔著分頁的記憶體，就在 `start()` 傳 `ondata(blob, seq)`：每一塊資料（每隔 `timeslice` 毫秒）會依序交給你，你回傳的 promise 會在下一塊、以及 `stop()` resolve 之前被等待，所以上傳的順序不會亂。有給 `ondata` 的話資料塊不會保留，`blob` 是 `null`，除非你傳 `keep: true`。要讓一段錄影跟 [`EventLog`](#EventLog) 對齊，就用 `recorder.t0` 來開始事件記錄，它是開始錄影那一刻的 `performance.now()`。"}],"params":[{"name":"canvas","type":"HTMLCanvasElement","optional":true,"default":"null","en":"What to record, usually `compositor.canvas`.","zh":"要錄什麼，通常是 `compositor.canvas`。"},{"name":"stream","type":"MediaStream","optional":true,"default":"null","en":"A ready video stream to record instead of a canvas.","zh":"要錄的現成影像串流，用來取代畫布。"},{"name":"fps","type":"Number","optional":true,"default":"60","en":"The capture rate of the canvas. Use [`recommendedFps(w, h)`](#recommendedFps): 30 for 2.7K and 4K.","zh":"畫布的擷取頻率。用 [`recommendedFps(w, h)`](#recommendedFps)：2.7K 和 4K 是 30。"},{"name":"audioTracks","type":"MediaStreamTrack[]","optional":true,"default":"[]","en":"The sound, for example `tap.tracks` of an [`AudioTap`](#AudioTap). Several tracks are mixed into one, because Chrome records only the first audio track of a stream.","zh":"聲音，例如 [`AudioTap`](#AudioTap) 的 `tap.tracks`。給好幾條會混成一條，因為 Chrome 只會錄一個串流的第一條音軌。"},{"name":"audioOnly","type":"Boolean","optional":true,"default":"false","en":"Record only the audio tracks: the sound as a file of its own. Then `canvas` and `stream` are not needed.","zh":"只錄音軌：把聲音另外存成一個檔。這時不需要 `canvas` 和 `stream`。"},{"name":"videoBitsPerSecond","type":"Number","optional":true,"en":"The video bitrate. The default is [`recommendedBitrate()`](#recommendedBitrate) of the canvas size and `fps` (12 Mb/s when there is no canvas).","zh":"影像位元率。預設是依畫布尺寸和 `fps` 算出的 [`recommendedBitrate()`](#recommendedBitrate)（沒有畫布的話是 12 Mb/s）。"},{"name":"audioBitsPerSecond","type":"Number","optional":true,"default":"192000","en":"The audio bitrate.","zh":"聲音位元率。"},{"name":"mimeType","type":"String","optional":true,"en":"Force one MIME type. The default is the first one the browser supports from [`VIDEO_MIMES`](#VIDEO_MIMES) or [`AUDIO_MIMES`](#AUDIO_MIMES), see [`pickMime()`](#pickMime).","zh":"強制用某一個 MIME 型別。預設是瀏覽器支援的第一個 [`VIDEO_MIMES`](#VIDEO_MIMES) 或 [`AUDIO_MIMES`](#AUDIO_MIMES)，見 [`pickMime()`](#pickMime)。"},{"name":"prefer","type":"'mp4' | 'webm'","optional":true,"default":"null","en":"Try this container first when picking the MIME type.","zh":"挑 MIME 型別的時候先試這種容器。"}],"needs":{"en":"A browser with `MediaRecorder` (`Recorder.supported` tells), and a canvas or a stream, or `audioOnly: true`: otherwise the constructor throws a `TypeError`. Mixing several audio tracks makes an `AudioContext`, which needs the page to have had a click. A camera or microphone in the stream needs the user's permission, as always.","zh":"需要有 `MediaRecorder` 的瀏覽器（`Recorder.supported` 會告訴你），也要有畫布或串流，或 `audioOnly: true`：否則建構時會丟出 `TypeError`。混好幾條音軌會建一個 `AudioContext`，頁面需要先被點過一次。串流裡有鏡頭或麥克風的話，一樣需要使用者許可。"},"notes":[],"examples":[{"code":"import { Recorder } from '@openav/record';\n\nconst canvas = document.getElementById('c');\nconst g = canvas.getContext('2d');\nlet x = 0;\n(function animate() {                          // something moving to record\n  g.fillStyle = '#111'; g.fillRect(0, 0, 400, 300);\n  g.fillStyle = '#ffd166'; g.fillRect(x = (x + 5) % 380, 120, 20, 60);\n  requestAnimationFrame(animate);\n})();\n\nconst rec = new Recorder({ canvas, fps: 30 });\nrec.start();\nawait new Promise((done) => setTimeout(done, 1000));   // record for one second\nconst take = await rec.stop();\nconsole.log(take.ext, take.width, take.height, take.fps, take.audio, take.blob.size > 0);\n// mp4 400 300 30 false true   (webm where the browser cannot write mp4)\n\nconst a = document.getElementById('app').appendChild(document.createElement('a'));\na.href = URL.createObjectURL(take.blob); a.download = 'take.' + take.ext; a.textContent = 'Download';","caption":{"en":"Records one second of the moving square and logs what the file is. A Download link appears on the page.","zh":"錄一秒鐘會動的方塊，印出這個檔案是什麼。頁面上出現一個下載連結。"},"run":"module","lang":"js"},{"code":"import { Recorder, AudioTap } from '@openav/record';\n\nconst tap = new AudioTap();                                  // the sound that goes into the file\nconst osc = new OscillatorNode(tap.context, { frequency: 220 });\ntap.add(osc); osc.start();\nawait tap.resume();                                          // from a click, in a real page\n\nconst video = new Recorder({ canvas: document.getElementById('c'), fps: 30, audioTracks: tap.tracks });\nconst sound = new Recorder({ audioOnly: true, audioTracks: tap.workTracks });   // the sound as its own file\nvideo.start(); sound.start();\nawait new Promise((done) => setTimeout(done, 1000));\nconst [v, a] = await Promise.all([video.stop(), sound.stop()]);\nconsole.log(v.ext, v.audio, a.ext, a.width);   // mp4 true m4a 0","caption":{"en":"One second of video with a 220 Hz tone in it, and the same tone as a separate audio file. The picture and the sound of the video enter the same recorder.","zh":"一秒鐘的影片，裡面有一個 220 Hz 的音，同一個音另外存成一個聲音檔。影片的畫面和聲音進的是同一個錄影器。"},"run":"module","lang":"js"},{"code":"import { Recorder } from '@openav/record';\n\nconst chunks = [];\nconst rec = new Recorder({ canvas: document.getElementById('c'), fps: 30 });\nrec.start({\n  timeslice: 250,\n  ondata: async (blob, seq) => { chunks.push(blob.size); },   // send it somewhere here; the next chunk waits\n});\nawait new Promise((done) => setTimeout(done, 1000));\nconst take = await rec.stop();\nconsole.log(take.blob, take.chunks === chunks.length, take.bytes === chunks.reduce((a, b) => a + b, 0));   // null true true","caption":{"en":"With `ondata` the chunks are handed to you one by one, in order, and the result carries no `blob`: this is how a long take stays out of the tab's memory.","zh":"有給 `ondata` 的話，資料塊會依序一塊一塊交給你，結果裡沒有 `blob`：長時間的錄影就是這樣不佔分頁的記憶體。"},"run":"module","lang":"js"}],"related":["Compositor","AudioTap","EventLog","pickMime","recommendedFps","startRecording","stopRecording"],"keywords":{"en":["record","recorder","mediarecorder","video","mp4","webm","canvas capture","audio","download","blob"],"zh":["錄影","錄影器","影片","mp4","webm","畫布擷取","聲音","下載","blob","錄製"]},"tbd":[{"en":"Whether data arrives while the recording is still running depends on the container and the browser. In headless Chromium, writing MP4, `bytes` stayed 0 for a one-second take with a `timeslice` of 200 and the chunks came at `stop()`. Other browsers and WebM were not tried.","zh":"錄影進行中資料會不會陸續送來，要看容器和瀏覽器。在 headless Chromium 錄 MP4 的時候，一秒鐘的錄影、`timeslice` 設 200，`bytes` 一直是 0，資料塊是在 `stop()` 時才來。其他瀏覽器和 WebM 沒有試過。"}],"claims":[],"members":[{"name":"state","member":"getter","signature":"","doc":"'inactive' | 'recording' | 'paused'","line":69},{"name":"recording","member":"getter","signature":"","doc":"","line":70},{"name":"elapsed","member":"getter","signature":"","doc":"ms since start while recording; the take's length once stopped; 0 before.","line":72},{"name":"bytes","member":"getter","signature":"","doc":"Bytes handed over so far.","line":74},{"name":"start","member":"method","signature":"{ timeslice = 1000, ondata = null, keep } = {}","doc":"Start recording.","line":100},{"name":"stop","member":"method","signature":"","doc":"Stop and finish.","line":141},{"name":"supported","member":"static property","signature":"","doc":"True when this browser can record a canvas.","line":64}]},
{"id":"resolveSize","key":"resolveSize","name":"resolveSize","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"resolveSize(size)","import":"import { resolveSize } from '@openav/record';","source":{"file":"packages/record/layout.js","line":79,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/layout.js#L79"},"summary":{"en":"Turns any way of naming a size, like 'vertical-4k', '1080x1920' or [w, h], into { w, h }, rounded up to even numbers.","zh":"把任何寫尺寸的方式，像 'vertical-4k'、'1080x1920' 或 [w, h]，變成 { w, h }，並進位成偶數。"},"description":[{"en":"It is what the [`Compositor`](#Compositor) uses for its `size` and `setSize()`. A size that cannot be read (`'wat'`, a negative number, a missing side) throws a `TypeError`: `record: unknown size \"wat\"`. Even sizes matter because H.264 and VP9 encode 4:2:0 colour.","zh":"[`Compositor`](#Compositor) 的 `size` 和 `setSize()` 用的就是它。讀不懂的尺寸（`'wat'`、負數、少一邊）會丟出 `TypeError`：`record: unknown size \"wat\"`。尺寸要偶數，是因為 H.264 和 VP9 用 4:2:0 的色彩編碼。"}],"params":[{"name":"size","type":"String | Number[] | Object","en":"A preset id from [`PRESETS`](#PRESETS), `'WxH'` (also with `×` or `*`), `[w, h]` or `{ w, h }`.","zh":"[`PRESETS`](#PRESETS) 的 id、`'WxH'`（也可以用 `×` 或 `*`）、`[w, h]` 或 `{ w, h }`。"}],"returns":{"type":"Object","en":"`{ w, h }`, whole numbers, each rounded up to the next even number.","zh":"`{ w, h }`，整數，各自進位到下一個偶數。"},"notes":[],"examples":[{"code":"import { resolveSize } from '@openav/record';\n\nconsole.log(resolveSize('vertical-4k'));      // { w: 2160, h: 3840 }\nconsole.log(resolveSize('1081x1921'));        // { w: 1082, h: 1922 }\nconsole.log(resolveSize([1000, 563]));        // { w: 1000, h: 564 }\ntry { resolveSize('wat'); } catch (e) { console.log(e.message); }   // record: unknown size \"wat\"","caption":{"en":"Four ways of naming a size: a preset, text, a list, and a mistake that throws. Odd sides become even.","zh":"四種寫尺寸的方式：預設、文字、陣列，和一個會丟出錯誤的寫法。奇數的邊會變成偶數。"},"run":"module","lang":"js"}],"related":["PRESETS","presetById","Compositor","layoutRects"],"keywords":{"en":["size","resolution","even","width height","parse size","normalize"],"zh":["尺寸","解析度","偶數","寬高","解析尺寸","正規化"]},"tbd":[],"claims":[]},
{"id":"signalsOfMidi","key":"signalsOfMidi","name":"signalsOfMidi","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"signalsOfMidi(ev, { families = 'both', device = null } = {})","import":"import { signalsOfMidi } from '@openav/record';","source":{"file":"packages/record/take.js","line":189,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L189"},"summary":{"en":"Turns one MIDI event into the signal events a keyboard playing it would publish, so a file can drive a work like live playing.","zh":"把一個 MIDI 事件轉成鍵盤彈它時會發布的訊號事件，讓一個檔案能像現場演奏那樣驅動作品。"},"description":[{"en":"It is what [`fromMidiFile()`](#fromMidiFile) uses to build a take from a `.mid` file: each event of the file becomes the signals a keyboard would have published, so [`TakePlayer`](#TakePlayer) can play it into a show. Call it yourself when you have MIDI events from somewhere else and want them on a [`Signals`](#Signals) bus.","zh":"[`fromMidiFile()`](#fromMidiFile) 用它把 `.mid` 檔做成 take：檔案裡每個事件都變成鍵盤本來會發布的訊號，這樣 [`TakePlayer`](#TakePlayer) 就能把它播進演出裡。如果你有別處來的 MIDI 事件，想放到 [`Signals`](#Signals) 匯流排上，就自己呼叫它。"}],"params":[{"name":"ev","type":"Object","en":"A MIDI event `{ type, ch, note?, vel?, cc?, value? }`. `type` is `noteon`, `noteoff`, `cc`, `pitchbend` (`value` 0..16383), `chanat` or `polyat`; velocities and values are 0..127.","zh":"MIDI 事件 `{ type, ch, note?, vel?, cc?, value? }`。`type` 是 `noteon`、`noteoff`、`cc`、`pitchbend`（`value` 是 0..16383）、`chanat` 或 `polyat`，力度和數值是 0..127。"},{"name":"families","type":"'both' | 'legacy' | 'channel'","optional":true,"default":"'both'","en":"Which spellings to make: the classic names (`midi/note/on`, `midi/cc/64`, `midi/bend`), the per-channel ones (`midi/ch/1/note/60`, `midi/ch/1/cc/64`…), or both, as hardware does.","zh":"要做哪幾種寫法：傳統名稱（`midi/note/on`、`midi/cc/64`、`midi/bend`）、每頻道的（`midi/ch/1/note/60`、`midi/ch/1/cc/64`…），或兩種都做，就像硬體一樣。"},{"name":"device","type":"String","optional":true,"default":"null","en":"A device name to put in the classic note events.","zh":"要放進傳統音符事件的裝置名稱。"}],"returns":{"type":"Object[]","en":"A list of `{ name, value }`. Note velocity becomes 0..1 (`vel` and `velocity` for the classic spelling), controllers 0..1, pitch bend -1..1. `chanat` and `polyat` exist only in the per-channel spelling.","zh":"`{ name, value }` 的清單。音符力度變成 0..1（傳統寫法有 `vel` 和 `velocity`），控制器是 0..1，彎音是 -1..1。`chanat` 和 `polyat` 只有每頻道的寫法。"},"notes":[],"examples":[{"code":"import { signalsOfMidi } from '@openav/record';\n\nconst ev = { type: 'noteon', ch: 1, note: 60, vel: 100 };\nconsole.log(signalsOfMidi(ev));\n// [{ name: 'midi/note/on', value: { note: 60, vel: 0.787402, velocity: 100, ch: 1 } },\n//  { name: 'midi/ch/1/note/60', value: 0.787402 }]\nconsole.log(signalsOfMidi(ev, { families: 'channel' }));   // [{ name: 'midi/ch/1/note/60', value: 0.787402 }]\nconsole.log(signalsOfMidi({ type: 'cc', ch: 1, cc: 64, value: 127 }, { families: 'legacy' }));\n// [{ name: 'midi/cc/64', value: 1 }]","caption":{"en":"One note-on becomes two signals (the two spellings), or one when you pick a family. The sustain pedal at full is a 1.","zh":"一個 note-on 變成兩個訊號（兩種寫法），挑一種寫法就只有一個。踩到底的延音踏板是 1。"},"run":"module","lang":"js"}],"related":["fromMidiFile","midiEventsOf","TakePlayer","Signals"],"keywords":{"en":["midi to signals","convert","midi event","note on","cc","pitch bend","signal names"],"zh":["midi 轉訊號","轉換","midi 事件","音符開","控制器","彎音","訊號名稱"]},"tbd":[],"claims":[]},
{"id":"TAKE_VERSION","key":"TAKE_VERSION","name":"TAKE_VERSION","aliases":[],"kind":"constant","package":"@openav/record","category":"record","documented":true,"signature":"TAKE_VERSION = 1","import":"import { TAKE_VERSION } from '@openav/record';","source":{"file":"packages/record/take.js","line":19,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L19"},"summary":{"en":"The version number of the take format, which is 1. Every take has `v: 1`.","zh":"take 格式的版本號，是 1。每個 take 都有 `v: 1`。"},"description":[{"en":"A take is a plain object `{ v, name, createdAt, durationMs, events, meta }`, see [`normalizeTake()`](#normalizeTake). [`validateTake()`](#validateTake) complains when `v` is not this number, and [`normalizeTake()`](#normalizeTake) writes it.","zh":"take 是一個普通物件 `{ v, name, createdAt, durationMs, events, meta }`，見 [`normalizeTake()`](#normalizeTake)。`v` 不是這個數字時，[`validateTake()`](#validateTake) 會抱怨，[`normalizeTake()`](#normalizeTake) 會把它寫進去。"}],"value":{"type":"Number","en":"`1`.","zh":"版本號就是 `1`。"},"params":[],"notes":[],"examples":[{"code":"import { TAKE_VERSION, normalizeTake } from '@openav/record';\n\nconsole.log(TAKE_VERSION);                       // 1\nconsole.log(normalizeTake({ events: [] }).v);    // 1","caption":{"en":"Logs the version, and that a take built from nothing gets it.","zh":"印出版本號，以及從空白做出來的 take 也會有它。"},"run":"module","lang":"js"}],"related":["normalizeTake","validateTake"],"keywords":{"en":["take version","format version","schema","v: 1"],"zh":["take 版本","格式版本","結構","版本號"]},"tbd":[],"claims":[]},
{"id":"TakePlayer","key":"TakePlayer","name":"TakePlayer","aliases":[],"kind":"class","package":"@openav/record","category":"record","documented":true,"signature":"new TakePlayer({ signals = null, take = null, loop = true, speed = 1, yieldToLive = false, resumeAfter = 0, isLive = defaultIsLive, chase = true, clock, maxStepMs = 250, onEvent = null, onState = null, now = defaultNow } = {})","import":"import { TakePlayer } from '@openav/record';","source":{"file":"packages/record/take-player.js","line":105,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take-player.js#L105"},"summary":{"en":"Plays a take back into a bus at the right times, so the work, its sound and its chord detector cannot tell it from you playing.","zh":"把一個 take 在對的時間播回匯流排，讓作品、它的聲音和和弦偵測器分不出這是播放還是你在彈。"},"description":[{"en":"Give it a bus and a take, call `play()`, and it publishes the take's events into the bus at their times: the same signals a keyboard would have, so a work's visuals, its sound ([`Sound`](#Sound)) and its chord detector respond exactly as to your hands. That is what lets a work's preview play itself. Pass `clock: 'manual'` and call `player.update(dt)` from your loop for full control.","zh":"給它一個匯流排和一個 take，呼叫 `play()`，它就會在對的時間把 take 的事件發布到匯流排上：跟鍵盤會發的訊號一樣，所以作品的畫面、它的聲音（[`Sound`](#Sound)）和和弦偵測器的反應，跟面對你的雙手時完全一樣。這就是作品的預覽可以自己演奏的原因。傳 `clock: 'manual'` 並在你的迴圈裡呼叫 `player.update(dt)`，就能完全自己掌控。"},{"en":"No stuck notes, ever: the player remembers every key it pressed, in the spelling it pressed it, and the sustain pedal, and lets go of them on pause, stop, seek and every loop wrap. With `yieldToLive`, playing a key yourself makes the take fall silent; with `resumeAfter: 8` it comes back 8 seconds after you stop. Its state is `'playing'`, `'paused'`, `'yielded'` or `'stopped'`, and `position` (milliseconds), `loops`, `playing` and `yielded` are plain properties.","zh":"絕不會卡音：播放器記得它按下的每個鍵（用它按下時的寫法）和延音踏板，並在暫停、停止、跳轉和每次循環回到開頭的時候放開它們。有 `yieldToLive` 的話，你自己彈一個鍵，take 就會安靜下來。有 `resumeAfter: 8` 的話，你停手 8 秒後它會回來。它的狀態是 `'playing'`、`'paused'`、`'yielded'` 或 `'stopped'`，`position`（毫秒）、`loops`、`playing`、`yielded` 是普通屬性。"}],"params":[{"name":"signals","type":"Signals","optional":true,"default":"null","en":"The [`Signals`](#Signals) bus to publish into. It also defines `take/playing` (0 or 1) and `take/position` (0..1).","zh":"要發布訊號的 [`Signals`](#Signals) 匯流排。它也會定義 `take/playing`（0 或 1）和 `take/position`（0..1）。"},{"name":"take","type":"Object | String","optional":true,"default":"null","en":"A take or its JSON. You can `load()` another one later.","zh":"一個 take 或它的 JSON。之後可以用 `load()` 換別的。"},{"name":"loop","type":"Boolean","optional":true,"default":"true","en":"Start again at the end. Off, it stops there.","zh":"到結尾就從頭再來。關掉的話，就停在結尾。"},{"name":"speed","type":"Number","optional":true,"default":"1","en":"Playback speed: 0.5 is half speed.","zh":"播放速度：0.5 是一半的速度。"},{"name":"yieldToLive","type":"Boolean","optional":true,"default":"false","en":"Fall silent when someone plays live: a key-down that the player did not send (hardware, the on-screen piano, a phone) pauses the take and releases its notes.","zh":"有人現場彈的時候就安靜下來：播放器沒送出的按鍵（硬體、螢幕鋼琴、手機）會暫停 take 並放開它的音。"},{"name":"resumeAfter","type":"Number","optional":true,"default":"0","en":"Seconds without live playing before a take that gave way resumes. 0 means it stays paused.","zh":"被讓位的 take 要等多少秒沒有現場演奏才恢復。0 表示維持暫停。"},{"name":"isLive","type":"Function","optional":true,"default":"any key-down","en":"`isLive(name, value)`, what counts as live playing. The default is any key-down that did not come from the player; knobs do not count.","zh":"`isLive(name, value)`，什麼算現場演奏。預設是任何不是播放器送出的按鍵。旋鈕不算。"},{"name":"chase","type":"Boolean","optional":true,"default":"true","en":"When you `seek()`, send again the last value of every controller before that point (the sustain pedal, knobs), so the take is in the state it would have been in.","zh":"`seek()` 的時候，把那個時間點之前每個控制器的最後一個值再送一次（延音踏板、旋鈕），讓 take 處在原本該有的狀態。"},{"name":"clock","type":"'raf' | 'timer' | 'manual'","optional":true,"en":"Who drives the time: `'raf'` (`requestAnimationFrame`, the default in a page), `'timer'` (a 10 ms timer: tighter for sound and it keeps going, slower, in a background tab) or `'manual'` (you call `update(dt)` or `tick()`; the default where there is no `requestAnimationFrame`).","zh":"誰來帶動時間：`'raf'`（`requestAnimationFrame`，頁面裡的預設）、`'timer'`（10 毫秒的計時器：對聲音比較準，背景分頁也會繼續只是比較慢），或 `'manual'`（你呼叫 `update(dt)` 或 `tick()`，沒有 `requestAnimationFrame` 的環境預設是這個）。"},{"name":"maxStepMs","type":"Number","optional":true,"default":"250","en":"The longest time one step may move forward. After a gap (a frozen tab) the take slows down instead of firing a burst of notes.","zh":"一步最多往前走多久。遇到空檔（分頁凍結）的時候，take 會變慢，而不是一口氣打出一堆音。"},{"name":"onEvent","type":"Function","optional":true,"default":"null","en":"`onEvent({ t, name, value }, player)`, for every event as it is played.","zh":"`onEvent({ t, name, value }, player)`，每個事件播出時都呼叫。"},{"name":"onState","type":"Function","optional":true,"default":"null","en":"`onState(state, reason)`, when it changes: `'playing'`, `'paused'`, `'yielded'` or `'stopped'`.","zh":"`onState(state, reason)`，狀態改變時呼叫：`'playing'`、`'paused'`、`'yielded'` 或 `'stopped'`。"},{"name":"now","type":"Function","optional":true,"default":"performance.now","en":"The clock, in milliseconds.","zh":"時鐘，單位是毫秒。"}],"notes":[],"examples":[{"code":"import { TakePlayer } from '@openav/record';\nimport { Signals } from '@openav/core';\n\nconst take = { v: 1, name: 'riff', durationMs: 600, events: [\n  { t: 100, name: 'midi/note/on', value: { note: 60, vel: 0.9, ch: 1 } },\n  { t: 400, name: 'midi/note/off', value: { note: 60, ch: 1 } },\n] };\nconst signals = new Signals();\nconst heard = [];\nsignals.on('midi/note/on', (v) => heard.push('on ' + v.note));\nsignals.on('midi/note/off', (v) => heard.push('off ' + v.note));\n\nconst player = new TakePlayer({ signals, take, loop: false, clock: 'manual' });\nconsole.log(player.state, player.duration);                   // stopped 600\nplayer.play();\nfor (let i = 0; i < 20; i++) player.update(0.05);              // one second, in 50 ms steps\nconsole.log(heard.join(', '), player.state);                   // on 60, off 60 paused","caption":{"en":"A two-event take is played by hand-cranked time into a bus: the listeners hear the note start and end, and because `loop` is off the player stays at the end of the take (`state` reads 'paused', `position` is 600).","zh":"用手搖的時間把只有兩個事件的 take 播進匯流排：聽的人聽到音開始和結束，因為 `loop` 關著，播放器就停在 take 的結尾（`state` 是 `'paused'`，`position` 是 600）。"},"run":"module","lang":"js"},{"code":"import { TakePlayer } from '@openav/record';\nimport { Signals } from '@openav/core';\n\nconst take = { v: 1, name: 'loop', durationMs: 800, events: [\n  { t: 0, name: 'midi/note/on', value: { note: 64, vel: 0.8, ch: 1 } },\n  { t: 600, name: 'midi/note/off', value: { note: 64, ch: 1 } },\n] };\nconst signals = new Signals();\nconst log = [];\nsignals.on('midi/note/on', (v) => log.push('note ' + v.note));\nconst player = new TakePlayer({ signals, take, yieldToLive: true, resumeAfter: 1, clock: 'manual',\n  onState: (state, reason) => log.push(state + (reason ? ' (' + reason + ')' : '')) });\nplayer.play();\nplayer.update(0.1);\nsignals.pulse('midi/note/on', { note: 72, vel: 1, ch: 1 });   // someone plays live: the take gives way\nlog.push('live');\nfor (let i = 0; i < 25; i++) player.update(0.05);              // 1.25 s of silence: it comes back\nconsole.log(log.join(' | '));\n// playing | note 64 | note 72 | yielded (live) | live | playing (resume)","caption":{"en":"A looping take gives way when a key is played live and returns one second after the playing stops. The log shows the order of events.","zh":"循環播放的 take 在有人現場按鍵時讓位，現場演奏停止一秒後又回來。log 顯示事件的順序。"},"run":"module","lang":"js"}],"related":["TakeRecorder","EventLog","fromMidiFile","noteOffFor","Sound","Signals"],"keywords":{"en":["play take","playback","replay","loop","midi playback","preview plays itself","yield to live","seek","stuck notes"],"zh":["播放 take","播放","重播","循環","midi 播放","預覽自己演奏","讓位給現場","跳轉","卡音"]},"tbd":[],"claims":[],"members":[{"name":"duration","member":"getter","signature":"","doc":"","line":155},{"name":"progress","member":"getter","signature":"","doc":"","line":156},{"name":"state","member":"getter","signature":"","doc":"","line":157},{"name":"load","member":"method","signature":"take","doc":"Replace the take (stops the current one, releasing its notes).","line":160},{"name":"play","member":"method","signature":"{ from } = {}","doc":"Play from the current position (from the start after the end), or from `from` ms.","line":171},{"name":"pause","member":"method","signature":"reason","doc":"Pause where it is; sounding notes are released.","line":182},{"name":"stop","member":"method","signature":"","doc":"Stop and rewind; sounding notes are released.","line":191},{"name":"seek","member":"method","signature":"ms","doc":"Jump to `ms` (sounding notes released; controllers chased when `chase`).","line":202},{"name":"update","member":"method","signature":"dt","doc":"Advance by `dt` seconds (the Loop's / lab.frame's dt).","line":214},{"name":"tick","member":"method","signature":"nowMs = this.now()","doc":"Advance to clock time `nowMs` (default now()): what the raf and timer clocks call.","line":217},{"name":"dispose","member":"method","signature":"","doc":"Stop, release, unsubscribe.","line":224}]},
{"id":"TakeRecorder","key":"TakeRecorder","name":"TakeRecorder","aliases":[],"kind":"class","package":"@openav/record","category":"record","documented":true,"signature":"new TakeRecorder({ signals, filter = /^midi\\//, name = 'take', meta = {}, overdub = false, throttleMs = 0, now = defaultNow } = {})","import":"import { TakeRecorder } from '@openav/record';","source":{"file":"packages/record/take-player.js","line":39,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take-player.js#L39"},"summary":{"en":"Records what you play: it listens to every midi/* signal on a bus from start() to stop() and gives you a take.","zh":"錄下你彈的東西：從 start() 到 stop() 聽匯流排上每個 midi/* 訊號，然後給你一個 take。"},"description":[{"en":"A `TakeRecorder` is an [`EventLog`](#EventLog) that listens to `midi/*` on a bus. Press record: `rec.start()`. Play. Press stop: `const take = rec.stop()`. The take is plain data, so it can be saved as JSON, turned into a `.mid` ([`toMidiFile()`](#toMidiFile)) or played back ([`TakePlayer`](#TakePlayer)). It has every member of `EventLog` (`mark()`, `between()`, `toJSON()`…).","zh":"`TakeRecorder` 是一個聽匯流排上 `midi/*` 的 [`EventLog`](#EventLog)。按下錄音：`rec.start()`。彈。按下停止：`const take = rec.stop()`。take 是單純的資料，所以可以存成 JSON、轉成 `.mid`（[`toMidiFile()`](#toMidiFile)），或播回去（[`TakePlayer`](#TakePlayer)）。`EventLog` 的所有成員它都有（`mark()`、`between()`、`toJSON()`…）。"},{"en":"No stuck notes: a recorder stopped in the middle of a chord closes the keys that are still held (and a sustain pedal that is still down) in the take itself, at the stop time. The events are the signals as they were published, so a take from hardware holds both naming families (`midi/note/on` and `midi/ch/1/note/60`) and the device names, and playback publishes all of them. Pass `recorder.t0` of a video [`Recorder`](#Recorder) to `start()` to line the take up with the video.","zh":"不會有卡住的音：在彈和弦的中間停止錄影，還按著的鍵（和還踩著的延音踏板）會在停止的時間，於 take 本身裡被放開。事件就是訊號被發布時的原樣，所以從硬體錄的 take 兩種名稱寫法（`midi/note/on` 和 `midi/ch/1/note/60`）和裝置名稱都在，播放時全部發布出去。把影片 [`Recorder`](#Recorder) 的 `recorder.t0` 傳給 `start()`，就能讓 take 跟影片對齊。"}],"params":[{"name":"signals","type":"Signals","optional":false,"en":"The [`Signals`](#Signals) bus to listen to.","zh":"要聽的 [`Signals`](#Signals) 匯流排。"},{"name":"filter","type":"RegExp | Function","optional":true,"default":"/^midi\\//","en":"Which signals are the take: a pattern for the name, or `filter(name, value, meta)`.","zh":"哪些訊號算進 take：名稱的正規表達式，或 `filter(name, value, meta)`。"},{"name":"name","type":"String","optional":true,"default":"'take'","en":"The name of the take.","zh":"take 的名字。"},{"name":"meta","type":"Object","optional":true,"default":"{}","en":"Free fields copied into the take: `{ bpm, device, notes }`. The device is found from the events if you leave it out.","zh":"複製進 take 的自由欄位：`{ bpm, device, notes }`。沒給裝置的話，會從事件裡找。"},{"name":"overdub","type":"Boolean","optional":true,"default":"false","en":"Also record what a [`TakePlayer`](#TakePlayer) is playing back right now. Off, a recorder ignores playback.","zh":"連 [`TakePlayer`](#TakePlayer) 正在播放的也錄下來。關著的時候，錄影器會忽略播放的內容。"},{"name":"throttleMs","type":"Number","optional":true,"default":"0","en":"For continuous signals (knobs, aftertouch): at most one event per name in this many milliseconds.","zh":"對連續訊號（旋鈕、觸後）：每個名字在這麼多毫秒內最多一個事件。"},{"name":"now","type":"Function","optional":true,"default":"performance.now","en":"The clock, in milliseconds.","zh":"時鐘，單位是毫秒。"}],"notes":[],"examples":[{"code":"import { TakeRecorder } from '@openav/record';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nlet clock = 0;\nconst rec = new TakeRecorder({ signals, now: () => clock, meta: { bpm: 100 } });\nrec.start();\nclock = 100; signals.pulse('midi/note/on', { note: 60, vel: 0.9, ch: 1 });\nclock = 400; signals.pulse('midi/note/off', { note: 60, ch: 1 });\nclock = 450; signals.pulse('midi/note/on', { note: 62, vel: 0.5, ch: 1 });   // still held when we stop\nclock = 600; signals.set('audio/rms', 0.2);                                  // not a midi/* signal\nconst take = rec.stop();\nconsole.log(take.events.map((e) => e.t + ' ' + e.name).join(' | '));\n// 100 midi/note/on | 400 midi/note/off | 450 midi/note/on | 600 midi/note/off\nconsole.log(take.durationMs, take.meta);   // 600 { bpm: 100 }","caption":{"en":"Records four events from a bus on a clock moved by hand. The last note was still held, so the take closes it at the stop time; the `audio/rms` signal is not part of the take.","zh":"用手動撥動的時鐘，從匯流排錄四個事件。最後一個音還按著，所以 take 在停止的時間把它放開。`audio/rms` 訊號不屬於這個 take。"},"run":"module","lang":"js"}],"related":["TakePlayer","EventLog","toMidiFile","trimSilence","Recorder"],"keywords":{"en":["record midi","take","record what you play","capture performance","looper","midi recorder","stop","overdub"],"zh":["錄 midi","take","錄下彈的東西","擷取演奏","循環錄音","midi 錄音","停止","疊錄"]},"tbd":[],"claims":[],"members":[{"name":"start","member":"method","signature":"at = this.now()","doc":"Clear and start listening.","line":61},{"name":"add","member":"method","signature":"name, value = null, opts = {}","doc":"","line":69},{"name":"stop","member":"method","signature":"at = this.now()","doc":"Stop listening and return the take.","line":84},{"name":"toTake","member":"method","signature":"{ name = this.name, meta = this.meta } = {}","doc":"","line":95}]},
{"id":"takeStats","key":"takeStats","name":"takeStats","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"takeStats(take)","import":"import { takeStats } from '@openav/record';","source":{"file":"packages/record/take.js","line":223,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L223"},"summary":{"en":"Counts what is in a take: notes, events, length, the channels used, and the lowest and highest note.","zh":"數出一個 take 裡有什麼：音符數、事件數、長度、用到的頻道，以及最低和最高的音。"},"description":[{"en":"Good for showing what a take is before it is played or saved. Notes are counted through [`midiEventsOf()`](#midiEventsOf), which picks one naming family, so the count is the number of keys pressed.","zh":"適合在播放或儲存一個 take 之前，顯示它是什麼。音符是透過 [`midiEventsOf()`](#midiEventsOf) 數的，它只選一種名稱寫法，所以數字就是按過幾個鍵。"}],"params":[{"name":"take","type":"Object","en":"The take.","zh":"這個 take。"}],"returns":{"type":"Object","en":"`{ notes, events, durationMs, channels, low, high }`. `notes` counts key-downs, once each even when the take holds two spellings of them. `channels` is a sorted list of MIDI channels. `low` and `high` are MIDI note numbers, or `null` for a take without notes.","zh":"`{ notes, events, durationMs, channels, low, high }`。`notes` 數的是按下的鍵，每個音只算一次，即使 take 裡有兩種寫法。`channels` 是排好序的 MIDI 頻道清單。`low` 和 `high` 是 MIDI 音符號碼，沒有音符的 take 是 `null`。"},"notes":[],"examples":[{"code":"import { takeStats } from '@openav/record';\n\nconst take = { v: 1, name: 'riff', durationMs: 1500, events: [\n  { t: 500, name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } },\n  { t: 900, name: 'midi/note/off', value: { note: 60, ch: 1 } },\n  { t: 1010, name: 'midi/note/on', value: { note: 64, vel: 0.5, ch: 1 } },\n  { t: 1400, name: 'midi/note/off', value: { note: 64, ch: 1 } },\n] };\nconsole.log(takeStats(take));   // { notes: 2, events: 4, durationMs: 1500, channels: [1], low: 60, high: 64 }","caption":{"en":"Two notes, four events, one channel, from C4 to E4.","zh":"兩個音符、四個事件、一個頻道，從 C4 到 E4。"},"run":"module","lang":"js"}],"related":["midiEventsOf","normalizeTake","noteOf"],"keywords":{"en":["stats","statistics","count notes","range","summary of take","length"],"zh":["統計","音符數","音域","摘要","take 的內容","長度"]},"tbd":[],"claims":[]},
{"id":"toMidiFile","key":"toMidiFile","name":"toMidiFile","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"toMidiFile(take, { ppq = 960, bpm } = {})","import":"import { toMidiFile } from '@openav/record';","source":{"file":"packages/record/smf.js","line":46,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/smf.js#L46"},"summary":{"en":"Writes a take as a Standard MIDI File (type 0) that opens in any DAW.","zh":"把一個 take 寫成標準 MIDI 檔（type 0），任何 DAW 都能開。"},"description":[{"en":"One track holds note on and off with velocity, controllers (the sustain pedal is CC 64), pitch bend, and channel and poly aftertouch, each on its own channel; channel 10 stays the drums. The take's name becomes the track name, and a 4/4 time signature is written only so a DAW's grid looks sane. Times are kept to the millisecond whatever the tempo, so the file plays like the take. Only one naming family of the take is used, so nothing doubles ([`midiEventsOf()`](#midiEventsOf)). To save it, wrap it in a `Blob` of type `audio/midi`.","zh":"一條音軌裡有音符開關（含力度）、控制器（延音踏板是 CC 64）、彎音，以及頻道和單音的觸後，各自在自己的頻道。頻道 10 還是鼓。take 的名字會變成音軌名稱，另外寫了 4/4 拍的拍號，只是讓 DAW 的格線看起來正常。不管速度是多少，時間都精確到毫秒，所以檔案播出來跟 take 一樣。只會用 take 裡的一種名稱寫法，所以不會重複（[`midiEventsOf()`](#midiEventsOf)）。要存檔的話，把它包成型別是 `audio/midi` 的 `Blob`。"}],"params":[{"name":"take","type":"Object","en":"The take.","zh":"這個 take。"},{"name":"ppq","type":"Number","optional":true,"default":"960","en":"Ticks per quarter note.","zh":"每四分音符幾個 tick。"},{"name":"bpm","type":"Number","optional":true,"en":"The tempo written in the file. Left out, it is `take.meta.bpm`, and then 120.","zh":"寫進檔案的速度。沒給的話用 `take.meta.bpm`，再沒有就是 120。"}],"returns":{"type":"Uint8Array","en":"The bytes of the `.mid` file.","zh":"`.mid` 檔的位元組。"},"notes":[],"examples":[{"code":"import { toMidiFile, fromMidiFile } from '@openav/record';\n\nconst take = { v: 1, name: 'riff', durationMs: 1500, meta: { bpm: 120 }, events: [\n  { t: 500, name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } },\n  { t: 900, name: 'midi/note/off', value: { note: 60, ch: 1 } },\n] };\nconst bytes = toMidiFile(take);\nconsole.log(bytes.constructor.name, String.fromCharCode(...bytes.slice(0, 4)));   // Uint8Array MThd\nconsole.log(fromMidiFile(bytes).name);                                            // riff\n\nconst a = document.getElementById('app').appendChild(document.createElement('a'));\na.href = URL.createObjectURL(new Blob([bytes], { type: 'audio/midi' }));\na.download = 'riff.mid'; a.textContent = 'Download riff.mid';","caption":{"en":"Writes a two-event take as a MIDI file, reads it back by its name, and offers it as a download link on the page.","zh":"把只有兩個事件的 take 寫成 MIDI 檔，用名字確認讀得回來，並在頁面上提供下載連結。"},"run":"module","lang":"js"}],"related":["fromMidiFile","midiEventsOf","TakeRecorder","quantizeTake"],"keywords":{"en":["midi file","smf",".mid","export","save midi","daw","download midi","standard midi file"],"zh":["midi 檔","smf",".mid","匯出","存成 midi","daw","下載 midi","標準 midi 檔"]},"tbd":[],"claims":[]},
{"id":"trimSilence","key":"trimSilence","name":"trimSilence","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"trimSilence(take, { keep = 0 } = {})","import":"import { trimSilence } from '@openav/record';","source":{"file":"packages/record/take.js","line":242,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L242"},"summary":{"en":"Cuts the silence before the first key press of a take, so the first note comes at the moment you choose.","zh":"把一個 take 第一次按鍵之前的靜默剪掉，讓第一個音出現在你指定的時間。"},"description":[{"en":"Everything before the first key-down is cut away, but not lost: a controller value set during the silence, such as the sustain pedal or a knob, is kept and moved to the start (to `t = 0` if it was before the cut), so the take still starts in the same state. If the take has no key-down at all, it is moved so its first event is at `keep`.","zh":"第一次按鍵之前的東西會被剪掉，但不會遺失：靜默裡設定的控制器數值，例如延音踏板或旋鈕，會保留，並移到開頭（剪掉的範圍內的會變成 `t = 0`），所以 take 開始時的狀態不變。如果 take 裡完全沒有按鍵，就整體移動，讓它的第一個事件落在 `keep`。"}],"params":[{"name":"take","type":"Object","en":"The take.","zh":"這個 take。"},{"name":"keep","type":"Number","optional":true,"default":"0","en":"Milliseconds of lead-in to keep before the first key-down.","zh":"第一次按鍵之前要保留幾毫秒的空白。"}],"returns":{"type":"Object","en":"A new take. Everything moved earlier by the same amount, and `durationMs` is shorter by that amount. A take that starts at once comes back as it is.","zh":"一個新的 take。所有事件都提早同樣的量，`durationMs` 也縮短那麼多。一開始就有音的 take 會原樣回傳。"},"notes":[],"examples":[{"code":"import { trimSilence } from '@openav/record';\n\nconst take = { v: 1, name: 'riff', durationMs: 1500, events: [\n  { t: 500, name: 'midi/note/on', value: { note: 60, vel: 0.8, ch: 1 } },\n  { t: 900, name: 'midi/note/off', value: { note: 60, ch: 1 } },\n] };\nconst t = trimSilence(take, { keep: 100 });\nconsole.log(t.events.map((e) => e.t), t.durationMs);   // [100, 500] 1100","caption":{"en":"A take that starts half a second late is moved earlier so its first note comes at 100 ms; its length shrinks by the 400 ms that were cut.","zh":"一個晚了半秒才開始的 take 被往前移，讓第一個音出現在 100 毫秒。長度縮短被剪掉的 400 毫秒。"},"run":"module","lang":"js"}],"related":["quantizeTake","normalizeTake","TakeRecorder","takeStats"],"keywords":{"en":["trim","silence","cut","lead in","start of take","remove silence"],"zh":["修剪","靜默","剪掉","開頭","去掉空白"]},"tbd":[],"claims":[]},
{"id":"validateTake","key":"validateTake","name":"validateTake","aliases":[],"kind":"function","package":"@openav/record","category":"record","documented":true,"signature":"validateTake(take)","import":"import { validateTake } from '@openav/record';","source":{"file":"packages/record/take.js","line":54,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/take.js#L54"},"summary":{"en":"Checks a take and lists everything wrong with it as readable sentences. An empty list means it is valid.","zh":"檢查一個 take，把有問題的地方都列成看得懂的句子。空的清單表示沒問題。"},"description":[{"en":"It checks: `v` is [`TAKE_VERSION`](#TAKE_VERSION), `name` is a string, `durationMs` is a number from 0 up, `events` is an array, each event is an object with a signal `name` and a `t` of 0 or more, the events are in time order and none is after `durationMs`, and `meta`, if present, is an object. Use it on a take you read from a file; to repair one, use [`normalizeTake()`](#normalizeTake).","zh":"它檢查：`v` 是 [`TAKE_VERSION`](#TAKE_VERSION)，`name` 是字串，`durationMs` 是 0 以上的數字，`events` 是陣列，每個事件都是有訊號 `name` 和 0 以上的 `t` 的物件，事件依時間排序而且沒有任何一個超過 `durationMs`，`meta` 如果有的話是物件。用在從檔案讀來的 take 上。要修復的話，用 [`normalizeTake()`](#normalizeTake)。"}],"params":[{"name":"take","type":"any","en":"The take to check.","zh":"要檢查的 take。"}],"returns":{"type":"String[]","en":"Sentences such as `v must be 1 (got 2)` or `events[3] is out of order (t 100 < 200)`. `[]` when valid. It never throws.","zh":"像 `v must be 1 (got 2)` 或 `events[3] is out of order (t 100 < 200)` 這樣的句子。沒問題就是 `[]`。它不會丟出錯誤。"},"notes":[],"examples":[{"code":"import { validateTake } from '@openav/record';\n\nconst good = { v: 1, name: 'riff', durationMs: 1500, events: [{ t: 500, name: 'midi/note/on', value: { note: 60 } }] };\nconsole.log(validateTake(good));                  // []\nconsole.log(validateTake({ v: 2, events: [{ t: -1, name: '' }] }));\n// [\"v must be 1 (got 2)\", \"name must be a string\", \"durationMs must be a number ≥ 0\",\n//  \"events[0].name must be a signal name\", \"events[0].t must be ms ≥ 0\"]\nconsole.log(validateTake(null));                  // [\"not an object\"]","caption":{"en":"A valid take gives an empty list; a broken one lists every problem; something that is not even an object says so.","zh":"合格的 take 得到空清單。壞掉的會列出每個問題。根本不是物件的東西會直接這麼說。"},"run":"module","lang":"js"}],"related":["normalizeTake","TAKE_VERSION"],"keywords":{"en":["validate","check take","errors","lint","take format","problems"],"zh":["驗證","檢查 take","錯誤","take 格式","問題清單"]},"tbd":[],"claims":[]},
{"id":"VIDEO_MIMES","key":"VIDEO_MIMES","name":"VIDEO_MIMES","aliases":[],"kind":"constant","package":"@openav/record","category":"record","documented":true,"signature":"VIDEO_MIMES = array, 6 items","import":"import { VIDEO_MIMES } from '@openav/record';","source":{"file":"packages/record/mime.js","line":12,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/record/mime.js#L12"},"summary":{"en":"The video MIME types the recorder tries, best first: MP4 with H.264 and AAC, then WebM.","zh":"錄影器會依序嘗試的影片 MIME 型別，最好的在前：先是 H.264 加 AAC 的 MP4，再來是 WebM。"},"description":[{"en":"MP4 comes first because it opens everywhere a performance video goes next (an editor, Instagram, a phone's gallery) without a remux, and Chrome encodes H.264 in hardware. The first string asks for a profile (`avc1.640033`, High profile level 5.1, enough for 4K at 30 and 1080 by 1920 at 60) because Chrome says no to the generic `avc1,mp4a` but yes to the qualified one, so both are in the list for other browsers. Plain `video/mp4` is for Safari. [`pickMime()`](#pickMime) walks this list.","zh":"MP4 排第一，因為表演影片接下來要去的地方（剪輯軟體、Instagram、手機相簿）都能直接開，不用重新封裝，而且 Chrome 用硬體編 H.264。第一個字串指定了 profile（`avc1.640033`，High profile level 5.1，夠 4K 30 和 1080 乘 1920 60 用），因為 Chrome 對籠統的 `avc1,mp4a` 回答不行、對指定 profile 的卻回答可以，所以兩個都放進清單給別的瀏覽器用。單純的 `video/mp4` 是給 Safari 的。[`pickMime()`](#pickMime) 就是走過這份清單。"}],"value":{"type":"String[]","en":"A frozen list of 6 strings: `video/mp4;codecs=avc1.640033,mp4a.40.2`, `video/mp4;codecs=avc1,mp4a`, `video/webm;codecs=vp9,opus`, `video/webm;codecs=vp8,opus`, `video/webm`, `video/mp4`.","zh":"一份凍結的清單，有 6 個字串：`video/mp4;codecs=avc1.640033,mp4a.40.2`、`video/mp4;codecs=avc1,mp4a`、`video/webm;codecs=vp9,opus`、`video/webm;codecs=vp8,opus`、`video/webm`、`video/mp4`。"},"params":[],"notes":[],"examples":[{"code":"import { VIDEO_MIMES, pickMime } from '@openav/record';\n\nconsole.log(VIDEO_MIMES.length, VIDEO_MIMES[0]);   // 6 video/mp4;codecs=avc1.640033,mp4a.40.2\nconsole.log(pickMime((m) => MediaRecorder.isTypeSupported(m)) || '(the browser chooses)');","caption":{"en":"Logs the first candidate, and which one this browser would use.","zh":"印出第一個候選，以及這個瀏覽器實際會用哪一個。"},"run":"module","lang":"js"}],"related":["AUDIO_MIMES","pickMime","extFor","Recorder"],"keywords":{"en":["mime","mp4","webm","h264","codec","container","video format","candidates"],"zh":["mime","mp4","webm","h264","編碼","容器","影片格式","候選"]},"tbd":[],"claims":[]},
{"id":"option-clock","key":"option:clock","name":"clock","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_clock=follow | off","source":{"file":"p5/oav-p5.js","line":155,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L155"},"summary":{"en":"Whether an external MIDI clock (from a DAW, Strudel or a drum machine) moves `drumPhase`, `drumBpm` and the drum machine: `follow` or `off`.","zh":"外部的 MIDI 時鐘（來自 DAW、Strudel 或鼓機）要不要推動 `drumPhase`、`drumBpm` 和鼓機：`follow` 或 `off`。"},"description":[{"en":"With `follow`, an external MIDI clock (24 ticks a beat, plus start, stop and song position) is followed: [`drumPhase`](#drumPhase), [`beatPhase`](#beatPhase), [`drumBar`](#drumBar), [`drumStep`](#drumStep) and [`drumBpm`](#drumBpm) read it, and a drum machine that is up takes its tempo and its downbeat from it, starting and stopping with it. [`OAVp5.clock`](#OAVp5.clock) tells you what it hears. With nothing sending a clock, nothing changes.","zh":"設成 `follow` 的時候，外部的 MIDI 時鐘（每拍 24 個 tick，加上 start、stop 和 song position）會被跟隨：[`drumPhase`](#drumPhase)、[`beatPhase`](#beatPhase)、[`drumBar`](#drumBar)、[`drumStep`](#drumStep)、[`drumBpm`](#drumBpm) 讀的就是它，已經在跑的鼓機也會從它取得速度和第一拍，跟著它開始和停止。[`OAVp5.clock`](#OAVp5.clock) 會告訴你它聽到什麼。沒有東西送時鐘過來的時候，什麼都不會改變。"},{"en":"`off` ignores every clock message: `OAVp5.clock.info.state` stays `'none'`. Use it when something on the page or the machine sends a clock you do not want the sketch to follow.","zh":"`off` 會忽略所有時鐘訊息：`OAVp5.clock.info.state` 一直是 `'none'`。頁面上或機器上有東西在送時鐘、而你不想讓草圖跟著它走的時候用。"}],"value":{"type":"String","options":"follow | off","default":"follow","en":"Anything except `off` means `follow`.","zh":"`off` 以外的任何值都當作 `follow`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { clock: … };` above the loader line wins, then `?oav_clock=…` on the page address, then `?clock=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_clock=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { clock: … };` 優先，其次是網頁網址上的 `?oav_clock=…`，最後是 script 自己網址上的 `?clock=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_clock=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { clock: 'off' };\n\nfunction setup() {\n  createCanvas(400, 300);\n  OAVp5.input([0xFA]);                          // MIDI start\n  setInterval(() => OAVp5.input([0xF8]), 20);   // clock ticks: 24 a beat, here 50 a second\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('clock option: ' + OAVp5.options.clock, 20, 30);\n  text('external clock: ' + OAVp5.clock.info.state, 20, 60);   // none: the clock is ignored\n}","caption":{"en":"The sketch sends itself a made-up MIDI clock, and with `clock: 'off'` it is ignored: the state stays \"none\". Delete the first line and it reads \"running\".","zh":"草圖自己送出一個假的 MIDI 時鐘，設成 `clock: 'off'` 就被忽略：狀態一直是 \"none\"。把第一行刪掉，就會變成 \"running\"。"},"run":"sketch","lang":"js"}],"related":["OAVp5.clock","drumPhase","beatPhase","startDrums","option:link"],"keywords":{"en":["clock","midi clock","sync","tempo","external clock","follow","DAW","Strudel"],"zh":["時鐘","midi 時鐘","同步","速度","外部時鐘","跟隨","DAW"]},"tbd":[],"claims":[{"expr":"OAVp5.options.clock","equals":"follow"}]},
{"id":"option-compact","key":"option:compact","name":"compact","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_compact=on | off","source":{"file":"p5/oav-p5.js","line":152,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L152"},"summary":{"en":"Shows the 🎹 panel as a small box in the corner, a third of the window high, instead of a full-width bar: `on` or `off`.","zh":"把 🎹 面板顯示成角落的一個小方塊，高度是視窗的三分之一，而不是整條橫幅：`on` 或 `off`。"},"description":[{"en":"Normally the panel opened by 🎹 is a bar across the whole window, up to half its height. With `compact` it is a box at most 560 pixels wide and a third of the window high, in the corner the 🎹 sits in, so the page beside it stays in view. It is the default on strudel.cc, where the code editor needs the room.","zh":"一般來說，按 🎹 打開的面板是橫跨整個視窗的一條，最高到視窗的一半。設成 `compact` 就變成一個最寬 560 像素、高度是視窗三分之一的方塊，放在 🎹 所在的那個角落，旁邊的頁面還看得到。在 strudel.cc 上它是預設，因為程式編輯器需要那塊空間。"}],"value":{"type":"String","options":"on | off","default":"off (on in Strudel)","en":"Only `on` turns it on. `OAVp5.options.compact` reads back as `true` or `false`.","zh":"只有 `on` 會打開它。`OAVp5.options.compact` 讀回來是 `true` 或 `false`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { compact: … };` above the loader line wins, then `?oav_compact=…` on the page address, then `?compact=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_compact=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { compact: … };` 優先，其次是網頁網址上的 `?oav_compact=…`，最後是 script 自己網址上的 `?compact=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_compact=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { compact: 'on', panel: 'open' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('compact: ' + OAVp5.options.compact, 20, 30);   // true\n}","caption":{"en":"The panel opens at once as a box in the bottom-right corner instead of a bar across the whole window.","zh":"面板一開始就打開，是右下角的一個方塊，而不是橫跨整個視窗的一條。"},"run":"sketch","lang":"js"}],"related":["option:panel","option:strudel","option:corner","option:dock"],"keywords":{"en":["compact","small panel","corner box","strudel","panel size"],"zh":["精簡","小面板","角落","strudel","面板大小"]},"tbd":[],"claims":[{"expr":"OAVp5.options.compact","equals":false}]},
{"id":"option-corner","key":"option:corner","name":"corner","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_corner=right | left","source":{"file":"p5/oav-p5.js","line":141,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L141"},"summary":{"en":"Which side of the window the 🎹 button, its cards and the compact panel sit on: `right` or `left`.","zh":"🎹 按鈕、它的提示卡片和精簡面板放在視窗的哪一邊：`right` 或 `left`。"},"description":[{"en":"Moves the 🎹 to the left or right edge, with the cards that appear beside it. [`scope()`](#scope) draws on the opposite side, so the two never meet. On openprocessing.org the default is `left`, because OpenProcessing's own icon sits at the top right.","zh":"把 🎹 移到左邊或右邊，它旁邊出現的卡片也跟著移動。[`scope()`](#scope) 畫在對面那一側，所以兩個不會碰在一起。在 openprocessing.org 上預設是 `left`，因為 OpenProcessing 自己的圖示在右上角。"}],"value":{"type":"String","options":"right | left","default":"right (left on OpenProcessing)","en":"Anything except `left` means `right`.","zh":"`left` 以外的任何值都當作 `right`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { corner: … };` above the loader line wins, then `?oav_corner=…` on the page address, then `?corner=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_corner=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { corner: … };` 優先，其次是網頁網址上的 `?oav_corner=…`，最後是 script 自己網址上的 `?corner=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_corner=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { corner: 'left' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('corner: ' + OAVp5.options.corner + ' (look for the 🎹)', 20, 30);\n}","caption":{"en":"The 🎹 button sits in the bottom-left corner of the window instead of the bottom-right.","zh":"🎹 按鈕在視窗的左下角，不是右下角。"},"run":"sketch","lang":"js"}],"related":["option:dock","option:panel","option:compact","scope"],"keywords":{"en":["corner","left","right","position","move the button","layout"],"zh":["角落","左邊","右邊","位置","移動按鈕","版面"]},"tbd":[],"claims":[{"expr":"OAVp5.options.corner","equals":"right"}]},
{"id":"option-debug","key":"option:debug","name":"debug","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_debug=1 | 0","source":{"file":"p5/oav-p5.js","line":158,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L158"},"summary":{"en":"Prints `[oav-p5 debug]` lines in the console about clicks, the 🎹 button and loading, for finding out why the 🎹 does not respond.","zh":"在 console 印出 `[oav-p5 debug]` 開頭的幾行，關於點擊、🎹 按鈕和載入，用來查 🎹 為什麼沒反應。"},"description":[{"en":"Two seconds after the 🎹 is placed, a line says whether the 🎹 in the page is oav-p5's own copy and where oav-p5 loads the rest of OAV from. Every click then adds a line with the pointer position, what was clicked, and what is on top at that spot, which is how you find a page element sitting over the 🎹. It also says when the panel opens, when the page replaced the 🎹 and which name asked for the microphone.","zh":"🎹 放上頁面兩秒之後，會印出一行說頁面上的 🎹 是不是 oav-p5 自己的那一個，以及 oav-p5 從哪裡載入 OAV 的其他部分。之後每次點擊都多一行，記下指標的位置、點到了什麼、那個位置最上面是什麼，這就是找出是哪個頁面元素蓋在 🎹 上面的方法。面板打開、頁面把 🎹 換掉、是哪個名稱去要了麥克風，它也會說。"}],"value":{"type":"String","options":"1 | 0","default":"'' (off)","en":"Any value except an empty one and `0` turns it on.","zh":"除了空的和 `0`，任何值都會打開它。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { debug: … };` above the loader line wins, then `?oav_debug=…` on the page address, then `?debug=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_debug=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { debug: … };` 優先，其次是網頁網址上的 `?oav_debug=…`，最後是 script 自己網址上的 `?debug=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_debug=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { debug: 1 };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('open the console, then click the canvas', 20, 30);   // [oav-p5 debug] lines appear\n}","caption":{"en":"Open the console: two seconds after load a debug line talks about the 🎹, and every click on the canvas adds one saying what was under the pointer.","zh":"打開 console：載入兩秒之後會有一行 debug 說到 🎹，之後每點一下畫布就多一行，說指標底下是什麼。"},"run":"sketch","lang":"js"}],"related":["option:hints","option:panel","OAVp5.check"],"keywords":{"en":["debug","console","troubleshoot","button does not respond","trace","log"],"zh":["除錯","console","疑難排解","按鈕沒反應","追蹤","記錄"]},"tbd":[],"claims":[]},
{"id":"option-dock","key":"option:dock","name":"dock","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_dock=bottom | top","source":{"file":"p5/oav-p5.js","line":140,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L140"},"summary":{"en":"Which edge of the window the 🎹 button and its cards sit on, `bottom` or `top`; set it and the panel starts on that edge too.","zh":"🎹 按鈕和它的提示卡片放在視窗的哪個邊：`bottom` 或 `top`。有設定的話，面板一開始也在這個邊。"},"description":[{"en":"Moves the 🎹, its cards and the panel to the top or bottom edge. On openprocessing.org the default is `top`, because OpenProcessing's console floats over the bottom of the sketch. The drum machine's card sits below a panel docked at the top.","zh":"把 🎹、它的卡片和面板移到上緣或下緣。在 openprocessing.org 上預設是 `top`，因為 OpenProcessing 的 console 會浮在草圖的下方。鼓機的卡片會放在固定在上緣的面板下面。"},{"en":"The panel itself has two buttons for whoever is looking at it: **Dock** cycles bottom → top → floating (drag a floating panel by its bar), and **Size** cycles small → medium → large. This browser remembers the choice, and it wins over the option. When the sketch sets no `dock`, the panel starts at the bottom, also on openprocessing.org (from 2026-10-20: at the top it covered the sketch). The 🎹 moves out of the panel's way only when the two share an edge. Reloading the same page (Cmd+Enter on OpenProcessing) opens the panel again if it was open, and restarts drums started from its 🥁 on the same line and tempo.","zh":"面板上有兩個按鈕給正在看的人用：「位置」依序切換下方、上方、浮動（浮動時拖曳面板上面那一列就能移動），「大小」依序切換小、中、大。這個瀏覽器會記住你的選擇，而且優先於這個選項。草圖沒有設定 `dock` 時，面板一開始在下方，openprocessing.org 上也一樣（2026-10-20 起，之前放在上方會蓋住草圖）。只有面板和 🎹 在同一個邊時，🎹 才會讓開。同一個頁面重新整理（OpenProcessing 按 Cmd+Enter）時，面板原本開著就會自動打開，從面板 🥁 開的鼓機也會用同一行節奏和速度再打起來。"}],"value":{"type":"String","options":"bottom | top","default":"bottom (top on OpenProcessing)","en":"Anything except `top` means `bottom`.","zh":"`top` 以外的任何值都當作 `bottom`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { dock: … };` above the loader line wins, then `?oav_dock=…` on the page address, then `?dock=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_dock=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { dock: … };` 優先，其次是網頁網址上的 `?oav_dock=…`，最後是 script 自己網址上的 `?dock=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_dock=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { dock: 'top' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('dock: ' + OAVp5.options.dock + ' (look for the 🎹)', 20, 30);\n}","caption":{"en":"The 🎹 button sits at the top-right of the window instead of the bottom-right.","zh":"🎹 按鈕在視窗的右上角，不是右下角。"},"run":"sketch","lang":"js"}],"related":["option:corner","option:panel","option:compact"],"keywords":{"en":["dock","top","bottom","position","move the button","layout","openprocessing console"],"zh":["停靠","上方","下方","位置","移動按鈕","版面","openprocessing console"]},"tbd":[],"claims":[{"expr":"OAVp5.options.dock","equals":"bottom"}]},
{"id":"option-drumlatency","key":"option:drumlatency","name":"drumlatency","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_drumlatency=auto | 0 to 500 (milliseconds)","source":{"file":"p5/oav-p5.js","line":135,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L135"},"summary":{"en":"How long it takes a drum hit to be heard after oav-p5 hands it to the audio clock, so pictures land with the sound: `auto` or milliseconds.","zh":"鼓點從 oav-p5 交給音訊時鐘到你聽到要多久，讓畫面跟聲音同時到：`auto` 或毫秒數。"},"description":[{"en":"The drum machine hands each hit to the audio clock slightly ahead of time, and tells the sketch ([`drumHit()`](#drumHit), [`kickDetected()`](#kickDetected), [`env('kick')`](#env)) when you will hear it, which is the scheduled moment plus the output latency. `auto` uses the latency the browser reports. If the browser's figure is smaller than what your output really takes, the picture runs ahead of the sound, and giving the number yourself fixes that. The project's README names a Bluetooth speaker as the case and suggests trying 200.","zh":"鼓機把每一下鼓點稍微提前交給音訊時鐘，並在你會聽到的那一刻通知草圖（[`drumHit()`](#drumHit)、[`kickDetected()`](#kickDetected)、[`env('kick')`](#env)），那一刻就是排定的時間加上輸出的延遲。`auto` 用瀏覽器回報的延遲。瀏覽器回報的數字如果比你的輸出實際花的時間小，畫面就會跑在聲音前面，自己給一個數字就能解決。專案的 README 舉的例子是藍牙喇叭，並建議試試 200。"}],"value":{"type":"String | Number","options":"auto | 0 to 500 (milliseconds)","default":"auto","en":"A number is held between 0 and 500. Anything that is not a number means `auto`. `OAVp5.options.drumlatency` reads back in seconds (`0.2` for 200).","zh":"數字會被限制在 0 到 500 之間。不是數字的值都當作 `auto`。`OAVp5.options.drumlatency` 讀回來的單位是秒（200 讀回 `0.2`）。"},"params":[],"notes":[{"en":"Only the default drum machine (`drums=lookahead`) uses it: the frame-driven one has nothing scheduled ahead.","zh":"只有預設的鼓機（`drums=lookahead`）會用到它：逐幀推進的那一種沒有提前排程的東西。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { drumlatency: … };` above the loader line wins, then `?oav_drumlatency=…` on the page address, then `?drumlatency=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_drumlatency=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { drumlatency: … };` 優先，其次是網頁網址上的 `?oav_drumlatency=…`，最後是 script 自己網址上的 `?drumlatency=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_drumlatency=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { drumlatency: 200 };\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, [~ sd]*2, hh*8', 108);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('drumlatency: ' + OAVp5.options.drumlatency + ' s', 20, 30);\n  circle(width / 2, height / 2, 50 + env('kick', 0.01, 0.3) * 200);   // told 0.2 s after the hit is scheduled\n}","caption":{"en":"The circle swells on each kick, told 200 milliseconds after the hit was handed to the audio clock, to match a speaker that is that late. Click the canvas once if you hear nothing.","zh":"圓在每一下大鼓脹大，通知的時間是鼓點交給音訊時鐘 200 毫秒之後，配合一個正好慢那麼多的喇叭。如果沒聽到聲音，點一下畫布。"},"run":"sketch","lang":"js"}],"related":["option:drums","startDrums","drumHit","env"],"keywords":{"en":["drum latency","latency","bluetooth","audio delay","sync picture and sound","lip sync"],"zh":["鼓機延遲","延遲","藍牙","聲音延遲","畫面聲音同步","對嘴"]},"tbd":[{"en":"No Bluetooth speaker was tried: the suggested 200 comes from the project's README, not from a measurement made for this page.","zh":"沒有用藍牙喇叭試過：建議的 200 來自專案的 README，不是為這一頁量過的。"}],"claims":[{"expr":"OAVp5.options.drumlatency","equals":"auto"}]},
{"id":"option-drums","key":"option:drums","name":"drums","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_drums=lookahead | frame","source":{"file":"p5/oav-p5.js","line":134,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L134"},"summary":{"en":"How the drum machine is clocked: `lookahead` (hits scheduled ahead on the audio clock) or `frame` (moved by animation frames, 0.5's way).","zh":"鼓機用什麼來計時：`lookahead`（提前在音訊時鐘上排程）或 `frame`（跟著動畫畫面走，0.5 版的做法）。"},"description":[{"en":"With `lookahead`, a heartbeat in a Web Worker hands the hits to the audio clock a little ahead of time, so the beat stays steady when frames stutter or the tab is busy, and the sketch is told when each hit is heard. With `frame`, the machine is moved by the animation frames instead, as it was in oav-p5 0.5. It is the safety net: where the lookahead machine misbehaves, `frame` brings the old behaviour back, and the timing is then only as steady as the frame rate.","zh":"設成 `lookahead` 的時候，Web Worker 裡有一個心跳，會把鼓點提前一點交給音訊時鐘，所以畫面卡頓或分頁很忙的時候，拍子還是穩的，草圖也會在每一下鼓點被聽到的時候收到通知。設成 `frame` 則是回到 oav-p5 0.5 的做法，由動畫畫面推著鼓機走。這是保險：提前排程的鼓機在某個地方出狀況的時候，`frame` 把舊的做法找回來，這時拍子只會跟畫面更新率一樣穩。"}],"value":{"type":"String","options":"lookahead | frame","default":"lookahead","en":"Anything except `frame` means `lookahead`.","zh":"`frame` 以外的任何值都當作 `lookahead`。"},"params":[],"notes":[{"en":"`OAVp5.drums.clock` and the 🩺 check-up say which clock is running.","zh":"`OAVp5.drums.clock` 和 🩺 體檢會告訴你現在是哪一種時鐘在跑。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { drums: … };` above the loader line wins, then `?oav_drums=…` on the page address, then `?drums=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_drums=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { drums: … };` 優先，其次是網頁網址上的 `?oav_drums=…`，最後是 script 自己網址上的 `?drums=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_drums=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { drums: 'frame' };\n\nfunction setup() {\n  createCanvas(400, 300);\n  startDrums('bd*4, [~ sd]*2, hh*8', 108);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('drums: ' + OAVp5.options.drums + ' (clock: ' + OAVp5.drums.clock.clock + ')', 20, 30);\n  circle(width / 2, height / 2, 50 + env('kick', 0.01, 0.3) * 200);\n}","caption":{"en":"The drum machine is moved by the animation frames, and the text says so. Click the canvas once if you hear nothing.","zh":"鼓機由動畫畫面推著走，文字也這麼說。如果沒聽到聲音，點一下畫布。"},"run":"sketch","lang":"js"}],"related":["option:drumlatency","startDrums","OAVp5.check"],"keywords":{"en":["drums clock","lookahead","frame","timing","safety net","worker","drum machine"],"zh":["鼓機時鐘","提前排程","逐幀","節拍準確","保險","worker","鼓機"]},"tbd":[],"claims":[{"expr":"OAVp5.options.drums","equals":"lookahead"}]},
{"id":"option-hardware","key":"option:hardware","name":"hardware","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_hardware=auto | click | off","source":{"file":"p5/oav-p5.js","line":126,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L126"},"summary":{"en":"When oav-p5 asks the browser for Web MIDI, which a real controller needs: `auto` at load, `click` when the 🎹 panel opens, or `off`.","zh":"oav-p5 什麼時候向瀏覽器要 Web MIDI（真的控制器需要它）：`auto` 是載入時，`click` 是 🎹 面板打開時，`off` 是不要。"},"description":[{"en":"With `auto`, oav-p5 asks for Web MIDI as soon as it loads, so a plugged-in controller plays the sketch without anyone pressing anything. The browser remembers your answer per site. With `click`, it asks only when the 🎹 panel is first opened. With `off`, it never asks by itself, and the on-screen controller and a phone still play the sketch.","zh":"設成 `auto` 的時候，oav-p5 一載入就去要 Web MIDI，所以接上去的控制器不用誰按什麼就能彈草圖。瀏覽器會依網站記住你的答案。設成 `click`，只有第一次打開 🎹 面板時才去要。設成 `off`，它自己永遠不會去要，螢幕上的控制器和手機還是能彈草圖。"},{"en":"[`OAVp5.connectHardware()`](#OAVp5.connectHardware) asks whatever the option says, and [`OAVp5.hardware`](#OAVp5.hardware) tells you the state: `'off'` until it has been asked.","zh":"不管這個選項怎麼設，[`OAVp5.connectHardware()`](#OAVp5.connectHardware) 都會去要，[`OAVp5.hardware`](#OAVp5.hardware) 告訴你現在的狀態：還沒要過之前是 `'off'`。"}],"value":{"type":"String","options":"auto | click | off","default":"auto (click in Strudel)","en":"Only `auto` and `click` ask. Any other value acts like `off`.","zh":"只有 `auto` 和 `click` 會去要。其他的值效果跟 `off` 一樣。"},"params":[],"notes":[{"en":"On strudel.cc (or a page where Strudel's own functions are present) the default is `click`, because Strudel's `midin()` asks for MIDI itself. Setting `hardware` yourself wins.","zh":"在 strudel.cc（或頁面上本來就有 Strudel 自己的函式）預設是 `click`，因為 Strudel 的 `midin()` 會自己去要 MIDI。你自己設了 `hardware` 就以你的為準。"},{"en":"Safari and iPad have no Web MIDI at all: when oav-p5 asks (or you call `OAVp5.connectHardware()`), the state there becomes `'unavailable'`.","zh":"Safari 和 iPad 根本沒有 Web MIDI：oav-p5 去要的時候（或你呼叫 `OAVp5.connectHardware()`），那裡的狀態就會變成 `'unavailable'`。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { hardware: … };` above the loader line wins, then `?oav_hardware=…` on the page address, then `?hardware=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_hardware=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { hardware: … };` 優先，其次是網頁網址上的 `?oav_hardware=…`，最後是 script 自己網址上的 `?hardware=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_hardware=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { hardware: 'click' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('hardware option: ' + OAVp5.options.hardware, 20, 30);\n  text('Web MIDI state: ' + OAVp5.hardware, 20, 60);   // stays \"off\" until you open the 🎹 panel\n}","caption":{"en":"The page does not ask for Web MIDI when it loads, and the state stays \"off\". Press 🎹: that opens the panel and asks. Delete the first line and it asks at once.","zh":"載入的時候頁面不會去要 Web MIDI，狀態一直是 \"off\"。按 🎹：這會打開面板並去要。把第一行刪掉，就會一載入就去要。"},"run":"sketch","lang":"js"}],"related":["OAVp5.hardware","OAVp5.connectHardware","OAVp5.devices","option:panel","option:strudel"],"keywords":{"en":["hardware","web midi","midi permission","ask for midi","controller","usb midi"],"zh":["硬體","web midi","midi 權限","要 midi","控制器","usb midi"]},"tbd":[],"claims":[{"expr":"OAVp5.options.hardware","equals":"auto"}]},
{"id":"option-hints","key":"option:hints","name":"hints","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_hints=on | off","source":{"file":"p5/oav-p5.js","line":1076,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1076"},"summary":{"en":"Whether oav-p5 shows its explaining cards beside the 🎹 (blocked microphone, no Web MIDI, an error in your callback): `on` or `off`.","zh":"oav-p5 要不要在 🎹 旁邊顯示說明的卡片（麥克風被擋、沒有 Web MIDI、你的回呼出錯）：`on` 或 `off`。"},"description":[{"en":"When something goes wrong, a small card beside the 🎹 says what happened, why, and what to press, in the browser's language, once per page. `hints: 'off'` takes the cards off the page: the console still says the same thing and [`OAVp5.hints`](#OAVp5.hints) still lists what was said. Use it for a finished piece that goes on a wall or into a recording.","zh":"出問題的時候，🎹 旁邊會跳出一張小卡片，用瀏覽器的語言說明發生了什麼、為什麼、要按什麼，每個頁面只說一次。`hints: 'off'` 讓卡片從頁面上消失：console 還是說一樣的話，[`OAVp5.hints`](#OAVp5.hints) 也還是列出說過的內容。做好的作品要放在展場牆上或錄影的時候用。"},{"en":"It also hides the corner oscilloscope of [`scope()`](#scope), which is part of the same on-page help. `scope=off` hides only that.","zh":"它也會把 [`scope()`](#scope) 的角落示波器藏起來，因為那是同一套網頁上的輔助。`scope=off` 只藏它。"}],"value":{"type":"String","options":"on | off","default":"on","en":"Only `off` turns the cards off. Any other value, including the text `false`, leaves them on.","zh":"只有 `off` 會把卡片關掉。其他任何值，包括文字 `false`，都讓卡片保持開著。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { hints: … };` above the loader line wins, then `?oav_hints=…` on the page address, then `?hints=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_hints=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { hints: … };` 優先，其次是網頁網址上的 `?oav_hints=…`，最後是 script 自己網址上的 `?hints=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_hints=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { hints: 'off' };\n\nfunction setup() {\n  createCanvas(400, 300);\n  lag(1);   // an unnamed lag() outside draw(): oav-p5 has something to say about that\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('cards said: ' + OAVp5.hints.length + ' (none on the page)', 20, 30);\n  text(OAVp5.hints.map((h) => h.what).join('\\n'), 20, 60);\n}","caption":{"en":"oav-p5 notices the lag() in setup() and would normally show a card. With hints off nothing appears on the page, but the console and the list in OAVp5.hints still have it.","zh":"oav-p5 注意到 setup() 裡的 lag()，平常會顯示一張卡片。設成 hints off，頁面上什麼都不會出現，但 console 和 OAVp5.hints 的清單裡還是有。"},"run":"sketch","lang":"js"},{"code":"window.OAV_P5 = { hints: false };   // not 'off': the value is turned into the text \"false\"\n\nfunction setup() {\n  createCanvas(400, 300);\n  lag(1);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('the card is still beside the 🎹', 20, 30);\n}","caption":{"en":"The gotcha: `false` is not `'off'`, so the card still appears.","zh":"常見的陷阱：`false` 不等於 `'off'`，所以卡片還是會出現。"},"run":"sketch","lang":"js"}],"related":["OAVp5.hints","option:scope","option:debug","OAVp5.check"],"keywords":{"en":["hints","cards","messages","quiet","hide the cards","exhibition","warnings"],"zh":["提示","卡片","訊息","安靜","藏起卡片","展場","警告"]},"tbd":[],"claims":[{"before":"lag(1)","expr":"document.querySelectorAll('.oav-p5-hint').length","equals":1}]},
{"id":"option-link","key":"option:link","name":"link","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_link=a room name","source":{"file":"p5/oav-p5.js","line":156,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L156"},"summary":{"en":"Joins a room: oav-p5 pages with the same room hear what each other's programs play, over the relay. A room name, or empty for none.","zh":"加入一個房間：房間相同的 oav-p5 頁面，會透過中繼聽到彼此的程式送出的聲音。填房間名，空的就是不加入。"},"description":[{"en":"Every oav-p5 page with the same room (another tab, another computer) gets what this page's programs play into the virtual MIDI output (notes, controllers and the clock), over the phone relay, and plays it at the moment it sounds here. On the other side, `OAVp5.last.source` is `'link'`. Nothing it receives is sent on, so two pages cannot loop. The room is also the phones' room, so one QR code plays every linked page.","zh":"房間相同的 oav-p5 頁面（另一個分頁、另一台電腦），會透過手機中繼收到這個頁面的程式送進虛擬 MIDI 輸出的東西（音符、控制器和時鐘），並在這裡聽到的那一刻播出來。收到的那一邊，`OAVp5.last.source` 是 `'link'`。收到的東西不會再往外送，所以兩個頁面不會互相繞圈。房間也是手機用的房間，所以一個 QR code 就能彈每一個連起來的頁面。"},{"en":"[`OAVp5.link.info`](#OAVp5.link.info) tells you the room, whether it is connected and how many other pages were heard. The 🎹 grows a 🔗: green when another page is there, yellow while connecting or alone, red when the relay is gone. [`linkdelay`](#option-linkdelay) adds a buffer.","zh":"[`OAVp5.link.info`](#OAVp5.link.info) 告訴你房間、有沒有連上、聽到幾個其他頁面。🎹 會多一個 🔗：另一個頁面在的時候是綠色，連線中或只有自己是黃色，中繼斷線是紅色。[`linkdelay`](#option-linkdelay) 可以多加一段緩衝。"}],"value":{"type":"String","options":"a room name","default":"'' (no room)","en":"Only letters, digits, `_` and `-` are kept, and only the first 32 characters.","zh":"只留下字母、數字、`_` 和 `-`，而且只取前 32 個字元。"},"params":[],"needs":{"en":"A network connection to the relay ([`relay`](#option-relay), `wss://remote.cheyuwu.com/relay` unless you set another). Without it the 🔗 is red and nothing is shared.","zh":"要能連上中繼（[`relay`](#option-relay)，沒另外設的話是 `wss://remote.cheyuwu.com/relay`）。連不上的話 🔗 是紅色，什麼都不會分享。"},"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { link: … };` above the loader line wins, then `?oav_link=…` on the page address, then `?link=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_link=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { link: … };` 優先，其次是網頁網址上的 `?oav_link=…`，最後是 script 自己網址上的 `?link=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_link=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { link: 'my-room' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const info = OAVp5.link.info;\n  text('room: ' + OAVp5.options.link, 20, 30);\n  text('state: ' + (info ? info.state + ', ' + info.peers + ' other page(s)' : 'not linked'), 20, 60);\n}","caption":{"en":"Open the same address in two tabs: each shows the room and, once connected, one other page. Anything one tab's program plays into OAV Virtual MIDI is heard by the other.","zh":"在兩個分頁打開同一個網址：兩邊都顯示房間，連上之後顯示有另一個頁面。一個分頁的程式送進 OAV Virtual MIDI 的東西，另一個分頁也聽得到。"},"run":"static","lang":"js"}],"related":["OAVp5.link","OAVp5.link.info","option:linkdelay","option:relay","option:room"],"keywords":{"en":["link","room","sync pages","two computers","relay","share midi","strudel"],"zh":["連結","房間","同步頁面","兩台電腦","中繼","分享 midi","strudel"]},"tbd":[{"en":"The headless check cannot reach a relay, so this example is not run. Behaviour between two pages was read from the code and the project's README, not seen here.","zh":"無頭瀏覽器的檢查連不到中繼，所以這個範例沒有跑。兩個頁面之間的行為是讀程式碼和專案的 README 得知的，這裡沒有親眼看過。"}],"claims":[{"expr":"OAVp5.options.link","equals":""}]},
{"id":"option-linkdelay","key":"option:linkdelay","name":"linkdelay","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_linkdelay=0 to 2000 (milliseconds)","source":{"file":"p5/oav-p5.js","line":157,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L157"},"summary":{"en":"Extra milliseconds of buffer on the notes a linked page receives, to smooth out an uneven network: 0 to 2000.","zh":"連在一起的頁面收到音符時多加的緩衝毫秒數，用來撫平不穩的網路：0 到 2000。"},"description":[{"en":"Only matters together with [`link`](#option-link): without a room nothing is received. Each note arriving from another page is played at the moment it sounded there, which already allows for the trip. `linkdelay` adds this much on top, so a note that took longer than usual is still on time. The price is that everything from the other page arrives that much later.","zh":"只有跟 [`link`](#option-link) 一起用才有意義：沒有房間就不會收到任何東西。從另一個頁面來的每個音符，會在它在那邊響起的那一刻播出，這已經把路上花的時間算進去了。`linkdelay` 在那之上再多加這麼多，所以走得比平常久的音符也還是準時。代價是另一個頁面來的所有東西都晚這麼多才到。"}],"value":{"type":"Number","options":"0 to 2000 (milliseconds)","default":"0","en":"Held between 0 and 2000. Anything that is not a number is 0.","zh":"會被限制在 0 到 2000 之間。不是數字的值就是 0。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { linkdelay: … };` above the loader line wins, then `?oav_linkdelay=…` on the page address, then `?linkdelay=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_linkdelay=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { linkdelay: … };` 優先，其次是網頁網址上的 `?oav_linkdelay=…`，最後是 script 自己網址上的 `?linkdelay=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_linkdelay=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { linkdelay: 80 };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('linkdelay: ' + OAVp5.options.linkdelay + ' ms', 20, 30);\n  text('link: ' + (OAVp5.link.info ? 'room ' + OAVp5.link.info.room : 'no room, so nothing to delay'), 20, 60);   // add link: 'my-room' to use it\n}","caption":{"en":"Without a `link` room the number is only stored: the sketch says there is nothing to delay. Add `link: 'my-room'` to the options and it applies to every note that arrives.","zh":"沒有 `link` 房間的時候，這個數字只是存著：草圖說沒有東西可以延遲。在選項裡加上 `link: 'my-room'`，就會套用到每個收到的音符。"},"run":"sketch","lang":"js"}],"related":["option:link","OAVp5.link.info","option:relay"],"keywords":{"en":["link delay","buffer","network jitter","late notes","smooth"],"zh":["連結延遲","緩衝","網路抖動","晚到的音符","平順"]},"tbd":[],"claims":[{"expr":"OAVp5.options.linkdelay","equals":0}]},
{"id":"option-mic","key":"option:mic","name":"mic","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_mic=auto | click | off","source":{"file":"p5/oav-p5.js","line":128,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L128"},"summary":{"en":"Who may turn the microphone on: `auto` (the first read of `micLevel` and friends), `click` (only 🎤 or `startMic()`) or `off` (nobody).","zh":"誰可以把麥克風打開：`auto`（第一次讀 `micLevel` 這些）、`click`（只有 🎤 或 `startMic()`）或 `off`（誰都不行）。"},"description":[{"en":"With `auto`, the first time a sketch reads [`micLevel`](#micLevel), [`bass`](#bass), `mid`, `treble` or [`beat`](#beat), oav-p5 asks the browser for the microphone. With `click`, reading them does nothing: the microphone starts only when someone presses 🎤 in the 🎹 panel or the sketch calls [`startMic()`](#startMic), so the permission prompt arrives when you choose. With `off`, nothing opens the microphone, not even `startMic()`, which resolves `false`.","zh":"設成 `auto`，草圖第一次讀 [`micLevel`](#micLevel)、[`bass`](#bass)、`mid`、`treble` 或 [`beat`](#beat) 的時候，oav-p5 就會向瀏覽器要麥克風。設成 `click`，讀它們不會有任何作用：只有有人在 🎹 面板按 🎤、或草圖呼叫 [`startMic()`](#startMic) 時麥克風才會開，這樣權限的詢問會在你決定的時候才跳出來。設成 `off`，什麼都不能開麥克風，連 `startMic()` 也不行，它會 resolve 成 `false`。"},{"en":"`off` is a promise to the page: a gallery piece that must never listen can say so. `click` is for pieces where a permission prompt out of nowhere would be rude, or where the sound only matters after an intro. The values of the microphone stay `0` while it is off.","zh":"`off` 是對頁面的一個承諾：一件絕對不能收音的展場作品可以這樣宣告。`click` 適合不該憑空跳出權限詢問的作品，或是聲音要等片頭之後才有意義的作品。麥克風關著的時候，它的所有值都是 `0`。"}],"value":{"type":"String","options":"auto | click | off","default":"auto","en":"Anything else means `auto`.","zh":"其他的值都當作 `auto`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { mic: … };` above the loader line wins, then `?oav_mic=…` on the page address, then `?mic=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_mic=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { mic: … };` 優先，其次是網頁網址上的 `?oav_mic=…`，最後是 script 自己網址上的 `?mic=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_mic=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { mic: 'click' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  startMic();   // now the browser asks\n}\n\nfunction draw() {\n  background(20);\n  circle(width / 2, height / 2, 50 + micLevel * 300);   // reading micLevel no longer asks\n  fill(255);\n  text('microphone: ' + OAVp5.mic.state + ' (click the canvas to start it)', 20, 30);\n}","caption":{"en":"Reading micLevel does not ask for the microphone: the state stays \"off\" and the circle stays small. Click the canvas and the browser asks.","zh":"讀 micLevel 不會去要麥克風：狀態一直是 \"off\"，圓一直很小。點一下畫布，瀏覽器才會詢問。"},"run":"sketch","lang":"js"},{"code":"window.OAV_P5 = { mic: 'off' };\n\nlet answer = 'click the canvas to try';\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  startMic().then((ok) => { answer = 'startMic() resolved ' + ok; });\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('microphone: ' + OAVp5.mic.state, 20, 30);\n  text(answer, 20, 60);   // startMic() resolved false\n}","caption":{"en":"Nothing can open the microphone: the state stays \"off\" and startMic() resolves false.","zh":"什麼都不能開麥克風：狀態一直是 \"off\"，`startMic()` resolve 成 false。"},"run":"sketch","lang":"js"}],"related":["micLevel","startMic","stopMic","OAVp5.mic.state","micIsOn"],"keywords":{"en":["mic option","mic permission","ask for the mic","no mic","privacy","click to start"],"zh":["收音選項","收音權限","要不要收音","不收音","隱私","點了才開"]},"tbd":[{"en":"Not tried on openprocessing.org itself: that `?sketch_oav_mic=click` on the page address reaches the sketch.","zh":"還沒有在 openprocessing.org 本站實測：頁面網址上的 `?sketch_oav_mic=click` 有沒有傳到草圖。"}],"claims":[{"expr":"OAVp5.options.mic","equals":"auto"}]},
{"id":"option-opc","key":"option:opc","name":"opc","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_opc=on | off","source":{"file":"p5/oav-p5.js","line":127,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L127"},"summary":{"en":"Whether oav-p5 wires the controller to the variables of OP Configurator (OpenProcessing's sliders and buttons): `on` or `off`.","zh":"oav-p5 要不要把控制器接到 OP Configurator（OpenProcessing 的滑桿和按鈕）的變數上：`on` 或 `off`。"},"description":[{"en":"On OpenProcessing, `OPC.slider('radius', 10)` declares a variable with a slider. With `opc` on, oav-p5 then wires the controller to the OPC variables in order: knob 1 to the first slider, knob 2 to the second, the faders after the knobs, pad 1 to the first toggle or button. OPC's own panel moves with the knob. [`OAVp5.wiring()`](#OAVp5.wiring) lists who drives what.","zh":"在 OpenProcessing 上，`OPC.slider('radius', 10)` 宣告一個有滑桿的變數。`opc` 開著的時候，oav-p5 會依序把控制器接到 OPC 的變數：旋鈕 1 接第一個滑桿，旋鈕 2 接第二個，推桿接在旋鈕之後，打擊墊 1 接第一個開關或按鈕。OPC 自己的面板會跟著旋鈕動。[`OAVp5.wiring()`](#OAVp5.wiring) 會列出誰推動什麼。"},{"en":"`off` leaves OPC's variables to their own sliders. A control the sketch reads itself (`knob1` in `draw()`) is the sketch's anyway: the slider at that place stops following it.","zh":"`off` 讓 OPC 的變數只聽自己的滑桿。草圖自己有讀的控制（`draw()` 裡的 `knob1`）本來就歸草圖：那個位置的滑桿不再跟著它動。"}],"value":{"type":"String","options":"on | off","default":"on","en":"Only `off` turns it off.","zh":"只有 `off` 會把它關掉。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { opc: … };` above the loader line wins, then `?oav_opc=…` on the page address, then `?opc=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_opc=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { opc: … };` 優先，其次是網頁網址上的 `?oav_opc=…`，最後是 script 自己網址上的 `?opc=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_opc=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { opc: 'off' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('opc option: ' + OAVp5.options.opc, 20, 30);\n  text('controls wired to OPC variables: ' + OAVp5.wiring().length, 20, 60);   // 0\n}","caption":{"en":"Outside OpenProcessing there is no OPC, so the count is 0 either way. On OpenProcessing with sliders declared, it is 0 with `opc: 'off'` and the number of sliders and buttons without it.","zh":"在 OpenProcessing 以外沒有 OPC，所以不管怎麼設都是 0。在 OpenProcessing 上宣告了滑桿的話，設成 `opc: 'off'` 是 0，沒設就是滑桿和按鈕的數量。"},"run":"sketch","lang":"js"}],"related":["OAVp5.wiring","knob1","knob","pad1"],"keywords":{"en":["opc","op configurator","openprocessing sliders","wire controller to sliders","map knobs"],"zh":["opc","op configurator","openprocessing 滑桿","控制器接滑桿","對應旋鈕"]},"tbd":[{"en":"Not tried with OpenProcessing's real OPC library. The wiring was checked against a stand-in object with the same `options` shape, and the example here runs where no OPC exists, so it only shows the option arriving.","zh":"沒有用 OpenProcessing 真正的 OPC 函式庫試過。接線是拿一個有同樣 `options` 結構的替身物件檢查的，這裡的範例在沒有 OPC 的地方跑，所以只能看到選項有送達。"}],"claims":[{"expr":"OAVp5.options.opc","equals":"on"},{"before":"window.OPC = { options: { radius: { type: 'slider', name: 'radius', min: 0, max: 100, value: 10 } }, set() {} }","expr":"JSON.stringify(OAVp5.wiring())","equals":"[{\"control\":\"knob1\",\"name\":\"radius\"}]"}]},
{"id":"option-panel","key":"option:panel","name":"panel","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_panel=closed | open | off","source":{"file":"p5/oav-p5.js","line":126,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L126"},"summary":{"en":"Whether the 🎹 panel starts shut, open, or does not exist: `closed`, `open` or `off`.","zh":"🎹 面板一開始是收起來、打開，還是根本沒有：`closed`、`open` 或 `off`。"},"description":[{"en":"`closed` shows just the small 🎹 button, and pressing it opens the panel: the on-screen controller, the buttons for 🎤, 🔊, 🥁, ⏺, 📷, 📱 and 🩺. `open` opens the panel as soon as the page loads, for a workshop where everyone needs the on-screen controller at once. `off` removes the 🎹 button altogether, for a piece in an exhibition: there is no button and [`OAVp5.panel.open()`](#OAVp5.panel.open) does nothing. The cards that explain a problem still appear in the corner, unless [`hints`](#option-hints) is `off` too.","zh":"`closed` 只顯示小小的 🎹 按鈕，按下去才打開面板：螢幕上的控制器，加上 🎤、🔊、🥁、⏺、📷、📱、🩺 這些按鈕。`open` 在頁面一載入就打開面板，適合工作坊，每個人一開始就需要螢幕上的控制器。`off` 把 🎹 按鈕整個拿掉，適合展場裡的作品：沒有按鈕，[`OAVp5.panel.open()`](#OAVp5.panel.open) 也什麼都不做。說明問題的卡片還是會出現在角落，除非 [`hints`](#option-hints) 也設成 `off`。"}],"value":{"type":"String","options":"closed | open | off","default":"closed","en":"Any value except `open` and `off` means `closed`.","zh":"`open` 和 `off` 以外的任何值都當作 `closed`。"},"params":[],"notes":[{"en":"With `off` everything the panel does is still there as code: `startMic()`, `startSound()`, `startRecording()` and the options keep working, and the console still says what goes wrong.","zh":"設成 `off` 之後，面板能做的事還是都能用程式做：`startMic()`、`startSound()`、`startRecording()` 和各個選項照常運作，出問題時 console 也還是會說。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { panel: … };` above the loader line wins, then `?oav_panel=…` on the page address, then `?panel=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_panel=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { panel: … };` 優先，其次是網頁網址上的 `?oav_panel=…`，最後是 script 自己網址上的 `?panel=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_panel=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { panel: 'open' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('panel: ' + OAVp5.options.panel, 20, 30);\n}","caption":{"en":"The 🎹 panel is open as soon as the page loads: the on-screen controller and its buttons.","zh":"頁面一載入，🎹 面板就是打開的：螢幕上的控制器和它的按鈕。"},"run":"sketch","lang":"js"},{"code":"window.OAV_P5 = { panel: 'off' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('panel: ' + OAVp5.options.panel + ' (no 🎹 on this page)', 20, 30);\n}","caption":{"en":"There is no 🎹 button at all. The sketch still hears a real controller and everything else.","zh":"完全沒有 🎹 按鈕。草圖還是聽得到真的控制器和其他一切。"},"run":"sketch","lang":"js"}],"related":["OAVp5.panel","OAVp5.panel.open","option:compact","option:dock","option:corner"],"keywords":{"en":["panel","open the panel","hide the button","exhibition mode","on-screen controller","piano button"],"zh":["面板","打開面板","藏起按鈕","展場模式","螢幕控制器","鋼琴按鈕"]},"tbd":[],"claims":[{"expr":"OAVp5.options.panel","equals":"closed"}]},
{"id":"option-profile","key":"option:profile","name":"profile","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_profile=a profile id","source":{"file":"p5/oav-p5.js","line":126,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L126"},"summary":{"en":"Which controller the on-screen 🎹 shows, by profile id such as `akai-lpd8`; by default it follows the one that is plugged in.","zh":"螢幕上的 🎹 顯示哪一種控制器，用設定檔 id 指定，例如 `akai-lpd8`。不指定的話，它跟著插上去的那一個。"},"description":[{"en":"The on-screen controller of the 🎹 panel draws this device, and `knob1`, `fader1` and `pad1` mean the first knob, fader and pad printed on it. A sketch written with the LPD8 in mind, for instance, can ask for it with `profile: 'akai-lpd8'` so that whoever has no LPD8 still gets one on screen. The picker inside the panel changes it by hand.","zh":"🎹 面板上的螢幕控制器會畫出這一台設備，`knob1`、`fader1`、`pad1` 指的就是它上面印的第一顆旋鈕、第一支推桿、第一個墊子。例如照著 LPD8 寫的草圖，可以用 `profile: 'akai-lpd8'` 指定它，沒有 LPD8 的人螢幕上也有一台。面板裡的選單可以手動換。"},{"en":"If you do not set it, the panel starts as the MiniLab 3 and then follows a real controller as soon as one that OAV has a profile for is plugged in. If you do set it, that choice stays. An id that does not exist is reported in the console with the list of known ids, and the panel shows no controller.","zh":"你不設的話，面板一開始是 MiniLab 3，一旦插上 OAV 有設定檔的真控制器，就改成跟著它。你設了的話，就維持你的選擇。不存在的 id 會在 console 回報，並列出已知的 id，面板也不會顯示控制器。"}],"value":{"type":"String","options":"a profile id","default":"arturia-minilab3","en":"Any id that @openav/midi has a profile for: `arturia-minilab3`, `akai-lpd8`, `korg-nanokontrol2`, `generic-keys49` and the rest. When you set nothing, the panel follows the controller that is plugged in.","zh":"@openav/midi 有設定檔的任何 id：`arturia-minilab3`、`akai-lpd8`、`korg-nanokontrol2`、`generic-keys49` 等等。你不設的話，面板跟著插上去的那一台控制器。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { profile: … };` above the loader line wins, then `?oav_profile=…` on the page address, then `?profile=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_profile=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { profile: … };` 優先，其次是網頁網址上的 `?oav_profile=…`，最後是 script 自己網址上的 `?profile=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_profile=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { profile: 'akai-lpd8', panel: 'open' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('profile: ' + OAVp5.options.profile, 20, 30);\n  circle(width / 2, height / 2, 40 + knob1 * 240);   // knob 1 of the LPD8\n}","caption":{"en":"The panel opens with an Akai LPD8 on screen: eight pads and eight knobs. Turn its first knob and the circle grows.","zh":"面板打開，螢幕上是一台 Akai LPD8：八個墊子、八顆旋鈕。轉它的第一顆旋鈕，圓就變大。"},"run":"sketch","lang":"js"}],"related":["knob1","fader1","pad1","option:panel","OAVp5.input"],"keywords":{"en":["profile","controller model","minilab","lpd8","on-screen controller","device layout"],"zh":["設定檔","控制器型號","minilab","lpd8","螢幕控制器","設備配置"]},"tbd":[],"claims":[{"expr":"OAVp5.options.profile","equals":"arturia-minilab3"}]},
{"id":"option-recfps","key":"option:recfps","name":"recfps","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_recfps=1 to 60","source":{"file":"p5/oav-p5.js","line":132,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L132"},"summary":{"en":"The frame rate of recorded video, 1 to 60 frames a second.","zh":"錄下的影片的畫面更新率，每秒 1 到 60 格。"},"description":[{"en":"How many frames a second the recorder takes from the canvas, for [`startRecording()`](#startRecording) and the ⏺ button. 30 suits most places Instagram and YouTube accept. 60 is smoother for fast motion and makes a bigger file. One take can ask for its own rate: `startRecording({ fps: 60 })`.","zh":"錄影時每秒從畫布取幾格，給 [`startRecording()`](#startRecording) 和 ⏺ 按鈕用。30 適合 Instagram 和 YouTube 接受的大多數場合。60 對快速的動作比較順，檔案也比較大。單一次錄影也可以自己指定：`startRecording({ fps: 60 })`。"}],"value":{"type":"Number","options":"1 to 60","default":"30","en":"Rounded to a whole number and held between 1 and 60. Anything that is not a number means 30.","zh":"會四捨五入成整數，並限制在 1 到 60 之間。不是數字的值就當作 30。"},"params":[],"notes":[{"en":"It is read back as `OAVp5.options.recfps` and as `OAVp5.record.fps`.","zh":"它可以用 `OAVp5.options.recfps` 和 `OAVp5.record.fps` 讀回來。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { recfps: … };` above the loader line wins, then `?oav_recfps=…` on the page address, then `?recfps=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_recfps=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { recfps: … };` 優先，其次是網頁網址上的 `?oav_recfps=…`，最後是 script 自己網址上的 `?recfps=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_recfps=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { recfps: 60 };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('recording frame rate: ' + OAVp5.record.fps, 20, 30);\n}","caption":{"en":"Takes recorded from this page are 60 frames a second instead of 30.","zh":"這個頁面錄下的影片是每秒 60 格，不是 30 格。"},"run":"sketch","lang":"js"}],"related":["option:reclimit","option:record","startRecording","OAVp5.record.fps"],"keywords":{"en":["recfps","recording frame rate","fps","60 fps","video smoothness"],"zh":["錄影畫面更新率","fps","每秒幾格","60 fps","影片流暢"]},"tbd":[],"claims":[{"expr":"OAVp5.options.recfps","equals":30}]},
{"id":"option-reclimit","key":"option:reclimit","name":"reclimit","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_reclimit=1 or more (seconds)","source":{"file":"p5/oav-p5.js","line":133,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L133"},"summary":{"en":"The longest one recording may last, in seconds: a take stops by itself there.","zh":"一次錄影最長可以錄多久，單位是秒：錄到這裡就會自己停下來。"},"description":[{"en":"A long take fills the tab's memory, so every recording stops by itself at this length, saves what it has and shows a card saying so. Raise it for a long piece, record in parts, or give one take its own length: `startRecording(10)` stops after ten seconds, and a number above the limit is cut down to the limit.","zh":"太長的影片會把分頁的記憶體吃光，所以每次錄影都會在這個長度自己停下來，存下已經錄到的，並跳出一張卡片說明。長的作品就把它調高，或是分段錄，也可以替單次錄影指定長度：`startRecording(10)` 十秒後停下，超過上限的數字會被砍到上限。"}],"value":{"type":"Number","options":"1 or more (seconds)","default":"300","en":"At least 1. Anything that is not a number means 300 (five minutes).","zh":"至少是 1。不是數字的值就當作 300（五分鐘）。"},"params":[],"notes":[{"en":"It is read back as `OAVp5.options.reclimit` and as `OAVp5.record.limit`.","zh":"它可以用 `OAVp5.options.reclimit` 和 `OAVp5.record.limit` 讀回來。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { reclimit: … };` above the loader line wins, then `?oav_reclimit=…` on the page address, then `?reclimit=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_reclimit=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { reclimit: … };` 優先，其次是網頁網址上的 `?oav_reclimit=…`，最後是 script 自己網址上的 `?reclimit=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_reclimit=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { reclimit: 60 };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('longest take: ' + OAVp5.record.limit + ' seconds', 20, 30);\n}","caption":{"en":"A recording from this page stops by itself after a minute.","zh":"這個頁面錄的影片，錄滿一分鐘就會自己停。"},"run":"sketch","lang":"js"}],"related":["option:recfps","option:record","startRecording","OAVp5.record.limit"],"keywords":{"en":["reclimit","recording limit","maximum length","long recording","stops by itself"],"zh":["錄影上限","最長時間","長時間錄影","自動停止"]},"tbd":[],"claims":[{"expr":"OAVp5.options.reclimit","equals":300}]},
{"id":"option-record","key":"option:record","name":"record","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_record=on | nomic | off","source":{"file":"p5/oav-p5.js","line":131,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L131"},"summary":{"en":"Whether recording is available and whether it includes the microphone: `on`, `nomic` or `off`.","zh":"錄影能不能用、錄影要不要包含麥克風：`on`、`nomic` 或 `off`。"},"description":[{"en":"With `on`, ⏺ and 📷 are in the 🎹 panel and [`startRecording()`](#startRecording) records the canvas together with the sound the sketch makes, the microphone included when it is on. `nomic` keeps the microphone out of the recording. With it in and no headphones, the speakers are recorded a second time through the microphone, so leave it out when you play sound out loud. `off` takes ⏺ and 📷 away and makes `startRecording()` do nothing.","zh":"設成 `on`，⏺ 和 📷 都在 🎹 面板裡，[`startRecording()`](#startRecording) 會把畫布和草圖發出的聲音一起錄下來，麥克風開著的話也包含在內。`nomic` 讓麥克風不進錄影。麥克風收進去又沒戴耳機的話，喇叭的聲音會透過麥克風被錄第二次，所以大聲放聲音的時候把它關掉。`off` 把 ⏺ 和 📷 拿掉，讓 `startRecording()` 什麼都不做。"},{"en":"`off` is a promise to the page, like `mic=off`: a piece that must not be captured can say so. One take can still choose its own: `startRecording({ mic: false })`.","zh":"`off` 跟 `mic=off` 一樣是對頁面的承諾：一件不能被錄下來的作品可以這樣宣告。單次錄影還是可以自己決定：`startRecording({ mic: false })`。"}],"value":{"type":"String","options":"on | nomic | off","default":"on","en":"Anything else means `on`.","zh":"其他的值都當作 `on`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { record: … };` above the loader line wins, then `?oav_record=…` on the page address, then `?record=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_record=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { record: … };` 優先，其次是網頁網址上的 `?oav_record=…`，最後是 script 自己網址上的 `?record=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_record=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { record: 'nomic' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(3);   // three seconds, without the microphone\n}\n\nfunction draw() {\n  background(20 + 20 * sin(frameCount * 0.05));\n  fill(255);\n  text('press r to record 3 seconds (' + (recordingIsOn ? 'recording' : 'idle') + ')', 20, 30);\n}","caption":{"en":"Press r: three seconds of the canvas are recorded and downloaded, with the microphone left out even if it is on.","zh":"按 r：錄下三秒的畫布並下載，就算麥克風開著也不會錄進去。"},"run":"sketch","lang":"js"}],"related":["startRecording","option:recfps","option:reclimit","option:title","recordingIsOn"],"keywords":{"en":["record option","recording","no mic in recording","disable recording","screenshot button"],"zh":["錄影選項","錄影","錄影不含收音","關閉錄影","截圖按鈕"]},"tbd":[],"claims":[{"expr":"OAVp5.options.record","equals":"on"}]},
{"id":"option-relay","key":"option:relay","name":"relay","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_relay=a ws:// or wss:// address","source":{"file":"p5/oav-p5.js","line":127,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L127"},"summary":{"en":"The address of the relay that phones and `link` rooms talk through: a WebSocket URL.","zh":"手機和 `link` 房間用來傳話的中繼的位址：一個 WebSocket 網址。"},"description":[{"en":"The 📱 button shows a QR code that sends a phone to the remote page, and the phone's MIDI tab plays your sketch through this relay. [`link`](#option-link) rooms use it too. The default is the project's public relay. To keep your traffic on your own machine, run the relay from a checkout of the repository with `node packages/relay/server.js` and point `relay` at `ws://localhost:7458/relay`.","zh":"📱 按鈕會顯示一個 QR code，讓手機打開遙控頁面，手機的 MIDI 分頁就是透過這個中繼來彈你的草圖。[`link`](#option-link) 的房間也用它。預設是專案的公開中繼。想讓資料只留在自己的機器上，就從儲存庫的副本執行 `node packages/relay/server.js`，再把 `relay` 指到 `ws://localhost:7458/relay`。"}],"value":{"type":"String (URL)","options":"a ws:// or wss:// address","default":"wss://remote.cheyuwu.com/relay","en":"Used as given.","zh":"照你給的使用。"},"params":[],"notes":[{"en":"Nothing connects to it at load. It is used when you open the 📱 card ([`OAVp5.phone()`](#OAVp5.phone)) or set `link`.","zh":"載入的時候不會連它。要打開 📱 卡片（[`OAVp5.phone()`](#OAVp5.phone)）或設了 `link` 才會用到。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { relay: … };` above the loader line wins, then `?oav_relay=…` on the page address, then `?relay=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_relay=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { relay: … };` 優先，其次是網頁網址上的 `?oav_relay=…`，最後是 script 自己網址上的 `?relay=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_relay=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { relay: 'ws://localhost:7458/relay' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('relay: ' + OAVp5.options.relay, 20, 30);   // used when you open 📱 or set link\n}","caption":{"en":"The sketch only prints the address in use: a phone or a linked page would now connect to a relay on your own computer.","zh":"草圖只印出現在用的位址：手機或連在一起的頁面，現在會連到你自己電腦上的中繼。"},"run":"sketch","lang":"js"}],"related":["option:room","option:remote","option:link","OAVp5.phone"],"keywords":{"en":["relay","websocket","server","phone connection","self-host","wss"],"zh":["中繼","websocket","伺服器","手機連線","自己架","wss"]},"tbd":[],"claims":[]},
{"id":"option-remote","key":"option:remote","name":"remote","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_remote=an https:// address","source":{"file":"p5/oav-p5.js","line":129,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L129"},"summary":{"en":"The address of the remote page that the 📱 QR code opens on a phone.","zh":"📱 的 QR code 在手機上打開的那個遙控頁面的位址。"},"description":[{"en":"The phone's page is where the on-screen controller on the phone lives. The QR code and the link under it are this address plus the relay, the room and the MIDI tab. Change it when you host the remote page yourself, for example on a local network where the phone cannot reach the public site.","zh":"手機上的控制器就在那個頁面裡。QR code 和它下面的連結，是這個位址再加上中繼、房間和 MIDI 分頁。自己架遙控頁面的時候才需要改它，例如手機連不到公開網站的區域網路。"}],"value":{"type":"String (URL)","options":"an https:// address","default":"https://openaudiovisual.com/packages/remote/","en":"Used as given, with `?relay=…&room=…&tab=midi` added.","zh":"照你給的使用，後面接上 `?relay=…&room=…&tab=midi`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { remote: … };` above the loader line wins, then `?oav_remote=…` on the page address, then `?remote=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_remote=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { remote: … };` 優先，其次是網頁網址上的 `?oav_remote=…`，最後是 script 自己網址上的 `?remote=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_remote=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { remote: 'http://192.168.0.10:8080/packages/remote/' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('remote page: ' + OAVp5.options.remote, 20, 30);   // the 📱 QR code points here\n}","caption":{"en":"The sketch prints the remote page address in use. Press 🎹, then 📱: the QR code and the link under it point at that address.","zh":"草圖印出現在用的遙控頁面位址。按 🎹 再按 📱：QR code 和它下面的連結都指向那個位址。"},"run":"sketch","lang":"js"}],"related":["option:relay","option:room","OAVp5.phone"],"keywords":{"en":["remote page","qr code","phone page","self-host remote"],"zh":["遙控頁面","qr code","手機頁面","自己架遙控"]},"tbd":[],"claims":[]},
{"id":"option-room","key":"option:room","name":"room","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_room=a room name","source":{"file":"p5/oav-p5.js","line":127,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L127"},"summary":{"en":"The name of the room phones join to play this page: by default one made up per browser.","zh":"手機要加入哪個房間才能彈這個頁面：預設是每個瀏覽器自己亂數取的一個。"},"description":[{"en":"A phone and a page meet in a room on the [`relay`](#option-relay). Left empty, oav-p5 picks a name like `p5-k3x9a` the first time and keeps it in this browser (`localStorage`), so the same QR code keeps working on reload. If [`link`](#option-link) is set, its room is used. Set `room` yourself when you want a name people can type or remember, such as one per class.","zh":"手機和頁面在 [`relay`](#option-relay) 上的一個房間碰面。留空的話，oav-p5 第一次會挑一個像 `p5-k3x9a` 的名字，存在這個瀏覽器裡（`localStorage`），所以重新整理之後同一個 QR code 還是有用。設了 [`link`](#option-link) 的話，就用它的房間。想要一個大家打得出來、記得住的名字，例如一班一個，就自己設 `room`。"},{"en":"A phone needs only the room name to join, so pick a name people cannot guess if that matters. [`OAVp5.phone()`](#OAVp5.phone) returns the room in use.","zh":"手機只需要房間名稱就能加入，所以如果這很重要，就取一個別人猜不到的名字。[`OAVp5.phone()`](#OAVp5.phone) 會回傳現在用的房間。"}],"value":{"type":"String","options":"a room name","default":"'' (random per browser)","en":"Left empty, a name like `p5-k3x9a` is made up and kept in this browser. Only letters, digits, `_` and `-` are kept, and only the first 32 characters. If nothing is left, the room is `default`.","zh":"留空的話，會自己取一個像 `p5-k3x9a` 的名字，存在這個瀏覽器裡。只留下字母、數字、`_` 和 `-`，而且只取前 32 個字元。如果什麼都沒剩下，房間就叫 `default`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { room: … };` above the loader line wins, then `?oav_room=…` on the page address, then `?room=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_room=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { room: … };` 優先，其次是網頁網址上的 `?oav_room=…`，最後是 script 自己網址上的 `?room=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_room=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { room: 'class-2026' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('room: ' + OAVp5.options.room, 20, 30);   // the phones' QR code uses this room\n}","caption":{"en":"The sketch prints the room name. The 📱 card now shows `class-2026` instead of a random name.","zh":"草圖印出房間名稱。📱 卡片上現在顯示 `class-2026`，不是亂數的名字。"},"run":"sketch","lang":"js"}],"related":["option:relay","option:link","option:remote","OAVp5.phone"],"keywords":{"en":["room","phone room","channel","session name","qr code"],"zh":["房間","手機房間","頻道","場次名稱","qr code"]},"tbd":[],"claims":[{"expr":"OAVp5.options.room","equals":""}]},
{"id":"option-scope","key":"option:scope","name":"scope","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_scope=on | off","source":{"file":"p5/oav-p5.js","line":3069,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L3069"},"summary":{"en":"Whether `scope()` draws its oscilloscope in the corner of the page: `on` or `off`. `hints=off` turns it off too.","zh":"`scope()` 要不要在網頁角落畫它的示波器：`on` 或 `off`。`hints=off` 也會把它關掉。"},"description":[{"en":"With `off`, [`scope()`](#scope) draws nothing in the corner and only returns its value, so a sketch can leave its `scope()` calls in and hide the plot for a show. [`drawTrail()`](#drawTrail), which draws on the canvas, is not affected.","zh":"設成 `off`，[`scope()`](#scope) 不會在角落畫任何東西，只回傳它的值，所以演出的時候草圖可以把 `scope()` 留在程式裡，只是把圖藏起來。畫在畫布上的 [`drawTrail()`](#drawTrail) 不受影響。"}],"value":{"type":"String","options":"on | off","default":"on","en":"Only `off` turns it off.","zh":"只有 `off` 會把它關掉。"},"params":[],"notes":[{"en":"If the plot does not show up, check [`hints`](#option-hints) too: `hints=off` hides it as well.","zh":"圖沒有出現的話，也要檢查 [`hints`](#option-hints)：`hints=off` 也會把它藏起來。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { scope: … };` above the loader line wins, then `?oav_scope=…` on the page address, then `?scope=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_scope=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { scope: … };` 優先，其次是網頁網址上的 `?oav_scope=…`，最後是 script 自己網址上的 `?scope=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_scope=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { scope: 'off' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  const wave = 0.5 + 0.5 * sin(frameCount * 0.05);\n  circle(width / 2, height / 2, 40 + scope(wave, 'wave') * 200);   // returns wave, draws nothing\n}","caption":{"en":"The circle still breathes, but there is no plot in the corner of the page. Delete the first line and it comes back.","zh":"圓還是一漲一縮，但網頁角落沒有圖。把第一行刪掉，圖就回來了。"},"run":"sketch","lang":"js"}],"related":["scope","drawTrail","option:hints"],"keywords":{"en":["scope option","hide oscilloscope","hide plot","exhibition","no corner plot"],"zh":["示波器選項","藏起示波器","藏起圖","展場","角落沒有圖"]},"tbd":[],"claims":[]},
{"id":"option-shim","key":"option:shim","name":"shim","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_shim=on | off","source":{"file":"p5/oav-p5.js","line":127,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L127"},"summary":{"en":"Whether oav-p5 replaces `navigator.requestMIDIAccess` so sketches with their own Web MIDI code also hear the 🎹 and phones: `on` or `off`.","zh":"oav-p5 要不要取代 `navigator.requestMIDIAccess`，讓自己寫了 Web MIDI 程式的草圖也聽得到 🎹 和手機：`on` 或 `off`。"},"description":[{"en":"With `on`, a sketch that calls `navigator.requestMIDIAccess()` itself (WebMidi.js does) gets the real ports and one more input called \"OAV Virtual MIDI\", which carries whatever the on-screen controller and the phone play. It also works in Safari and on iPad, where there is no Web MIDI: the sketch gets the virtual port alone, so a WebMidi.js sketch still runs there. Programs can play into its output as well (Strudel's `.midi('OAV Virtual MIDI')`).","zh":"設成 `on`，自己呼叫 `navigator.requestMIDIAccess()` 的草圖（WebMidi.js 就是）會拿到真的連接埠，再多一個叫 \"OAV Virtual MIDI\" 的輸入，螢幕控制器和手機彈的東西都從它進來。在沒有 Web MIDI 的 Safari 和 iPad 上也能用：草圖只拿到虛擬連接埠，所以 WebMidi.js 的草圖在那裡還是能跑。其他程式也可以把東西送進它的輸出（Strudel 的 `.midi('OAV Virtual MIDI')`）。"},{"en":"`off` leaves the browser's own `requestMIDIAccess` alone. Use it when a page already has its own MIDI handling that must not see an extra port.","zh":"`off` 不動瀏覽器自己的 `requestMIDIAccess`。頁面本來就有自己的 MIDI 處理、不能多出一個連接埠的時候用。"}],"value":{"type":"String","options":"on | off","default":"on","en":"Only `off` turns it off.","zh":"只有 `off` 會把它關掉。"},"params":[],"notes":[{"en":"Order matters: oav-p5 has to load before the code that asks for MIDI, which is why the loader line goes first. A page that opened MIDI earlier keeps the browser's own answer, and 🩺 says so.","zh":"順序很重要：oav-p5 要比要 MIDI 的程式先載入，這就是為什麼載入那一行要放在最前面。更早就開了 MIDI 的頁面會維持瀏覽器自己的答案，🩺 也會這麼說。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { shim: … };` above the loader line wins, then `?oav_shim=…` on the page address, then `?shim=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_shim=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { shim: … };` 優先，其次是網頁網址上的 `?oav_shim=…`，最後是 script 自己網址上的 `?shim=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_shim=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { shim: 'off' };\n\nlet names = 'asking…';\n\nfunction setup() {\n  createCanvas(400, 300);\n  navigator.requestMIDIAccess().then(\n    (access) => { names = [...access.inputs.values()].map((i) => i.name).join(', ') || 'no inputs'; },\n    (error) => { names = 'refused: ' + error.name; }\n  );\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('shim: ' + OAVp5.options.shim, 20, 30);\n  text('MIDI inputs: ' + names, 20, 60);\n}","caption":{"en":"With the shim off, the sketch gets only the browser's own answer (the real inputs, or a refusal). Delete the first line and \"OAV Virtual MIDI\" is in the list. The browser may ask you for MIDI permission.","zh":"關掉 shim，草圖只會拿到瀏覽器自己的答案（真的輸入，或被拒絕）。把第一行刪掉，清單裡就多了 \"OAV Virtual MIDI\"。瀏覽器可能會問你要不要給 MIDI 權限。"},"run":"sketch","lang":"js"}],"related":["OAVp5.input","option:hardware","midiMessage","OAVp5.last"],"keywords":{"en":["shim","virtual midi","requestMIDIAccess","WebMidi.js","OAV Virtual MIDI","safari midi"],"zh":["shim","虛擬 midi","requestMIDIAccess","WebMidi.js","OAV Virtual MIDI","safari midi"]},"tbd":[],"claims":[{"expr":"OAVp5.options.shim","equals":"on"}]},
{"id":"option-sound","key":"option:sound","name":"sound","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_sound=click | off | an instrument id","source":{"file":"p5/oav-p5.js","line":130,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L130"},"summary":{"en":"Whether notes are heard: `click` (only after `startSound()` or 🔊), `off` (never), or an instrument id that starts at the first click.","zh":"音符有沒有聲音：`click`（要呼叫 `startSound()` 或按 🔊 才有）、`off`（永遠沒有），或一個樂器 id，從第一次點擊就開始出聲。"},"description":[{"en":"With `click`, nothing makes a sound until the sketch calls [`startSound()`](#startSound) or someone presses 🔊 in the 🎹 panel. With an instrument id, every key, pad, the on-screen controller and a phone are heard from the first click on the page, with no code at all, so any MIDI sketch becomes playable by adding the option to the page address: `?oav_sound=piano`. `off` is a promise to the page: nothing sounds, [`startSound()`](#startSound) resolves `false` and the 🔊 button is hidden.","zh":"設成 `click`，要等草圖呼叫 [`startSound()`](#startSound)，或有人在 🎹 面板按 🔊，才會有聲音。給一個樂器 id，每個琴鍵、打擊墊、螢幕控制器和手機，從頁面上的第一次點擊開始就都聽得到，一行程式都不用寫，所以任何 MIDI 草圖只要在網址加上 `?oav_sound=piano` 就能彈。`off` 是對頁面的承諾：什麼都不發聲，[`startSound()`](#startSound) 會 resolve 成 `false`，🔊 按鈕也會被藏起來。"},{"en":"Browsers let a page play sound only after a click, and a MIDI key is not a click, so the first sound waits for one: a card says \"sound is ready but silent\" until then.","zh":"瀏覽器要網頁被點過一下才讓它出聲，而 MIDI 的琴鍵不算點擊，所以第一個聲音要等一次點擊：在那之前會有一張卡片說「聲音準備好了，還沒響」。"}],"value":{"type":"String","options":"click | off | an instrument id","default":"click","en":"An instrument id such as `piano`, `organ`, `strings`, `harp` or `music-box`. `on` and `auto` start the default instrument (the piano). An id that does not exist plays the default synth (`synth/pad`) and the console lists the ids that exist.","zh":"樂器 id，像是 `piano`、`organ`、`strings`、`harp` 或 `music-box`。`on` 和 `auto` 會啟動預設的樂器（鋼琴）。不存在的 id 會播預設的合成器（`synth/pad`），console 會列出存在的 id。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { sound: … };` above the loader line wins, then `?oav_sound=…` on the page address, then `?sound=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_sound=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { sound: … };` 優先，其次是網頁網址上的 `?oav_sound=…`，最後是 script 自己網址上的 `?sound=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_sound=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { sound: 'organ' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.play(60 + floor(random(12)), 0.8);   // a key, as a controller would play it\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('sound: ' + OAVp5.sound.state + ' (' + OAVp5.sound.instrument + ')', 20, 30);\n  text('click the canvas to play a note', 20, 60);\n}","caption":{"en":"No startSound() call: the organ is heard from your first click, which also plays a note. The state goes from \"waiting\" to \"on\".","zh":"沒有呼叫 startSound()：從你的第一次點擊開始就聽得到管風琴，那一下點擊同時彈一個音。狀態從 \"waiting\" 變成 \"on\"。"},"run":"sketch","lang":"js"},{"code":"window.OAV_P5 = { sound: 'off' };\n\nlet answer = 'click the canvas to try';\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  startSound().then((ok) => { answer = 'startSound() resolved ' + ok; });\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('sound: ' + OAVp5.sound.state, 20, 30);\n  text(answer, 20, 60);   // startSound() resolved false\n}","caption":{"en":"Nothing can make sound: the state stays \"off\", startSound() resolves false, and the 🔊 button is not in the panel.","zh":"什麼都不能發聲：狀態一直是 \"off\"，`startSound()` resolve 成 false，面板裡也沒有 🔊 按鈕。"},"run":"sketch","lang":"js"}],"related":["startSound","stopSound","soundIsOn","option:tone","OAVp5.play"],"keywords":{"en":["sound option","instrument","piano","organ","play without code","audio output","mute"],"zh":["聲音選項","樂器","鋼琴","管風琴","不用寫程式就有聲音","音訊輸出","靜音"]},"tbd":[],"claims":[{"expr":"OAVp5.options.sound","equals":"click"}]},
{"id":"option-strudel","key":"option:strudel","name":"strudel","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_strudel=auto | on | off","source":{"file":"p5/oav-p5.js","line":148,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L148"},"summary":{"en":"Whether oav-p5 treats the page as Strudel, the live-coding editor, and behaves politely beside it: `auto`, `on` or `off`.","zh":"oav-p5 要不要把這個頁面當成 Strudel（即時編碼編輯器），在它旁邊客氣一點：`auto`、`on` 或 `off`。"},"description":[{"en":"Strudel is a live-coding editor, not a sketch, so a few things change when oav-p5 sees it: the names Strudel owns (`note`, `velocity`, `beat`) are left alone and one card says what to read instead, MIDI is asked for by Strudel's own `midin()` rather than at load ([`hardware`](#option-hardware) becomes `click`), the 🎹 panel becomes the [`compact`](#option-compact) box, and the 🎹 sits above Strudel's side-panel buttons. oav-p5 only looks at the address and at the names on the page: nothing of Strudel is called.","zh":"Strudel 是即時編碼編輯器，不是草圖，所以 oav-p5 看到它的時候有幾件事會改變：Strudel 自己擁有的名稱（`note`、`velocity`、`beat`）不去動，並用一張卡片說明要改讀什麼，MIDI 交給 Strudel 自己的 `midin()` 去要，不是載入的時候（[`hardware`](#option-hardware) 變成 `click`），🎹 面板變成 [`compact`](#option-compact) 的小方塊，🎹 放在 Strudel 側邊按鈕的上面。oav-p5 只看網址和頁面上有哪些名稱：不會呼叫 Strudel 的任何東西。"},{"en":"`on` forces this behaviour on any page, `off` forces the plain one even on strudel.cc. Settings you give yourself ([`hardware`](#option-hardware), [`compact`](#option-compact)) still win over what Strudel mode would choose.","zh":"`on` 讓任何頁面都用這種做法，`off` 讓 strudel.cc 上也用一般的做法。你自己給的設定（[`hardware`](#option-hardware)、[`compact`](#option-compact)）還是比 Strudel 模式的選擇優先。"}],"value":{"type":"String","options":"auto | on | off","default":"auto","en":"`auto` means on strudel.cc, or on a page where Strudel's own functions (`note()` and `s()` with `stack()` or `samples()`) are already there. Anything else means `auto`.","zh":"`auto` 的意思是在 strudel.cc 上，或頁面上本來就有 Strudel 自己的函式（`note()` 和 `s()`，加上 `stack()` 或 `samples()`）。其他的值都當作 `auto`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { strudel: … };` above the loader line wins, then `?oav_strudel=…` on the page address, then `?strudel=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_strudel=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { strudel: … };` 優先，其次是網頁網址上的 `?oav_strudel=…`，最後是 script 自己網址上的 `?strudel=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_strudel=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { strudel: 'on' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('hardware: ' + OAVp5.options.hardware, 20, 30);   // click: Strudel asks for MIDI itself\n  text('compact panel: ' + OAVp5.options.compact, 20, 60);   // true\n}","caption":{"en":"On an ordinary page, strudel on switches to Strudel's manners: Web MIDI is not asked for at load and the panel will be the compact box.","zh":"在一般的頁面上，strudel 設成 on 就換成 Strudel 的規矩：載入的時候不去要 Web MIDI，面板也會是精簡的小方塊。"},"run":"sketch","lang":"js"}],"related":["option:hardware","option:compact","option:link","option:clock","OAVp5.clock"],"keywords":{"en":["strudel","live coding","strudel.cc","tidal","midin","strudel mode"],"zh":["strudel","即時編碼","strudel.cc","tidal","midin","strudel 模式"]},"tbd":[],"claims":[{"expr":"OAVp5.options.compact","equals":false}]},
{"id":"option-telemetry","key":"option:telemetry","name":"telemetry","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_telemetry=off | on","source":{"file":"p5/oav-p5.js","line":129,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L129"},"summary":{"en":"Whether the on-screen controller may send one anonymous page-load count to the project: `off` or `on`.","zh":"螢幕上的控制器可不可以把一筆匿名的頁面載入次數送給專案：`off` 或 `on`。"},"description":[{"en":"In oav-p5 it is off. With `on`, when the 🎹 panel builds the on-screen controller, @openav/midi sends one anonymous hit per page load to the project's Google Analytics, so the project can see where its controllers end up. The @openav/midi README (section Telemetry) lists exactly what it sends: the kind of embed, the profile, the page's origin (never the path, query or title) and the version. Nothing anyone plays is sent and no cookie is set.","zh":"在 oav-p5 裡預設是關的。設成 `on`，🎹 面板建立螢幕控制器的時候，@openav/midi 會把每次頁面載入的一筆匿名紀錄送到專案的 Google Analytics，讓專案知道它的控制器用在哪些地方。@openav/midi 的 README（Telemetry 那一節）列出它確切送了什麼：嵌入的種類、設定檔、頁面的來源網域（絕不含路徑、查詢字串或標題）和版本。沒有任何人彈的東西會被送出，也不會設定 cookie。"}],"value":{"type":"String","options":"off | on","default":"off","en":"Only `on` turns it on.","zh":"只有 `on` 會打開它。"},"params":[],"notes":[{"en":"According to that README nothing is sent from localhost, local network addresses, `file://` or openaudiovisual.com itself, or when the browser sends Do Not Track or Global Privacy Control.","zh":"依照那份 README，從 localhost、區域網路位址、`file://` 或 openaudiovisual.com 本身都不會送，瀏覽器送出「不要追蹤」或 Global Privacy Control 的時候也不會送。"},{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { telemetry: … };` above the loader line wins, then `?oav_telemetry=…` on the page address, then `?telemetry=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_telemetry=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { telemetry: … };` 優先，其次是網頁網址上的 `?oav_telemetry=…`，最後是 script 自己網址上的 `?telemetry=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_telemetry=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { telemetry: 'on' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('telemetry: ' + OAVp5.options.telemetry, 20, 30);   // nothing visible changes\n}","caption":{"en":"Nothing visible changes. The option is passed to the on-screen controller when the 🎹 panel first opens.","zh":"畫面上看不出任何變化。這個選項會在 🎹 面板第一次打開的時候傳給螢幕控制器。"},"run":"sketch","lang":"js"}],"related":["option:panel","option:profile"],"keywords":{"en":["telemetry","analytics","privacy","anonymous","tracking","opt in"],"zh":["遙測","分析","隱私","匿名","追蹤","選擇加入"]},"tbd":[{"en":"The hit itself was not observed here: the example runs on localhost, where nothing is sent. What it sends is as stated in the @openav/midi README.","zh":"這裡沒有親眼看到那筆紀錄送出：範例在 localhost 上跑，那裡不會送任何東西。它送了什麼，以 @openav/midi 的 README 為準。"}],"claims":[{"expr":"OAVp5.options.telemetry","equals":"off"}]},
{"id":"option-title","key":"option:title","name":"title","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_title=a name","source":{"file":"p5/oav-p5.js","line":133,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L133"},"summary":{"en":"A name that goes into the file name of recordings and images, as in `oav-my-piece-20261015-183012.mp4`.","zh":"放進錄影和圖片檔名裡的名稱，例如 `oav-my-piece-20261015-183012.mp4`。"},"description":[{"en":"Without it a recording is `oav-20261015-183012.mp4`. With `title: 'my-piece'` it is `oav-my-piece-20261015-183012.mp4`, so takes from several pieces sort themselves. One take can have its own name: `startRecording({ name: 'take-2' })` replaces it.","zh":"沒有它的話，錄影檔名是 `oav-20261015-183012.mp4`。設了 `title: 'my-piece'`，就是 `oav-my-piece-20261015-183012.mp4`，好幾件作品的錄影就會自己排好。單次錄影也可以有自己的名字：`startRecording({ name: 'take-2' })` 會取代它。"}],"value":{"type":"String","options":"a name","default":"'' (no name)","en":"Spaces and the characters a file name cannot hold become `-`, and only the first 40 characters are used.","zh":"空白和檔名不能用的字元會變成 `-`，而且只取前 40 個字元。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { title: … };` above the loader line wins, then `?oav_title=…` on the page address, then `?title=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_title=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { title: … };` 優先，其次是網頁網址上的 `?oav_title=…`，最後是 script 自己網址上的 `?title=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_title=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { title: 'my-piece' };\n\nlet saved = 'press r to record 2 seconds';\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction keyPressed() {\n  if (key === 'r') startRecording(2);\n}\n\nfunction recordingSaved(info) {\n  saved = 'saved as ' + info.name;\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(saved, 20, 30);\n}","caption":{"en":"Press r: two seconds are recorded and downloaded, and the sketch shows the file name, which starts with oav-my-piece-.","zh":"按 r：錄兩秒並下載，草圖顯示檔名，檔名以 oav-my-piece- 開頭。"},"run":"sketch","lang":"js"}],"related":["startRecording","recordingSaved","option:record"],"keywords":{"en":["title","file name","recording name","name the video","download name"],"zh":["標題","檔名","錄影名稱","影片命名","下載檔名"]},"tbd":[],"claims":[{"expr":"OAVp5.options.title","equals":""}]},
{"id":"option-tone","key":"option:tone","name":"tone","aliases":[],"kind":"option","package":"oav-p5","category":"options","documented":true,"signature":"?oav_tone=an ES module URL","source":{"file":"p5/oav-p5.js","line":130,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L130"},"summary":{"en":"Where Tone.js, the sound engine behind `startSound()`, is loaded from: an ES module address. Empty means jsDelivr.","zh":"Tone.js（`startSound()` 背後的聲音引擎）從哪裡載入：一個 ES module 的位址。空的就是 jsDelivr。"},"description":[{"en":"Sound ([`startSound()`](#startSound), the `sound` option, the 🔊 button) uses Tone.js 15, which oav-p5 loads the first time sound is wanted. By default it comes from jsDelivr. Point `tone` at your own copy when the page is behind a school firewall or an ad blocker that stops the CDN, or when you serve everything yourself. If it cannot be loaded, a card says which host was unreachable and what to try.","zh":"聲音（[`startSound()`](#startSound)、`sound` 選項、🔊 按鈕）用的是 Tone.js 15，oav-p5 在第一次需要聲音的時候才載入它。預設從 jsDelivr 來。頁面在學校防火牆或擋廣告的外掛後面、CDN 被擋住，或是你自己架所有東西的時候，把 `tone` 指到你自己的那一份。載不到的時候，會有一張卡片說是哪個主機連不上，以及可以怎麼試。"}],"value":{"type":"String (URL)","options":"an ES module URL","default":"'' (jsDelivr)","en":"The address of Tone.js version 15 as an ES module. Left empty, it is `https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm`, and `OAVp5.options.tone` reads `''`.","zh":"Tone.js 第 15 版的 ES module 位址。留空的話用 `https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm`，`OAVp5.options.tone` 讀回來是 `''`。"},"params":[],"notes":[{"en":"Set it before oav-p5 loads. `window.OAV_P5 = { tone: … };` above the loader line wins, then `?oav_tone=…` on the page address, then `?tone=…` on the script's own URL. Values in `window.OAV_P5` are turned into text, so write `'off'`, not `false`. On OpenProcessing the page address takes `?sketch_oav_tone=…` (the project's README says OpenProcessing forwards it to the sketch).","zh":"要在 oav-p5 載入之前設好。寫在載入那一行上面的 `window.OAV_P5 = { tone: … };` 優先，其次是網頁網址上的 `?oav_tone=…`，最後是 script 自己網址上的 `?tone=…`。寫在 `window.OAV_P5` 裡的值會被轉成文字，所以要寫 `'off'`，不要寫 `false`。在 OpenProcessing 上，網頁網址要寫成 `?sketch_oav_tone=…`（專案的 README 說 OpenProcessing 會把它轉給草圖）。"}],"examples":[{"code":"window.OAV_P5 = { tone: 'https://cdn.jsdelivr.net/npm/tone@15.0.4/+esm' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('Tone.js from: ' + OAVp5.options.tone, 20, 30);   // the address here is the built-in one\n}","caption":{"en":"Prints where Tone.js would come from. The address here is the built-in one, so nothing changes. Swap in your own copy to host it yourself.","zh":"印出 Tone.js 會從哪裡來。這裡的位址就是內建的那個，所以不會有任何改變。換成你自己的那一份，就能自己架。"},"run":"sketch","lang":"js"}],"related":["option:sound","startSound","soundIsOn"],"keywords":{"en":["tone","tone.js","cdn","offline sound","self-host","jsdelivr","firewall"],"zh":["tone","tone.js","cdn","離線聲音","自己架","jsdelivr","防火牆"]},"tbd":[],"claims":[{"expr":"OAVp5.options.tone","equals":""}]},
{"id":"OAVp5","key":"OAVp5","name":"OAVp5","aliases":["midi","oavMidi"],"kind":"object","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5","source":{"file":"p5/oav-p5.js","line":110,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L110"},"summary":{"en":"The object oav-p5 hangs everything on: MIDI state, the panel, the microphone, the clock, the check-up. `midi` and `oavMidi` are the same object.","zh":"oav-p5 把所有東西掛在上面的那個物件：MIDI 狀態、面板、麥克風、時鐘、體檢。`midi` 和 `oavMidi` 是同一個物件。"},"description":[{"en":"It is there as soon as oav-p5 has loaded, in setup() and draw() alike. The plain names of the p5 style (`knob1`, `note`, `micLevel`) are the easy way in. `OAVp5` is the way in when you want more: the raw MIDI state ([`OAVp5.note`](#OAVp5.note), [`OAVp5.cc`](#OAVp5.cc), [`OAVp5.bend`](#OAVp5.bend), [`OAVp5.held`](#OAVp5.held), [`OAVp5.last`](#OAVp5.last)), subscriptions ([`OAVp5.on()`](#OAVp5.on)), a way to send bytes ([`OAVp5.input()`](#OAVp5.input)), the microphone's numbers ([`OAVp5.mic`](#OAVp5.mic)), the external clock ([`OAVp5.clock`](#OAVp5.clock)), the link ([`OAVp5.link`](#OAVp5.link)), the values over time ([`OAVp5.chop`](#OAVp5.chop)) and the check-up ([`OAVp5.check()`](#OAVp5.check)).","zh":"只要 oav-p5 載入了，它就在那裡，setup() 和 draw() 裡都一樣。p5 風格的簡單名稱（`knob1`、`note`、`micLevel`）是輕鬆的入口。想要更多的時候就用 `OAVp5`：原始的 MIDI 狀態（[`OAVp5.note`](#OAVp5.note)、[`OAVp5.cc`](#OAVp5.cc)、[`OAVp5.bend`](#OAVp5.bend)、[`OAVp5.held`](#OAVp5.held)、[`OAVp5.last`](#OAVp5.last)）、訂閱（[`OAVp5.on()`](#OAVp5.on)）、自己送位元組進去的方法（[`OAVp5.input()`](#OAVp5.input)）、麥克風的數字（[`OAVp5.mic`](#OAVp5.mic)）、外部時鐘（[`OAVp5.clock`](#OAVp5.clock)）、連結（[`OAVp5.link`](#OAVp5.link)）、數值隨時間變化（[`OAVp5.chop`](#OAVp5.chop)），還有體檢（[`OAVp5.check()`](#OAVp5.check)）。"},{"en":"Three names, one object. `OAVp5` is always there. `oavMidi` is always there too. `midi` is there unless the page already had a `midi` of its own, in which case it is left alone and the console says to read `OAVp5` instead. `OAVp5.options` holds the loading options as oav-p5 read them: `OAVp5.options.mic`, and so on for each of the loading options (for example [`mic`](#option-mic)).","zh":"三個名字，同一個物件。`OAVp5` 一定在，`oavMidi` 也一定在。`midi` 則是頁面本來沒有自己的 `midi` 時才有，如果頁面已經有了，就不去動它，console 會說要改讀 `OAVp5`。`OAVp5.options` 放的是 oav-p5 讀到的載入選項：`OAVp5.options.mic`，其他每個載入選項（例如 [`mic`](#option-mic)）也一樣。"}],"params":[],"needs":{"en":"Only the loader line. If it has not loaded, `OAVp5` is not defined, and a sketch that reads it stops with \"OAVp5 is not defined\".","zh":"只需要載入那一行。如果它沒載入，`OAVp5` 就沒有定義，讀它的草圖會停在「OAVp5 is not defined」。"},"notes":[{"en":"`midi` and `oavMidi` are the same object under other names.","zh":"`midi` 和 `oavMidi` 是同一個物件的其他名字。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('same object: ' + (OAVp5 === midi && OAVp5 === oavMidi), 20, 30);\n  text('version: ' + OAVp5.version, 20, 60);\n  text('MIDI hardware: ' + OAVp5.hardware, 20, 90);\n  text('microphone: ' + OAVp5.mic.state, 20, 120);   // reading the state does not start it\n  text('external clock: ' + OAVp5.clock.info.state, 20, 150);\n}","caption":{"en":"A small status board: the three names are one object, then the version and the state of MIDI, the microphone and the clock.","zh":"一塊小小的狀態板：三個名字是同一個物件，然後是版本，以及 MIDI、麥克風和時鐘的狀態。"},"run":"sketch","lang":"js"}],"related":["OAVp5.version","OAVp5.on","OAVp5.input","OAVp5.check","OAVp5.mic","OAVp5.clock","OAVp5.chop"],"keywords":{"en":["OAVp5","midi object","oavMidi","namespace","advanced","state","api object"],"zh":["OAVp5","midi 物件","oavMidi","命名空間","進階","狀態","api 物件"]},"tbd":[],"claims":[{"expr":"OAVp5 === midi && OAVp5 === oavMidi","equals":true}]},
{"id":"OAVp5.check","key":"OAVp5.check","name":"OAVp5.check","aliases":[],"kind":"function","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.check([opts])","source":{"file":"p5/oav-p5.js","line":2818,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L2818"},"summary":{"en":"Runs the 🩺 check-up: the version, page, browser, microphone, MIDI, sound, drums, recording, wiring and hints, as a card to screenshot and as an object.","zh":"執行 🩺 體檢：版本、頁面、瀏覽器、麥克風、MIDI、聲音、鼓機、錄影、接線和提示，做成一張可以截圖的卡片，也回傳成物件。"},"description":[{"en":"The same thing the 🩺 button in the panel does. When something does not work for a student, `OAVp5.check()` in the console (or 🩺) gives a card that says what this browser did: whether the page is in an iframe, whether the microphone is on or blocked, whether Web MIDI exists and which devices it found, the last message, the sound and drum state, whether MP4 recording works, what the controller is wired to, which of the sketch's own names stepped in the way, and which cards have appeared. It has a copy button and a link to the site's full device check.","zh":"跟面板裡的 🩺 按鈕做的事一樣。學生那邊有東西不能用的時候，在 console 打 `OAVp5.check()`（或按 🩺）就會得到一張卡片，說這個瀏覽器做了什麼：頁面是不是在 iframe 裡、麥克風開著還是被擋、有沒有 Web MIDI、找到哪些設備、最後一則訊息、聲音和鼓機的狀態、能不能錄 MP4、控制器接到哪些變數、草圖自己的哪些名稱擋住了路，還有出現過哪些卡片。卡片上有複製按鈕，也有連到網站完整裝置檢查的連結。"}],"params":[{"name":"opts","type":"Object","optional":true,"en":"`{ show: false }` skips the card on the page and only returns the report (the text still goes to the console). Left out, the card is shown.","zh":"`{ show: false }` 不顯示頁面上的卡片，只回傳報告（文字還是會印到 console）。不寫的話就顯示卡片。"}],"returns":{"type":"Object","en":"The report: `{ version, time, page, browser, microphone, midi, sound, drums, wiring, own, hints, external, recording, text }`. `text` is the whole card as plain lines, the other fields are the same facts as data (`microphone.state`, `midi.hardware`, `midi.devices` …).","zh":"報告：`{ version, time, page, browser, microphone, midi, sound, drums, wiring, own, hints, external, recording, text }`。`text` 是整張卡片的純文字，其他欄位是同樣的資訊做成的資料（`microphone.state`、`midi.hardware`、`midi.devices` …）。"},"notes":[{"en":"It is a snapshot taken when you call it, not a live view. Calling it again replaces the card.","zh":"它是呼叫那一刻的快照，不是即時的畫面。再呼叫一次會取代那張卡片。"}],"examples":[{"code":"let report;\n\nfunction setup() {\n  createCanvas(400, 300);\n  report = OAVp5.check({ show: false });   // the report, without the card on the page\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  textSize(10);\n  text(report.text, 10, 10, width - 20, height - 20);\n}","caption":{"en":"Prints the check-up text on the canvas: version, browser, microphone, MIDI and the rest, one line each.","zh":"把體檢的文字印在畫布上：版本、瀏覽器、麥克風、MIDI 等等，一項一行。"},"run":"sketch","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction mousePressed() {\n  OAVp5.check();   // the card appears over the page\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('click the canvas for the check-up card', 20, 30);\n}","caption":{"en":"Click the canvas: the check-up card opens over the page, with a copy button, ready to screenshot.","zh":"點一下畫布：體檢卡片會蓋在頁面上，附有複製按鈕，可以直接截圖。"},"run":"sketch","lang":"js"}],"related":["OAVp5.hints","OAVp5.version","option:debug","OAVp5.mic.state","OAVp5.hardware"],"keywords":{"en":["check","check-up","diagnose","troubleshoot","report","screenshot","why does it not work"],"zh":["體檢","檢查","診斷","疑難排解","報告","截圖","為什麼不能用"]},"tbd":[],"claims":[{"expr":"typeof OAVp5.check({ show: false }).text","equals":"string"}]},
{"id":"OAVp5.clock","key":"OAVp5.clock","name":"OAVp5.clock","aliases":[],"kind":"object","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.clock","source":{"file":"p5/oav-p5.js","line":713,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L713"},"summary":{"en":"What oav-p5 hears of an external MIDI clock: its state, tempo and where it is in the bar. Nothing until another program or device starts sending one.","zh":"oav-p5 聽到的外部 MIDI 時鐘：它的狀態、速度、現在在小節的哪裡。在別的程式或設備開始送時鐘之前什麼都沒有。"},"description":[{"en":"Some programs keep the time and everyone else follows: Strudel (`midicmd(\"clock*96, <start stop>/8\")`), a DAW over a virtual cable, a drum machine on a MIDI cable. They send 24 clock messages per beat, plus start, continue, stop and song position. oav-p5 counts them: [`OAVp5.clock.info`](#OAVp5.clock.info) says whether a clock is running, [`OAVp5.clock.bpm`](#OAVp5.clock.bpm) its tempo and [`OAVp5.clock.phase`](#OAVp5.clock.phase) how far through the bar and the beat it is now.","zh":"有些程式負責打拍子，其他人跟著走：Strudel（`midicmd(\"clock*96, <start stop>/8\")`）、透過虛擬線連過來的 DAW、用 MIDI 線接的鼓機。它們每拍送 24 個時鐘訊息，加上 start、continue、stop 和 song position。oav-p5 把它們數起來：[`OAVp5.clock.info`](#OAVp5.clock.info) 說有沒有時鐘在跑，[`OAVp5.clock.bpm`](#OAVp5.clock.bpm) 是它的速度，[`OAVp5.clock.phase`](#OAVp5.clock.phase) 是現在走到小節和拍子的哪裡。"},{"en":"When a clock is moving, [`drumPhase`](#drumPhase), [`beatPhase`](#beatPhase), [`drumBar`](#drumBar), [`drumStep`](#drumStep) and [`drumBpm`](#drumBpm) read it, and a drum machine that is up follows it. [`?oav_clock=off`](#option-clock) turns all of that off. Clock messages are not delivered to [`midiMessage()`](#midiMessage): they belong to the clock.","zh":"時鐘在動的時候，[`drumPhase`](#drumPhase)、[`beatPhase`](#beatPhase)、[`drumBar`](#drumBar)、[`drumStep`](#drumStep)、[`drumBpm`](#drumBpm) 讀的就是它，已經在跑的鼓機也會跟著它。[`?oav_clock=off`](#option-clock) 會把這些全部關掉。時鐘訊息不會交給 [`midiMessage()`](#midiMessage)：它們屬於時鐘。"}],"params":[],"notes":[{"en":"The first clock message loads the counting code (@openav/midi's `MidiClock`) from next to oav-p5.js, which takes a few frames. Until then the members read `none`, `null` and `null`.","zh":"第一個時鐘訊息會從 oav-p5.js 旁邊載入負責計算的程式（@openav/midi 的 `MidiClock`），要幾個畫面。在那之前，這些成員讀到的是 `none`、`null`、`null`。"},{"en":"This is advanced: most sketches only read `drumPhase` and let it follow a clock.","zh":"這是進階用法：大多數草圖只讀 `drumPhase`，讓它自己去跟著時鐘。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  OAVp5.input([0xFA]);                          // MIDI start\n  setInterval(() => OAVp5.input([0xF8]), 20);   // clock ticks: 24 a beat, here 50 a second (125 BPM)\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('clock: ' + OAVp5.clock.info.state, 20, 30);\n  text('tempo: ' + (OAVp5.clock.bpm ? nf(OAVp5.clock.bpm, 1, 1) : '-') + ' BPM', 20, 60);\n  const p = OAVp5.clock.phase;\n  if (p) circle(40 + p.beat * (width - 80), 150, 30);   // sweeps across once a beat\n}","caption":{"en":"The sketch sends itself a made-up MIDI clock at about 125 BPM. A circle sweeps across once per beat, and the state and tempo are printed.","zh":"草圖自己送出一個假的 MIDI 時鐘，大約 125 BPM。一個圓每拍從左掃到右一次，狀態和速度也印出來。"},"run":"sketch","lang":"js"}],"related":["OAVp5.clock.info","OAVp5.clock.bpm","OAVp5.clock.phase","option:clock","drumPhase","beatPhase"],"keywords":{"en":["clock","midi clock","external clock","sync","tempo","follow","DAW","Strudel"],"zh":["時鐘","midi 時鐘","外部時鐘","同步","速度","跟隨","DAW","Strudel"]},"tbd":[],"claims":[{"expr":"OAVp5.clock.info.state","equals":"none"}]},
{"id":"OAVp5.clock.bpm","key":"OAVp5.clock.bpm","name":"OAVp5.clock.bpm","aliases":[],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.clock.bpm","source":{"file":"p5/oav-p5.js","line":716,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L716"},"summary":{"en":"The tempo of the external MIDI clock in beats a minute, or `null` while there is none.","zh":"外部 MIDI 時鐘的速度，單位是每分鐘幾拍，沒有的時候是 `null`。"},"description":[{"en":"The raw number, not rounded. Use it for anything that should run at the clock's tempo: `OAVp5.clock.bpm / 60` is beats a second. If the drum machine follows the clock, [`drumBpm`](#drumBpm) shows it rounded to a tenth.","zh":"原始的數字，沒有四捨五入。任何要跟著時鐘速度走的東西都可以用它：`OAVp5.clock.bpm / 60` 就是每秒幾拍。鼓機如果跟著時鐘，[`drumBpm`](#drumBpm) 會顯示四捨五入到小數一位的值。"}],"value":{"type":"Number | null","en":"Fitted over the last 24 clock ticks, so it settles within a moment and does not jump with one late tick. `null` before two ticks have arrived or when `clock=off`.","zh":"用最近 24 個時鐘 tick 擬合出來的，所以一下子就會穩定，不會因為一個晚到的 tick 就跳動。兩個 tick 到齊之前，或設了 `clock=off`，都是 `null`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  OAVp5.input([0xFA]);                          // MIDI start\n  setInterval(() => OAVp5.input([0xF8]), 20);   // clock ticks: 24 a beat, here 50 a second (125 BPM)\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  textSize(48);\n  textAlign(CENTER, CENTER);\n  const bpm = OAVp5.clock.bpm;\n  text(bpm ? nf(bpm, 1, 1) + ' BPM' : 'no clock', width / 2, height / 2);\n}","caption":{"en":"Prints the tempo of the made-up clock the sketch sends itself: about 125 BPM once a few ticks have arrived.","zh":"印出草圖自己送的假時鐘的速度：幾個 tick 到齊之後大約是 125 BPM。"},"run":"sketch","lang":"js"}],"related":["OAVp5.clock","OAVp5.clock.info","OAVp5.clock.phase","drumBpm"],"keywords":{"en":["bpm","tempo","beats per minute","external tempo","midi clock bpm"],"zh":["bpm","速度","每分鐘幾拍","外部速度","midi 時鐘速度"]},"tbd":[],"claims":[{"expr":"OAVp5.clock.bpm === null","equals":true}]},
{"id":"OAVp5.clock.info","key":"OAVp5.clock.info","name":"OAVp5.clock.info","aliases":[],"kind":"object","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.clock.info","source":{"file":"p5/oav-p5.js","line":715,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L715"},"summary":{"en":"A snapshot of the external clock: `state` (`none`, `running`, `stopped` or `lost`), `bpm`, whether the drums follow it, where it came from, and counts.","zh":"外部時鐘的快照：`state`（`none`、`running`、`stopped` 或 `lost`）、`bpm`、鼓機有沒有跟著它、它從哪裡來，還有各種計數。"},"description":[{"en":"`state` is `'none'` until a clock message has come, `'running'` while the clock is going, `'stopped'` after a MIDI stop (the position holds and clocks may still arrive), and `'lost'` when no clock message has arrived for 0.6 seconds. `bpm` is the tempo fitted over the last 24 ticks, rounded to two decimals, or `null` before two ticks. `running` is true while the transport runs, which a stream of clocks with no start message counts as too (`implicit` is true then).","zh":"`state` 在收到任何時鐘訊息之前是 `'none'`，時鐘在走的時候是 `'running'`，MIDI stop 之後是 `'stopped'`（位置停住，時鐘訊息可能還在來），超過 0.6 秒沒收到時鐘訊息就是 `'lost'`。`bpm` 是用最近 24 個 tick 擬合出來的速度，四捨五入到小數兩位，兩個 tick 之前是 `null`。`running` 在播放中是 true，只有時鐘、沒有 start 訊息的串流也算（那時 `implicit` 是 true）。"},{"en":"`following` is true when the drums read this clock: it is running, has a tempo and `clock=off` is not set. `source` says where the messages came from: `'hardware'`, `'screen'`, `'phone'`, `'virtual-out'` (a program playing into OAV Virtual MIDI) or `'link'`. `clocks`, `dupes`, `starts`, `stops` and `continues` count what arrived; a start sent three times with one timestamp counts once and adds to `dupes`.","zh":"`following` 在鼓機讀這個時鐘的時候是 true：它在跑、有速度、而且沒有設 `clock=off`。`source` 說訊息從哪裡來：`'hardware'`、`'screen'`、`'phone'`、`'virtual-out'`（某個程式送進 OAV Virtual MIDI）或 `'link'`。`clocks`、`dupes`、`starts`、`stops`、`continues` 數的是收到了什麼：用同一個時間戳送三次的 start 只算一次，其餘加進 `dupes`。"}],"value":{"type":"Object","en":"A new object each time you read it: `{ state, bpm, running, following, source, implicit, clocks, dupes, starts, stops, continues }`. Before any clock message it is `{ state: 'none', bpm: null, running: false, following: false, source: null, clocks: 0 }`.","zh":"每次讀都是一個新的物件：`{ state, bpm, running, following, source, implicit, clocks, dupes, starts, stops, continues }`。在任何時鐘訊息之前是 `{ state: 'none', bpm: null, running: false, following: false, source: null, clocks: 0 }`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  OAVp5.input([0xFA]);                          // MIDI start\n  setInterval(() => OAVp5.input([0xF8]), 20);   // clock ticks: 24 a beat, here 50 a second (125 BPM)\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const i = OAVp5.clock.info;\n  text('state: ' + i.state + (i.running ? ' (running)' : ''), 20, 30);\n  text('bpm: ' + i.bpm + ',  following: ' + i.following, 20, 60);\n  text('source: ' + i.source + ',  clocks heard: ' + i.clocks, 20, 90);\n}","caption":{"en":"Prints the clock's snapshot while the sketch sends itself a made-up clock: the state goes from \"none\" to \"running\" within a moment.","zh":"草圖自己送一個假的時鐘，同時印出時鐘的快照：狀態很快就從 \"none\" 變成 \"running\"。"},"run":"sketch","lang":"js"}],"related":["OAVp5.clock","OAVp5.clock.bpm","OAVp5.clock.phase","option:clock"],"keywords":{"en":["clock info","clock state","is the clock running","external tempo","midi clock status"],"zh":["時鐘資訊","時鐘狀態","時鐘有沒有在跑","外部速度","midi 時鐘狀態"]},"tbd":[],"claims":[{"expr":"OAVp5.clock.info.following","equals":false}]},
{"id":"OAVp5.clock.phase","key":"OAVp5.clock.phase","name":"OAVp5.clock.phase","aliases":[],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.clock.phase","source":{"file":"p5/oav-p5.js","line":718,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L718"},"summary":{"en":"Where the external clock is right now: `{ pos, bar, beat, bpm, running }`, or `null` while there is no tempo.","zh":"外部時鐘現在走到哪裡：`{ pos, bar, beat, bpm, running }`，沒有速度的時候是 `null`。"},"description":[{"en":"A bar is four beats. `bar` and `beat` count up from 0 and fall back to 0 at the next downbeat, the same way [`drumPhase`](#drumPhase) and [`beatPhase`](#beatPhase) do. After a stop the position holds. A clock with no start message gets its downbeat from the first tick after a silence of half a second, so a phase is there anyway.","zh":"一個小節是四拍。`bar` 和 `beat` 從 0 往上數，到下一個第一拍就回到 0，跟 [`drumPhase`](#drumPhase)、[`beatPhase`](#beatPhase) 一樣。stop 之後位置會停住。沒有 start 訊息的時鐘，以沉默半秒之後的第一個 tick 當第一拍，所以還是有相位可以用。"}],"value":{"type":"Object | null","en":"A new object each time you read it: `pos` is the number of bars since the start (0 is the downbeat of bar 1), `bar` and `beat` are 0..1 through the current bar and beat, `bpm` the tempo and `running` whether the transport is moving. `null` until a tempo is known.","zh":"每次讀都是一個新的物件：`pos` 是從 start 起算經過幾個小節（0 是第 1 小節的第一拍），`bar` 和 `beat` 是目前小節和拍子走了 0 到 1 的多少，`bpm` 是速度，`running` 是播放有沒有在動。知道速度之前是 `null`。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  OAVp5.input([0xFA]);                          // MIDI start\n  setInterval(() => OAVp5.input([0xF8]), 20);   // clock ticks: 24 a beat, here 50 a second (125 BPM)\n}\n\nfunction draw() {\n  background(20);\n  const p = OAVp5.clock.phase;\n  if (!p) return;\n  noStroke();\n  fill(255, 211, 107);\n  circle(40 + p.bar * (width - 80), 100, 30);   // once a bar (four beats)\n  fill(120, 190, 255);\n  circle(40 + p.beat * (width - 80), 200, 30);  // once a beat\n}","caption":{"en":"Two circles sweep from left to right on the made-up clock: the blue one once per beat, the yellow one once per bar of four beats.","zh":"兩個圓依著假的時鐘從左掃到右：藍色的每拍一次，黃色的每小節（四拍）一次。"},"run":"sketch","lang":"js"}],"related":["OAVp5.clock","OAVp5.clock.info","drumPhase","beatPhase"],"keywords":{"en":["phase","position in the bar","beat phase","bar phase","downbeat","sync to clock"],"zh":["相位","小節位置","拍子相位","第一拍","同步時鐘"]},"tbd":[],"claims":[{"expr":"OAVp5.clock.phase === null","equals":true}]},
{"id":"OAVp5.drums.clock","key":"OAVp5.drums.clock","name":"OAVp5.drums.clock","aliases":[],"kind":"object","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.drums.clock","source":{"file":"p5/oav-p5.js","line":1451,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1451"},"summary":{"en":"How the drum machine keeps time: which clock moves it, how far ahead it schedules and how long the callbacks wait for the speakers.","zh":"鼓機怎麼計時：用哪一種時鐘推動、提前排程多久，以及 callback 為了等喇叭而延後多久。"},"description":[{"en":"This is the diagnostic behind the 🩺 check-up. When drum hits feel late or early, look here first: the handbook puts a Bluetooth speaker at 0.15 to 0.25 seconds of `latency`, and [`?oav_drumlatency=200`](#option-drumlatency) sets it by hand. `late`, `skipped`, `warning` and `why` are further notes the check-up reads.","zh":"這是 🩺 檢查報告背後的診斷資料。打擊感覺太晚或太早的時候，先看這裡：說明文件寫藍牙喇叭的 `latency` 大約是 0.15 到 0.25 秒，[`?oav_drumlatency=200`](#option-drumlatency) 可以手動設定。`late`、`skipped`、`warning`、`why` 是檢查報告會讀的其他備註。"}],"value":{"type":"Object","range":"{ clock, heartbeat, lookahead, latency, audio, extended, preDraw, … }","en":"A new object on every read. `clock` is `'lookahead'` (hits are handed to the audio clock ahead of time, the default) or `'frame'` (moved by the frames, the [`drums`](#option-drums) option). `heartbeat` is `'worker'` (a Web Worker wakes it every 25 ms), `'interval'` (the fallback), `'frame'` with the frame clock, or `'none'` before the machine exists. `lookahead` is the seconds scheduled ahead and `extended` is true when a slow `draw()` made it look further ahead. `latency` is the seconds the callbacks wait for the output of the speakers, `audio` whether the kit's audio clock is running, and `preDraw` the p5 hook that delivers the callbacks right before `draw()`.","zh":"每次讀都是一個新的物件。`clock` 是 `'lookahead'`（打擊事先交給音訊時鐘排好，預設）或 `'frame'`（跟著畫面走，載入選項 [`drums`](#option-drums)）。`heartbeat` 是 `'worker'`（Web Worker 每 25 毫秒叫醒它）、`'interval'`（備用的做法）、用畫面時鐘的時候是 `'frame'`，鼓機還沒有的時候是 `'none'`。`lookahead` 是提前排程的秒數，`extended` 為 true 表示 `draw()` 太慢，讓它看得更遠。`latency` 是 callback 為了等喇叭輸出而延後的秒數，`audio` 是鼓組的音訊時鐘有沒有在跑，`preDraw` 是在 `draw()` 前面送 callback 的 p5 掛鉤。"},"params":[],"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  textSize(14);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const c = OAVp5.drums.clock;\n  text('clock: ' + c.clock + ' (' + c.heartbeat + ')', 20, 30);\n  text('lookahead: ' + c.lookahead + ' s', 20, 60);\n  text('latency: ' + c.latency + ' s', 20, 90);\n  text('hook before draw: ' + (c.preDraw || 'none'), 20, 120);\n}","caption":{"en":"The clock settings of the machine while it plays.","zh":"鼓機播放時的時鐘設定。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums","startDrums","drumPhase"],"keywords":{"en":["clock","latency","lookahead","timing","late","delay","heartbeat","diagnostic","check-up"],"zh":["時鐘","延遲","提前排程","時間準不準","診斷","檢查報告"]},"tbd":[{"en":"What exactly `late`, `skipped`, `warning` and `why` count was not checked.","zh":"`late`、`skipped`、`warning`、`why` 各自精確計的是什麼，還沒有查證。"}],"claims":[]},
{"id":"OAVp5.drums.machine","key":"OAVp5.drums.machine","name":"OAVp5.drums.machine","aliases":[],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.drums.machine","source":{"file":"p5/oav-p5.js","line":1437,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1437"},"summary":{"en":"The drum machine from @openav/drums that the grid on the page belongs to, or `null` before it has loaded.","zh":"頁面上那個鼓機格子背後的 @openav/drums 鼓機，還沒載入的時候是 `null`。"},"description":[{"en":"It is what `mountDrums()` of [@openav/drums](#mountDrums) returns, with `seq` (the sequencer), `engine` (the kit), `play(on)`, `setPattern(p)`, `setBpm(bpm)`, `phase()`, `now()`, `info()` and the rhythm functions. `OAVp5.drums.machine.play(false)` stops the sound and the counting but leaves the grid up: [`OAVp5.drums.state`](#OAVp5.drums.state) then says `'stopped'`. It is an advanced handle; the names on this page cover what a sketch normally needs.","zh":"就是 [@openav/drums](#mountDrums) 的 `mountDrums()` 回傳的東西，有 `seq`（序列器）、`engine`（鼓組）、`play(on)`、`setPattern(p)`、`setBpm(bpm)`、`phase()`、`now()`、`info()` 和節奏相關的函式。`OAVp5.drums.machine.play(false)` 會讓鼓機停止發聲和計數，但格子還留在畫面上：[`OAVp5.drums.state`](#OAVp5.drums.state) 這時是 `'stopped'`。它是進階的把手，這一頁的名稱已經涵蓋草圖平常需要的一切。"}],"value":{"type":"Object | null","en":"Set once, when [`startDrums()`](#startDrums) has loaded the machine. `null` before that and after a failed load.","zh":"[`startDrums()`](#startDrums) 把鼓機載入之後設定一次。在那之前，或載入失敗之後，都是 `null`。"},"params":[],"needs":{"en":"It is `null` until [`startDrums()`](#startDrums) (or 🥁) has loaded the machine. Check for `null` before you use it.","zh":"在 [`startDrums()`](#startDrums)（或 🥁）把鼓機載入之前都是 `null`，使用之前先檢查。"},"notes":[],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  startDrums('four on floor', 120);\n  textAlign(CENTER, CENTER);\n}\n\nfunction mousePressed() {\n  const m = OAVp5.drums.machine;\n  if (m) m.play(!m.seq.playing);   // pause and resume, the grid stays\n}\n\nfunction draw() {\n  background(OAVp5.drums.state === 'stopped' ? 80 : 20);\n  fill(255);\n  text('click to pause or resume (' + OAVp5.drums.state + ')', width / 2, height / 2);\n}","caption":{"en":"A click pauses the machine and the grid stays on the page; the state reads \"stopped\" until the next click.","zh":"點一下讓鼓機暫停，格子還留在頁面上，狀態會顯示 \"stopped\"，再點一下就繼續。"},"run":"sketch","lang":"js"}],"related":["OAVp5.drums","OAVp5.drums.state","startDrums"],"keywords":{"en":["machine","mountDrums","sequencer","engine","advanced","drums handle"],"zh":["鼓機","序列器","引擎","進階","把手"]},"tbd":[],"claims":[{"expr":"OAVp5.drums.machine","equals":"null"}]},
{"id":"OAVp5.hints","key":"OAVp5.hints","name":"OAVp5.hints","aliases":[],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.hints","source":{"file":"p5/oav-p5.js","line":1193,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1193"},"summary":{"en":"The list of cards oav-p5 has shown or would have shown so far (a blocked microphone, no Web MIDI, an error in your callback), each with what, why and what to press.","zh":"oav-p5 到現在顯示過（或本來會顯示）的卡片清單（麥克風被擋、沒有 Web MIDI、你的回呼出錯），每張都寫著發生什麼、為什麼、要按什麼。"},"description":[{"en":"Each situation is said once per page. The list holds them all, including the ones the page did not show because [`hints=off`](#option-hints) is set, so it is the place to look when a sketch does something odd and nothing is on screen: `console.log(OAVp5.hints)`.","zh":"每種狀況在一個頁面只說一次。這個清單把它們全都記著，包括因為設了 [`hints=off`](#option-hints) 而沒有顯示在頁面上的，所以草圖行為怪怪的、螢幕上又什麼都沒有的時候，就看這裡：`console.log(OAVp5.hints)`。"}],"value":{"type":"Array<Object>","en":"Starts empty and only grows. Each item is `{ id, kind, what, why, next }`: `id` names the situation (`'micDenied'`, `'chopOut:lag'`), `kind` is `'error'`, `'warn'` or `'info'`, and `what`, `why`, `next` are the three lines of the card in the browser's language.","zh":"一開始是空的，只會變長。每一項是 `{ id, kind, what, why, next }`：`id` 是這個狀況的名字（`'micDenied'`、`'chopOut:lag'`），`kind` 是 `'error'`、`'warn'` 或 `'info'`，`what`、`why`、`next` 是卡片上的三行字，用瀏覽器的語言寫。"},"params":[],"notes":[{"en":"A card that went away because the situation ended (the microphone came on) stays in the list.","zh":"因為狀況結束（麥克風開起來了）而收掉的卡片，還是會留在清單裡。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  lag(1);   // an unnamed lag() outside draw(): oav-p5 has something to say\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.hints.length + ' card(s) so far', 20, 30);\n  text(OAVp5.hints.map((h) => h.kind + ': ' + h.what).join('\\n'), 20, 60);\n}","caption":{"en":"Lists what oav-p5 has said so far: here, that lag() was called outside draw(). The same sentence is in the console.","zh":"列出 oav-p5 到現在說過的話：這裡是 lag() 在 draw() 外面被呼叫。同樣的句子也在 console 裡。"},"run":"sketch","lang":"js"}],"related":["option:hints","OAVp5.check","option:debug"],"keywords":{"en":["hints","cards","messages","warnings","what went wrong","errors list"],"zh":["提示","卡片","訊息","警告","哪裡出錯","錯誤清單"]},"tbd":[],"claims":[{"before":"lag(1)","expr":"OAVp5.hints.map((h) => h.id).join()","equals":"chopOut:lag"}]},
{"id":"OAVp5.link","key":"OAVp5.link","name":"OAVp5.link","aliases":[],"kind":"object","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.link","source":{"file":"p5/oav-p5.js","line":854,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L854"},"summary":{"en":"What the link to other oav-p5 pages in the same room is doing: only `OAVp5.link.info`, and `null` unless a room is set.","zh":"和同一個房間裡其他 oav-p5 頁面之間的連結現在的情形：只有 `OAVp5.link.info`，沒有設房間的時候是 `null`。"},"description":[{"en":"A link joins oav-p5 pages that share a room: what one page's programs play into OAV Virtual MIDI (notes, controllers and the clock) is heard by every other page in the room, at the moment it sounds there. You switch it on with [`?oav_link=my-room`](#option-link), and [`OAVp5.link.info`](#OAVp5.link.info) is where you look at how it is going. There is nothing else to call: the link is made and closed by the option.","zh":"連結把共用同一個房間的 oav-p5 頁面連在一起：一個頁面的程式送進 OAV Virtual MIDI 的東西（音符、控制器和時鐘），房間裡其他每個頁面都會在它在那邊響起的那一刻聽到。用 [`?oav_link=my-room`](#option-link) 打開它，[`OAVp5.link.info`](#OAVp5.link.info) 是看它進行得如何的地方。沒有別的東西要呼叫：連結的建立和結束都由那個選項決定。"}],"params":[],"notes":[{"en":"This is advanced. It needs a network connection to the relay ([`relay`](#option-relay)).","zh":"這是進階用法。它需要連得上中繼（[`relay`](#option-relay)）。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text(OAVp5.link.info ? 'linked' : 'not linked: add link: \\'my-room\\' to the options', 20, 30);\n}","caption":{"en":"Says whether this page is linked. Without a `link` room it is not, and the sketch says so.","zh":"說這個頁面有沒有連結。沒有 `link` 房間的話就沒有，草圖會這麼說。"},"run":"sketch","lang":"js"}],"related":["OAVp5.link.info","option:link","option:linkdelay","option:relay"],"keywords":{"en":["link","room","linked pages","share midi between pages","relay"],"zh":["連結","房間","連在一起的頁面","頁面之間分享 midi","中繼"]},"tbd":[],"claims":[{"expr":"OAVp5.link.info === null","equals":true}]},
{"id":"OAVp5.link.info","key":"OAVp5.link.info","name":"OAVp5.link.info","aliases":[],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.link.info","source":{"file":"p5/oav-p5.js","line":856,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L856"},"summary":{"en":"The state of the link: its room, `state` (`connecting`, `open`, `closed`), how many other pages are there, and the trip time. `null` when no room is set.","zh":"連結的狀態：它的房間、`state`（`connecting`、`open`、`closed`）、有幾個其他頁面在、還有來回的時間。沒有設房間時是 `null`。"},"description":[{"en":"Read it in `draw()` for a status line, or look at the 🔗 on the 🎹, which turns green when another page is there, yellow while connecting or alone, and red when the relay is gone. The 🩺 check-up prints the same facts. A new object is made on every read.","zh":"在 `draw()` 裡讀它來做狀態列，或是看 🎹 上的 🔗：另一個頁面在的時候是綠色，連線中或只有自己是黃色，中繼不見是紅色。🩺 體檢也印出同樣的資訊。每次讀都會產生一個新的物件。"}],"value":{"type":"Object | null","en":"`null` without a `link` room. With one: `{ room, state, peers, rtt, trip, late, sent, received, … }`. `state` is `'connecting'`, `'open'` or `'closed'` (the relay is gone and it retries). `peers` is how many other pages were heard in the last 6 seconds. `rtt` is the shortest round trip to the relay in the last 30 seconds and `trip` the one-way trip estimated from it, both in milliseconds. `late` counts notes that arrived after the moment they should have sounded and were played at once. `sent` and `received` count messages. The object also has `diff`, `lead`, `msgs`, `dupes` and `id`, which explain the clock alignment (packages/relay/link.js).","zh":"沒有 `link` 房間的時候是 `null`。有的話是 `{ room, state, peers, rtt, trip, late, sent, received, … }`。`state` 是 `'connecting'`、`'open'` 或 `'closed'`（中繼不見了，正在重試）。`peers` 是最近 6 秒內聽到幾個其他頁面。`rtt` 是最近 30 秒到中繼最短的一趟來回，`trip` 是由它估出來的單程時間，單位都是毫秒。`late` 數的是比該響的時間晚到、被立刻播出的音符。`sent` 和 `received` 數訊息。物件裡還有 `diff`、`lead`、`msgs`、`dupes` 和 `id`，它們說明時鐘怎麼對齊（packages/relay/link.js）。"},"params":[],"notes":[],"examples":[{"code":"window.OAV_P5 = { link: 'my-room' };\n\nfunction setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  const i = OAVp5.link.info;\n  text('room: ' + i.room + ',  state: ' + i.state, 20, 30);\n  text('other pages: ' + i.peers + ',  one-way trip: ' + (i.trip == null ? '-' : i.trip + ' ms'), 20, 60);\n}","caption":{"en":"Open the same address in two tabs: both say \"open\" and one other page, and show the trip time.","zh":"在兩個分頁打開同一個網址：兩邊都顯示 \"open\" 和一個其他頁面，並顯示來回的時間。"},"run":"static","lang":"js"},{"code":"function setup() {\n  createCanvas(400, 300);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('link.info: ' + JSON.stringify(OAVp5.link.info), 20, 30);   // null: no room\n}","caption":{"en":"Without a `link` room it prints null.","zh":"沒有 `link` 房間的話，印出 null。"},"run":"sketch","lang":"js"}],"related":["OAVp5.link","option:link","option:linkdelay","option:relay"],"keywords":{"en":["link info","link status","room","peers","latency","trip time"],"zh":["連結資訊","連結狀態","房間","其他頁面","延遲","來回時間"]},"tbd":[{"en":"The values between two real pages were not observed here: the headless check cannot reach a relay. The fields and their meanings are from the code (packages/relay/link.js) and the project's README.","zh":"兩個真的頁面之間的數值沒有親眼看過：無頭瀏覽器的檢查連不到中繼。欄位和它們的意思出自程式碼（packages/relay/link.js）和專案的 README。"}],"claims":[{"expr":"OAVp5.link.info === null","equals":true}]},
{"id":"OAVp5.sound.shell","key":"OAVp5.sound.shell","name":"OAVp5.sound.shell","aliases":["OAVp5.sound.engine","OAVp5.sound.module"],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.sound.shell","source":{"file":"p5/oav-p5.js","line":1318,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L1318"},"summary":{"en":"The loaded parts of @openav/sound: the `Sound` shell, its Tone.js engine and the module itself. All three are `null` until sound has loaded.","zh":"載入進來的 @openav/sound 各部分：`Sound` 外殼、它的 Tone.js 引擎，以及模組本身。聲音載入之前三個都是 `null`。"},"description":[{"en":"`OAVp5.sound.shell` is the `Sound` instance of [@openav/sound](#Sound), `OAVp5.sound.engine` is the Tone.js engine it plays through (`noteOn`, `noteOff`, `sustain`, `releaseAll`, `setInstrument`) and `OAVp5.sound.module` is the whole module, for example `OAVp5.sound.module.listInstruments()`, which lists every instrument id. They are advanced handles: the sketch-level names ([`startSound()`](#startSound), [`soundIsOn`](#soundIsOn)) cover everything a sketch normally needs.","zh":"`OAVp5.sound.shell` 是 [@openav/sound](#Sound) 的 `Sound` 實例，`OAVp5.sound.engine` 是它用來發聲的 Tone.js 引擎（`noteOn`、`noteOff`、`sustain`、`releaseAll`、`setInstrument`），`OAVp5.sound.module` 是整個模組，例如 `OAVp5.sound.module.listInstruments()` 會列出所有樂器代號。它們是進階的把手：草圖層級的名稱（[`startSound()`](#startSound)、[`soundIsOn`](#soundIsOn)）已經涵蓋草圖平常需要的一切。"}],"value":{"type":"Object | null","en":"Set once, when [`startSound()`](#startSound) has loaded @openav/sound (the engine and the shell at the same moment, the module a little before). `null` before that, and after a failed load.","zh":"[`startSound()`](#startSound) 載入 @openav/sound 之後設定一次（引擎和外殼同時，模組稍微早一點）。在那之前，或載入失敗之後，都是 `null`。"},"params":[],"needs":{"en":"Call [`startSound()`](#startSound) first and wait until [`OAVp5.sound.state`](#OAVp5.sound.state) is no longer `'loading'`, or check for `null` before you use them.","zh":"先呼叫 [`startSound()`](#startSound)，等 [`OAVp5.sound.state`](#OAVp5.sound.state) 離開 `'loading'` 之後再用，也可以在使用之前先檢查它是不是 `null`。"},"notes":[],"examples":[{"code":"let ids = [];\n\nfunction setup() {\n  createCanvas(400, 300);\n  startSound('synth/pluck');\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  textSize(12);\n  if (!ids.length && OAVp5.sound.module) ids = OAVp5.sound.module.listInstruments().map((d) => d.id);\n  text(ids.length ? ids.join(', ') : 'loading…', 20, 20, width - 40, height - 40);\n}","caption":{"en":"Lists every instrument id that [`startSound()`](#startSound) accepts, as soon as the module has loaded.","zh":"模組一載入，就列出 [`startSound()`](#startSound) 接受的所有樂器代號。"},"run":"sketch","lang":"js"}],"related":["OAVp5.sound","startSound","OAVp5.sound.state"],"keywords":{"en":["sound shell","engine","module","listInstruments","tone engine","instrument list","advanced"],"zh":["聲音外殼","引擎","模組","樂器清單","進階"]},"tbd":[],"claims":[{"expr":"OAVp5.sound.shell","equals":"null"}]},
{"id":"OAVp5.version","key":"OAVp5.version","name":"OAVp5.version","aliases":[],"kind":"variable","package":"oav-p5","category":"oavp5","documented":true,"signature":"OAVp5.version","source":{"file":"p5/oav-p5.js","line":259,"url":"https://github.com/frank890417/open-audiovisual/blob/main/p5/oav-p5.js#L259"},"summary":{"en":"The version of oav-p5 that loaded, as text such as `'0.8.0'`.","zh":"載入的 oav-p5 的版本，是像 `'0.8.0'` 這樣的文字。"},"description":[{"en":"Useful in a bug report or to check which build a page got: `openaudiovisual.com/p5/oav-p5.js` is always the latest, while a jsDelivr address with a tag is frozen. The same number is on the first line of the 🩺 check-up.","zh":"回報問題或確認頁面拿到哪個版本時有用：`openaudiovisual.com/p5/oav-p5.js` 永遠是最新版，帶版本標籤的 jsDelivr 網址則是固定的。同一個數字也在 🩺 體檢的第一行。"}],"value":{"type":"String","en":"Set when oav-p5 loads and never changes.","zh":"oav-p5 載入時設定，之後不會變。"},"params":[],"notes":[{"en":"If the file loads twice (the loader line and the OpenProcessing library list), the first one wins and the second does nothing.","zh":"如果這個檔案載入了兩次（載入那一行和 OpenProcessing 的函式庫清單各一次），先載入的那一個生效，第二個什麼都不做。"}],"examples":[{"code":"function setup() {\n  createCanvas(400, 300);\n  textAlign(CENTER, CENTER);\n  textSize(32);\n}\n\nfunction draw() {\n  background(20);\n  fill(255);\n  text('oav-p5 ' + OAVp5.version, width / 2, height / 2);\n}","caption":{"en":"Shows the version of oav-p5 this page loaded.","zh":"顯示這個頁面載入的 oav-p5 版本。"},"run":"sketch","lang":"js"}],"related":["OAVp5","OAVp5.check"],"keywords":{"en":["version","build","which version","release"],"zh":["版本","版次","哪個版本","發行"]},"tbd":[],"claims":[{"expr":"typeof OAVp5.version","equals":"string"}]},
{"id":"BEND_CENTER","key":"BEND_CENTER","name":"BEND_CENTER","aliases":[],"kind":"constant","package":"@openav/midi","category":"midi-io","documented":true,"signature":"BEND_CENTER = 8192","import":"import { BEND_CENTER } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":15,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L15"},"summary":{"en":"The resting value of a pitch-bend wheel: 8192, the middle of the 14-bit range 0..16383.","zh":"彎音輪休息時的數值：8192，也就是 14 位元範圍 0..16383 的中間。"},"description":[{"en":"Use it when you build a pitch-bend message by hand: `{ type: 'pitchbend', ch: 1, value: BEND_CENTER }` is a wheel at rest. Spring-loaded wheels in a profile (`spring`) go back to this value when released.","zh":"手動組彎音訊息時用得到：`{ type: 'pitchbend', ch: 1, value: BEND_CENTER }` 就是休息中的彎音輪。設定檔裡帶彈簧的彎音輪（`spring`）放開後會回到這個值。"}],"value":{"type":"Number","range":"8192","en":"A constant. A pitch-bend `value` equal to it means \"not bent\" and becomes 0 in [`bendToUnit()`](#bendToUnit).","zh":"常數。彎音輪的 `value` 等於它就表示「沒有彎」，在 [`bendToUnit()`](#bendToUnit) 裡會變成 0。"},"params":[],"notes":[],"examples":[{"code":"import { BEND_CENTER, encodeMessage } from '@openav/midi';\n\nconsole.log(BEND_CENTER);                                          // 8192\nconsole.log(encodeMessage({ type: 'pitchbend', ch: 1, value: BEND_CENTER }));","caption":{"en":"Prints 8192, then the bytes of a pitch bend at rest.","zh":"印出 8192，再印出休息位置彎音訊息的位元組。"},"run":"module","lang":"js"}],"related":["bendToUnit","unitToBend"],"keywords":{"en":["pitch bend centre","8192","rest","wheel"],"zh":["彎音輪中心","8192","休息位置"]},"tbd":[],"claims":[]},
{"id":"bendToUnit","key":"bendToUnit","name":"bendToUnit","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"bendToUnit(v)","import":"import { bendToUnit } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":92,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L92"},"summary":{"en":"Turns a 14-bit pitch-bend value (0..16383) into −1..1, with the centre (8192) exactly 0.","zh":"把 14 位元的彎音輪數值（0..16383）轉成 −1..1，中心（8192）剛好是 0。"},"description":[{"en":"MIDI centres pitch bend at 8192, so there are 8192 steps below the centre and 8191 above. Below the centre it divides by 8192 and above by 8191, so the bottom reaches exactly −1, the top exactly +1 and the rest position exactly 0. This is the number published as `midi/bend` and `midi/ch/<ch>/bend`.","zh":"MIDI 把彎音輪的中心定在 8192，所以中心以下有 8192 級，以上有 8191 級。它在中心以下除以 8192，以上除以 8191，因此最低剛好是 −1，最高剛好是 +1，休息位置剛好是 0。發佈成 `midi/bend` 和 `midi/ch/<ch>/bend` 的就是這個數字。"}],"params":[{"name":"v","type":"Number","en":"A pitch-bend value, 0..16383 (what `parseMessage()` puts in `value`).","zh":"彎音輪的數值，0..16383（`parseMessage()` 放在 `value` 裡的那個）。"}],"returns":{"type":"Number","en":"−1 at 0, 0 at 8192, +1 at 16383.","zh":"0 時是 −1，8192 時是 0，16383 時是 +1。"},"notes":[],"examples":[{"code":"import { bendToUnit, unitToBend } from '@openav/midi';\n\nconsole.log(bendToUnit(0), bendToUnit(8192), bendToUnit(16383));   // -1 0 1\nconsole.log(bendToUnit(12288));                                    // about 0.5\nconsole.log(unitToBend(bendToUnit(5000)));                         // 5000","caption":{"en":"Prints −1, 0, 1, then about 0.5, then 5000: the round trip is exact.","zh":"印出 −1、0、1，接著約 0.5，最後 5000：來回轉換不會失真。"},"run":"module","lang":"js"}],"related":["unitToBend","BEND_CENTER","genericSignals"],"keywords":{"en":["pitch bend","bend","wheel","14-bit","normalize","midi/bend"],"zh":["彎音輪","彎音","14 位元","正規化","midi/bend"]},"tbd":[],"claims":[]},
{"id":"CONTROL_TYPES","key":"CONTROL_TYPES","name":"CONTROL_TYPES","aliases":[],"kind":"constant","package":"@openav/midi","category":"midi-io","documented":true,"signature":"CONTROL_TYPES = array, 8 items","import":"import { CONTROL_TYPES } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":27,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L27"},"summary":{"en":"The kinds of control a profile may contain: `knob`, `encoder`, `fader`, `pad`, `button`, `keys`, `wheel`, `strip`.","zh":"設定檔裡可以出現的控制項種類：`knob`、`encoder`、`fader`、`pad`、`button`、`keys`、`wheel`、`strip`。"},"description":[{"en":"What each sends: `knob` and `fader` a CC (0..1); `encoder` a CC that is either relative (position plus a `/delta` signal) or absolute; `pad` a note (velocity while held, with a `/hit` pulse) or a CC; `button` a note or CC, momentary or toggle; `keys` a range of notes; `wheel` and `strip` a CC or pitch bend, optionally bipolar and spring-loaded.","zh":"各自送什麼：`knob` 和 `fader` 送 CC（0..1）。`encoder` 送 CC，可以是相對（位置加上 `/delta` 訊號）或絕對。`pad` 送音符（按著時是力度，並有 `/hit` 脈衝）或 CC。`button` 送音符或 CC，可以是瞬間或切換。`keys` 是一段音域的音符。`wheel` 和 `strip` 送 CC 或彎音，可以是雙向，也可以帶彈簧。"}],"value":{"type":"string[]","range":"['knob', 'encoder', 'fader', 'pad', 'button', 'keys', 'wheel', 'strip']","en":"A plain array. A control's `type` must be one of these or [`validateProfile()`](#validateProfile) reports it.","zh":"一個普通陣列。控制項的 `type` 必須是其中之一，否則 [`validateProfile()`](#validateProfile) 會回報。"},"params":[],"notes":[],"examples":[{"code":"import { CONTROL_TYPES, PROFILES } from '@openav/midi';\n\nconst count = {};\nfor (const p of PROFILES) for (const c of p.controls) count[c.type] = (count[c.type] || 0) + 1;\nconsole.log(CONTROL_TYPES.map((t) => t + ': ' + (count[t] || 0)).join(', '));","caption":{"en":"Prints how many controls of each type the shipped profiles hold, in the order of the list.","zh":"依清單順序，印出內建設定檔裡每種控制項各有幾個。"},"run":"module","lang":"js"}],"related":["validateProfile","RESERVED_IDS","normalizeProfile"],"keywords":{"en":["control types","knob","fader","pad","encoder","strip","wheel","button","keys"],"zh":["控制項類型","旋鈕","推桿","打擊墊","編碼器","觸控條","彎音輪","按鈕","琴鍵"]},"tbd":[],"claims":[]},
{"id":"controllerRoutes","key":"controllerRoutes","name":"controllerRoutes","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"controllerRoutes(profile, bindings, { short = profile.short, ...extra } = {})","import":"import { controllerRoutes } from '@openav/midi';","source":{"file":"packages/midi/manager.js","line":159,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/manager.js#L159"},"summary":{"en":"Builds mapper routes for \"this control drives that param\": `controllerRoutes(profile, { knob1: 'hue' })` routes the knob's signal to the param `hue`.","zh":"建立「這個控制項推動那個參數」的 Mapper 路由：`controllerRoutes(profile, { knob1: 'hue' })` 會把旋鈕的訊號接到參數 `hue`。"},"description":[{"en":"This is rule 1 of the project (continuous control goes through params): a world does not read `midi/lpd8/k1` in `update()`, it declares a param and lets a route drive it. An id the profile does not have throws `profile akai-lpd8 has no control \"x\"`, so a typo is loud.","zh":"這是專案的第一條規則（連續控制要經過參數）：世界不在 `update()` 裡讀 `midi/lpd8/k1`，而是宣告一個參數，由路由來推動它。設定檔沒有的 id 會丟出 `profile akai-lpd8 has no control \"x\"`，打錯字一定看得見。"}],"params":[{"name":"profile","type":"Object","en":"The device's profile (raw or normalized): used to check the ids and to know which controls are bipolar.","zh":"那台機器的設定檔（原始或整理過的都行）：用來檢查 id，並判斷哪些控制項是雙向的。"},{"name":"bindings","type":"Object","en":"`{ controlId: target }`, where `target` is a param name or a route object (`{ target, inMin, inMax, … }`). A pad written as `{ target: 'burst', on: 'hit' }` listens to the `/hit` pulse instead of its velocity.","zh":"`{ 控制項 id: target }`，`target` 是參數名稱，或路由物件（`{ target, inMin, inMax, … }`）。打擊墊寫成 `{ target: 'burst', on: 'hit' }` 時，聽的是 `/hit` 脈衝而不是力度。"},{"name":"short","type":"String","en":"The signal namespace. The default is the profile's `short`; give another when you run the device under a different one.","zh":"訊號的命名空間，預設是設定檔的 `short`。同一台機器用了別的命名空間時才需要改。"},{"name":"extra","type":"Object","optional":true,"en":"Every other key of the third argument (`{ inMin: 0, inMax: 0.5 }`) is copied onto each route as a default; a key in the route object itself wins.","zh":"第三個參數裡其他的鍵（例如 `{ inMin: 0, inMax: 0.5 }`）會當成預設值複製到每一條路由上，路由物件自己有的鍵優先。"}],"returns":{"type":"Object[]","en":"`[{ source, target, … }]`, ready for `mapper.addRoute(route)`. A bipolar control (a pitch wheel) gets `inMin: -1, inMax: 1`. Any other key in the third argument is copied onto every route.","zh":"`[{ source, target, … }]`，可以直接用 `mapper.addRoute(route)`。雙向的控制項（彎音輪）會自動加上 `inMin: -1, inMax: 1`。第三個參數裡的其他鍵，會複製到每一條路由上。"},"notes":[],"examples":[{"code":"import { controllerRoutes, profileById } from '@openav/midi';\n\nconst routes = controllerRoutes(profileById('lpd8'), {\n  k1: 'hue',                                   // knob 1 drives the param \"hue\"\n  k2: { target: 'size', inMin: 0, inMax: 1 },  // a route object, extra keys are kept\n  pad1: { target: 'burst', on: 'hit' },        // fire on the hit, not on the velocity\n});\nfor (const r of routes) console.log(r.source, '->', r.target);","caption":{"en":"Prints `midi/lpd8/k1 -> hue`, `midi/lpd8/k2 -> size` and `midi/lpd8/pad1/hit -> burst`: the signal each control drives its param from.","zh":"印出 `midi/lpd8/k1 -> hue`、`midi/lpd8/k2 -> size` 和 `midi/lpd8/pad1/hit -> burst`：每個控制項用哪個訊號去推動它的參數。"},"run":"module","lang":"js"}],"related":["MidiController","profileSignals","linkControllers"],"keywords":{"en":["map knob to param","route","mapper","bind control","continuous control","knob to hue"],"zh":["旋鈕對應參數","路由","mapper","綁定控制項","連續控制","旋鈕控制色相"]},"tbd":[],"claims":[]},
{"id":"createVirtualMIDIAccess","key":"createVirtualMIDIAccess","name":"createVirtualMIDIAccess","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"createVirtualMIDIAccess({ name = 'OAV Virtual MIDI', manufacturer = 'open-audiovisual', id = 'oav-virtual', sysex = false, onOutput = null, onClear = null } = {})","import":"import { createVirtualMIDIAccess } from '@openav/midi';","source":{"file":"packages/midi/virtual-access.js","line":139,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/virtual-access.js#L139"},"summary":{"en":"Builds a Web MIDI `MIDIAccess` that always has one more input and output of its own, so a sketch written for a MIDI keyboard works with none plugged in.","zh":"建立一個 Web MIDI 的 `MIDIAccess`，它永遠多出一組自己的輸入和輸出，讓寫給 MIDI 鍵盤的草圖在沒有接任何機器時也能動。"},"description":[{"en":"A 2018 sketch that calls `navigator.requestMIDIAccess()` (or WebMidi.js) and waits for a keyboard shows nothing on a laptop without one, and on an iPad has no Web MIDI at all. Hand it this access instead and the on-screen controller becomes a real-looking MIDI input: the sketch's own unmodified code receives `midimessage` events with the same bytes a keyboard sends.","zh":"2018 年寫的草圖呼叫 `navigator.requestMIDIAccess()`（或 WebMidi.js）然後等鍵盤，在沒有鍵盤的筆電上什麼都不會出現，在 iPad 上更是完全沒有 Web MIDI。改成交給它這個 access，螢幕上的控制器就變成一個看起來像真的 MIDI 輸入：草圖原封不動的程式碼，會收到跟鍵盤送出的位元組一模一樣的 `midimessage` 事件。"},{"en":"The virtual OUTPUT is a real-looking port too: a program that plays out to it (Strudel's `.midi('OAV Virtual MIDI')`, a WebMidi.js sketch) calls `send(bytes, timestamp)`, and `onOutput` hears both. Real ports can be merged in with `addReal(realAccess)`: they come FIRST in `inputs`, the virtual one last, so a sketch that only listens to `inputs[0]` takes the hardware when it is there and the virtual controller when it is not. Real ports pass through untouched, so nothing is delivered twice.","zh":"虛擬「輸出」也長得像真的埠：對它輸出的程式（Strudel 的 `.midi('OAV Virtual MIDI')`、WebMidi.js 草圖）會呼叫 `send(bytes, timestamp)`，`onOutput` 會同時收到位元組和時間戳。可以用 `addReal(realAccess)` 併入真實的埠：它們排在 `inputs` 的「前面」，虛擬的排最後，所以只聽 `inputs[0]` 的草圖，有硬體時用硬體，沒有時用虛擬控制器。真實的埠原封不動地穿過，所以不會重複送達。"}],"params":[{"name":"name","type":"String","en":"The name of the virtual port, as the sketch sees it.","zh":"虛擬埠的名稱，也就是草圖看到的名字。"},{"name":"manufacturer","type":"String","en":"The `manufacturer` the virtual ports report.","zh":"虛擬埠回報的 `manufacturer`。"},{"name":"id","type":"String","en":"The port id prefix: the ports are `<id>-in` and `<id>-out`.","zh":"埠 id 的前綴：埠的 id 是 `<id>-in` 和 `<id>-out`。"},{"name":"sysex","type":"Boolean","en":"What `access.sysexEnabled` reports.","zh":"`access.sysexEnabled` 回報的值。"},{"name":"onOutput","type":"Function | null","en":"`(bytes, timestamp) => …` called when a program sends to the virtual OUTPUT. `timestamp` is when the bytes should sound (`performance.now()` milliseconds), or `undefined` for \"now\".","zh":"程式送到虛擬「輸出」時呼叫的 `(bytes, timestamp) => …`。`timestamp` 是這些位元組該發聲的時間（`performance.now()` 毫秒），`undefined` 表示「現在」。"},{"name":"onClear","type":"Function | null","en":"Called when the program calls `output.clear()` (Web MIDI: drop what is still waiting).","zh":"程式呼叫 `output.clear()` 時被呼叫（Web MIDI 的意思是：丟掉還在排隊的）。"}],"returns":{"type":"Object","en":"`{ access, input, output, emit(bytes), addReal(realAccess), real, requests, dispose() }`. Give `access` to the program. `emit()` plays bytes into the virtual input.","zh":"`{ access, input, output, emit(bytes), addReal(realAccess), real, requests, dispose() }`。把 `access` 交給程式。`emit()` 把位元組彈進虛擬輸入。"},"notes":[{"en":"`access.inputs` and `access.outputs` are `Map`s, as in Web MIDI. `input.listening` counts the listeners a program put on the port, and `requests` counts how often [`virtualRequestMIDIAccess()`](#virtualRequestMIDIAccess) handed the access out.","zh":"跟 Web MIDI 一樣，`access.inputs` 和 `access.outputs` 都是 `Map`。`input.listening` 是程式在這個埠上掛了幾個監聽，`requests` 是 [`virtualRequestMIDIAccess()`](#virtualRequestMIDIAccess) 把這個 access 交出去幾次。"}],"examples":[{"code":"import { createVirtualMIDIAccess } from '@openav/midi';\n\nconst v = createVirtualMIDIAccess({\n  name: 'Demo MIDI',\n  onOutput: (bytes, timestamp) => console.log('program played', bytes, timestamp),\n});\n\nconst input = [...v.access.inputs.values()][0];\nconst output = [...v.access.outputs.values()][0];\nconsole.log(input.name, '|', input.listening, 'listeners');\n\ninput.onmidimessage = (e) => console.log('program heard', [...e.data]);   // what a 2018 sketch does\nv.emit([0x90, 60, 100]);              // the on-screen keyboard plays: program heard [144, 60, 100]\noutput.send([0x90, 62, 80]);          // the program plays out:      program played [144, 62, 80] undefined","caption":{"en":"Prints the port name with 0 listeners, then `program heard [144, 60, 100]`, then `program played [144, 62, 80] undefined` (no timestamp means \"now\").","zh":"印出埠名與 0 個監聽，接著是 `program heard [144, 60, 100]`，再來是 `program played [144, 62, 80] undefined`（沒有時間戳就是「現在」）。"},"run":"module","lang":"js"}],"related":["virtualRequestMIDIAccess","Midi","MidiController"],"keywords":{"en":["virtual midi","web midi shim","requestMIDIAccess","webmidi.js","fake midi input","no hardware","ipad","strudel"],"zh":["虛擬 midi","web midi 轉接","requestMIDIAccess","webmidi.js","假的 midi 輸入","沒有硬體","ipad","strudel"]},"tbd":[{"en":"Not tried for this page: a real WebMidi.js or Strudel sketch talking to the virtual ports, and how the `onOutput` timestamps line up with a hardware port. The example calls the ports directly.","zh":"這一頁沒有實測：真正的 WebMidi.js 或 Strudel 草圖跟虛擬埠對話，以及 `onOutput` 的時間戳跟硬體埠比起來對不對得上。範例是直接呼叫那些埠。"}],"claims":[]},
{"id":"DEFAULT_PROFILE","key":"DEFAULT_PROFILE","name":"DEFAULT_PROFILE","aliases":[],"kind":"constant","package":"@openav/midi","category":"midi-io","documented":true,"signature":"DEFAULT_PROFILE = \"arturia-minilab3\"","import":"import { DEFAULT_PROFILE } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":26,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L26"},"summary":{"en":"The profile id used when none is given: `'arturia-minilab3'`.","zh":"沒有指定時使用的設定檔 id：`'arturia-minilab3'`。"},"description":[{"en":"[`parseEmbedOptions()`](#parseEmbedOptions) fills `profile` with it. [`createController()`](#createController) and [`ControllerCanvas`](#ControllerCanvas) have the same id as their own default.","zh":"[`parseEmbedOptions()`](#parseEmbedOptions) 會用它填入 `profile`。[`createController()`](#createController) 和 [`ControllerCanvas`](#ControllerCanvas) 自己的預設值也是同一個 id。"}],"value":{"type":"String","range":"'arturia-minilab3'","en":"A constant. `<oav-controller>` without a `profile` attribute and the iframe embed without `?profile=` show this device.","zh":"常數。沒有 `profile` 屬性的 `<oav-controller>`，和沒有 `?profile=` 的 iframe 嵌入，都會顯示這台機器。"},"params":[],"notes":[],"examples":[{"code":"import { DEFAULT_PROFILE, parseEmbedOptions } from '@openav/midi/embed';\n\nconsole.log(DEFAULT_PROFILE);\nconsole.log(parseEmbedOptions({}).profile === DEFAULT_PROFILE);   // true","caption":{"en":"Prints `arturia-minilab3`, then `true`.","zh":"印出 `arturia-minilab3`，再印出 `true`。"},"run":"module","lang":"js"}],"related":["parseEmbedOptions","createController"],"keywords":{"en":["default profile","minilab3","default controller"],"zh":["預設設定檔","minilab3","預設控制器"]},"tbd":[],"claims":[]},
{"id":"describe","key":"describe","name":"describe","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"describe(ev)","import":"import { describe } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":195,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L195"},"summary":{"en":"Returns a short readable line for a parsed MIDI event, for logs and learn screens: `CC ch1 #74=64`, `Bend ch1 0.50`.","zh":"把解析過的 MIDI 事件變成一行好讀的文字，給記錄和學習畫面用：`CC ch1 #74=64`、`Bend ch1 0.50`。"},"description":[{"en":"The formats are `Note On  ch1 n60 v100` (two spaces after \"On\"), `Note Off ch1 n60`, `CC ch1 #74=64`, `Bend ch1 0.50` (the bend as −1..1 with two decimals), `Pressure ch1 50`, `Poly AT ch1 n60 33` and `Program ch1 5`. The [`Midi`](#Midi) engine writes one of these to its log for each message it parses (clock is left out).","zh":"格式是 `Note On  ch1 n60 v100`（On 後面兩個空格）、`Note Off ch1 n60`、`CC ch1 #74=64`、`Bend ch1 0.50`（彎音換成 −1..1，兩位小數）、`Pressure ch1 50`、`Poly AT ch1 n60 33`、`Program ch1 5`。[`Midi`](#Midi) 引擎解析的每則訊息（時鐘除外），都會把這樣的一行寫進它的記錄。"}],"params":[{"name":"ev","type":"Object","en":"An event from [`parseMessage()`](#parseMessage).","zh":"[`parseMessage()`](#parseMessage) 回傳的事件。"}],"returns":{"type":"String","en":"The line. `null` gives `?`, and a type without a format (`clock`, `sysex`) gives its type name.","zh":"那一行文字。給 `null` 回傳 `?`，沒有格式的類型（`clock`、`sysex`）則回傳類型名稱。"},"notes":[{"en":"Easily confused with `describeMessage()` and `describeDetail()`, which write the shorter lines the `<oav-controller>` readout shows (`CC 70 = 53`). This one takes an event and is meant for logs.","zh":"容易跟 `describeMessage()`、`describeDetail()` 搞混：那兩個寫的是 `<oav-controller>` 讀數列用的較短文字（`CC 70 = 53`）。這一個吃事件物件，是給記錄用的。"}],"examples":[{"code":"import { parseMessage, describe } from '@openav/midi';\n\nfor (const bytes of [[0x90, 60, 100], [0xb0, 74, 64], [0xe0, 0, 96]]) {\n  console.log(describe(parseMessage(bytes)));\n}","caption":{"en":"Prints `Note On  ch1 n60 v100`, `CC ch1 #74=64` and `Bend ch1 0.50`.","zh":"印出 `Note On  ch1 n60 v100`、`CC ch1 #74=64` 和 `Bend ch1 0.50`。"},"run":"module","lang":"js"}],"related":["parseMessage","describeMessage","describeDetail"],"keywords":{"en":["describe","log line","readable","midi monitor","text"],"zh":["描述","記錄文字","好讀","midi 監看","文字"]},"tbd":[],"claims":[]},
{"id":"EchoGuard","key":"EchoGuard","name":"EchoGuard","aliases":[],"kind":"class","package":"@openav/midi","category":"midi-io","documented":true,"signature":"new EchoGuard({ window = 40, max = 256 } = {})","import":"import { EchoGuard } from '@openav/midi';","source":{"file":"packages/midi/ports.js","line":72,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/ports.js#L72"},"summary":{"en":"Recognises our own MIDI messages coming back: remember what was sent, and an identical message arriving within a short window is an echo, not a performer.","zh":"認出「自己送出的 MIDI 又回來了」：記住送出過什麼，短時間內抵達的相同訊息就是回音，不是演奏者彈的。"},"description":[{"en":"A virtual cable or a DAW can send your message straight back on an input, and the engine would treat it as a performer. `sent(bytes)` records a message and `isEcho(bytes)` asks whether an arriving one is that message returning. Each sent message cancels at most ONE echo, so two real identical messages are not both eaten.","zh":"虛擬線路或 DAW 可能把你送出的訊息原樣從輸入送回來，引擎會把它當成演奏者。`sent(bytes)` 記下一則訊息，`isEcho(bytes)` 問「剛到的這則是不是那一則回來了」。每則送出的訊息最多只抵銷「一個」回音，所以兩則真正相同的訊息不會被一起吃掉。"},{"en":"A note-on with velocity 0 counts as a note-off, in both directions, because a DAW may rewrite one as the other. System messages (clock and the like) are never recorded. [`Midi`](#Midi) has one (`midi.echo`, with the `echoWindow` option) and drops the echoes it finds, counted in `midi.echoDropped`.","zh":"力度 0 的 note-on 當成 note-off，雙向都算，因為 DAW 可能把其中一種改寫成另一種。系統訊息（時鐘之類）不會被記錄。[`Midi`](#Midi) 內建一個（`midi.echo`，由 `echoWindow` 選項設定），並把找到的回音丟掉，數量記在 `midi.echoDropped`。"}],"params":[{"name":"window","type":"Number","en":"Milliseconds an echo may take to come back. `0` turns the guard off.","zh":"回音可以晚多久回來（毫秒）。`0` 表示關閉。"},{"name":"max","type":"Number","en":"How many sent messages to remember. Only the newest `max` are kept.","zh":"要記住幾則送出的訊息，只留最新的 `max` 則。"}],"notes":[],"examples":[{"code":"import { EchoGuard } from '@openav/midi';\n\nconst guard = new EchoGuard({ window: 40 });\nguard.sent([0x90, 60, 100], 1000);                 // we sent note-on 60 at t = 1000 ms\n\nconsole.log(guard.isEcho([0x90, 60, 100], 1010));  // true: it came back 10 ms later\nconsole.log(guard.isEcho([0x90, 60, 100], 1011));  // false: one send explains one echo\nguard.sent([0x90, 62, 100], 2000);\nconsole.log(guard.isEcho([0x90, 62, 100], 2100));  // false: 100 ms later is a performer","caption":{"en":"Prints `true`, `false`, `false`: an echo is used up once, and a message that arrives too late is a real note.","zh":"印出 `true`、`false`、`false`：回音只抵銷一次，太晚到的訊息是真的音符。"},"run":"module","lang":"js"}],"related":["Midi","isLive"],"keywords":{"en":["echo","feedback loop","loopback","duplicate","own messages","loopMIDI"],"zh":["回音","回授迴圈","迴路","重複","自己的訊息","loopMIDI"]},"tbd":[],"claims":[],"members":[{"name":"sent","member":"method","signature":"bytes, now = performance.now()","doc":"Record a message we just sent.","line":79},{"name":"isEcho","member":"method","signature":"bytes, now = performance.now()","doc":"Is this arriving message our own coming back?","line":85},{"name":"clear","member":"method","signature":"","doc":"","line":99}]},
{"id":"encodeMessage","key":"encodeMessage","name":"encodeMessage","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"encodeMessage(ev)","import":"import { encodeMessage } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":48,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L48"},"summary":{"en":"Turns an event object back into MIDI bytes, the inverse of `parseMessage()`: `{ type: 'cc', ch: 1, cc: 74, value: 64 }` becomes `[0xb0, 74, 64]`.","zh":"把事件物件轉回 MIDI 位元組，是 `parseMessage()` 的反向：`{ type: 'cc', ch: 1, cc: 74, value: 64 }` 會變成 `[0xb0, 74, 64]`。"},"description":[{"en":"`encodeMessage({ type: 'noteon', ch: 10, note: 36, vel: 127 })` is `[0x99, 36, 127]`. The channel is held to 1..16 and every data byte is rounded and held to 0..127, so a bad value cannot produce an invalid message. A missing `vel` is 100 for `noteon` and 0 for `noteoff`.","zh":"`encodeMessage({ type: 'noteon', ch: 10, note: 36, vel: 127 })` 得到 `[0x99, 36, 127]`。頻道會限制在 1..16，每個資料位元組都會四捨五入並限制在 0..127，所以錯誤的數值做不出不合法的訊息。沒給 `vel` 的話，`noteon` 是 100，`noteoff` 是 0。"},{"en":"A `pitchbend` `value` (0..16383) is split into two 7-bit bytes, low byte first, as MIDI requires. The package uses this for the on-screen controller and for LED feedback.","zh":"`pitchbend` 的 `value`（0..16383）會照 MIDI 規定拆成兩個 7 位元的位元組，低位在前。套件用它來做螢幕上的控制器和 LED 回饋。"}],"params":[{"name":"ev","type":"Object","en":"An event: `type` is `noteon`, `noteoff`, `polyat`, `cc`, `program`, `chanat` or `pitchbend`, with the fields [`parseMessage()`](#parseMessage) uses for that type. `ch` defaults to 1.","zh":"事件物件：`type` 是 `noteon`、`noteoff`、`polyat`、`cc`、`program`、`chanat` 或 `pitchbend`，欄位跟 [`parseMessage()`](#parseMessage) 對該類型用的一樣。`ch` 預設是 1。"}],"returns":{"type":"number[] | null","en":"The bytes, ready for `output.send()`. `null` for a type it cannot make (`clock`, `sysex`, anything unknown).","zh":"位元組陣列，可以直接給 `output.send()`。做不出來的類型（`clock`、`sysex`、不認得的）回傳 `null`。"},"notes":[],"examples":[{"code":"import { encodeMessage, parseMessage } from '@openav/midi';\n\nconst bytes = encodeMessage({ type: 'noteon', ch: 10, note: 36, vel: 127 });\nconsole.log(bytes);                // [153, 36, 127]\nconsole.log(parseMessage(bytes));  // back to the same event","caption":{"en":"Prints `[153, 36, 127]` (0x99, 0x24, 0x7f), then the event it came from.","zh":"印出 `[153, 36, 127]`（0x99、0x24、0x7f），再印出它原本的事件。"},"run":"module","lang":"js"},{"code":"import { encodeMessage } from '@openav/midi';\n\nconsole.log(encodeMessage({ type: 'pitchbend', ch: 1, value: 8192 }));  // centre: [224, 0, 64]\nconsole.log(encodeMessage({ type: 'cc', ch: 1, cc: 74, value: 300 }));  // value held to 127\nconsole.log(encodeMessage({ type: 'clock' }));                          // null","caption":{"en":"Three results: the pitch-bend centre, a value held to 127, and `null` for a message type it cannot make.","zh":"三個結果：彎音輪的中心、被限制在 127 的數值，以及做不出來的訊息類型回傳 `null`。"},"run":"module","lang":"js"}],"related":["parseMessage","describe","unitToBend","Midi"],"keywords":{"en":["encode","midi bytes","build message","note on bytes","send","inverse of parse"],"zh":["編碼","midi 位元組","組訊息","送出","parse 的反向"]},"tbd":[],"claims":[]},
{"id":"eventOfSignal","key":"eventOfSignal","name":"eventOfSignal","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"eventOfSignal(name, value, { channel = 1 } = {})","import":"import { eventOfSignal } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":156,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L156"},"summary":{"en":"The inverse of `genericSignals()`: turns one generic MIDI signal (`midi/ch/10/note/36`, `midi/cc/74`) back into the MIDI event it came from.","zh":"`genericSignals()` 的反向：把一個通用的 MIDI 訊號（`midi/ch/10/note/36`、`midi/cc/74`）轉回它原本的 MIDI 事件。"},"description":[{"en":"It lets anything that only hears a [`Signals`](/docs/#signals-openav-midi) bus (a recorded take playing back, the on-screen piano, a phone) drive a controller model as if the bytes had arrived. Values come back on the 0..127 scale they were published from (value × 127, rounded), so the round trip through `genericSignals()` is exact.","zh":"有了它，只聽得到 [`Signals`](/zh/docs/#signals-openav-midi) 匯流排的東西（正在回放的錄音、螢幕上的鋼琴、手機），也能像收到原始位元組一樣去推動控制器模型。數值會還原成當初發佈時的 0..127 尺度（值 × 127 再四捨五入），所以跟 `genericSignals()` 來回轉換不會失真。"},{"en":"A note signal with a value above 0 is a `noteon` (velocity at least 1), and 0 is a `noteoff`. The on-screen piano publishes channel 0, which is read as `channel`. A hardware message is published under both families, so a listener that wants each message once should use [`followMidiSignals()`](#followMidiSignals) instead of calling this on every name.","zh":"音符訊號的值大於 0 是 `noteon`（力度至少 1），等於 0 是 `noteoff`。螢幕上的鋼琴發佈的頻道是 0，會被當成 `channel` 參數。硬體的一則訊息會用兩組名稱各發佈一次，所以想「每則訊息只處理一次」的話，請用 [`followMidiSignals()`](#followMidiSignals)，不要對每個名稱都呼叫這個函式。"}],"params":[{"name":"name","type":"String","en":"A signal name. Only the generic names starting with `midi/` are understood.","zh":"訊號名稱。只認得 `midi/` 開頭的通用名稱。"},{"name":"value","type":"any","en":"The signal's value: a number 0..1 (−1..1 for bend), or the `{ note, vel, velocity, ch }` payload of `midi/note/on` and `midi/note/off`.","zh":"訊號的值：0..1 的數字（彎音是 −1..1），或 `midi/note/on`、`midi/note/off` 那個 `{ note, vel, velocity, ch }` 內容。"},{"name":"channel","type":"Number","en":"The channel to use for the legacy names that carry none (`midi/cc/<n>`, `midi/bend`).","zh":"沒有頻道資訊的短名稱（`midi/cc/<n>`、`midi/bend`）要用哪個頻道。"}],"returns":{"type":"{ family, ev } | null","en":"`family` is `'channel'` or `'legacy'`, and `ev` is the event. `null` for a device name (`midi/minilab3/…`), `midi/virtual` and anything else.","zh":"`family` 是 `'channel'` 或 `'legacy'`，`ev` 是事件。裝置名稱（`midi/minilab3/…`）、`midi/virtual` 和其他名稱都回傳 `null`。"},"notes":[],"examples":[{"code":"import { eventOfSignal } from '@openav/midi';\n\nconsole.log(JSON.stringify(eventOfSignal('midi/ch/10/note/36', 0.5)));\nconsole.log(JSON.stringify(eventOfSignal('midi/cc/74', 1, { channel: 4 })));\nconsole.log(eventOfSignal('midi/minilab3/knob1', 1));   // a device name: null","caption":{"en":"Prints a `noteon` on channel 10 with velocity 64, a CC 74 on channel 4 with value 127, then `null`.","zh":"印出頻道 10、力度 64 的 `noteon`，頻道 4、值 127 的 CC 74，最後是 `null`。"},"run":"module","lang":"js"}],"related":["genericSignals","followMidiSignals","ControllerCanvas"],"keywords":{"en":["signal to midi","reverse","inverse","signals to events","take playback","bus"],"zh":["訊號轉 midi","反向","訊號轉事件","回放","匯流排"]},"tbd":[],"claims":[]},
{"id":"findPort","key":"findPort","name":"findPort","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"findPort(ports, query)","import":"import { findPort } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":143,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L143"},"summary":{"en":"Finds a MIDI port by name: a `/regex/flags`, then the exact name, then a case-insensitive substring; `null` when nothing matches.","zh":"用名稱找 MIDI 埠：先試 `/regex/flags`，再試完整名稱，最後試不分大小寫的子字串，都找不到就回傳 `null`。"},"description":[{"en":"This is how the `midi-out` attribute, the `hardware` attribute's port name and [`createController()`](#createController)'s `setOutput(query)` read what you typed.","zh":"`midi-out` 屬性、`hardware` 屬性裡的埠名，以及 [`createController()`](#createController) 的 `setOutput(query)`，都是用它來理解你打的字。"}],"params":[{"name":"ports","type":"Object[]","en":"Ports with a `name` (an array, such as `midi.outputs`).","zh":"有 `name` 的埠（陣列，例如 `midi.outputs`）。"},{"name":"query","type":"String","en":"A name, a part of a name (`'IAC'`, `'loopMIDI'`), or `'/^mini/'`. Flags after the closing slash are used; without flags the regex is case-insensitive.","zh":"名稱、名稱的一部分（`'IAC'`、`'loopMIDI'`），或 `'/^mini/'`。結尾斜線後面的旗標會被採用，沒寫旗標的話正規表示式不分大小寫。"}],"returns":{"type":"Object | null","en":"The first matching port. A broken regex finds nothing instead of throwing.","zh":"第一個符合的埠。壞掉的正規表示式只會找不到，不會丟出錯誤。"},"notes":[],"examples":[{"code":"import { findPort } from '@openav/midi';\n\nconst ports = [{ name: 'IAC Driver Bus 1' }, { name: 'loopMIDI Port' }, { name: 'Minilab3 MIDI' }];\nconsole.log(findPort(ports, 'iac').name);        // IAC Driver Bus 1  (substring, any case)\nconsole.log(findPort(ports, '/^mini/').name);    // Minilab3 MIDI\nconsole.log(findPort(ports, 'nanoKONTROL'));     // null\nconsole.log(findPort(ports, '/[/'));             // null: a broken regex finds nothing","caption":{"en":"Prints `IAC Driver Bus 1`, `Minilab3 MIDI`, `null`, `null`.","zh":"印出 `IAC Driver Bus 1`、`Minilab3 MIDI`、`null`、`null`。"},"run":"module","lang":"js"}],"related":["pickOutput","Midi","createController"],"keywords":{"en":["find port","port by name","regex","iac","loopmidi","midi-out"],"zh":["找埠","依名稱找埠","正規表示式","iac","loopmidi","midi-out"]},"tbd":[],"claims":[]},
{"id":"findProfile","key":"findProfile","name":"findProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"findProfile(ref, profiles)","import":"import { findProfile } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":105,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L105"},"summary":{"en":"Finds a profile in a list by id or short name, or returns `null`; no validation, no loading.","zh":"在清單裡用 id 或短名稱找設定檔，找不到回傳 `null`，不驗證也不載入。"},"description":[{"en":"Use it when you hold your own list, for example the profiles a picker offers (`controller.controllers.profiles`), and want the same id-or-short lookup the element uses.","zh":"手上有自己的清單時用，例如選單提供的設定檔（`controller.controllers.profiles`），而你想用跟元素相同的「id 或短名稱」查法。"}],"params":[{"name":"ref","type":"String","en":"A profile `id` or `short` name.","zh":"設定檔的 `id` 或 `short` 短名稱。"},{"name":"profiles","type":"Object[]","en":"The list to search (required here, unlike [`profileById()`](#profileById), which uses the shipped ones).","zh":"要搜尋的清單（這裡一定要給，不像 [`profileById()`](#profileById) 固定用內建的）。"}],"returns":{"type":"Object | null","en":"The profile in the list.","zh":"清單裡的那份設定檔。"},"notes":[],"examples":[{"code":"import { PROFILES } from '@openav/midi';\nimport { findProfile } from '@openav/midi/embed';\n\nconsole.log(findProfile('lpd8', PROFILES).id);         // akai-lpd8\nconsole.log(findProfile('akai-lpd8', PROFILES).short); // lpd8\nconsole.log(findProfile('nope', PROFILES));            // null","caption":{"en":"Prints `akai-lpd8`, `lpd8` and `null`.","zh":"印出 `akai-lpd8`、`lpd8` 和 `null`。"},"run":"module","lang":"js"}],"related":["profileById","resolveProfile"],"keywords":{"en":["find profile","lookup in list","short name"],"zh":["找設定檔","清單查詢","短名稱"]},"tbd":[],"claims":[]},
{"id":"genericSignals","key":"genericSignals","name":"genericSignals","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"genericSignals(ev, { device = null, legacy = true, channel = true } = {})","import":"import { genericSignals } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":106,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L106"},"summary":{"en":"Lists the device-independent signals one MIDI event becomes: `midi/cc/74`, `midi/note/on`, `midi/bend`, and their per-channel twins such as `midi/ch/2/cc/74`.","zh":"列出一則 MIDI 事件會變成哪些跟裝置無關的訊號：`midi/cc/74`、`midi/note/on`、`midi/bend`，以及各頻道版本，例如 `midi/ch/2/cc/74`。"},"description":[{"en":"Two families, both published for every hardware port and every virtual controller. The short ones are what most worlds read: a `noteon` becomes the pulse `midi/note/on` with `{ note, vel (0..1), velocity (1..127), ch, device }`, a `noteoff` becomes `midi/note/off` with `{ note, ch, device }`, a `cc` becomes `midi/cc/<n>` (0..1), and a `pitchbend` becomes `midi/bend` (−1..1).","zh":"兩組名稱，每個硬體連接埠和每個虛擬控制器都會發佈。短名稱是大多數世界讀的：`noteon` 變成脈衝 `midi/note/on`，內容是 `{ note, vel（0..1）, velocity（1..127）, ch, device }`。`noteoff` 變成 `midi/note/off`，內容是 `{ note, ch, device }`。`cc` 變成 `midi/cc/<n>`（0..1），`pitchbend` 變成 `midi/bend`（−1..1）。"},{"en":"The per-channel ones are for two devices that share a CC number, or a work that cares about the channel: `midi/ch/<ch>/note/<n>` (velocity 0..1 while held, 0 on release), `midi/ch/<ch>/cc/<n>`, `midi/ch/<ch>/bend`, `midi/ch/<ch>/pressure` (channel aftertouch) and `midi/ch/<ch>/poly/<n>` (polyphonic aftertouch). Pressure and poly exist only in this family. `program`, `clock` and the other system messages produce nothing.","zh":"分頻道的那組給「兩台機器共用同一個 CC 編號」或「作品在意頻道」的時候用：`midi/ch/<ch>/note/<n>`（按著時是力度 0..1，放開是 0）、`midi/ch/<ch>/cc/<n>`、`midi/ch/<ch>/bend`、`midi/ch/<ch>/pressure`（頻道觸後）、`midi/ch/<ch>/poly/<n>`（複音觸後）。壓力和 poly 只存在於這一組。`program`、`clock` 和其他系統訊息不會產生訊號。"},{"en":"There is deliberately no `midi/cc/<ch>/<n>`: under prefix subscriptions it would collide with \"CC 1 on any channel\".","zh":"刻意沒有 `midi/cc/<ch>/<n>` 這種名稱：用前綴訂閱時，它會跟「任何頻道的 CC 1」搞混。"}],"params":[{"name":"ev","type":"Object","en":"An event from [`parseMessage()`](#parseMessage). `null` gives an empty list.","zh":"[`parseMessage()`](#parseMessage) 回傳的事件。給 `null` 會得到空陣列。"},{"name":"device","type":"String | null","en":"Written into the `device` field of the `midi/note/on` and `midi/note/off` pulses (the port slug).","zh":"寫進 `midi/note/on`、`midi/note/off` 脈衝的 `device` 欄位（連接埠的 slug）。"},{"name":"legacy","type":"Boolean","en":"Include the short names that ignore the channel (`midi/note/on`, `midi/cc/<n>`, `midi/bend`).","zh":"是否包含不分頻道的短名稱（`midi/note/on`、`midi/cc/<n>`、`midi/bend`）。"},{"name":"channel","type":"Boolean","en":"Include the per-channel names (`midi/ch/<ch>/…`).","zh":"是否包含分頻道的名稱（`midi/ch/<ch>/…`）。"}],"returns":{"type":"{ name, value, pulse?, min?, max? }[]","en":"One item per signal. Hand the list to [`publish()`](#publish).","zh":"每個訊號一項。把整個陣列交給 [`publish()`](#publish) 即可。"},"notes":[],"examples":[{"code":"import { genericSignals } from '@openav/midi';\n\nconst names = (ev) => genericSignals(ev).map((s) => s.name);\nconsole.log(names({ type: 'cc', ch: 2, cc: 74, value: 64 }));\nconsole.log(names({ type: 'noteon', ch: 10, note: 36, vel: 127 }));\nconsole.log(names({ type: 'chanat', ch: 1, value: 80 }));","caption":{"en":"Prints `['midi/cc/74', 'midi/ch/2/cc/74']`, then the two note-on names, then the single per-channel pressure name.","zh":"印出 `['midi/cc/74', 'midi/ch/2/cc/74']`，接著是 note-on 的兩個名稱，最後是只有分頻道版本的壓力名稱。"},"run":"module","lang":"js"},{"code":"import { genericSignals } from '@openav/midi';\n\nconst ev = { type: 'noteon', ch: 1, note: 60, vel: 100 };\nconsole.log(genericSignals(ev, { device: 'my-keys' })[0].value);\nconsole.log(genericSignals(ev, { legacy: false }).map((s) => s.name));","caption":{"en":"The first prints the pulse payload `{ note: 60, vel: 0.787…, velocity: 100, ch: 1, device: 'my-keys' }`, the second only the per-channel name.","zh":"第一行印出脈衝內容 `{ note: 60, vel: 0.787…, velocity: 100, ch: 1, device: 'my-keys' }`，第二行只剩分頻道的名稱。"},"run":"module","lang":"js"}],"related":["eventOfSignal","publish","parseMessage","followMidiSignals"],"keywords":{"en":["signal names","midi/cc","midi/note/on","midi/bend","midi/ch","per channel","pulse","publish midi"],"zh":["訊號名稱","midi/cc","midi/note/on","midi/bend","分頻道","脈衝","發佈 midi"]},"tbd":[],"claims":[]},
{"id":"groupsOf","key":"groupsOf","name":"groupsOf","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"groupsOf(p)","import":"import { groupsOf } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":206,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L206"},"summary":{"en":"Groups a profile's controls by their `group` (`knob`, `pad`, `fader` …), returning `{ group: [control ids] }`.","zh":"依 `group`（`knob`、`pad`、`fader` …）把設定檔的控制項分組，回傳 `{ group: [控制項 id] }`。"},"description":[{"en":"The groups follow the TouchDesigner tool's out CHOPs (`out_knob`, `out_slider`, `out_pad` …). A control gets its type as group unless the profile gives another. [`MidiController`](#MidiController) has a method of the same name that returns the current values instead of the ids.","zh":"分組方式跟 TouchDesigner 工具的輸出 CHOP（`out_knob`、`out_slider`、`out_pad` …）一致。控制項沒特別指定的話，就用它的類型當分組。[`MidiController`](#MidiController) 有個同名的方法，回傳的是目前的數值，不是 id。"}],"params":[{"name":"p","type":"Object","en":"A normalized profile, so every control has its `group` filled in.","zh":"整理過的設定檔，這樣每個控制項才有 `group`。"}],"returns":{"type":"Object","en":"An object of arrays of control ids.","zh":"由控制項 id 陣列組成的物件。"},"notes":[],"examples":[{"code":"import { groupsOf, normalizeProfile, profileById } from '@openav/midi';\n\nconst groups = groupsOf(normalizeProfile(profileById('lpd8')));\nconsole.log(Object.keys(groups));   // [ 'pad', 'knob' ]\nconsole.log(groups.knob);           // [ 'k1', 'k2', ... 'k8' ]","caption":{"en":"Prints the group names and then the eight knob ids.","zh":"印出分組名稱，再印出八個旋鈕的 id。"},"run":"module","lang":"js"}],"related":["profileSignals","normalizeProfile","MidiController"],"keywords":{"en":["group controls","knobs list","touchdesigner chop","out_knob"],"zh":["控制項分組","旋鈕清單","touchdesigner chop","out_knob"]},"tbd":[],"claims":[]},
{"id":"indexProfile","key":"indexProfile","name":"indexProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"indexProfile(p)","import":"import { indexProfile } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":136,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L136"},"summary":{"en":"Builds the lookup from a MIDI message to the control it moves: `\"cc:1:74\"` maps to the knob listening for CC 74 on channel 1.","zh":"建立「MIDI 訊息 → 它會推動的控制項」的查詢表：`\"cc:1:74\"` 對應到聽頻道 1 的 CC 74 的那顆旋鈕。"},"description":[{"en":"This is how [`MidiController`](#MidiController) decides which control a message belongs to. Use it to build your own router. Each value is a list because a message can move more than one control.","zh":"[`MidiController`](#MidiController) 就是用它判斷一則訊息屬於哪個控制項。要自己做路由時可以拿來用。每個值都是清單，因為一則訊息可能推動不只一個控制項。"}],"params":[{"name":"p","type":"Object","en":"A normalized profile (each control needs its `ch`).","zh":"整理過的設定檔（每個控制項都要有 `ch`）。"}],"returns":{"type":"Map<string, { control, note? }[]>","en":"The keys are `cc:<ch>:<cc>`, `note:<ch>:<note>`, `polyat:<ch>:<note>`, `pitchbend:<ch>`, `chanat:<ch>`, `program:<ch>`. A keys control gets one entry per note of its range.","zh":"鍵的格式是 `cc:<ch>:<cc>`、`note:<ch>:<note>`、`polyat:<ch>:<note>`、`pitchbend:<ch>`、`chanat:<ch>`、`program:<ch>`。琴鍵控制項的音域裡每個音都有一筆。"},"notes":[],"examples":[{"code":"import { indexProfile, normalizeProfile, profileById } from '@openav/midi';\n\nconst index = indexProfile(normalizeProfile(profileById('lpd8')));\nconsole.log(index.get('cc:1:70').map((hit) => hit.control.id));     // [ 'k1' ]\nconsole.log(index.get('note:10:36').map((hit) => hit.control.id));  // [ 'pad1' ]\nconsole.log(index.get('cc:1:99'));                                  // undefined","caption":{"en":"Prints `['k1']` for CC 70, `['pad1']` for note 36 on channel 10, and `undefined` for a message the device does not send.","zh":"印出 CC 70 對應的 `['k1']`、頻道 10 音符 36 對應的 `['pad1']`，以及這台機器不會送的訊息所對應的 `undefined`。"},"run":"module","lang":"js"}],"related":["normalizeProfile","MidiController"],"keywords":{"en":["index","lookup","message to control","route message","cc:1:74"],"zh":["索引","查詢表","訊息對應控制項","路由","cc:1:74"]},"tbd":[],"claims":[]},
{"id":"isLive","key":"isLive","name":"isLive","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"isLive(p)","import":"import { isLive } from '@openav/midi';","source":{"file":"packages/midi/ports.js","line":10,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/ports.js#L10"},"summary":{"en":"Says whether a MIDI port can still be sent to: it exists and its `state` is not `'disconnected'`.","zh":"判斷一個 MIDI 埠還能不能送資料：它存在，而且 `state` 不是 `'disconnected'`。"},"description":[{"en":"A port that was unplugged can still be an object in your hands. Sending to it gets nowhere, and the engine's try/catch swallows the error, so nothing on screen says why. The output rules ([`pickOutput()`](#pickOutput), [`reselectOutput()`](#reselectOutput)) therefore skip every port that is not live.","zh":"被拔掉的埠，在你手上仍然是個物件。對它送資料到不了任何地方，引擎的 try/catch 又會把錯誤吞掉，畫面上看不出原因。所以輸出的挑選規則（[`pickOutput()`](#pickOutput)、[`reselectOutput()`](#reselectOutput)）會跳過所有不存活的埠。"}],"params":[{"name":"p","type":"MIDIPort | null","en":"A port shaped like a `MIDIOutput`: it only needs a `state`.","zh":"長得像 `MIDIOutput` 的埠，只需要有 `state`。"}],"returns":{"type":"Boolean","en":"`false` for `null` and for a disconnected port.","zh":"給 `null` 或已斷線的埠回傳 `false`。"},"notes":[],"examples":[{"code":"import { isLive } from '@openav/midi';\n\nconsole.log(isLive({ name: 'A', state: 'connected' }));      // true\nconsole.log(isLive({ name: 'B', state: 'disconnected' }));   // false\nconsole.log(isLive(null));                                   // false","caption":{"en":"Prints `true`, `false`, `false`.","zh":"印出 `true`、`false`、`false`。"},"run":"module","lang":"js"}],"related":["pickOutput","reselectOutput"],"keywords":{"en":["port alive","disconnected","port state","unplugged"],"zh":["埠存活","斷線","埠狀態","被拔掉"]},"tbd":[],"claims":[]},
{"id":"isProfileUrl","key":"isProfileUrl","name":"isProfileUrl","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"isProfileUrl(ref)","import":"import { isProfileUrl } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":102,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L102"},"summary":{"en":"Tells a URL to a profile file apart from a profile id: `true` for `./pads.json`, `https://…`, `/p/x`, `false` for `'akai-lpd8'`.","zh":"分辨「指向設定檔的網址」和「設定檔 id」：`./pads.json`、`https://…`、`/p/x` 回傳 `true`，`'akai-lpd8'` 回傳 `false`。"},"description":[{"en":"It is the test [`resolveProfile()`](#resolveProfile) uses to decide between looking an id up and fetching a file. A bare name that happens to end in `.json` also counts as a URL.","zh":"[`resolveProfile()`](#resolveProfile) 就是用它決定要「查 id」還是「抓檔案」。剛好以 `.json` 結尾的單純名稱也會被當成網址。"}],"params":[{"name":"ref","type":"any","en":"What the page or attribute gave. Anything that is not a string is `false`.","zh":"頁面或屬性給的內容。不是字串一律是 `false`。"}],"returns":{"type":"Boolean","en":"`true` when the string starts with `http:`, `https:`, `blob:` or `data:`, starts with `/`, `./` or `../`, or ends in `.json` (optionally followed by `?…` or `#…`).","zh":"字串以 `http:`、`https:`、`blob:`、`data:` 開頭，或以 `/`、`./`、`../` 開頭，或以 `.json` 結尾（後面可接 `?…` 或 `#…`）時為 `true`。"},"notes":[],"examples":[{"code":"import { isProfileUrl } from '@openav/midi/embed';\n\nfor (const ref of ['akai-lpd8', './pads.json', 'https://x.org/my.json', '/p/x', 'my.json']) {\n  console.log(ref, isProfileUrl(ref));\n}","caption":{"en":"Prints `false` for the id and `true` for the other four.","zh":"id 印出 `false`，其他四個印出 `true`。"},"run":"module","lang":"js"}],"related":["resolveProfile","loadProfile"],"keywords":{"en":["url or id","profile reference","is url"],"zh":["網址或 id","設定檔參照","是不是網址"]},"tbd":[],"claims":[]},
{"id":"linkControllers","key":"linkControllers","name":"linkControllers","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"linkControllers(relay, signals, controllers)","import":"import { linkControllers } from '@openav/midi';","source":{"file":"packages/midi/link.js","line":11,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/link.js#L11"},"summary":{"en":"Links the on-screen controllers to phones over the room relay: tells phones which device is on screen, echoes this machine's moves to them, and mirrors theirs here.","zh":"透過房間中繼把螢幕上的控制器和手機連起來：告訴手機現在螢幕上是哪台機器，把這台電腦上的動作回傳給手機，也把手機的動作鏡射到這裡。"},"description":[{"en":"A phone's MIDI tab publishes the device's signals itself (the controller lives there), so this side only does three things. When you `select` another device it tells the phones with `relay.config('midi', { profile })`. When a knob, fader, encoder, wheel, strip or toggle button on THIS machine moves from hardware or the screen, it queues `relay.feedback('midi/<short>/<id>', value)` so the phone's knobs follow. When a phone plays (`midi/virtual`) it mirrors that on this screen, state and view only, because the phone already published the signals.","zh":"手機的 MIDI 分頁自己會發佈機器的訊號（控制器就住在那裡），所以這一端只做三件事。你用 `select` 換了機器，就用 `relay.config('midi', { profile })` 告訴手機。這台電腦上的旋鈕、推桿、編碼器、彎音輪、觸控條或切換按鈕被硬體或螢幕動了，就把 `relay.feedback('midi/<short>/<id>', value)` 排進佇列，讓手機上的旋鈕跟著動。手機彈了（`midi/virtual`），就鏡射在這個螢幕上，只更新狀態和畫面，因為手機已經發佈過訊號了。"},{"en":"This is what `@openav/remote`'s show-side host and the lab's midi module call. You need it only when you wire phones into a page yourself.","zh":"`@openav/remote` 在演出端的主機，和實驗室的 midi 模組，用的就是它。只有在你自己把手機接進頁面時才需要。"}],"params":[{"name":"relay","type":"Object","en":"A relay client with `config(name, value)` and `feedback(signalName, value)` (the show runner's `RelayClient`).","zh":"中繼客戶端，要有 `config(name, value)` 和 `feedback(signalName, value)`（演出執行器的 `RelayClient`）。"},{"name":"signals","type":"Signals","en":"The bus the phones' `midi/virtual` messages arrive on.","zh":"手機的 `midi/virtual` 訊息抵達的匯流排。"},{"name":"controllers","type":"MidiControllers","en":"The manager whose controllers are linked.","zh":"要連起來的控制器所屬的管理器。"}],"returns":{"type":"{ flush(), dispose() }","en":"`flush()` sends the queued feedback to the relay (call it at a steady pace, for example once per frame). `dispose()` stops listening.","zh":"`flush()` 把佇列中的回饋送給中繼（以穩定的節奏呼叫，例如每個畫面一次）。`dispose()` 停止監聽。"},"notes":[],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { MidiControllers, PROFILES, linkControllers } from '@openav/midi';\n\nconst signals = new Signals();\nconst mcs = new MidiControllers({ profiles: PROFILES, signals, initial: 'akai-lpd8' });\n\n// a stand-in for the relay client: it prints what would go to the phones\nconst relay = {\n  config: (name, value) => console.log('config', name, value),\n  feedback: (name, value) => console.log('feedback', name, value.toFixed(2)),\n};\nconst link = linkControllers(relay, signals, mcs);\n\nmcs.current.setValue('k1', 0.5);   // a knob moves on this machine\nlink.flush();                       // feedback midi/lpd8/k1 0.50\nmcs.select('korg-nanokontrol2');   // config midi { profile: 'korg-nanokontrol2' }\nlink.dispose();","caption":{"en":"Prints `feedback midi/lpd8/k1 0.50` after the flush, then `config midi { profile: 'korg-nanokontrol2' }` when the device on screen changes.","zh":"flush 之後印出 `feedback midi/lpd8/k1 0.50`，螢幕上的裝置換掉時再印出 `config midi { profile: 'korg-nanokontrol2' }`。"},"run":"module","lang":"js"}],"related":["MidiControllers","controllerRoutes","MidiController"],"keywords":{"en":["phone","remote","relay","ipad controller","feedback to phone","mirror"],"zh":["手機","遙控","中繼","ipad 控制器","回饋給手機","鏡射"]},"tbd":[],"claims":[]},
{"id":"loadProfile","key":"loadProfile","name":"loadProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"loadProfile(ref, o = {})","import":"import { loadProfile } from '@openav/midi';","source":{"file":"packages/midi/index.js","line":261,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/index.js#L261"},"summary":{"en":"Loads a controller profile by id, short name or URL (a JSON file anywhere CORS allows), checking any file or object you give it; returns a promise.","zh":"用 id、短名稱或網址（任何 CORS 允許的 JSON 檔）載入控制器設定檔，你給的檔案或物件會先檢查，回傳 promise。"},"description":[{"en":"It is [`resolveProfile()`](#resolveProfile) with the shipped profiles and the page's base URL filled in. `<oav-controller profile=\"./my-device.json\">` calls it to read your file. A bad file says why instead of drawing nothing.","zh":"它就是 [`resolveProfile()`](#resolveProfile)，只是已經幫你帶入內建設定檔和頁面的基底網址。`<oav-controller profile=\"./my-device.json\">` 就是呼叫它來讀你的檔案。檔案有問題時會說明原因，而不是什麼都不畫。"}],"params":[{"name":"ref","type":"String | Object","en":"An id (`'akai-lpd8'`), a short name (`'lpd8'`), a URL or path to a profile JSON, or a profile object.","zh":"id（`'akai-lpd8'`）、短名稱（`'lpd8'`）、設定檔 JSON 的網址或路徑，或是設定檔物件。"},{"name":"o","type":"Object","en":"Options passed on to [`resolveProfile()`](#resolveProfile): `fetch`, `base`, and `profiles` (default: every shipped profile). `base` defaults to the page's `document.baseURI`, so a relative path is read from the page.","zh":"轉交給 [`resolveProfile()`](#resolveProfile) 的選項：`fetch`、`base`，以及 `profiles`（預設是所有內建設定檔）。`base` 預設是頁面的 `document.baseURI`，所以相對路徑會以頁面為準。"}],"returns":{"type":"Promise<Object>","en":"The raw profile. Rejects with every problem listed when a file or object is invalid, or with `unknown MIDI profile \"x\" (known: …)` when an id does not exist.","zh":"原始的設定檔。檔案或物件不合法時 reject，並列出所有問題。id 不存在則 reject 為 `unknown MIDI profile \"x\" (known: …)`。"},"notes":[],"examples":[{"code":"import { loadProfile } from '@openav/midi';\n\nconst lpd8 = await loadProfile('lpd8');\nconsole.log(lpd8.id, lpd8.controls.length);\n\ntry { await loadProfile('no-such-box'); } catch (e) { console.log(e.message.slice(0, 70) + ' …'); }","caption":{"en":"Prints `akai-lpd8 16`, then the beginning of the error for an id that does not exist (it goes on to list every known id).","zh":"印出 `akai-lpd8 16`，再印出不存在的 id 所產生的錯誤訊息開頭（後面還會列出所有認得的 id）。"},"run":"module","lang":"js"},{"code":"import { loadProfile, profileById } from '@openav/midi';\n\n// a profile file of your own; here a fake fetch stands in for the network\nconst mine = { ...structuredClone(profileById('lpd8')), id: 'my-lpd8', short: 'mylpd8', name: 'My LPD8' };\nconst fetch = async (url) => { console.log('fetching', url); return { ok: true, json: async () => mine }; };\n\nconst p = await loadProfile('./profiles/my-lpd8.json', { fetch, base: 'https://example.com/show/' });\nconsole.log(p.name);","caption":{"en":"Prints the full URL it fetched (`https://example.com/show/profiles/my-lpd8.json`), then `My LPD8`.","zh":"印出它抓取的完整網址（`https://example.com/show/profiles/my-lpd8.json`），再印出 `My LPD8`。"},"run":"module","lang":"js"}],"related":["resolveProfile","profileById","validateProfile","createController"],"keywords":{"en":["load profile","profile url","custom controller","fetch json","my device"],"zh":["載入設定檔","設定檔網址","自訂控制器","抓取 json","我的機器"]},"tbd":[],"claims":[]},
{"id":"matchProfile","key":"matchProfile","name":"matchProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"matchProfile(portName, profiles)","import":"import { matchProfile } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":163,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L163"},"summary":{"en":"Finds the device profile that owns a MIDI port name, such as `'Minilab3 MIDI'`, by testing each profile's `match` patterns.","zh":"用各個設定檔的 `match` 規則，找出「這個 MIDI 埠名」屬於哪一台機器，例如 `'Minilab3 MIDI'`。"},"description":[{"en":"Only `device` profiles are considered (a generic layout never matches by name). A profile matches when one of its `match.ports` regular expressions (case-insensitive) hits the name. The score starts at 1, gains 1 when a `match.prefer` pattern also hits, and loses 2 when a `match.avoid` pattern hits. The highest positive score wins, so a port a device only exposes for something else (`Minilab3 DIN THRU`, `Minilab3 MCU/HUI`) does not match.","zh":"只考慮 `device` 設定檔（通用版面永遠不會靠名稱配對）。設定檔的某個 `match.ports` 正規表示式（不分大小寫）對得上名稱，就算符合。分數從 1 開始，`match.prefer` 也對上就加 1，`match.avoid` 對上就扣 2。取最高的正分，所以機器為了別的用途開出來的埠（`Minilab3 DIN THRU`、`Minilab3 MCU/HUI`）不會被配對。"},{"en":"This is how [`MidiControllers`](#MidiControllers) turns a plugged-in device into the right on-screen controller. When a device exposes several ports, [`pickPort()`](#pickPort) chooses which one to listen to.","zh":"[`MidiControllers`](#MidiControllers) 就是靠它把接上的機器變成正確的螢幕控制器。一台機器開出好幾個埠時，由 [`pickPort()`](#pickPort) 決定聽哪一個。"}],"params":[{"name":"portName","type":"String","en":"The name of a MIDI input port.","zh":"MIDI 輸入埠的名稱。"},{"name":"profiles","type":"Object[]","en":"The profiles to search, usually [`PROFILES`](#PROFILES).","zh":"要搜尋的設定檔，通常是 [`PROFILES`](#PROFILES)。"}],"returns":{"type":"{ profile, score } | null","en":"The best match, or `null` when no `device` profile fits.","zh":"最佳的符合者。沒有任何 `device` 設定檔符合時回傳 `null`。"},"notes":[],"examples":[{"code":"import { matchProfile, PROFILES } from '@openav/midi';\n\nfor (const name of ['LPD8 mk2', 'Minilab3 MIDI', 'Minilab3 DIN THRU', 'Some Unknown Synth']) {\n  const m = matchProfile(name, PROFILES);\n  console.log(name, '->', m ? m.profile.id + ' (score ' + m.score + ')' : null);\n}","caption":{"en":"Prints `akai-lpd8 (score 1)`, `arturia-minilab3 (score 2)`, then `null` for the DIN THRU port and for an unknown synth.","zh":"印出 `akai-lpd8 (score 1)`、`arturia-minilab3 (score 2)`，DIN THRU 埠和不認識的合成器則是 `null`。"},"run":"module","lang":"js"}],"related":["pickPort","MidiControllers","PROFILES"],"keywords":{"en":["match port","device detection","auto detect","port name","recognise controller"],"zh":["配對埠","偵測裝置","自動辨識","埠名","認出控制器"]},"tbd":[],"claims":[]},
{"id":"Midi","key":"Midi","name":"Midi","aliases":[],"kind":"class","package":"@openav/midi","category":"midi-io","documented":true,"signature":"new Midi({ signals = null, filterOut = 'IAC', requestAccess = null, echoWindow = 40, prefer = [] } = {})","import":"import { Midi } from '@openav/midi';","source":{"file":"packages/midi/index.js","line":45,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/index.js#L45"},"summary":{"en":"The Web MIDI engine: asks the browser for access, listens to every input, publishes what arrives as signals, and sends to one output port.","zh":"Web MIDI 引擎：向瀏覽器要存取權，聽每一個輸入埠，把收到的訊息發佈成訊號，並送到一個輸出埠。"},"description":[{"en":"Create it, then `await midi.enable()`. It resolves `true` once the browser has given access and `false` when there is no Web MIDI (Safari, iPad) or the permission was refused; the reason is written to the log (`onMessage`). Nothing is listened to or sent before that.","zh":"建立之後，`await midi.enable()`。瀏覽器給了存取權就回傳 `true`，沒有 Web MIDI（Safari、iPad）或權限被拒絕就回傳 `false`，原因會寫進記錄（`onMessage`）。在那之前不會聽也不會送任何東西。"},{"en":"In: every message from every listened input goes to the callbacks you added with `listen(fn)` (raw bytes, including clock), then is parsed and published as [`genericSignals()`](#genericSignals) names. With two or more inputs it also publishes `midi/<slug>/note/on`, `midi/<slug>/note/off`, `midi/<slug>/cc/<n>` and `midi/<slug>/bend`, so devices do not collide. The slug comes from the port name (`Minilab3 MIDI` becomes `minilab3-midi`), so a device that drops and reconnects keeps its identity. Clock messages are not logged and publish nothing.","zh":"輸入：每個被聆聽的輸入埠送來的每一則訊息，會先交給你用 `listen(fn)` 加的函式（原始位元組，包含時鐘），再被解析並發佈成 [`genericSignals()`](#genericSignals) 的名稱。有兩個以上輸入埠時，另外會發佈 `midi/<slug>/note/on`、`midi/<slug>/note/off`、`midi/<slug>/cc/<n>` 和 `midi/<slug>/bend`，讓不同機器不會互相打架。slug 由埠名產生（`Minilab3 MIDI` 變成 `minilab3-midi`），所以機器斷線再接回來，身分不變。時鐘訊息不寫記錄，也不發佈訊號。"},{"en":"Out: `send(bytes)`, `noteOn()`, `noteOff()` and `cc()` go to `midi.out`, the chosen output port, and are remembered for echo filtering. If that port is unplugged the engine moves to the next best live port (your choice by name, then `prefer`, then the first), moves back when your choice returns, and never lets an unrelated device steal the output. Every switch writes one line to the log and one `console.warn`, and calls the `onOutput` subscribers.","zh":"輸出：`send(bytes)`、`noteOn()`、`noteOff()`、`cc()` 都會送到 `midi.out`（選定的輸出埠），並記下來做回音過濾。那個埠被拔掉時，引擎會換到下一個最好的存活埠（先你用名稱指定的，再 `prefer`，最後第一個），你指定的埠回來就換回去，而且不相干的新裝置不會搶走輸出。每次切換都會寫一行記錄、一個 `console.warn`，並通知 `onOutput` 的訂閱者。"},{"en":"Hooks you can assign: `onCC(cc, value01, ch)`, `onNote(note, velocity01, on, ch)`, `onMessage({ dir, text })` (the log), `onSend(bytes)` (sees everything sent, for a meter) and `onDeviceChange(devices)`. Fields: `enabled`, `inputs`, `outputs`, `out`, `access`, `echoDropped`.","zh":"可以指定的掛鉤：`onCC(cc, value01, ch)`、`onNote(note, velocity01, on, ch)`、`onMessage({ dir, text })`（記錄）、`onSend(bytes)`（看得到所有送出的訊息，做音量表用）、`onDeviceChange(devices)`。欄位有：`enabled`、`inputs`、`outputs`、`out`、`access`、`echoDropped`。"}],"params":[{"name":"signals","type":"Signals | null","en":"Publish every input message here as `midi/…` signals (see [`genericSignals()`](#genericSignals)). Without it the engine still calls `listen()` callbacks and the `on…` hooks.","zh":"把每則輸入訊息發佈成 `midi/…` 訊號（見 [`genericSignals()`](#genericSignals)）。不給的話，引擎仍然會呼叫 `listen()` 的函式和 `on…` 掛鉤。"},{"name":"filterOut","type":"String | RegExp | null","en":"Input port NAMES never to listen to, as a regular expression (a string is read as its source, case-insensitive). The default `'IAC'` is the macOS bus you send into Ableton or TouchDesigner, so what you send never comes back as input. `null` listens to everything.","zh":"永遠不聽的輸入埠「名稱」，寫成正規表示式（字串會當成它的內容，不分大小寫）。預設 `'IAC'` 是 macOS 上你用來送進 Ableton 或 TouchDesigner 的匯流排，這樣你送出去的東西不會又從輸入回來。給 `null` 就是全部都聽。"},{"name":"requestAccess","type":"Function | null","en":"Where the `MIDIAccess` comes from, called as `requestAccess({ sysex: false })` and returning a promise. The default is `navigator.requestMIDIAccess`. Pass your own for a test, a shim, or a host that wraps Web MIDI for old sketches and hands the REAL one here.","zh":"`MIDIAccess` 從哪裡來，會以 `requestAccess({ sysex: false })` 呼叫，要回傳 promise。預設是 `navigator.requestMIDIAccess`。測試、轉接層，或是替舊草圖包裝了 Web MIDI 的環境，可以傳自己的函式，並把「真的」那個交到這裡。"},{"name":"echoWindow","type":"Number","en":"Milliseconds. An input message identical to one just SENT within this window is our own echo and is dropped (counted in `echoDropped`). `0` turns it off.","zh":"毫秒。輸入的訊息如果跟這個時間內剛「送出」的某一則完全相同，就視為自己的回音而丟掉（計入 `echoDropped`）。`0` 表示關閉。"},{"name":"prefer","type":"Array<String | RegExp>","en":"Output priority after the port you chose by name: the first entry that matches a live port wins (a string matches as a substring), else the first port. Example: `['IAC']`.","zh":"你用名稱指定的埠之後的輸出優先順序：第一個對得上存活埠的項目勝出（字串以子字串比對），都沒有就用第一個埠。例如 `['IAC']`。"}],"needs":{"en":"Call `await midi.enable()` first. Browsers with Web MIDI (Chrome, Edge, Firefox) ask the visitor for permission at that moment, so call it from a click when you can. Safari and iPad have no Web MIDI: `enable()` gives `false`.","zh":"要先呼叫 `await midi.enable()`。有 Web MIDI 的瀏覽器（Chrome、Edge、Firefox）會在這個時候向訪客要權限，所以可以的話請在點擊裡呼叫。Safari 和 iPad 沒有 Web MIDI，`enable()` 會回傳 `false`。"},"notes":[{"en":"`createShow()` creates one and enables it for you. `createController()` and `<oav-controller>` share one engine per page, so a page with five controllers asks for MIDI once.","zh":"`createShow()` 會替你建立一個並啟用它。`createController()` 和 `<oav-controller>` 在同一個頁面上共用一個引擎，所以一頁放五個控制器也只會要一次 MIDI。"},{"en":"The engine sends MIDI data only and plays no audio. To hear the notes you need a synth on the other end (a DAW on the IAC bus, a hardware synth), or `@openav/sound`.","zh":"引擎只送 MIDI 資料，自己不發聲。想聽到音符，另一端要有合成器（接在 IAC 匯流排上的 DAW、硬體合成器），或是用 `@openav/sound`。"}],"examples":[{"code":"import { Midi } from '@openav/midi';\n\n// a stand-in for navigator.requestMIDIAccess, so this runs without a device\nconst output = { name: 'Demo out', state: 'connected', send: (bytes) => console.log('out', bytes) };\nconst access = { inputs: new Map(), outputs: new Map([['o', output]]) };\n\nconst midi = new Midi({ requestAccess: async () => access });\nconsole.log(await midi.enable());   // true\nmidi.noteOn(60, 100);               // out [144, 60, 100]\nmidi.noteOff(60);                   // out [128, 60, 0]","caption":{"en":"Prints `true`, then the two messages that reach the output port: note-on and note-off for middle C.","zh":"印出 `true`，再印出送到輸出埠的兩則訊息：中央 C 的 note-on 和 note-off。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { Midi } from '@openav/midi';\n\nconst keys = { name: 'Demo Keys', state: 'connected', onmidimessage: null };\nconst access = { inputs: new Map([['k', keys]]), outputs: new Map() };\n\nconst signals = new Signals();\nconst midi = new Midi({ signals, requestAccess: async () => access });\nawait midi.enable();\n\nsignals.on('midi/cc/74', (v) => console.log('knob', v.toFixed(2)));\nkeys.onmidimessage({ data: Uint8Array.of(0xb0, 74, 127) });   // the keyboard \"plays\" CC 74 = 127","caption":{"en":"Prints `knob 1.00`: a message arriving on the input port became the signal `midi/cc/74`.","zh":"印出 `knob 1.00`：輸入埠收到的訊息變成了訊號 `midi/cc/74`。"},"run":"module","lang":"js"},{"code":"import { Midi } from '@openav/midi';\n\nconst port = (name) => ({ name, state: 'connected', send: () => {} });\nconst iac = port('IAC Driver Bus 1'), synth = port('Synth');\nconst access = { inputs: new Map(), outputs: new Map([['a', iac], ['b', synth]]) };\n\nconst midi = new Midi({ requestAccess: async () => access });\nmidi.onOutput(({ reason, text }) => console.log(reason, '-', text));\nawait midi.enable('IAC');\nconsole.log(midi.out.name);          // IAC Driver Bus 1\n\naccess.outputs.delete('a'); iac.state = 'disconnected';\naccess.onstatechange();              // what the browser does when a port is unplugged\nconsole.log(midi.out.name);          // Synth","caption":{"en":"Prints the chosen port, then a `fallback` line saying the output moved to \"Synth\" (the engine also writes it with `console.warn`), then `Synth`.","zh":"印出選定的埠，接著一行 `fallback`，說明輸出換到了「Synth」（引擎也會用 `console.warn` 印一次），最後印出 `Synth`。"},"run":"module","lang":"js"},{"code":"import { Midi } from '@openav/midi';\n\nconst midi = new Midi();\nif (await midi.enable()) {\n  console.log('inputs:', midi.devices().map((d) => d.name));\n} else {\n  console.log('no Web MIDI here, or the permission was refused');\n}","caption":{"en":"The real thing: the browser asks for MIDI permission, then the console lists your input devices (or says there is no Web MIDI).","zh":"真正的用法：瀏覽器會詢問 MIDI 權限，接著主控台列出你的輸入裝置（或告訴你沒有 Web MIDI）。"},"run":"static","lang":"js"}],"related":["EchoGuard","pickOutput","reselectOutput","genericSignals","createController","MidiControllers","ScoreMidi"],"keywords":{"en":["web midi","midi engine","requestMIDIAccess","midi in","midi out","send","panic","iac","hot-plug","permission","enable"],"zh":["web midi","midi 引擎","輸入","輸出","送出","全部關閉","iac","熱插拔","權限","啟用"]},"tbd":[{"en":"Not tried for this page: the real permission prompt and real MIDI hardware in each browser. The examples run against a stand-in for `MIDIAccess`, and the browser behaviour (Chrome, Edge and Firefox ask for permission, Safari and iPad have no Web MIDI) is as the package README describes it.","zh":"這一頁沒有實測：各個瀏覽器真實的權限詢問，以及真實的 MIDI 硬體。範例是用假的 `MIDIAccess` 跑的，瀏覽器的行為（Chrome、Edge、Firefox 會詢問權限，Safari 和 iPad 沒有 Web MIDI）是照套件 README 的說法寫的。"}],"claims":[],"members":[{"name":"devices","member":"method","signature":"","doc":"Device list for UIs: [{slug, name, listening}].","line":83},{"name":"setListening","member":"method","signature":"slug, on","doc":"Mute/unmute one device (by slug) without unplugging it.","line":87},{"name":"onDevices","member":"method","signature":"fn","doc":"Subscribe to device-list changes (hot-plug, mute).","line":89},{"name":"log","member":"method","signature":"dir, text","doc":"","line":96},{"name":"enable","member":"method","signature":"preferredOut = ''","doc":"Request access and wire inputs.","line":99},{"name":"onOutput","member":"method","signature":"fn","doc":"Subscribe to output changes (hot-plug, switch back, your own selectOutput): fn({ text, reason, next, out }).","line":151},{"name":"selectOutput","member":"method","signature":"name","doc":"Send to this output from now on, and remember it as THE one you want: if it is unplugged and plugged in again, the output returns to it.","line":154},{"name":"outputFor","member":"method","signature":"nameOrRe","doc":"The output port a device answers on (LED feedback): same name as its input, else a regex match.","line":166},{"name":"listen","member":"method","signature":"fn","doc":"Raw bytes from every (listened) input: (bytes, {slug, name, input}) → void.","line":171},{"name":"send","member":"method","signature":"bytes, { echo = true } = {}","doc":"Send raw bytes out.","line":203},{"name":"noteOn","member":"method","signature":"note, vel = 100, ch = 1","doc":"","line":208},{"name":"noteOff","member":"method","signature":"note, ch = 1","doc":"","line":209},{"name":"cc","member":"method","signature":"cc, val, ch = 1","doc":"","line":210},{"name":"dispose","member":"method","signature":"","doc":"Let go of every port and listener (an embed removed from the page).","line":213},{"name":"panic","member":"method","signature":"","doc":"Full panic — all notes off / all sound off / sustain off / per-note insurance.","line":222}]},
{"id":"MidiController","key":"MidiController","name":"MidiController","aliases":[],"kind":"class","package":"@openav/midi","category":"midi-io","documented":true,"signature":"new MidiController(profile, { signals = null, sink = null, short = null, onLed = null } = {})","import":"import { MidiController } from '@openav/midi';","source":{"file":"packages/midi/controller.js","line":36,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/controller.js#L36"},"summary":{"en":"One MIDI controller (a profile) as a live model: MIDI bytes go in, control values and signals come out, and a hand on screen sends the bytes the device would.","zh":"把一台 MIDI 控制器（一份設定檔）做成活的模型：MIDI 位元組進去，控制項的值和訊號出來，而螢幕上的手會送出跟實體機器一樣的位元組。"},"description":[{"en":"The one idea: the on-screen controller and the hardware speak the SAME language, MIDI bytes. `setValue()`, `press()` and the other virtual hands encode the exact message the real control would send and feed it through `ingest()`, the same door the hardware uses. So a virtual knob and a real one can never drift apart, and what your work receives is identical whichever hand moved it. Pass the bytes of a real device to `ingest(bytes, { source: 'hw' })`.","zh":"核心想法：螢幕上的控制器和硬體說的是「同一種語言」，MIDI 位元組。`setValue()`、`press()` 等虛擬的手，會編出真實控制項會送的那則訊息，再從 `ingest()` 送進去，也就是硬體走的同一個入口。所以虛擬旋鈕和真的旋鈕不會漂開，作品收到的東西也一樣，不論是哪隻手動的。真實機器的位元組請用 `ingest(bytes, { source: 'hw' })` 餵進去。"},{"en":"The `source` of a message decides what is published, so every event is published exactly once. `'hw'`: the device lives here, and [`Midi`](#Midi) already published the generic `midi/…` names, so only `midi/<short>/…` is added. `'ui'`: a virtual hand, so it publishes `midi/<short>/…`, the generic names, and `midi/virtual` (the raw bytes, for Web MIDI shims). `'mirror'`: someone else published it (a phone, feedback), so only the state and the views move.","zh":"訊息的 `source` 決定發佈什麼，讓每個事件只發佈一次。`'hw'`：機器就在這裡，[`Midi`](#Midi) 已經發佈過通用的 `midi/…` 名稱，所以只多發佈 `midi/<short>/…`。`'ui'`：虛擬的手，所以會發佈 `midi/<short>/…`、通用名稱，以及 `midi/virtual`（原始位元組，給 Web MIDI 轉接用）。`'mirror'`：別人已經發佈過了（手機、回饋），所以只更新狀態和畫面。"},{"en":"Signals per control (values are normalised, `raw` keeps the MIDI number): `midi/<short>/<id>` (0..1, −1..1 for a bipolar wheel) and `/raw`; an encoder's `/delta`; a pad's `/hit` pulse `{ vel, velocity, note, ch }` and `/pressure`; a keys control's `/on` and `/off` pulses and `midi/<short>/n<note>` while a key is held; and `midi/<short>/last`, a pulse saying what was touched last. [`profileSignals()`](#profileSignals) lists them for a profile.","zh":"每個控制項的訊號（數值已正規化，`raw` 保留 MIDI 的號碼）：`midi/<short>/<id>`（0..1，雙向彎音輪是 −1..1）和 `/raw`。旋鈕有 `/delta`，打擊墊有 `/hit` 脈衝 `{ vel, velocity, note, ch }` 和 `/pressure`，琴鍵控制項有 `/on`、`/off` 脈衝，按著某個鍵時有 `midi/<short>/n<note>`，另外還有 `midi/<short>/last`，這個脈衝說明最後碰了哪個控制項。[`profileSignals()`](#profileSignals) 能列出某份設定檔的全部名稱。"},{"en":"Each control keeps a state `{ value, raw, held, pressure, pos }` in `controller.state` (read it with `get(id)`). `controller.keys` maps each held key's note to its velocity. `controller.profile` is the working copy (learning changes it), `controller.base` the normalized profile before any learning, `controller.lastEvent` the last message with its text, and `controller.learning` and `controller.learned` track learn mode.","zh":"每個控制項的狀態是 `{ value, raw, held, pressure, pos }`，存在 `controller.state`（用 `get(id)` 讀）。`controller.keys` 把每個按著的琴鍵音符對應到力度。`controller.profile` 是正在使用的副本（學習會改動它），`controller.base` 是還沒學習之前整理過的設定檔，`controller.lastEvent` 是最後一則訊息和它的文字，`controller.learning` 與 `controller.learned` 記錄學習模式。"}],"params":[{"name":"profile","type":"Object","en":"A raw or normalized profile (see [`PROFILES`](#PROFILES)). It is copied, so changing the object later does nothing.","zh":"原始或整理過的設定檔（見 [`PROFILES`](#PROFILES)）。會被複製一份，之後改動原物件不會有影響。"},{"name":"signals","type":"Signals | null","en":"Publish the controller's signals here.","zh":"把控制器的訊號發佈到這裡。"},{"name":"sink","type":"Function | null","en":"`(name, value, { pulse, min, max }) => …` called for each signal instead of `signals` (a relay, a phone). When both are given, `sink` wins.","zh":"每個訊號改成呼叫 `(name, value, { pulse, min, max }) => …`，不走 `signals`（例如中繼、手機）。兩個都給的話以 `sink` 為準。"},{"name":"short","type":"String | null","en":"Overrides the signal namespace, so two controllers of the same device publish under different names.","zh":"覆寫訊號的命名空間，這樣兩台同型號的控制器就能用不同名稱發佈。"},{"name":"onLed","type":"Function | null","en":"`(bytes) => …` called with the MIDI that lights or dims a control's LED on the hardware, whenever its state changes.","zh":"控制項狀態改變時，用「讓硬體上 LED 亮起或變暗的 MIDI」呼叫 `(bytes) => …`。"}],"notes":[{"en":"Inside a show, do not read `midi/lpd8/k1` in `update()`: route it to a param with [`controllerRoutes()`](#controllerRoutes). You rarely create a `MidiController` yourself: [`MidiControllers`](#MidiControllers) makes one per profile, and [`createController()`](#createController) wraps all of it.","zh":"在演出裡不要在 `update()` 讀 `midi/lpd8/k1`，要用 [`controllerRoutes()`](#controllerRoutes) 把它接到參數上。你很少需要自己建立 `MidiController`：[`MidiControllers`](#MidiControllers) 會替每份設定檔建立一個，[`createController()`](#createController) 則把這一切包起來。"}],"examples":[{"code":"import { MidiController, profileById } from '@openav/midi';\n\nconst lpd8 = new MidiController(profileById('lpd8'));\nlpd8.onChange((id, st, info) => id && console.log(info.source, id, st.value.toFixed(2), st.raw));\n\nlpd8.ingest([0xb0, 70, 127], { source: 'hw' });   // the real knob 1 moved to the top\nlpd8.setValue('k2', 0.5);                          // a finger on the on-screen knob 2\nlpd8.press('pad1', 0.8);                           // a finger on pad 1","caption":{"en":"Prints three lines: `hw k1 1.00 127`, `ui k2 0.50 64`, `ui pad1 0.80 102`. The same model, three hands.","zh":"印出三行：`hw k1 1.00 127`、`ui k2 0.50 64`、`ui pad1 0.80 102`。同一個模型，三隻手。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { MidiController, profileById } from '@openav/midi';\n\nconst signals = new Signals();\nconst mc = new MidiController(profileById('minilab3'), { signals });\nsignals.on('midi/minilab3/pad3/hit', (hit) => console.log('pad 3 hit', hit));\n\nmc.press('pad3', 0.5);\nconsole.log(signals.get('midi/minilab3/pad3'));       // 0.50...: velocity while held\nmc.release('pad3');\nconsole.log(signals.get('midi/minilab3/pad3'));       // 0","caption":{"en":"Prints the `/hit` pulse `{ vel: 0.50…, velocity: 64, note: 38, ch: 10 }`, then the pad's value while held and after release.","zh":"印出 `/hit` 脈衝 `{ vel: 0.50…, velocity: 64, note: 38, ch: 10 }`，再印出打擊墊按著時與放開後的值。"},"run":"module","lang":"js"},{"code":"import { MidiController, profileById } from '@openav/midi';\n\n// a generic layout learns whatever you move\nconst mc = new MidiController(profileById('generic-8k8p'));\nmc.learn('k3');                                       // arm control k3\nmc.ingest([0xb0, 50, 99], { source: 'hw' });          // the next CC from the hardware is bound to it\nconsole.log(JSON.stringify(mc.exportMapping()));       // {\"k3\":{\"msg\":\"cc\",\"ch\":1,\"cc\":50}}\n\nconst again = new MidiController(profileById('generic-8k8p'));\nagain.applyMapping(mc.exportMapping());\nagain.ingest([0xb0, 50, 10], { source: 'hw' });\nconsole.log(again.get('k3').raw);                     // 10","caption":{"en":"Prints the learned binding of k3, then `10`: a second controller with the saved mapping reacts to the same CC.","zh":"印出 k3 學到的綁定，再印出 `10`：套用了存起來的對應的第二個控制器，對同一個 CC 會有反應。"},"run":"module","lang":"js"}],"related":["MidiControllers","createController","controllerRoutes","profileSignals","parseMessage"],"keywords":{"en":["controller model","midi controller","ingest","learn mode","mapping","led feedback","virtual knob","press","set value"],"zh":["控制器模型","midi 控制器","餵入訊息","學習模式","對應","led 回饋","虛擬旋鈕","按下","設定數值"]},"tbd":[],"claims":[],"members":[{"name":"id","member":"getter","signature":"","doc":"","line":68},{"name":"control","member":"method","signature":"id","doc":"","line":69},{"name":"get","member":"method","signature":"id","doc":"","line":70},{"name":"groups","member":"method","signature":"","doc":"{group: [values…]} — the TouchDesigner tool's out CHOPs (out_knob, out_slider, out_pad…), as arrays.","line":72},{"name":"onChange","member":"method","signature":"fn","doc":"","line":77},{"name":"onMessage","member":"method","signature":"fn","doc":"Every message that passes through, once, after state has moved: ({bytes, ev, source, ids}) → void.","line":81},{"name":"ingest","member":"method","signature":"data, { source = 'hw' } = {}","doc":"Feed MIDI bytes.","line":91},{"name":"setValue","member":"method","signature":"id, v, { source = 'ui' } = {}","doc":"Absolute controls (knob, fader, wheel/strip, absolute encoder): v in 0..1 (−1..1 bipolar).","line":197},{"name":"turn","member":"method","signature":"id, steps, { source = 'ui' } = {}","doc":"Endless encoder: signed steps.","line":207},{"name":"press","member":"method","signature":"id, vel = 1, note = null, { source = 'ui' } = {}","doc":"Pads, buttons, keys: press with velocity 0..1 (keys/pads: which note).","line":215},{"name":"release","member":"method","signature":"id, note = null, { source = 'ui' } = {}","doc":"","line":228},{"name":"pressure","member":"method","signature":"id, v, note = null, { source = 'ui' } = {}","doc":"Pad/key pressure (poly aftertouch, or channel pressure when the profile says so).","line":237},{"name":"rest","member":"method","signature":"id, { source = 'ui' } = {}","doc":"Spring-loaded wheels go home (pitch bend → centre).","line":244},{"name":"releaseAll","member":"method","signature":"{ source = 'ui' } = {}","doc":"Let go of everything the virtual hands hold (blur, page hide, octave change).","line":246},{"name":"mirror","member":"method","signature":"id, value","doc":"Apply a value that arrived from elsewhere (relay feedback) without publishing.","line":251},{"name":"ledBytes","member":"method","signature":"c, st = this.state.get(c.id), note = null","doc":"Bytes that light (or dim) one control's LED on the hardware, from its state.","line":260},{"name":"ledSnapshot","member":"method","signature":"","doc":"Every LED at its current state — send once when the hardware connects (idle colours).","line":271},{"name":"learn","member":"method","signature":"id","doc":"Arm learn for one control: the next hardware message of a compatible kind is bound to it.","line":275},{"name":"bindMessage","member":"method","signature":"id, ev","doc":"Rebind a control to the message `ev` (swapping with whoever had it).","line":298},{"name":"exportMapping","member":"method","signature":"","doc":"Learned bindings as JSON (persist per device: localStorage, a file, the work's meta).","line":312},{"name":"applyMapping","member":"method","signature":"m = {}","doc":"","line":321},{"name":"resetMapping","member":"method","signature":"","doc":"","line":330}]},
{"id":"MidiControllers","key":"MidiControllers","name":"MidiControllers","aliases":[],"kind":"class","package":"@openav/midi","category":"midi-io","documented":true,"signature":"new MidiControllers({ profiles, signals = null, sink = null, midi = null, initial = null, storage = safeStorage() } = {})","import":"import { MidiControllers } from '@openav/midi';","source":{"file":"packages/midi/manager.js","line":17,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/manager.js#L17"},"summary":{"en":"Ties hardware ports to controller profiles: plug in a MiniLab 3 and the on-screen one appears and moves with your hands; unplug it and it stays playable.","zh":"把硬體埠跟控制器設定檔接起來：接上 MiniLab 3，螢幕上的 MiniLab 3 就出現並跟著你的手動，拔掉它，虛擬的那台仍然可以玩。"},"description":[{"en":"There is one [`MidiController`](#MidiController) per profile, created the first time it is asked for (`get(id)`), so a virtual MiniLab and a real MiniLab are the SAME object: whichever hand moves it, the work receives the same `midi/minilab3/…` signals. `current` is the controller the panel shows.","zh":"每份設定檔只有一個 [`MidiController`](#MidiController)，第一次被要求時才建立（`get(id)`），所以虛擬的 MiniLab 和真的 MiniLab 是「同一個物件」：不論哪隻手在動，作品收到的都是同樣的 `midi/minilab3/…` 訊號。`current` 是面板正在顯示的那個控制器。"},{"en":"With a `midi` attached it matches every input port to a profile by name ([`matchProfile()`](#matchProfile), one port per device through [`pickPort()`](#pickPort)), switches `current` to the first device it recognises, sends the LED colours to its output port, and routes each port's messages to that profile's controller. A port nobody recognises can be given to a profile by hand with `assign(slug, profileId)`; that choice is remembered per port name.","zh":"接上 `midi` 之後，它會用名稱把每個輸入埠對到設定檔（[`matchProfile()`](#matchProfile)，一台機器只取一個埠，由 [`pickPort()`](#pickPort) 決定），把 `current` 切到第一台認得的機器，把 LED 顏色送到它的輸出埠，並把每個埠的訊息導向該設定檔的控制器。沒有人認得的埠，可以用 `assign(slug, profileId)` 手動指定給某份設定檔，這個選擇會依埠名記起來。"},{"en":"It remembers in `storage` under `openav.midi.map.<profile id>` (learned mappings, restored when the controller is created) and `openav.midi.assign` (manual port assignments).","zh":"它會把資料記在 `storage` 的 `openav.midi.map.<設定檔 id>`（學到的對應，建立控制器時還原）和 `openav.midi.assign`（手動指定的埠）。"}],"params":[{"name":"profiles","type":"Object[]","en":"The profiles to manage, usually [`PROFILES`](#PROFILES). Required: the first one is shown when `initial` is not given.","zh":"要管理的設定檔，通常是 [`PROFILES`](#PROFILES)。必填，沒給 `initial` 時會先顯示第一個。"},{"name":"signals","type":"Signals | null","en":"Passed to every [`MidiController`](#MidiController) it creates.","zh":"交給它建立的每個 [`MidiController`](#MidiController)。"},{"name":"sink","type":"Function | null","en":"Passed on to every controller, as in `MidiController`.","zh":"交給每個控制器，用法同 `MidiController`。"},{"name":"midi","type":"Midi | null","en":"An enabled (or about-to-be-enabled) [`Midi`](#Midi) to follow. Leave it out for no hardware, and call `attach(midi)` later.","zh":"要跟隨的 [`Midi`](#Midi)（已啟用或即將啟用）。不給就是沒有硬體，之後再呼叫 `attach(midi)`。"},{"name":"initial","type":"String | null","en":"The profile id shown before any hardware arrives.","zh":"硬體出現之前先顯示的設定檔 id。"},{"name":"storage","type":"Storage | null","en":"Where learned mappings and manual port assignments are remembered: anything with `getItem` and `setItem`. The default is `localStorage` when the page has it, otherwise nothing is remembered.","zh":"用來記住學到的對應和手動指定的埠：任何有 `getItem`、`setItem` 的東西。預設是頁面有的話就用 `localStorage`，沒有的話就不記。"}],"notes":[],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { MidiControllers, PROFILES } from '@openav/midi';\n\nconst signals = new Signals();\nconst mcs = new MidiControllers({ profiles: PROFILES, signals, initial: 'akai-lpd8' });\n\nmcs.current.setValue('k1', 1);\nconsole.log(signals.get('midi/lpd8/k1'));    // 1\nmcs.select('korg-nanokontrol2');\nconsole.log(mcs.current.id);                  // korg-nanokontrol2","caption":{"en":"Prints `1` (a finger on the on-screen LPD8 knob became a signal), then `korg-nanokontrol2` after switching the device on screen.","zh":"印出 `1`（螢幕上 LPD8 旋鈕被手指推動，變成了訊號），切換螢幕裝置之後再印出 `korg-nanokontrol2`。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { Midi, MidiControllers, PROFILES } from '@openav/midi';\n\n// a fake Minilab3 on a fake MIDIAccess\nconst keys = { name: 'Minilab3 MIDI', state: 'connected', onmidimessage: null };\nconst access = { inputs: new Map([['k', keys]]), outputs: new Map() };\n\nconst signals = new Signals();\nconst midi = new Midi({ signals, requestAccess: async () => access });\nawait midi.enable();\n\nconst mcs = new MidiControllers({ profiles: PROFILES, signals, midi });\nconsole.log(mcs.current.id, '<-', mcs.hardwareFor('arturia-minilab3'));\nkeys.onmidimessage({ data: Uint8Array.of(0xb0, 74, 127) });         // the real knob 1\nconsole.log(signals.get('midi/minilab3/knob1'));                     // 1","caption":{"en":"Prints `arturia-minilab3 <- Minilab3 MIDI` (the port was recognised by name), then `1`: a message from the port moved knob 1 of the on-screen MiniLab 3.","zh":"印出 `arturia-minilab3 <- Minilab3 MIDI`（埠名被認出來了），再印出 `1`：埠送來的訊息推動了螢幕上 MiniLab 3 的旋鈕 1。"},"run":"module","lang":"js"}],"related":["MidiController","Midi","matchProfile","pickPort","createController","mountMidiPanel"],"keywords":{"en":["plug in controller","hardware to profile","auto detect","assign port","on-screen controller","manager","hot-plug","devices"],"zh":["接上控制器","硬體對設定檔","自動偵測","指派埠","螢幕控制器","管理器","熱插拔","裝置"]},"tbd":[{"en":"LED feedback (the colours sent back to a device when it is recognised) was not tried on real hardware. Some controls in the shipped profiles are marked `verified: false`: their messages come from the manual but nobody has confirmed them on a real unit yet, and Learn rebinds them.","zh":"LED 回饋（機器被認出時送回去的顏色）沒有在真實的硬體上實測。內建設定檔裡有些控制項標著 `verified: false`：它們的訊息來自說明書，但還沒有人在真實的機器上確認過，可以用學習重新綁定。"}],"claims":[],"members":[{"name":"attach","member":"method","signature":"midi","doc":"Start following a hardware engine (now, or later: an embed connects only when asked).","line":40},{"name":"detach","member":"method","signature":"","doc":"Stop following hardware; the on-screen controllers stay, still playable.","line":50},{"name":"register","member":"method","signature":"profile","doc":"Add (or replace) a profile at runtime — one loaded from a URL, one moved to another channel.","line":59},{"name":"get","member":"method","signature":"profileId","doc":"The controller for a profile (created on first use, with any learned mapping restored).","line":71},{"name":"select","member":"method","signature":"profileId","doc":"","line":81},{"name":"onChange","member":"method","signature":"fn","doc":"","line":82},{"name":"devices","member":"method","signature":"","doc":"Hardware ports and who they drive: [{slug, name, profileId, auto, listening}]","line":86},{"name":"hardwareFor","member":"method","signature":"profileId","doc":"Is a hardware port feeding this controller right now?","line":90},{"name":"assign","member":"method","signature":"slug, profileId, { persist = true } = {}","doc":"Route a port to a profile by hand (unknown device → generic layout), or null to un-route.","line":96},{"name":"dispose","member":"method","signature":"","doc":"","line":152}]},
{"id":"normalizeProfile","key":"normalizeProfile","name":"normalizeProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"normalizeProfile(raw)","import":"import { normalizeProfile } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":117,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L117"},"summary":{"en":"Validates a profile and returns a filled-in copy with defaults for channel, labels, groups, encoder mode and portrait layout; the input is not changed.","zh":"驗證設定檔，回傳一份補好預設值的副本，包含頻道、標籤、分組、旋鈕模式和直立版面，傳進去的物件不會被改動。"},"description":[{"en":"It throws `invalid MIDI profile \"<id>\":` followed by every problem when the profile is not valid. Otherwise it fills in: `channel` (1), `learn` (true for a generic layout), `stack` (true), `sources` and `match` (empty lists), and per control `label` (its id), `group` (its type), `ch` (the profile's channel), an encoder's `relative` (`offset64`), a button's `mode` (`momentary`) and a pitch-bend wheel's `bipolar` and `spring` (both true). Sections get `label`, `flow` (`row`) and `spans`, and `portrait` is built from the sections, top to bottom, with each section's height as its weight when you did not write one.","zh":"設定檔不合法時，它會丟出 `invalid MIDI profile \"<id>\":`，後面接上每一個問題。合法的話會補上：`channel`（1）、`learn`（通用版面為 true）、`stack`（true）、`sources` 和 `match`（空清單），每個控制項的 `label`（用它的 id）、`group`（用它的類型）、`ch`（設定檔的頻道）、旋鈕的 `relative`（`offset64`）、按鈕的 `mode`（`momentary`）、彎音輪的 `bipolar` 和 `spring`（都是 true）。區塊會補上 `label`、`flow`（`row`）和 `spans`，沒寫 `portrait` 的話，會由各區塊從上到下排出來，高度當作權重。"},{"en":"[`MidiController`](#MidiController), [`ControllerCanvas`](#ControllerCanvas) and the view all work on the normalized form, which is what `controller.profile` gives you.","zh":"[`MidiController`](#MidiController)、[`ControllerCanvas`](#ControllerCanvas) 和畫面元件用的都是整理後的版本，也就是 `controller.profile` 給你的那份。"}],"params":[{"name":"raw","type":"Object","en":"A raw profile. It must pass [`validateProfile()`](#validateProfile).","zh":"原始設定檔，必須通過 [`validateProfile()`](#validateProfile)。"}],"returns":{"type":"Object","en":"A new profile object.","zh":"一個新的設定檔物件。"},"notes":[],"examples":[{"code":"import { normalizeProfile, profileById } from '@openav/midi';\n\nconst raw = profileById('lpd8');\nconst p = normalizeProfile(raw);\n\nconsole.log(raw.channel, '->', p.channel);                          // undefined -> 1\nconsole.log(p.controls[0].id, p.controls[0].label, p.controls[0].group);\nconsole.log('learn:', p.learn, '| portrait bands:', p.portrait.length);\n\ntry { normalizeProfile({ id: 'X' }); } catch (e) { console.log(e.message); }","caption":{"en":"Prints the defaults it filled in, then the error message listing what is wrong with `{ id: 'X' }`.","zh":"印出它補上的預設值，再印出 `{ id: 'X' }` 有哪些問題的錯誤訊息。"},"run":"module","lang":"js"}],"related":["validateProfile","PROFILES","MidiController","indexProfile"],"keywords":{"en":["normalize","defaults","fill in profile","portrait","prepare profile"],"zh":["整理","預設值","補齊設定檔","直立版面","準備設定檔"]},"tbd":[],"claims":[]},
{"id":"parseMessage","key":"parseMessage","name":"parseMessage","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"parseMessage(data)","import":"import { parseMessage } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":18,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L18"},"summary":{"en":"Turns the bytes of one MIDI message into an event object such as `{ type: 'noteon', ch: 1, note: 60, vel: 100 }`, or `null` for bytes it does not know.","zh":"把一則 MIDI 訊息的位元組轉成事件物件，例如 `{ type: 'noteon', ch: 1, note: 60, vel: 100 }`，不認得的位元組就回傳 `null`。"},"description":[{"en":"`parseMessage([0x90, 60, 100])` is `{ type: 'noteon', ch: 1, note: 60, vel: 100 }`. Channels are numbered 1 to 16, the way they are printed on a device (the status byte stores 0 to 15), and data bytes are cut to 0..127. Every other function in this package that handles MIDI speaks these event objects.","zh":"`parseMessage([0x90, 60, 100])` 會得到 `{ type: 'noteon', ch: 1, note: 60, vel: 100 }`。頻道編號是 1 到 16，跟機器上印的一樣（狀態位元組裡存的是 0 到 15），資料位元組會被限制在 0..127。這個套件裡其他處理 MIDI 的函式，講的都是這種事件物件。"},{"en":"The `type` is `noteon` and `noteoff` (`note`, `vel`), `polyat` (polyphonic aftertouch: `note`, `value`), `cc` (`cc`, `value`), `program` (`value`), `chanat` (channel pressure: `value`) or `pitchbend` (`value` 0..16383, centre 8192). System messages come back as `clock`, `start`, `continue`, `stop`, `sysex` (with `data`, the whole message) or `system` (with the `status` byte).","zh":"`type` 可能是 `noteon`、`noteoff`（有 `note`、`vel`）、`polyat`（複音觸後：`note`、`value`）、`cc`（`cc`、`value`）、`program`（`value`）、`chanat`（頻道壓力：`value`）、`pitchbend`（`value` 為 0..16383，中心是 8192）。系統訊息則是 `clock`、`start`、`continue`、`stop`、`sysex`（附 `data`，整則訊息）或 `system`（附 `status` 位元組）。"},{"en":"A note-on with velocity 0 comes back as `noteoff`, because that is how many devices say \"released\". Bytes that start with a data byte give `null`: the browser has already resolved running status before it hands you the message.","zh":"力度為 0 的 note-on 會回傳成 `noteoff`，因為很多機器就是這樣表示「放開」。位元組開頭是資料位元組時回傳 `null`：瀏覽器在把訊息交給你之前，已經處理過 running status。"}],"params":[{"name":"data","type":"Array | Uint8Array","en":"The bytes of ONE message, the way Web MIDI hands them over (`event.data`): a status byte first, then up to two data bytes.","zh":"「一則」訊息的位元組，格式跟 Web MIDI 給你的一樣（`event.data`）：先是狀態位元組，後面最多兩個資料位元組。"}],"returns":{"type":"Object | null","en":"The event, or `null` when the input is empty, starts with a data byte (below 0x80) or is a channel status it has no case for.","zh":"事件物件。輸入是空的、以資料位元組（小於 0x80）開頭，或是它沒處理的頻道狀態時，回傳 `null`。"},"notes":[{"en":"It is pure: no DOM, no Web MIDI, no state. [`encodeMessage()`](#encodeMessage) is its inverse, and [`describe()`](#describe) turns the result into a line of text.","zh":"它是純函式：沒有 DOM、沒有 Web MIDI、沒有狀態。[`encodeMessage()`](#encodeMessage) 是它的反向，[`describe()`](#describe) 則把結果變成一行文字。"}],"examples":[{"code":"import { parseMessage } from '@openav/midi';\n\nconsole.log(parseMessage([0x90, 60, 100]));   // note on, middle C\nconsole.log(parseMessage([0x90, 60, 0]));     // velocity 0 reads as note off\nconsole.log(parseMessage([0xe0, 0, 64]));     // pitch bend, centre","caption":{"en":"Open the console: three event objects, a `noteon`, a `noteoff` and a `pitchbend` with `value: 8192`.","zh":"打開主控台：印出三個事件物件，分別是 `noteon`、`noteoff`，以及 `value: 8192` 的 `pitchbend`。"},"run":"module","lang":"js"},{"code":"import { parseMessage } from '@openav/midi';\n\n// what a Web MIDI port gives you: event.data is a Uint8Array\nconst ev = parseMessage(Uint8Array.of(0xb0, 74, 100));\nif (ev && ev.type === 'cc') {\n  console.log('CC', ev.cc, 'on channel', ev.ch, '=', (ev.value / 127).toFixed(2));\n}","caption":{"en":"Prints `CC 74 on channel 1 = 0.79`: a control-change value turned into the 0 to 1 range a sketch wants.","zh":"印出 `CC 74 on channel 1 = 0.79`：把控制變更的值換成草圖想要的 0 到 1。"},"run":"module","lang":"js"}],"related":["encodeMessage","describe","genericSignals","MidiController"],"keywords":{"en":["midi message","parse","decode","bytes","status byte","note on","control change","pitch bend","event"],"zh":["midi 訊息","解析","解碼","位元組","狀態位元組","音符開","控制變更","彎音輪","事件"]},"tbd":[],"claims":[]},
{"id":"pickOutput","key":"pickOutput","name":"pickOutput","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"pickOutput(outputs, { preferred = '', prefer = [] } = {})","import":"import { pickOutput } from '@openav/midi';","source":{"file":"packages/midi/ports.js","line":18,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/ports.js#L18"},"summary":{"en":"Chooses which MIDI output to send to from a list: the port you named, else the first match of a preference list, else the first live port.","zh":"從清單裡挑要送到哪個 MIDI 輸出埠：先是你指定的名稱，其次是偏好清單的第一個符合者，最後才是第一個存活的埠。"},"description":[{"en":"The order is: 1) the port named `preferred`, 2) the first port that matches each entry of `prefer`, in order, 3) the first port. Only live ports (see [`isLive()`](#isLive)) are considered, so a disconnected port is never picked.","zh":"順序是：1）名稱符合 `preferred` 的埠，2）依序比對 `prefer` 每一項，取第一個符合的埠，3）第一個埠。只考慮存活的埠（見 [`isLive()`](#isLive)），所以斷線的埠不會被選到。"},{"en":"It is a pure function over plain objects, so you can test the rules without any hardware. The [`Midi`](#Midi) engine applies them through [`reselectOutput()`](#reselectOutput).","zh":"它是處理普通物件的純函式，所以不用任何硬體就能測這些規則。[`Midi`](#Midi) 引擎是透過 [`reselectOutput()`](#reselectOutput) 來套用它們。"}],"params":[{"name":"outputs","type":"MIDIOutput[]","en":"The candidate ports (objects with a `name` and a `state`).","zh":"候選的埠（有 `name` 和 `state` 的物件）。"},{"name":"preferred","type":"String","en":"The port you asked for: tried by exact name, then as a substring.","zh":"你指定的埠：先比對完整名稱，再比對子字串。"},{"name":"prefer","type":"Array<String | RegExp>","en":"Fallback order. The first entry that matches a live port wins (a string is a substring).","zh":"備用順序。第一個對得上存活埠的項目勝出（字串以子字串比對）。"}],"returns":{"type":"MIDIOutput | null","en":"The chosen port, or `null` when no port is live.","zh":"選到的埠。沒有任何存活的埠時回傳 `null`。"},"notes":[],"examples":[{"code":"import { pickOutput } from '@openav/midi';\n\nconst port = (name, state = 'connected') => ({ name, state });\nconst outs = [port('Synth A'), port('IAC Driver Bus 1'), port('Synth B')];\n\nconsole.log(pickOutput(outs).name);                              // Synth A\nconsole.log(pickOutput(outs, { preferred: 'IAC' }).name);        // IAC Driver Bus 1\nconsole.log(pickOutput(outs, { prefer: [/synth b/i] }).name);    // Synth B\nconsole.log(pickOutput([port('X', 'disconnected')]));            // null","caption":{"en":"Prints `Synth A`, `IAC Driver Bus 1`, `Synth B` and `null`: the first port, your choice, the preference list, and nothing live.","zh":"印出 `Synth A`、`IAC Driver Bus 1`、`Synth B` 和 `null`：分別是第一個埠、你指定的埠、偏好清單，以及沒有任何存活埠。"},"run":"module","lang":"js"}],"related":["reselectOutput","isLive","Midi"],"keywords":{"en":["choose output","output port","prefer","iac","select port"],"zh":["挑輸出埠","輸出埠","偏好","iac","選埠"]},"tbd":[],"claims":[]},
{"id":"pickPort","key":"pickPort","name":"pickPort","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"pickPort(ports, profile)","import":"import { pickPort } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":180,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L180"},"summary":{"en":"Of several ports one device exposes (\"Minilab3 MIDI\", \"Minilab3 DIN THRU\", \"Minilab3 MCU/HUI\"), picks the one to listen to.","zh":"一台機器開出好幾個埠（「Minilab3 MIDI」、「Minilab3 DIN THRU」、「Minilab3 MCU/HUI」）時，挑出要聽的那一個。"},"description":[{"en":"A port scores 1 when its name matches one of the profile's `match.ports` (otherwise it is out), plus 1 for a `prefer` match and minus 2 for an `avoid` match. Only positive scores count and the highest wins. It is the second half of device detection after [`matchProfile()`](#matchProfile).","zh":"埠名符合設定檔的某個 `match.ports` 得 1 分（不符合就出局），符合 `prefer` 加 1 分，符合 `avoid` 扣 2 分。只算正分，最高分勝出。它是 [`matchProfile()`](#matchProfile) 之後裝置偵測的第二步。"}],"params":[{"name":"ports","type":"Object[]","en":"Candidate ports: objects with a `name`.","zh":"候選的埠：有 `name` 的物件。"},{"name":"profile","type":"Object","en":"The device's profile (its `match.ports`, `prefer` and `avoid`).","zh":"那台機器的設定檔（用它的 `match.ports`、`prefer`、`avoid`）。"}],"returns":{"type":"Object | null","en":"The best port, or `null` when none of them fits the profile.","zh":"最合適的埠。沒有任何一個符合設定檔時回傳 `null`。"},"notes":[],"examples":[{"code":"import { pickPort, profileById } from '@openav/midi';\n\nconst ports = [{ name: 'Minilab3 DIN THRU' }, { name: 'Minilab3 MIDI' }, { name: 'Minilab3 MCU/HUI' }];\nconsole.log(pickPort(ports, profileById('minilab3')).name);   // Minilab3 MIDI\nconsole.log(pickPort([{ name: 'Other' }], profileById('minilab3')));   // null","caption":{"en":"Prints `Minilab3 MIDI`, then `null` when no port belongs to the device.","zh":"印出 `Minilab3 MIDI`，埠都不屬於那台機器時印出 `null`。"},"run":"module","lang":"js"}],"related":["matchProfile","MidiControllers"],"keywords":{"en":["pick port","multiple ports","din thru","mcu","which port"],"zh":["挑埠","多個埠","din thru","mcu","聽哪個埠"]},"tbd":[],"claims":[]},
{"id":"profileById","key":"profileById","name":"profileById","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"profileById(id)","import":"import { profileById } from '@openav/midi';","source":{"file":"packages/midi/profiles/index.js","line":41,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles/index.js#L41"},"summary":{"en":"Looks up a shipped controller profile by its id (`'akai-lpd8'`) or its short signal name (`'lpd8'`), or returns `null`.","zh":"用 id（`'akai-lpd8'`）或訊號用的短名稱（`'lpd8'`）找一個內建的控制器設定檔，找不到回傳 `null`。"},"description":[{"en":"Only profiles from `PROFILES` are searched. A profile you loaded from a URL is not in there: use [`resolveProfile()`](#resolveProfile), which also accepts ids. It does not normalize or validate: pass the result to [`normalizeProfile()`](#normalizeProfile) when you need the filled-in form.","zh":"只會在 `PROFILES` 裡找。從網址載入的設定檔不在裡面，請用 [`resolveProfile()`](#resolveProfile)，它也收 id。它不會整理也不會驗證，需要填好預設值的版本時，請把結果交給 [`normalizeProfile()`](#normalizeProfile)。"}],"params":[{"name":"id","type":"String","en":"A profile `id` or its `short` name (the part in `midi/<short>/…`).","zh":"設定檔的 `id` 或 `short` 短名稱（`midi/<short>/…` 裡的那一段）。"}],"returns":{"type":"Object | null","en":"The raw profile from [`PROFILES`](#PROFILES), the same object each time, or `null`.","zh":"[`PROFILES`](#PROFILES) 裡的原始設定檔，每次回傳同一個物件，找不到則是 `null`。"},"notes":[],"examples":[{"code":"import { profileById } from '@openav/midi';\n\nconst lpd8 = profileById('lpd8');                    // short name\nconsole.log(lpd8.id, '|', lpd8.name, '|', lpd8.controls.length, 'controls');\nconsole.log(profileById('akai-lpd8') === lpd8);      // true: the same profile by its id\nconsole.log(profileById('no-such-box'));             // null","caption":{"en":"Prints the LPD8's id, name and 16 controls, then `true`, then `null`.","zh":"印出 LPD8 的 id、名稱和 16 個控制項，接著 `true`，最後 `null`。"},"run":"module","lang":"js"}],"related":["PROFILES","resolveProfile","normalizeProfile","findProfile"],"keywords":{"en":["find profile","lookup","profile by id","short name","lpd8","minilab3"],"zh":["找設定檔","查詢","依 id 找","短名稱","lpd8","minilab3"]},"tbd":[],"claims":[]},
{"id":"PROFILES","key":"PROFILES","name":"PROFILES","aliases":[],"kind":"constant","package":"@openav/midi","category":"midi-io","documented":true,"signature":"PROFILES = array, 27 items","import":"import { PROFILES } from '@openav/midi';","source":{"file":"packages/midi/profiles/index.js","line":35,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles/index.js#L35"},"summary":{"en":"Every controller profile that ships with the package, as an array of plain JSON objects: real devices first, then the generic layouts.","zh":"套件內建的所有控制器設定檔，是一個由普通 JSON 物件組成的陣列：先是真實的機器，再來是通用版面。"},"description":[{"en":"A profile describes one controller as data: what it looks like (the faceplate sections), every control on it (type and the exact MIDI message it sends), LED feedback where the device accepts it, how to recognise its port name, and the document each number came from. Each one is a JSON file in `packages/midi/profiles/`; the full field list is in the package README and the [controllers page](/controllers/) shows them all.","zh":"設定檔把一台控制器寫成資料：長什麼樣子（面板的各個區塊）、上面每個控制項（類型，以及它送出的確切 MIDI 訊息）、機器有支援的 LED 回饋、怎麼認出它的埠名，以及每個數字的出處文件。每個設定檔是 `packages/midi/profiles/` 裡的一個 JSON 檔，完整欄位在套件的 README 裡，[控制器頁面](/zh/controllers/)則列出全部。"},{"en":"A `device` profile cites its sources and is matched to ports by name, so plugging the device in picks it automatically. A `generic` layout (`generic-8k8p`, `generic-keys49` …) never auto-matches: you assign a device to it and it learns the controls in the order you move them.","zh":"`device` 設定檔要註明資料來源，並且用名稱跟埠配對，所以接上機器就會自動選到它。`generic`（通用）版面（`generic-8k8p`、`generic-keys49` …）永遠不會自動配對：你把某台機器指定給它，它會照你轉動的順序學習控制項。"},{"en":"Pick one with [`profileById()`](#profileById) (by `id` or by the short name), and hand it to [`createController()`](#createController), [`MidiController`](#MidiController) or [`MidiControllers`](#MidiControllers). Do not edit these objects in place: copy first (`structuredClone`), as [`withChannel()`](#withChannel) does.","zh":"用 [`profileById()`](#profileById) 挑一個（用 `id` 或短名稱），再交給 [`createController()`](#createController)、[`MidiController`](#MidiController) 或 [`MidiControllers`](#MidiControllers)。不要直接修改這些物件，要先複製（`structuredClone`），[`withChannel()`](#withChannel) 就是這樣做的。"}],"value":{"type":"Object[]","en":"The order is the order a device picker shows. Each item is a raw profile (see [`normalizeProfile()`](#normalizeProfile) for the filled-in form): `id`, `short`, `name`, `kind` (`'device'` or `'generic'`), `face`, `sections`, `controls` and, for a device, `match` and `sources`.","zh":"順序就是裝置選單顯示的順序。每一項是原始設定檔（填好預設值的版本見 [`normalizeProfile()`](#normalizeProfile)）：有 `id`、`short`、`name`、`kind`（`'device'` 或 `'generic'`）、`face`、`sections`、`controls`，真實機器還有 `match` 和 `sources`。"},"params":[],"notes":[{"en":"A control marked `verified: false` (with a `note` saying why) has a message that comes from the manual or a guess and has not been confirmed on a real unit. The faceplate shows a small orange `?` on its label, and Learn rebinds it in one move.","zh":"標著 `verified: false` 的控制項（並附上 `note` 說明原因），它的訊息來自說明書或推測，還沒有在真實的機器上確認過。面板上它的標籤旁會有一個橘色的 `?`，用學習一下就能重新綁定。"}],"examples":[{"code":"import { PROFILES } from '@openav/midi';\n\nfor (const p of PROFILES.slice(0, 4)) {\n  console.log(p.id, '|', p.short, '|', p.kind, '|', p.controls.length, 'controls');\n}\nconsole.log(PROFILES.filter((p) => p.kind === 'generic').map((p) => p.id));","caption":{"en":"Prints the first four devices (id, short name, kind, control count), then the ids of the generic layouts.","zh":"印出前四台機器（id、短名稱、類型、控制項數量），再印出通用版面的 id。"},"run":"module","lang":"js"}],"related":["profileById","normalizeProfile","validateProfile","matchProfile"],"keywords":{"en":["profiles","controllers list","devices","minilab3","lpd8","launchpad","generic layout","midi controller data"],"zh":["設定檔","控制器清單","機器","通用版面","midi 控制器資料","minilab3","lpd8","launchpad"]},"tbd":[],"claims":[]},
{"id":"profileSignals","key":"profileSignals","name":"profileSignals","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"profileSignals(p)","import":"import { profileSignals } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":193,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L193"},"summary":{"en":"Lists every signal name a profile can publish under its own namespace (`midi/lpd8/…` for the LPD8), for docs, tests and mapping screens.","zh":"列出一份設定檔會發佈在它自己命名空間底下的所有訊號名稱（LPD8 是 `midi/lpd8/…`），給文件、測試和映射畫面用。"},"description":[{"en":"For each control: `midi/<short>/<id>` and `/raw`; an encoder adds `/delta`; a pad adds `/hit` (and `/pressure` when it has aftertouch). A keys control has only `/on`, `/off` and one `midi/<short>/n<note>` per key. These are the names a [`MidiController`](#MidiController) publishes, so a route from `controllerRoutes()` or a `signals.on()` can use them.","zh":"每個控制項有 `midi/<short>/<id>` 和 `/raw`。旋鈕多一個 `/delta`，打擊墊多一個 `/hit`（有觸後的話再加 `/pressure`）。琴鍵控制項只有 `/on`、`/off`，以及每個琴鍵一個 `midi/<short>/n<note>`。這些就是 [`MidiController`](#MidiController) 發佈的名稱，所以 `controllerRoutes()` 的路由或 `signals.on()` 都能用。"}],"params":[{"name":"p","type":"Object","en":"A profile (its `short` and `controls` are read).","zh":"設定檔（會讀它的 `short` 和 `controls`）。"}],"returns":{"type":"string[]","en":"Signal names, starting with `midi/<short>/last`.","zh":"訊號名稱，第一個是 `midi/<short>/last`。"},"notes":[],"examples":[{"code":"import { profileSignals, profileById } from '@openav/midi';\n\nconst names = profileSignals(profileById('lpd8'));\nconsole.log(names.length, 'signals');\nconsole.log(names.slice(0, 5));","caption":{"en":"Prints how many signals the LPD8 can publish and the first five: `midi/lpd8/last`, then pad 1's value, raw value and hit, then pad 2.","zh":"印出 LPD8 能發佈幾個訊號和前五個：`midi/lpd8/last`，然後是 pad 1 的值、原始值、hit，接著是 pad 2。"},"run":"module","lang":"js"}],"related":["controllerRoutes","MidiController","groupsOf"],"keywords":{"en":["signal names","midi/lpd8","list signals","what does it publish"],"zh":["訊號名稱","列出訊號","會發佈什麼"]},"tbd":[],"claims":[]},
{"id":"publish","key":"publish","name":"publish","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"publish(signals, list)","import":"import { publish } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":186,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L186"},"summary":{"en":"Publishes a list of `{ name, value, pulse?, min?, max? }` into a `Signals` registry, declaring each signal the first time it is seen.","zh":"把一串 `{ name, value, pulse?, min?, max? }` 發佈到 `Signals` 登錄表，每個訊號第一次出現時會先宣告。"},"description":[{"en":"For a new continuous signal it defines the range from `min` and `max` (0 and 1 when missing) and `source: 'midi'`. A pulse is defined as `kind: 'pulse'`. Then it calls `signals.pulse(name, value)` or `signals.set(name, value)`.","zh":"遇到新的連續訊號，會用 `min`、`max`（沒寫就是 0 和 1）和 `source: 'midi'` 來宣告範圍。脈衝則宣告成 `kind: 'pulse'`。接著呼叫 `signals.pulse(name, value)` 或 `signals.set(name, value)`。"},{"en":"The [`Midi`](#Midi) engine and [`MidiController`](#MidiController) already do this for you. Call it yourself when you read MIDI some other way, for example from a WebSocket, and want the same names in your show.","zh":"[`Midi`](#Midi) 引擎和 [`MidiController`](#MidiController) 已經替你做了這件事。只有在你用別的方式讀 MIDI（例如從 WebSocket）、又想在作品裡得到同樣名稱的時候，才需要自己呼叫。"}],"params":[{"name":"signals","type":"Signals","en":"The registry from `@openav/core`. `null` makes the call do nothing.","zh":"`@openav/core` 的登錄表。給 `null` 就什麼都不做。"},{"name":"list","type":"Object[]","en":"Usually the result of [`genericSignals()`](#genericSignals). `pulse: true` fires a pulse, anything else sets a value.","zh":"通常是 [`genericSignals()`](#genericSignals) 的結果。`pulse: true` 會觸發脈衝，其他的會設定數值。"}],"notes":[],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { parseMessage, genericSignals, publish } from '@openav/midi';\n\nconst signals = new Signals();\nsignals.on('midi/cc/74', (v) => console.log('cc 74 is now', v.toFixed(2)));\n\n// bytes from anywhere: a WebSocket, a file, a test\npublish(signals, genericSignals(parseMessage([0xb0, 74, 100])));\nconsole.log(signals.get('midi/ch/1/cc/74'));","caption":{"en":"Prints `cc 74 is now 0.79`, then the same value read from the per-channel name.","zh":"印出 `cc 74 is now 0.79`，再印出從分頻道名稱讀到的同一個值。"},"run":"module","lang":"js"}],"related":["genericSignals","parseMessage","Midi","MidiController"],"keywords":{"en":["publish","signals","define signal","midi to signals","pulse"],"zh":["發佈","訊號","宣告訊號","midi 轉訊號","脈衝"]},"tbd":[],"claims":[]},
{"id":"RELATIVE_MODES","key":"RELATIVE_MODES","name":"RELATIVE_MODES","aliases":[],"kind":"constant","package":"@openav/midi","category":"midi-io","documented":true,"signature":"RELATIVE_MODES = array, 4 items","import":"import { RELATIVE_MODES } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":89,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L89"},"summary":{"en":"The four ways an endless encoder can write a turn as a CC value: `offset64`, `offset16`, `twos` and `signbit`.","zh":"無限旋鈕把一次轉動寫成 CC 值的四種方式：`offset64`、`offset16`、`twos`、`signbit`。"},"description":[{"en":"Use it to check or list the encodings, for example when you build a profile editor. `validateProfile()` reports an encoder whose `relative` is not in this list.","zh":"用來檢查或列出編碼方式，例如做設定檔編輯器的時候。`validateProfile()` 會回報 `relative` 不在這個清單裡的旋鈕。"}],"value":{"type":"string[]","range":"['offset64', 'offset16', 'twos', 'signbit']","en":"A plain array. [`relativeDelta()`](#relativeDelta) and [`relativeValue()`](#relativeValue) take one of these as `mode`, and a profile's `relative` field must be one of them (or `false` for an absolute encoder).","zh":"一個普通陣列。[`relativeDelta()`](#relativeDelta) 和 [`relativeValue()`](#relativeValue) 的 `mode` 要從裡面挑一個，設定檔的 `relative` 欄位也必須是其中之一（絕對值旋鈕寫 `false`）。"},"params":[],"notes":[],"examples":[{"code":"import { RELATIVE_MODES } from '@openav/midi';\n\nconsole.log(RELATIVE_MODES);","caption":{"en":"Prints the four mode names.","zh":"印出四個模式名稱。"},"run":"module","lang":"js"}],"related":["relativeDelta","relativeValue","validateProfile"],"keywords":{"en":["relative modes","encoder encodings","offset64","twos","signbit"],"zh":["相對模式","編碼方式","旋鈕編碼"]},"tbd":[],"claims":[]},
{"id":"relativeDelta","key":"relativeDelta","name":"relativeDelta","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"relativeDelta(value, mode = 'offset64')","import":"import { relativeDelta } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":68,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L68"},"summary":{"en":"Turns the CC value an endless encoder sends into signed steps: with `offset64`, 65 is +1, 63 is −1 and 64 is no move.","zh":"把無限旋鈕送來的 CC 值轉成有正負號的步數：用 `offset64` 時，65 是 +1，63 是 −1，64 是沒動。"},"description":[{"en":"An endless encoder has no position, so it sends how far it moved, and devices disagree on how to write it. `offset64`: 64 is no move, 65 is +1, 63 is −1. `offset16`: 16 is no move, 17 is +1, 15 is −1. `twos`: 1..63 are +1..+63 and 127..65 are −1..−63 (two's complement). `signbit`: 1..63 are positive, 65..127 are −1..−63 (sign and magnitude).","zh":"無限旋鈕沒有位置，它送出的是「轉了多少」，而各家機器的寫法不一樣。`offset64`：64 是沒動，65 是 +1，63 是 −1。`offset16`：16 是沒動，17 是 +1，15 是 −1。`twos`：1..63 是 +1..+63，127..65 是 −1..−63（二的補數）。`signbit`：1..63 是正的，65..127 是 −1..−63（符號加大小）。"},{"en":"An unknown `mode` throws `unknown relative mode \"x\"`. [`MidiController`](#MidiController) uses this for every `encoder` control whose profile says `relative`, and publishes the result as `midi/<short>/<id>/delta`.","zh":"不認得的 `mode` 會丟出 `unknown relative mode \"x\"`。[`MidiController`](#MidiController) 對每個設定檔有寫 `relative` 的 `encoder` 控制項都用它，並把結果發佈成 `midi/<short>/<id>/delta`。"}],"params":[{"name":"value","type":"Number","en":"The CC value, 0..127 (only the low 7 bits are read).","zh":"CC 的值，0..127（只讀低 7 位元）。"},{"name":"mode","type":"String","en":"How the device writes a turn: one of [`RELATIVE_MODES`](#RELATIVE_MODES). A profile names it in the control's `relative` field.","zh":"機器怎麼表示一次轉動：[`RELATIVE_MODES`](#RELATIVE_MODES) 其中之一。設定檔在控制項的 `relative` 欄位裡寫明。"}],"returns":{"type":"Number","en":"The signed step count, positive for clockwise.","zh":"有正負號的步數，順時針為正。"},"notes":[],"examples":[{"code":"import { relativeDelta } from '@openav/midi';\n\nconsole.log(relativeDelta(65));            // 1  (offset64, the default)\nconsole.log(relativeDelta(63));            // -1\nconsole.log(relativeDelta(127, 'twos'));   // -1\nconsole.log(relativeDelta(67, 'signbit')); // -3","caption":{"en":"Prints 1, −1, −1 and −3: the same kind of turn written four ways.","zh":"印出 1、−1、−1、−3：同一種轉動的四種寫法。"},"run":"module","lang":"js"}],"related":["relativeValue","RELATIVE_MODES","MidiController"],"keywords":{"en":["encoder","endless","relative","delta","offset64","twos complement","steps","knob"],"zh":["編碼器","無限旋鈕","相對值","增量","步數","二的補數","旋鈕"]},"tbd":[],"claims":[]},
{"id":"relativeValue","key":"relativeValue","name":"relativeValue","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"relativeValue(delta, mode = 'offset64')","import":"import { relativeValue } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":79,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L79"},"summary":{"en":"The inverse of `relativeDelta()`: the CC value that says \"turned by this many steps\" in a given encoder encoding.","zh":"`relativeDelta()` 的反向：在指定的旋鈕編碼下，代表「轉了這麼多步」的 CC 值。"},"description":[{"en":"A virtual encoder sends the same encoding a hardware one does, so the on-screen controller and the real device look identical to everything downstream. `relativeValue(-1)` is 63 and `relativeValue(3, 'twos')` is 3. With `offset16` the result is held at 0, so it can only go down to −16.","zh":"虛擬的旋鈕送出跟實體旋鈕一樣的編碼，所以螢幕上的控制器和真實機器，在下游看起來完全一樣。`relativeValue(-1)` 是 63，`relativeValue(3, 'twos')` 是 3。`offset16` 的結果會停在 0，所以最多只能往下到 −16。"},{"en":"An unknown `mode` throws `unknown relative mode \"x\"`.","zh":"不認得的 `mode` 會丟出 `unknown relative mode \"x\"`。"}],"params":[{"name":"delta","type":"Number","en":"Signed steps. Rounded and held to −63..63.","zh":"有正負號的步數，會四捨五入並限制在 −63..63。"},{"name":"mode","type":"String","en":"One of [`RELATIVE_MODES`](#RELATIVE_MODES).","zh":"[`RELATIVE_MODES`](#RELATIVE_MODES) 其中之一。"}],"returns":{"type":"Number","en":"The CC value, 0..127.","zh":"CC 的值，0..127。"},"notes":[],"examples":[{"code":"import { relativeValue, relativeDelta, RELATIVE_MODES } from '@openav/midi';\n\nfor (const mode of RELATIVE_MODES) {\n  const cc = relativeValue(-3, mode);\n  console.log(mode, 'sends', cc, 'which reads back as', relativeDelta(cc, mode));\n}","caption":{"en":"Four lines: turning −3 steps is sent as 61, 13, 125 and 67, and each reads back as −3.","zh":"四行輸出：轉 −3 步在四種編碼下分別送出 61、13、125、67，讀回來都是 −3。"},"run":"module","lang":"js"}],"related":["relativeDelta","RELATIVE_MODES"],"keywords":{"en":["encoder","relative","encode steps","virtual encoder","offset64"],"zh":["編碼器","相對值","步數轉 CC","虛擬旋鈕"]},"tbd":[],"claims":[]},
{"id":"reselectOutput","key":"reselectOutput","name":"reselectOutput","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"reselectOutput({ current, outputs, preferred = '', prefer = [] })","import":"import { reselectOutput } from '@openav/midi';","source":{"file":"packages/midi/ports.js","line":40,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/ports.js#L40"},"summary":{"en":"Decides what to send to after the port list changed or the current port died, and returns the one line to print about it.","zh":"埠清單改變、或目前的埠斷線之後，決定現在該送到哪裡，並回傳一行要印出來的說明。"},"description":[{"en":"It switches only when the current port is gone or disconnected (to the best live one, or to none), or when the port you named is back while a stand-in is in use (back to it). In every other case it stays: plugging in an unrelated device never steals the output. The text is plain ASCII, such as `MIDI out: switched to \"Synth\" (\"IAC Driver Bus 1\" was unplugged)`.","zh":"只有兩種情況會換：目前的埠不見了或斷線（換到最好的存活埠，或是變成沒有），或是你指定的埠回來了而現在用的是替代品（換回去）。其他情況一律不動：插上不相干的新裝置，不會搶走輸出。說明文字是純 ASCII，例如 `MIDI out: switched to \"Synth\" (\"IAC Driver Bus 1\" was unplugged)`。"},{"en":"This is the rule behind the [`Midi`](#Midi) engine's output hot-plug. It never touches Web MIDI, so a test can call it with plain objects.","zh":"[`Midi`](#Midi) 引擎處理輸出熱插拔，靠的就是這條規則。它完全不碰 Web MIDI，所以測試時用普通物件呼叫就行。"}],"params":[{"name":"current","type":"MIDIOutput | null","en":"The port in use now.","zh":"現在正在用的埠。"},{"name":"outputs","type":"MIDIOutput[]","en":"The ports there are now.","zh":"現在有的埠。"},{"name":"preferred","type":"String","en":"The port you chose by name (the one to go back to).","zh":"你用名稱選的埠（要回去的那一個）。"},{"name":"prefer","type":"Array<String | RegExp>","en":"Fallback order, as in [`pickOutput()`](#pickOutput).","zh":"備用順序，跟 [`pickOutput()`](#pickOutput) 一樣。"}],"returns":{"type":"{ next, changed, reason, text }","en":"`next` is the port to use. `changed` says whether it differs from `current`. `reason` is `same`, `found`, `fallback`, `back` or `none`. `text` is the line to log (an empty string when nothing changed).","zh":"`next` 是該用的埠，`changed` 表示是否跟 `current` 不同，`reason` 是 `same`、`found`、`fallback`、`back` 或 `none`，`text` 是要記錄的一行（沒變時是空字串）。"},"notes":[],"examples":[{"code":"import { reselectOutput } from '@openav/midi';\n\nconst iac = { name: 'IAC Driver Bus 1', state: 'connected' };\nconst synth = { name: 'Synth', state: 'connected' };\n\n// the IAC bus is unplugged: only the synth is left\nconst r = reselectOutput({ current: iac, outputs: [synth], preferred: 'IAC Driver Bus 1' });\nconsole.log(r.reason, r.changed, r.next.name);\nconsole.log(r.text);\n\n// the IAC bus comes back while the synth stands in: go back to your choice\nconst back = reselectOutput({ current: synth, outputs: [synth, iac], preferred: 'IAC Driver Bus 1' });\nconsole.log(back.reason, back.next.name);","caption":{"en":"Prints `fallback true Synth` and its sentence, then `back IAC Driver Bus 1`.","zh":"印出 `fallback true Synth` 和它的說明句，再印出 `back IAC Driver Bus 1`。"},"run":"module","lang":"js"}],"related":["pickOutput","isLive","Midi"],"keywords":{"en":["hot-plug","unplugged","fallback output","switch back","output changed"],"zh":["熱插拔","被拔掉","備用輸出","換回來","輸出改變"]},"tbd":[],"claims":[]},
{"id":"RESERVED_IDS","key":"RESERVED_IDS","name":"RESERVED_IDS","aliases":[],"kind":"constant","package":"@openav/midi","category":"midi-io","documented":true,"signature":"RESERVED_IDS = array, 7 items","import":"import { RESERVED_IDS } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L30"},"summary":{"en":"Control ids a profile may not use, because they are signal suffixes: `last`, `raw`, `delta`, `hit`, `on`, `off`, `pressure`.","zh":"設定檔裡不能拿來當控制項 id 的名字，因為它們是訊號的後綴：`last`、`raw`、`delta`、`hit`、`on`、`off`、`pressure`。"},"description":[{"en":"A profile's `short` has a separate reserved list (`cc`, `note`, `bend`, `ch`, `virtual`, `raw`, `clock`) because those are the generic `midi/…` families. Both are checked by `validateProfile()`.","zh":"設定檔的 `short` 另有一份保留字清單（`cc`、`note`、`bend`、`ch`、`virtual`、`raw`、`clock`），因為它們是通用的 `midi/…` 家族名稱。兩份都由 `validateProfile()` 檢查。"}],"value":{"type":"string[]","range":"['last', 'raw', 'delta', 'hit', 'on', 'off', 'pressure']","en":"A plain array. `last` is the signal `midi/<short>/last`, and the others are the suffixes a control's signals use (`/raw`, `/delta`, `/hit`, `/on`, `/off`, `/pressure`), so [`validateProfile()`](#validateProfile) rejects them as control ids.","zh":"一個普通陣列。`last` 是訊號 `midi/<short>/last` 的名字，其他的是控制項訊號會用的後綴（`/raw`、`/delta`、`/hit`、`/on`、`/off`、`/pressure`），所以 [`validateProfile()`](#validateProfile) 不接受它們當控制項 id。"},"params":[],"notes":[],"examples":[{"code":"import { RESERVED_IDS, validateProfile, profileById } from '@openav/midi';\n\nconsole.log(RESERVED_IDS);\nconst bad = structuredClone(profileById('lpd8'));\nbad.controls[0].id = 'hit';\nconsole.log(validateProfile(bad).filter((p) => p.includes('reserved')));","caption":{"en":"Prints the list, then the problem raised when a control is renamed to `hit`.","zh":"印出清單，再印出把某個控制項改名為 `hit` 時回報的問題。"},"run":"module","lang":"js"}],"related":["validateProfile","CONTROL_TYPES"],"keywords":{"en":["reserved names","forbidden id","signal suffix"],"zh":["保留字","不能用的 id","訊號後綴"]},"tbd":[],"claims":[]},
{"id":"resolveProfile","key":"resolveProfile","name":"resolveProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"async resolveProfile(ref, { profiles = [], fetch = globalThis.fetch, base = undefined } = {})","import":"import { resolveProfile } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":111,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L111"},"summary":{"en":"Turns a profile object, an id or short name, or a URL to a profile JSON into a profile; an object or a file that is not valid is rejected with every problem listed.","zh":"把設定檔物件、id 或短名稱，或指向設定檔 JSON 的網址，變成一份設定檔，物件或檔案不合法就 reject，並列出所有問題。"},"description":[{"en":"Errors are written for a person: `no MIDI profile given`, `unknown MIDI profile \"x\" (known: …)`, `could not load MIDI profile <url> (404)` and `invalid MIDI profile (<where>):` followed by the list from [`validateProfile()`](#validateProfile).","zh":"錯誤訊息是寫給人看的：`no MIDI profile given`、`unknown MIDI profile \"x\" (known: …)`、`could not load MIDI profile <url> (404)`，以及 `invalid MIDI profile (<where>):` 後面接上 [`validateProfile()`](#validateProfile) 的清單。"}],"params":[{"name":"ref","type":"String | Object","en":"A profile object (checked and returned), an id or short name (looked up in `profiles`), or a URL (fetched). A URL is a string starting with `http:`, `https:`, `blob:`, `data:`, `/`, `./` or `../`, or ending in `.json`.","zh":"設定檔物件（檢查後直接回傳）、id 或短名稱（在 `profiles` 裡查），或網址（會去抓）。網址指的是以 `http:`、`https:`、`blob:`、`data:`、`/`、`./`、`../` 開頭，或以 `.json` 結尾的字串。"},{"name":"profiles","type":"Object[]","en":"Where ids are looked up. The default is an empty list, so pass [`PROFILES`](#PROFILES) (or use [`loadProfile()`](#loadProfile), which does).","zh":"用來查 id 的清單。預設是空的，所以要傳入 [`PROFILES`](#PROFILES)（或改用已經幫你傳好的 [`loadProfile()`](#loadProfile)）。"},{"name":"fetch","type":"Function","en":"Replaces `fetch`, for tests or special loading. It must return `{ ok, status?, json() }`.","zh":"取代 `fetch`，給測試或特殊的載入方式用。必須回傳 `{ ok, status?, json() }`。"},{"name":"base","type":"String","en":"A base URL that a relative path is resolved against.","zh":"解析相對路徑用的基底網址。"}],"returns":{"type":"Promise<Object>","en":"The raw profile (not normalized).","zh":"原始的設定檔（沒有整理過）。"},"notes":[],"examples":[{"code":"import { resolveProfile, PROFILES, profileById } from '@openav/midi';\n\nconsole.log((await resolveProfile('nano2', { profiles: PROFILES })).id);   // korg-nanokontrol2\nconsole.log((await resolveProfile(profileById('lpd8'))).id);                 // an object is checked and returned\n\ntry { await resolveProfile({ id: 'Bad' }); } catch (e) { console.log(e.message.split('\\n').slice(0, 3).join(' | ')); }","caption":{"en":"Prints `korg-nanokontrol2`, `akai-lpd8`, then the error header and the first two problems of an invalid object.","zh":"印出 `korg-nanokontrol2`、`akai-lpd8`，再印出一個不合法物件的錯誤標題和前兩個問題。"},"run":"module","lang":"js"}],"related":["loadProfile","findProfile","isProfileUrl","validateProfile"],"keywords":{"en":["resolve profile","profile from url","profile json","fetch profile","async"],"zh":["解析設定檔","從網址取得設定檔","設定檔 json","抓取設定檔","非同步"]},"tbd":[],"claims":[]},
{"id":"ScoreMidi","key":"ScoreMidi","name":"ScoreMidi","aliases":[],"kind":"class","package":"@openav/midi","category":"midi-io","documented":true,"signature":"new ScoreMidi({ midi, channel = 15, segment = { note: 36 }, cue = { note: 84 }, length = 0.15, scrub = false, enabled = true, timer = (fn, ms) => setTimeout(fn, ms) } = {})","import":"import { ScoreMidi } from '@openav/midi';","source":{"file":"packages/midi/score-out.js","line":23,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/score-out.js#L23"},"summary":{"en":"Turns a show's segment changes and cues into MIDI notes and CCs on one channel, so a DAW or lighting desk can follow the show.","zh":"把演出的段落切換和 cue 變成某個頻道上的 MIDI 音符與 CC，讓 DAW 或燈光台能跟著演出走。"},"description":[{"en":"The score knows when a segment starts and when a cue passes. An outside program cannot read the score, but it can read MIDI: launch a clip on a note, jump a scene on a CC. Create a `ScoreMidi`, then call `out.segment({ index, cause })` and `out.cue({ name, n })` (`createShow({ score: { …, midi: { channel: 15 } } })` does this for you).","zh":"樂譜知道每個段落什麼時候開始、cue 什麼時候經過。外部程式讀不到樂譜，但讀得到 MIDI：用某個音符啟動片段，用某個 CC 切換場景。建立一個 `ScoreMidi`，然後呼叫 `out.segment({ index, cause })` 和 `out.cue({ name, n })`（`createShow({ score: { …, midi: { channel: 15 } } })` 會替你做這件事）。"},{"en":"Markers go out only while the show plays (`cause` is `play` or `release`). A `reset` never sends, and `seek` and `jump` send only with `scrub: true`. Everything goes through the engine's own `noteOn`, `noteOff` and `cc`, so the MIDI OUT meter shows it and `panic()` can silence it. Off unless you create it.","zh":"只有演出在播放時才送標記（`cause` 是 `play` 或 `release`）。`reset` 永遠不送，`seek` 和 `jump` 只有在 `scrub: true` 時才送。所有訊息都走引擎自己的 `noteOn`、`noteOff`、`cc`，所以 MIDI OUT 音量表看得到，`panic()` 也關得掉。你不建立它就不會送。"}],"params":[{"name":"midi","type":"Midi","en":"What to send through: a [`Midi`](#Midi) (anything with `noteOn`, `noteOff` and `cc`). Required: without it the constructor throws.","zh":"要透過誰送出：一個 [`Midi`](#Midi)（任何有 `noteOn`、`noteOff`、`cc` 的東西都行）。必填，沒給建構時會丟出錯誤。"},{"name":"channel","type":"Number","en":"The MIDI channel, 1..16 (anything else throws).","zh":"MIDI 頻道，1..16（其他值會丟出錯誤）。"},{"name":"segment","type":"false | Object","en":"`{ note, cc, velocity }`: segment number i sends note `note + i` and/or CC `cc` with value i. `velocity` defaults to 100, `note` to 36 (give `note: null` to send only the CC). `false` sends no segment markers.","zh":"`{ note, cc, velocity }`：第 i 段會送出音符 `note + i` 和（或）CC `cc`（值為 i）。`velocity` 預設 100，`note` 預設 36（只想送 CC 的話，給 `note: null`）。`false` 表示不送段落標記。"},{"name":"cue","type":"false | Object","en":"`{ note, notes, cc, velocity }`: every cue sends `note` (or its own note from the map `notes`, by cue name) and/or CC `cc` with the cue's number as value. `note` defaults to 84 (give `note: null` to send only the CC). `false` sends no cue markers.","zh":"`{ note, notes, cc, velocity }`：每個 cue 會送出 `note`（或在 `notes` 對照表裡依 cue 名稱查到的專屬音符）和（或）CC `cc`，值是 cue 的編號。`note` 預設 84（只想送 CC 的話，給 `note: null`）。`false` 表示不送 cue 標記。"},{"name":"length","type":"Number","en":"Seconds before a marker note's note-off. A marker note is a trigger, not a held note.","zh":"標記音符送出後，隔幾秒才送 note-off。標記音符是觸發用的，不是按著的音。"},{"name":"scrub","type":"Boolean","en":"Also mark jumps the playhead made by hand (`seek`, `jump`). Off by default, so dragging the scrubber does not launch clips in the DAW.","zh":"手動拖動播放頭造成的跳轉（`seek`、`jump`）也要送標記。預設關閉，這樣拖動時間軸不會在 DAW 裡誤觸發片段。"},{"name":"enabled","type":"Boolean","en":"Set to `false` (or later, `out.enabled = false`) to send nothing.","zh":"設成 `false`（或之後改 `out.enabled = false`）就什麼都不送。"},{"name":"timer","type":"Function","en":"`(fn, ms) => …` used to schedule the note-offs. It is `setTimeout`; pass your own in a test.","zh":"排程 note-off 用的 `(fn, ms) => …`，預設是 `setTimeout`，測試時可以傳自己的。"}],"notes":[],"examples":[{"code":"import { ScoreMidi } from '@openav/midi';\n\n// anything with noteOn / noteOff / cc will do; here it just prints\nconst midi = {\n  noteOn: (note, vel, ch) => console.log('note on ', note, vel, 'ch', ch),\n  noteOff: (note, ch) => console.log('note off', note, 'ch', ch),\n  cc: (cc, val, ch) => console.log('cc', cc, '=', val, 'ch', ch),\n};\n\nconst out = new ScoreMidi({\n  midi, channel: 15,\n  segment: { note: 36, cc: 20 },\n  cue: { note: 84, notes: { 'dusk.glow': 90 }, cc: 21 },\n});\n\nout.segment({ index: 2, cause: 'play' });   // note 38 and CC 20 = 2\nout.cue({ name: 'dusk.glow', n: 4 });       // note 90 and CC 21 = 4\nout.segment({ index: 1, cause: 'seek' });   // nothing: scrub is off","caption":{"en":"Prints note 38 and CC 20 = 2 for segment 2, note 90 and CC 21 = 4 for the cue, nothing for the seek, and the note-offs 0.15 s later.","zh":"印出第 2 段的音符 38 和 CC 20 = 2，cue 的音符 90 和 CC 21 = 4，跳轉不會有任何輸出，0.15 秒後再印出 note-off。"},"run":"module","lang":"js"}],"related":["Midi","createController"],"keywords":{"en":["score to midi","cue","segment","daw sync","follow the show","markers","trigger clips","lighting desk"],"zh":["樂譜轉 midi","cue","段落","daw 同步","跟著演出","標記","觸發片段","燈光台"]},"tbd":[],"claims":[],"members":[{"name":"segmentMessages","member":"method","signature":"evt","doc":"What a segment change sends: [{ type:'note'|'cc', number, value }] (also the unit-test surface).","line":49},{"name":"cueMessages","member":"method","signature":"evt","doc":"What a cue sends.","line":58},{"name":"segment","member":"method","signature":"evt","doc":"A segment began.","line":76},{"name":"cue","member":"method","signature":"evt","doc":"A cue was passed.","line":78}]},
{"id":"unitToBend","key":"unitToBend","name":"unitToBend","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"unitToBend(u)","import":"import { unitToBend } from '@openav/midi';","source":{"file":"packages/midi/parse.js","line":93,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/parse.js#L93"},"summary":{"en":"Turns a −1..1 number into a 14-bit pitch-bend value (0..16383), the inverse of `bendToUnit()`.","zh":"把 −1..1 的數字轉成 14 位元的彎音輪數值（0..16383），是 `bendToUnit()` 的反向。"},"description":[{"en":"It does not hold `u` to −1..1, so clamp it first if it can overshoot. [`encodeMessage()`](#encodeMessage) clamps the final value into 0..16383 anyway, so a pitch-bend message built from an overshoot stays valid.","zh":"它不會把 `u` 限制在 −1..1，可能超出範圍的話請先自己限制。不過 [`encodeMessage()`](#encodeMessage) 最後會把數值限制在 0..16383，所以用超出範圍的數字組成的彎音訊息仍然是合法的。"}],"params":[{"name":"u","type":"Number","en":"A position from −1 (fully down) to 1 (fully up). 0 is the centre.","zh":"位置，從 −1（最下）到 1（最上），0 是中心。"}],"returns":{"type":"Number","en":"The rounded 14-bit value: `unitToBend(0)` is 8192, `unitToBend(1)` is 16383.","zh":"四捨五入後的 14 位元數值：`unitToBend(0)` 是 8192，`unitToBend(1)` 是 16383。"},"notes":[],"examples":[{"code":"import { unitToBend, encodeMessage } from '@openav/midi';\n\nconsole.log(unitToBend(-1), unitToBend(0), unitToBend(1));   // 0 8192 16383\nconsole.log(encodeMessage({ type: 'pitchbend', ch: 1, value: unitToBend(0.5) }));","caption":{"en":"Prints `0 8192 16383`, then the three bytes of a half-up pitch bend.","zh":"印出 `0 8192 16383`，再印出「推到一半」的彎音訊息的三個位元組。"},"run":"module","lang":"js"}],"related":["bendToUnit","BEND_CENTER","encodeMessage"],"keywords":{"en":["pitch bend","bend","unit to bend","14-bit","wheel"],"zh":["彎音輪","彎音","14 位元","轉換"]},"tbd":[],"claims":[]},
{"id":"validateProfile","key":"validateProfile","name":"validateProfile","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"validateProfile(p)","import":"import { validateProfile } from '@openav/midi';","source":{"file":"packages/midi/profiles.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/profiles.js#L38"},"summary":{"en":"Checks a controller profile and returns every problem as a sentence; an empty array means it is valid.","zh":"檢查一個控制器設定檔，把每個問題都寫成一句話回傳。空陣列表示沒問題。"},"description":[{"en":"It reports all problems at once, not just the first. It checks: `id` and `short` are lower-case `[a-z0-9_-]` and `short` is not a reserved family name (`cc`, `note`, `bend`, `ch`, `virtual`, `raw`, `clock`), `name` and `kind` exist, a `device` cites at least one source, `match` regexes compile, `face` has a size, every control has a unique non-reserved `id` (see [`RESERVED_IDS`](#RESERVED_IDS)), a known `type` and `msg`, numbers in range and no two controls listening to the same message, encoders name their `relative` mode, and the `sections` (and `portrait`, if given) show every control exactly once, inside the faceplate and within their grid.","zh":"它會一次回報所有問題，不是只講第一個。檢查的項目有：`id` 和 `short` 是小寫的 `[a-z0-9_-]`，而且 `short` 不是保留的家族名稱（`cc`、`note`、`bend`、`ch`、`virtual`、`raw`、`clock`）。有 `name` 和 `kind`。`device` 至少註明一個來源。`match` 的正規表示式能編譯。`face` 有尺寸。每個控制項的 `id` 不重複也不是保留字（見 [`RESERVED_IDS`](#RESERVED_IDS)），有認得的 `type` 和 `msg`，數字在範圍內，且沒有兩個控制項聽同一則訊息。旋鈕要寫明 `relative` 模式。`sections`（和有給的話 `portrait`）要讓每個控制項剛好出現一次，位在面板內，並且塞得進它的格子。"},{"en":"This is the check behind `normalizeProfile()` (it throws with this list), `resolveProfile()` and the test that validates every shipped profile. Run it on your own file before you try it in a page.","zh":"`normalizeProfile()`（用這份清單丟出錯誤）、`resolveProfile()`，以及驗證所有內建設定檔的測試，背後都是這個檢查。把自己的檔案放進網頁之前，先跑一次。"}],"params":[{"name":"p","type":"Object","en":"A raw profile object (for example parsed from your JSON file).","zh":"原始的設定檔物件（例如從你的 JSON 檔解析出來的）。"}],"returns":{"type":"string[]","en":"The problems, in plain words. `[]` is valid.","zh":"用白話寫的問題清單。`[]` 表示合法。"},"notes":[],"examples":[{"code":"import { validateProfile, profileById } from '@openav/midi';\n\nconsole.log(validateProfile(profileById('lpd8')));   // [] : valid\n\nconst mine = {\n  id: 'My Box', short: 'cc', name: 'My box', kind: 'device', face: { w: 10, h: 10 },\n  controls: [{ id: 'k1', type: 'knob', msg: 'cc', cc: 200 }], sections: [],\n};\nfor (const problem of validateProfile(mine)) console.log('-', problem);","caption":{"en":"Prints `[]` for a shipped profile, then a list of what is wrong with the made-up one (bad id, reserved short name, no source, CC out of range, control not shown).","zh":"內建設定檔印出 `[]`，接著列出自己亂寫的那個有什麼問題（id 不合法、短名稱是保留字、沒有來源、CC 超出範圍、控制項沒有被顯示）。"},"run":"module","lang":"js"}],"related":["normalizeProfile","resolveProfile","CONTROL_TYPES","RESERVED_IDS"],"keywords":{"en":["validate","check profile","lint","errors","profile schema","my own controller"],"zh":["驗證","檢查設定檔","錯誤","設定檔格式","自己的控制器"]},"tbd":[],"claims":[]},
{"id":"virtualRequestMIDIAccess","key":"virtualRequestMIDIAccess","name":"virtualRequestMIDIAccess","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"virtualRequestMIDIAccess({ real = null, make, waitMs = 1200 })","import":"import { virtualRequestMIDIAccess } from '@openav/midi';","source":{"file":"packages/midi/virtual-access.js","line":203,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/virtual-access.js#L203"},"summary":{"en":"Wraps `navigator.requestMIDIAccess` so every caller gets the virtual access: real ports join if they answer in time, and the virtual input stays without Web MIDI.","zh":"包裝 `navigator.requestMIDIAccess`，讓每個呼叫者都拿到虛擬的 access：真實的埠有及時回應就併進來，就算沒有 Web MIDI 或被拒絕，虛擬輸入也還在。"},"description":[{"en":"If the real request answers within `waitMs` (1200 ms by default), its ports are there from the start. If it answers later they hot-plug in as `statechange` events. If it is refused or missing (Safari, iPad), the virtual input is still there. A permission prompt nobody answers cannot hang the sketch: it gets the access after `waitMs` anyway.","zh":"真實請求在 `waitMs`（預設 1200 毫秒）內回應的話，它的埠一開始就在。晚一點回應，就以 `statechange` 事件熱插拔進來。被拒絕或不存在（Safari、iPad）時，虛擬輸入還是在。沒有人回答的權限詢問不會卡死草圖：`waitMs` 一到它就會拿到 access。"}],"params":[{"name":"real","type":"Function | null","en":"The original `requestMIDIAccess`, already bound (`navigator.requestMIDIAccess.bind(navigator)`), or `null` when there is none. The real request still happens, permission prompt and all.","zh":"原本的 `requestMIDIAccess`，要先綁好 `this`（`navigator.requestMIDIAccess.bind(navigator)`），沒有的話給 `null`。真實的請求仍然會進行，權限詢問也一樣。"},{"name":"make","type":"Function","en":"`(opts) => …` returning the object of [`createVirtualMIDIAccess()`](#createVirtualMIDIAccess). Required. Return the same one each time to share it.","zh":"`(opts) => …`，回傳 [`createVirtualMIDIAccess()`](#createVirtualMIDIAccess) 的物件。必填。每次回傳同一個就能共用。"},{"name":"waitMs","type":"Number","en":"How long to wait for the real request before answering anyway.","zh":"最多等真實請求多久，時間到就先回覆。"}],"returns":{"type":"Function","en":"A function `(opts?) => Promise<MIDIAccess>` to put where `navigator.requestMIDIAccess` was.","zh":"一個函式 `(opts?) => Promise<MIDIAccess>`，放到原本 `navigator.requestMIDIAccess` 的位置。"},"notes":[],"examples":[{"code":"import { createVirtualMIDIAccess, virtualRequestMIDIAccess } from '@openav/midi';\n\nconst virtual = createVirtualMIDIAccess({ name: 'Demo MIDI' });\nconst requestMIDIAccess = virtualRequestMIDIAccess({ real: null, make: () => virtual });\n\n// this is what an old sketch does, unchanged\nconst access = await requestMIDIAccess();\nconsole.log([...access.inputs.values()].map((p) => p.name));   // [ 'Demo MIDI' ]\nconsole.log(access === virtual.access, virtual.requests);      // true 1","caption":{"en":"Prints `['Demo MIDI']`, then `true 1`: the caller got the virtual access, and it was asked for once.","zh":"印出 `['Demo MIDI']`，再印出 `true 1`：呼叫者拿到的是虛擬的 access，而且只被要求過一次。"},"run":"module","lang":"js"},{"code":"import { createVirtualMIDIAccess, virtualRequestMIDIAccess } from '@openav/midi';\n\nconst virtual = createVirtualMIDIAccess();\nnavigator.requestMIDIAccess = virtualRequestMIDIAccess({\n  real: navigator.requestMIDIAccess ? navigator.requestMIDIAccess.bind(navigator) : null,\n  make: () => virtual,\n});\n// from now on every sketch on the page that asks for MIDI gets the virtual input too","caption":{"en":"The real wiring: put it before the sketch runs and its old MIDI code needs no change.","zh":"真正的接法：放在草圖執行之前，它舊的 MIDI 程式碼就不用改。"},"run":"static","lang":"js"}],"related":["createVirtualMIDIAccess","Midi"],"keywords":{"en":["shim","navigator.requestMIDIAccess","old sketch","wrap web midi","virtual input","safari"],"zh":["轉接","navigator.requestMIDIAccess","舊草圖","包裝 web midi","虛擬輸入","safari"]},"tbd":[],"claims":[]},
{"id":"withChannel","key":"withChannel","name":"withChannel","aliases":[],"kind":"function","package":"@openav/midi","category":"midi-io","documented":true,"signature":"withChannel(profile, ch)","import":"import { withChannel } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":133,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L133"},"summary":{"en":"Returns a copy of a profile with every control moved to another MIDI channel, for a unit that is set to a channel other than the profile's.","zh":"回傳一份設定檔的副本，所有控制項都改到另一個 MIDI 頻道，給「機器設定的頻道跟設定檔不同」的情況用。"},"description":[{"en":"Explicit LED channels (`led.ch`) stay as they are, since LED feedback may use a different channel from the control. This is what the `channel` option of [`createController()`](#createController) and the `channel` attribute of `<oav-controller>` call.","zh":"明確寫出的 LED 頻道（`led.ch`）會維持原樣，因為 LED 回饋用的頻道可能跟控制項不同。[`createController()`](#createController) 的 `channel` 選項和 `<oav-controller>` 的 `channel` 屬性，呼叫的就是它。"}],"params":[{"name":"profile","type":"Object","en":"A profile.","zh":"設定檔。"},{"name":"ch","type":"Number","en":"The channel, 1..16. A falsy value (`0`, `null`) returns the same profile object untouched.","zh":"頻道，1..16。給假值（`0`、`null`）就原封不動回傳同一個設定檔物件。"}],"returns":{"type":"Object","en":"A deep copy, validated, with `channel` and every control's `ch` set. The original is not changed.","zh":"深層複製的副本，已通過驗證，`channel` 和每個控制項的 `ch` 都改好了。原本的不會被改動。"},"notes":[],"examples":[{"code":"import { profileById } from '@openav/midi';\nimport { withChannel } from '@openav/midi/embed';\n\nconst lpd8 = profileById('lpd8');\nconst moved = withChannel(lpd8, 3);\n\nconsole.log(lpd8.controls[0].ch, '->', moved.controls[0].ch);          // 10 -> 3\nconsole.log(moved.controls.every((c) => c.ch === 3), moved.channel);   // true 3","caption":{"en":"Prints `10 -> 3`: the shipped profile keeps its channel, the copy has every control on channel 3.","zh":"印出 `10 -> 3`：內建的設定檔頻道不變，副本的每個控制項都在頻道 3。"},"run":"module","lang":"js"}],"related":["createController","OavController","normalizeProfile"],"keywords":{"en":["channel","change channel","midi channel override","copy profile"],"zh":["頻道","改頻道","覆寫頻道","複製設定檔"]},"tbd":[],"claims":[]},
{"id":"CANVAS_THEME","key":"CANVAS_THEME","name":"CANVAS_THEME","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"CANVAS_THEME = object, 16 keys","import":"import { CANVAS_THEME } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":43,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L43"},"summary":{"en":"The default look of `ControllerCanvas`: background, body, text, idle colours, `accent`, `glow` and fonts. Pass any subset as the `theme` option.","zh":"`ControllerCanvas` 的預設外觀：背景、機身、文字、待機顏色、`accent`、`glow` 和字型。把任何子集當作 `theme` 選項傳入即可。"},"description":[{"en":"Colours are CSS colour strings. `bg: null` lets the video or page underneath show through, and `accent: '#7ea6ff'` makes the panel monochrome-with-one-colour, which suits a branded video.","zh":"顏色都是 CSS 顏色字串。`bg: null` 讓底下的影片或頁面透出來，`accent: '#7ea6ff'` 則讓整個面板只用一個顏色，很適合有品牌色的影片。"}],"value":{"type":"Object","en":"A frozen object (you cannot change it; pass `theme` instead). Keys: `bg` (the area around the device; `null` leaves what is underneath), `body` (the faceplate; `null` uses the profile's `face.body`), `ink` (big text), `label` (control labels), `dim` (faceplate and section lettering), `idle` (a pad at rest), `button`, `cap` (knob caps), `well` (fader slots and strip wells), `screen`, `screenInk`, `screenDim`, `accent` (one colour for everything that moves; `null` keeps each control's own), `glow` (0 flat to 1 like the on-screen controller) and the fonts `font` and `mono`.","zh":"凍結的物件（不能修改，要用 `theme` 選項覆寫）。鍵有：`bg`（機器周圍的區域，`null` 表示保留底下原本的畫面）、`body`（面板，`null` 用設定檔的 `face.body`）、`ink`（大字）、`label`（控制項標籤）、`dim`（面板與區塊的文字）、`idle`（待機的打擊墊）、`button`、`cap`（旋鈕蓋）、`well`（推桿溝槽和觸控條底）、`screen`、`screenInk`、`screenDim`、`accent`（所有會動的東西用同一個顏色，`null` 保留各控制項自己的顏色）、`glow`（0 是平面，1 是跟螢幕控制器一樣的光暈），以及字型 `font` 和 `mono`。"},"params":[],"notes":[],"examples":[{"code":"import { CANVAS_THEME, ControllerCanvas } from '@openav/midi';\n\nconsole.log(CANVAS_THEME.bg, CANVAS_THEME.accent, CANVAS_THEME.glow);\n\n// a transparent, one-colour, flat panel\nconst panel = new ControllerCanvas({ profile: 'lpd8', theme: { bg: null, accent: '#ffb547', glow: 0 } });\nconsole.log(panel.theme.bg, panel.theme.accent, panel.theme.glow, panel.theme.ink);","caption":{"en":"Prints the defaults (`#060709 null 1`), then the panel's theme with your three keys replaced and everything else (such as `ink`) kept.","zh":"印出預設值（`#060709 null 1`），再印出面板的外觀：你指定的三個鍵被取代，其他（例如 `ink`）維持原樣。"},"run":"module","lang":"js"}],"related":["ControllerCanvas","controllerGeometry"],"keywords":{"en":["theme","colors","accent","glow","transparent background","branding"],"zh":["外觀","顏色","強調色","光暈","透明背景","品牌色"]},"tbd":[],"claims":[]},
{"id":"controlDetail","key":"controlDetail","name":"controlDetail","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"controlDetail(mc, id, st, info = {})","import":"import { controlDetail } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":160,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L160"},"summary":{"en":"Builds the `control` event payload for one control change: `id`, `value`, `raw`, `signal`, the parsed `message`, its `bytes` and where it came from.","zh":"為一次控制項變化組出 `control` 事件的內容：`id`、`value`、`raw`、`signal`、解析後的 `message`、`bytes`，以及它從哪裡來。"},"description":[{"en":"Always there: `profile` (the profile id), `id`, `type`, `label`, `value`, `raw`, `signal`, `message`, `bytes`, `source` and `time` (`performance.now()`, rounded). `value` is 0..1 (−1..1 for a bipolar wheel); a pad is its velocity while held and 0 on release; a button is 0 or 1; for keys it is that note's velocity. `raw` is the MIDI number (0..127, pitch bend 0..16383). `signal` is the name the rest of open-audiovisual uses (`midi/lpd8/k1`; for a key `midi/minilab3/n60`). `message` is the parsed MIDI (`{ type: 'cc', ch: 1, cc: 70, value: 53 }`) and `bytes` the raw bytes. `source` is `'ui'` (a finger, a mouse, `set()`), `'hardware'` (a real port, or bytes you `ingest()`) or `'mirror'`.","zh":"一定會有的欄位：`profile`（設定檔 id）、`id`、`type`、`label`、`value`、`raw`、`signal`、`message`、`bytes`、`source`、`time`（`performance.now()`，取整數）。`value` 是 0..1（雙向彎音輪是 −1..1），打擊墊在按著時是力度、放開是 0，按鈕是 0 或 1，琴鍵則是那個音的力度。`raw` 是 MIDI 的號碼（0..127，彎音是 0..16383）。`signal` 是 open-audiovisual 其他部分用的名稱（`midi/lpd8/k1`，琴鍵是 `midi/minilab3/n60`）。`message` 是解析後的 MIDI（`{ type: 'cc', ch: 1, cc: 70, value: 53 }`），`bytes` 是原始位元組。`source` 是 `'ui'`（手指、滑鼠、`set()`）、`'hardware'`（真實的埠，或你 `ingest()` 的位元組）或 `'mirror'`。"},{"en":"Sometimes there: `note` and `velocity` (note messages, `velocity` 0 for a release), `delta` (a relative encoder: the signed steps), `held` (a pad or button), and `pressure` (a pad, when it is above 0). The `control`, `noteon` and `noteoff` events carry it as their `detail`.","zh":"有時才有的欄位：`note` 和 `velocity`（音符訊息，放開時 `velocity` 為 0）、`delta`（相對旋鈕：有正負號的步數）、`held`（打擊墊或按鈕）、`pressure`（打擊墊，大於 0 時才有）。`control`、`noteon`、`noteoff` 事件把它當作 `detail`。"}],"params":[{"name":"mc","type":"MidiController","en":"The controller the change happened on.","zh":"發生變化的控制器。"},{"name":"id","type":"String","en":"The control id.","zh":"控制項 id。"},{"name":"st","type":"Object","en":"Its state after the change (`mc.get(id)`).","zh":"變化之後的狀態（`mc.get(id)`）。"},{"name":"info","type":"Object","en":"What `onChange` gave: `{ source, ev, note, bytes }`.","zh":"`onChange` 給的內容：`{ source, ev, note, bytes }`。"}],"returns":{"type":"Object","en":"The payload, plain JSON that is safe to `postMessage`.","zh":"事件內容，是可以安全 `postMessage` 的純 JSON。"},"notes":[],"examples":[{"code":"import { MidiController, profileById } from '@openav/midi';\nimport { controlDetail, describeDetail } from '@openav/midi/embed';\n\nconst mc = new MidiController(profileById('minilab3'));\nmc.onChange((id, st, info) => {\n  if (!id) return;\n  const detail = controlDetail(mc, id, st, info);\n  console.log(describeDetail(detail));\n  console.log(detail.bytes, detail.source, detail.signal);\n});\n\nmc.setValue('knob1', 0.42);","caption":{"en":"Prints the readout line `midi/minilab3/knob1 = 0.42 ← CC 74 = 53 · ch 1`, then the bytes `[176, 74, 53]`, `ui` and the signal name.","zh":"印出讀數列的文字 `midi/minilab3/knob1 = 0.42 ← CC 74 = 53 · ch 1`，再印出位元組 `[176, 74, 53]`、`ui` 和訊號名稱。"},"run":"module","lang":"js"}],"related":["describeDetail","describeMessage","OavController","ControllerHost","toParent"],"keywords":{"en":["control event","event payload","detail","value raw signal","bytes","source"],"zh":["control 事件","事件內容","detail","value raw signal","位元組","來源"]},"tbd":[],"claims":[]},
{"id":"ControllerCanvas","key":"ControllerCanvas","name":"ControllerCanvas","aliases":[],"kind":"class","package":"@openav/midi","category":"controllers","documented":true,"signature":"new ControllerCanvas({ profile = 'arturia-minilab3', signals = null, controller = null, mapping = null, theme = {}, notes = 'auto', screen = true, padding = 0.05, now = defaultNow } = {})","import":"import { ControllerCanvas } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":257,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L257"},"summary":{"en":"Draws a controller's faceplate into any 2D canvas context, every frame, at any size: the bottom of a performance video, an overlay, a thumbnail.","zh":"把控制器的面板畫進任何 2D 畫布，每個畫面都畫，任何大小都行：演出影片的下半部、疊圖、縮圖。"},"description":[{"en":"The on-screen controller is DOM: perfect to play, impossible to record, because a page cannot capture its own DOM into a video. This draws the same faceplate, every section where it sits on the device and in the same colours, straight into a 2D context. It is vector and drawn at the output size, so a 4K frame is as sharp as a 1080 one, and it costs a few hundred path calls a frame. `@openav/record`'s `Compositor#setPanel` takes one.","zh":"螢幕上的控制器是 DOM：很適合彈，卻沒辦法錄，因為網頁不能把自己的 DOM 錄進影片。這個類別把同一個面板直接畫進 2D 畫布，每個區塊都在機器上原本的位置，用同樣的顏色。它是向量，直接以輸出的大小繪製，所以 4K 畫面跟 1080 一樣清楚，每個畫面約花幾百次路徑呼叫。`@openav/record` 的 `Compositor#setPanel` 就是接收它。"},{"en":"Call `panel.draw(ctx, { x, y, w, h })` every frame, or `panel.renderTo(canvas)` to fill a canvas of its own. What a video needs that a screen does not: a touch has to stay visible for a few frames (30 fps is 33 ms; a staccato note can be shorter), so released keys and pads fade out instead of switching off, the control a hand just moved glows, and the keyboard follows the device's octave buttons. `panel.aspect` is the faceplate's width ÷ height, for reserving an area of that shape. With neither `signals` nor `controller` the panel just stays at rest.","zh":"每個畫面呼叫 `panel.draw(ctx, { x, y, w, h })`，或用 `panel.renderTo(canvas)` 畫滿一張自己的畫布。影片需要而螢幕不需要的東西：一次觸碰要在畫面上留幾格（30 fps 是 33 毫秒，很短的斷音可能比這還短），所以放開的琴鍵和打擊墊會淡出而不是直接熄掉，剛被手動過的控制項會發光，鍵盤也會跟著機器的八度按鈕移動。`panel.aspect` 是面板的寬除以高，可用來預留相同形狀的區域。`signals` 和 `controller` 都沒給時，面板就只是靜止不動。"}],"params":[{"name":"profile","type":"String | Object","en":"Profile id, short name or object. An unknown id throws `ControllerCanvas: unknown profile \"x\"`.","zh":"設定檔 id、短名稱或物件。不認得的 id 會丟出 `ControllerCanvas: unknown profile \"x\"`。"},{"name":"signals","type":"Signals | null","en":"Follow the generic MIDI names on this bus (`midi/ch/<ch>/…`, `midi/note/on|off`, `midi/cc/<n>`, `midi/bend`): live hardware, the on-screen controller, the QWERTY piano, phones and a recorded take playing back all move the panel. Each message counts once, though hardware publishes it under two names.","zh":"跟隨這條匯流排上的通用 MIDI 名稱（`midi/ch/<ch>/…`、`midi/note/on|off`、`midi/cc/<n>`、`midi/bend`）：實際的硬體、螢幕控制器、電腦鍵盤鋼琴、手機，以及正在回放的錄音，都會讓面板動起來。硬體的一則訊息雖然用兩個名稱發佈，也只會算一次。"},{"name":"controller","type":"MidiController | null","en":"Follow this existing controller instead of the bus: exactly that device, with its learned mapping. A recorded take does not reach it. When given, `signals` is ignored.","zh":"改為跟隨這個現有的控制器，不聽匯流排：正好就是那一台，連同它學到的對應。錄音回放不會傳到它。有給的話會忽略 `signals`。"},{"name":"mapping","type":"Object | null","en":"Learned bindings to apply to the panel's own controller: the `exportMapping()` of the live device.","zh":"要套用到面板自己的控制器上的學習結果：就是實機控制器的 `exportMapping()`。"},{"name":"theme","type":"Object","en":"Any subset of [`CANVAS_THEME`](#CANVAS_THEME): colours, `glow`, fonts.","zh":"[`CANVAS_THEME`](#CANVAS_THEME) 的任何子集：顏色、`glow`、字型。"},{"name":"notes","type":"Boolean | 'auto'","en":"Write the held notes large under the faceplate. `'auto'` does it when the area has room, `true` makes room, `false` never.","zh":"把按著的音大大地寫在面板下面。`'auto'` 是區域有空間時才寫，`true` 會擠出空間，`false` 永遠不寫。"},{"name":"screen","type":"Boolean","en":"The device's own screen (MiniLab 3, KeyStep) shows the last touch, as the hardware's does: `Knob 3 · 87`, `C4 · 100`.","zh":"機器自己的螢幕（MiniLab 3、KeyStep）顯示最後一次碰了什麼，跟實機一樣：`Knob 3 · 87`、`C4 · 100`。"},{"name":"padding","type":"Number","en":"Space around the faceplate, as a share of the area's short side.","zh":"面板周圍的留白，以區域短邊的比例表示。"},{"name":"now","type":"Function","en":"The clock in milliseconds that the fades use; default `performance.now()`. The `now` you pass to `draw()` must be on the same clock.","zh":"淡出效果用的時鐘（毫秒），預設是 `performance.now()`。傳給 `draw()` 的 `now` 要用同一個時鐘。"}],"notes":[{"en":"It is a silent model: it publishes nothing back to the bus. `panel.dispose()` stops listening to the bus or controller (a controller you passed in is left as it was).","zh":"它是個安靜的模型：不會往匯流排發佈任何東西。`panel.dispose()` 會停止聽匯流排或控制器（你傳進來的控制器不會被動到）。"}],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { ControllerCanvas, genericSignals, publish } from '@openav/midi';\n\nconst signals = new Signals();\nconst panel = new ControllerCanvas({ profile: 'minilab3', signals });\n\n// a chord, a pad and a knob, published as the hardware would\nconst play = (ev) => publish(signals, genericSignals(ev));\nplay({ type: 'noteon', ch: 1, note: 60, vel: 100 });\nplay({ type: 'noteon', ch: 1, note: 64, vel: 100 });\nplay({ type: 'noteon', ch: 10, note: 36, vel: 127 });\nplay({ type: 'cc', ch: 1, cc: 74, value: 100 });\n\npanel.renderTo(document.getElementById('c'));   // fills the 400 x 300 canvas","caption":{"en":"The Arturia MiniLab 3 is drawn into the canvas with C4 and E4 lit, pad 1 lit, and knob 1 turned up. The device screen reads `Knob 1 · 100`.","zh":"Arturia MiniLab 3 被畫進畫布，C4 和 E4 亮著，pad 1 亮著，旋鈕 1 轉大了。機器的螢幕寫著 `Knob 1 · 100`。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { ControllerCanvas, genericSignals, publish } from '@openav/midi';\n\nconst signals = new Signals();\nconst panel = new ControllerCanvas({ profile: 'lpd8', signals, theme: { accent: '#7ea6ff' } });\nconst ctx = document.getElementById('c').getContext('2d');\nconst send = (ev) => publish(signals, genericSignals(ev));\n\nlet frame = 0;\nfunction tick() {\n  const pad = 36 + Math.floor(frame / 20) % 8;\n  if (frame % 20 === 0) send({ type: 'noteon', ch: 10, note: pad, vel: 110 });   // hit a pad\n  if (frame % 20 === 8) send({ type: 'noteoff', ch: 10, note: pad });             // let go: it fades out\n  panel.draw(ctx, { x: 0, y: 0, w: 400, h: 300 });\n  if (++frame < 120) requestAnimationFrame(tick);\n}\ntick();","caption":{"en":"A different pad of the LPD8 lights every third of a second and then fades, all in one accent colour, as it would in a recorded video.","zh":"LPD8 的打擊墊每三分之一秒換一個亮起，然後淡出，全部是同一個強調色，就像錄進影片時的樣子。"},"run":"module","lang":"js"}],"related":["controllerGeometry","CANVAS_THEME","followMidiSignals","ControllerView","eventOfSignal","MidiController"],"keywords":{"en":["draw controller","canvas","video overlay","record controller","compositor","picture in picture","faceplate on canvas","thumbnail"],"zh":["畫控制器","畫布","影片疊圖","錄製控制器","合成","子母畫面","畫布上的面板","縮圖"]},"tbd":[],"claims":[],"members":[{"name":"profile","member":"getter","signature":"","doc":"The profile being drawn (normalized).","line":297},{"name":"aspect","member":"getter","signature":"","doc":"Width ÷ height of the faceplate — layouts reserve an area of this shape (a picture-in-picture corner).","line":299},{"name":"geometry","member":"method","signature":"rect","doc":"Geometry for a rect (cached while the rect stays the same).","line":335},{"name":"reset","member":"method","signature":"","doc":"Release everything (stuck-key insurance between takes).","line":342},{"name":"draw","member":"method","signature":"ctx, rect, now = this.now()","doc":"Draw into `rect` of a 2D context.","line":354},{"name":"renderTo","member":"method","signature":"canvas, now = this.now()","doc":"Draw into a canvas of its own (an overlay, a thumbnail): fills the whole canvas.","line":385},{"name":"dispose","member":"method","signature":"","doc":"Stop listening.","line":392}]},
{"id":"controllerGeometry","key":"controllerGeometry","name":"controllerGeometry","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"controllerGeometry(profile, rect, { padding = 0.05, notes = 'auto' } = {})","import":"import { controllerGeometry } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":81,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L81"},"summary":{"en":"Computes where everything goes when a profile's faceplate is drawn into a pixel rectangle: the face, every section, every control cell, and the notes strip.","zh":"算出設定檔的面板畫進某個像素矩形時，每樣東西的位置：面板、每個區塊、每個控制項的格子，以及音名條。"},"description":[{"en":"The faceplate keeps its own proportions, centred in the rect. The sections are laid out the way the on-screen controller's CSS grid does (row or column flow, spans), so the canvas drawing and the DOM agree. It is pure, so it is the part to test, and the part to use when you want to draw something of your own on top of a control.","zh":"面板維持自己的比例，置中在矩形裡。區塊的排法跟螢幕控制器的 CSS grid 一致（橫向或直向排列、跨格），所以畫布上的繪製和 DOM 版本一致。它是純函式，適合拿來測試，也適合在你想在某個控制項上疊自己畫的東西時使用。"}],"params":[{"name":"profile","type":"Object","en":"A normalized profile (`MidiController#profile`).","zh":"整理過的設定檔（`MidiController#profile`）。"},{"name":"rect","type":"{ x, y, w, h }","en":"The area, in pixels.","zh":"區域，單位是像素。"},{"name":"padding","type":"Number","en":"Space around the faceplate, as a share of the rect's short side (0..1).","zh":"面板周圍的留白，以矩形短邊的比例表示（0..1）。"},{"name":"notes","type":"Boolean | 'auto'","en":"Reserve a strip under the faceplate for the held notes: `true` always, `false` never, `'auto'` when the rect has room.","zh":"在面板下面預留一條放按著的音的位置：`true` 一定有，`false` 一定沒有，`'auto'` 是矩形有空間時才有。"}],"returns":{"type":"{ u, face, notes, sections, controls, deco }","en":"`u` is the pixel size of one face unit. `face` is `{ x, y, w, h }`. `notes` is the strip or `null`. `sections` is a list with `id`, `label`, the box and its `grid`. `controls` is a `Map` from control id to `{ id, type, section, x, y, w, h }`. `deco` lists the decorations (screen, logo, lettering) in pixels.","zh":"`u` 是一個面板單位的像素大小。`face` 是 `{ x, y, w, h }`。`notes` 是那一條或 `null`。`sections` 是帶有 `id`、`label`、外框和 `grid` 的清單。`controls` 是從控制項 id 對到 `{ id, type, section, x, y, w, h }` 的 `Map`。`deco` 列出以像素表示的裝飾（螢幕、標誌、文字）。"},"notes":[],"examples":[{"code":"import { controllerGeometry, normalizeProfile, profileById } from '@openav/midi';\n\nconst profile = normalizeProfile(profileById('lpd8'));\nconst g = controllerGeometry(profile, { x: 0, y: 0, w: 400, h: 300 });\n\nconsole.log(g.face);                    // the faceplate, centred, in pixels\nconsole.log(g.controls.get('pad1'));    // where pad 1 is\nconsole.log(g.controls.size, 'controls', g.notes ? 'with a notes strip' : 'without a notes strip');","caption":{"en":"Prints the faceplate rectangle (370 wide, centred), pad 1's cell in pixels, then `16 controls with a notes strip`.","zh":"印出面板的矩形（寬 370，置中）、pad 1 的格子位置（像素），再印出 `16 controls with a notes strip`。"},"run":"module","lang":"js"}],"related":["ControllerCanvas","keyboardGeometry","normalizeProfile"],"keywords":{"en":["layout","geometry","rect","cell positions","where is the knob","faceplate size"],"zh":["版面","幾何","矩形","格子位置","旋鈕在哪","面板大小"]},"tbd":[],"claims":[]},
{"id":"ControllerHost","key":"ControllerHost","name":"ControllerHost","aliases":[],"kind":"class","package":"@openav/midi","category":"controllers","documented":true,"signature":"new ControllerHost(profile = 'arturia-minilab3', { profiles = PROFILES, signals = null, sink = null, channel = null, follow = false, midi = null, requestAccess = null, filterOut = 'IAC', storage = undefined, telemetry = true, embedKind = 'headless' } = {})","import":"import { ControllerHost } from '@openav/midi';","source":{"file":"packages/midi/index.js","line":277,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/index.js#L277"},"summary":{"en":"The object `createController()` returns: one controller with its events, its playable faceplate, its hardware connection and its MIDI output.","zh":"`createController()` 回傳的物件：一台控制器，包含事件、可以彈的面板、硬體連線和 MIDI 輸出。"},"description":[{"en":"Events, with `ctl.on(name, fn)` (returns an unsubscribe function): `control` (every knob, pad, key or button change), `noteon` and `noteoff` (the same payload, for pads, keys and note buttons), `connect` and `disconnect` (`{ port, profile }`: a hardware port started or stopped driving it), `status`, `profilechange` (`{ profile, name }`) and `'*'`, which gets `(name, detail)` for all of them. The `control` payload is the one described under [`controlDetail()`](#controlDetail). `ready` and `error` exist only on the `<oav-controller>` element.","zh":"事件用 `ctl.on(name, fn)` 訂閱（回傳取消訂閱的函式）：`control`（任何旋鈕、打擊墊、琴鍵、按鈕的變化）、`noteon` 和 `noteoff`（內容相同，給打擊墊、琴鍵和音符按鈕）、`connect` 和 `disconnect`（`{ port, profile }`：某個硬體埠開始或停止驅動它）、`status`、`profilechange`（`{ profile, name }`），以及 `'*'`，所有事件都會以 `(name, detail)` 通知它。`control` 的內容見 [`controlDetail()`](#controlDetail)。`ready` 和 `error` 只有 `<oav-controller>` 元素才有。"},{"en":"`ctl.status` is `{ hardware, input, output, outputState }`. `hardware` is `off`, `asking`, `waiting` (listening, but the device is not plugged in), `connected`, `unavailable` (the browser has no Web MIDI) or `denied` (permission refused). `outputState` is `off`, `asking`, `open`, `missing` (no port matches), `unavailable` or `denied`. `ctl.profileId`, `ctl.controller` and `ctl.profile` say which device is on screen; `ctl.controllers`, `ctl.view`, `ctl.midi` and `ctl.output` are the objects underneath.","zh":"`ctl.status` 是 `{ hardware, input, output, outputState }`。`hardware` 可能是 `off`、`asking`、`waiting`（在聽，但機器沒接上）、`connected`、`unavailable`（瀏覽器沒有 Web MIDI）或 `denied`（權限被拒絕）。`outputState` 可能是 `off`、`asking`、`open`、`missing`（沒有符合的埠）、`unavailable` 或 `denied`。`ctl.profileId`、`ctl.controller`、`ctl.profile` 告訴你螢幕上是哪台機器，`ctl.controllers`、`ctl.view`、`ctl.midi`、`ctl.output` 則是底下的物件。"},{"en":"What a hand plays on the screen leaves through the output port as real MIDI; moves of your real hardware are not forwarded (your software already hears the device), and input with the same name as the chosen output (the loopMIDI echo) is muted.","zh":"螢幕上的手彈出來的，會從輸出埠以真正的 MIDI 送出。真實硬體的動作不會被轉送（你的軟體本來就聽得到那台機器），名稱跟所選輸出埠相同的輸入（loopMIDI 的回音）會被靜音。"}],"params":[{"name":"profile","type":"String | Object","en":"Profile id, short name or object.","zh":"設定檔 id、短名稱或物件。"},{"name":"profiles","type":"Object[]","en":"The profiles a picker or `follow` may switch to. Default: every shipped one.","zh":"選單或 `follow` 可以切換到的設定檔。預設是所有內建的。"},{"name":"signals","type":"Signals | null","en":"Also publish `midi/<short>/…` into a show.","zh":"同時把 `midi/<short>/…` 發佈進演出。"},{"name":"sink","type":"Function | null","en":"Publish through this function instead (a relay), as in [`MidiController`](#MidiController).","zh":"改由這個函式發佈（中繼），用法同 [`MidiController`](#MidiController)。"},{"name":"channel","type":"Number | null","en":"Move every control to this MIDI channel (your unit is set to another one). See [`withChannel()`](#withChannel).","zh":"把所有控制項移到這個 MIDI 頻道（你的機器設定在別的頻道時）。見 [`withChannel()`](#withChannel)。"},{"name":"follow","type":"Boolean","en":"When a different known device is plugged in, show that one.","zh":"接上另一台認得的機器時，就改顯示那一台。"},{"name":"midi","type":"Midi | null","en":"Use this engine (a show's) instead of the page-shared one.","zh":"改用這個引擎（演出的），不用頁面共用的。"},{"name":"requestAccess","type":"Function | null","en":"Where the `MIDIAccess` comes from, as in [`Midi`](#Midi). Giving it makes the controller use its own engine.","zh":"`MIDIAccess` 從哪裡來，用法同 [`Midi`](#Midi)。給了它，控制器就會用自己的引擎。"},{"name":"filterOut","type":"String | RegExp","en":"The input-port filter of [`Midi`](#Midi). Anything but the default `'IAC'` also gives the controller its own engine.","zh":"[`Midi`](#Midi) 的輸入埠過濾。任何不是預設 `'IAC'` 的值，也會讓控制器用自己的引擎。"},{"name":"storage","type":"Storage | null","en":"Where learned mappings are kept, as in [`MidiControllers`](#MidiControllers). Default: `localStorage` when available.","zh":"學到的對應存在哪裡，用法同 [`MidiControllers`](#MidiControllers)。預設是有 `localStorage` 就用它。"},{"name":"telemetry","type":"Boolean","en":"`false` sends no anonymous page-load hit.","zh":"`false` 就不送匿名的頁面載入資料。"},{"name":"embedKind","type":"String","en":"Which kind of embed this is for the hit: `'headless'` (default), `'element'` or `'iframe'`.","zh":"這次嵌入屬於哪一種，供那筆資料用：`'headless'`（預設）、`'element'` 或 `'iframe'`。"}],"needs":{"en":"`connectHardware()`, `setOutput()` and `requestMidi()` are the only calls that ask the browser for MIDI. Chrome, Edge and Firefox show a permission prompt, so call them from a click when you can. Without Web MIDI (Safari, iPad) the controller still plays and fires events, and `status` says `unavailable`.","zh":"只有 `connectHardware()`、`setOutput()`、`requestMidi()` 會向瀏覽器要 MIDI。Chrome、Edge、Firefox 會跳出權限詢問，所以可以的話請在點擊裡呼叫。沒有 Web MIDI（Safari、iPad）時，控制器仍然能彈、能觸發事件，`status` 會顯示 `unavailable`。"},"notes":[],"examples":[{"code":"import { ControllerHost } from '@openav/midi';\n\n// same as createController('lpd8'); the class is exported for instanceof and subclassing\nconst ctl = new ControllerHost('lpd8', { channel: 3 });\nconsole.log(ctl instanceof ControllerHost, ctl.profileId, ctl.status.hardware);\n\nconsole.log(ctl.set('k1', 1));                      // [178, 70, 127]: channel 3 is status byte 0xb2\nawait ctl.setProfile('nano2');                       // show another device\nconsole.log(ctl.profileId, ctl.profile.controls.every((c) => c.ch === 3));","caption":{"en":"Prints `true akai-lpd8 off`, then the bytes of knob 1 on channel 3, then the new device with the channel override kept.","zh":"印出 `true akai-lpd8 off`，再印出旋鈕 1 在頻道 3 的位元組，最後是換過的新機器，頻道覆寫仍然有效。"},"run":"module","lang":"js"}],"related":["createController","MidiController","ControllerView","OavController","controlDetail"],"keywords":{"en":["controller host","events","status","connect hardware","set output","learn","mount","ingest"],"zh":["控制器主機","事件","狀態","連接硬體","設定輸出","學習","掛載","餵入訊息"]},"tbd":[],"claims":[],"members":[{"name":"controller","member":"getter","signature":"","doc":"The live model (MidiController) of the device on screen.","line":304},{"name":"profile","member":"getter","signature":"","doc":"Its normalized profile (controls, faceplate, sources).","line":306},{"name":"on","member":"method","signature":"name, fn","doc":"Subscribe: 'control' | 'noteon' | 'noteoff' | 'connect' | 'disconnect' | 'status' | 'profilechange' | '*' (name, detail).","line":310},{"name":"set","member":"method","signature":"id, value, { note = null, quiet = false } = {}","doc":"Move a control like a hand would (see embed.js setControl).","line":333},{"name":"press","member":"method","signature":"id, velocity = 1, note = null","doc":"","line":334},{"name":"release","member":"method","signature":"id, note = null","doc":"","line":335},{"name":"get","member":"method","signature":"id","doc":"Current value of one control (0..1, −1..1 bipolar), or null.","line":337},{"name":"values","member":"method","signature":"","doc":"{ id: value } for every control.","line":339},{"name":"reset","member":"method","signature":"{ quiet = false } = {}","doc":"Let go of everything and bring every control home (sends what a hand would, unless quiet).","line":341},{"name":"releaseAll","member":"method","signature":"","doc":"Let go of every held pad and key (blur, page hide): the DAW gets its note-offs.","line":351},{"name":"ingest","member":"method","signature":"bytes, { source = 'hardware' } = {}","doc":"MIDI arriving from anywhere (a WebSocket, another page, a port you manage): the faceplate follows.","line":354},{"name":"send","member":"method","signature":"bytes","doc":"Raw bytes out of the MIDI output port (if one is open).","line":360},{"name":"mount","member":"method","signature":"el, { layout = 'auto' } = {}","doc":"Draw the playable faceplate into `el` (it fills el and follows its size).","line":367},{"name":"setLayout","member":"method","signature":"layout","doc":"","line":373},{"name":"learn","member":"method","signature":"on = !this.learning","doc":"Learn mode: tap a control on screen, then move the one on your hardware (or ingest() it).","line":375},{"name":"setProfile","member":"method","signature":"ref","doc":"Show another device: id, short name, URL, or profile object.","line":383},{"name":"connectHardware","member":"method","signature":"port = null","doc":"Follow the real device: its port drives the on-screen one (asks for Web MIDI).","line":409},{"name":"disconnectHardware","member":"method","signature":"","doc":"","line":421},{"name":"requestMidi","member":"method","signature":"","doc":"Ask for Web MIDI without connecting anything (an output picker needs the port list).","line":453},{"name":"outputs","member":"method","signature":"","doc":"Output port names, once Web MIDI is on.","line":455},{"name":"setOutput","member":"method","signature":"query","doc":"Send what is played here to a MIDI output: a name, a substring ('IAC', 'loopMIDI') or '/regex/'.","line":457},{"name":"dispose","member":"method","signature":"","doc":"Remove everything this controller added: listeners, the view, its share of the MIDI engine.","line":505}]},
{"id":"ControllerView","key":"ControllerView","name":"ControllerView","aliases":[],"kind":"class","package":"@openav/midi","category":"controllers","documented":true,"signature":"new ControllerView(container, controller, { mode = 'auto', minTouch = 38 } = {})","import":"import { ControllerView } from '@openav/midi';","source":{"file":"packages/midi/view.js","line":177,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/view.js#L177"},"summary":{"en":"The playable on-screen faceplate of a controller: knobs, faders, pads and keys you drive with a mouse, a finger or the keyboard, repainted from the controller's state.","zh":"控制器在螢幕上可以彈的面板：旋鈕、推桿、打擊墊、琴鍵，可以用滑鼠、手指或鍵盤操作，畫面隨著控制器的狀態重畫。"},"description":[{"en":"Two shapes, chosen from the box it lives in. `face`: the device as it looks, sections where they sit on the real faceplate, scaled to fit (desktop, iPad, a phone held sideways). `stack`: the same controls re-gridded top to bottom for a tall box (a phone held upright), as the profile's `portrait` says. Nothing scrolls and nothing shrinks below a thumb. Square grid devices (Launchpad) stay `face`. `mode` fixes one; `'auto'` follows the box.","zh":"兩種形狀，依它所在的盒子大小決定。`face`：機器原本的樣子，各區塊在真實面板上的位置，縮放到剛好放進去（桌機、iPad、橫拿的手機）。`stack`：同樣的控制項，照設定檔的 `portrait` 重新由上到下排列，給高的盒子用（直拿的手機）。不會捲動，也不會縮到比手指小。方形格子的機器（Launchpad）一律維持 `face`。`mode` 可以固定其中一種，`'auto'` 則跟著盒子走。"},{"en":"Every control repaints from the controller's state, so a finger here and a knob on the desk look the same, and a move from hardware gets a brief glow ring so you can see your hand arrive. Touch: drag a knob (right or up raises it), drag or click a fader or strip, strike a pad (higher on the pad is softer, lower is harder), hold a button, play the keys (multi-touch works). The mouse wheel turns knobs and faders.","zh":"每個控制項都從控制器的狀態重畫，所以螢幕上的手指和桌上的旋鈕看起來一樣，硬體動作則會有一圈短暫的光環，讓你看見自己的手「抵達」螢幕。操作：拖曳旋鈕（往右或往上是增加）、拖曳或點推桿與觸控條、敲打擊墊（敲在上半部較輕，下半部較重）、按住按鈕、彈琴鍵（支援多點觸控）。滑鼠滾輪可以轉旋鈕和推桿。"},{"en":"Keyboard: Tab reaches every control except the piano keys; the arrow keys, Page Up/Down, Home and End move knobs, faders and strips (Shift for fine steps); Space or Enter strikes pads and buttons, held while the key is held. Learn mode (`setLearning(true)`) turns every tap into \"bind this control\", and `view.onLearnTap = (id) => …` tells you which.","zh":"鍵盤：Tab 可以走到每個控制項（琴鍵除外），方向鍵、Page Up/Down、Home、End 移動旋鈕、推桿和觸控條（加 Shift 是微調），空白鍵或 Enter 敲打擊墊和按鈕，按著鍵就一直按著。學習模式（`setLearning(true)`）會讓每一次點擊都變成「綁定這個控制項」，`view.onLearnTap = (id) => …` 會告訴你是哪一個。"}],"params":[{"name":"container","type":"HTMLElement","en":"The element to fill. It needs a size: the view is 100% wide and tall, so give it a height. It can be inside a shadow root: the view's CSS goes into that root, not the page.","zh":"要填滿的元素。它要有大小：畫面是 100% 寬、100% 高，所以要給高度。可以放在 shadow root 裡，畫面的 CSS 會放進那個 root，不會碰到頁面。"},{"name":"controller","type":"MidiController","en":"The model to show and drive.","zh":"要顯示並驅動的模型。"},{"name":"mode","type":"String","en":"`'auto'`, `'face'` or `'stack'` (below).","zh":"`'auto'`、`'face'` 或 `'stack'`（見下）。"},{"name":"minTouch","type":"Number","en":"Pixels a control needs before `face` gives way to `stack`.","zh":"控制項至少要有幾個像素大，`face` 才不會被換成 `stack`。"}],"needs":{"en":"A browser DOM and a container with a height. `createController(…).mount(el)` is the usual way to get one.","zh":"要有瀏覽器的 DOM，而且容器要有高度。平常用 `createController(…).mount(el)` 取得它。"},"notes":[],"examples":[{"code":"import { ControllerView, MidiController, profileById } from '@openav/midi';\n\nconst app = document.getElementById('app');\napp.style.height = '240px';\n\nconst lpd8 = new MidiController(profileById('lpd8'));\nconst view = new ControllerView(app, lpd8);   // fills #app\n\nlpd8.press('pad1');                            // pad 1 lights up\nsetTimeout(() => lpd8.release('pad1'), 600);","caption":{"en":"The AKAI LPD8 appears in the page and pad 1 lights for a moment. Try dragging a knob or tapping the pads.","zh":"頁面上出現 AKAI LPD8，pad 1 亮了一下。可以試著拖曳旋鈕或敲打擊墊。"},"run":"module","lang":"js"},{"code":"import { ControllerView, MidiController, profileById } from '@openav/midi';\n\nconst app = document.getElementById('app');\napp.style.height = '360px';\napp.style.width = '220px';                    // a tall, narrow box\n\nconst view = new ControllerView(app, new MidiController(profileById('lpd8')));\nconsole.log(view.mode);                        // 'stack': re-gridded for a tall box\nview.setMode('face');\nconsole.log(view.mode);                        // 'face': the device as it looks","caption":{"en":"Prints `stack`, then `face` after forcing the layout: the same controls in the two shapes.","zh":"印出 `stack`，強制指定版面之後印出 `face`：同樣的控制項，兩種形狀。"},"run":"module","lang":"js"}],"related":["createController","MidiController","ControllerCanvas","mountMidiPanel","OavController","VIEW_CSS"],"keywords":{"en":["faceplate","on-screen controller","view","touch","knobs and pads","face stack layout","accessibility","learn"],"zh":["面板","螢幕控制器","畫面","觸控","旋鈕與打擊墊","版面","無障礙","學習"]},"tbd":[],"claims":[],"members":[{"name":"setController","member":"method","signature":"c","doc":"","line":196},{"name":"setLearning","member":"method","signature":"on","doc":"","line":206},{"name":"setMode","member":"method","signature":"mode = 'auto'","doc":"'auto' (choose from the box) | 'face' | 'stack'.","line":208},{"name":"dispose","member":"method","signature":"","doc":"","line":462}]},
{"id":"createController","key":"createController","name":"createController","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"createController(profile = 'arturia-minilab3', o = {})","import":"import { createController } from '@openav/midi';","source":{"file":"packages/midi/index.js","line":275,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/index.js#L275"},"summary":{"en":"Makes one MIDI controller you can play, listen to, and point at hardware or a DAW, with no element and no framework: `createController('akai-lpd8')`.","zh":"做出一台可以彈、可以聽、可以接硬體或 DAW 的 MIDI 控制器，不需要元素也不需要框架：`createController('akai-lpd8')`。"},"description":[{"en":"Four lines are enough: `const ctl = createController('akai-lpd8')`, `ctl.mount(el)` for the playable faceplate (optional, it works headless too), `ctl.on('control', fn)` to hear every knob, pad and key whether it came from the screen or the hardware, and `ctl.connectHardware()` to follow the real device. `ctl.set(id, value)` moves a control the way a hand would.","zh":"四行就夠：`const ctl = createController('akai-lpd8')`，`ctl.mount(el)` 畫出可以彈的面板（可有可無，沒有畫面也能用），`ctl.on('control', fn)` 聽每一個旋鈕、打擊墊、琴鍵，不論來自螢幕還是硬體，`ctl.connectHardware()` 跟隨真實的機器。`ctl.set(id, value)` 則像一隻手一樣去動某個控制項。"},{"en":"Controllers on one page share one [`Midi`](#Midi) engine, so the browser asks for MIDI permission once. Nothing asks until you call `connectHardware()`, `setOutput()` or `requestMidi()`. Pass `signals` to publish `midi/<short>/…` into a show.","zh":"同一個頁面上的控制器共用一個 [`Midi`](#Midi) 引擎，所以瀏覽器只會詢問一次 MIDI 權限。在你呼叫 `connectHardware()`、`setOutput()` 或 `requestMidi()` 之前，什麼都不會詢問。傳入 `signals` 就能把 `midi/<short>/…` 發佈進演出。"}],"params":[{"name":"profile","type":"String | Object","en":"A profile id (`'akai-lpd8'`), a short name (`'lpd8'`) or a profile object. For a URL, load it first with [`loadProfile()`](#loadProfile). An unknown id throws `unknown MIDI profile \"x\" (known: …)`.","zh":"設定檔 id（`'akai-lpd8'`）、短名稱（`'lpd8'`）或設定檔物件。要用網址的話，先用 [`loadProfile()`](#loadProfile) 載入。不認得的 id 會丟出 `unknown MIDI profile \"x\" (known: …)`。"},{"name":"o","type":"Object","en":"Options, the same as the second argument of [`ControllerHost`](#ControllerHost): `profiles`, `signals`, `sink`, `channel`, `follow`, `midi`, `requestAccess`, `filterOut`, `storage`, `telemetry`, `embedKind`.","zh":"選項，跟 [`ControllerHost`](#ControllerHost) 的第二個參數一樣：`profiles`、`signals`、`sink`、`channel`、`follow`、`midi`、`requestAccess`、`filterOut`、`storage`、`telemetry`、`embedKind`。"}],"returns":{"type":"ControllerHost","en":"The controller. Its methods are listed under [`ControllerHost`](#ControllerHost).","zh":"控制器本身。它的方法列在 [`ControllerHost`](#ControllerHost) 底下。"},"notes":[{"en":"Like `<oav-controller>`, it sends one anonymous hit per page load (which kind of embed and which device; nothing anyone plays) to the project's analytics, and nothing from localhost or the LAN. Turn it off with `createController(profile, { telemetry: false })`. The README's Telemetry section has the details.","zh":"跟 `<oav-controller>` 一樣，每次載入頁面會對專案的分析送出一筆匿名資料（哪一種嵌入、哪一台機器，不含任何人彈的內容），從 localhost 或區域網路則不送。用 `createController(profile, { telemetry: false })` 可以關掉。細節在 README 的 Telemetry 一節。"}],"examples":[{"code":"import { createController } from '@openav/midi';\n\nconst ctl = createController('akai-lpd8');\nctl.on('control', (e) => console.log(e.signal, e.value.toFixed(2), '<-', e.source));\n\nctl.set('k1', 0.5);        // a hand on knob 1\nctl.set('pad1', 1);        // press pad 1\nctl.set('pad1', 0);        // let go\nconsole.log(ctl.values().k1, ctl.get('nope'));","caption":{"en":"Prints three `control` events (knob 0.50, pad 1.00, pad 0.00, each from 'ui'), then the knob's value and `null` for an id that does not exist.","zh":"印出三個 `control` 事件（旋鈕 0.50、打擊墊 1.00、打擊墊 0.00，來源都是 'ui'），再印出旋鈕的值，以及不存在的 id 回傳的 `null`。"},"run":"module","lang":"js"},{"code":"import { createController } from '@openav/midi';\n\n// MIDI out to a DAW: a stand-in for the browser's Web MIDI shows what would be sent\nconst iac = { name: 'IAC Driver Bus 1', state: 'connected', send: (bytes) => console.log('to the DAW', bytes) };\nconst requestAccess = async () => ({ inputs: new Map(), outputs: new Map([['o', iac]]) });\n\nconst ctl = createController('akai-lpd8', { requestAccess });\nconsole.log(await ctl.setOutput('IAC'));   // IAC Driver Bus 1\nctl.set('pad1', 1);                        // to the DAW [153, 36, 127]\nctl.set('pad1', 0);                        // to the DAW [137, 36, 0]\nctl.ingest([0x99, 37, 90]);                // hardware-style input: the faceplate follows, nothing is sent out","caption":{"en":"Prints the port name, then the two messages a hand on the screen sends. The last line prints nothing: what arrives from hardware is not forwarded.","zh":"印出埠名，再印出螢幕上的手送出的兩則訊息。最後一行不會有輸出：從硬體進來的東西不會被轉送。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { createController } from '@openav/midi';\n\nconst signals = new Signals();\nconst ctl = createController('minilab3', { signals });\n\nconst app = document.getElementById('app');\napp.style.height = '240px';\nctl.mount(app);                            // the playable faceplate fills #app\n\nctl.set('knob1', 0.8);                     // the knob on screen turns\nconsole.log(signals.get('midi/minilab3/knob1'));","caption":{"en":"The MiniLab 3 appears in the page with knob 1 turned up, and the console prints its signal value (0.80…). Play it with the mouse.","zh":"頁面上出現 MiniLab 3，旋鈕 1 被轉大了，主控台印出它的訊號值（0.80…）。可以用滑鼠玩。"},"run":"module","lang":"js"}],"related":["ControllerHost","OavController","MidiController","loadProfile","Midi","ControllerView"],"keywords":{"en":["create controller","embed controller","headless controller","on-screen controller","react","vue","midi out","connect hardware","play controller"],"zh":["建立控制器","嵌入控制器","無畫面控制器","螢幕控制器","react","vue","midi 輸出","連接硬體","彈控制器"]},"tbd":[],"claims":[]},
{"id":"defineController","key":"defineController","name":"defineController","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"defineController(tag = 'oav-controller')","import":"import { defineController } from '@openav/midi/element';","source":{"file":"packages/midi/element.js","line":352,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/element.js#L352"},"summary":{"en":"Registers the controller element under another tag name (importing `@openav/midi/element` already registers it as `oav-controller`).","zh":"把 `<oav-controller>` 元素註冊成另一個標籤名稱（匯入 `@openav/midi/element` 時已經註冊好 `<oav-controller>` 了）。"},"description":[{"en":"A tag name can be registered only once per page, so use this when `oav-controller` clashes with another library, or when you want two differently-named copies. The default tag registers the exported [`OavController`](#OavController) class itself; any other name registers a subclass of it.","zh":"一個標籤名稱在同一個頁面只能註冊一次，所以當 `oav-controller` 跟別的函式庫衝突，或你想要兩個不同名字的副本時，就用它。預設標籤註冊的是匯出的 [`OavController`](#OavController) 類別本身，其他名稱註冊的是它的子類別。"}],"params":[{"name":"tag","type":"String","en":"The new tag name. It must contain a hyphen, as every custom element does.","zh":"新的標籤名稱，必須包含連字號，所有自訂元素都是如此。"}],"returns":{"type":"Function | null","en":"The element class registered for that tag (an already-registered tag returns what is there). `null` when the page has no `customElements`.","zh":"註冊在該標籤上的元素類別（標籤已經註冊過就回傳現有的）。頁面沒有 `customElements` 時回傳 `null`。"},"notes":[],"examples":[{"code":"import { defineController } from '@openav/midi/element';\n\ndefineController('my-pads');                          // now <my-pads> works too\n\nconst app = document.getElementById('app');\napp.innerHTML = '<my-pads profile=\"generic-16p\"></my-pads>';\nconst el = app.firstElementChild;\nawait el.ready;\nconsole.log(el.tagName, el.profileData.id);           // MY-PADS generic-16p","caption":{"en":"A 16-pad generic controller appears from a tag you named yourself; the console prints `MY-PADS generic-16p`.","zh":"一台 16 個打擊墊的通用控制器，從你自己取名的標籤出現，主控台印出 `MY-PADS generic-16p`。"},"run":"module","lang":"js"}],"related":["OavController"],"keywords":{"en":["register","custom element name","tag name","customElements.define"],"zh":["註冊","自訂元素名稱","標籤名稱","customElements.define"]},"tbd":[],"claims":[]},
{"id":"describeDetail","key":"describeDetail","name":"describeDetail","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"describeDetail(d)","import":"import { describeDetail } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":199,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L199"},"summary":{"en":"Writes the readout line for a `control` event payload: `midi/minilab3/knob1 = 0.42 ← CC 74 = 53 · ch 1`.","zh":"為 `control` 事件的內容寫出讀數列的文字：`midi/minilab3/knob1 = 0.42 ← CC 74 = 53 · ch 1`。"},"description":[{"en":"This is the text `<oav-controller readout>` shows under the faceplate. Use it for your own log or caption. A payload without a `message` shows only the signal and the value.","zh":"`<oav-controller readout>` 在面板下面顯示的就是這段文字。拿去做你自己的記錄或字幕也很方便。沒有 `message` 的內容只會顯示訊號和數值。"}],"params":[{"name":"d","type":"Object | null","en":"A payload from [`controlDetail()`](#controlDetail) (or a `control` event's `detail`). `null` gives an empty string.","zh":"[`controlDetail()`](#controlDetail) 的內容（或 `control` 事件的 `detail`）。給 `null` 回傳空字串。"}],"returns":{"type":"String","en":"The signal name, the value (two decimals, or a whole number from 100 up), then `←`, the message as [`describeMessage()`](#describeMessage) writes it and the channel.","zh":"訊號名稱、數值（兩位小數，100 以上則是整數），接著是 `←`、用 [`describeMessage()`](#describeMessage) 寫的訊息，和頻道。"},"notes":[],"examples":[{"code":"import { createController } from '@openav/midi';\nimport { describeDetail } from '@openav/midi/embed';\n\nconst ctl = createController('minilab3');\nctl.on('control', (d) => console.log(describeDetail(d)));\n\nctl.set('knob1', 0.42);\nctl.set('pitch', 1);\nctl.set('pad1', 1);","caption":{"en":"Prints `midi/minilab3/knob1 = 0.42 ← CC 74 = 53 · ch 1`, then the same kind of line for the pitch bend and pad 1.","zh":"印出 `midi/minilab3/knob1 = 0.42 ← CC 74 = 53 · ch 1`，接著是彎音輪和 pad 1 的同類文字。"},"run":"module","lang":"js"}],"related":["controlDetail","describeMessage","OavController"],"keywords":{"en":["readout line","event text","knob = 0.42","caption"],"zh":["讀數列","事件文字","旋鈕 = 0.42","字幕"]},"tbd":[],"claims":[]},
{"id":"describeMessage","key":"describeMessage","name":"describeMessage","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"describeMessage(m)","import":"import { describeMessage } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":185,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L185"},"summary":{"en":"Writes one parsed MIDI message the way a person reads it: `CC 70 = 53`, `Note 36 · vel 90`, `Bend 12288`.","zh":"把一則解析過的 MIDI 訊息寫成人讀得懂的樣子：`CC 70 = 53`、`Note 36 · vel 90`、`Bend 12288`。"},"description":[{"en":"These are the shorter lines of the `<oav-controller>` readout. [`describe()`](#describe) writes the longer log lines (`CC ch1 #70=53`).","zh":"這是 `<oav-controller>` 讀數列用的較短文字。[`describe()`](#describe) 寫的是較長的記錄文字（`CC ch1 #70=53`）。"}],"params":[{"name":"m","type":"Object | null","en":"An event from [`parseMessage()`](#parseMessage). `null` gives an empty string.","zh":"[`parseMessage()`](#parseMessage) 的事件。給 `null` 回傳空字串。"}],"returns":{"type":"String","en":"`CC 70 = 53`, `Note 36 · vel 90`, `Note 36 off`, `Bend 12288` (the raw 14-bit value), `Pressure 50`, `Poly AT 36 = 12`, `Program 5`; any other type gives its name.","zh":"`CC 70 = 53`、`Note 36 · vel 90`、`Note 36 off`、`Bend 12288`（原始的 14 位元數值）、`Pressure 50`、`Poly AT 36 = 12`、`Program 5`，其他類型則回傳類型名稱。"},"notes":[],"examples":[{"code":"import { parseMessage } from '@openav/midi';\nimport { describeMessage } from '@openav/midi/embed';\n\nfor (const bytes of [[0xb0, 70, 53], [0x99, 36, 90], [0x89, 36, 0], [0xe0, 0, 96]]) {\n  console.log(describeMessage(parseMessage(bytes)));\n}","caption":{"en":"Prints `CC 70 = 53`, `Note 36 · vel 90`, `Note 36 off` and `Bend 12288`.","zh":"印出 `CC 70 = 53`、`Note 36 · vel 90`、`Note 36 off` 和 `Bend 12288`。"},"run":"module","lang":"js"}],"related":["describeDetail","describe","parseMessage"],"keywords":{"en":["readout","message text","cc 70","readable midi"],"zh":["讀數列","訊息文字","cc 70","好讀的 midi"]},"tbd":[],"claims":[]},
{"id":"EMBED_ATTRS","key":"EMBED_ATTRS","name":"EMBED_ATTRS","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"EMBED_ATTRS = array, 11 items","import":"import { EMBED_ATTRS } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":22,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L22"},"summary":{"en":"The attributes the `oav-controller` element reads, which are also the query parameters of the iframe page: `profile`, `layout`, `hardware`, `midi-out` and more.","zh":"`<oav-controller>` 讀取的屬性，也同時是 iframe 頁面的查詢參數：`profile`、`layout`、`hardware`、`midi-out`、`channel` 等等。"},"description":[{"en":"The iframe page copies every one of these from its URL onto the element. The extra parameters `bg`, `pad`, `frame`, `origin` and `lang` belong to the iframe page and are read by [`parseEmbedOptions()`](#parseEmbedOptions). What each attribute means is under [`OavController`](#OavController).","zh":"iframe 頁面會把網址裡的這些參數逐一複製到元素上。另外的 `bg`、`pad`、`frame`、`origin`、`lang` 是 iframe 頁面專用的，由 [`parseEmbedOptions()`](#parseEmbedOptions) 讀取。每個屬性的意思寫在 [`OavController`](#OavController) 底下。"}],"value":{"type":"string[]","range":"['profile', 'layout', 'hardware', 'midi-out', 'channel', 'learn', 'picker', 'readout', 'follow', 'theme', 'telemetry']","en":"A plain array, in this order. The element watches these (and `lang`) for changes.","zh":"一個普通陣列，順序如上。元素會監看這些屬性（加上 `lang`）有沒有改變。"},"params":[],"notes":[],"examples":[{"code":"import { EMBED_ATTRS } from '@openav/midi/embed';\n\nconsole.log(EMBED_ATTRS.join(' '));","caption":{"en":"Prints the eleven attribute names.","zh":"印出十一個屬性名稱。"},"run":"module","lang":"js"}],"related":["OavController","parseEmbedOptions","EMBED_EVENTS"],"keywords":{"en":["attributes","query parameters","iframe parameters","embed options"],"zh":["屬性","查詢參數","iframe 參數","嵌入選項"]},"tbd":[],"claims":[]},
{"id":"EMBED_EVENTS","key":"EMBED_EVENTS","name":"EMBED_EVENTS","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"EMBED_EVENTS = array, 9 items","import":"import { EMBED_EVENTS } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":24,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L24"},"summary":{"en":"The nine events the `oav-controller` element dispatches and the iframe posts to its page, from `ready` and `control` to `profilechange` and `error`.","zh":"`<oav-controller>` 會觸發、iframe 也會送給外層頁面的事件：`ready`、`control`、`noteon`、`noteoff`、`connect`、`disconnect`、`status`、`profilechange`、`error`。"},"description":[{"en":"The iframe also posts a tenth type, `values`, as the answer to the `get` command; it is not an element event, so it is not in this list. The payloads are described under [`OavController`](#OavController) and [`controlDetail()`](#controlDetail).","zh":"iframe 還會送出第十種類型 `values`，那是對 `get` 指令的回答，不是元素事件，所以不在這份清單裡。各事件的內容寫在 [`OavController`](#OavController) 和 [`controlDetail()`](#controlDetail) 底下。"}],"value":{"type":"string[]","range":"['ready', 'control', 'noteon', 'noteoff', 'connect', 'disconnect', 'status', 'profilechange', 'error']","en":"A plain array, in this order. The element dispatches each as a `CustomEvent` that bubbles and is composed; the iframe page forwards each to `window.parent` with [`toParent()`](#toParent).","zh":"一個普通陣列，順序如上。元素把每一個都當成會冒泡、可穿過 shadow root 的 `CustomEvent` 觸發，iframe 頁面則用 [`toParent()`](#toParent) 把每一個轉送給 `window.parent`。"},"params":[],"notes":[],"examples":[{"code":"import { EMBED_EVENTS } from '@openav/midi/embed';\n\nconsole.log(EMBED_EVENTS.join(' '));","caption":{"en":"Prints the nine event names.","zh":"印出九個事件名稱。"},"run":"module","lang":"js"}],"related":["OavController","toParent","controlDetail","EMBED_ATTRS"],"keywords":{"en":["events","control event","noteon","ready","status","custom events"],"zh":["事件","control 事件","noteon","ready","status","自訂事件"]},"tbd":[],"claims":[]},
{"id":"EMBED_LAYOUTS","key":"EMBED_LAYOUTS","name":"EMBED_LAYOUTS","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"EMBED_LAYOUTS = array, 3 items","import":"import { EMBED_LAYOUTS } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":25,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L25"},"summary":{"en":"The three layouts a controller can have: `auto`, `face` (the device as it looks) and `stack` (re-gridded for a tall box).","zh":"控制器的三種版面：`auto`、`face`（機器原本的樣子）和 `stack`（為高的盒子重新排列）。"},"description":[{"en":"A profile can opt out of `stack` (`stack: false`); square grid devices such as the Launchpad always stay `face`. See [`preferredAspect()`](#preferredAspect).","zh":"設定檔可以不要 `stack`（`stack: false`），像 Launchpad 這種方形的格子機器永遠維持 `face`。見 [`preferredAspect()`](#preferredAspect)。"}],"value":{"type":"string[]","range":"['auto', 'face', 'stack']","en":"A plain array. [`parseEmbedOptions()`](#parseEmbedOptions) turns any other `layout` value into `'auto'`, and the `layout` command of the iframe accepts only these.","zh":"一個普通陣列。[`parseEmbedOptions()`](#parseEmbedOptions) 會把其他的 `layout` 值變成 `'auto'`，iframe 的 `layout` 指令也只接受這三個。"},"params":[],"notes":[],"examples":[{"code":"import { EMBED_LAYOUTS, parseEmbedOptions } from '@openav/midi/embed';\n\nconsole.log(EMBED_LAYOUTS);\nconsole.log(parseEmbedOptions({ layout: 'stack' }).layout);      // stack\nconsole.log(parseEmbedOptions({ layout: 'sideways' }).layout);   // auto: not in the list","caption":{"en":"Prints the three names, then `stack`, then `auto`.","zh":"印出三個名稱，再印出 `stack`，最後印出 `auto`。"},"run":"module","lang":"js"}],"related":["parseEmbedOptions","preferredAspect","OavController"],"keywords":{"en":["layout","face","stack","auto","phone layout"],"zh":["版面","face","stack","auto","手機版面"]},"tbd":[],"claims":[]},
{"id":"embedChannel","key":"embedChannel","name":"embedChannel","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"embedChannel(v)","import":"import { embedChannel } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":49,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L49"},"summary":{"en":"Reads the `channel` attribute: a whole number from 1 to 16, or `null` for anything else.","zh":"讀取 `channel` 屬性：1 到 16 的整數，其他任何值都是 `null`。"},"description":[{"en":"A `null` means \"keep the profile's own channels\". A number makes [`withChannel()`](#withChannel) move every control there.","zh":"`null` 代表「維持設定檔本來的頻道」。有數字的話，[`withChannel()`](#withChannel) 會把所有控制項移到那個頻道。"}],"params":[{"name":"v","type":"any","en":"The attribute value, a number or a string.","zh":"屬性的值，數字或字串。"}],"returns":{"type":"Number | null","en":"The channel, or `null` when it is missing, empty, not a whole number or outside 1..16.","zh":"頻道，沒寫、空字串、不是整數或不在 1..16 時是 `null`。"},"notes":[],"examples":[{"code":"import { embedChannel } from '@openav/midi/embed';\n\nconsole.log(embedChannel('10'), embedChannel(3));                     // 10 3\nconsole.log(embedChannel('0'), embedChannel('17'), embedChannel('x'), embedChannel(''));   // null null null null","caption":{"en":"Prints `10 3`, then four `null`s.","zh":"印出 `10 3`，再印出四個 `null`。"},"run":"module","lang":"js"}],"related":["withChannel","parseEmbedOptions"],"keywords":{"en":["midi channel","channel attribute","validate channel"],"zh":["midi 頻道","channel 屬性","檢查頻道"]},"tbd":[],"claims":[]},
{"id":"embedColor","key":"embedColor","name":"embedColor","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"embedColor(v)","import":"import { embedColor } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":56,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L56"},"summary":{"en":"Checks the `bg` colour of the iframe page and returns a safe value: `transparent`, a hex colour, or a plain colour name; anything else is `null`.","zh":"檢查 iframe 頁面的 `bg` 顏色，回傳安全的值：`transparent`、十六進位顏色或單純的顏色名稱，其他一律是 `null`。"},"description":[{"en":"The iframe page puts the result into a CSS variable, so a value that could break out of CSS (`red;}body{`) is refused. It does not check that a name is a real colour: any 3 to 20 letters pass, and CSS ignores the ones it does not know.","zh":"iframe 頁面會把結果放進一個 CSS 變數，所以可能跳出 CSS 的值（`red;}body{`）會被拒絕。它不檢查名稱是不是真的顏色：任何 3 到 20 個字母都會通過，CSS 不認得的就會被忽略。"}],"params":[{"name":"v","type":"String","en":"The value from the query string.","zh":"來自查詢字串的值。"}],"returns":{"type":"String | null","en":"`'transparent'`; `'#'` plus 3, 4, 6 or 8 hex digits (the `#` is optional in the input, so `abc` becomes `#abc`); or 3 to 20 letters, lower-cased (`'RED'` becomes `'red'`); otherwise `null`.","zh":"`'transparent'`，或 `'#'` 加上 3、4、6 或 8 個十六進位數字（輸入裡的 `#` 可有可無，所以 `abc` 會變成 `#abc`），或 3 到 20 個英文字母（轉成小寫，`'RED'` 變成 `'red'`），其他是 `null`。"},"notes":[],"examples":[{"code":"import { embedColor } from '@openav/midi/embed';\n\nconsole.log(embedColor('transparent'), embedColor('#0b0b0c'), embedColor('000'), embedColor('RED'));\nconsole.log(embedColor('red;}body{'), embedColor('#12345'), embedColor(''));   // null null null","caption":{"en":"Prints `transparent #0b0b0c #000 red`, then three `null`s.","zh":"印出 `transparent #0b0b0c #000 red`，再印出三個 `null`。"},"run":"module","lang":"js"}],"related":["parseEmbedOptions"],"keywords":{"en":["background colour","bg parameter","transparent","hex","safe css"],"zh":["背景顏色","bg 參數","透明","十六進位","安全的 css"]},"tbd":[],"claims":[]},
{"id":"embedFlag","key":"embedFlag","name":"embedFlag","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"embedFlag(v)","import":"import { embedFlag } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L30"},"summary":{"en":"Reads a boolean attribute: present means on, unless it says `false`, `0`, `off` or `no`.","zh":"讀取布林屬性：有寫就是開，除非內容是 `false`、`0`、`off` 或 `no`。"},"description":[{"en":"This is why `<oav-controller readout>` and `readout=\"true\"` both turn the readout on and `readout=\"off\"` turns it off. Any other word counts as on, so `readout=\"ask\"` is `true`.","zh":"所以 `<oav-controller readout>` 和 `readout=\"true\"` 都會開啟讀數列，`readout=\"off\"` 則會關掉。其他任何字都算開，所以 `readout=\"ask\"` 也是 `true`。"}],"params":[{"name":"v","type":"any","en":"The attribute value: `null`/`undefined`/`false` (absent) or a string. A bare attribute is the empty string.","zh":"屬性的值：`null`、`undefined`、`false`（沒寫）或字串。只寫屬性名稱時是空字串。"}],"returns":{"type":"Boolean","en":"Absent is `false`; `true` is `true`; a string is `true` unless it is (case-insensitively, trimmed) `false`, `0`, `off` or `no`.","zh":"沒寫是 `false`，`true` 是 `true`，字串除了（不分大小寫、去掉空白後）`false`、`0`、`off`、`no` 之外都是 `true`。"},"notes":[],"examples":[{"code":"import { embedFlag } from '@openav/midi/embed';\n\nconsole.log(embedFlag(''), embedFlag('true'), embedFlag('ask'));    // true true true\nconsole.log(embedFlag(null), embedFlag('off'), embedFlag('No'));    // false false false","caption":{"en":"Prints three `true`s, then three `false`s.","zh":"印出三個 `true`，再印出三個 `false`。"},"run":"module","lang":"js"}],"related":["parseEmbedOptions","embedMode"],"keywords":{"en":["boolean attribute","flag","on off","true false"],"zh":["布林屬性","旗標","開關","true false"]},"tbd":[],"claims":[]},
{"id":"embedMode","key":"embedMode","name":"embedMode","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"embedMode(v, { bare = 'on' } = {})","import":"import { embedMode } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L38"},"summary":{"en":"Reads `hardware` and `midi-out`: off, `ask` (a button, so the permission prompt waits for a click) or on, plus which port.","zh":"讀取 `hardware` 和 `midi-out`：關閉、`ask`（顯示按鈕，讓權限詢問等到點擊）或開啟，以及指定哪個埠。"},"description":[{"en":"Absent or `false`/`0`/`off`/`no` is off. `ask` is ask. `on`, `true`, `yes`, `auto` is on without a port. Any other text is on, and the text is the port name or `/regex/` to look for (see [`findPort()`](#findPort)): `embedMode('IAC')` is `{ mode: 'on', port: 'IAC' }`.","zh":"沒寫或是 `false`/`0`/`off`/`no` 就是關閉。`ask` 就是 ask。`on`、`true`、`yes`、`auto` 是開啟但不指定埠。其他任何文字都是開啟，而那段文字就是要找的埠名或 `/regex/`（見 [`findPort()`](#findPort)）：`embedMode('IAC')` 是 `{ mode: 'on', port: 'IAC' }`。"}],"params":[{"name":"v","type":"any","en":"The attribute value.","zh":"屬性的值。"},{"name":"bare","type":"String","en":"What a bare attribute (no value) means: `'on'` for `hardware` (the default), `'ask'` for `midi-out`, because there is no sensible default port to send to.","zh":"只寫屬性、沒有值時的意思：`hardware` 是 `'on'`（預設），`midi-out` 是 `'ask'`，因為送出去沒有合理的預設埠。"}],"returns":{"type":"{ mode, port }","en":"`mode` is `'off'`, `'ask'` or `'on'`. `port` is a string only when the value names one.","zh":"`mode` 是 `'off'`、`'ask'` 或 `'on'`。只有值裡有指定埠時，`port` 才是字串。"},"notes":[],"examples":[{"code":"import { embedMode } from '@openav/midi/embed';\n\nconsole.log(embedMode(null));                    // { mode: 'off', port: null }\nconsole.log(embedMode(''));                      // { mode: 'on', port: null }  (a bare hardware attribute)\nconsole.log(embedMode('', { bare: 'ask' }));     // { mode: 'ask', port: null } (a bare midi-out attribute)\nconsole.log(embedMode('loopMIDI'));              // { mode: 'on', port: 'loopMIDI' }","caption":{"en":"Prints four `{ mode, port }` objects: off, on, ask, and on with a named port.","zh":"印出四個 `{ mode, port }` 物件：關閉、開啟、ask，以及指定了埠名的開啟。"},"run":"module","lang":"js"}],"related":["parseEmbedOptions","embedFlag","findPort"],"keywords":{"en":["hardware attribute","midi-out attribute","ask","port name","permission"],"zh":["hardware 屬性","midi-out 屬性","ask","埠名","權限"]},"tbd":[],"claims":[]},
{"id":"fadeLevel","key":"fadeLevel","name":"fadeLevel","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"fadeLevel(now, at, ms)","import":"import { fadeLevel } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":64,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L64"},"summary":{"en":"The brightness of a fading touch: 1 at the moment it happened, 0 after `ms`, eased so it still reads on a 30 fps frame.","zh":"一次淡出中的觸碰目前有多亮：發生的那一刻是 1，經過 `ms` 之後是 0，並做了緩和，所以在 30 fps 的畫面上仍然看得出來。"},"description":[{"en":"[`ControllerCanvas`](#ControllerCanvas) fades released keys (180 ms), pads (450 ms), the glow of a touched control (650 ms), the last-touch text under the faceplate (1600 ms) and on the device screen (3200 ms) with it. A video frame is 33 ms at 30 fps, so a staccato note that was 20 ms long would be gone before the next frame without a fade.","zh":"[`ControllerCanvas`](#ControllerCanvas) 用它讓放開的琴鍵（180 毫秒）、打擊墊（450 毫秒）、被碰到的控制項光暈（650 毫秒）、面板下方的最後觸碰文字（1600 毫秒）和機器螢幕上的文字（3200 毫秒）淡出。30 fps 的影片一格是 33 毫秒，沒有淡出的話，20 毫秒長的斷音在下一格之前就消失了。"}],"params":[{"name":"now","type":"Number","en":"The current time in milliseconds.","zh":"目前的時間（毫秒）。"},{"name":"at","type":"Number | null","en":"When the touch happened, on the same clock. `null` or `undefined` gives 0.","zh":"觸碰發生的時間，用同一個時鐘。`null` 或 `undefined` 會得到 0。"},{"name":"ms","type":"Number","en":"How long the fade lasts. Not above 0 gives 0.","zh":"淡出持續多久。不大於 0 會得到 0。"}],"returns":{"type":"Number","en":"From 1 down to 0. The curve is the square of the remaining time, so it falls quickly at first.","zh":"從 1 降到 0。曲線是剩餘時間的平方，所以一開始掉得比較快。"},"notes":[],"examples":[{"code":"import { fadeLevel } from '@openav/midi';\n\nfor (const now of [100, 200, 300, 400, 500]) console.log(now, fadeLevel(now, 100, 400));","caption":{"en":"Prints 1 at the moment of the touch, then 0.5625, 0.25, 0.0625 and 0 at the end of the fade.","zh":"觸碰當下印出 1，之後是 0.5625、0.25、0.0625，淡出結束時是 0。"},"run":"module","lang":"js"}],"related":["ControllerCanvas"],"keywords":{"en":["fade","ease","decay","video frame","staccato"],"zh":["淡出","緩和","衰減","影片畫格","斷音"]},"tbd":[],"claims":[]},
{"id":"followMidiSignals","key":"followMidiSignals","name":"followMidiSignals","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"followMidiSignals(signals, onEvent, { channel = 1, defer = (fn) => queueMicrotask(fn) } = {})","import":"import { followMidiSignals } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":194,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L194"},"summary":{"en":"Listens to a `Signals` bus and hands you each MIDI message back exactly once, as an event, although hardware publishes it under two names.","zh":"監聽 `Signals` 匯流排，並把每則 MIDI 訊息「剛好一次」以事件的形式還給你，雖然硬體會用兩個名稱發佈同一則訊息。"},"description":[{"en":"Hardware and the on-screen controllers publish every message twice: the legacy name (`midi/note/on`) and then the per-channel one (`midi/ch/1/note/60`) in one call. The QWERTY piano and some takes publish only the legacy name. A per-channel name counts at once; a legacy name waits for a microtask and is dropped if its per-channel twin shows up first. So a relative encoder never turns twice, a legacy CC (which has no channel) never lands on the wrong channel, and order is kept.","zh":"硬體和螢幕控制器對每則訊息都會發佈兩次：先是短名稱（`midi/note/on`），緊接著在同一次呼叫裡是分頻道的名稱（`midi/ch/1/note/60`）。電腦鍵盤鋼琴和某些錄音只發佈短名稱。分頻道的名稱會立刻算數，短名稱則等一個 microtask，如果它分頻道的孿生兄弟先到了就丟掉。所以相對旋鈕不會轉兩次，沒有頻道的短名稱 CC 也不會落到錯誤的頻道，順序也會保持。"},{"en":"[`ControllerCanvas`](#ControllerCanvas) uses it. Use it yourself to turn a bus back into MIDI events, for example to drive your own visual from a recorded take.","zh":"[`ControllerCanvas`](#ControllerCanvas) 用的就是它。想把匯流排變回 MIDI 事件時可以自己用，例如用錄下的演奏去驅動你自己的畫面。"}],"params":[{"name":"signals","type":"Signals","en":"The bus. It needs `onAny(cb)`.","zh":"匯流排，需要有 `onAny(cb)`。"},{"name":"onEvent","type":"Function","en":"`(ev) => …` called with an event of [`parseMessage()`](#parseMessage) for each message.","zh":"每則訊息都以 [`parseMessage()`](#parseMessage) 的事件格式呼叫 `(ev) => …`。"},{"name":"channel","type":"Number","en":"The channel given to legacy CC and bend names, which carry none.","zh":"指定給短名稱 CC 和彎音用的頻道，因為它們本身沒有頻道資訊。"},{"name":"defer","type":"Function","en":"`(fn) => …` that schedules the flush of legacy-only names. Default `queueMicrotask`; replaceable in tests.","zh":"排程「只有短名稱」那批訊息的 flush 用的 `(fn) => …`。預設是 `queueMicrotask`，測試時可以換掉。"}],"returns":{"type":"{ flush(), dispose() }","en":"`flush()` delivers legacy-only messages that are still waiting. `dispose()` stops listening and drops them.","zh":"`flush()` 送出還在等待的「只有短名稱」訊息。`dispose()` 停止監聽並丟掉它們。"},"notes":[],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { followMidiSignals, genericSignals, publish } from '@openav/midi';\n\nconst signals = new Signals();\nconst follower = followMidiSignals(signals, (ev) => console.log(ev.type, ev.note ?? ev.cc, ev.ch));\n\n// the hardware publishes a message under both names; the follower reports it once\npublish(signals, genericSignals({ type: 'noteon', ch: 2, note: 60, vel: 100 }));\npublish(signals, genericSignals({ type: 'cc', ch: 2, cc: 74, value: 64 }));\n\n// the QWERTY piano publishes only the short name: it arrives after a microtask, or at flush()\nsignals.pulse('midi/note/on', { note: 64, vel: 0.8, ch: 0 });\nfollower.flush();\nfollower.dispose();","caption":{"en":"Prints three lines: `noteon 60 2`, `cc 74 2`, then `noteon 64 1` for the piano (channel 0 becomes the default channel 1).","zh":"印出三行：`noteon 60 2`、`cc 74 2`，然後是鍵盤鋼琴的 `noteon 64 1`（頻道 0 變成預設的頻道 1）。"},"run":"module","lang":"js"}],"related":["eventOfSignal","genericSignals","ControllerCanvas"],"keywords":{"en":["signals to midi events","dedupe","each message once","bus listener","take playback"],"zh":["訊號轉 midi 事件","去重複","每則一次","匯流排監聽","回放"]},"tbd":[],"claims":[]},
{"id":"FRAME_COMMANDS","key":"FRAME_COMMANDS","name":"FRAME_COMMANDS","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"FRAME_COMMANDS = array, 8 items","import":"import { FRAME_COMMANDS } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":271,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L271"},"summary":{"en":"The commands a page may send into an embed iframe: `set`, `ingest`, `reset`, `profile`, `layout`, `get`, `press`, `release`.","zh":"外層頁面可以送進嵌入 iframe 的指令：`set`、`ingest`、`reset`、`profile`、`layout`、`get`、`press`、`release`。"},"description":[{"en":"What each does: `set` `{ id, value, note?, quiet? }` moves a control, `press` `{ id, velocity?, note? }` and `release` `{ id, note? }` hold and let go, `ingest` `{ bytes }` feeds MIDI (one message or a list), `reset` brings everything home, `profile` `{ profile }` switches the device, `layout` `{ layout }` changes the layout, and `get` asks for every value (the iframe answers with a `values` message).","zh":"各指令的作用：`set` `{ id, value, note?, quiet? }` 移動控制項，`press` `{ id, velocity?, note? }` 與 `release` `{ id, note? }` 按住與放開，`ingest` `{ bytes }` 餵入 MIDI（一則或一串），`reset` 讓所有東西回到原位，`profile` `{ profile }` 切換機器，`layout` `{ layout }` 改變版面，`get` 要求所有數值（iframe 會用一則 `values` 訊息回答）。"}],"value":{"type":"string[]","range":"['set', 'ingest', 'reset', 'profile', 'layout', 'get', 'press', 'release']","en":"A plain array. [`fromParent()`](#fromParent) ignores a message whose `type` is not one of these.","zh":"一個普通陣列。[`fromParent()`](#fromParent) 會忽略 `type` 不在這份清單裡的訊息。"},"params":[],"notes":[],"examples":[{"code":"import { FRAME_COMMANDS, toFrame } from '@openav/midi/embed';\n\nconsole.log(FRAME_COMMANDS.join(' '));\nconsole.log(toFrame('set', { id: 'k1', value: 0.5 }));   // the message you post to the iframe","caption":{"en":"Prints the eight commands, then the message `{ target: 'openav', v: 1, type: 'set', id: 'k1', value: 0.5 }`.","zh":"印出八個指令，再印出訊息 `{ target: 'openav', v: 1, type: 'set', id: 'k1', value: 0.5 }`。"},"run":"module","lang":"js"}],"related":["fromParent","toFrame","EMBED_EVENTS"],"keywords":{"en":["iframe commands","control the iframe","postMessage commands"],"zh":["iframe 指令","控制 iframe","postMessage 指令"]},"tbd":[],"claims":[]},
{"id":"fromFrame","key":"fromFrame","name":"fromFrame","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"fromFrame(data)","import":"import { fromFrame } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":280,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L280"},"summary":{"en":"Page side: reads a message that came from an embed iframe, returns `{ type, frame, detail, v }`, or `null` when it is not one of ours.","zh":"外層頁面這一側：讀取從嵌入 iframe 來的訊息，回傳 `{ type, frame, detail, v }`，不是我們的訊息就回傳 `null`。"},"description":[{"en":"Other scripts and extensions post to a page all the time, so always filter. Check `event.origin` too when the page cares who sent it: this function checks the shape, not the sender.","zh":"別的腳本和瀏覽器擴充功能隨時都在對頁面送訊息，所以一定要過濾。頁面在意是誰送的話，也要檢查 `event.origin`：這個函式檢查的是格式，不是寄件者。"}],"params":[{"name":"data","type":"any","en":"The `event.data` of a `message` event.","zh":"`message` 事件的 `event.data`。"}],"returns":{"type":"{ type, frame, detail, v } | null","en":"`null` unless `data` is an object with `source: 'openav'` and a string `type`.","zh":"除非 `data` 是帶有 `source: 'openav'` 和字串 `type` 的物件，否則回傳 `null`。"},"notes":[],"examples":[{"code":"import { fromFrame, toParent } from '@openav/midi/embed';\n\n// what a page does with every 'message' event; here the message is made by hand\nconst data = toParent('control', { id: 'k1', signal: 'midi/lpd8/k1', value: 1 }, { frame: 'pads' });\nconst msg = fromFrame(data);\nif (msg && msg.type === 'control') console.log(msg.frame, msg.detail.signal, msg.detail.value);\n\nconsole.log(fromFrame({ type: 'control' }), fromFrame('hello'));   // null null","caption":{"en":"Prints `pads midi/lpd8/k1 1`, then `null null` for messages that are not ours.","zh":"印出 `pads midi/lpd8/k1 1`，再對不是我們的訊息印出 `null null`。"},"run":"module","lang":"js"},{"code":"window.addEventListener('message', (e) => {\n  const msg = fromFrame(e.data);\n  if (!msg) return;                                        // not from an openav iframe\n  if (msg.frame === 'pads' && msg.type === 'control') {\n    console.log(msg.detail.id, msg.detail.value);\n  }\n});","caption":{"en":"The listener to put on the page that holds the iframe.","zh":"放在包著 iframe 的那個頁面上的監聽程式。"},"run":"static","lang":"js"}],"related":["toParent","toFrame","fromParent","POST_SOURCE"],"keywords":{"en":["postMessage listener","message from iframe","read embed message"],"zh":["postMessage 監聽","來自 iframe 的訊息","讀取嵌入訊息"]},"tbd":[],"claims":[]},
{"id":"fromParent","key":"fromParent","name":"fromParent","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"fromParent(data)","import":"import { fromParent } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":291,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L291"},"summary":{"en":"Frame side: validates a command from the page and returns a clean command, or `null` (the message is ignored).","zh":"iframe 這一側：驗證外層頁面送來的指令，回傳整理乾淨的指令，不合格就回傳 `null`（訊息被忽略）。"},"description":[{"en":"It is strict, because anything on the web can post to an iframe. The message needs `target: 'openav'` and a `type` from [`FRAME_COMMANDS`](#FRAME_COMMANDS). Control ids must be lower-case `[a-z0-9_-]`. `value` must be a finite number. `note` must be an integer 0..127 (else `null`). A `press` velocity is held to 0..1. `ingest` takes one message of 1 to 3 bytes whose first byte is a status byte (128 or more), or up to 256 such messages. `profile` takes an id or an `https://` URL (not `http://`). `layout` must be in [`EMBED_LAYOUTS`](#EMBED_LAYOUTS).","zh":"它很嚴格，因為網路上任何東西都能對 iframe 送訊息。訊息必須有 `target: 'openav'`，`type` 要在 [`FRAME_COMMANDS`](#FRAME_COMMANDS) 裡。控制項 id 必須是小寫的 `[a-z0-9_-]`。`value` 必須是有限的數字。`note` 必須是 0..127 的整數（否則是 `null`）。`press` 的力度會限制在 0..1。`ingest` 接受一則 1 到 3 個位元組、第一個位元組是狀態位元組（128 以上）的訊息，或最多 256 則這樣的訊息。`profile` 接受 id 或 `https://` 網址（不接受 `http://`）。`layout` 必須在 [`EMBED_LAYOUTS`](#EMBED_LAYOUTS) 裡。"}],"params":[{"name":"data","type":"any","en":"The `event.data` of a `message` event.","zh":"`message` 事件的 `event.data`。"}],"returns":{"type":"Object | null","en":"A command such as `{ type: 'set', id: 'k1', value: 0.5, note: null, quiet: false }`, or `null`.","zh":"像 `{ type: 'set', id: 'k1', value: 0.5, note: null, quiet: false }` 這樣的指令，或 `null`。"},"notes":[],"examples":[{"code":"import { fromParent, toFrame } from '@openav/midi/embed';\n\nconst show = (data) => console.log(JSON.stringify(fromParent(data)));\n\nshow(toFrame('set', { id: 'k1', value: '0.5' }));    // value is read as a number\nshow(toFrame('press', { id: 'pad1', velocity: 2 })); // velocity held to 1\nshow(toFrame('ingest', { bytes: [0xb0, 70, 64] }));\n\n// all of these are ignored: null\nshow({ source: 'openav', type: 'set', id: 'k1', value: 1 });    // wrong key: source, not target\nshow(toFrame('set', { id: '<img>', value: 1 }));                // bad id\nshow(toFrame('profile', { profile: 'http://x.org/p.json' }));   // http, not https\nshow(toFrame('eval', { code: 'alert(1)' }));                    // not a command","caption":{"en":"Prints three clean commands, then four `null`s for messages the frame would ignore.","zh":"印出三個整理乾淨的指令，再對 frame 會忽略的四則訊息印出四個 `null`。"},"run":"module","lang":"js"}],"related":["toFrame","fromFrame","toParent","FRAME_COMMANDS"],"keywords":{"en":["validate command","iframe security","postMessage validation","ignore bad messages"],"zh":["驗證指令","iframe 安全","postMessage 驗證","忽略不合法訊息"]},"tbd":[],"claims":[]},
{"id":"keyboardGeometry","key":"keyboardGeometry","name":"keyboardGeometry","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"keyboardGeometry(from, to, rect)","import":"import { keyboardGeometry } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":138,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L138"},"summary":{"en":"Computes the rectangles of piano keys from one note to another inside a rect: white keys side by side, black keys over the seams at 60% of the height.","zh":"算出從某個音到另一個音的琴鍵在矩形裡的位置：白鍵並排，黑鍵蓋在縫上，高度是 60%。"},"description":[{"en":"A range never starts on a black key: a black key with no white key before it is left out, the way the on-screen keyboard does. White keys share the width equally, so 48 to 72 is 15 white keys and 10 black ones.","zh":"音域不會從黑鍵開始：前面沒有白鍵的黑鍵會被省略，螢幕上的鍵盤也是這樣。白鍵平分寬度，所以 48 到 72 是 15 個白鍵加 10 個黑鍵。"}],"params":[{"name":"from","type":"Number","en":"The first MIDI note.","zh":"第一個 MIDI 音符。"},{"name":"to","type":"Number","en":"The last MIDI note.","zh":"最後一個 MIDI 音符。"},{"name":"rect","type":"{ x, y, w, h }","en":"The area the keys fill.","zh":"琴鍵要填滿的區域。"}],"returns":{"type":"{ note, white, x, y, w, h }[]","en":"One item per key, the white keys first and then the black ones (the draw order, so the black keys end up on top).","zh":"每個琴鍵一項，先是白鍵再是黑鍵（也就是繪製順序，這樣黑鍵會疊在上面）。"},"notes":[],"examples":[{"code":"import { keyboardGeometry } from '@openav/midi';\n\nconst keys = keyboardGeometry(60, 72, { x: 0, y: 0, w: 400, h: 100 });\nconsole.log(keys.filter((k) => k.white).length, 'white,', keys.filter((k) => !k.white).length, 'black');\nconsole.log(keys[0].note, keys[0].x, keys[0].w);   // 60 (C4) is the first white key","caption":{"en":"Prints `8 white, 5 black` (one octave and the next C), then the first key's note and its position.","zh":"印出 `8 white, 5 black`（一個八度加上下一個 C），再印出第一個鍵的音符和位置。"},"run":"module","lang":"js"}],"related":["ControllerCanvas","keyWindow","controllerGeometry"],"keywords":{"en":["piano keys","keyboard layout","white keys","black keys","draw keyboard"],"zh":["琴鍵","鍵盤版面","白鍵","黑鍵","畫鍵盤"]},"tbd":[],"claims":[]},
{"id":"keyWindow","key":"keyWindow","name":"keyWindow","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"keyWindow(from, to, base, note)","import":"import { keyWindow } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":157,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L157"},"summary":{"en":"Tells which note a drawn keyboard should start at after a note outside its printed range arrives, so it follows the device's octave buttons.","zh":"一個超出印刷音域的音符到來之後，算出畫出來的鍵盤該從哪個音開始，這樣它就能跟著機器的八度按鈕走。"},"description":[{"en":"A keyboard control catches notes outside its printed range, because the device's Oct+/Oct− buttons shift what the keys send. When one arrives, the drawing moves and stays there, as the device does. A 48 to 72 MiniLab with Oct+ pressed (a note of 76) shows 60 to 84. A range narrower than an octave just moves to reach the note.","zh":"琴鍵控制項會接住超出印刷音域的音，因為機器的 Oct+/Oct− 按鈕會改變琴鍵送出的音。這種音到來時，畫面會移動並停在那裡，就像實機一樣。48 到 72 的 MiniLab 按下 Oct+（音符 76）後，畫面會顯示 60 到 84。窄於一個八度的音域，則是直接移動到夠到那個音為止。"}],"params":[{"name":"from","type":"Number","en":"The first note of the printed range.","zh":"印刷音域的第一個音。"},{"name":"to","type":"Number","en":"The last note of the printed range.","zh":"印刷音域的最後一個音。"},{"name":"base","type":"Number","en":"The first note shown now.","zh":"目前顯示的第一個音。"},{"name":"note","type":"Number","en":"A note that just went down.","zh":"剛剛按下的音。"}],"returns":{"type":"Number","en":"The new first note: `base` when the note fits, otherwise `base` moved by the fewest octaves that reach it, held to the MIDI range.","zh":"新的第一個音：音符放得進去就回傳 `base`，否則是把 `base` 移動剛好夠的最少八度，並限制在 MIDI 範圍內。"},"notes":[],"examples":[{"code":"import { keyWindow } from '@openav/midi';\n\nconsole.log(keyWindow(48, 72, 48, 60));    // 48: inside the window, unchanged\nconsole.log(keyWindow(48, 72, 48, 76));    // 60: Oct+ on the MiniLab\nconsole.log(keyWindow(48, 72, 48, 40));    // 36: Oct-\nconsole.log(keyWindow(48, 72, 48, 127));   // 103: held to the MIDI range","caption":{"en":"Prints 48, 60, 36 and 103.","zh":"印出 48、60、36、103。"},"run":"module","lang":"js"}],"related":["keyboardGeometry","ControllerCanvas"],"keywords":{"en":["octave shift","oct+","keyboard window","follow octave"],"zh":["八度移位","oct+","鍵盤視窗","跟著八度"]},"tbd":[],"claims":[]},
{"id":"midiNoteName","key":"midiNoteName","name":"midiNoteName","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"midiNoteName(n)","import":"import { midiNoteName } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":40,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L40"},"summary":{"en":"Turns a MIDI note number into its name with the octave, middle C being C4: `midiNoteName(60)` is `'C4'`, `midiNoteName(61)` is `'C#4'`.","zh":"把 MIDI 音符號碼轉成帶八度的音名，中央 C 是 C4：`midiNoteName(60)` 是 `'C4'`，`midiNoteName(61)` 是 `'C#4'`。"},"description":[{"en":"This is the on-screen keyboard's convention. Some software (Ableton Live, Yamaha) calls 60 \"C3\": the number is the same, only the octave label differs. In a p5 sketch, [`noteName()`](#noteName) does the same and also writes flats when you ask.","zh":"這是螢幕鍵盤採用的寫法。有些軟體（Ableton Live、Yamaha）把 60 叫做「C3」：號碼是一樣的，只有八度的標示不同。在 p5 草圖裡，[`noteName()`](#noteName) 做同樣的事，而且需要時可以寫成降記號。"}],"params":[{"name":"n","type":"Number","en":"A MIDI note number, 0..127.","zh":"MIDI 音符號碼，0..127。"}],"returns":{"type":"String","en":"Sharps only (`'C#4'`, never `'Db4'`). 0 is `'C-1'` and 127 is `'G9'`.","zh":"只用升記號（`'C#4'`，不會是 `'Db4'`）。0 是 `'C-1'`，127 是 `'G9'`。"},"notes":[],"examples":[{"code":"import { midiNoteName } from '@openav/midi';\n\nconsole.log([60, 61, 69, 0, 127].map(midiNoteName));   // [ 'C4', 'C#4', 'A4', 'C-1', 'G9' ]","caption":{"en":"Prints the five note names.","zh":"印出五個音名。"},"run":"module","lang":"js"}],"related":["noteName","noteNumber","touchText"],"keywords":{"en":["note name","midi note","C4","middle C","convert"],"zh":["音名","midi 音符","C4","中央 C","轉換"]},"tbd":[],"claims":[]},
{"id":"mountMidiPanel","key":"mountMidiPanel","name":"mountMidiPanel","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"mountMidiPanel(parent, mcs, { mode = 'full', open = true, id = null, onToggle = null, closeButton = mode === 'dock', profiles = null, contained = false } = {})","import":"import { mountMidiPanel } from '@openav/midi';","source":{"file":"packages/midi/panel.js","line":62,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/panel.js#L62"},"summary":{"en":"Mounts a controller panel: a toolbar (device picker, hardware status, port picker, Learn, last message) above a playable faceplate, docked at the bottom or full size.","zh":"掛上一個控制器面板：上面一條工具列，有機器選單、硬體狀態、埠選單、學習按鈕和最後一則訊息的監看，下面是可以彈的面板，可以停靠在底部或填滿。"},"description":[{"en":"The bar shows which device is on screen (the picker switches it), a chip saying whether a hardware port drives it, a port picker when hardware is attached, a Learn button (click a control, then move the one on the hardware; a second button resets the mapping), and a tiny monitor that prints the last message, so you see the hardware talking before you see what it moves. Plugging in a known device switches the picker and the faceplate.","zh":"工具列顯示螢幕上是哪台機器（可以用選單切換）、一個說明有沒有硬體埠在驅動它的標籤、接了硬體時的埠選單、學習按鈕（點一個控制項，再動硬體上對應的那一個，另一個按鈕可以還原對應），以及一個小監看視窗印出最後一則訊息，讓你在看到它推動什麼之前，先看到硬體在說話。接上認得的機器時，選單和面板都會跟著切換。"}],"params":[{"name":"parent","type":"HTMLElement","en":"The element to add the panel to. In `full` mode the panel is `position: absolute; inset: 0`, so give `parent` a position (`relative`) and a size.","zh":"要加進面板的元素。`full` 模式的面板是 `position: absolute; inset: 0`，所以 `parent` 要設 position（`relative`）並有大小。"},{"name":"mcs","type":"MidiControllers","en":"The manager that holds the devices and the hardware.","zh":"管理裝置與硬體的管理器。"},{"name":"mode","type":"String","en":"`'full'` fills `parent`. `'dock'` is a drawer fixed to the bottom of the window, 40% of the window height by default, with a grip to drag its height (remembered in `localStorage`).","zh":"`'full'` 填滿 `parent`。`'dock'` 是固定在視窗底部的抽屜，預設佔視窗高度的 40%，有把手可以拖曳調整高度（記在 `localStorage`）。"},{"name":"open","type":"Boolean","en":"Whether it starts open. A closed dock builds its view only when opened.","zh":"一開始是否打開。關著的停靠面板要到打開時才建立畫面。"},{"name":"id","type":"String | null","en":"An element id for the panel.","zh":"面板的元素 id。"},{"name":"onToggle","type":"Function | null","en":"`(open) => …` called when the panel opens or closes.","zh":"面板打開或關閉時呼叫 `(open) => …`。"},{"name":"closeButton","type":"Boolean","en":"Show a ✕ button. The default is on in `dock` mode only.","zh":"是否顯示 ✕ 按鈕。預設只有 `dock` 模式會顯示。"},{"name":"profiles","type":"String[] | null","en":"Limit the device picker to these profile ids.","zh":"把機器選單限制在這些設定檔 id。"},{"name":"contained","type":"Boolean","en":"Dock inside `parent` (absolute) instead of the window (fixed).","zh":"停靠在 `parent` 裡面（absolute），不是停靠在視窗（fixed）。"}],"returns":{"type":"{ el, view, isOpen, open(), close(), toggle(), dispose() }","en":"`el` is the panel element, `view` the [`ControllerView`](#ControllerView) (once it exists), `isOpen` a boolean. `open()`, `close()` and `toggle()` return the same object; `close()` also releases held notes.","zh":"`el` 是面板元素，`view` 是 [`ControllerView`](#ControllerView)（建立之後才有），`isOpen` 是布林值。`open()`、`close()`、`toggle()` 回傳同一個物件，`close()` 還會放開按著的音。"},"notes":[{"en":"The labels on the toolbar (Learn, Reset, the hardware chip) are written in Chinese only, whatever the page language is. `<oav-controller>` has an English and a Chinese toolbar; this panel is the one the lab and the phone tab use.","zh":"工具列上的文字是中文（學習、還原、硬體 …），不隨頁面語言改變。`<oav-controller>` 的工具列有英文和中文版，這個面板則是實驗室和手機分頁在用的那個。"}],"examples":[{"code":"import { MidiControllers, PROFILES, mountMidiPanel } from '@openav/midi';\n\nconst app = document.getElementById('app');\napp.style.cssText = 'position: relative; height: 320px';\n\nconst mcs = new MidiControllers({ profiles: PROFILES, initial: 'akai-lpd8' });\nconst panel = mountMidiPanel(app, mcs, { mode: 'full' });\n\nconsole.log(panel.isOpen, panel.view.constructor.name);   // true ControllerView\nmcs.current.press('pad2');                                  // the pad lights up in the panel\nsetTimeout(() => mcs.current.release('pad2'), 500);","caption":{"en":"A panel with the toolbar and the AKAI LPD8 appears in the page; pad 2 lights for half a second. Switch the device in the picker.","zh":"頁面上出現帶工具列的面板和 AKAI LPD8，pad 2 亮了半秒。可以在選單裡切換機器。"},"run":"module","lang":"js"}],"related":["MidiControllers","ControllerView","PANEL_CSS","createController"],"keywords":{"en":["panel","dock","controller panel","device picker","learn button","monitor","drawer"],"zh":["面板","停靠","控制器面板","機器選單","學習按鈕","監看","抽屜"]},"tbd":[],"claims":[]},
{"id":"OavController","key":"OavController","name":"OavController","aliases":[],"kind":"class","package":"@openav/midi","category":"controllers","documented":true,"signature":"new OavController()","import":"import { OavController } from '@openav/midi/element';","source":{"file":"packages/midi/element.js","line":82,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/element.js#L82"},"summary":{"en":"The `oav-controller` custom element: a playable MIDI controller you paste into any page with one line, with no import map and no build.","zh":"`<oav-controller>` 自訂元素：用一行就能貼進任何頁面的、可以彈的 MIDI 控制器，不需要 import map，也不需要建置。"},"description":[{"en":"Load `@openav/midi/element` (`https://openaudiovisual.com/packages/midi/element.js`) once and write `<oav-controller profile=\"akai-lpd8\" readout></oav-controller>`. Importing the file registers the tag by itself. It is a shadow-DOM element, so the page's CSS cannot bend it and it cannot bend the page. It plays with a mouse, a finger, a pen and the keyboard, with or without Web MIDI: on Safari and iPad it still plays and fires events, and only the hardware and MIDI-out parts say they are unavailable.","zh":"載入一次 `@openav/midi/element`（`https://openaudiovisual.com/packages/midi/element.js`），然後寫 `<oav-controller profile=\"akai-lpd8\" readout></oav-controller>`。匯入這個檔案時會自動註冊標籤。它是 shadow DOM 元素，所以頁面的 CSS 影響不到它，它也不會影響頁面。滑鼠、手指、觸控筆和鍵盤都能彈，有沒有 Web MIDI 都行：在 Safari 和 iPad 上仍然能彈、能觸發事件，只有硬體和 MIDI 輸出的部分會說「不能用」。"},{"en":"Attributes, each also a property of the same name (`midi-out` is `el.midiOut`): `profile` (an id, a short name or a URL to a profile JSON; default `arturia-minilab3`), `layout` (`auto`, `face` as it looks, `stack` re-gridded for a tall box), `hardware` (off by default; bare or `on` follows the real device, `ask` shows a button so the permission prompt waits for a click, or a port name or `/regex/`), `midi-out` (off by default; bare or `ask` lets the visitor pick, or a port name, substring or `/regex/` such as `IAC` or `loopMIDI`), `channel` (1 to 16: move every control to that channel), `picker`, `readout` (the last message under the faceplate), `learn` (a Learn button), `follow` (show whatever known device is plugged in), `theme` (`dark` or `light`: the toolbar only, the device stays the device), `telemetry` (`off` sends no anonymous hit) and `lang` (the toolbar language: `zh…` is Chinese, anything else English; it reads the nearest `lang` on the page).","zh":"屬性，每一個也是同名的屬性（property）（`midi-out` 對應 `el.midiOut`）：`profile`（id、短名稱，或指向設定檔 JSON 的網址，預設 `arturia-minilab3`）、`layout`（`auto`，`face` 是原本的樣子，`stack` 是為高的盒子重新排列）、`hardware`（預設關閉，只寫屬性或 `on` 是跟隨真實機器，`ask` 會顯示按鈕，讓權限詢問等到點擊才跳出，也可以寫埠名或 `/regex/`）、`midi-out`（預設關閉，只寫屬性或 `ask` 讓訪客自己挑，也可以寫埠名、子字串或 `/regex/`，例如 `IAC`、`loopMIDI`）、`channel`（1 到 16：把所有控制項移到那個頻道）、`picker`、`readout`（面板下面顯示最後一則訊息）、`learn`（學習按鈕）、`follow`（接上哪台認得的機器就顯示哪一台）、`theme`（`dark` 或 `light`：只影響工具列，機器還是機器的樣子）、`telemetry`（`off` 就不送匿名資料）和 `lang`（工具列語言：`zh…` 是中文，其他是英文，會讀頁面上最近的 `lang`）。"},{"en":"Events are `CustomEvent`s that bubble and cross the shadow root, so `el.addEventListener('control', e => e.detail)` works anywhere above it: `ready` `{ profile, name, controls }`, `control` (see [`controlDetail()`](#controlDetail)), `noteon`, `noteoff`, `connect`, `disconnect`, `status`, `profilechange` and `error` `{ message }`. [`EMBED_EVENTS`](#EMBED_EVENTS) lists them. Methods: `set()`, `get()`, `values()`, `reset()`, `press()`, `release()`, `ingest()`, `send()`, `on()`, `connectHardware()`, `disconnectHardware()`, `setOutput()` and `toggleLearn()`.","zh":"事件是會冒泡、也會穿過 shadow root 的 `CustomEvent`，所以在它上面任何一層用 `el.addEventListener('control', e => e.detail)` 都收得到：`ready` `{ profile, name, controls }`、`control`（見 [`controlDetail()`](#controlDetail)）、`noteon`、`noteoff`、`connect`、`disconnect`、`status`、`profilechange` 和 `error` `{ message }`。[`EMBED_EVENTS`](#EMBED_EVENTS) 列出了它們。方法有：`set()`、`get()`、`values()`、`reset()`、`press()`、`release()`、`ingest()`、`send()`、`on()`、`connectHardware()`、`disconnectHardware()`、`setOutput()`、`toggleLearn()`。"},{"en":"It fills its width, and its height follows the faceplate (a taller stack on a phone) unless you set one. CSS variables: `--oav-bg`, `--oav-fg`, `--oav-accent`, `--oav-line`, `--oav-font`, and `--oav-ratio` (width ÷ height). Parts: `::part(toolbar)`, `::part(body)`, `::part(readout)`.","zh":"它的寬度填滿容器，高度則跟著面板走（手機上會用比較高的直排），除非你自己設高度。CSS 變數：`--oav-bg`、`--oav-fg`、`--oav-accent`、`--oav-line`、`--oav-font`，以及 `--oav-ratio`（寬除以高）。可用的 part：`::part(toolbar)`、`::part(body)`、`::part(readout)`。"}],"params":[],"needs":{"en":"Wait for the controller to exist: `await el.ready` (a promise that resolves with the element), or listen for the `ready` event. Before that, `set()`, `press()`, `release()` and `ingest()` return `null` and print one console warning, and `get()` also returns `null`. It never asks for MIDI permission unless `hardware`, `midi-out` or Learn is used.","zh":"要等控制器建好：`await el.ready`（會以元素本身完成的 promise），或是監聽 `ready` 事件。在那之前，`set()`、`press()`、`release()`、`ingest()` 會回傳 `null` 並在主控台印出一次警告，`get()` 也回傳 `null`。除非用了 `hardware`、`midi-out` 或學習，否則它不會要 MIDI 權限。"},"notes":[{"en":"For a place that only takes an iframe, the same element is served at `/embed/controller/` (query parameters mirror the attributes). Hardware and MIDI out inside the frame need `allow=\"midi\"` on the `<iframe>`. See [`parseEmbedOptions()`](#parseEmbedOptions) and [`toParent()`](#toParent).","zh":"只能放 iframe 的地方，同一個元素也放在 `/embed/controller/`（查詢參數對應到屬性）。要在 frame 裡用硬體和 MIDI 輸出，`<iframe>` 要加上 `allow=\"midi\"`。見 [`parseEmbedOptions()`](#parseEmbedOptions) 和 [`toParent()`](#toParent)。"},{"en":"If a page imports both `@openav/midi` and `@openav/midi/element`, the browser loads `index.js` twice, because the element imports its own copy under a `?v=` stamp. Checks across the two copies fail: `el.core instanceof ControllerHost` is `false`. Talk to the element through its methods and events instead.","zh":"如果頁面同時匯入 `@openav/midi` 和 `@openav/midi/element`，瀏覽器會把 `index.js` 載入兩次，因為元素匯入的是自己那份、網址帶著 `?v=` 版本戳記。跨這兩份的檢查會失敗：`el.core instanceof ControllerHost` 是 `false`。請改用元素的方法和事件跟它溝通。"},{"en":"Like [`createController()`](#createController), it sends one anonymous hit per page load, never from localhost or the LAN. `telemetry=\"off\"` stops it.","zh":"跟 [`createController()`](#createController) 一樣，每次載入頁面會送一筆匿名資料，從 localhost 或區域網路則不送。`telemetry=\"off\"` 可以關掉。"}],"examples":[{"code":"// importing the file registers <oav-controller>\nimport '@openav/midi/element';\n\nconst app = document.getElementById('app');\napp.innerHTML = '<oav-controller profile=\"akai-lpd8\" readout></oav-controller>';\nconst el = app.firstElementChild;\n\nawait el.ready;                                    // the controller exists now\nel.on('control', (d) => console.log(d.signal, d.value.toFixed(2), d.source));\nel.set('k1', 0.5);                                 // turns knob 1 on the screen\nel.ingest([0xb0, 71, 127]);                        // MIDI from \"elsewhere\": knob 2 follows","caption":{"en":"An LPD8 appears with a message line under it. The console prints `midi/lpd8/k1 0.50 ui`, then `midi/lpd8/k2 1.00 hardware`. Drag the knobs or tap the pads.","zh":"出現一台 LPD8，下面有一行訊息列。主控台印出 `midi/lpd8/k1 0.50 ui`，接著是 `midi/lpd8/k2 1.00 hardware`。可以拖曳旋鈕或敲打擊墊。"},"run":"module","lang":"js"},{"code":"import '@openav/midi/element';\n\nconst app = document.getElementById('app');\napp.innerHTML = '<oav-controller profile=\"lpd8\" picker style=\"max-width: 420px\"></oav-controller>';\nconst el = app.firstElementChild;\nawait el.ready;\n\nel.addEventListener('profilechange', (e) => console.log('now showing', e.detail.name));\nel.profile = 'nano2';                              // the same as setAttribute('profile', 'nano2')\nconsole.log(el.status.hardware, el.channel);       // off null","caption":{"en":"The toolbar has a device picker. Setting `el.profile` switches the faceplate to the Korg nanoKONTROL2 and the console prints its name, then `off null`.","zh":"工具列上有機器選單。設定 `el.profile` 會把面板換成 KORG nanoKONTROL2，主控台印出它的名稱，再印出 `off null`。"},"run":"module","lang":"js"},{"code":"<script type=\"module\" src=\"https://openaudiovisual.com/packages/midi/element.js\"></script>\n\n<!-- plays on its own; the real device and MIDI out stay off until you ask -->\n<oav-controller profile=\"akai-lpd8\" readout></oav-controller>\n\n<!-- a button asks for the real device on click; a MIDI output named like IAC receives what you play -->\n<oav-controller profile=\"arturia-minilab3\" hardware=\"ask\" midi-out=\"IAC\" picker learn></oav-controller>","caption":{"en":"The two lines you paste into any page. The first plays with the mouse; the second shows a Connect button and a Learn button.","zh":"貼進任何頁面的就是這幾行。第一台用滑鼠就能彈，第二台會顯示連接按鈕和學習按鈕。"},"run":"static","lang":"html"}],"related":["defineController","createController","ControllerHost","parseEmbedOptions","EMBED_ATTRS","EMBED_EVENTS","controlDetail"],"keywords":{"en":["oav-controller","custom element","web component","embed controller","html tag","attributes","midi-out","hardware","shadow dom"],"zh":["oav-controller","自訂元素","web component","嵌入控制器","html 標籤","屬性","midi-out","硬體","shadow dom"]},"tbd":[{"en":"Not tried for this page: `hardware` and `midi-out` inside an iframe in each browser, and a real MIDI device behind the element. The `allow=\"midi\"` requirement is as the package README states it.","zh":"這一頁沒有實測：各個瀏覽器的 iframe 裡的 `hardware` 和 `midi-out`，以及元素背後接真實的 MIDI 機器。`allow=\"midi\"` 的要求是照套件 README 的說法寫的。"}],"claims":[],"members":[{"name":"connectedCallback","member":"method","signature":"","doc":"---------------------------------------------------------------- lifecycle","line":102},{"name":"disconnectedCallback","member":"method","signature":"","doc":"","line":103},{"name":"attributeChangedCallback","member":"method","signature":"name, was, now","doc":"","line":104},{"name":"set","member":"method","signature":"id, value, opts = {}","doc":"Move a control like a hand (pads / buttons: > 0 presses, 0 releases; keys: set('keys', vel, {note})).","line":288},{"name":"get","member":"method","signature":"id","doc":"","line":289},{"name":"values","member":"method","signature":"","doc":"","line":290},{"name":"reset","member":"method","signature":"opts","doc":"","line":291},{"name":"press","member":"method","signature":"id, velocity = 1, note = null","doc":"","line":292},{"name":"release","member":"method","signature":"id, note = null","doc":"","line":293},{"name":"ingest","member":"method","signature":"bytes, opts","doc":"MIDI in from anywhere (a WebSocket, another tab): the faceplate follows, `control` fires with source 'hardware'.","line":295},{"name":"send","member":"method","signature":"bytes","doc":"Raw bytes to the MIDI output port (when midi-out is open).","line":297},{"name":"on","member":"method","signature":"name, fn","doc":"Subscribe to an event by name; the callback gets the payload.","line":299},{"name":"connectHardware","member":"method","signature":"","doc":"","line":300},{"name":"disconnectHardware","member":"method","signature":"","doc":"","line":301},{"name":"setOutput","member":"method","signature":"query","doc":"","line":302},{"name":"toggleLearn","member":"method","signature":"on","doc":"","line":303},{"name":"core","member":"getter","signature":"","doc":"The headless controller (same API as createController()).","line":313},{"name":"controller","member":"getter","signature":"","doc":"The live model of the device on screen (MidiController).","line":315},{"name":"controllers","member":"getter","signature":"","doc":"Every device this element can show (MidiControllers).","line":317},{"name":"view","member":"getter","signature":"","doc":"The faceplate (ControllerView).","line":319},{"name":"midi","member":"getter","signature":"","doc":"The Web MIDI engine, once hardware or MIDI out is on.","line":321},{"name":"status","member":"getter","signature":"","doc":"{ hardware, input, output, outputState, profile }","line":323},{"name":"profileData","member":"getter","signature":"","doc":"The normalized profile on screen.","line":325},{"name":"learn","member":"getter","signature":"","doc":"","line":82},{"name":"picker","member":"getter","signature":"","doc":"","line":82},{"name":"readout","member":"getter","signature":"","doc":"","line":82},{"name":"follow","member":"getter","signature":"","doc":"","line":82},{"name":"profile","member":"getter","signature":"","doc":"","line":82},{"name":"layout","member":"getter","signature":"","doc":"","line":82},{"name":"theme","member":"getter","signature":"","doc":"","line":82},{"name":"hardware","member":"getter","signature":"","doc":"","line":82},{"name":"midiOut","member":"getter","signature":"","doc":"","line":82},{"name":"channel","member":"getter","signature":"","doc":"","line":82},{"name":"observedAttributes","member":"static property","signature":"","doc":"","line":83}]},
{"id":"PANEL_CSS","key":"PANEL_CSS","name":"PANEL_CSS","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"PANEL_CSS = \"\\n.mcp { position: absolute; inset: 0; display: flex; flex-direction: column; …\"","import":"import { PANEL_CSS } from '@openav/midi';","source":{"file":"packages/midi/panel.js","line":15,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/panel.js#L15"},"summary":{"en":"The stylesheet text of the controller panel (`mountMidiPanel`), as a string.","zh":"控制器面板（`mountMidiPanel`）的樣式表文字，是一個字串。"},"description":[{"en":"Advanced: it is exported for pages that want to read or override the rules. You never need it to mount a panel.","zh":"進階用途：匯出給想讀或覆寫規則的頁面。掛上面板時完全用不到它。"}],"value":{"type":"String","en":"CSS for the `.mcp…` classes. `mountMidiPanel()` injects it by itself, once per document (a `<style id=\"openav-midi-panel-css\">` in the head).","zh":"`.mcp…` 那些 class 的 CSS。`mountMidiPanel()` 會自己注入，每個文件一次（放在 head 的 `<style id=\"openav-midi-panel-css\">`）。"},"params":[],"notes":[],"examples":[{"code":"import { PANEL_CSS } from '@openav/midi';\n\nconsole.log(PANEL_CSS.includes('.mcp.dock'));   // the drawer rules","caption":{"en":"Prints `true`: the panel's rules are readable CSS text.","zh":"印出 `true`：面板的規則是看得懂的 CSS 文字。"},"run":"module","lang":"js"}],"related":["mountMidiPanel","VIEW_CSS"],"keywords":{"en":["css","panel style","dock style"],"zh":["css","面板樣式","停靠樣式"]},"tbd":[],"claims":[]},
{"id":"parseEmbedOptions","key":"parseEmbedOptions","name":"parseEmbedOptions","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"parseEmbedOptions(src)","import":"import { parseEmbedOptions } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":72,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L72"},"summary":{"en":"Reads the options of one embed from an element, a query string, an object or a getter function, and returns them cleaned up with defaults.","zh":"從元素、查詢字串、物件或取值函式讀出一個嵌入的選項，整理過並補上預設值後回傳。"},"description":[{"en":"It is the one place that decides what the attributes and the iframe's query string mean, so the element and the iframe always agree. Defaults: `profile` is `arturia-minilab3`, `layout` `auto`, `theme` `dark`, `hardware` and `midiOut` off (so embedding never asks for MIDI permission), the booleans false, `telemetry` true, and `channel`, `bg`, `frame`, `origin`, `lang` are `null`.","zh":"元素和 iframe 的查詢字串各代表什麼意思，都由它一個地方決定，所以兩邊一定一致。預設值：`profile` 是 `arturia-minilab3`，`layout` 是 `auto`，`theme` 是 `dark`，`hardware` 和 `midiOut` 是關閉（所以嵌入不會要 MIDI 權限），布林值都是 false，`telemetry` 是 true，`channel`、`bg`、`frame`、`origin`、`lang` 是 `null`。"},{"en":"Rules worth knowing: an unknown `layout` becomes `auto`; `theme` is `light` or `dark`; `midi-out` may also be written `midiout` or `midiOut`, and alone it means `ask`; `frame` may also be written `id` and is cut to 64 characters; `origin` is kept only as `https://host` or `http://host` with no path (anything else, such as `javascript:`, becomes `null`), `lang` is cut to 16 characters, and `bg` goes through [`embedColor()`](#embedColor).","zh":"值得知道的規則：不認得的 `layout` 變成 `auto`。`theme` 只有 `light` 或 `dark`。`midi-out` 也可以寫成 `midiout` 或 `midiOut`，單獨寫表示 `ask`。`frame` 也可以寫成 `id`，並且截成 64 個字元。`origin` 只接受沒有路徑的 `https://host` 或 `http://host`（其他的，像 `javascript:`，會變成 `null`）。`lang` 截成 16 個字元。`bg` 會經過 [`embedColor()`](#embedColor)。"}],"params":[{"name":"src","type":"Element | URLSearchParams | Object | Function","en":"Where the options live: an element (read with `getAttribute`), a `URLSearchParams` (the iframe page passes its own), a plain object `{ profile: 'lpd8' }`, or a function `(name) => value | null`.","zh":"選項放在哪裡：元素（用 `getAttribute` 讀）、`URLSearchParams`（iframe 頁面傳自己的）、普通物件 `{ profile: 'lpd8' }`，或是函式 `(name) => value | null`。"}],"returns":{"type":"Object","en":"`{ profile, layout, hardware, midiOut, channel, learn, picker, readout, follow, telemetry, theme, bg, frame, origin, lang }`. `hardware` and `midiOut` are `{ mode, port }` as in [`embedMode()`](#embedMode).","zh":"`{ profile, layout, hardware, midiOut, channel, learn, picker, readout, follow, telemetry, theme, bg, frame, origin, lang }`。`hardware` 和 `midiOut` 的格式是 [`embedMode()`](#embedMode) 那樣的 `{ mode, port }`。"},"notes":[],"examples":[{"code":"import { parseEmbedOptions } from '@openav/midi/embed';\n\nconst q = new URLSearchParams('profile=lpd8&layout=stack&hardware&midi-out=IAC&readout&bg=transparent&origin=https://example.com');\nconst o = parseEmbedOptions(q);\nconsole.log(o.profile, o.layout, o.hardware, o.midiOut);\nconsole.log(o.readout, o.bg, o.origin, o.channel);","caption":{"en":"Prints `lpd8 stack`, `hardware` as `{ mode: 'on', port: null }`, `midiOut` as `{ mode: 'on', port: 'IAC' }`, then `true transparent https://example.com null`.","zh":"印出 `lpd8 stack`、`hardware` 的 `{ mode: 'on', port: null }`、`midiOut` 的 `{ mode: 'on', port: 'IAC' }`，再印出 `true transparent https://example.com null`。"},"run":"module","lang":"js"},{"code":"import { parseEmbedOptions } from '@openav/midi/embed';\n\nconsole.log(JSON.stringify(parseEmbedOptions({})));                       // every default\nconst o = parseEmbedOptions({ 'midi-out': '', hardware: 'off' });\nconsole.log(JSON.stringify(o.midiOut), JSON.stringify(o.hardware));      // midiOut asks, hardware stays off\nconsole.log(parseEmbedOptions({ origin: 'javascript:alert(1)' }).origin);   // null","caption":{"en":"Prints every default as JSON, then `midiOut` as `{\"mode\":\"ask\",\"port\":null}` next to `hardware` as off, then `null`.","zh":"以 JSON 印出所有預設值，再印出 `{\"mode\":\"ask\",\"port\":null}` 的 `midiOut`（旁邊是關閉的 `hardware`），最後是 `null`。"},"run":"module","lang":"js"}],"related":["embedMode","embedFlag","embedChannel","embedColor","EMBED_ATTRS","OavController"],"keywords":{"en":["embed options","query string","attributes to options","iframe params","defaults"],"zh":["嵌入選項","查詢字串","屬性轉選項","iframe 參數","預設值"]},"tbd":[],"claims":[]},
{"id":"POST_SOURCE","key":"POST_SOURCE","name":"POST_SOURCE","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"POST_SOURCE = \"openav\"","import":"import { POST_SOURCE } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":268,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L268"},"summary":{"en":"The word that marks a message as an open-audiovisual embed message: `'openav'`, sent as `source` by the iframe and as `target` by the page.","zh":"標記一則訊息屬於 open-audiovisual 嵌入的字：`'openav'`，iframe 送出時放在 `source`，外層頁面送進去時放在 `target`。"},"description":[{"en":"[`toParent()`](#toParent) and [`toFrame()`](#toFrame) write it for you, and [`fromFrame()`](#fromFrame) and [`fromParent()`](#fromParent) check it.","zh":"[`toParent()`](#toParent) 和 [`toFrame()`](#toFrame) 會替你寫入，[`fromFrame()`](#fromFrame) 和 [`fromParent()`](#fromParent) 負責檢查。"}],"value":{"type":"String","range":"'openav'","en":"A constant. A message from the iframe has `source: 'openav'`, and a command to the iframe has `target: 'openav'`. Anything else is ignored on both sides.","zh":"常數。iframe 送出的訊息有 `source: 'openav'`，送進 iframe 的指令有 `target: 'openav'`。兩邊都會忽略其他任何訊息。"},"params":[],"notes":[],"examples":[{"code":"import { POST_SOURCE, toParent, toFrame } from '@openav/midi/embed';\n\nconsole.log(POST_SOURCE);                              // openav\nconsole.log(toParent('ready').source, toFrame('get').target);   // openav openav","caption":{"en":"Prints `openav`, then `openav openav`.","zh":"印出 `openav`，再印出 `openav openav`。"},"run":"module","lang":"js"}],"related":["POST_VERSION","toParent","toFrame","fromFrame","fromParent"],"keywords":{"en":["postMessage","openav","message source","iframe protocol"],"zh":["postMessage","openav","訊息來源","iframe 協定"]},"tbd":[],"claims":[]},
{"id":"POST_VERSION","key":"POST_VERSION","name":"POST_VERSION","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"POST_VERSION = 1","import":"import { POST_VERSION } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":269,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L269"},"summary":{"en":"The version of the iframe message format, `1`, written as `v` in every message.","zh":"iframe 訊息格式的版本，`1`，會寫成每則訊息裡的 `v`。"},"description":[{"en":"Messages from [`toParent()`](#toParent) and [`toFrame()`](#toFrame) carry it as `v: 1`.","zh":"[`toParent()`](#toParent) 和 [`toFrame()`](#toFrame) 的訊息都會帶著 `v: 1`。"}],"value":{"type":"Number","range":"1","en":"A constant, so a page can tell when the format changes. [`fromFrame()`](#fromFrame) gives `v` back (1 when a message lacks it).","zh":"常數，讓頁面在格式改變時能分辨。[`fromFrame()`](#fromFrame) 會把 `v` 還給你（訊息沒有時是 1）。"},"params":[],"notes":[],"examples":[{"code":"import { POST_VERSION, toParent } from '@openav/midi/embed';\n\nconsole.log(POST_VERSION, toParent('ready').v);   // 1 1","caption":{"en":"Prints `1 1`.","zh":"印出 `1 1`。"},"run":"module","lang":"js"}],"related":["POST_SOURCE","toParent","fromFrame"],"keywords":{"en":["protocol version","message format version"],"zh":["協定版本","訊息格式版本"]},"tbd":[],"claims":[]},
{"id":"preferredAspect","key":"preferredAspect","name":"preferredAspect","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"preferredAspect(profile, width, { layout = 'auto', minTouch = 38 } = {})","import":"import { preferredAspect } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":251,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L251"},"summary":{"en":"Works out the width ÷ height a controller wants in a box of a given width: its faceplate when every control stays finger-sized, a taller stack otherwise.","zh":"算出控制器在某個寬度的盒子裡想要的寬高比：每個控制項都夠手指按的話用面板本身的比例，否則用比較高的直排。"},"description":[{"en":"`<oav-controller>` uses it to set its height, so a controller sizes itself from its width. In `auto`, the faceplate's own proportions are used while the smallest control stays at least `minTouch` pixels at that width; otherwise it becomes a stack, about 0.8 to 1.5 times as tall as it is wide depending on how many bands the profile's `portrait` has. Square grid devices (the Launchpad, `stack: false`) always keep their face.","zh":"`<oav-controller>` 用它來決定自己的高度，所以控制器會依寬度自己決定大小。在 `auto` 下，只要在那個寬度時最小的控制項仍有至少 `minTouch` 像素，就用面板本身的比例，否則改成直排，高度大約是寬度的 0.8 到 1.5 倍，視設定檔 `portrait` 有幾條而定。方形格子的機器（Launchpad，`stack: false`）永遠維持 `face`。"}],"params":[{"name":"profile","type":"Object","en":"A normalized profile (`face`, `sections`, `portrait`, `stack`).","zh":"整理過的設定檔（`face`、`sections`、`portrait`、`stack`）。"},{"name":"width","type":"Number","en":"The box width in pixels.","zh":"盒子的寬度（像素）。"},{"name":"layout","type":"String","en":"`'auto'`, `'face'` or `'stack'`.","zh":"`'auto'`、`'face'` 或 `'stack'`。"},{"name":"minTouch","type":"Number","en":"The smallest control size in pixels that still counts as finger-sized.","zh":"還算夠手指按的最小控制項尺寸（像素）。"}],"returns":{"type":"{ mode, aspect }","en":"`mode` is `'face'` or `'stack'`, and `aspect` is the width ÷ height to give the box.","zh":"`mode` 是 `'face'` 或 `'stack'`，`aspect` 是要給盒子的寬高比。"},"notes":[],"examples":[{"code":"import { normalizeProfile, profileById } from '@openav/midi';\nimport { preferredAspect } from '@openav/midi/embed';\n\nconst lpd8 = normalizeProfile(profileById('lpd8'));\nconsole.log(preferredAspect(lpd8, 1200));                      // { mode: 'face', aspect: 3.70... }\nconsole.log(preferredAspect(lpd8, 340));                       // { mode: 'stack', aspect: 0.92... } : a phone\nconsole.log(preferredAspect(lpd8, 340, { layout: 'face' }));   // forced back to the face\n\nconst launchpad = normalizeProfile(profileById('lpmini'));\nconsole.log(preferredAspect(launchpad, 300));                  // { mode: 'face', aspect: 1 } : always square","caption":{"en":"Prints four results: a wide face, a tall stack for a narrow box, the face when forced, and the square Launchpad.","zh":"印出四個結果：寬的面板、窄盒子用的高直排、強制指定時的面板，以及方形的 Launchpad。"},"run":"module","lang":"js"}],"related":["OavController","EMBED_LAYOUTS","ControllerView"],"keywords":{"en":["aspect ratio","height from width","responsive","phone layout","size"],"zh":["寬高比","由寬度算高度","響應式","手機版面","大小"]},"tbd":[],"claims":[]},
{"id":"restValue","key":"restValue","name":"restValue","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"restValue(c)","import":"import { restValue } from '@openav/midi/embed';","source":{"file":"packages/midi/embed.js","line":238,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L238"},"summary":{"en":"Says where a control rests: knobs and faders at their default (0), bipolar wheels at the centre, pads, buttons and keys at 0.","zh":"說明控制項休息時在哪裡：旋鈕和推桿在預設值（0），雙向彎音輪在中心，打擊墊、按鈕和琴鍵在 0。"},"description":[{"en":"`reset()` on [`ControllerHost`](#ControllerHost) uses it to bring every control home. An endless encoder has no real position, so only its drawn position goes to the middle and no MIDI is sent for it.","zh":"[`ControllerHost`](#ControllerHost) 的 `reset()` 用它讓每個控制項回到原位。無限旋鈕沒有真正的位置，所以只有畫面上的位置會回到中間，而且不會送出 MIDI。"}],"params":[{"name":"c","type":"Object","en":"A control definition from a normalized profile.","zh":"整理過的設定檔中的控制項定義。"}],"returns":{"type":"Number","en":"0 for keys, pads, buttons and bipolar controls; otherwise the control's `def`, else 0.5 for an endless encoder and 0 for the rest.","zh":"琴鍵、打擊墊、按鈕和雙向控制項是 0。其他用控制項的 `def`，沒有的話無限旋鈕是 0.5，其餘是 0。"},"notes":[],"examples":[{"code":"import { normalizeProfile, profileById } from '@openav/midi';\nimport { restValue } from '@openav/midi/embed';\n\nconst p = normalizeProfile(profileById('minilab3'));\nfor (const id of ['knob1', 'pitch', 'enc', 'pad1', 'keys']) {\n  console.log(id, restValue(p.controls.find((c) => c.id === id)));\n}","caption":{"en":"Prints 0 for the knob, the pitch strip (centre), the pad and the keys, and 0.5 for the endless encoder.","zh":"旋鈕、音高觸控條（中心）、打擊墊和琴鍵印出 0，無限旋鈕印出 0.5。"},"run":"module","lang":"js"}],"related":["setControl","ControllerHost"],"keywords":{"en":["rest position","home","reset controls","default value"],"zh":["休息位置","歸位","重設控制項","預設值"]},"tbd":[],"claims":[]},
{"id":"setControl","key":"setControl","name":"setControl","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"setControl(mc, id, value, { note = null, source = 'ui' } = {})","import":"import { setControl } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":221,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L221"},"summary":{"en":"Moves one control the way a hand would, whatever its type, and returns the MIDI bytes it sent: `setControl(mc, 'k1', 0.5)`.","zh":"像一隻手一樣去動某個控制項，不論它是什麼類型，並回傳它送出的 MIDI 位元組：`setControl(mc, 'k1', 0.5)`。"},"description":[{"en":"What `value` means depends on the type. Knob, fader, wheel, strip and encoder: 0..1 (−1..1 for a bipolar one). Pad and momentary button: above 0 presses (with that velocity, held to 1) and 0 releases. Toggle button: 0.5 or more is on. Keys: `value` is the velocity, above 0 presses that `note` and 0 releases it. This is what `set()` on [`ControllerHost`](#ControllerHost) and `<oav-controller>` call.","zh":"`value` 的意思依類型而定。旋鈕、推桿、彎音輪、觸控條、編碼器：0..1（雙向的是 −1..1）。打擊墊和瞬間按鈕：大於 0 是按下（用那個力度，上限 1），0 是放開。切換按鈕：0.5 以上是開。琴鍵：`value` 是力度，大於 0 按下那個 `note`，0 放開它。`ControllerHost` 和 `<oav-controller>` 的 `set()` 呼叫的就是它。"}],"params":[{"name":"mc","type":"MidiController","en":"The controller.","zh":"控制器。"},{"name":"id","type":"String","en":"The control id. An unknown id throws `<profile> has no control \"x\" (controls: …)`.","zh":"控制項 id。不存在的 id 會丟出 `<profile> has no control \"x\" (controls: …)`。"},{"name":"value","type":"Number","en":"The value (below). Anything that is not a finite number throws `set(\"id\"): value must be a number`.","zh":"數值（見下）。不是有限數字的值會丟出 `set(\"id\"): value must be a number`。"},{"name":"note","type":"Number | null","en":"For a keys control: which note. The default is the first key of its range.","zh":"琴鍵控制項要給：哪個音。預設是音域的第一個鍵。"},{"name":"source","type":"String","en":"`'ui'` (default) publishes and reaches MIDI out. `'mirror'` moves only the faceplate (two-way binding).","zh":"`'ui'`（預設）會發佈並送到 MIDI 輸出。`'mirror'` 只動面板（雙向綁定用）。"}],"returns":{"type":"number[] | null","en":"The bytes sent, or `null` when nothing changed (for example releasing a pad that is not held).","zh":"送出的位元組，沒有變化時（例如放開一個沒按著的打擊墊）回傳 `null`。"},"notes":[],"examples":[{"code":"import { MidiController, profileById } from '@openav/midi';\nimport { setControl } from '@openav/midi/embed';\n\nconst mc = new MidiController(profileById('lpd8'));\nconsole.log(setControl(mc, 'k1', 0.5));      // [176, 70, 64]  : a knob\nconsole.log(setControl(mc, 'pad1', 1));      // [153, 36, 127] : a pad pressed\nconsole.log(setControl(mc, 'pad1', 0));      // [137, 36, 0]   : released\nconsole.log(setControl(mc, 'pad1', 0));      // null           : nothing to release","caption":{"en":"Prints the three messages the control types send, then `null`.","zh":"印出幾種控制項送出的三則訊息，最後是 `null`。"},"run":"module","lang":"js"}],"related":["valuesOf","MidiController","ControllerHost","restValue"],"keywords":{"en":["set control","move a knob","press a pad","simulate hand","send value"],"zh":["設定控制項","轉旋鈕","按打擊墊","模擬手","送出數值"]},"tbd":[],"claims":[]},
{"id":"toFrame","key":"toFrame","name":"toFrame","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"toFrame(type, args = {})","import":"import { toFrame } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":285,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L285"},"summary":{"en":"Page side: builds a command to post into an embed iframe: `{ target: 'openav', v: 1, type, ...args }`.","zh":"外層頁面這一側：組出要送進嵌入 iframe 的指令：`{ target: 'openav', v: 1, type, ...args }`。"},"description":[{"en":"It does not check the command: the iframe does, with [`fromParent()`](#fromParent), and ignores what it does not understand. Post to the frame's real origin, not `*`, when you can.","zh":"它不檢查指令內容：iframe 會用 [`fromParent()`](#fromParent) 檢查，不懂的就忽略。可以的話請送給 frame 真正的來源，不要用 `*`。"}],"params":[{"name":"type","type":"String","en":"The command: one of [`FRAME_COMMANDS`](#FRAME_COMMANDS).","zh":"指令：[`FRAME_COMMANDS`](#FRAME_COMMANDS) 其中之一。"},{"name":"args","type":"Object","en":"The command's fields (`{ id, value }` for `set`). Copied through JSON.","zh":"指令的欄位（`set` 是 `{ id, value }`）。會經過 JSON 複製。"}],"returns":{"type":"Object","en":"A message ready for `iframe.contentWindow.postMessage(msg, origin)`.","zh":"可以直接給 `iframe.contentWindow.postMessage(msg, origin)` 的訊息。"},"notes":[],"examples":[{"code":"import { toFrame } from '@openav/midi/embed';\n\nconsole.log(JSON.stringify(toFrame('set', { id: 'k1', value: 0.5 })));\nconsole.log(JSON.stringify(toFrame('ingest', { bytes: [0xb0, 70, 64] })));\nconsole.log(JSON.stringify(toFrame('profile', { profile: 'korg-nanokontrol2' })));\n// iframe.contentWindow.postMessage(toFrame('get'), 'https://openaudiovisual.com');","caption":{"en":"Prints three command messages; the last line (commented out) is how you would send one.","zh":"印出三則指令訊息，最後一行（被註解掉的）是實際送出一則的做法。"},"run":"module","lang":"js"}],"related":["fromParent","toParent","FRAME_COMMANDS","POST_SOURCE"],"keywords":{"en":["postMessage command","control the iframe","page to iframe","send command"],"zh":["postMessage 指令","控制 iframe","頁面傳給 iframe","送出指令"]},"tbd":[],"claims":[]},
{"id":"toParent","key":"toParent","name":"toParent","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"toParent(type, detail = null, { frame = null } = {})","import":"import { toParent } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":276,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L276"},"summary":{"en":"Frame side: builds the message the iframe posts to its page for an event: `{ source: 'openav', v: 1, type, frame, detail }`.","zh":"iframe 這一側：為一個事件組出 iframe 要送給外層頁面的訊息：`{ source: 'openav', v: 1, type, frame, detail }`。"},"description":[{"en":"The iframe page calls `parent.postMessage(toParent(name, detail, { frame }), target)` for every event, where `target` is the `?origin=` parameter or `*`. With `?origin=https://your.site` the frame posts only to that origin and accepts commands only from it.","zh":"iframe 頁面對每個事件都呼叫 `parent.postMessage(toParent(name, detail, { frame }), target)`，`target` 是 `?origin=` 參數，沒有就是 `*`。加上 `?origin=https://your.site` 之後，frame 只會送給那個來源，也只接受從它來的指令。"}],"params":[{"name":"type","type":"String","en":"The event name (see [`EMBED_EVENTS`](#EMBED_EVENTS)), or `values`.","zh":"事件名稱（見 [`EMBED_EVENTS`](#EMBED_EVENTS)），或 `values`。"},{"name":"detail","type":"any","en":"The event payload. It is copied through JSON, so the message is always `postMessage`-safe (`undefined` becomes `null`).","zh":"事件內容。會經過 JSON 複製，所以訊息一定能安全地 `postMessage`（`undefined` 變成 `null`）。"},{"name":"frame","type":"String | null","en":"A name for this iframe (the `?frame=` parameter), echoed in every message so a page with several frames can tell them apart.","zh":"這個 iframe 的名字（`?frame=` 參數），每則訊息都會帶著，這樣頁面上有好幾個 frame 時才分得出來。"}],"returns":{"type":"Object","en":"The message `{ source: 'openav', v: 1, type, frame, detail }`.","zh":"訊息物件 `{ source: 'openav', v: 1, type, frame, detail }`。"},"notes":[],"examples":[{"code":"import { toParent } from '@openav/midi/embed';\n\nconst msg = toParent('control', { id: 'k1', value: 0.5 }, { frame: 'pads' });\nconsole.log(JSON.stringify(msg));               // {\"source\":\"openav\",\"v\":1,\"type\":\"control\",\"frame\":\"pads\",\"detail\":{\"id\":\"k1\",\"value\":0.5}}\nconsole.log(JSON.stringify(toParent('ready')));  // frame and detail are null","caption":{"en":"Prints the full message for a `control` event, then a bare `ready` message with `frame` and `detail` as `null`.","zh":"印出 `control` 事件的完整訊息，再印出沒有 `frame` 和 `detail` 的單純 `ready` 訊息（兩者都是 `null`）。"},"run":"module","lang":"js"}],"related":["fromFrame","toFrame","fromParent","POST_SOURCE","EMBED_EVENTS"],"keywords":{"en":["postMessage","iframe to page","embed message","frame name"],"zh":["postMessage","iframe 傳給頁面","嵌入訊息","frame 名稱"]},"tbd":[],"claims":[]},
{"id":"touchText","key":"touchText","name":"touchText","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"touchText(c, st, note = null)","import":"import { touchText } from '@openav/midi';","source":{"file":"packages/midi/canvas.js","line":167,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/canvas.js#L167"},"summary":{"en":"Writes what a device's screen says about a touch: `Knob 1 · 87`, `C4 · 100`, `Pad 3 · 96`, `Pitch +0.42`.","zh":"寫出機器的螢幕會怎麼描述一次觸碰：`Knob 1 · 87`、`C4 · 100`、`Pad 3 · 96`、`Pitch +0.42`。"},"description":[{"en":"[`ControllerCanvas`](#ControllerCanvas) draws this text on the device's screen (MiniLab 3, KeyStep) so a video shows what the hardware's own screen would. It is pure, so it is easy to reuse for a caption or a log.","zh":"[`ControllerCanvas`](#ControllerCanvas) 把這段文字畫在機器的螢幕上（MiniLab 3、KeyStep），讓影片裡看得到實機螢幕會顯示的內容。它是純函式，拿去當字幕或記錄也很方便。"}],"params":[{"name":"c","type":"Object","en":"The control's definition (from the normalized profile). `null` gives an empty string.","zh":"控制項的定義（來自整理過的設定檔）。給 `null` 會回傳空字串。"},{"name":"st","type":"Object","en":"Its state: `{ raw, value, held, pos }`.","zh":"它的狀態：`{ raw, value, held, pos }`。"},{"name":"note","type":"Number | null","en":"For a keys control, the note that was touched.","zh":"琴鍵控制項要給：被碰到的音符。"}],"returns":{"type":"String","en":"The line. A control whose label is only a number is named after its type (`Knob 1`). A pad shows its velocity only while held, a button shows `on` or `off`, a relative encoder shows its position as a percentage, and a bipolar wheel shows a signed value.","zh":"那一行文字。標籤只是數字的控制項，會用類型來命名（`Knob 1`）。打擊墊只有按著時才顯示力度，按鈕顯示 `on` 或 `off`，相對旋鈕以百分比顯示位置，雙向彎音輪則顯示有正負號的值。"},"notes":[],"examples":[{"code":"import { touchText, normalizeProfile, profileById } from '@openav/midi';\n\nconst p = normalizeProfile(profileById('minilab3'));\nconst c = (id) => p.controls.find((x) => x.id === id);\n\nconsole.log(touchText(c('knob1'), { raw: 87 }));                // Knob 1 · 87\nconsole.log(touchText(c('keys'), { raw: 100 }, 60));            // C4 · 100\nconsole.log(touchText(c('pad3'), { held: true, raw: 96 }));     // Pad 3 · 96\nconsole.log(touchText(c('pitch'), { value: -0.5 }));            // Pitch −0.50","caption":{"en":"Prints the four lines shown in the comments.","zh":"印出註解裡的那四行。"},"run":"module","lang":"js"}],"related":["ControllerCanvas","midiNoteName"],"keywords":{"en":["device screen text","last touched","caption","knob 1 87"],"zh":["機器螢幕文字","最後碰觸","字幕","knob 1 87"]},"tbd":[],"claims":[]},
{"id":"valuesOf","key":"valuesOf","name":"valuesOf","aliases":[],"kind":"function","package":"@openav/midi","category":"controllers","documented":true,"signature":"valuesOf(mc)","import":"import { valuesOf } from '@openav/midi';","source":{"file":"packages/midi/embed.js","line":206,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/embed.js#L206"},"summary":{"en":"Reads every control's current value from a controller as `{ id: number }`; for keys it is 1 while any key is held.","zh":"從控制器讀出每個控制項目前的值，格式是 `{ id: number }`，琴鍵控制項在有任何一個鍵按著時是 1。"},"description":[{"en":"This is what `values()` returns on [`ControllerHost`](#ControllerHost) and `<oav-controller>`, and what the iframe answers to the `get` command.","zh":"[`ControllerHost`](#ControllerHost) 和 `<oav-controller>` 的 `values()` 回傳的就是它，iframe 對 `get` 指令的回答也是它。"}],"params":[{"name":"mc","type":"MidiController","en":"The controller.","zh":"控制器。"}],"returns":{"type":"Object","en":"One number per control id, in the profile's order.","zh":"每個控制項 id 一個數字，順序跟設定檔一致。"},"notes":[],"examples":[{"code":"import { MidiController, profileById } from '@openav/midi';\nimport { valuesOf } from '@openav/midi/embed';\n\nconst mc = new MidiController(profileById('lpd8'));\nmc.setValue('k1', 1);\nmc.press('pad2', 0.5);\nconst values = valuesOf(mc);\nconsole.log(Object.keys(values).length, 'controls');\nconsole.log('k1 =', values.k1, '| pad2 =', values.pad2.toFixed(2), '| pad1 =', values.pad1);","caption":{"en":"Prints `16 controls`, then `k1 = 1 | pad2 = 0.50 | pad1 = 0`: one number for every control of the LPD8.","zh":"印出 `16 controls`，再印出 `k1 = 1 | pad2 = 0.50 | pad1 = 0`：LPD8 的每個控制項各有一個數字。"},"run":"module","lang":"js"}],"related":["setControl","ControllerHost"],"keywords":{"en":["all values","snapshot","current state","values object"],"zh":["所有數值","快照","目前狀態","數值物件"]},"tbd":[],"claims":[]},
{"id":"VIEW_CSS","key":"VIEW_CSS","name":"VIEW_CSS","aliases":[],"kind":"constant","package":"@openav/midi","category":"controllers","documented":true,"signature":"VIEW_CSS = \"\\n.mcv { position: relative; width: 100%; height: 100%; overflow: hidden; touc…\"","import":"import { VIEW_CSS } from '@openav/midi';","source":{"file":"packages/midi/view.js","line":32,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/midi/view.js#L32"},"summary":{"en":"The stylesheet text of the on-screen controller (`ControllerView`), as a string.","zh":"螢幕控制器（`ControllerView`）的樣式表文字，是一個字串。"},"description":[{"en":"Advanced. You never need it to show a controller. It is exported for pages that build their own shadow DOM, a server-rendered preview or a theme that wants to read the rules. Colours use the CSS variables `--oav-focus`, `--body`, `--c` and `--cap` that the view sets per control.","zh":"進階用途。要顯示控制器的話完全用不到它。它是匯出給「自己建 shadow DOM 的頁面、伺服器端渲染的預覽、想讀規則的佈景」用的。顏色用的 CSS 變數有 `--oav-focus`、`--body`、`--c` 和 `--cap`，其中後三個由畫面針對每個控制項設定。"}],"value":{"type":"String","en":"CSS for the `.mcv…` classes. The view injects it by itself, once per document, or once per shadow root when it lives inside one (a `<style id=\"openav-midi-view-css\">`).","zh":"`.mcv…` 那些 class 的 CSS。畫面會自己注入，每個文件注入一次，放在 shadow root 裡時則是每個 root 一次（一個 `<style id=\"openav-midi-view-css\">`）。"},"params":[],"notes":[],"examples":[{"code":"import { VIEW_CSS } from '@openav/midi';\n\nconsole.log(typeof VIEW_CSS, VIEW_CSS.length > 1000);\nconsole.log(VIEW_CSS.includes('.mcv-pad'));   // the rules for pads","caption":{"en":"Prints `string true`, then `true`: it is plain CSS text you can read or inject yourself.","zh":"印出 `string true`，再印出 `true`：它就是純 CSS 文字，可以自己讀或自己注入。"},"run":"module","lang":"js"}],"related":["ControllerView","PANEL_CSS"],"keywords":{"en":["css","stylesheet","shadow dom","view style"],"zh":["css","樣式表","shadow dom","畫面樣式"]},"tbd":[],"claims":[]},
{"id":"KeysPiano","key":"KeysPiano","name":"KeysPiano","aliases":[],"kind":"class","package":"@openav/keys","category":"keys","documented":true,"signature":"new KeysPiano(container, opts = {})","import":"import { KeysPiano } from '@openav/keys';","source":{"file":"packages/keys/index.js","line":48,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/keys/index.js#L48"},"summary":{"en":"An on-screen piano you can play with the mouse, a finger or the computer keyboard, that calls a function for every note.","zh":"一架螢幕上的鋼琴，可以用滑鼠、手指或電腦鍵盤彈，每彈一個音就呼叫你的函式。"},"description":[{"en":"`new KeysPiano(el, { onNote })` builds the keys inside `el`. Whatever plays a key (a click or a touch on it, sweeping across the keys with the button held, the computer keyboard, or your own call to `press()`) ends up in `onNote(note, vel01, on)`: `on` is `true` for a key going down and `false` for one coming up, and `vel01` is 0..1 (0 on the way up). The black and white pattern follows the real pitch, so a piano can start on any note. A key that is already down ignores a second `press()`.","zh":"`new KeysPiano(el, { onNote })` 在 `el` 裡蓋出琴鍵。不管是誰彈了一個鍵（在上面點一下或觸碰、按著按鈕滑過琴鍵、電腦鍵盤，或你自己呼叫 `press()`），最後都會進到 `onNote(note, vel01, on)`：`on` 是 `true` 表示鍵按下去，`false` 表示放開，`vel01` 是 0..1（放開時是 0）。黑白鍵的排法照真正的音高走，所以鋼琴可以從任何一個音開始。已經按著的鍵再 `press()` 會被忽略。"},{"en":"The computer keyboard is off until you set `piano.captureEnabled = true`, and it is up to you to pass the page's `keydown` and `keyup` events to [`handleKeyDown(e)`](#KeysPiano) and `handleKeyUp(e)` (see [`PIANO_KEYMAP`](#PIANO_KEYMAP) for which letter is which note). [`mountKeys()`](#mountKeys) does all of that for you.","zh":"電腦鍵盤一開始是關的，要自己設 `piano.captureEnabled = true`，並且由你把頁面的 `keydown` 和 `keyup` 事件交給 `handleKeyDown(e)` 和 `handleKeyUp(e)`（哪個字母是哪個音見 [`PIANO_KEYMAP`](#PIANO_KEYMAP)）。[`mountKeys()`](#mountKeys) 把這些全部替你做了。"}],"params":[{"name":"container","type":"HTMLElement","en":"The element the keys are built in. It gets the class `oav-keys` and is emptied first.","zh":"要把琴鍵蓋在裡面的元素。它會得到 `oav-keys` 這個 class，並且先被清空。"},{"name":"opts","type":"Object","optional":true,"default":"{}","en":"Options: `base` (lowest key, default 48), `octaves` (default 2), `semitones` (the number of keys, default `octaves × 12 + 1`), `velocity` (0..127 for the mouse and the computer keyboard, default 100), `onNote(note, vel01, on)`, `onOctave(base)`, `fill` (size the keys in percent of the container, so it can be any width), `keyWidth` (px per white key when not filling, default 27), `minBase` (24) and `maxBase` (84), the range the octave keys can move `base` in.","zh":"選項：`base`（最低的鍵，預設 48）、`octaves`（預設 2）、`semitones`（鍵的數量，預設 `octaves × 12 + 1`）、`velocity`（滑鼠和電腦鍵盤的力道，0..127，預設 100）、`onNote(note, vel01, on)`、`onOctave(base)`、`fill`（琴鍵用容器的百分比算大小，所以可以是任何寬度）、`keyWidth`（不 fill 的時候每個白鍵幾像素，預設 27）、`minBase`（24）和 `maxBase`（84），也就是八度鍵能移動 `base` 的範圍。"}],"notes":[{"en":"`handleKeyDown(e)` returns `true` when the piano used the event, and then you should call `e.preventDefault()`. While capturing, every letter `a`–`z` counts as used, even those that play no note, so a stray letter cannot trigger a hotkey; digits, space and the arrows pass through. Held keys with `Ctrl`, `Cmd` or `Alt` are never used.","zh":"鋼琴用掉這個事件的時候，`handleKeyDown(e)` 會回傳 `true`，那時你要呼叫 `e.preventDefault()`。在擷取鍵盤的時候，每個字母 `a`–`z` 都算被用掉，連不發音的也是，這樣打錯的字母不會觸發快捷鍵。數字、空白鍵、方向鍵會放行。有按著 `Ctrl`、`Cmd`、`Alt` 的組合鍵永遠不算。"}],"examples":[{"code":"import { KeysPiano } from '@openav/keys';\n\nconst piano = new KeysPiano(document.getElementById('app'), {\n  base: 60, octaves: 1,\n  onNote: (note, vel, on) => console.log(on ? 'on' : 'off', piano.noteName(note), vel.toFixed(2)),\n});\npiano.press(64, 0.5);     // on E4 0.50\npiano.release(64);        // off E4 0.00","caption":{"en":"A one-octave piano from middle C appears on the page. Click its keys and the console logs each note by name.","zh":"頁面上出現一架從中央 C 開始、一個八度的鋼琴。點它的琴鍵，console 就用音名印出每個音。"},"run":"module","lang":"js"},{"code":"import { KeysPiano } from '@openav/keys';\n\nconst piano = new KeysPiano(document.getElementById('app'), {\n  fill: true,                                     // as wide as the container\n  onNote: (note, vel, on) => { if (on) console.log('note', note); },\n});\npiano.captureEnabled = true;                      // the computer keyboard plays\nwindow.addEventListener('keydown', (e) => { if (piano.handleKeyDown(e)) e.preventDefault(); });\nwindow.addEventListener('keyup', (e) => piano.handleKeyUp(e));\nwindow.addEventListener('blur', () => piano.releaseAll());","caption":{"en":"Click the page, then press A, S, D, F… to play from C3, Z and X to change octave. The letter keys no longer reach the rest of the page.","zh":"點一下頁面，然後按 A、S、D、F…從 C3 開始彈，用 Z 和 X 換八度。字母鍵不會再傳到頁面的其他地方。"},"run":"module","lang":"js"}],"related":["mountKeys","SimPlayer","PIANO_KEYMAP","notePressed","noteName"],"keywords":{"en":["piano","keyboard","on-screen","keys","qwerty","press","release","glissando","touch","virtual piano"],"zh":["鋼琴","鍵盤","螢幕鋼琴","琴鍵","電腦鍵盤","按下","放開","滑音","觸控"]},"tbd":[],"claims":[],"members":[{"name":"noteName","member":"method","signature":"n","doc":"","line":74},{"name":"setBase","member":"method","signature":"n","doc":"","line":119},{"name":"press","member":"method","signature":"note, vel01","doc":"","line":126},{"name":"release","member":"method","signature":"note","doc":"","line":132},{"name":"releaseAll","member":"method","signature":"","doc":"","line":137},{"name":"handleKeyDown","member":"method","signature":"e","doc":"Returns true when the piano consumed the event (caller: preventDefault + return).","line":142},{"name":"handleKeyUp","member":"method","signature":"e","doc":"","line":158}]},
{"id":"mountKeys","key":"mountKeys","name":"mountKeys","aliases":[],"kind":"function","package":"@openav/keys","category":"keys","documented":true,"signature":"mountKeys(container, { signals, chord = null, base = 48, octaves = 2, sim = true, capture = true } = {})","import":"import { mountKeys } from '@openav/keys';","source":{"file":"packages/keys/index.js","line":229,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/keys/index.js#L229"},"summary":{"en":"Puts a playable piano on the page in one call, with a keyboard switch and a self-playing performer, and publishes every note as `midi/note/on` and `midi/note/off`.","zh":"一個呼叫就在頁面上放一架可以彈的鋼琴，附電腦鍵盤開關和會自己演奏的演奏者，並把每個音發布成 `midi/note/on` 和 `midi/note/off`。"},"description":[{"en":"`mountKeys()` is the whole keyboard department in one line: a [`KeysPiano`](#KeysPiano) you can play with the mouse, a finger or the computer keyboard, and a [`SimPlayer`](#SimPlayer) that plays by itself. Above the piano are two checkboxes, both off to begin with: **keyboard** (the QWERTY keys play, `Z` and `X` move an octave, `Shift` plays harder) and **simulate performance**.","zh":"`mountKeys()` 一行就把整個鍵盤部門裝好：一架可以用滑鼠、手指或電腦鍵盤彈的 [`KeysPiano`](#KeysPiano)，加上一個會自己彈的 [`SimPlayer`](#SimPlayer)。鋼琴上面有兩個勾選框，一開始都沒勾：**keyboard**（QWERTY 鍵能彈，`Z` 和 `X` 換八度，`Shift` 彈得比較重）和 **simulate performance**。"},{"en":"Everything is published as the standard `midi/note/on` and `midi/note/off`, the same names a real MIDI keyboard uses, so a world, a sound engine or a chord detector cannot tell a QWERTY performance from hardware. Call `keys.update(dt)` once per frame, or the simulated performer does not move.","zh":"所有東西都發布成標準的 `midi/note/on` 和 `midi/note/off`，跟真的 MIDI 鍵盤用同樣的名字，所以世界、聲音引擎或和弦偵測器分不出 QWERTY 演奏和硬體的差別。每個畫面要呼叫一次 `keys.update(dt)`，不然模擬演奏者不會動。"}],"params":[{"name":"container","type":"HTMLElement","en":"The element to put the piano in. It gets a small bar of checkboxes and the piano under it. `dispose()` empties it again.","zh":"要放鋼琴的元素。裡面會有一條小小的勾選列，和它底下的鋼琴。`dispose()` 會再把它清空。"},{"name":"signals","type":"Signals","optional":true,"en":"The [`Signals`](#Signals) bus to publish into. Every key down is `midi/note/on` `{ note, vel, ch: 0 }` (`vel` 0..1) and every key up `midi/note/off` `{ note, ch: 0 }`. Without a bus the piano still works and calls `chord`.","zh":"要發布訊號的 [`Signals`](#Signals) 匯流排。每個按下去的鍵是 `midi/note/on` `{ note, vel, ch: 0 }`（`vel` 是 0..1），每個放開的鍵是 `midi/note/off` `{ note, ch: 0 }`。沒給匯流排的話鋼琴照樣能彈，也會呼叫 `chord`。"},{"name":"chord","type":"ChordDetector","optional":true,"default":"null","en":"Anything with `noteOn(note, vel01)` and `noteOff(note)`, usually a chord detector from `@openav/chord`, to be fed every note directly.","zh":"任何有 `noteOn(note, vel01)` 和 `noteOff(note)` 的東西，通常是 `@openav/chord` 的和弦偵測器，每個音都會直接餵給它。"},{"name":"base","type":"Number","optional":true,"default":"48","en":"The MIDI note of the lowest key (48 is C3). The simulated performer plays an octave above it.","zh":"最低那個鍵的 MIDI 音符號碼（48 是 C3）。模擬演奏者彈的是它高一個八度的地方。"},{"name":"octaves","type":"Number","optional":true,"default":"2","en":"How many octaves the piano shows. It has `octaves × 12 + 1` keys, so two octaves end on a C.","zh":"鋼琴有幾個八度。鍵數是 `octaves × 12 + 1`，所以兩個八度會結束在一個 C。"},{"name":"sim","type":"Boolean","optional":true,"default":"true","en":"Show the \"simulate performance\" checkbox. The performer exists either way: `keys.sim.toggle(true)` starts it from code.","zh":"要不要顯示「simulate performance」勾選框。不管有沒有顯示，演奏者都在：用程式 `keys.sim.toggle(true)` 就能開始。"},{"name":"capture","type":"Boolean","optional":true,"default":"true","en":"Show the \"keyboard\" checkbox that lets the computer keyboard play the piano. With `false` there is no checkbox and `captureBox` is `null`; you can still set `keys.piano.captureEnabled = true`.","zh":"要不要顯示「keyboard」勾選框，讓電腦鍵盤能彈這架鋼琴。給 `false` 就沒有勾選框，`captureBox` 是 `null`。你還是可以自己設 `keys.piano.captureEnabled = true`。"}],"returns":{"type":"Object","en":"`{ piano, sim, update(dt), dispose(), captureBox }`: the [`KeysPiano`](#KeysPiano), the [`SimPlayer`](#SimPlayer), a function to call every frame with the seconds since the last one (it drives the performer), a function that takes everything down again, and the \"keyboard\" checkbox (or `null`).","zh":"`{ piano, sim, update(dt), dispose(), captureBox }`：[`KeysPiano`](#KeysPiano)、[`SimPlayer`](#SimPlayer)、每個畫面要呼叫的函式（參數是距離上一次過了幾秒，它帶動演奏者）、把一切拆掉的函式，以及「keyboard」勾選框（或 `null`）。"},"needs":{"en":"A page with a DOM. The keyboard checkbox must be ticked before the computer keyboard plays, on purpose: while it is on, every letter key is taken by the piano, so hotkeys on letters stop working. A key listener goes on `window` until `dispose()`.","zh":"需要有 DOM 的頁面。要先勾「keyboard」電腦鍵盤才會彈，這是故意的：勾著的時候所有字母鍵都被鋼琴吃掉，字母的快捷鍵就不能用。`window` 上的按鍵監聽會一直在，直到 `dispose()`。"},"notes":[{"en":"In a p5 sketch you do not need this: the 🎹 panel of oav-p5 is a piano like this one, and [`notePressed()`](#notePressed) hears it. `mountKeys()` is for a page that has no oav-p5.","zh":"在 p5 草圖裡用不到這個：oav-p5 的 🎹 面板就是一架這樣的鋼琴，[`notePressed()`](#notePressed) 聽得到它。`mountKeys()` 是給沒有 oav-p5 的頁面用的。"}],"examples":[{"code":"import { mountKeys } from '@openav/keys';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nsignals.on('midi/note/on', ({ note, vel }) => console.log('on', note, vel.toFixed(2)));\nsignals.on('midi/note/off', ({ note }) => console.log('off', note));\n\nconst keys = mountKeys(document.getElementById('app'), { signals, base: 48 });\nkeys.piano.press(60, 0.8);     // as if C4 was clicked\nkeys.piano.release(60);\n// on 60 0.80\n// off 60","caption":{"en":"A two-octave piano appears on the page with its two checkboxes; click a key and the console logs the same events the code below sends.","zh":"頁面上出現兩個八度的鋼琴和它的兩個勾選框。點一個鍵，console 就印出跟下面程式送出的同樣的事件。"},"run":"module","lang":"js"},{"code":"import { mountKeys } from '@openav/keys';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst heard = [];\nsignals.on('midi/note/on', ({ note }) => heard.push(note));\n\nconst keys = mountKeys(document.getElementById('app'), { signals, octaves: 3, capture: false });\nkeys.sim.toggle(true);                          // the performer plays by itself\nconst t0 = performance.now();\nlet last = t0;\n(function frame(now) {\n  keys.update((now - last) / 1000); last = now;   // seconds since the last frame\n  if (now - t0 < 3000) return requestAnimationFrame(frame);\n  console.log(heard.length > 0, keys.captureBox);  // true null\n  keys.dispose();                                   // the piano is taken down\n})(t0);","caption":{"en":"A three-octave piano plays itself for three seconds, its keys lighting up, and then it is taken down. `capture: false` leaves out the keyboard checkbox.","zh":"一架三個八度的鋼琴自己彈三秒，琴鍵一個個亮起來，然後被拆掉。`capture: false` 讓「keyboard」勾選框不出現。"},"run":"module","lang":"js"}],"related":["KeysPiano","SimPlayer","PIANO_KEYMAP","Signals","notePressed"],"keywords":{"en":["piano","keyboard","keys","qwerty","on-screen piano","midi/note/on","simulate","virtual keyboard","play"],"zh":["鋼琴","鍵盤","琴鍵","螢幕鋼琴","電腦鍵盤","模擬演奏","虛擬鍵盤","彈琴"]},"tbd":[],"claims":[]},
{"id":"PIANO_KEYMAP","key":"PIANO_KEYMAP","name":"PIANO_KEYMAP","aliases":[],"kind":"constant","package":"@openav/keys","category":"keys","documented":true,"signature":"PIANO_KEYMAP = object, 17 keys","import":"import { PIANO_KEYMAP } from '@openav/keys';","source":{"file":"packages/keys/index.js","line":18,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/keys/index.js#L18"},"summary":{"en":"Which computer-keyboard letter plays which note: `a` is the base note, `w` a semitone higher, `s` two.","zh":"電腦鍵盤的哪個字母彈哪個音：`a` 是最低音，`w` 高一個半音，`s` 高兩個半音。"},"description":[{"en":"The layout the music software calls the DAW standard: the `A S D F G H J K L ;` row is the white keys and the `W E T Y U O P` row above it is the black keys, placed where they sit on a piano. With a piano that starts on C (`base` 48, 60…) it reads C, C♯, D, D♯, E, F, F♯, G, G♯, A, A♯, B, C, C♯, D, D♯, E. The keys are lowercase letters; [`KeysPiano.handleKeyDown(e)`](#KeysPiano) lowercases the key first. `Z` and `X` are not in the map: they move the octave.","zh":"這是音樂軟體稱為 DAW 標準的排法：`A S D F G H J K L ;` 這一排是白鍵，上面一排 `W E T Y U O P` 是黑鍵，位置就跟鋼琴上一樣。鋼琴從 C 開始（`base` 是 48、60…）的話，從左到右是 C、C♯、D、D♯、E、F、F♯、G、G♯、A、A♯、B、C、C♯、D、D♯、E。鍵是小寫字母。[`KeysPiano.handleKeyDown(e)`](#KeysPiano) 會先把鍵轉成小寫。`Z` 和 `X` 不在這張表裡：它們是換八度。"}],"value":{"type":"Object","en":"`{ a: 0, w: 1, s: 2, e: 3, d: 4, f: 5, t: 6, g: 7, y: 8, h: 9, u: 10, j: 11, k: 12, o: 13, l: 14, p: 15, ';': 16 }`: key to semitones above the piano's `base`.","zh":"`{ a: 0, w: 1, s: 2, e: 3, d: 4, f: 5, t: 6, g: 7, y: 8, h: 9, u: 10, j: 11, k: 12, o: 13, l: 14, p: 15, ';': 16 }`：鍵對應到比鋼琴的 `base` 高幾個半音。"},"params":[],"notes":[],"examples":[{"code":"import { PIANO_KEYMAP } from '@openav/keys';\n\nconst base = 60;                                   // middle C\nconst noteFor = (key) => (key in PIANO_KEYMAP ? base + PIANO_KEYMAP[key] : null);\nconsole.log(noteFor('a'), noteFor('w'), noteFor('k'), noteFor('q'));   // 60 61 72 null\nconsole.log(Object.keys(PIANO_KEYMAP).join(''));                        // awsedftgyhujkolp;","caption":{"en":"Looks up the note of a few letters on a piano that starts on middle C: `k` is the C an octave up, and `q` plays nothing.","zh":"在從中央 C 開始的鋼琴上查幾個字母的音：`k` 是高八度的 C，`q` 不發音。"},"run":"module","lang":"js"}],"related":["KeysPiano","mountKeys"],"keywords":{"en":["keymap","qwerty","computer keyboard","letters","mapping","daw","a s d f"],"zh":["鍵盤對應","電腦鍵盤","字母","對應表","qwerty","彈琴"]},"tbd":[],"claims":[]},
{"id":"SimPlayer","key":"SimPlayer","name":"SimPlayer","aliases":[],"kind":"class","package":"@openav/keys","category":"keys","documented":true,"signature":"new SimPlayer({ press, release, base = 60, scale = [0, 2, 4, 7, 9], density = 1 } = {})","import":"import { SimPlayer } from '@openav/keys';","source":{"file":"packages/keys/index.js","line":170,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/keys/index.js#L170"},"summary":{"en":"A hands-free performer: it presses and releases notes on its own, in a scale, so a demo can play itself.","zh":"一個不用手的演奏者：它自己在音階裡按下和放開音符，讓示範可以自己演奏。"},"description":[{"en":"It plays like someone noodling. Every gesture it picks at random: mostly a step up or down the scale, sometimes a leap, now and then a triad stacked in the scale, and real rests. Notes last 0.18 to 0.68 seconds (a triad twice as long), the next gesture comes 0.22 to 0.77 seconds later (a rest lasts 0.6 to 2 seconds), all at the default `density` of 1, and the notes are kept between 36 and 96. Because it presses and releases through your own two functions, whatever listens downstream cannot tell it from a person.","zh":"它彈起來像有人隨手彈。每個動作都是隨機挑的：大多是在音階上往上或往下一步，有時跳一個音程，偶爾彈一個疊在音階上的三和弦，也有真正的休止。每個音持續 0.18 到 0.68 秒（三和弦長兩倍），下一個動作在 0.22 到 0.77 秒之後（休止 0.6 到 2 秒），這些都是 `density` 為預設的 1 時的數字，音符被限制在 36 到 96。因為它是透過你的兩個函式按下和放開，下游的東西分不出它跟真人的差別。"},{"en":"It starts off. Call `toggle(true)` and then `update(dt)` once per frame with the seconds since the last frame; it does nothing while it is off. `toggle(false)` releases every note it is holding, so none sticks. The notes are random (it uses `Math.random`, there is no seed).","zh":"它一開始是關的。呼叫 `toggle(true)`，然後每個畫面用距離上一個畫面過了幾秒呼叫 `update(dt)`。關著的時候它什麼都不做。`toggle(false)` 會放開它按著的所有音，不會有音卡住。音是隨機的（用的是 `Math.random`，沒有 seed）。"}],"params":[{"name":"press","type":"Function","optional":false,"en":"`press(note, vel01)`: called when a note starts, with its MIDI number and a velocity between 0.45 and 0.85. Usually the `press` of a [`KeysPiano`](#KeysPiano), so the keys light up.","zh":"`press(note, vel01)`：一個音開始時呼叫，帶著 MIDI 號碼和 0.45 到 0.85 之間的力道。通常是 [`KeysPiano`](#KeysPiano) 的 `press`，這樣琴鍵會亮。"},{"name":"release","type":"Function","optional":false,"en":"`release(note)`: called when the note ends.","zh":"`release(note)`：這個音結束時呼叫。"},{"name":"base","type":"Number","optional":true,"default":"60","en":"The MIDI note the scale starts on.","zh":"音階從哪個 MIDI 音符開始。"},{"name":"scale","type":"Number[]","optional":true,"default":"[0, 2, 4, 7, 9]","en":"The notes it may play, as semitones above `base`. The default is a pentatonic scale, which sounds fine whatever happens.","zh":"它可以彈的音，用比 `base` 高幾個半音來表示。預設是五聲音階，怎麼彈都不會難聽。"},{"name":"density","type":"Number","optional":true,"default":"1","en":"How busy it is. The gaps between notes are divided by it, so 2 plays about twice as often.","zh":"多忙。音和音之間的間隔會除以它，所以 2 大約是兩倍密。"}],"notes":[],"examples":[{"code":"import { SimPlayer } from '@openav/keys';\n\nconst played = [];\nconst sim = new SimPlayer({\n  press: (note, vel) => played.push(note),\n  release: () => {},\n  base: 60, density: 2,\n});\nsim.toggle(true);\nfor (let i = 0; i < 600; i++) sim.update(1 / 60);   // ten seconds, in frames\nconsole.log(played.length > 5, Math.min(...played) >= 36, Math.max(...played) <= 96);   // true true true\nsim.toggle(false);","caption":{"en":"Runs the performer for ten simulated seconds without any sound or screen and logs that it played a good number of notes, all inside 36 to 96.","zh":"不用聲音也不用畫面，讓演奏者跑十秒的模擬時間，印出它彈了不少音，而且全都在 36 到 96 之間。"},"run":"module","lang":"js"},{"code":"import { KeysPiano, SimPlayer } from '@openav/keys';\n\nconst piano = new KeysPiano(document.getElementById('app'), { base: 48, octaves: 2 });\nconst sim = new SimPlayer({\n  press: (n, v) => piano.press(n, v),        // through the piano: the keys light up\n  release: (n) => piano.release(n),\n  base: 60, scale: [0, 2, 3, 5, 7, 8, 10],   // a natural minor scale\n});\nsim.toggle(true);\nconst t0 = performance.now();\nlet last = t0;\n(function frame(now) {\n  sim.update((now - last) / 1000); last = now;\n  if (now - t0 < 4000) requestAnimationFrame(frame); else sim.toggle(false);\n})(t0);","caption":{"en":"A piano plays itself in a minor scale for four seconds: the keys light up as the notes go down and up.","zh":"一架鋼琴在小調音階裡自己彈四秒：音符按下和放開的時候，琴鍵跟著亮和暗。"},"run":"module","lang":"js"}],"related":["KeysPiano","mountKeys"],"keywords":{"en":["simulate","simulated performer","auto play","demo","noodle","random notes","bot","generative"],"zh":["模擬演奏","自動演奏","示範","隨手彈","隨機音符","演奏者"]},"tbd":[],"claims":[],"members":[{"name":"toggle","member":"method","signature":"on = !this.enabled","doc":"","line":181},{"name":"update","member":"method","signature":"dt","doc":"","line":201}]},
{"id":"boneBasis","key":"boneBasis","name":"boneBasis","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"boneBasis(prev, next, palmNormal, left)","import":"import { boneBasis } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":48,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L48"},"summary":{"en":"An advanced helper: the orthonormal basis `[x, y, z]` of a bone, rebuilt from two joints and the palm normal the way LeapJS expects.","zh":"進階用的輔助函式：用兩個關節和掌心法線重建一根骨頭的正交基底 `[x, y, z]`，做法符合 LeapJS 的期待。"},"description":[{"en":"The sensor gives joints, not rotations, but LeapJS sketches read a rotation basis for every bone, so [`V6Converter`](#V6Converter) uses this to fill in `bases`. You only need it if you write your own converter.","zh":"感測器給的是關節，不是旋轉，但 LeapJS 的草圖會對每根骨頭讀一個旋轉基底，所以 [`V6Converter`](#V6Converter) 用它來填 `bases`。只有自己寫轉換器的時候才會需要它。"}],"params":[{"name":"prev","type":"Array<Number>","en":"The joint at the base of the bone, `[x, y, z]` in millimetres.","zh":"骨頭根部的關節 `[x, y, z]`，單位是公釐。"},{"name":"next","type":"Array<Number>","en":"The joint at the tip end of the bone.","zh":"骨頭前端的關節。"},{"name":"palmNormal","type":"Array<Number>","en":"The palm normal; `-palmNormal` becomes the \"back of the hand\" axis (y).","zh":"掌心法線，`-palmNormal` 會成為「手背」那一軸（y）。"},{"name":"left","type":"Boolean","en":"`true` for a left hand: the x axis is negated, so the determinant is negative, which is how LeapJS tells left from right.","zh":"左手就傳 `true`：x 軸會反向，行列式變成負的，LeapJS 就是靠這個分辨左右手。"}],"returns":{"type":"Array","en":"Three rows `[x, y, z]`, each a unit vector rounded to 3 decimals. z runs from the tip back to the base (LeapJS's `Bone.direction()` is `-z`).","zh":"三列 `[x, y, z]`，各是四捨五入到小數三位的單位向量。z 從前端指回根部（LeapJS 的 `Bone.direction()` 是 `-z`）。"},"notes":[],"examples":[{"code":"import { boneBasis } from '@openav/leap';\n\n// a bone pointing straight ahead (-z), palm facing down: the basis is the identity\nconsole.log(boneBasis([0, 200, 0], [0, 200, -40], [0, -1, 0], false));\n// the same bone on a left hand: x flips\nconsole.log(boneBasis([0, 200, 0], [0, 200, -40], [0, -1, 0], true)[0]);","caption":{"en":"The console prints the identity `[ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ]`, then `[ -1, 0, 0 ]` for the left hand.","zh":"主控台印出單位矩陣 `[ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ]`，然後左手的 `[ -1, 0, 0 ]`。"},"run":"module","lang":"js"}],"related":["palmBasis","V6Converter","rawToV6"],"keywords":{"en":["bone basis","rotation","leapjs","orthonormal","bone","basis matrix"],"zh":["骨頭基底","旋轉","leapjs","正交","骨頭","基底矩陣"]},"tbd":[],"claims":[]},
{"id":"leap-DEFAULT_PORT","key":"DEFAULT_PORT","name":"DEFAULT_PORT","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"DEFAULT_PORT = 6437","import":"import { DEFAULT_PORT } from '@openav/leap/bridge';","source":{"file":"packages/leap/bridge/leap-bridge.mjs","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/bridge/leap-bridge.mjs#L30"},"summary":{"en":"The Leap bridge's default port: 6437, the port the original Leap service used, so old `leap.js` sketches find it unchanged.","zh":"Leap 橋接程式預設的連接埠：6437，是原本的 Leap 服務用的連接埠，舊的 `leap.js` 草圖不用改就找得到。"},"description":[{"en":"The default of `startBridge({ port })` and of `--port`. The environment variable `LEAP_BRIDGE_PORT` changes it for the command line. This is the port of the **Leap** bridge; the out-bridge has its own ([`DEFAULT_PORT` of `@openav/out`](#out-DEFAULT_PORT), 6450).","zh":"`startBridge({ port })` 和 `--port` 的預設值。環境變數 `LEAP_BRIDGE_PORT` 可以改命令列用的值。這是 **Leap** 橋接程式的連接埠。輸出橋接有自己的（[`@openav/out` 的 `DEFAULT_PORT`](#out-DEFAULT_PORT)，6450）。"}],"params":[],"notes":[],"examples":[],"related":["DEFAULT_URL"],"keywords":{"en":["port","6437","default port","bridge port"],"zh":["連接埠","6437","預設連接埠","橋接連接埠"]},"tbd":[],"claims":[]},
{"id":"DEFAULT_URL","key":"DEFAULT_URL","name":"DEFAULT_URL","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"DEFAULT_URL = \"ws://127.0.0.1:6437/v6.json\"","import":"import { DEFAULT_URL } from '@openav/leap';","source":{"file":"packages/leap/index.js","line":36,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/index.js#L36"},"summary":{"en":"The default address `LeapInput` connects to: `ws://127.0.0.1:6437/v6.json`, the bridge on this machine speaking the LeapJS protocol.","zh":"`LeapInput` 預設連的位址：`ws://127.0.0.1:6437/v6.json`，也就是這台機器上、講 LeapJS 通訊協定的橋接程式。"},"description":[{"en":"Port 6437 is the port the original Leap service used, so old `leap.js` sketches find the bridge without changing anything. `…/raw` on the same port serves the raw lines instead. In a show, `?leap=ws://host:port/v6.json` in the page address points a page at another bridge, and `?leap=sim` starts the simulator.","zh":"6437 是原本的 Leap 服務用的連接埠，所以舊的 `leap.js` 草圖不用改任何東西就找得到橋接程式。同一個連接埠的 `…/raw` 提供的是原始行格式。在演出裡，網址加上 `?leap=ws://host:port/v6.json` 可以讓頁面改連別的橋接程式，`?leap=sim` 則會啟動模擬器。"}],"params":[],"notes":[],"examples":[],"related":["LeapInput"],"keywords":{"en":["default url","bridge address","ws://127.0.0.1:6437","websocket url","6437"],"zh":["預設網址","橋接位址","ws://127.0.0.1:6437","websocket 位址","6437"]},"tbd":[],"claims":[]},
{"id":"deviceEvent","key":"deviceEvent","name":"deviceEvent","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"deviceEvent({ device = null, service = true } = {})","import":"import { deviceEvent } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":197,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L197"},"summary":{"en":"A LeapJS device event telling the client whether a device is attached and streaming, plus our own `service` flag.","zh":"一則 LeapJS 裝置事件，告訴用戶端裝置有沒有接上、有沒有在串流，另外附上我們自己加的 `service` 旗標。"},"description":[{"en":"LeapJS (protocol 5 and up) turns it into `deviceAttached`, `deviceStreaming` and `streamingStarted`. The bridge sends one on connect and on every status change. `LeapInput` maps it to its status: no service is `'no-service'`, a service without a device is `'no-device'`, and a streaming device is `'tracking'`.","zh":"LeapJS（通訊協定 5 以上）會把它轉成 `deviceAttached`、`deviceStreaming` 和 `streamingStarted`。橋接程式在連線時，以及每次狀態改變時都會送一則。`LeapInput` 把它對應成自己的狀態：沒有服務是 `'no-service'`，有服務沒裝置是 `'no-device'`，裝置在串流就是 `'tracking'`。"}],"params":[{"name":"device","type":"String | null","optional":true,"default":"null","en":"The device id, or `null` for no device.","zh":"裝置 id，沒有裝置就是 `null`。"},{"name":"service","type":"Boolean","optional":true,"default":"true","en":"Whether the tracking service is running. LeapJS ignores this; [`LeapInput`](#LeapInput) uses it to tell \"no tracking service\" from \"no device\".","zh":"追蹤服務有沒有在跑。LeapJS 會忽略它。[`LeapInput`](#LeapInput) 用它區分「沒有追蹤服務」和「沒有裝置」。"}],"returns":{"type":"Object","en":"`{ event: { type: 'deviceEvent', state: { attached, streaming, id, type: 'Peripheral', service } } }`; `id` is `'none'` without a device.","zh":"`{ event: { type: 'deviceEvent', state: { attached, streaming, id, type: 'Peripheral', service } } }`，沒有裝置時 `id` 是 `'none'`。"},"notes":[],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { LeapInput, deviceEvent } from '@openav/leap';\n\nconst signals = new Signals();\nconst leap = new LeapInput({ signals });\nfor (const event of [deviceEvent({ device: null, service: false }), deviceEvent({ device: null }), deviceEvent({ device: 'LE51' })]) {\n  leap.ingest(event);\n  console.log(leap.status, signals.get('leap/status'));\n}","caption":{"en":"Feeding three device events through a `LeapInput`: the console prints `no-service 1`, `no-device 2`, `tracking 3`.","zh":"把三則裝置事件餵給 `LeapInput`：主控台印出 `no-service 1`、`no-device 2`、`tracking 3`。"},"run":"module","lang":"js"}],"related":["v6Header","LeapInput","STATUS"],"keywords":{"en":["device event","device attached","streaming","no device","no service","status"],"zh":["裝置事件","裝置接上","串流","沒有裝置","沒有服務","狀態"]},"tbd":[],"claims":[]},
{"id":"FINGERS","key":"FINGERS","name":"FINGERS","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"FINGERS = array, 5 items","import":"import { FINGERS } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":21,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L21"},"summary":{"en":"The five finger names in order, `thumb index middle ring pinky`: the order of `hand.fingers` and the names in the fingertip signals.","zh":"依序排好的五個手指名稱 `thumb index middle ring pinky`：`hand.fingers` 的順序，也是指尖訊號用的名字。"},"description":[{"en":"`hand.fingers[0]` is the thumb and `hand.fingers[4]` the pinky, so `FINGERS[i]` names `hand.fingers[i]`. The fingertip signals are `leap/hand/<side>/<finger>/x|y|z` with these names, for example `leap/hand/right/index/y`.","zh":"`hand.fingers[0]` 是拇指，`hand.fingers[4]` 是小指，所以 `FINGERS[i]` 就是 `hand.fingers[i]` 的名字。指尖訊號是 `leap/hand/<side>/<finger>/x|y|z`，用的就是這些名字，例如 `leap/hand/right/index/y`。"}],"params":[],"notes":[],"examples":[],"related":["LeapInput","handsOf","leapSignals"],"keywords":{"en":["fingers","finger names","thumb","index","middle","ring","pinky","fingertip"],"zh":["手指","手指名稱","拇指","食指","中指","無名指","小指","指尖"]},"tbd":[],"claims":[]},
{"id":"handAngles","key":"handAngles","name":"handAngles","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"handAngles(direction, normal)","import":"import { handAngles } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":69,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L69"},"summary":{"en":"Turns a hand's direction and palm normal into `{ pitch, yaw, roll }` in radians, with the LeapJS conventions.","zh":"把手的方向和掌心法線轉成以弧度計的 `{ pitch, yaw, roll }`，沿用 LeapJS 的慣例。"},"description":[{"en":"A flat hand, palm down, fingers away from you, is `{ pitch: 0, yaw: 0, roll: 0 }`. The three angles are the `leap/hand/<side>/pitch|yaw|roll` signals, which `LeapInput` computes with this function from each hand.","zh":"手平放、掌心朝下、手指朝向遠離你的方向，就是 `{ pitch: 0, yaw: 0, roll: 0 }`。這三個角度就是 `leap/hand/<side>/pitch|yaw|roll` 這幾個訊號，`LeapInput` 對每隻手都是用這個函式算出來的。"}],"params":[{"name":"direction","type":"Array<Number>","en":"The direction the fingers point, `[x, y, z]` (a unit vector).","zh":"手指指的方向 `[x, y, z]`（單位向量）。"},{"name":"normal","type":"Array<Number>","en":"The palm normal `[x, y, z]`, pointing out of the palm (down when the palm faces the sensor).","zh":"掌心法線 `[x, y, z]`，從手掌往外指（掌心朝向感測器時是向下）。"}],"returns":{"type":"Object","en":"`{ pitch, yaw, roll }`, each −π..π: `pitch` is the fingers tipping up, `yaw` turning toward +x (the right), `roll` the palm turning over.","zh":"`{ pitch, yaw, roll }`，各自在 −π..π：`pitch` 是手指往上翹，`yaw` 是轉向 +x（右邊），`roll` 是手掌翻轉。"},"notes":[{"en":"When the fingers point straight up there is no yaw or roll to measure, and the function answers π (3.14) for them. Expect that at the top of a gesture, and do not map those two signals when the hand is vertical.","zh":"手指正對著天花板的時候，沒有 yaw 或 roll 可量，函式會回答 π（3.14）。手勢做到最高的時候會遇到，手垂直時請不要去映射那兩個訊號。"}],"examples":[{"code":"import { handAngles } from '@openav/leap';\n\nconst round = (a) => ({ pitch: +a.pitch.toFixed(2), yaw: +a.yaw.toFixed(2), roll: +a.roll.toFixed(2) });\nconsole.log(round(handAngles([0, 0, -1], [0, -1, 0])));           // flat, palm down: all 0\nconsole.log(round(handAngles([0, 0.5, -0.866], [0, -0.866, -0.5])));   // fingers tipped up 30 degrees: pitch 0.52\nconsole.log(round(handAngles([0.707, 0, -0.707], [0, -1, 0])));    // fingers turned 45 degrees to the right: yaw 0.79\nconsole.log(round(handAngles([0, 0, -1], [0.5, -0.866, 0])));      // palm turned over 30 degrees: roll 0.52","caption":{"en":"The console prints the angles for four hands: all zeros, then `pitch: 0.52`, `yaw: 0.79` and `roll: 0.52`, one angle changing each time.","zh":"主控台印出四隻手的角度：全是 0，然後是 `pitch: 0.52`、`yaw: 0.79` 和 `roll: 0.52`，每次只有一個角度在變。"},"run":"module","lang":"js"}],"related":["normalizePoint","LeapInput","palmBasis"],"keywords":{"en":["pitch","yaw","roll","angles","hand rotation","orientation","radians","palm normal"],"zh":["俯仰","偏航","翻滾","角度","手的旋轉","方向","弧度","掌心法線"]},"tbd":[],"claims":[]},
{"id":"handBones","key":"handBones","name":"handBones","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"handBones(hand)","import":"import { handBones } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":248,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L248"},"summary":{"en":"The bones to draw for a hand, as pairs of points: each finger's joints in turn, the line across the knuckles, and the forearm.","zh":"一隻手要畫的骨頭，用成對的點表示：每根手指的關節依序相連、橫過指節的那條線，以及前臂。"},"description":[{"en":"It only lists the pairs; you choose how to draw them. [`LeapInput.skeleton()`](#LeapInput) calls it for you. Use it directly when you draw with p5, three.js or your own projection.","zh":"它只列出成對的點，怎麼畫由你決定。[`LeapInput.skeleton()`](#LeapInput) 會替你呼叫它。用 p5、three.js 或自己的投影來畫的時候，就直接用它。"}],"params":[{"name":"hand","type":"Object","en":"A hand from [`handsOf()`](#handsOf) or `leap.hands`.","zh":"來自 [`handsOf()`](#handsOf) 或 `leap.hands` 的一隻手。"}],"returns":{"type":"Array","en":"An array of `[pointA, pointB]` pairs, each point `[x, y, z]` in millimetres. A hand with five 5-joint fingers, a wrist and an elbow gives 25.","zh":"`[pointA, pointB]` 成對的陣列，每個點是 `[x, y, z]`，單位是公釐。一隻有五根各 5 個關節的手指、手腕和手肘的手，會得到 25 條。"},"notes":[],"examples":[{"code":"import { INTERACTION_BOX, handBones, handsOf, mockFrame, normalizePoint } from '@openav/leap';\n\nconst ctx = document.getElementById('c').getContext('2d');\nctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\nctx.strokeStyle = '#ffd166'; ctx.lineWidth = 2;\nconst at = (p) => { const n = normalizePoint(p, INTERACTION_BOX, false); return [n[0] * 400, (1 - n[1]) * 300]; };   // front view\nfor (const hand of handsOf(mockFrame(2.0))) {\n  for (const [a, b] of handBones(hand)) {\n    ctx.beginPath(); ctx.moveTo(...at(a)); ctx.lineTo(...at(b)); ctx.stroke();\n  }\n}","caption":{"en":"Both mock hands are drawn on the canvas in a front view, bone by bone.","zh":"兩隻假手用正視圖畫在畫布上，一根骨頭一根骨頭地畫。"},"run":"module","lang":"js"}],"related":["handsOf","LeapInput","normalizePoint"],"keywords":{"en":["bones","skeleton","draw hand","joints","hand lines"],"zh":["骨頭","骨架","畫手","關節","手的線條"]},"tbd":[],"claims":[]},
{"id":"HandGestures","key":"HandGestures","name":"HandGestures","aliases":[],"kind":"class","package":"@openav/leap","category":"leap","documented":true,"signature":"new HandGestures(thresholds = THRESHOLDS)","import":"import { HandGestures } from '@openav/leap';","source":{"file":"packages/leap/gesture.js","line":39,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/gesture.js#L39"},"summary":{"en":"Turns pinch and grab strengths into clean start and end events, per hand plus a both-hands pinch, using hysteresis so a wobbling value fires once.","zh":"把捏合和握拳的力度變成乾淨的開始與結束事件，每隻手各一組，另外還有雙手捏合，用遲滯處理，所以抖動的值只會觸發一次。"},"description":[{"en":"A pinch strength hovers around any single threshold while the fingers are \"almost touching\", and one threshold then fires start, end, start, end sixty times a second. So every gesture is a trigger with two thresholds: it **starts** above `on` and only **ends** below `off`. The defaults are a pinch starting above 0.86 and ending below 0.5, a both-hands pinch entering when both hands are above 0.8 and leaving when either drops below 0.5, and a grab (fist) above 0.8 and below 0.5. A hand that disappears ends all of its gestures.","zh":"手指「快要碰到」的時候，捏合力度會在任何單一門檻附近晃來晃去，只有一個門檻的話，就會每秒六十次地開始、結束、開始、結束。所以每個手勢都有兩個門檻：高過 `on` 才**開始**，低於 `off` 才**結束**。預設值是：捏合高於 0.86 開始、低於 0.5 結束。雙手捏合在兩隻手都高於 0.8 時進入，任何一隻低於 0.5 就離開。握拳高於 0.8 開始、低於 0.5 結束。手消失的時候，它所有的手勢都會結束。"},{"en":"`LeapInput` already runs one (`leap.gestures`) and turns its events into the `leap/…/pinch-start` pulses. Use `HandGestures` on its own when you have hands from somewhere else.","zh":"`LeapInput` 已經內建一個（`leap.gestures`），並把它的事件變成 `leap/…/pinch-start` 這些脈衝。你的手資料如果是從別的地方來，才需要單獨使用 `HandGestures`。"}],"params":[{"name":"thresholds","type":"Object","optional":true,"default":"THRESHOLDS","en":"Overrides for the defaults in [`THRESHOLDS`](#THRESHOLDS): `{ pinch: { on, off }, bothPinch: { on, off }, grab: { on, off } }`. Keys you leave out keep their defaults.","zh":"覆寫 [`THRESHOLDS`](#THRESHOLDS) 的預設值：`{ pinch: { on, off }, bothPinch: { on, off }, grab: { on, off } }`。沒寫的鍵維持預設。"}],"notes":[],"examples":[{"code":"import { HandGestures } from '@openav/leap';\n\nconst gestures = new HandGestures();\nconst frame = (pinch) => gestures.update([{ side: 'right', pinch, grab: 0 }]).map((e) => e.type);\nconsole.log(frame(0.85));   // []               below 0.86: not yet\nconsole.log(frame(0.9));    // [ 'pinch-start' ]\nconsole.log(frame(0.6));    // []               still held: only below 0.5 ends it\nconsole.log(frame(0.4));    // [ 'pinch-end' ]","caption":{"en":"The console prints `[]`, `[ 'pinch-start' ]`, `[]`, `[ 'pinch-end' ]`: the pinch starts above 0.86 and holds until it drops under 0.5.","zh":"主控台印出 `[]`、`[ 'pinch-start' ]`、`[]`、`[ 'pinch-end' ]`：捏合高於 0.86 才開始，一直撐到低於 0.5。"},"run":"module","lang":"js"},{"code":"import { HandGestures } from '@openav/leap';\n\nconst gestures = new HandGestures({ pinch: { on: 0.7, off: 0.3 } });   // easier to start, harder to let go\nconst hand = (side, pinch) => ({ side, pinch, grab: 0 });\nconst names = (events) => events.map((e) => e.type + ':' + e.side);\nconsole.log(names(gestures.update([hand('right', 0.75), hand('left', 0.82)])));   // single pinches only: both need 0.8\nconsole.log(names(gestures.update([hand('right', 0.9), hand('left', 0.9)])));     // now both hands pinch\nconsole.log(names(gestures.update([])));                                          // both hands leave: everything ends","caption":{"en":"The console prints `[ 'pinch-start:left', 'pinch-start:right' ]`, then `[ 'both-pinch-start:both' ]`, then the three ends when the hands leave.","zh":"主控台先印出 `[ 'pinch-start:left', 'pinch-start:right' ]`，接著是 `[ 'both-pinch-start:both' ]`，兩隻手離開時則印出三個結束事件。"},"run":"module","lang":"js"}],"related":["Hysteresis","THRESHOLDS","LeapInput","mockFrame"],"keywords":{"en":["gesture","gestures","pinch","grab","fist","hysteresis","schmitt trigger","start end","both hands"],"zh":["手勢","捏合","握拳","遲滯","施密特觸發","開始結束","雙手"]},"tbd":[],"claims":[],"members":[{"name":"update","member":"method","signature":"hands","doc":"","line":48},{"name":"state","member":"getter","signature":"","doc":"What is held right now: { left: {pinch, grab}, right: {…}, both }","line":67}]},
{"id":"handsOf","key":"handsOf","name":"handsOf","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"handsOf(msg)","import":"import { handsOf } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":211,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L211"},"summary":{"en":"Reads either wire format (raw line or LeapJS v6 frame) into the one hand shape the rest of the package uses; anything else gives `[]`.","zh":"把兩種傳輸格式（原始行或 LeapJS v6 影格）都讀成套件其他部分共用的同一種手的形狀，其他任何東西都回傳 `[]`。"},"description":[{"en":"This is the hand shape worlds draw from (`leap.hands`). A raw frame carries the fingers inline; a v6 frame carries them as `pointables` joined to their hand by `handId`, and `handsOf()` does that join. Both give the same hand, so code written against `leap.hands` does not care which URL the bridge was reached on.","zh":"這就是世界用來畫手的形狀（`leap.hands`）。原始影格把手指放在手的裡面。v6 影格把手指放成 `pointables`，用 `handId` 接到它們的手，`handsOf()` 負責做這個合併。兩者給出同一隻手，所以針對 `leap.hands` 寫的程式不在乎橋接程式是從哪個網址連的。"}],"params":[{"name":"msg","type":"Object","en":"A parsed message: a raw frame (`t: 'f'`) or a v6 frame (with `hands` and `pointables`).","zh":"一則解析好的訊息：原始影格（`t: 'f'`）或 v6 影格（有 `hands` 和 `pointables`）。"}],"returns":{"type":"Array","en":"The hands, each `{ id, side: 'left' | 'right', confidence, pinch, grab, palm, velocity, normal, direction, width, fingers, extended, elbow, wrist, timeVisible }`. Positions are in millimetres; `fingers` is 5 arrays of joints (the last is the tip), `extended` 5 booleans.","zh":"手的陣列，每隻是 `{ id, side: 'left' | 'right', confidence, pinch, grab, palm, velocity, normal, direction, width, fingers, extended, elbow, wrist, timeVisible }`。位置單位是公釐。`fingers` 是 5 個關節陣列（最後一個是指尖），`extended` 是 5 個布林值。"},"notes":[],"examples":[{"code":"import { mockFrame, rawToV6, handsOf } from '@openav/leap';\n\nconst raw = mockFrame(2.0);\nconst fromRaw = handsOf(raw)[0];\nconst fromV6 = handsOf(rawToV6(raw))[0];                 // the same frame as LeapJS would send it\nconsole.log(fromRaw.side, fromRaw.palm, fromRaw.pinch);   // right [43.6, 233.2, 53.6] 1\nconsole.log(fromV6.side, fromV6.palm, fromV6.pinch);      // the same hand\nconsole.log(handsOf(null), handsOf({}));                  // [] []","caption":{"en":"Both formats give the same hand: the console prints `right [ 43.6, 233.2, 53.6 ] 1` twice, then `[] []`.","zh":"兩種格式給出同一隻手：主控台兩次印出 `right [ 43.6, 233.2, 53.6 ] 1`，然後是 `[] []`。"},"run":"module","lang":"js"}],"related":["handBones","rawToV6","mockFrame","LeapInput"],"keywords":{"en":["hands of","parse hands","hand shape","fingers","joints","v6 frame","raw frame","convert"],"zh":["解析手","手的形狀","手指","關節","v6 影格","原始影格"]},"tbd":[],"claims":[]},
{"id":"Hysteresis","key":"Hysteresis","name":"Hysteresis","aliases":[],"kind":"class","package":"@openav/leap","category":"leap","documented":true,"signature":"new Hysteresis(on, off)","import":"import { Hysteresis } from '@openav/leap';","source":{"file":"packages/leap/gesture.js","line":20,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/gesture.js#L20"},"summary":{"en":"One Schmitt trigger: `update(v)` says `'start'` when the value rises above `on`, and `'end'` only when it falls below `off`.","zh":"一個施密特觸發器：數值升到 `on` 以上，`update(v)` 回報 `'start'`，要降到 `off` 以下才會回報 `'end'`。"},"description":[{"en":"The gap between `on` and `off` is what stops a value that hovers near one threshold from flickering. Each call to `update()` returns `'start'` (it just became active), `'end'` (it just stopped) or `null` (nothing changed). A missing or `NaN` value while active counts as an end, so a lost sensor lets go. `active` tells you whether it is on now.","zh":"`on` 和 `off` 之間的空隙，就是讓在某個門檻附近徘徊的數值不會閃爍的原因。每次呼叫 `update()` 會回傳 `'start'`（剛變成啟動）、`'end'`（剛停止）或 `null`（沒有變化）。啟動中遇到缺值或 `NaN` 會當作結束，所以感測器掉了就會放開。`active` 告訴你它現在是不是開著。"}],"params":[{"name":"on","type":"Number","en":"The level the value must rise **above** to start.","zh":"數值要**高過**它才會開始。"},{"name":"off","type":"Number","en":"The level the value must fall **below** to end. It must be lower than `on`, or the constructor throws \"Hysteresis: off must be below on\".","zh":"數值要**低於**它才會結束。必須比 `on` 低，否則建構函式會丟出 \"Hysteresis: off must be below on\"。"}],"notes":[],"examples":[{"code":"import { Hysteresis } from '@openav/leap';\n\nconst trigger = new Hysteresis(0.8, 0.4);\nconst values = [0.5, 0.9, 0.6, 0.3, 0.5];\nconsole.log(values.map((v) => trigger.update(v)));   // [ null, 'start', null, 'end', null ]","caption":{"en":"The console prints `[ null, 'start', null, 'end', null ]`: 0.6 is below 0.8 but above 0.4, so it is still held.","zh":"主控台印出 `[ null, 'start', null, 'end', null ]`：0.6 低於 0.8，但高於 0.4，所以還是按住的狀態。"},"run":"module","lang":"js"}],"related":["HandGestures","THRESHOLDS"],"keywords":{"en":["hysteresis","schmitt trigger","threshold","debounce","flicker","start end"],"zh":["遲滯","施密特觸發","門檻","防抖動","閃爍"]},"tbd":[],"claims":[],"members":[{"name":"update","member":"method","signature":"v","doc":"","line":25},{"name":"release","member":"method","signature":"","doc":"Force the end (hand lost).","line":31}]},
{"id":"INTERACTION_BOX","key":"INTERACTION_BOX","name":"INTERACTION_BOX","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"INTERACTION_BOX = object, 2 keys","import":"import { INTERACTION_BOX } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":24,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L24"},"summary":{"en":"The classic Leap interaction box, `{ center: [0, 200, 0], size: [235.247, 235.247, 147.751] }` in millimetres, that positions are normalised against.","zh":"經典的 Leap 互動範圍盒，`{ center: [0, 200, 0], size: [235.247, 235.247, 147.751] }`，單位是公釐，位置就是對它做正規化。"},"description":[{"en":"A frozen object: the centre sits 200 mm above the sensor, and the box measures 235.247 mm along x, 235.247 mm along y and 147.751 mm along z (the sensor's own axes: x across, y up, z toward the player). [`normalizePoint()`](#normalizePoint) maps it to 0..1, and the console panel draws it as an outline so you can see when a hand leaves it. Pass your own box to `new LeapInput({ box })` to change where 0 and 1 are.","zh":"一個凍結的物件：中心在感測器上方 200 公釐，三個軸的尺寸是感測器自己的座標軸：x 是 235 公釐、y 是 235 公釐、z 是 148 公釐。[`normalizePoint()`](#normalizePoint) 把它對應到 0..1，主控台面板把它畫成外框，這樣你就看得到手什麼時候離開。要改 0 和 1 在哪裡，就傳自己的盒子給 `new LeapInput({ box })`。"}],"params":[],"notes":[],"examples":[],"related":["normalizePoint","LeapInput","mountLeapPanel"],"keywords":{"en":["interaction box","tracking volume","box","center","size","millimetres"],"zh":["互動範圍盒","追蹤範圍","範圍盒","中心","尺寸","公釐"]},"tbd":[],"claims":[]},
{"id":"LeapInput","key":"LeapInput","name":"LeapInput","aliases":[],"kind":"class","package":"@openav/leap","category":"leap","documented":true,"signature":"new LeapInput({ signals = null, url = DEFAULT_URL, reconnect = true, fingers = true, box = INTERACTION_BOX, thresholds = THRESHOLDS, WebSocket: WS = null } = {})","import":"import { LeapInput } from '@openav/leap';","source":{"file":"packages/leap/index.js","line":64,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/index.js#L64"},"summary":{"en":"Reads Leap Motion hands from the local bridge and publishes them as `leap/*` signals, plus the joints (`leap.hands`) for drawing the hand itself.","zh":"從本機的橋接程式讀取 Leap Motion 的手，發布成 `leap/*` 訊號，並在 `leap.hands` 提供關節資料，讓你把手本身畫出來。"},"description":[{"en":"Browsers cannot talk to the Leap Motion sensor directly, so a small program on the same machine, the **bridge** ([`startBridge()`](#leap-startBridge), `ws://127.0.0.1:6437`), reads it and serves the hands over a WebSocket. `LeapInput` connects to that bridge and does one job: it turns hands into **normalised signals** and never touches params or worlds. In a show, `createShow({ modules: { leap: true } })` builds one and returns it as `show.leap`; the console's L1 panel shows which link is missing.","zh":"瀏覽器沒辦法直接跟 Leap Motion 感測器溝通，所以同一台機器上有個小程式，也就是**橋接程式**（[`startBridge()`](#leap-startBridge)，`ws://127.0.0.1:6437`），負責讀取它，再用 WebSocket 把手送出來。`LeapInput` 連到那個橋接程式，只做一件事：把手變成**正規化的訊號**，絕不碰參數或世界。在演出裡，`createShow({ modules: { leap: true } })` 會建好一個，用 `show.leap` 回傳。主控台的 L1 面板會告訴你哪一段連線不通。"},{"en":"It publishes, for each side (`left`, `right`): `leap/hand/<side>/present` (0 or 1), `x`, `y`, `z` (the palm in the interaction box, 0..1, y up, z toward the player), `pinch` and `grab` (0..1), `roll`, `pitch`, `yaw` (−π..π radians), `speed` (0..1, 1 = 1 m/s), and `<finger>/x|y|z` for the five fingertips. Shared: `leap/status` (0 no bridge, 1 bridge but no tracking service, 2 service but no device, 3 tracking), `leap/hands` (0..2) and `leap/both/distance` (palm to palm, 1 = 40 cm). The full list with its ranges is what [`leapSignals()`](#leapSignals) returns.","zh":"每一邊（`left`、`right`）它會發布：`leap/hand/<side>/present`（0 或 1）、`x`、`y`、`z`（掌心在互動範圍盒裡的位置，0..1，y 向上，z 朝向玩家）、`pinch` 和 `grab`（0..1）、`roll`、`pitch`、`yaw`（−π..π 弧度）、`speed`（0..1，1 = 每秒 1 公尺），還有五個指尖的 `<finger>/x|y|z`。共用的有：`leap/status`（0 沒有橋接、1 有橋接但追蹤服務沒開、2 服務在但沒有裝置、3 追蹤中）、`leap/hands`（0..2）和 `leap/both/distance`（兩掌之間的距離，1 = 40 公分）。完整的清單和範圍就是 [`leapSignals()`](#leapSignals) 的回傳值。"},{"en":"Gestures are pulses with a payload `{ side, x, y, z, strength }`: `leap/hand/<side>/pinch-start|pinch-end` and `grab-start|grab-end`, and `leap/both/pinch-start|pinch-end` (payload `{ x, y, z, distance }` at the midpoint). They use hysteresis, so a pinch hovering near the threshold fires once and not sixty times a second (see [`HandGestures`](#HandGestures)). A hand that leaves the view lets go: its `present`, `pinch`, `grab` and `speed` go to 0 and its gestures end.","zh":"手勢是帶有 `{ side, x, y, z, strength }` 的脈衝：`leap/hand/<side>/pinch-start|pinch-end` 和 `grab-start|grab-end`，以及 `leap/both/pinch-start|pinch-end`（內容是中點的 `{ x, y, z, distance }`）。它們有遲滯，所以在門檻附近徘徊的捏合只會觸發一次，不會一秒鐘觸發六十次（見 [`HandGestures`](#HandGestures)）。手離開畫面就會放開：它的 `present`、`pinch`、`grab` 和 `speed` 歸零，手勢也跟著結束。"},{"en":"For continuous control, map a signal to a param (`{ source: 'leap/hand/right/y', target: 'energy' }`) so the timeline, a knob and a hand are interchangeable. Use `leap.hands` (joints in millimetres) only to draw the hand itself.","zh":"連續控制請把訊號映射到參數（`{ source: 'leap/hand/right/y', target: 'energy' }`），這樣時間軸、旋鈕和手就可以互換。`leap.hands`（以公釐計的關節）只用來把手本身畫出來。"}],"params":[{"name":"signals","type":"Signals","optional":true,"default":"null","en":"The [`Signals`](#Signals) registry to publish into. Without one the input still works (`hands`, `onFrame`) but publishes nothing.","zh":"要發布進去的 [`Signals`](#Signals) 登錄表。沒有的話，這個輸入仍然能用（`hands`、`onFrame`），只是不發布任何訊號。"},{"name":"url","type":"String","optional":true,"default":"ws://127.0.0.1:6437/v6.json","en":"The bridge address: the LeapJS protocol (the default, [`DEFAULT_URL`](#DEFAULT_URL)) or `…/raw` for the raw line format. Any service that still speaks the LeapJS v6 protocol works too.","zh":"橋接程式的位址：LeapJS 通訊協定（預設，[`DEFAULT_URL`](#DEFAULT_URL)），或 `…/raw` 的原始行格式。任何還在講 LeapJS v6 通訊協定的服務也行。"},{"name":"reconnect","type":"Boolean","optional":true,"default":"true","en":"Whether to try again after the connection drops, waiting 0.5 s, then 1 s, 2 s … up to 5 s between tries.","zh":"連線中斷之後要不要重試：每次等待 0.5 秒、1 秒、2 秒……最長 5 秒。"},{"name":"fingers","type":"Boolean","optional":true,"default":"true","en":"Publish the fingertip signals (3 per finger, 30 across two hands). `false` leaves them out and keeps the signal list short.","zh":"要不要發布指尖訊號（每根手指 3 個，兩隻手共 30 個）。`false` 會省略它們，訊號清單比較短。"},{"name":"box","type":"Object","optional":true,"default":"INTERACTION_BOX","en":"The interaction box `{ center, size }` in millimetres that positions are normalised against. The default is [`INTERACTION_BOX`](#INTERACTION_BOX).","zh":"位置據以正規化的互動範圍盒 `{ center, size }`，單位是公釐。預設是 [`INTERACTION_BOX`](#INTERACTION_BOX)。"},{"name":"thresholds","type":"Object","optional":true,"default":"THRESHOLDS","en":"Overrides for the gesture hysteresis, such as `{ pinch: { on: 0.9, off: 0.6 } }` (see [`THRESHOLDS`](#THRESHOLDS)). Keys you leave out keep their defaults.","zh":"手勢遲滯值的覆寫，例如 `{ pinch: { on: 0.9, off: 0.6 } }`（見 [`THRESHOLDS`](#THRESHOLDS)）。沒寫的鍵維持預設。"},{"name":"WebSocket","type":"Function","optional":true,"default":"globalThis.WebSocket","en":"The WebSocket constructor to use. Pass your own to test without a network.","zh":"要用的 WebSocket 建構函式。傳你自己的，就能在沒有網路的情況下測試。"}],"needs":{"en":"For real hands: the bridge must be running (`node packages/leap/bridge/leap-bridge.mjs`, or `--mock` for animated made-up hands) and you must call `connect()`. Nothing is published until the first frame arrives. No sensor and no bridge: feed it hands with `ingest()`, or call `simulate()` to turn the mouse into hands.","zh":"要接真的手：橋接程式必須在跑（`node packages/leap/bridge/leap-bridge.mjs`，或加 `--mock` 得到會動的假手），而且你要呼叫 `connect()`。第一個影格到達之前不會發布任何東西。沒有感測器也沒有橋接程式：用 `ingest()` 餵手給它，或呼叫 `simulate()` 把滑鼠變成手。"},"notes":[{"en":"Fields to read: `status` (`'idle'`, `'connecting'`, `'bridge'`, `'no-service'`, `'no-device'`, `'tracking'`, `'simulated'`, `'closed'`), `hands` (an array; each hand has `id`, `side`, `palm`, `fingers` (5 × 5 joints), `pinch`, `grab`, `velocity`, `normal`, `direction` … in millimetres), `frame` (the latest message as it came), `frames`, `fps`, `device`, `serviceVersion`, `gestures` and `box`.","zh":"可以讀的欄位：`status`（`'idle'`、`'connecting'`、`'bridge'`、`'no-service'`、`'no-device'`、`'tracking'`、`'simulated'`、`'closed'`）、`hands`（陣列，每隻手有 `id`、`side`、`palm`、`fingers`（5 × 5 個關節）、`pinch`、`grab`、`velocity`、`normal`、`direction` ……，單位是公釐）、`frame`（最近收到的原始訊息）、`frames`、`fps`、`device`、`serviceVersion`、`gestures` 和 `box`。"},{"en":"While the simulator runs it owns the hands: real frames from the bridge are ignored, and what the bridge says is remembered and restored when the simulator stops.","zh":"模擬器在跑的時候，手由它掌管：橋接程式來的真實影格會被忽略，橋接程式的狀態會被記住，等模擬器停止再恢復。"},{"en":"The signals are normalised (y = 1 is up). `leap.hands` and the LeapJS frames keep the sensor's own millimetre units, with +z toward the player.","zh":"訊號是正規化的（y = 1 代表向上）。`leap.hands` 和 LeapJS 的影格則保留感測器自己的公釐單位，+z 朝向玩家。"},{"en":"How the bridge talks, the formats and the troubleshooting table are in the [package README](https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/README.md).","zh":"橋接程式怎麼溝通、各種格式和疑難排解表，都在[套件的 README](https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/README.md)。"}],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { LeapInput, mockFrame } from '@openav/leap';\n\nconst signals = new Signals();\nconst leap = new LeapInput({ signals });\nleap.ingest(mockFrame(2.0));                       // a made-up frame: the same path real frames take\nconsole.log(leap.status, leap.hands.length);       // tracking 2\nconsole.log(signals.get('leap/hand/right/pinch')); // 1\nleap.skeleton(document.getElementById('c').getContext('2d'), 400, 300);","caption":{"en":"The console prints `tracking 2` and `1` (the mock right hand is pinching at second 2), and both hands are drawn on the canvas.","zh":"主控台印出 `tracking 2` 和 `1`（假的右手在第 2 秒正在捏合），兩隻手也畫在畫布上。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { LeapInput, mockFrame } from '@openav/leap';\n\nconst signals = new Signals();\nconst leap = new LeapInput({ signals });\nsignals.onAny((name, value, meta) => {\n  if (meta.kind === 'pulse') console.log(name, value.strength.toFixed(2));\n});\nfor (let i = 0; i <= 360; i++) leap.ingest(mockFrame(i / 60));   // six seconds of mock hands at 60 frames a second\nconsole.log(signals.get('leap/hands'), signals.get('leap/status'));","caption":{"en":"Six seconds of mock hands, with the pulses listed as they fire: the right hand pinches twice (`leap/hand/right/pinch-start 0.86`, then `pinch-end 0.50`, again) and the left fist closes once (`leap/hand/left/grab-start 0.80`). It ends with `2 3` (two hands, status 3 = tracking).","zh":"六秒鐘的假手，脈衝在觸發時一一列出：右手捏合兩次（`leap/hand/right/pinch-start 0.86`，然後 `pinch-end 0.50`，再來一次），左手握拳一次（`leap/hand/left/grab-start 0.80`）。最後印出 `2 3`（兩隻手，狀態 3 = 追蹤中）。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { LeapInput } from '@openav/leap';\n\nconst signals = new Signals();\nconst leap = new LeapInput({ signals });\nconst ctx = document.getElementById('c').getContext('2d');\n\nleap.simulate({ target: window });                  // mouse and touch become hands\nleap.onFrame(() => {\n  ctx.clearRect(0, 0, 400, 300);\n  leap.skeleton(ctx, 400, 300);\n  document.getElementById('app').textContent = 'y ' + signals.get('leap/hand/right/y').toFixed(2) + '  pinch ' + signals.get('leap/hand/right/pinch').toFixed(2);\n});","caption":{"en":"Move the mouse over the page: a right hand follows it, and the text shows its height (`y`). Press the button to pinch, right-click to make a fist.","zh":"在頁面上移動滑鼠：右手會跟著動，文字顯示它的高度（`y`）。按下按鍵是捏合，按右鍵是握拳。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { LeapInput } from '@openav/leap';\n\nconst signals = new Signals();\nconst leap = new LeapInput({ signals }).connect();       // ws://127.0.0.1:6437/v6.json\nleap.onStatus((status) => console.log('leap', status));  // connecting, bridge, tracking …\nsignals.on('leap/hand/right/pinch-start', ({ x, y }) => console.log('pinch at', x, y));","caption":{"en":"With the bridge running and a hand over the sensor, the console follows the link (`leap connecting`, `leap bridge`, `leap tracking`) and prints a line for every pinch.","zh":"橋接程式開著、手放在感測器上方，主控台會跟著連線的進展（`leap connecting`、`leap bridge`、`leap tracking`），每次捏合印出一行。"},"run":"static","lang":"js"}],"related":["leapSignals","mockFrame","HandGestures","mountLeapPanel","normalizePoint","handsOf","Signals","createShow"],"keywords":{"en":["leap","leap motion","ultraleap","hand tracking","hands","pinch","grab","fist","palm","fingertip","bridge","gesture","simulate"],"zh":["leap","leap motion","手部追蹤","手","捏合","握拳","掌心","指尖","橋接","手勢","模擬"]},"tbd":[{"en":"Not tried with a real Leap Motion sensor here: the examples feed made-up hands (`mockFrame`) through the same `ingest()` path.","zh":"這裡沒有用真正的 Leap Motion 感測器試過：範例是把假手（`mockFrame`）餵進同一條 `ingest()` 路徑。"},{"en":"On a public https page, whether Chrome asks for permission before the page may reach `127.0.0.1` (the package README says it needs the \"loopback-network\" permission) was not tried here.","zh":"在公開的 https 頁面上，Chrome 是否會在頁面連到 `127.0.0.1` 之前詢問權限（套件 README 說需要「loopback-network」權限），這裡沒有實測。"}],"claims":[],"members":[{"name":"connect","member":"method","signature":"","doc":"Open the bridge connection (reconnects with back-off until disconnect()).","line":101},{"name":"disconnect","member":"method","signature":"","doc":"","line":107},{"name":"connected","member":"getter","signature":"","doc":"","line":116},{"name":"hand","member":"method","signature":"side","doc":"The hand on one side, or null.","line":119},{"name":"normalize","member":"method","signature":"p, clamp = true","doc":"mm → 0..1 in this input's interaction box.","line":122},{"name":"onFrame","member":"method","signature":"cb","doc":"","line":124},{"name":"onStatus","member":"method","signature":"cb","doc":"","line":125},{"name":"ingest","member":"method","signature":"msg","doc":"Feed one parsed message (what the socket would deliver): a v6 header, a v6 frame, a device event, or a raw status / frame line.","line":132},{"name":"simulate","member":"method","signature":"{ target = typeof window !== 'undefined' ? window : null, hz = 60 } = {}","doc":"Mouse / touch become hands, through the same path as the sensor.","line":170},{"name":"simulating","member":"getter","signature":"","doc":"","line":213},{"name":"stopSimulation","member":"method","signature":"","doc":"Stop the simulator (the bridge's hands come back if it is connected).","line":216},{"name":"project","member":"method","signature":"p, w, h, { view = 'oblique', box = this.box } = {}","doc":"Uses this input's interaction box (so drawings line up with the leap/* signals) unless `box` is given.","line":227},{"name":"skeleton","member":"method","signature":"ctx, w, h, { view = 'oblique', box = this.box, colorLeft = 'rgba(55,201,120,0.95)', colorRight = 'rgba(255,209,102,0.95)', lineWidth = 2, hands = this.hands } = {}","doc":"Draw the hands onto a 2D context (see project() for views).","line":235},{"name":"mountPanel","member":"method","signature":"el, opts","doc":"A small status + skeleton panel (the console's L1 section uses it; so does the lab).","line":264}]},
{"id":"leapSignals","key":"leapSignals","name":"leapSignals","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"leapSignals({ fingers = true } = {})","import":"import { leapSignals } from '@openav/leap';","source":{"file":"packages/leap/index.js","line":43,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/index.js#L43"},"summary":{"en":"The list of every signal `LeapInput` publishes, with its range, as `[[name, meta], …]`, ready to hand to `signals.define()`.","zh":"`LeapInput` 發布的所有訊號清單，連同範圍，格式是 `[[name, meta], …]`，可以直接交給 `signals.define()`。"},"description":[{"en":"`new LeapInput({ signals })` already declares all of these, so you rarely call this yourself. It is useful when you want the list: to build a signal picker, to check that a route's source exists, or to declare the signals before the sensor is connected. The names it returns are the vocabulary of the [Signals reference](https://openaudiovisual.com/docs/#signals) for Leap.","zh":"`new LeapInput({ signals })` 已經把這些全部宣告好了，所以很少需要自己呼叫。想要這份清單的時候它就很有用：做訊號選單、檢查某條路由的來源存不存在、或在感測器還沒接上之前先宣告訊號。它回傳的名稱就是[訊號參考](https://openaudiovisual.com/zh/docs/#signals)裡 Leap 的那一批詞彙。"}],"params":[{"name":"fingers","type":"Boolean","optional":true,"default":"true","en":"Include the fingertip signals. `false` leaves out the 30 of them.","zh":"要不要包含指尖訊號。`false` 會少掉那 30 個。"}],"returns":{"type":"Array","en":"The pairs `[name, meta]`: 63 with fingers, 33 without. Each `meta` has `source: 'leap'`, and `min` and `max` (the roll, pitch and yaw ones in radians, −π..π) or `kind: 'pulse'`.","zh":"`[name, meta]` 的成對清單：有指尖是 63 個，沒有是 33 個。每個 `meta` 都有 `source: 'leap'`，以及 `min` 和 `max`（roll、pitch、yaw 是弧度，−π..π），脈衝則是 `kind: 'pulse'`。"},"notes":[],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { leapSignals } from '@openav/leap';\n\nconsole.log(leapSignals().length, leapSignals({ fingers: false }).length);   // 63 33\nconst signals = new Signals();\nfor (const [name, meta] of leapSignals({ fingers: false })) signals.define(name, meta);\nconsole.log(signals.meta.get('leap/hand/right/roll').min);                   // -3.14159…\nconsole.log(signals.meta.get('leap/hand/left/pinch-start').kind);            // pulse","caption":{"en":"The console prints `63 33`, then the roll range's lower end (−π) and `pulse`.","zh":"主控台印出 `63 33`，然後是 roll 範圍的下限（−π）和 `pulse`。"},"run":"module","lang":"js"}],"related":["LeapInput","Signals","mockFrame"],"keywords":{"en":["leap signals","signal list","leap/hand","define signals","declarations"],"zh":["leap 訊號","訊號清單","宣告訊號","訊號名稱"]},"tbd":[],"claims":[]},
{"id":"mockFrame","key":"mockFrame","name":"mockFrame","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"mockFrame(t, id = Math.round(t * 60), { hands = 2 } = {})","import":"import { mockFrame } from '@openav/leap';","source":{"file":"packages/leap/mock.js","line":104,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/mock.js#L104"},"summary":{"en":"One frame of made-up, animated hands at time `t`: a right hand circling and pinching every 3 seconds, a left hand rising, falling and slowly making a fist.","zh":"時間 `t` 的一個影格的假手，會動：右手繞圈、每 3 秒捏合一次，左手升降，並且慢慢握拳。"},"description":[{"en":"This is the bridge's `--mock` stream and the way to try Leap code without a sensor. The right hand pinches for about a second out of every 3, the left hand pinches once every 12 seconds, and at those moments (for example second 8) both are pinched, so the both-hands pinch fires too. The left fist comes and goes slowly. The output goes through exactly the same conversion and publishing as a real Leap Motion Controller.","zh":"這就是橋接程式 `--mock` 模式送出的串流，也是沒有感測器時試 Leap 程式的辦法。右手每 3 秒有大約 1 秒在捏合，左手每 12 秒捏合一次，兩者重疊的時刻（例如第 8 秒），兩隻手同時捏合，所以雙手捏合也會觸發。左手的拳頭則慢慢地出現又消失。輸出走的轉換與發布路徑，跟真正的 Leap Motion Controller 完全一樣。"}],"params":[{"name":"t","type":"Number","en":"Time in seconds. The result depends only on `t`, so it is the same every time.","zh":"時間，單位是秒。結果只跟 `t` 有關，所以每次都一樣。"},{"name":"id","type":"Number","optional":true,"default":"Math.round(t * 60)","en":"The frame number written into the frame.","zh":"寫進影格裡的影格編號。"},{"name":"hands","type":"Number","optional":true,"default":"2","en":"How many hands: 2 (right and left), 1 (the right only) or 0.","zh":"手的數量：2（右手和左手）、1（只有右手）或 0。"}],"returns":{"type":"Object","en":"A frame in the raw line format: `{ t: 'f', id, fps: 60, hands: [hand, …] }`, the right hand (`side: 'R'`) first. Feed it to [`LeapInput.ingest()`](#LeapInput) or [`handsOf()`](#handsOf).","zh":"原始行格式的影格：`{ t: 'f', id, fps: 60, hands: [hand, …] }`，右手（`side: 'R'`）在前面。可以餵給 [`LeapInput.ingest()`](#LeapInput) 或 [`handsOf()`](#handsOf)。"},"notes":[],"examples":[{"code":"import { mockFrame } from '@openav/leap';\n\nfor (const t of [0.5, 2.0, 8.0]) {\n  const frame = mockFrame(t);\n  console.log(t, frame.hands.map((h) => h.side + ' pinch ' + h.pinch).join(' · '));\n}","caption":{"en":"The console prints `0.5 R pinch 0 · L pinch 0`, `2 R pinch 1 · L pinch 0` and `8 R pinch 1 · L pinch 1` (the both-hands pinch).","zh":"主控台印出 `0.5 R pinch 0 · L pinch 0`、`2 R pinch 1 · L pinch 0` 和 `8 R pinch 1 · L pinch 1`（雙手捏合）。"},"run":"module","lang":"js"}],"related":["mockHand","LeapInput","handsOf"],"keywords":{"en":["mock","fake hands","test frame","demo hands","no sensor","leap simulation","mock frame"],"zh":["假手","模擬","測試影格","沒有感測器","示範用的手","mock"]},"tbd":[],"claims":[]},
{"id":"mockHand","key":"mockHand","name":"mockHand","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"mockHand({ id = 1, side = 'R', palm = [0, 200, 0], pinch = 0, grab = 0, roll = 0, yaw = 0, pitch = 0, vel = [0, 0, 0], conf = 1, width = 85 } = {})","import":"import { mockHand } from '@openav/leap';","source":{"file":"packages/leap/mock.js","line":42,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/mock.js#L42"},"summary":{"en":"Builds one made-up hand, with all five fingers and the arm, from a palm position plus pinch, grab, roll, yaw and pitch.","zh":"用掌心位置加上捏合、握拳、roll、yaw、pitch，做出一隻假手，五根手指和手臂都有。"},"description":[{"en":"It places a plausible hand in space: palm, knuckles, fingers curled by `grab`, thumb and index meeting as `pinch` goes to 1, wrist and elbow. [`LeapInput.simulate()`](#LeapInput) builds its hands from the mouse with it, and [`mockFrame()`](#mockFrame) with the animated ones. Use it to put a hand exactly where a test needs it.","zh":"它在空間裡放一隻合理的手：掌心、指節、隨 `grab` 彎起來的手指、`pinch` 到 1 時碰在一起的拇指和食指、手腕和手肘。[`LeapInput.simulate()`](#LeapInput) 用它把滑鼠變成手，[`mockFrame()`](#mockFrame) 用它做出會動的手。需要把手剛好放在某個位置來測試時，就用它。"}],"params":[{"name":"id","type":"Number","optional":true,"default":"1","en":"The hand's id.","zh":"這隻手的 id。"},{"name":"side","type":"'L' | 'R'","optional":true,"default":"'R'","en":"`'R'` for a right hand, `'L'` for a left.","zh":"右手用 `'R'`，左手用 `'L'`。"},{"name":"palm","type":"Array<Number>","optional":true,"default":"[0, 200, 0]","en":"The palm position `[x, y, z]` in millimetres (+x right, +y up, +z toward the player).","zh":"掌心位置 `[x, y, z]`，單位是公釐（+x 向右、+y 向上、+z 朝向玩家）。"},{"name":"pinch","type":"Number","optional":true,"default":"0","en":"Thumb to index, 0..1. At 1 the two fingertips meet.","zh":"拇指到食指，0..1。到 1 的時候兩個指尖碰在一起。"},{"name":"grab","type":"Number","optional":true,"default":"0","en":"How closed the fist is, 0..1.","zh":"拳頭握得多緊，0..1。"},{"name":"roll","type":"Number","optional":true,"default":"0","en":"Radians; positive rolls toward the thumb side.","zh":"弧度，正值是往拇指那一側翻。"},{"name":"yaw","type":"Number","optional":true,"default":"0","en":"Radians; positive turns the fingers toward +x.","zh":"弧度，正值是讓手指轉向 +x。"},{"name":"pitch","type":"Number","optional":true,"default":"0","en":"Radians; positive tips the fingers up.","zh":"弧度，正值是讓手指往上翹。"},{"name":"vel","type":"Array<Number>","optional":true,"default":"[0, 0, 0]","en":"Palm velocity in millimetres per second.","zh":"掌心速度，單位是公釐每秒。"},{"name":"conf","type":"Number","optional":true,"default":"1","en":"Tracking confidence, 0..1.","zh":"追蹤的信心程度，0..1。"},{"name":"width","type":"Number","optional":true,"default":"85","en":"Palm width in millimetres.","zh":"手掌寬度，單位是公釐。"}],"returns":{"type":"Object","en":"A hand in the raw line format: `{ id, side, conf, pinch, grab, palm, vel, n, dir, w, f, ext, arm }`, where `f` is 5 fingers × 5 joints (thumb to pinky; the last joint of each is the tip). Wrap hands in `{ t: 'f', id, fps, hands: [...] }` to make a frame.","zh":"原始行格式的手：`{ id, side, conf, pinch, grab, palm, vel, n, dir, w, f, ext, arm }`，其中 `f` 是 5 根手指 × 5 個關節（從拇指到小指，每根的最後一個關節是指尖）。把手包進 `{ t: 'f', id, fps, hands: [...] }` 就成了一個影格。"},"notes":[],"examples":[{"code":"import { mockHand } from '@openav/leap';\n\nconst open = mockHand({ palm: [0, 200, 0] });\nconst pinched = mockHand({ palm: [0, 200, 0], pinch: 1 });\nconst gap = (hand) => Math.hypot(...hand.f[0][4].map((v, i) => v - hand.f[1][4][i])).toFixed(0);\nconsole.log('thumb to index tip, open:', gap(open), 'mm  pinched:', gap(pinched), 'mm');","caption":{"en":"The console measures the distance between the thumb tip and the index tip: `100 mm` for the open hand and `0 mm` when pinched.","zh":"主控台量出拇指指尖和食指指尖的距離：手張開時是 `100 mm`，捏合時是 `0 mm`。"},"run":"module","lang":"js"}],"related":["mockFrame","LeapInput","handsOf","handBones"],"keywords":{"en":["mock hand","fake hand","hand model","test hand","pinch","grab","palm position"],"zh":["假手","手的模型","測試用的手","捏合","握拳","掌心位置"]},"tbd":[],"claims":[]},
{"id":"mountLeapPanel","key":"mountLeapPanel","name":"mountLeapPanel","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"mountLeapPanel(el, leap, { width = 300, simulate = true, simTarget } = {})","import":"import { mountLeapPanel } from '@openav/leap';","source":{"file":"packages/leap/panel.js","line":28,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/panel.js#L28"},"summary":{"en":"Mounts a small panel that shows whether the Leap is alive (bridge, service, device, hands) and draws what it sees, with a button to simulate hands.","zh":"掛上一個小面板，顯示 Leap 有沒有活著（橋接、服務、裝置、手）並畫出它看到的東西，還有一個模擬手的按鈕。"},"description":[{"en":"One status line says which link is the broken one, in order: no bridge, bridge but no tracking service, service but no device, tracking (with the number of hands and the frame rate). Under it, a small front view draws the interaction box (a hand outside it is visibly outside, its signals sitting clamped at the edge) and the hand skeletons. It redraws only on frames and status changes, so nothing runs while the sensor is idle.","zh":"一行狀態列會告訴你是哪一段連線壞了，依序是：沒有橋接、有橋接但追蹤服務沒開、有服務但沒有裝置、追蹤中（附上手的數量和影格率）。下面是個小小的正視圖，畫出互動範圍盒（手在盒子外面一眼就看得出來，它的訊號會被夾在邊緣）和手的骨架。它只在有影格或狀態改變時重畫，感測器閒置時什麼都不會跑。"},{"en":"In a show you do not call it: the console's **L1 · Input** panel mounts it when you declare `modules: { leap: true }`.","zh":"在演出裡不用自己呼叫：宣告 `modules: { leap: true }` 之後，主控台的 **L1 · Input** 面板會掛上它。"}],"params":[{"name":"el","type":"HTMLElement","en":"Where to put the panel.","zh":"面板要放在哪個元素裡。"},{"name":"leap","type":"LeapInput","en":"The [`LeapInput`](#LeapInput) to show.","zh":"要顯示的 [`LeapInput`](#LeapInput)。"},{"name":"width","type":"Number","optional":true,"default":"300","en":"The width of the drawing in CSS pixels; the height follows (about 0.62 of the width).","zh":"繪圖區的寬度（CSS 像素），高度跟著決定（約為寬度的 0.62 倍）。"},{"name":"simulate","type":"Boolean","optional":true,"default":"true","en":"Show the \"simulate\" button (mouse and touch become hands).","zh":"要不要顯示「simulate」按鈕（滑鼠和觸控變成手）。"},{"name":"simTarget","type":"HTMLElement | Window","optional":true,"en":"What the simulator listens on. Defaults to `leap.simTarget`, then the window.","zh":"模擬器監聽哪個目標。預設是 `leap.simTarget`，再不然是 window。"}],"returns":{"type":"Object","en":"`{ el, render(), destroy() }`: the panel's root element, a function that redraws it, and one that removes it and unsubscribes.","zh":"`{ el, render(), destroy() }`：面板的根元素、重畫它的函式，以及移除它並取消訂閱的函式。"},"notes":[],"examples":[{"code":"import { LeapInput, mockFrame, mountLeapPanel } from '@openav/leap';\n\nconst leap = new LeapInput();\nconst panel = mountLeapPanel(document.getElementById('app'), leap, { width: 360 });\nleap.ingest(mockFrame(2.0));          // a made-up frame stands in for the sensor\npanel.render();","caption":{"en":"A panel appears in the page: a green dot, the status line `tracking · 2 hands · 60 fps · R:pinch` (the right hand is pinching), a \"simulate\" button, and the two mock hands drawn in a small box.","zh":"頁面上出現一個面板：綠色圓點、狀態列 `tracking · 2 hands · 60 fps · R:pinch`（右手正在捏合）、一個「simulate」按鈕，以及畫在小方框裡的兩隻假手。"},"run":"module","lang":"js"}],"related":["LeapInput","createShow","mountConsole"],"keywords":{"en":["leap panel","status panel","skeleton","hands view","simulate button","leap status"],"zh":["leap 面板","狀態面板","骨架","手的畫面","模擬按鈕","leap 狀態"]},"tbd":[],"claims":[]},
{"id":"normalizePoint","key":"normalizePoint","name":"normalizePoint","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"normalizePoint(p, box = INTERACTION_BOX, clamp = true)","import":"import { normalizePoint } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":78,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L78"},"summary":{"en":"Maps a point in millimetres to 0..1 inside the interaction box, clamped by default: the same normalisation the `leap/*` signals use.","zh":"把一個以公釐計的點，換算成互動範圍盒裡的 0..1，預設會夾住：`leap/*` 訊號用的就是這個正規化。"},"description":[{"en":"The classic box is centred 200 mm above the sensor and about 235 × 235 × 148 mm ([`INTERACTION_BOX`](#INTERACTION_BOX)). A palm at the centre reads `[0.5, 0.5, 0.5]`; a palm far outside it reads 0 or 1 on that axis, which is why the signals \"sit clamped at the edge\" when a hand leaves the box. Pass `clamp = false` when you want to know how far outside it is.","zh":"經典的盒子中心在感測器上方 200 公釐，大小約 235 × 235 × 148 公釐（[`INTERACTION_BOX`](#INTERACTION_BOX)）。掌心在中心讀到 `[0.5, 0.5, 0.5]`，掌心遠在盒子外面，那個軸就讀到 0 或 1，這就是手離開盒子時訊號會「夾在邊緣」的原因。想知道超出多遠，就傳 `clamp = false`。"}],"params":[{"name":"p","type":"Array<Number>","en":"The point `[x, y, z]` in millimetres.","zh":"點 `[x, y, z]`，單位是公釐。"},{"name":"box","type":"Object","optional":true,"default":"INTERACTION_BOX","en":"The box `{ center, size }` in millimetres.","zh":"範圍盒 `{ center, size }`，單位是公釐。"},{"name":"clamp","type":"Boolean","optional":true,"default":"true","en":"Hold each value to 0..1. `false` lets a point outside the box go beyond.","zh":"把每個值限制在 0..1。`false` 的話，盒子外面的點可以超出範圍。"}],"returns":{"type":"Array<Number>","en":"`[x, y, z]`, each `(p − center) / size + 0.5`: 0.5 is the centre of the box, x grows to the right, y upward, z toward the player.","zh":"`[x, y, z]`，每個都是 `(p − center) / size + 0.5`：0.5 是盒子的中心，x 往右增加、y 往上增加、z 朝向玩家。"},"notes":[],"examples":[{"code":"import { normalizePoint } from '@openav/leap';\n\nconsole.log(normalizePoint([0, 200, 0]));                          // [ 0.5, 0.5, 0.5 ]: the centre\nconsole.log(normalizePoint([500, 0, -500]));                       // [ 1, 0, 0 ]: far outside, clamped\nconsole.log(normalizePoint([100, 320, 0]).map((v) => +v.toFixed(2)));   // [ 0.93, 1, 0.5 ]","caption":{"en":"The console prints `[ 0.5, 0.5, 0.5 ]`, `[ 1, 0, 0 ]` and `[ 0.93, 1, 0.5 ]`.","zh":"主控台印出 `[ 0.5, 0.5, 0.5 ]`、`[ 1, 0, 0 ]` 和 `[ 0.93, 1, 0.5 ]`。"},"run":"module","lang":"js"}],"related":["INTERACTION_BOX","LeapInput","handAngles"],"keywords":{"en":["normalize","interaction box","millimetres","0..1","clamp","palm position","scale hand"],"zh":["正規化","互動範圍盒","公釐","0..1","夾住","掌心位置"]},"tbd":[],"claims":[]},
{"id":"palmBasis","key":"palmBasis","name":"palmBasis","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"palmBasis(direction, normal, left)","import":"import { palmBasis } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":60,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L60"},"summary":{"en":"An advanced helper: the orthonormal basis `[x, y, z]` of the palm, from its direction and normal, in the rows LeapJS reads as `hand.r`.","zh":"進階用的輔助函式：由手的方向和法線算出掌心的正交基底 `[x, y, z]`，列的排法就是 LeapJS 當作 `hand.r` 來讀的那種。"},"description":[{"en":"Like [`boneBasis()`](#boneBasis), it exists so that the LeapJS frames [`V6Converter`](#V6Converter) writes have a `r` that `hand.rotationAngle()` can use. Worlds that draw hands read `leap.hands` and [`handAngles()`](#handAngles) instead.","zh":"和 [`boneBasis()`](#boneBasis) 一樣，它的存在是為了讓 [`V6Converter`](#V6Converter) 寫出的 LeapJS 影格有 `hand.rotationAngle()` 能用的 `r`。要畫手的世界讀的是 `leap.hands` 和 [`handAngles()`](#handAngles)。"}],"params":[{"name":"direction","type":"Array<Number>","en":"The direction the fingers point.","zh":"手指指的方向。"},{"name":"normal","type":"Array<Number>","en":"The palm normal.","zh":"掌心法線。"},{"name":"left","type":"Boolean","en":"`true` for a left hand: x is negated.","zh":"左手傳 `true`：x 會反向。"}],"returns":{"type":"Array","en":"Three rows `[x, y, z]` (x sideways, y the back of the hand, z toward the wrist), rounded to 3 decimals.","zh":"三列 `[x, y, z]`（x 是橫向、y 是手背、z 朝向手腕），四捨五入到小數三位。"},"notes":[],"examples":[{"code":"import { palmBasis } from '@openav/leap';\n\nconsole.log(palmBasis([0, 0, -1], [0, -1, 0], false));   // flat hand, palm down, fingers forward: the identity","caption":{"en":"The console prints the identity `[ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ]`.","zh":"主控台印出單位矩陣 `[ [ 1, 0, 0 ], [ 0, 1, 0 ], [ 0, 0, 1 ] ]`。"},"run":"module","lang":"js"}],"related":["boneBasis","handAngles","V6Converter"],"keywords":{"en":["palm basis","rotation","leapjs","hand.r","orthonormal"],"zh":["掌心基底","旋轉","leapjs","hand.r","正交"]},"tbd":[],"claims":[]},
{"id":"PROTOCOL_VERSION","key":"PROTOCOL_VERSION","name":"PROTOCOL_VERSION","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"PROTOCOL_VERSION = 6","import":"import { PROTOCOL_VERSION } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":27,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L27"},"summary":{"en":"The highest LeapJS protocol version we speak: 6. LeapJS 1.1.1 and earlier throw \"unrecognized version\" above that.","zh":"我們支援的 LeapJS 通訊協定最高版本：6。LeapJS 1.1.1 和更早的版本遇到更高的版本會丟出 \"unrecognized version\"。"},"description":[{"en":"[`v6Header()`](#v6Header) never reports more than this, and the bridge answers `/v7.json` as version 6. It is the default of the `version` argument there.","zh":"[`v6Header()`](#v6Header) 不會回報比這更高的版本，橋接程式也把 `/v7.json` 當成版本 6 回應。它是那裡 `version` 參數的預設值。"}],"params":[],"notes":[],"examples":[],"related":["v6Header","protocolOfPath","DEFAULT_URL"],"keywords":{"en":["protocol version","leapjs version","v6","version 6"],"zh":["通訊協定版本","leapjs 版本","v6","版本 6"]},"tbd":[],"claims":[]},
{"id":"protocolOfPath","key":"protocolOfPath","name":"protocolOfPath","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"protocolOfPath(pathname)","import":"import { protocolOfPath } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":202,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L202"},"summary":{"en":"Which protocol version a `/vN.json` path asks for, as a number, or `null` when the path is not one.","zh":"`/vN.json` 這種路徑要求的是哪個通訊協定版本，回傳數字，路徑不是這種格式就回傳 `null`。"},"description":[{"en":"The bridge uses it to decide whether a WebSocket upgrade is a LeapJS client at all: `/v6.json` yes, `/raw` is handled separately, anything else is refused with a 404.","zh":"橋接程式用它判斷一個 WebSocket 升級請求是不是 LeapJS 的用戶端：`/v6.json` 是，`/raw` 另外處理，其他的一律回 404 拒絕。"}],"params":[{"name":"pathname","type":"String","en":"The URL path, such as `'/v6.json'`.","zh":"網址的路徑，例如 `'/v6.json'`。"}],"returns":{"type":"Number | null","en":"The number in `/v<number>.json`, or `null`. It does not clip to 6: `/v7.json` gives 7 (the bridge then answers it as version 6).","zh":"`/v<數字>.json` 裡的數字，或 `null`。它不會截到 6：`/v7.json` 得到 7（橋接程式再以版本 6 回應）。"},"notes":[],"examples":[{"code":"import { protocolOfPath } from '@openav/leap';\n\nconsole.log(protocolOfPath('/v6.json'), protocolOfPath('/v3.json'), protocolOfPath('/v7.json'));   // 6 3 7\nconsole.log(protocolOfPath('/raw'), protocolOfPath('/v6.jsonx'), protocolOfPath(undefined));        // null null null","caption":{"en":"The console prints `6 3 7`, then `null null null`.","zh":"主控台印出 `6 3 7`，然後是 `null null null`。"},"run":"module","lang":"js"}],"related":["v6Header","PROTOCOL_VERSION"],"keywords":{"en":["protocol path","v6.json","protocol version","bridge endpoint"],"zh":["通訊協定路徑","v6.json","通訊協定版本","橋接端點"]},"tbd":[],"claims":[]},
{"id":"rawToV6","key":"rawToV6","name":"rawToV6","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"rawToV6(raw, tUs, opts)","import":"import { rawToV6 } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":185,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L185"},"summary":{"en":"One-off conversion of a raw frame to a LeapJS v6 frame, with a fresh converter each time (no history, so no velocities).","zh":"把一個原始影格一次轉成 LeapJS v6 影格，每次都用全新的轉換器（沒有歷史，所以沒有速度）。"},"description":[{"en":"Handy for tests and for looking at what a legacy sketch would receive. For a stream, keep one [`V6Converter`](#V6Converter) instead, so that `timeVisible` and `tipVelocity` can accumulate.","zh":"用在測試，以及查看舊版草圖會收到什麼的時候很方便。處理一條串流的話，請改為保留一個 [`V6Converter`](#V6Converter)，`timeVisible` 和 `tipVelocity` 才能累積。"}],"params":[{"name":"raw","type":"Object","en":"A raw frame `{ t: 'f', id, fps, hands }`.","zh":"原始影格 `{ t: 'f', id, fps, hands }`。"},{"name":"tUs","type":"Number","optional":true,"en":"The time in microseconds. Defaults to now.","zh":"時間，單位是微秒。預設是現在。"},{"name":"opts","type":"Object","optional":true,"en":"Options for the [`V6Converter`](#V6Converter): `{ box, smoothing }`.","zh":"給 [`V6Converter`](#V6Converter) 的選項：`{ box, smoothing }`。"}],"returns":{"type":"Object","en":"The v6 frame. Every field the LeapJS `Frame`, `Hand`, `Pointable`, `Finger` and `Bone` constructors read is there.","zh":"v6 影格。LeapJS 的 `Frame`、`Hand`、`Pointable`、`Finger` 和 `Bone` 建構函式會讀的每個欄位都有。"},"notes":[],"examples":[{"code":"import { mockFrame, rawToV6 } from '@openav/leap';\n\nconst frame = rawToV6(mockFrame(2.0, 120), 5_000_000);\nconsole.log(Object.keys(frame).join(' '));\nconsole.log(frame.hands.length, frame.pointables.length, frame.hands[0].type, frame.hands[0].pinchStrength);","caption":{"en":"The console prints the frame's keys (`id timestamp currentFrameRate hands pointables interactionBox gestures r s t`), then `2 10 right 1`.","zh":"主控台印出影格的欄位（`id timestamp currentFrameRate hands pointables interactionBox gestures r s t`），然後是 `2 10 right 1`。"},"run":"module","lang":"js"}],"related":["V6Converter","handsOf","v6Header","mockFrame"],"keywords":{"en":["raw to v6","convert frame","leapjs frame","one-off conversion"],"zh":["原始轉 v6","轉換影格","leapjs 影格","一次性轉換"]},"tbd":[],"claims":[]},
{"id":"leap-RESERVED_PORTS","key":"RESERVED_PORTS","name":"RESERVED_PORTS","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"RESERVED_PORTS = array, 1 items","import":"import { RESERVED_PORTS } from '@openav/leap/bridge';","source":{"file":"packages/leap/bridge/leap-bridge.mjs","line":31,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/bridge/leap-bridge.mjs#L31"},"summary":{"en":"The ports the Leap bridge refuses to use: `[7457]`, which belongs to The Last Input and the backstage monitor.","zh":"Leap 橋接程式拒絕使用的連接埠：`[7457]`，那是 The Last Input 和後台監看用的。"},"description":[{"en":"`startBridge({ port: 7457 })` throws \"port 7457 belongs to The Last Input\" immediately, so the bridge can never take the port the monitor listens on (`node packages/monitor/server.js`). The out-bridge refuses the same one.","zh":"`startBridge({ port: 7457 })` 會立刻丟出 \"port 7457 belongs to The Last Input\"，所以橋接程式絕不會占走後台監看監聽的連接埠（`node packages/monitor/server.js`）。輸出橋接也一樣拒絕這一個。"}],"params":[],"notes":[],"examples":[],"related":["MonitorFeed"],"keywords":{"en":["reserved ports","7457","port","refused port","monitor port"],"zh":["保留連接埠","7457","連接埠","拒絕的連接埠","監看連接埠"]},"tbd":[],"claims":[]},
{"id":"leap-startBridge","key":"startBridge","name":"startBridge","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"startBridge({ port = DEFAULT_PORT, host = '127.0.0.1', mock = false, hz = 60, bin = null, log = () => {} } = {})","import":"import { startBridge } from '@openav/leap/bridge';","source":{"file":"packages/leap/bridge/leap-bridge.mjs","line":45,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/bridge/leap-bridge.mjs#L45"},"summary":{"en":"Starts the Leap bridge on this machine: a loopback WebSocket and HTTP server that serves the sensor's hands to every page (Node only).","zh":"在這台機器上啟動 Leap 橋接程式：一個只綁 loopback 的 WebSocket 與 HTTP 伺服器，把感測器的手送給每一個頁面（只能在 Node 執行）。"},"description":[{"en":"This is what `node packages/leap/bridge/leap-bridge.mjs` runs (add `--mock` for made-up hands, `--port`, `--hz`, `--bin`, `--quiet`). It serves `ws://127.0.0.1:6437/v6.json` (the LeapJS protocol, which every 2014 to 2021 browser sketch speaks), `ws://…/raw` (the raw lines), the same lines as server-sent events on `GET /raw` and `/hands`, and `GET /health` (source, service, device, fps, hands, clients). Any page origin may read it, as the original Leap service allowed.","zh":"`node packages/leap/bridge/leap-bridge.mjs` 執行的就是它（加 `--mock` 得到假手，另有 `--port`、`--hz`、`--bin`、`--quiet`）。它提供 `ws://127.0.0.1:6437/v6.json`（LeapJS 通訊協定，2014 到 2021 年所有的瀏覽器草圖講的都是它）、`ws://…/raw`（原始行）、`GET /raw` 和 `/hands` 上以 server-sent events 送的同樣內容，以及 `GET /health`（來源、服務、裝置、fps、手、用戶端）。任何頁面來源都可以讀取，跟原本的 Leap 服務一樣。"},{"en":"Call it from your own Node program when you want the bridge inside it, for example in a test. `startBridge()` throws at once for port 7457 (it belongs to The Last Input and the monitor) and for a non-loopback `host`; a port already in use rejects the returned promise with a message that suggests `curl http://127.0.0.1:6437/health`.","zh":"想把橋接程式放進你自己的 Node 程式裡（例如測試）時，就自己呼叫它。連接埠是 7457（那是 The Last Input 和後台監看用的）或 `host` 不是 loopback，`startBridge()` 會立刻丟出錯誤。連接埠已被占用，回傳的 promise 會被拒絕，訊息會建議你執行 `curl http://127.0.0.1:6437/health`。"}],"params":[{"name":"port","type":"Number","optional":true,"default":"DEFAULT_PORT","en":"The port to listen on, [`DEFAULT_PORT`](#leap-DEFAULT_PORT) unless you say otherwise. 0 picks any free one. The ports in [`RESERVED_PORTS`](#leap-RESERVED_PORTS) (7457) are refused.","zh":"要監聽的連接埠，沒特別指定就是 [`DEFAULT_PORT`](#leap-DEFAULT_PORT)。0 表示隨便挑一個空的。[`RESERVED_PORTS`](#leap-RESERVED_PORTS) 裡的連接埠（7457）會被拒絕。"},{"name":"host","type":"String","optional":true,"default":"'127.0.0.1'","en":"The address to bind. Only loopback (`127.0.0.1`, `::1`, `localhost`) is allowed: hand data never leaves the machine.","zh":"要綁定的位址。只允許 loopback（`127.0.0.1`、`::1`、`localhost`）：手的資料絕不會離開這台機器。"},{"name":"mock","type":"Boolean","optional":true,"default":"false","en":"`true` serves two animated made-up hands ([`mockFrame()`](#mockFrame)) instead of reading a sensor.","zh":"`true` 的話，送出兩隻會動的假手（[`mockFrame()`](#mockFrame)），不讀感測器。"},{"name":"hz","type":"Number","optional":true,"default":"60","en":"The most frames per second sent to each client (held to 1..120). A slow client skips frames; it never queues them.","zh":"每個用戶端每秒最多收到幾個影格（限制在 1..120）。慢的用戶端會跳過影格，不會排隊。"},{"name":"bin","type":"String | null","optional":true,"default":"null","en":"The path to the `leap-stream` reader program. By default the bridge compiles its C reader on first start (needs the Xcode command line tools) and caches it.","zh":"`leap-stream` 讀取程式的路徑。預設是橋接程式第一次啟動時編譯它的 C 讀取程式（需要 Xcode 命令列工具）並快取起來。"},{"name":"log","type":"Function","optional":true,"default":"() => {}","en":"Called with a line of text for each event worth knowing (clients coming and going, status changes).","zh":"每一件值得知道的事（用戶端進出、狀態改變）都會以一行文字呼叫它。"}],"returns":{"type":"Promise<Object>","en":"Resolves once listening, with `{ port, state, health(), close() }`: the real port (useful with `port: 0`), the live state, a function returning the `/health` object, and a function that stops everything and returns a Promise.","zh":"開始監聽之後才完成，結果是 `{ port, state, health(), close() }`：實際的連接埠（`port: 0` 時有用）、即時狀態、回傳 `/health` 物件的函式，以及停止一切並回傳 Promise 的函式。"},"needs":{"en":"Node 22 or later, and for real hands the Ultraleap Hand Tracking service plus Xcode's `clang` to build the reader on first start. With `mock: true` neither is needed.","zh":"Node 22 以上，要接真的手，還需要 Ultraleap Hand Tracking 服務，以及第一次啟動時用來編譯讀取程式的 Xcode `clang`。`mock: true` 的話兩者都不需要。"},"notes":[],"examples":[{"code":"import { startBridge } from '@openav/leap/bridge';\n\nconst bridge = await startBridge({ mock: true, port: 0 });          // made-up hands, any free port\nconst socket = new WebSocket('ws://127.0.0.1:' + bridge.port + '/v6.json');\nsocket.onmessage = async ({ data }) => {\n  const message = JSON.parse(data);\n  if (message.serviceVersion) console.log('handshake:', message);    // { serviceVersion: 'mock', version: 6, bridge: 'open-audiovisual' }\n  if (message.hands) {                                               // a frame\n    console.log('hands in the frame:', message.hands.length);        // 2\n    socket.close();\n    await bridge.close();\n  }\n};","caption":{"en":"In Node: the first message is the handshake, `handshake: { serviceVersion: 'mock', version: 6, bridge: 'open-audiovisual' }`, then a frame arrives and the program prints `hands in the frame: 2` and stops the bridge.","zh":"在 Node 裡：第一則訊息是交握 `handshake: { serviceVersion: 'mock', version: 6, bridge: 'open-audiovisual' }`，接著收到一個影格，程式印出 `hands in the frame: 2`，並把橋接程式停掉。"},"run":"static","lang":"js"}],"related":["LeapInput","mockFrame","V6Converter","DEFAULT_URL"],"keywords":{"en":["leap bridge","bridge","server","websocket server","node","6437","mock","leap-bridge","ultraleap service"],"zh":["leap 橋接","橋接程式","伺服器","websocket 伺服器","node","6437","假手","ultraleap 服務"]},"tbd":[],"claims":[]},
{"id":"STATUS","key":"STATUS","name":"STATUS","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"STATUS = object, 4 keys","import":"import { STATUS } from '@openav/leap';","source":{"file":"packages/leap/index.js","line":37,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/index.js#L37"},"summary":{"en":"The four levels of the `leap/status` signal by name: `off` 0, `bridge` 1, `service` 2, `tracking` 3.","zh":"`leap/status` 訊號的四個等級，用名字表示：`off` 0、`bridge` 1、`service` 2、`tracking` 3。"},"description":[{"en":"A frozen object `{ off: 0, bridge: 1, service: 2, tracking: 3 }`: 0 no bridge, 1 the bridge is there but the tracking service is not, 2 the service is there but no device, 3 tracking. Use it to read the signal without magic numbers: `signals.get('leap/status') === STATUS.tracking`. Note it is the **number** published as `leap/status`; `leap.status` is a different thing, a string (`'tracking'`, `'no-device'` …), and the simulator counts as 3.","zh":"一個凍結的物件 `{ off: 0, bridge: 1, service: 2, tracking: 3 }`：0 沒有橋接、1 有橋接但追蹤服務沒開、2 有服務但沒有裝置、3 追蹤中。讀訊號時可以不用神祕數字：`signals.get('leap/status') === STATUS.tracking`。注意，發布成 `leap/status` 的是這個**數字**。`leap.status` 是另一回事，它是字串（`'tracking'`、`'no-device'` ……），而模擬器算 3。"}],"params":[],"notes":[{"en":"The code in `LeapInput` writes the numbers itself and does not read this object, so `STATUS` is a convenience for your own code. The strings map as: `tracking` and `simulated` to 3, `no-device` to 2, `bridge` and `no-service` to 1, the rest to 0.","zh":"`LeapInput` 裡的程式是自己寫入數字，並不讀這個物件，所以 `STATUS` 只是方便你自己的程式用。字串的對應是：`tracking` 和 `simulated` 是 3，`no-device` 是 2，`bridge` 和 `no-service` 是 1，其餘是 0。"}],"examples":[],"related":["LeapInput","deviceEvent","mountLeapPanel"],"keywords":{"en":["status","leap status","tracking","no bridge","no device","connection state"],"zh":["狀態","leap 狀態","追蹤中","沒有橋接","沒有裝置","連線狀態"]},"tbd":[],"claims":[]},
{"id":"THRESHOLDS","key":"THRESHOLDS","name":"THRESHOLDS","aliases":[],"kind":"constant","package":"@openav/leap","category":"leap","documented":true,"signature":"THRESHOLDS = object, 3 keys","import":"import { THRESHOLDS } from '@openav/leap';","source":{"file":"packages/leap/gesture.js","line":13,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/gesture.js#L13"},"summary":{"en":"The default hysteresis levels for the gestures: pinch 0.86 / 0.5, both-hands pinch 0.8 / 0.5, grab 0.8 / 0.5 (start above `on`, end below `off`).","zh":"手勢預設的遲滯門檻：捏合 0.86 / 0.5、雙手捏合 0.8 / 0.5、握拳 0.8 / 0.5（高於 `on` 開始，低於 `off` 結束）。"},"description":[{"en":"A frozen object `{ pinch: { on, off }, bothPinch: { on, off }, grab: { on, off } }`. The numbers come from The Last Input, tuned on a Leap Motion Controller 2, so one bridge means one feel. Change them per input with `new LeapInput({ thresholds: { pinch: { on: 0.9, off: 0.6 } } })` or `new HandGestures(...)`; you cannot change this object itself.","zh":"一個凍結的物件 `{ pinch: { on, off }, bothPinch: { on, off }, grab: { on, off } }`。數字來自 The Last Input，在 Leap Motion Controller 2 上調過，這樣一個橋接程式就是一種手感。要改的話，在每個輸入上設定：`new LeapInput({ thresholds: { pinch: { on: 0.9, off: 0.6 } } })` 或 `new HandGestures(...)`，這個物件本身改不了。"}],"params":[],"notes":[],"examples":[],"related":["HandGestures","Hysteresis","LeapInput"],"keywords":{"en":["thresholds","pinch threshold","grab threshold","hysteresis levels","0.86","defaults"],"zh":["門檻","捏合門檻","握拳門檻","遲滯值","預設值"]},"tbd":[],"claims":[]},
{"id":"V6Converter","key":"V6Converter","name":"V6Converter","aliases":[],"kind":"class","package":"@openav/leap","category":"leap","documented":true,"signature":"new V6Converter({ box = INTERACTION_BOX, smoothing = 0.5 } = {})","import":"import { V6Converter } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":91,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L91"},"summary":{"en":"Converts raw hand lines into LeapJS v6 frames, keeping the little state the protocol needs: time visible, smoothed positions and fingertip velocities.","zh":"把原始的手資料行轉成 LeapJS v6 影格，並保存通訊協定需要的少量狀態：可見時間、平滑過的位置和指尖速度。"},"description":[{"en":"Every browser sketch written for the old Leap service (LeapJS, 2014 to 2021) speaks the v6 protocol, so the bridge serves it on `/v6.json`. This class does the conversion: one converter per stream. It is stateful where the protocol is: a hand's `timeVisible` grows while it stays in view and starts over when it leaves, `stabilized…` positions are a light low-pass, and `tipVelocity` is a finite difference between two frames.","zh":"所有為舊版 Leap 服務寫的瀏覽器草圖（LeapJS，2014 到 2021 年）講的都是 v6 通訊協定，所以橋接程式在 `/v6.json` 提供這個格式。這個類別負責轉換：一條串流用一個轉換器。它在通訊協定要求有狀態的地方才有狀態：手在畫面裡時 `timeVisible` 會增加，離開再回來就重新開始，`stabilized…` 位置是輕微的低通濾波，`tipVelocity` 是兩個影格之間的差分。"},{"en":"LeapC gives joints, not bone rotations, so the bone bases are rebuilt from the joints and the palm normal ([`boneBasis()`](#boneBasis)). Gestures are not synthesised: `gestures` is always `[]`.","zh":"LeapC 給的是關節，不是骨頭的旋轉，所以骨頭的基底是從關節和掌心法線重建的（[`boneBasis()`](#boneBasis)）。手勢不會被合成：`gestures` 永遠是 `[]`。"}],"params":[{"name":"box","type":"Object","optional":true,"default":"INTERACTION_BOX","en":"The interaction box written into every frame's `interactionBox`.","zh":"寫進每個影格 `interactionBox` 的互動範圍盒。"},{"name":"smoothing","type":"Number","optional":true,"default":"0.5","en":"0..1: how strongly `stabilizedPalmPosition` and `stabilizedTipPosition` lag behind (0 follows exactly, close to 1 barely moves).","zh":"0..1：`stabilizedPalmPosition` 和 `stabilizedTipPosition` 落後的程度（0 完全跟隨，接近 1 幾乎不動）。"}],"notes":[],"examples":[{"code":"import { V6Converter, mockFrame } from '@openav/leap';\n\nconst converter = new V6Converter();\nconverter.convert(mockFrame(1.0), 1_000_000);                    // microseconds\nconst frame = converter.convert(mockFrame(1.1), 1_100_000);\nconsole.log(frame.hands[0].timeVisible);                          // 0.1: seen for a tenth of a second\nconsole.log(frame.pointables.length, frame.pointables[1].tipVelocity);   // 10 [ -21, 17, 28 ]  mm per second","caption":{"en":"The console prints `0.1`, then `10` pointables and the index fingertip's velocity `[ -21, 17, 28 ]` (millimetres per second between the two frames).","zh":"主控台印出 `0.1`，接著是 `10` 個 pointable，以及食指指尖的速度 `[ -21, 17, 28 ]`（兩個影格之間，每秒幾公釐）。"},"run":"module","lang":"js"}],"related":["rawToV6","v6Header","deviceEvent","handsOf"],"keywords":{"en":["v6","leapjs","convert","protocol","frame converter","raw to v6","legacy leap sketches"],"zh":["v6","leapjs","轉換","通訊協定","影格轉換","舊版 leap 草圖"]},"tbd":[],"claims":[],"members":[{"name":"convert","member":"method","signature":"raw, tUs = nowUs()","doc":"","line":104}]},
{"id":"v6Header","key":"v6Header","name":"v6Header","aliases":[],"kind":"function","package":"@openav/leap","category":"leap","documented":true,"signature":"v6Header(serviceVersion = 'unknown', version = PROTOCOL_VERSION)","import":"import { v6Header } from '@openav/leap';","source":{"file":"packages/leap/frame.js","line":188,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/leap/frame.js#L188"},"summary":{"en":"The first message of a LeapJS v6 connection: `{ serviceVersion, version }`, with `version` held to 1..6 because LeapJS throws above 6.","zh":"LeapJS v6 連線的第一則訊息：`{ serviceVersion, version }`，`version` 被限制在 1..6，因為 LeapJS 超過 6 會丟出錯誤。"},"description":[{"en":"The bridge sends this (plus `bridge: 'open-audiovisual'`) as soon as a client connects to `/v6.json`, and [`LeapInput`](#LeapInput) turns it into the status `'bridge'`. Ask for 7 and you get 6.","zh":"用戶端一連到 `/v6.json`，橋接程式就會送出這則訊息（外加 `bridge: 'open-audiovisual'`），[`LeapInput`](#LeapInput) 收到後會把狀態變成 `'bridge'`。要求 7 會得到 6。"}],"params":[{"name":"serviceVersion","type":"String","optional":true,"default":"'unknown'","en":"The tracking service version to report, such as `'5.20.0'`.","zh":"要回報的追蹤服務版本，例如 `'5.20.0'`。"},{"name":"version","type":"Number","optional":true,"default":"PROTOCOL_VERSION","en":"The protocol version the client asked for. Held to 1..6.","zh":"用戶端要求的通訊協定版本。限制在 1..6。"}],"returns":{"type":"Object","en":"`{ serviceVersion: String, version: Number }`.","zh":"物件 `{ serviceVersion: String, version: Number }`。"},"notes":[],"examples":[{"code":"import { v6Header } from '@openav/leap';\n\nconsole.log(v6Header('5.20.0'));        // { serviceVersion: '5.20.0', version: 6 }\nconsole.log(v6Header('x', 3));          // { serviceVersion: 'x', version: 3 }\nconsole.log(v6Header('x', 9).version);  // 6: never above what LeapJS accepts","caption":{"en":"The console prints the header for version 6, version 3, and `6` for a request of 9.","zh":"主控台印出版本 6 的標頭、版本 3 的標頭，以及要求 9 時得到的 `6`。"},"run":"module","lang":"js"}],"related":["deviceEvent","PROTOCOL_VERSION","protocolOfPath","LeapInput"],"keywords":{"en":["v6 header","handshake","service version","leapjs protocol"],"zh":["v6 標頭","交握","服務版本","leapjs 通訊協定"]},"tbd":[],"claims":[]},
{"id":"HandTracker","key":"HandTracker","name":"HandTracker","aliases":[],"kind":"class","package":"@openav/pose","category":"pose","documented":true,"signature":"new HandTracker({ signals = null, mirror = true } = {})","import":"import { HandTracker } from '@openav/pose';","source":{"file":"packages/pose/index.js","line":158,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pose/index.js#L158"},"summary":{"en":"Tracks up to two hands from the camera with 21 landmarks each and publishes palm position and thumb-to-finger pinch distances as `hand/*` signals.","zh":"用鏡頭追蹤最多兩隻手，每隻 21 個關節點，把掌心位置和拇指到手指的捏合距離發布成 `hand/*` 訊號。"},"description":[{"en":"The **fine-control** tracker: each hand gives two precise continuous controllers, thumb to index and thumb to middle finger, so two hands are four controllers you can pinch. The distances are normalised by the size of your palm (wrist to the base of the middle finger), so moving toward or away from the camera does not change a pinch: stage-robust. It runs on the device with MediaPipe Tasks Vision and downloads nothing until `enable()`. In a show, `createShow({ modules: { hands: true } })` builds one (`show.hands`) and the L1 panel gets a 🖐 hands button.","zh":"**精細控制**的追蹤器：每隻手提供兩個精確的連續控制，拇指到食指、拇指到中指，所以兩隻手就是四個可以捏的控制。距離會依你的手掌大小（手腕到中指根部）正規化，所以靠近或遠離鏡頭不會改變捏合的值，在舞台上很穩。它在裝置上用 MediaPipe Tasks Vision 執行，在 `enable()` 之前不會下載任何東西。在演出裡，`createShow({ modules: { hands: true } })` 會建好一個（`show.hands`），L1 面板會多出一個 🖐 hands 按鈕。"},{"en":"For each of `left` and `right` it publishes `hand/<side>/present` (0 or 1), `hand/<side>/x` and `/y` (the palm, 0..1; y flipped so 1 is raised), `hand/<side>/pinch/index` and `hand/<side>/pinch/middle` (0..1, **0 = touching**, 1 = wide open) and `hand/<side>/spread` (index fingertip to little fingertip, 0..1). A side with no hand in view gets `present` 0 and keeps its other values. Note that these names do not start with `pose/`, and are different from the body tracker's.","zh":"`left` 和 `right` 各自會發布 `hand/<side>/present`（0 或 1）、`hand/<side>/x` 和 `/y`（掌心，0..1，y 翻轉過，所以 1 是舉高）、`hand/<side>/pinch/index` 和 `hand/<side>/pinch/middle`（0..1，**0 = 碰在一起**，1 = 張得最開），以及 `hand/<side>/spread`（食指指尖到小指指尖，0..1）。畫面裡沒有手的那一邊，`present` 會是 0，其他的值維持不變。請注意，這些名稱不是 `pose/` 開頭，跟身體追蹤器的不一樣。"},{"en":"A pinch is a distance, so for \"pinch closes\" use a route with `invert`: `{ source: 'hand/right/pinch/index', target: 'tightness', invert: true, smooth: 0.1 }`. If you want pinch start and end as events, [`LeapInput`](#LeapInput)'s gestures do that; `HandTracker` only gives the continuous distances.","zh":"捏合是一個距離，所以想要「捏緊的時候變大」，就在路由裡用 `invert`：`{ source: 'hand/right/pinch/index', target: 'tightness', invert: true, smooth: 0.1 }`。如果你要把捏合的開始和結束當成事件，[`LeapInput`](#LeapInput) 的手勢就是做這個的。`HandTracker` 只提供連續的距離。"}],"params":[{"name":"signals","type":"Signals | null","optional":true,"default":"null","en":"The [`Signals`](#Signals) registry to publish into. Without one the tracker still runs and keeps `hands`, but publishes nothing.","zh":"要發布進去的 [`Signals`](#Signals) 登錄表。沒有的話，追蹤器照樣會跑、也會更新 `hands`，只是不發布任何訊號。"},{"name":"mirror","type":"Boolean","optional":true,"default":"true","en":"Treat the camera as a mirror: x is flipped, and the left and right labels MediaPipe gives are swapped so the signals match the body.","zh":"把鏡頭當成鏡子：x 會左右翻轉，MediaPipe 給的左手、右手標籤也會對調，讓訊號跟身體對得上。"}],"needs":{"en":"Call `await hands.enable()`: it loads the MediaPipe library from jsDelivr and the hand model from Google's model storage (so it needs the network), asks for the camera (`https` or `localhost`, and the user must allow it) and starts the loop. Until then the signals sit at their starting values.","zh":"要呼叫 `await hands.enable()`：它會從 jsDelivr 載入 MediaPipe 函式庫、從 Google 的模型儲存空間載入手的模型（所以需要網路）、向瀏覽器要鏡頭（`https` 或 `localhost`，使用者也要允許），然後啟動迴圈。在那之前，訊號都停在起始值。"},"notes":[{"en":"Fields to read: `hands` (`[{ handed: 'left' | 'right', lm }]`, `lm` the 21 landmarks `{ x, y, z }` in the camera image, y downward and not mirrored), `running`, `mirror`, `video`.","zh":"可以讀的欄位：`hands`（`[{ handed: 'left' | 'right', lm }]`，`lm` 是 21 個關節點 `{ x, y, z }`，在鏡頭影像裡，y 向下、沒有左右翻轉）、`running`、`mirror`、`video`。"},{"en":"Landmark indexes used: wrist 0, thumb tip 4, index tip 8, middle tip 12, little finger tip 20, base of the middle finger 9 (the palm position and the size reference).","zh":"用到的關節點編號：手腕 0、拇指指尖 4、食指指尖 8、中指指尖 12、小指指尖 20、中指根部 9（掌心位置，也是計算大小的參考）。"}],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { HandTracker } from '@openav/pose';\n\nconst signals = new Signals();\nconst hands = new HandTracker({ signals });\ndocument.addEventListener('click', () => hands.enable(), { once: true });   // the camera prompt, then the model\nsignals.on('hand/right/pinch/index', (distance) => {\n  if (distance < 0.1) console.log('right hand pinches thumb and index');\n});","caption":{"en":"Click the page and allow the camera; hold a hand up and pinch thumb to index finger: the console prints `right hand pinches thumb and index` while they touch.","zh":"點一下頁面並允許鏡頭，舉起一隻手、拇指碰食指：兩指相碰的時候，主控台印出 `right hand pinches thumb and index`。"},"run":"static","lang":"js"},{"code":"import { HandTracker } from '@openav/pose';\n\nconst hands = new HandTracker();                      // never enabled: no camera, no download\nconst lm = Array.from({ length: 21 }, () => ({ x: 0.5, y: 0.5, z: 0 }));\nlm[0] = { x: 0.5, y: 0.9, z: 0 };                     // wrist\nconst fingers = [[1, 0.38, 0.8, 0.28, 0.7, 0.2, 0.62, 0.14, 0.55],     // thumb: joints 1 to 4 as x, y pairs\n  [5, 0.42, 0.55, 0.4, 0.42, 0.39, 0.32, 0.38, 0.22],                  // index\n  [9, 0.5, 0.53, 0.5, 0.38, 0.5, 0.27, 0.5, 0.17],                     // middle\n  [13, 0.58, 0.55, 0.6, 0.42, 0.61, 0.32, 0.62, 0.23],                 // ring\n  [17, 0.64, 0.6, 0.67, 0.5, 0.69, 0.42, 0.7, 0.34]];                  // little\nfor (const [first, ...xy] of fingers) for (let i = 0; i < 4; i++) lm[first + i] = { x: xy[i * 2], y: xy[i * 2 + 1], z: 0 };\nhands.hands = [{ handed: 'right', lm }];              // what enable() keeps filling in from the camera\n\nconst ctx = document.getElementById('c').getContext('2d');\nctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\nhands.skeleton(ctx, 400, 300, { lineWidth: 3 });","caption":{"en":"An open right hand is drawn in yellow on the canvas from made-up landmarks, with bigger dots at the thumb, index and middle fingertips: the overlay `skeleton()` gives you over the camera picture.","zh":"用編造的關節點在畫布上用黃色畫出一隻張開的右手，拇指、食指、中指的指尖是比較大的圓點：這就是 `skeleton()` 疊在鏡頭畫面上的骨架。"},"run":"module","lang":"js"}],"related":["PoseTracker","Signals","Mapper","LeapInput","createShow"],"keywords":{"en":["hands","hand tracking","pinch","fingers","camera","webcam","mediapipe","landmarks","thumb index","spread","palm"],"zh":["手","手部追蹤","捏合","手指","鏡頭","網路攝影機","mediapipe","關節點","拇指食指","張開","掌心"]},"tbd":[{"en":"Not tried here with a live camera, so the real range of the pinch values (for example how close to 0 a firm pinch gets) is not measured.","zh":"這裡沒有用真正的鏡頭試過，所以捏合值實際的範圍（例如用力捏的時候能多接近 0）沒有量過。"}],"claims":[],"members":[{"name":"enable","member":"method","signature":"{ videoEl = null } = {}","doc":"","line":173},{"name":"stop","member":"method","signature":"","doc":"","line":190},{"name":"skeleton","member":"method","signature":"ctx, w, h, { colorLeft = 'rgba(55,201,120,0.9)', colorRight = 'rgba(255,209,102,0.9)', lineWidth = 2 } = {}","doc":"Draw hand skeletons onto a 2D context (overlay).","line":233}]},
{"id":"PoseTracker","key":"PoseTracker","name":"PoseTracker","aliases":[],"kind":"class","package":"@openav/pose","category":"pose","documented":true,"signature":"new PoseTracker({ signals = null, mirror = true } = {})","import":"import { PoseTracker } from '@openav/pose';","source":{"file":"packages/pose/index.js","line":23,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/pose/index.js#L23"},"summary":{"en":"Tracks one body from the camera and publishes wrist positions, wrist speed, hand spread, height and lean as `pose/*` signals.","zh":"用鏡頭追蹤一個人的身體，把手腕的位置、手腕速度、雙手距離、身高位置和傾斜，發布成 `pose/*` 訊號。"},"description":[{"en":"This is the **body** tracker: one person, 33 landmarks (the BlazePose set), run by MediaPipe Tasks Vision on the device. No video leaves the machine, and nothing is downloaded until you call `enable()`. For fine finger control use [`HandTracker`](#HandTracker) instead. In a show, `createShow({ modules: { pose: true } })` builds one and the console's L1 panel gets a 🕺 body button that calls `enable()`; it is `show.pose`.","zh":"這是**身體**追蹤器：一個人、33 個關節點（BlazePose 那一套），在裝置上由 MediaPipe Tasks Vision 執行。影像不會離開這台機器，在你呼叫 `enable()` 之前也不會下載任何東西。要精細控制手指的話，改用 [`HandTracker`](#HandTracker)。在演出裡，`createShow({ modules: { pose: true } })` 會建好一個，主控台的 L1 面板會多出一個 🕺 body 按鈕來呼叫 `enable()`，它就是 `show.pose`。"},{"en":"Signals, all normalised, with y **flipped** so that 1 means raised and 0 means the bottom of the frame (performance meaning beats pixel meaning): `pose/present` (0 or 1: someone is visible), `pose/hand/left/x` and `/y`, `pose/hand/right/x` and `/y` (wrist positions, 0..1), `pose/hand/left/v` and `pose/hand/right/v` (wrist speed, 0..1, clamped), `pose/hands/spread` (distance between the wrists, 0..1), `pose/height` (nose height, 0..1: crouching to standing) and `pose/lean` (shoulder-line tilt, −1..1). When nobody is in view only `pose/present` goes to 0; the others keep their last values.","zh":"訊號全都是正規化的，y **翻轉**過，所以 1 是舉高、0 是畫面底部（演出的意義比像素的意義重要）：`pose/present`（0 或 1：看得到人）、`pose/hand/left/x` 和 `/y`、`pose/hand/right/x` 和 `/y`（手腕位置，0..1）、`pose/hand/left/v` 和 `pose/hand/right/v`（手腕速度，0..1，超過會被夾住）、`pose/hands/spread`（兩個手腕的距離，0..1）、`pose/height`（鼻子的高度，0..1：蹲下到站直）和 `pose/lean`（肩線的傾斜，−1..1）。畫面裡沒人的時候，只有 `pose/present` 會變成 0，其他的維持最後的值。"},{"en":"Map these to params (`{ source: 'pose/hand/right/y', target: 'energy', smooth: 0.1 }`) so a raised hand does what a knob would. The detection runs on every animation frame while `running`.","zh":"把它們映射到參數（`{ source: 'pose/hand/right/y', target: 'energy', smooth: 0.1 }`），舉起的手就能做旋鈕做的事。偵測在 `running` 期間每個動畫畫面都會跑。"}],"params":[{"name":"signals","type":"Signals | null","optional":true,"default":"null","en":"The [`Signals`](#Signals) registry to publish into. Without one the tracker still runs and keeps `landmarks`, but publishes nothing.","zh":"要發布進去的 [`Signals`](#Signals) 登錄表。沒有的話，追蹤器照樣會跑、也會更新 `landmarks`，只是不發布任何訊號。"},{"name":"mirror","type":"Boolean","optional":true,"default":"true","en":"Treat the camera as a mirror: x is flipped, so moving toward the viewer's right makes x grow. Natural for performers. `false` keeps the camera's own left and right.","zh":"把鏡頭當成鏡子：x 會左右翻轉，所以往觀看者的右邊移動，x 就變大。對表演者很自然。`false` 則保留鏡頭本身的左右。"}],"needs":{"en":"Call `await pose.enable()`: it loads the MediaPipe library from jsDelivr and the model from Google's model storage (so it needs the network), asks the browser for the camera (the page needs `https` or `localhost`, and the user must allow it), and starts the loop. Until then every signal sits at its starting value.","zh":"要呼叫 `await pose.enable()`：它會從 jsDelivr 載入 MediaPipe 函式庫、從 Google 的模型儲存空間載入模型（所以需要網路）、向瀏覽器要鏡頭（網頁要在 `https` 或 `localhost` 底下，使用者也要允許），然後啟動迴圈。在那之前，所有訊號都停在起始值。"},"notes":[{"en":"Fields to read: `landmarks` (the latest 33 raw points `{ x, y, z, visibility }`, x and y 0..1 in the camera image, y downward and not mirrored; `null` when nobody is seen), `running`, `mirror`, `video`.","zh":"可以讀的欄位：`landmarks`（最新的 33 個原始點 `{ x, y, z, visibility }`，x、y 是鏡頭影像裡的 0..1，y 向下、沒有左右翻轉，看不到人就是 `null`）、`running`、`mirror`、`video`。"},{"en":"Landmark indexes (BlazePose): nose 0, shoulders 11 and 12, wrists 15 and 16, hips 23 and 24.","zh":"關節點編號（BlazePose）：鼻子 0、肩膀 11 和 12、手腕 15 和 16、髖部 23 和 24。"},{"en":"The model is MediaPipe's \"lite\" pose landmarker on the GPU, one pose at a time. If two people are in view, only one is tracked.","zh":"模型是 MediaPipe 的「lite」姿態偵測器，跑在 GPU 上，一次只追蹤一個姿態。畫面裡有兩個人的話，只會追蹤其中一個。"}],"examples":[{"code":"import { Signals } from '@openav/core';\nimport { PoseTracker } from '@openav/pose';\n\nconst signals = new Signals();\nconst pose = new PoseTracker({ signals });\ndocument.addEventListener('click', async () => {\n  await pose.enable();                                   // camera prompt, then the model downloads\n  console.log('tracking');\n}, { once: true });\nsetInterval(() => {\n  console.log('raised hand', signals.get('pose/hand/right/y').toFixed(2), 'present', signals.get('pose/present'));\n}, 500);","caption":{"en":"Click the page and allow the camera: the console prints `tracking`, then every half second the height of the right wrist (1 = raised) and whether someone is in view.","zh":"點一下頁面並允許鏡頭：主控台印出 `tracking`，然後每半秒印出右手腕的高度（1 = 舉高），以及畫面裡有沒有人。"},"run":"static","lang":"js"},{"code":"import { PoseTracker } from '@openav/pose';\n\nconst pose = new PoseTracker();                       // never enabled: no camera, no download\nconst point = (x, y) => ({ x, y, z: 0, visibility: 1 });\nconst landmarks = Array.from({ length: 33 }, () => point(0.5, 0.5));\nlandmarks[0] = point(0.5, 0.2);                       // nose\nlandmarks[11] = point(0.58, 0.35); landmarks[12] = point(0.42, 0.35);   // shoulders\nlandmarks[13] = point(0.68, 0.45); landmarks[14] = point(0.32, 0.25);   // elbows\nlandmarks[15] = point(0.74, 0.58); landmarks[16] = point(0.24, 0.1);    // wrists: one down, one raised\nlandmarks[23] = point(0.55, 0.62); landmarks[24] = point(0.45, 0.62);   // hips\nlandmarks[25] = point(0.56, 0.8); landmarks[26] = point(0.44, 0.8);     // knees\nlandmarks[27] = point(0.56, 0.95); landmarks[28] = point(0.44, 0.95);   // ankles\nlandmarks[29] = point(0.57, 0.97); landmarks[30] = point(0.43, 0.97);   // heels\nlandmarks[31] = point(0.6, 0.98); landmarks[32] = point(0.4, 0.98);     // toes\npose.landmarks = landmarks;                           // what enable() keeps filling in from the camera\n\nconst ctx = document.getElementById('c').getContext('2d');\nctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\npose.skeleton(ctx, 400, 300, { color: '#7aa6ff', lineWidth: 3 });","caption":{"en":"A stick figure with one arm raised is drawn on the canvas from made-up landmarks: the overlay `skeleton()` draws over the camera picture, without needing a camera.","zh":"用編造的關節點在畫布上畫出一個舉著一隻手的火柴人：這就是 `skeleton()` 疊在鏡頭畫面上的骨架，不需要鏡頭。"},"run":"module","lang":"js"},{"code":"import { Signals, Params, Loop } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\nimport { PoseTracker } from '@openav/pose';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'energy', min: 0, max: 1, def: 0.2 }]);\nconst mapper = new Mapper({ signals, params });\nmapper.addRoute({ source: 'pose/hand/right/y', target: 'energy', smooth: 0.1 });   // a raised hand is energy\nconst pose = new PoseTracker({ signals });\n\ndocument.addEventListener('click', () => pose.enable(), { once: true });\nnew Loop((dt) => {\n  mapper.update(dt);\n  const energy = params.resolve().energy;\n  const ctx = document.getElementById('c').getContext('2d');\n  ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\n  ctx.fillStyle = '#7aa6ff'; ctx.fillRect(20, 280, 360 * energy, 10);\n}).start();","caption":{"en":"Click to start the camera, then raise your right hand: the bar at the bottom of the canvas grows with it, because the pose signal is mapped to a param.","zh":"點一下啟動鏡頭，然後舉起右手：畫布底下的長條會跟著變長，因為姿態訊號被映射到一個參數。"},"run":"static","lang":"js"}],"related":["HandTracker","Signals","Mapper","LeapInput","createShow"],"keywords":{"en":["pose","body tracking","camera","webcam","mediapipe","skeleton","wrist","hand height","lean","blazepose","landmarks","dancer"],"zh":["姿態","身體追蹤","鏡頭","網路攝影機","mediapipe","骨架","手腕","手的高度","傾斜","關節點","舞者"]},"tbd":[{"en":"Not tried here with a live camera. In particular, which person's hand `pose/hand/left/*` follows in practice was not checked: the code takes landmark 16 for `left` and landmark 15 for `right` when `mirror` is true (the other way round when false), and the package docs say `left` is the viewer's left.","zh":"這裡沒有用真正的鏡頭試過。尤其是 `pose/hand/left/*` 實際上跟著誰的手，沒有查證：`mirror` 為 true 的時候，程式用關節點 16 當 `left`、15 當 `right`（false 時相反），套件文件則說 `left` 是觀看者的左邊。"},{"en":"Whether the camera permission prompt needs a click first was not tested; the package README calls `enable()` from a button.","zh":"鏡頭權限的詢問是否一定要先點一下，沒有測試過。套件的 README 是從按鈕呼叫 `enable()`。"}],"claims":[],"members":[{"name":"enable","member":"method","signature":"{ videoEl = null } = {}","doc":"Ask for camera, load the model (CDN), start the detection loop.","line":45},{"name":"stop","member":"method","signature":"","doc":"","line":62},{"name":"skeleton","member":"method","signature":"ctx, w, h, { color = 'rgba(122,166,255,0.9)', lineWidth = 2 } = {}","doc":"Draw the body skeleton onto a 2D context (overlay).","line":81}]},
{"id":"ChordDetector","key":"ChordDetector","name":"ChordDetector","aliases":[],"kind":"class","package":"@openav/chord","category":"chord","documented":true,"signature":"new ChordDetector({ window = 80, onChord = null, signals = null } = {})","import":"import { ChordDetector } from '@openav/chord';","source":{"file":"packages/chord/index.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/chord/index.js#L30"},"summary":{"en":"Groups notes struck close together into one gesture and analyses it: chord type, consonance, triad and cluster flags, published as `chord/*` signals.","zh":"把幾乎同時按下的音歸成一個手勢並分析它：和弦類型、協和度、三和弦與音叢的旗標，發布成 `chord/*` 訊號。"},"description":[{"en":"This is not a music-theory library. It is a **performance semantics** layer: raw notes in, one analysed event per gesture out, with features that are made to drive a work. A single note is a seed, a chord a colony, a consonant chord a flourish, a dissonant one decay; rename them for your own piece. In a show, `createShow({ modules: { chord: true } })` builds one (`{ chord: { window: 120 } }` tunes it) and connects it to `midi/note/on` and `midi/note/off`, so a MIDI keyboard, the on-screen piano and phones all feed it.","zh":"它管的是**演出語意**，不是樂理：原始的音進去，每個手勢出來一個分析過的事件，特徵都是為了驅動作品而設計的。單一個音是種子，和弦是一群生物，協和的和弦是繁盛，不協和的和弦是衰敗。你可以替自己的作品換掉這些名字。在演出裡，`createShow({ modules: { chord: true } })` 會建好一個（`{ chord: { window: 120 } }` 可以調整）並接上 `midi/note/on` 和 `midi/note/off`，所以 MIDI 鍵盤、螢幕上的鋼琴和手機都能餵它。"},{"en":"Each closed gesture publishes `chord/consonance` (−1..1), `chord/count` (0..10), `chord/root` (the lowest note) and the pulse `chord/event`, whose payload is the whole analysis: `{ notes, count, root, vel, pcs, consonance, isConsonant, isDissonant, isTriad, thirdsFraction, dissonanceLevel, chordType }`. `chordType` is one of `single`, `major`, `minor`, `sus2`, `sus4`, `dim`, `aug`, `maj7`, `min7`, `dom7`, `halfdim7`, `maj9`, `min9`, `dom9`, `six`, `min6`, `cluster` (a very dissonant pile) or `chord` (anything else). Inversions resolve to the root-position name.","zh":"每個收尾的手勢會發布 `chord/consonance`（−1..1）、`chord/count`（0..10）、`chord/root`（最低音）和脈衝 `chord/event`，脈衝的內容就是整份分析：`{ notes, count, root, vel, pcs, consonance, isConsonant, isDissonant, isTriad, thirdsFraction, dissonanceLevel, chordType }`。`chordType` 是 `single`、`major`、`minor`、`sus2`、`sus4`、`dim`、`aug`、`maj7`、`min7`、`dom7`、`halfdim7`、`maj9`、`min9`、`dom9`、`six`、`min6`、`cluster`（非常不協和的一堆音）或 `chord`（其他任何和弦）之一。轉位會被還原成原位和弦的名稱。"},{"en":"`consonance` is the average of an interval weight for every pair of notes (an octave or fifth is high, a semitone or tritone is negative). `isConsonant` is `consonance > 0.25` and `isDissonant` is `consonance < −0.15`, both only for two notes or more. `dissonanceLevel` is 0 for none, 1 for **mild** (a storm warning: foreshadow it) and 2 for **severe** (three or more notes that are a true cluster: commit to the decay). `isTriad` is true for `major`, `minor`, `sus2`, `sus4`, `dim` and `aug`, and `thirdsFraction` is the share of neighbouring notes exactly 3 or 4 semitones apart, so \"genuinely stacked thirds\" can be told from \"some consonant notes\".","zh":"`consonance` 是每一對音的音程權重的平均（八度或五度很高，半音或三全音是負的）。`isConsonant` 是 `consonance > 0.25`，`isDissonant` 是 `consonance < −0.15`，都只在兩個音以上才成立。`dissonanceLevel` 0 是沒有，1 是**輕微**（風暴警報：預告它），2 是**嚴重**（三個音以上的真正音叢：直接進入衰敗）。`isTriad` 在 `major`、`minor`、`sus2`、`sus4`、`dim`、`aug` 時為真，`thirdsFraction` 是相鄰兩個音剛好相差 3 或 4 個半音的比例，這樣就分得出「真正疊起來的三度」和「幾個協和的音」。"}],"params":[{"name":"window","type":"Number","optional":true,"default":"80","en":"How many milliseconds after the **last** `noteOn` the gesture is closed and analysed. Notes struck within this time of each other are one chord.","zh":"**最後一個** `noteOn` 之後等幾毫秒，就把這個手勢收尾並分析。彼此相隔不到這個時間按下的音，算同一個和弦。"},{"name":"onChord","type":"Function | null","optional":true,"default":"null","en":"Called with the analysis object each time a gesture is closed.","zh":"每次一個手勢收尾，就以分析結果物件呼叫一次。"},{"name":"signals","type":"Signals | null","optional":true,"default":"null","en":"A [`Signals`](#Signals) registry to publish `chord/*` into. The signals are declared when the detector is built.","zh":"要把 `chord/*` 發布進去的 [`Signals`](#Signals) 登錄表。訊號在建立偵測器時就宣告好了。"}],"needs":{"en":"Feed it with `noteOn(note, vel)` (and `noteOff(note)`), or let `createShow` do it from `midi/note/on|off`. Nothing is published until `window` milliseconds after the last `noteOn`, so the result is asynchronous. `analyze()` is the exception: it is instant and needs no setup.","zh":"用 `noteOn(note, vel)`（和 `noteOff(note)`）餵它，或讓 `createShow` 從 `midi/note/on|off` 餵。要等到最後一個 `noteOn` 之後 `window` 毫秒才會發布，所以結果是非同步的。`analyze()` 是例外：它立刻回傳，也不需要任何前置動作。"},"notes":[{"en":"Pass notes to `analyze()` lowest first: `root` is the first note, and the detector sorts for you but a direct call does not.","zh":"呼叫 `analyze()` 時音要由低到高：`root` 就是第一個音，偵測器會替你排序，但直接呼叫就不會。"},{"en":"A major second like `[60, 62]` is typed `cluster` (no template fits and its consonance is below −0.3) even though its `dissonanceLevel` is only 1. Level 2 needs three or more notes.","zh":"像 `[60, 62]` 這樣的大二度，類型會是 `cluster`（沒有範本符合，而且協和度低於 −0.3），不過它的 `dissonanceLevel` 只有 1。要到 2 至少得有三個音。"},{"en":"The gesture window does not know about held notes: a note already down when you play two more does not join them.","zh":"手勢窗口不知道還按著的音：已經按著的一個音，不會跟你後來彈的兩個音湊成一個和弦。"}],"examples":[{"code":"import { ChordDetector } from '@openav/chord';\n\nconst chord = new ChordDetector();\nconst major = chord.analyze([60, 64, 67]);\nconsole.log(major.chordType, major.isTriad, major.consonance, major.thirdsFraction);   // major true 0.75 1\nconst cluster = chord.analyze([60, 61, 62, 63]);\nconsole.log(cluster.chordType, cluster.dissonanceLevel, cluster.isDissonant);          // cluster 2 true","caption":{"en":"The console prints `major true 0.75 1` for C-E-G, then `cluster 2 true` for four neighbouring semitones.","zh":"主控台對 C-E-G 印出 `major true 0.75 1`，對四個相鄰半音印出 `cluster 2 true`。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\nimport { ChordDetector } from '@openav/chord';\n\nconst signals = new Signals();\nsignals.define('midi/note/on', { kind: 'pulse' });\nsignals.define('midi/note/off', { kind: 'pulse' });\nconst chord = new ChordDetector({ signals, window: 30 });\nsignals.on('chord/event', (a) => console.log('chord/event:', a.chordType, a.count, 'notes'));\nsignals.on('midi/note/on', ({ note, vel }) => chord.noteOn(note, vel));      // what createShow wires for you\nsignals.on('midi/note/off', ({ note }) => chord.noteOff(note));\n\nfor (const note of [60, 64, 67]) signals.pulse('midi/note/on', { note, vel: 0.8 });   // struck together\nsetTimeout(() => console.log(signals.get('chord/consonance'), signals.get('chord/root')), 100);","caption":{"en":"Three notes struck together become one event: the console prints `chord/event: major 3 notes`, then `0.75 60` (consonance and root).","zh":"同時按下的三個音變成一個事件：主控台印出 `chord/event: major 3 notes`，然後是 `0.75 60`（協和度和根音）。"},"run":"module","lang":"js"},{"code":"import { ChordDetector } from '@openav/chord';\n\nconst chord = new ChordDetector();\nconst types = [[60, 64, 67], [64, 67, 72], [60, 64, 67, 70], [60, 63, 66], [60, 65, 67], [60, 67]];\nfor (const notes of types) console.log(JSON.stringify(notes), chord.analyze(notes).chordType);","caption":{"en":"Inversions resolve to the root-position name: the console prints `major`, `major` (E-G-C is C major again), `dom7`, `dim`, `sus4`, and `chord` for the bare fifth, which matches no template.","zh":"轉位會還原成原位和弦的名稱：主控台印出 `major`、`major`（E-G-C 還是 C 大三和弦）、`dom7`、`dim`、`sus4`，以及空心五度的 `chord`，它不符合任何範本。"},"run":"module","lang":"js"}],"related":["Signals","Mapper","createShow"],"keywords":{"en":["chord","chord detection","consonance","dissonance","triad","cluster","major","minor","harmony","notes","midi","gesture window"],"zh":["和弦","和弦偵測","協和度","不協和","三和弦","音叢","大調","小調","和聲","音符","手勢窗口"]},"tbd":[],"claims":[],"members":[{"name":"noteOn","member":"method","signature":"note, vel = 0.8","doc":"Feed from MIDI or an on-screen keyboard.","line":53},{"name":"noteOff","member":"method","signature":"note","doc":"","line":59},{"name":"analyze","member":"method","signature":"notes, vels = []","doc":"Pure function: MIDI note numbers → analysis.","line":80}]},
{"id":"OscOut","key":"OscOut","name":"OscOut","aliases":[],"kind":"class","package":"@openav/osc","category":"osc","documented":true,"signature":"new OscOut(url = 'http://127.0.0.1:7456')","import":"import { OscOut } from '@openav/osc';","source":{"file":"packages/osc/index.js","line":15,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/osc/index.js#L15"},"summary":{"en":"Sends OSC messages from the browser to TouchDesigner, Max, Reaper or a spatialiser, batched once per frame, through a small local bridge.","zh":"把 OSC 訊息從瀏覽器送到 TouchDesigner、Max、Reaper 或空間化軟體，每個畫面打包一批，透過一個本機的小橋接程式。"},"description":[{"en":"A browser cannot send UDP, so `OscOut` posts batches of messages to a ~100-line Node program, `packages/osc/bridges/osc-bridge.js`, which sends them on as OSC packets. Start it with `node packages/osc/bridges/osc-bridge.js 7456 192.168.1.50 3456` (HTTP port, target host, UDP port; the defaults are `7456 127.0.0.1 3456`). Then `send(address, args)` as often as you like and `flush()` once per frame: only the **latest message per address** survives a frame, because a position stream does not need its in-between values.","zh":"瀏覽器沒辦法送 UDP，所以 `OscOut` 把一批一批的訊息用 POST 送給一支大約一百行的 Node 程式 `packages/osc/bridges/osc-bridge.js`，由它再當成 OSC 封包送出去。用 `node packages/osc/bridges/osc-bridge.js 7456 192.168.1.50 3456` 啟動它（HTTP 連接埠、目標主機、UDP 連接埠，預設是 `7456 127.0.0.1 3456`）。然後想送多少次都可以 `send(address, args)`，每個畫面 `flush()` 一次：每個畫面裡，每個位址只有**最後一則訊息**會送出去，因為位置的串流不需要中間的值。"},{"en":"Numbers go out as OSC floats (what Spat, Reaper and TouchDesigner expect), and anything else as a string. `createShow()` does not create an `OscOut`: make one, call `enable()`, flush it from `onFrame`, and if you like hand it to the worlds as `show.stage.io.osc` and to the Layers panel as `show.app.osc` (which then shows the OSC rate).","zh":"數字會當成 OSC 浮點數送出（Spat、Reaper 和 TouchDesigner 期待的格式），其他東西一律當成字串。`createShow()` 不會建立 `OscOut`：你自己建一個、呼叫 `enable()`，在 `onFrame` 裡 flush，想要的話再用 `show.stage.io.osc` 交給世界，用 `show.app.osc` 交給 Layers 面板（面板就會顯示 OSC 的訊息率）。"}],"params":[{"name":"url","type":"String","optional":true,"default":"'http://127.0.0.1:7456'","en":"The address of the OSC bridge (its HTTP port, not the UDP target).","zh":"OSC 橋接程式的位址（它的 HTTP 連接埠，不是 UDP 的目的地）。"}],"needs":{"en":"The bridge must be running, and you must `await osc.enable()` first: it probes `GET /health` and answers `false` if nobody is there. Until it succeeds `flush()` just throws the queue away. On a public https page, Chrome asks permission before the page may reach `127.0.0.1`.","zh":"橋接程式必須在跑，而且你要先 `await osc.enable()`：它會探測 `GET /health`，沒有人在就回答 `false`。在成功之前，`flush()` 只會把佇列丟掉。在公開的 https 頁面上，Chrome 會在頁面連到 `127.0.0.1` 之前先詢問權限。"},"notes":[{"en":"Fields and hooks: `enabled`, `udp` (the target the bridge reported), `onStatus = (ok, info) => {}` and `onMsg = (address, args) => {}` (high frequency: use sparingly).","zh":"欄位與掛鉤：`enabled`、`udp`（橋接程式回報的目的地）、`onStatus = (ok, info) => {}` 和 `onMsg = (address, args) => {}`（頻率很高，請節制使用）。"},{"en":"If the bridge disappears, the first failed flush calls `onStatus(false, 'bridge lost')`, and after more than 30 failed flushes in a row `enabled` turns false by itself, with no further message. Call `enable()` again to resume. The messages of a failed flush are lost.","zh":"橋接程式消失的話，第一次失敗的 flush 會呼叫 `onStatus(false, 'bridge lost')`，連續失敗超過 30 次之後，`enabled` 會自己變成 false，不再有任何訊息。想恢復就再呼叫一次 `enable()`。失敗那次 flush 的訊息會遺失。"},{"en":"Every number goes out as a 32-bit float, whole numbers included (the bridge's own header comment mentions integers, but its code only writes floats). If the receiver needs integers, convert there.","zh":"每個數字都會當成 32 位元浮點數送出，整數也一樣（橋接程式檔案開頭的註解提到整數，但它的程式碼只會寫浮點數）。接收端如果需要整數，請在那邊轉換。"}],"examples":[{"code":"import { OscOut } from '@openav/osc';\n\nconst osc = new OscOut();                          // http://127.0.0.1:7456\nconsole.log(await osc.enable());                   // true when the bridge answers\nfunction frame(x, y, z) {\n  osc.send('/source/3/xyz', [x, y, z]);            // queue as often as you like\n  osc.flush();                                     // once per frame\n}\nframe(0.1, 0.5, 0.9);","caption":{"en":"With `node packages/osc/bridges/osc-bridge.js` running, the console prints `true` and the bridge forwards `/source/3/xyz` as an OSC packet with three floats.","zh":"`node packages/osc/bridges/osc-bridge.js` 在跑的時候，主控台印出 `true`，橋接程式把 `/source/3/xyz` 當成帶三個浮點數的 OSC 封包轉送出去。"},"run":"static","lang":"js"},{"code":"import { OscOut } from '@openav/osc';\n\n// a stand-in for the bridge, so this runs anywhere\nglobalThis.fetch = async (url, options = {}) => {\n  if (options.body) console.log('POST', url, options.body);\n  return { json: async () => ({ ok: true, udp: '127.0.0.1:3456' }) };\n};\n\nconst osc = new OscOut();\nosc.onStatus = (ok, info) => console.log('status', ok, info);\nawait osc.enable();\nosc.send('/source/1/xyz', [0.1, 0.2, 0.3]);\nosc.send('/source/1/xyz', [0.4, 0.5, 0.6]);   // the same address: replaces the first\nosc.send('/gain', 0.8);                       // a single value is wrapped in an array\nawait osc.flush();\nconsole.log('rate', osc.rate());              // 2 messages went out","caption":{"en":"Three `send()` calls become two messages: the console prints `status true 127.0.0.1:3456`, then the one `POST` with `/source/1/xyz` carrying `[0.4,0.5,0.6]` and `/gain` carrying `[0.8]`, then `rate 2`.","zh":"三次 `send()` 變成兩則訊息：主控台印出 `status true 127.0.0.1:3456`，然後是那一個 `POST`，裡面 `/source/1/xyz` 帶著 `[0.4,0.5,0.6]`、`/gain` 帶著 `[0.8]`，最後是 `rate 2`。"},"run":"module","lang":"js"},{"code":"import { OscOut } from '@openav/osc';\n\nconst osc = new OscOut('http://127.0.0.1:7499');    // an address where no bridge runs\nosc.onStatus = (ok, info) => console.log('status', ok, info);\nconsole.log(await osc.enable());                  // false: nobody answers\nosc.send('/gain', 0.5);\nawait osc.flush();                                // nothing is sent while disabled: the queue is dropped","caption":{"en":"With no bridge, `enable()` does not throw: the console prints `status false bridge not running (node bridges/osc-bridge.js)`, then `false`. (The browser may also log one failed request.)","zh":"沒有橋接程式的時候，`enable()` 不會丟出錯誤：主控台印出 `status false bridge not running (node bridges/osc-bridge.js)`，然後是 `false`。（瀏覽器可能另外記下一次失敗的請求。）"},"run":"module","lang":"js"}],"related":["OutClient","createShow","Signals"],"keywords":{"en":["osc","open sound control","touchdesigner","max","reaper","spat","udp","bridge","send","flush","output","lighting"],"zh":["osc","open sound control","touchdesigner","max","reaper","空間化","udp","橋接","送出","輸出","燈光"]},"tbd":[{"en":"Not tried here against TouchDesigner, Max or Reaper: the examples use a stand-in for the bridge, and what the bridge does with the packets is read from its code (floats for numbers, strings for the rest).","zh":"這裡沒有對 TouchDesigner、Max 或 Reaper 實測：範例是用一個代替橋接程式的函式，橋接程式如何處理封包是從它的程式碼讀出來的（數字用浮點數，其餘用字串）。"}],"claims":[],"members":[{"name":"enable","member":"method","signature":"","doc":"","line":29},{"name":"disable","member":"method","signature":"","doc":"","line":45},{"name":"send","member":"method","signature":"addr, args","doc":"","line":47},{"name":"rate","member":"method","signature":"","doc":"msgs per second (resets on read) — monitor food.","line":54},{"name":"flush","member":"method","signature":"","doc":"","line":56}]},
{"id":"out-DEFAULT_PORT","key":"DEFAULT_PORT","name":"DEFAULT_PORT","aliases":[],"kind":"constant","package":"@openav/out","category":"osc","documented":true,"signature":"DEFAULT_PORT = 6450","import":"import { DEFAULT_PORT } from '@openav/out/bridge';","source":{"file":"packages/out/bridge/out-bridge.mjs","line":27,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/bridge/out-bridge.mjs#L27"},"summary":{"en":"The out-bridge's default port: 6450.","zh":"輸出橋接程式預設的連接埠：6450。"},"description":[{"en":"The default of `startBridge({ port })` and of `--port`; the environment variable `OUT_BRIDGE_PORT` changes it for the command line. It is the **out**-bridge's port: the Leap bridge has its own ([`DEFAULT_PORT` of `@openav/leap`](#leap-DEFAULT_PORT), 6437).","zh":"`startBridge({ port })` 和 `--port` 的預設值。環境變數 `OUT_BRIDGE_PORT` 可以改命令列用的值。這是**輸出**橋接程式的連接埠：Leap 橋接程式有自己的（[`@openav/leap` 的 `DEFAULT_PORT`](#leap-DEFAULT_PORT)，6437）。"}],"params":[],"notes":[],"examples":[],"related":["OUT_DEFAULT_URL"],"keywords":{"en":["port","6450","default port","out bridge port"],"zh":["連接埠","6450","預設連接埠","輸出橋接連接埠"]},"tbd":[],"claims":[]},
{"id":"DEFAULT_TIMEOUT_MS","key":"DEFAULT_TIMEOUT_MS","name":"DEFAULT_TIMEOUT_MS","aliases":[],"kind":"constant","package":"@openav/out","category":"osc","documented":true,"signature":"DEFAULT_TIMEOUT_MS = 500","import":"import { DEFAULT_TIMEOUT_MS } from '@openav/out/bridge';","source":{"file":"packages/out/bridge/out-bridge.mjs","line":29,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/bridge/out-bridge.mjs#L29"},"summary":{"en":"The watchdog time when the config does not say: 500 milliseconds without a value, and a channel is put on its safe value.","zh":"設定檔沒寫的時候，看門狗的時間：500 毫秒沒有收到值，頻道就會被放到它的安全值。"},"description":[{"en":"It is the default of `timeoutMs` in the bridge config (the minimum allowed is 20). [`OutClient`](#OutClient) re-sends held values every third of whatever the bridge reports, so a still slider is not mistaken for a dead page. Lower it for hardware that must stop quickly, but keep it above the slowest frame your page may have.","zh":"這是橋接設定裡 `timeoutMs` 的預設值（允許的最小值是 20）。[`OutClient`](#OutClient) 每隔橋接程式回報的時間的三分之一就重送目前的值，所以停著不動的滑桿不會被誤認為當掉的頁面。需要硬體很快停下來的話可以調低它，但要比你的頁面可能出現的最慢畫面更長。"}],"params":[],"notes":[],"examples":[],"related":["OutClient"],"keywords":{"en":["timeout","watchdog","500 ms","safe value","timeoutMs"],"zh":["逾時","看門狗","500 毫秒","安全值","timeoutMs"]},"tbd":[],"claims":[]},
{"id":"globToRegExp","key":"globToRegExp","name":"globToRegExp","aliases":[],"kind":"function","package":"@openav/out","category":"osc","documented":true,"signature":"globToRegExp(glob)","import":"import { globToRegExp } from '@openav/out/bridge';","source":{"file":"packages/out/bridge/out-bridge.mjs","line":37,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/bridge/out-bridge.mjs#L37"},"summary":{"en":"Turns a channel glob like `light/*` into the regular expression the bridge matches channel names with: only `*` is special, and it crosses slashes.","zh":"把像 `light/*` 這樣的頻道萬用字元，轉成橋接程式用來比對頻道名稱的正規表示式：只有 `*` 有特殊意義，而且它可以跨過斜線。"},"description":[{"en":"The `match` of a channel rule and the keys of `safe` in the bridge config are globs, and this is how both are read. Remember that `*` also crosses slashes: `light/*` fits `light/0/r`, and `*` alone fits every channel.","zh":"橋接設定裡，頻道規則的 `match` 和 `safe` 的鍵都是萬用字元，兩者都是用它來解讀。請記得 `*` 也能跨過斜線：`light/*` 符合 `light/0/r`，只寫 `*` 就符合每一個頻道。"}],"params":[{"name":"glob","type":"String","en":"The pattern. `*` matches any run of characters, slashes included. Everything else, including `.`, `?` and `+`, matches itself.","zh":"樣式。`*` 符合任何一串字元，包含斜線。其他一切，包括 `.`、`?` 和 `+`，都只符合它自己。"}],"returns":{"type":"RegExp","en":"An anchored expression: `globToRegExp('light/*')` is `/^light\\/.*$/`.","zh":"一個固定頭尾的正規表示式：`globToRegExp('light/*')` 是 `/^light\\/.*$/`。"},"notes":[],"examples":[{"code":"import { globToRegExp } from '@openav/out/bridge';\n\nconst lights = globToRegExp('light/*');\nconsole.log(lights.test('light/0/r'), lights.test('motor/1'));   // true false\nconsole.log(globToRegExp('*').test('any/channel/at/all'));       // true\nconsole.log(globToRegExp('a.b').test('axb'));                    // false: the dot is only a dot","caption":{"en":"In Node: the console prints `true false`, then `true`, then `false`.","zh":"在 Node 裡：主控台印出 `true false`，然後是 `true`，再來是 `false`。"},"run":"static","lang":"js"}],"related":["loadConfig"],"keywords":{"en":["glob","wildcard","pattern","match","channel match","regular expression","light/*"],"zh":["萬用字元","樣式","比對","頻道比對","正規表示式","light/*"]},"tbd":[],"claims":[]},
{"id":"loadConfig","key":"loadConfig","name":"loadConfig","aliases":[],"kind":"function","package":"@openav/out","category":"osc","documented":true,"signature":"loadConfig(file)","import":"import { loadConfig } from '@openav/out/bridge';","source":{"file":"packages/out/bridge/out-bridge.mjs","line":41,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/bridge/out-bridge.mjs#L41"},"summary":{"en":"Reads the out-bridge config from a JSON file and returns it as an object (Node only); throws when the file cannot be read.","zh":"從 JSON 檔讀取輸出橋接程式的設定並回傳成物件（只能在 Node 執行），讀不到檔案就丟出錯誤。"},"description":[{"en":"A thin helper around `fs.readFileSync` and `JSON.parse`. A missing file or invalid JSON throws one error that names the file (its text is written in Chinese in the code). The command line uses it for `--config out.json`; in your own program you may just as well pass an object to `startBridge({ config })`.","zh":"包著 `fs.readFileSync` 和 `JSON.parse` 的薄薄一層。找不到檔案或 JSON 不合法，會丟出一個指出檔名的錯誤（訊息在程式碼裡是用中文寫的）。命令列的 `--config out.json` 就是用它。在你自己的程式裡，直接把物件傳給 `startBridge({ config })` 也一樣。"}],"params":[{"name":"file","type":"String","en":"The path of the JSON file, such as `out.json` (copy `packages/out/bridge/out.example.json` to start).","zh":"JSON 檔的路徑，例如 `out.json`（可以從複製 `packages/out/bridge/out.example.json` 開始）。"}],"returns":{"type":"Object","en":"The parsed config, unchecked: [`startBridge()`](#out-startBridge) is what validates it.","zh":"解析後的設定，沒有檢查：驗證它的是 [`startBridge()`](#out-startBridge)。"},"notes":[],"examples":[{"code":"import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { loadConfig } from '@openav/out/bridge';\n\nconst file = path.join(os.tmpdir(), 'oav-example-out.json');          // a scratch file, not your real config\nfs.writeFileSync(file, JSON.stringify({ channels: [{ match: '*', sink: 'log' }], safe: { 'light/*': 0 } }));\nconst config = loadConfig(file);\nconsole.log(config.channels.length, config.safe);      // 1 { 'light/*': 0 }\ntry { loadConfig('missing.json'); } catch (error) { console.log('cannot load:', error.message.includes('missing.json')); }   // true","caption":{"en":"In Node: the console prints `1 { 'light/*': 0 }`, then `cannot load: true`: the error names the missing file.","zh":"在 Node 裡：主控台印出 `1 { 'light/*': 0 }`，然後是 `cannot load: true`：錯誤訊息裡有那個找不到的檔名。"},"run":"static","lang":"js"}],"related":["globToRegExp"],"keywords":{"en":["load config","config file","out.json","json","read config","bridge config"],"zh":["讀取設定","設定檔","out.json","json","橋接設定"]},"tbd":[],"claims":[]},
{"id":"OUT_DEFAULT_URL","key":"OUT_DEFAULT_URL","name":"OUT_DEFAULT_URL","aliases":[],"kind":"constant","package":"@openav/out","category":"osc","documented":true,"signature":"OUT_DEFAULT_URL = \"ws://127.0.0.1:6450/out\"","import":"import { OUT_DEFAULT_URL } from '@openav/out';","source":{"file":"packages/out/client.js","line":15,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/client.js#L15"},"summary":{"en":"The address `OutClient` connects to by default: `ws://127.0.0.1:6450/out`, the out-bridge on this machine.","zh":"`OutClient` 預設連的位址：`ws://127.0.0.1:6450/out`，也就是這台機器上的輸出橋接程式。"},"description":[{"en":"Port 6450 is the bridge's [`DEFAULT_PORT`](#out-DEFAULT_PORT) and `/out` is the only WebSocket path it serves. In the lab, `?out=ws://host:port/out` points a page at another bridge, `?out=sim` records values without one and `?out=0` turns it off.","zh":"6450 是橋接程式的 [`DEFAULT_PORT`](#out-DEFAULT_PORT)，`/out` 是它唯一提供的 WebSocket 路徑。在實驗室裡，`?out=ws://host:port/out` 可以讓頁面改連別的橋接程式，`?out=sim` 只記錄不連線，`?out=0` 則是關掉。"}],"params":[],"notes":[],"examples":[],"related":["OutClient"],"keywords":{"en":["default url","ws://127.0.0.1:6450/out","bridge address","6450","out url"],"zh":["預設網址","ws://127.0.0.1:6450/out","橋接位址","6450","輸出網址"]},"tbd":[],"claims":[]},
{"id":"OutClient","key":"OutClient","name":"OutClient","aliases":[],"kind":"class","package":"@openav/out","category":"osc","documented":true,"signature":"new OutClient({ url = OUT_DEFAULT_URL, id = null, WebSocket: WS, schedule, maxBackoffMs = 5000, minBackoffMs = 250 } = {})","import":"import { OutClient } from '@openav/out';","source":{"file":"packages/out/client.js","line":17,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/client.js#L17"},"summary":{"en":"The page side of the out-bridge: `set(channel, value)` sends a number to a named hardware channel, at most once per frame, and keeps the connection alive.","zh":"輸出橋接程式在頁面這一側的部分：`set(channel, value)` 把一個數字送到有名字的硬體頻道，每個畫面最多送一次，並且維持連線。"},"description":[{"en":"Hardware needs a number, and a page has a number: `out.set('light/0/r', 0.8)`. The channel name is yours (`a-z A-Z 0-9 _ - . /`, up to 128 characters) and the **bridge** ([`startBridge()`](#out-startBridge)) decides from its config where that channel goes: a WLED strip, a serial motor board, an OSC address. Values are plain numbers; the convention is 0..1. Several `set()` calls on one channel inside one frame leave as one value, the last. In a lab work `lab.out` is this class.","zh":"硬體需要的是數字，而頁面有數字：`out.set('light/0/r', 0.8)`。頻道名稱由你決定（`a-z A-Z 0-9 _ - . /`，最長 128 個字元），這個頻道要送去哪裡，由**橋接程式**（[`startBridge()`](#out-startBridge)）依它的設定決定：WLED 燈條、序列埠馬達板、OSC 位址。值就是普通的數字，慣例是 0..1。同一個畫面裡對同一個頻道呼叫好幾次 `set()`，只會送出最後一個值。在實驗室的作品裡，`lab.out` 就是這個類別。"},{"en":"It keeps the connection alive on its own: it reconnects with a growing delay (250 ms doubling up to 5 s), resends everything it knows after a reconnect (a restarted bridge has forgotten it all), and while the socket is open it re-sends the held values every `timeoutMs / 3` of the bridge's watchdog, so a slider that stands still is not mistaken for a frozen page. If the page really freezes, that heartbeat stops and the bridge puts the hardware on its safe values.","zh":"它自己會維持連線：用愈來愈長的間隔重連（250 毫秒起，加倍到 5 秒），重連之後把它知道的值全部重送一遍（重新啟動的橋接程式什麼都忘了），而且在 socket 開著的時候，每隔橋接程式的看門狗時間的三分之一就重送一次目前的值，所以停著不動的滑桿不會被誤認為當掉的頁面。如果頁面真的凍結，這個心跳就會停，橋接程式就讓硬體回到安全值。"}],"params":[{"name":"url","type":"String","optional":true,"default":"OUT_DEFAULT_URL","en":"The bridge's WebSocket address. The default is [`OUT_DEFAULT_URL`](#OUT_DEFAULT_URL), `ws://127.0.0.1:6450/out`.","zh":"橋接程式的 WebSocket 位址。預設是 [`OUT_DEFAULT_URL`](#OUT_DEFAULT_URL)，也就是 `ws://127.0.0.1:6450/out`。"},{"name":"id","type":"String | null","optional":true,"default":"null","en":"A name for this page (the work's id), sent to the bridge in its \"hello\" and shown in its log.","zh":"這個頁面的名字（作品的 id），會在 \"hello\" 訊息裡送給橋接程式，並顯示在它的紀錄裡。"},{"name":"WebSocket","type":"Function","optional":true,"en":"The WebSocket constructor to use. Defaults to the global one (also in Node 22). Pass your own to test without a network.","zh":"要用的 WebSocket 建構函式。預設是全域的那個（Node 22 也有）。傳你自己的，就能在沒有網路的情況下測試。"},{"name":"schedule","type":"Function","optional":true,"en":"A function that runs a callback once, on the next frame. The default is `requestAnimationFrame` (or a 16 ms timer when the tab is hidden or there is no `requestAnimationFrame`).","zh":"一個在下一個畫面執行一次回呼的函式。預設是 `requestAnimationFrame`（分頁被藏起來或沒有 `requestAnimationFrame` 時，改用 16 毫秒的計時器）。"},{"name":"maxBackoffMs","type":"Number","optional":true,"default":"5000","en":"The longest wait between reconnection attempts, in milliseconds.","zh":"重新連線之間最長的等待時間，單位是毫秒。"},{"name":"minBackoffMs","type":"Number","optional":true,"default":"250","en":"The first wait between reconnection attempts. It doubles each time up to `maxBackoffMs`.","zh":"重新連線之間第一次的等待時間。每次加倍，直到 `maxBackoffMs`。"}],"needs":{"en":"Call `connect()` (the bridge must be running: `node packages/out/bridge/out-bridge.mjs --config out.json`), or `simulate()` for no hardware at all. `set()` before the socket is open is not lost: it waits in `last` and goes out on open.","zh":"要呼叫 `connect()`（橋接程式必須在跑：`node packages/out/bridge/out-bridge.mjs --config out.json`），或呼叫 `simulate()` 完全不接硬體。在 socket 開啟之前呼叫 `set()` 不會遺失：它會先放在 `last`，等開啟之後送出。"},"notes":[{"en":"Fields to read: `status` (`'closed'`, `'connecting'`, `'open'`, `'sim'`), `last` (`{ channel: last value given to set() }`, also in simulate mode), `sent` (values actually put on the wire: less than the number of `set()` calls because of coalescing) and `bridge` (the bridge's last status message).","zh":"可以讀的欄位：`status`（`'closed'`、`'connecting'`、`'open'`、`'sim'`）、`last`（`{ 頻道: 最後給 set() 的值 }`，模擬模式下也有）、`sent`（真正送上線路的值：因為合併，會比 `set()` 呼叫次數少），以及 `bridge`（橋接程式最近的狀態訊息）。"},{"en":"On the wire a single value is `{ \"type\": \"set\", \"ch\", \"v\" }` and several are `{ \"type\": \"batch\", \"items\": [{ \"ch\", \"v\" }, …] }`, after a `{ \"type\": \"hello\", \"id\" }`.","zh":"在線路上，單一個值是 `{ \"type\": \"set\", \"ch\", \"v\" }`，多個值是 `{ \"type\": \"batch\", \"items\": [{ \"ch\", \"v\" }, …] }`，前面先有一則 `{ \"type\": \"hello\", \"id\" }`。"},{"en":"On a public https page, Chrome asks permission before the page may reach `127.0.0.1`; if it is refused, the status stays `'connecting'`.","zh":"在公開的 https 頁面上，Chrome 會在頁面連到 `127.0.0.1` 之前詢問權限，被拒絕的話，狀態會一直停在 `'connecting'`。"}],"examples":[{"code":"import { OutClient } from '@openav/out';\n\nconst out = new OutClient({ id: 'my-work' });       // ws://127.0.0.1:6450/out\nout.onStatus((status) => console.log('out', status));   // connecting, open\nout.connect();\nout.set('light/0/r', 0.8);                          // waits for the socket, then goes out\nfunction frame(t) {\n  out.set('light/0/g', 0.5 + 0.5 * Math.sin(t / 1000));   // call as often as you like: one value a frame leaves\n  requestAnimationFrame(frame);\n}\nrequestAnimationFrame(frame);","caption":{"en":"With the out-bridge running, the console prints `out connecting`, then `out open`, and the green channel breathes between 0 and 1.","zh":"輸出橋接程式開著的時候，主控台印出 `out connecting`，然後是 `out open`，綠色那個頻道在 0 和 1 之間起伏。"},"run":"static","lang":"js"},{"code":"import { OutClient } from '@openav/out';\n\nclass FakeSocket {                                  // stands in for the bridge's socket, so this runs anywhere\n  constructor() { this.readyState = 1; setTimeout(() => this.onopen(), 0); }\n  send(text) { console.log('wire:', text); }\n  close() {}\n}\nconst out = new OutClient({ id: 'demo', WebSocket: FakeSocket, schedule: (fn) => setTimeout(fn, 0) });\nout.connect();\nsetTimeout(() => {\n  out.set('light/0/r', 0.8);\n  out.set('light/0/r', 0.9);                        // same channel, same frame: only 0.9 leaves\n  out.batch({ 'light/0/g': 0.2, 'motor/1': 0.5 });\n  setTimeout(() => { console.log('sent', out.sent, out.status); out.close(); }, 50);\n}, 20);","caption":{"en":"The console prints the `hello`, then one `batch` of three values (`light/0/r` is 0.9, not 0.8), then `sent 3 open`.","zh":"主控台先印出 `hello`，然後是一個含三個值的 `batch`（`light/0/r` 是 0.9，不是 0.8），最後是 `sent 3 open`。"},"run":"module","lang":"js"},{"code":"import { OutClient } from '@openav/out';\n\nconst out = new OutClient().simulate();              // no bridge, no hardware\nout.set('light/0/r', 1);\nout.set('light/0/r', 0.25);\nout.set('typo', 'not a number');                     // ignored: not finite\nconsole.log(out.status, out.last);                   // sim { 'light/0/r': 0.25 }","caption":{"en":"Simulate mode records instead of sending: the console prints `sim { 'light/0/r': 0.25 }`; the bad value was ignored.","zh":"模擬模式只記錄不送出：主控台印出 `sim { 'light/0/r': 0.25 }`，壞的值被忽略了。"},"run":"module","lang":"js"}],"related":["OUT_DEFAULT_URL","OscOut","LeapInput"],"keywords":{"en":["out","output","hardware","wled","led strip","motor","serial","dmx","ddp","channel","watchdog","bridge","websocket","lights","set"],"zh":["輸出","硬體","wled","燈條","馬達","序列埠","ddp","頻道","看門狗","橋接","websocket","燈光"]},"tbd":[{"en":"Not tried here with real hardware (a WLED strip, a motor board): the examples use a stand-in socket and the bridge's `log` sink.","zh":"這裡沒有用真正的硬體（WLED 燈條、馬達板）試過：範例用的是代替的 socket 和橋接程式的 `log` 輸出。"}],"claims":[],"members":[{"name":"onStatus","member":"method","signature":"fn","doc":"","line":40},{"name":"connect","member":"method","signature":"","doc":"Open the socket (and keep it open: reconnects with exponential backoff, capped).","line":44},{"name":"simulate","member":"method","signature":"","doc":"No socket: values are only recorded in .last (for sketches without hardware, ?out=sim).","line":82},{"name":"close","member":"method","signature":"","doc":"","line":83},{"name":"set","member":"method","signature":"ch, v","doc":"","line":86},{"name":"batch","member":"method","signature":"obj","doc":"","line":94}]},
{"id":"out-RESERVED_PORTS","key":"RESERVED_PORTS","name":"RESERVED_PORTS","aliases":[],"kind":"constant","package":"@openav/out","category":"osc","documented":true,"signature":"RESERVED_PORTS = array, 1 items","import":"import { RESERVED_PORTS } from '@openav/out/bridge';","source":{"file":"packages/out/bridge/out-bridge.mjs","line":28,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/bridge/out-bridge.mjs#L28"},"summary":{"en":"The ports the out-bridge refuses to use: `[7457]`, which belongs to The Last Input and the backstage monitor.","zh":"輸出橋接程式拒絕使用的連接埠：`[7457]`，那是 The Last Input 和後台監看用的。"},"description":[{"en":"`startBridge({ port: 7457 })` throws at once, so the bridge can never take the port the monitor listens on. The Leap bridge refuses the same one ([`RESERVED_PORTS` of `@openav/leap`](#leap-RESERVED_PORTS)).","zh":"`startBridge({ port: 7457 })` 會立刻丟出錯誤，所以橋接程式絕不會占走後台監看監聽的連接埠。Leap 橋接程式也拒絕同一個（[`@openav/leap` 的 `RESERVED_PORTS`](#leap-RESERVED_PORTS)）。"}],"params":[],"notes":[],"examples":[],"related":["MonitorFeed"],"keywords":{"en":["reserved ports","7457","refused port","monitor port"],"zh":["保留連接埠","7457","拒絕的連接埠","監看連接埠"]},"tbd":[],"claims":[]},
{"id":"out-startBridge","key":"startBridge","name":"startBridge","aliases":[],"kind":"function","package":"@openav/out","category":"osc","documented":true,"signature":"startBridge({ port = DEFAULT_PORT, host = '127.0.0.1', config = null, watchdog = true, log = () => {}, print = (s) => console.log(s) } = {})","import":"import { startBridge } from '@openav/out/bridge';","source":{"file":"packages/out/bridge/out-bridge.mjs","line":57,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/out/bridge/out-bridge.mjs#L57"},"summary":{"en":"Starts the out-bridge: a loopback WebSocket server that routes channel values from the page to hardware sinks, with a watchdog that falls back to safe values (Node only).","zh":"啟動輸出橋接程式：一個只綁 loopback 的 WebSocket 伺服器，把頁面傳來的頻道值導向硬體輸出，並有看門狗在頁面沒聲音時退回安全值（只能在 Node 執行）。"},"description":[{"en":"This is what `node packages/out/bridge/out-bridge.mjs --config out.json` runs (also `--port`, `--no-watchdog`, `--quiet`; the environment variable `OUT_BRIDGE_PORT`). It serves `ws://127.0.0.1:6450/out`, which understands `{ type: 'hello', id }`, `{ type: 'set', ch, v }`, `{ type: 'batch', items }` and `{ type: 'status' }`, and answers with `{ type: 'status', … }`; `GET /health` reports the sinks, the channel count, the clients and the time since the last value. Channel names are `a-z A-Z 0-9 _ - . /` up to 128 characters, values must be finite numbers, and there is room for 4096 channels.","zh":"`node packages/out/bridge/out-bridge.mjs --config out.json` 執行的就是它（另有 `--port`、`--no-watchdog`、`--quiet`，以及環境變數 `OUT_BRIDGE_PORT`）。它提供 `ws://127.0.0.1:6450/out`，聽得懂 `{ type: 'hello', id }`、`{ type: 'set', ch, v }`、`{ type: 'batch', items }` 和 `{ type: 'status' }`，並以 `{ type: 'status', … }` 回應。`GET /health` 回報輸出、頻道數、用戶端和距離上一個值的時間。頻道名稱是 `a-z A-Z 0-9 _ - . /`，最長 128 個字元，值必須是有限數，最多容納 4096 個頻道。"},{"en":"The config routes channels: `channels` is a list of `{ match, sink, … }` and the **first** rule whose `match` glob fits the channel name takes it (`*` means anything, slashes included); a channel no rule fits is counted and dropped. The sinks are `log` (stdout), `udp-json` (UDP, `{\"ch\",\"v\"}`), `osc` (UDP, address `\"/\" + channel`, one float32), `ddp` (WLED over UDP 4048: `light/<i>/r|g|b`, 0..1, pixel index from 0, up to 10000 pixels) and `serial` (a device path, `ch value` lines or JSON, latest value only). `safe` maps a channel glob to its safe value, `timeoutMs` (default 500, at least 20) is the watchdog time, and `allowOrigins` lists the page origins that may connect (without it any page on this machine can).","zh":"設定檔負責把頻道導向各處：`channels` 是 `{ match, sink, … }` 的清單，**第一條** `match` 萬用字元符合頻道名稱的規則就接手它（`*` 代表任何東西，包含斜線）。沒有規則符合的頻道會被計數然後丟掉。輸出（sink）有 `log`（標準輸出）、`udp-json`（UDP，`{\"ch\",\"v\"}`）、`osc`（UDP，位址是 `\"/\" + 頻道`，一個 float32）、`ddp`（用 UDP 4048 控制 WLED：`light/<i>/r|g|b`，0..1，像素索引從 0 開始，最多 10000 個像素）和 `serial`（裝置路徑，`ch value` 的行或 JSON，只送最新的值）。`safe` 把頻道萬用字元對應到它的安全值，`timeoutMs`（預設 500，至少 20）是看門狗的時間，`allowOrigins` 列出允許連線的頁面來源（沒寫的話，這台機器上的任何頁面都能連）。"},{"en":"The **watchdog**: a channel that has a safe value and gets no new value for `timeoutMs` is sent its safe value once (a new value re-arms it). When the last client disconnects, every channel the bridge has seen that has a safe value gets it at once, and so does a clean shutdown. It does not cover channels the bridge has never heard of, nor a bridge killed with SIGKILL.","zh":"**看門狗**：有安全值的頻道，如果 `timeoutMs` 內沒有收到新的值，就會被送出一次安全值（新的值進來會重新啟動它）。最後一個用戶端斷線的時候，橋接程式見過的每個有安全值的頻道會立刻得到它的安全值，正常關閉時也一樣。它不涵蓋橋接程式從沒聽過的頻道，也不涵蓋被 SIGKILL 殺掉的橋接程式。"},{"en":"It throws at once for port 7457 (it belongs to The Last Input and the monitor), for a `host` that is not loopback, and for a bad config (no `channels`, an unknown sink, a `safe` value that is not a number, a `timeoutMs` under 20). A port already in use rejects the returned promise.","zh":"連接埠是 7457（那是 The Last Input 和後台監看用的）、`host` 不是 loopback、設定有問題（沒有 `channels`、不認得的 sink、`safe` 的值不是數字、`timeoutMs` 小於 20），它都會立刻丟出錯誤。連接埠已被占用，回傳的 promise 會被拒絕。"}],"params":[{"name":"port","type":"Number","optional":true,"default":"DEFAULT_PORT","en":"The port to listen on, [`DEFAULT_PORT`](#out-DEFAULT_PORT) unless you say otherwise. 0 picks any free one. The ports in [`RESERVED_PORTS`](#out-RESERVED_PORTS) (7457) are refused.","zh":"要監聽的連接埠，沒特別指定就是 [`DEFAULT_PORT`](#out-DEFAULT_PORT)。0 表示隨便挑一個空的。[`RESERVED_PORTS`](#out-RESERVED_PORTS) 裡的連接埠（7457）會被拒絕。"},{"name":"host","type":"String","optional":true,"default":"'127.0.0.1'","en":"The address to bind. Only loopback is allowed.","zh":"要綁定的位址。只允許 loopback。"},{"name":"config","type":"Object | null","optional":true,"default":"null","en":"The routing: `{ channels: [{ match, sink, … }], safe: { glob: value }, timeoutMs, allowOrigins }` (see the description). `null` sends every channel to the `log` sink. [`loadConfig(file)`](#loadConfig) reads it from a JSON file.","zh":"路由設定：`{ channels: [{ match, sink, … }], safe: { glob: value }, timeoutMs, allowOrigins }`（見說明）。`null` 的話，所有頻道都送到 `log` 輸出。[`loadConfig(file)`](#loadConfig) 可以從 JSON 檔讀取。"},{"name":"watchdog","type":"Boolean","optional":true,"default":"true","en":"`false` switches the watchdog off: no safe values are ever sent (the tests use it to prove the watchdog is what does the work).","zh":"`false` 會關掉看門狗：永遠不送安全值（測試用它來證明做事的是看門狗）。"},{"name":"log","type":"Function","optional":true,"default":"() => {}","en":"Called with a line of text for diagnostics: clients coming and going, safe values sent.","zh":"以一行文字呼叫，給診斷用：用戶端進出、送出了安全值。"},{"name":"print","type":"Function","optional":true,"default":"(s) => console.log(s)","en":"What the `log` sink prints each value with (`\"channel value\"`). The default is stdout.","zh":"`log` 輸出用來印每個值的函式（`\"channel value\"`）。預設是標準輸出。"}],"returns":{"type":"Promise<Object>","en":"Resolves once listening, with `{ port, state, health(), close() }`: the real port, the counters (`received`, `rejected`, `unrouted`, `safeSent` …), a function returning the `/health` object, and a function that puts every known channel on its safe value, stops the server and returns a Promise.","zh":"開始監聽之後才完成，結果是 `{ port, state, health(), close() }`：實際的連接埠、計數器（`received`、`rejected`、`unrouted`、`safeSent` ……）、回傳 `/health` 物件的函式，以及會讓每個已知頻道回到安全值、停掉伺服器並回傳 Promise 的函式。"},"needs":{"en":"Node 22 or later. The sinks need what they talk to: a WLED device for `ddp`, the device file for `serial`, a listener for `osc`.","zh":"Node 22 以上。各個輸出需要它要連的對象：`ddp` 要有 WLED 裝置，`serial` 要有裝置檔，`osc` 要有人在聽。"},"notes":[{"en":"The error messages the bridge throws and sends back to a page are written in Chinese in the code, so a page may show them in Chinese whatever its language.","zh":"橋接程式丟出的、以及回傳給頁面的錯誤訊息，在程式碼裡是用中文寫的，所以不管頁面是什麼語言，頁面上看到的可能是中文。"}],"examples":[{"code":"import { startBridge } from '@openav/out/bridge';\nimport { OutClient } from '@openav/out';\n\nconst bridge = await startBridge({\n  port: 0,                                                       // any free port\n  config: { channels: [{ match: '*', sink: 'log' }], safe: { 'light/*': 0 }, timeoutMs: 2000 },\n  print: (line) => console.log('sink:', line),\n});\nconst out = new OutClient({ url: 'ws://127.0.0.1:' + bridge.port + '/out', id: 'demo' }).connect();\nsetTimeout(() => out.set('light/0/r', 0.8), 100);                // reaches the sink\nsetTimeout(() => out.close(), 300);                              // the last client leaves: safe values go out\nsetTimeout(() => bridge.close(), 600);","caption":{"en":"In Node: the console prints `sink: light/0/r 0.8`, then `sink: light/0/r 0`: when the page goes away the bridge puts the light channel on its safe value.","zh":"在 Node 裡：主控台印出 `sink: light/0/r 0.8`，然後是 `sink: light/0/r 0`：頁面一離開，橋接程式就讓燈的頻道回到安全值。"},"run":"static","lang":"js"}],"related":["OutClient","loadConfig","globToRegExp","DEFAULT_TIMEOUT_MS"],"keywords":{"en":["out bridge","bridge","wled","ddp","serial","osc sink","watchdog","safe value","hardware","config","channels","node server"],"zh":["輸出橋接","橋接程式","wled","ddp","序列埠","osc","看門狗","安全值","硬體","設定檔","頻道","node 伺服器"]},"tbd":[{"en":"The `ddp` (WLED) and `serial` sinks were not tried here with real hardware; what the description says about them comes from the package README and the code. The `log` sink and the watchdog were run.","zh":"`ddp`（WLED）和 `serial` 輸出沒有用真正的硬體試過。說明裡講到它們的部分來自套件的 README 和程式碼。`log` 輸出和看門狗是實際跑過的。"}],"claims":[]},
{"id":"LearnWizard","key":"LearnWizard","name":"LearnWizard","aliases":[],"kind":"class","package":"@openav/mapping","category":"mapping","documented":true,"signature":"new LearnWizard({ mapper, keys, onChange = null })","import":"import { LearnWizard } from '@openav/mapping';","source":{"file":"packages/mapping/wizard.js","line":16,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/wizard.js#L16"},"summary":{"en":"Binds a list of params one after another: it arms learn for the first, you turn a knob, it binds it and arms the next. Skip, back and stop steer it.","zh":"依序綁定一整串參數：先替第一個備好 learn，你轉一個旋鈕，它就綁上去，再備好下一個。skip、back、stop 可以操控它。"},"description":[{"en":"Binding twelve params one by one with the learn chip means twelve clicks and twelve guesses about what to touch next. The wizard walks the list for you: `start()` arms learn for `keys[0]`, `wizard.current` says which param it is waiting for ('Turn a knob for: hue (1/12)'), you move a control, it is bound and the next one is armed. At the end learn is disarmed. The console's **🎛 map knobs in order** button is this class.","zh":"用 learn 標籤一個一個綁十二個參數，就是點十二次，還要猜十二次下一個該碰哪裡。精靈替你走完整份清單：`start()` 替 `keys[0]` 備好 learn，`wizard.current` 告訴你它在等哪個參數（「Turn a knob for: hue (1/12)」），你動一個控制項，它就綁上去，然後備好下一個。全部結束後，learn 會解除。主控台的 **🎛 map knobs in order** 按鈕就是這個類別。"},{"en":"It is pure logic on top of the mapper, with no screen of its own. Learn only ever adds routes, so running the wizard twice puts a second control on a param instead of losing the first. While it runs, it owns learn mode: call `sync()` every frame so that a click on some other learn chip cannot leave it waiting for nothing.","zh":"它是建在映射上面的純邏輯，沒有自己的畫面。learn 只會新增路由，所以跑兩次精靈，是替參數多加一個控制項，不會弄丟第一個。它在跑的時候擁有 learn 模式：請每個畫面呼叫 `sync()`，這樣有人點了別的 learn 標籤，也不會讓它空等。"}],"params":[{"name":"mapper","type":"Mapper","optional":false,"en":"The [`Mapper`](#Mapper) to bind through.","zh":"要透過它綁定的 [`Mapper`](#Mapper)。"},{"name":"keys","type":"Array<String> | Function","optional":false,"en":"The param keys to bind, in order, or a function that returns them (it is called when `start()` runs).","zh":"要綁定的參數 key，依序排列。也可以是回傳它們的函式（在 `start()` 執行時呼叫）。"},{"name":"onChange","type":"Function | null","optional":true,"default":"null","en":"Called with the wizard whenever what a banner would show changes: the current param, the position, finished or not.","zh":"只要橫幅會顯示的內容改變（目前的參數、位置、有沒有結束），就以這個精靈本身呼叫。"}],"notes":[{"en":"Fields to read: `active`, `done` (the run reached the end), `index`, `total`, `order`, and `added` (`[{ key, source }]`, what was bound in this run).","zh":"可以讀的欄位：`active`、`done`（這一輪走到了最後）、`index`、`total`、`order`，以及 `added`（`[{ key, source }]`，這一輪綁了什麼）。"},{"en":"While it runs the wizard wraps the mapper's `onLearn` and puts yours back when it ends, so your own `onLearn` keeps being called.","zh":"精靈在跑的時候會包住映射的 `onLearn`，結束時再還給你，所以你自己的 `onLearn` 照樣會被呼叫。"}],"examples":[{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper, LearnWizard } from '@openav/mapping';\n\nconst signals = new Signals();\nfor (const name of ['knob/1', 'knob/2']) signals.define(name, { min: 0, max: 1 });\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 0 }, { key: 'size', min: 0, max: 1, def: 0.2 }]);\nconst mapper = new Mapper({ signals, params });\nconst wizard = new LearnWizard({ mapper, keys: () => params.schema.map((p) => p.key) });\n\nwizard.start();\nconsole.log('waiting for', wizard.current);                       // hue\nconst turn = (name) => { signals.set(name, 0); signals.set(name, 0.9); };   // a knob that moves a lot\nturn('knob/1'); console.log('waiting for', wizard.current);       // size\nturn('knob/2'); console.log(wizard.done, wizard.added);","caption":{"en":"The console prints `waiting for hue`, `waiting for size`, then `true` and the two bindings `[ { key: 'hue', source: 'knob/1' }, { key: 'size', source: 'knob/2' } ]`.","zh":"主控台印出 `waiting for hue`、`waiting for size`，然後是 `true` 和兩個綁定 `[ { key: 'hue', source: 'knob/1' }, { key: 'size', source: 'knob/2' } ]`。"},"run":"module","lang":"js"}],"related":["Mapper","Params","mountConsole"],"keywords":{"en":["learn wizard","wizard","map knobs in order","learn","bind","midi learn","skip","back","stop"],"zh":["精靈","依序綁定","learn","學習","綁定","旋鈕","略過","上一個"]},"tbd":[],"claims":[],"members":[{"name":"total","member":"getter","signature":"","doc":"","line":34},{"name":"current","member":"getter","signature":"","doc":"The param being waited for, or null.","line":36},{"name":"start","member":"method","signature":"","doc":"","line":38},{"name":"skip","member":"method","signature":"","doc":"Leave the current param unbound and wait for the next one.","line":52},{"name":"back","member":"method","signature":"","doc":"Back to the previous param (it keeps what it already has; a turned knob adds one more).","line":54},{"name":"stop","member":"method","signature":"","doc":"End the run now.","line":59},{"name":"sync","member":"method","signature":"","doc":"Call every frame: while the wizard runs it owns learn mode, so a click on some other learn chip cannot leave it waiting for nothing.","line":62}]},
{"id":"loadMapUrl","key":"loadMapUrl","name":"loadMapUrl","aliases":[],"kind":"function","package":"@openav/mapping","category":"mapping","documented":true,"signature":"async loadMapUrl(mapper, value, { base = (typeof location !== 'undefined' ? location.href : undefined), fetchImpl = (typeof fetch !== 'undefined' ? fetch : null), timeoutMs = 8000 } = {})","import":"import { loadMapUrl } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":126,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L126"},"summary":{"en":"Fetches a map file from an address (what `?map=` calls) and puts it on the mapper; it never throws and never blocks the show.","zh":"從一個網址抓映射檔（`?map=` 呼叫的就是它）並放到映射上，絕不丟出例外，也不會卡住演出。"},"description":[{"en":"Fetches the address with `credentials: 'omit'`, and `importMap()`s the text. Any failure (refused address, no `fetch`, the server answered 404, offline, a site that blocks cross-origin reads, a timeout, a file that is not a map) lands in the report and in `mapper.report`, where the console shows it, and the routes stay as they were. It is async, so `createShow()` does not wait for it and the show starts regardless.","zh":"用 `credentials: 'omit'` 去抓這個網址，再把文字交給 `importMap()`。任何失敗（網址被拒絕、沒有 `fetch`、伺服器回 404、離線、擋住跨來源讀取的網站、逾時、檔案不是映射檔）都會寫進報告和 `mapper.report`，主控台會顯示出來，路由維持原樣。它是非同步的，所以 `createShow()` 不會等它，演出照常開始。"},{"en":"A map on another site must be served with CORS (`Access-Control-Allow-Origin: *`); raw GitHub files and gists are. In a show you rarely call it: open the page with `?map=<url>`, or pass `map: 'maps/a.json'` to `createShow()`.","zh":"別的網站上的映射檔必須用 CORS 提供（`Access-Control-Allow-Origin: *`），raw GitHub 檔案和 gist 都可以。做演出的時候很少直接呼叫它：開頁面時加上 `?map=<url>`，或在 `createShow()` 傳 `map: 'maps/a.json'` 就行了。"}],"params":[{"name":"mapper","type":"Mapper","en":"The [`Mapper`](#Mapper) to import into.","zh":"要匯入的 [`Mapper`](#Mapper)。"},{"name":"value","type":"String","en":"The address of the map file, checked by [`parseMapUrl()`](#parseMapUrl) first.","zh":"映射檔的網址，會先經過 [`parseMapUrl()`](#parseMapUrl) 檢查。"},{"name":"base","type":"String","optional":true,"default":"location.href","en":"The page address that relative addresses resolve against.","zh":"相對網址據以解析的頁面網址。"},{"name":"fetchImpl","type":"Function","optional":true,"default":"fetch","en":"The function that does the request. Replace it to test without a network.","zh":"真正發出請求的函式。換掉它，就能在沒有網路的情況下測試。"},{"name":"timeoutMs","type":"Number","optional":true,"default":"8000","en":"Give up after this many milliseconds.","zh":"超過這麼多毫秒就放棄。"}],"returns":{"type":"Promise<Object>","en":"The report: what `mapper.importMap()` returned (`source: 'url'`, `address`, `added`, `skipped` …), or `{ ok: false, source: 'url', address, error }`. The same report is kept in `mapper.report`.","zh":"報告：`mapper.importMap()` 的回傳值（`source: 'url'`、`address`、`added`、`skipped` ……），或 `{ ok: false, source: 'url', address, error }`。同一份報告也存在 `mapper.report`。"},"notes":[],"examples":[{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper, loadMapUrl } from '@openav/mapping';\n\nconst signals = new Signals();\nsignals.define('audio/rms');\nconst params = new Params([{ key: 'bloom', min: 0, max: 100, def: 0 }]);\nconst mapper = new Mapper({ signals, params, profile: 'garden' });\n\nconst file = { format: 'openav.map', version: 1, world: 'garden', routes: [{ signal: 'audio/rms', param: 'bloom' }] };\nconst fetchImpl = async () => ({ ok: true, status: 200, text: async () => JSON.stringify(file) });   // no network needed\nconst report = await loadMapUrl(mapper, 'maps/stage-left.json', { base: 'https://openaudiovisual.com/x/', fetchImpl });\nconsole.log(report.ok, report.address, report.added);","caption":{"en":"With a stand-in for the network, the console prints `true https://openaudiovisual.com/x/maps/stage-left.json 1`: the route came in.","zh":"用一個代替網路的函式，主控台印出 `true https://openaudiovisual.com/x/maps/stage-left.json 1`：路由進來了。"},"run":"module","lang":"js"},{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper, loadMapUrl } from '@openav/mapping';\n\nconst mapper = new Mapper({ signals: new Signals(), params: new Params([]) });\nconst report = await loadMapUrl(mapper, 'http://example.com/map.json', { base: 'https://openaudiovisual.com/' });\nconsole.log(report.ok, report.error);        // false 'a map from another site must be served over https'\nconsole.log(mapper.report === report);       // true: the console would show it too","caption":{"en":"A refused address is a report, not an exception: the console prints `false a map from another site must be served over https`, then `true`.","zh":"被拒絕的網址是一份報告，不是例外：主控台印出 `false a map from another site must be served over https`，然後是 `true`。"},"run":"module","lang":"js"}],"related":["parseMapUrl","parseMap","Mapper","createShow"],"keywords":{"en":["load map","?map=","fetch map","map url","import from url","share mapping","load mapping file"],"zh":["載入映射","?map=","抓映射檔","從網址匯入","分享映射","映射網址"]},"tbd":[],"claims":[]},
{"id":"MAP_FORMAT","key":"MAP_FORMAT","name":"MAP_FORMAT","aliases":[],"kind":"constant","package":"@openav/mapping","category":"mapping","documented":true,"signature":"MAP_FORMAT = \"openav.map\"","import":"import { MAP_FORMAT } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":42,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L42"},"summary":{"en":"The text `\"openav.map\"`: the value of `format` in every map file. A file with another format is refused.","zh":"文字 `\"openav.map\"`：每個映射檔裡 `format` 的值。格式不同的檔案會被拒絕。"},"description":[{"en":"A map file is JSON with `\"format\": \"openav.map\"`, a `version`, a `world` name and a list of `routes`. [`parseMap()`](#parseMap) refuses anything whose `format` is not this string, so you cannot import some other tool's JSON by accident. The one exception is a bare array (what `mapper.toJSON()` returns), which is read as version 0.","zh":"映射檔是 JSON，裡面有 `\"format\": \"openav.map\"`、`version`、`world` 名稱和一份 `routes` 清單。[`parseMap()`](#parseMap) 會拒絕 `format` 不是這個字串的東西，所以不會誤把別的工具的 JSON 匯進來。唯一的例外是單純的陣列（`mapper.toJSON()` 回傳的那種），會被當成版本 0 來讀。"}],"params":[],"notes":[],"examples":[],"related":["MAP_VERSION","MAP_MAX_BYTES","parseMap","serializeMap"],"keywords":{"en":["map format","openav.map","map file","format","json"],"zh":["映射檔","格式","openav.map","json"]},"tbd":[],"claims":[]},
{"id":"MAP_MAX_BYTES","key":"MAP_MAX_BYTES","name":"MAP_MAX_BYTES","aliases":[],"kind":"constant","package":"@openav/mapping","category":"mapping","documented":true,"signature":"MAP_MAX_BYTES = 262144","import":"import { MAP_MAX_BYTES } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":44,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L44"},"summary":{"en":"The longest map text `parseMap()` will read: 262144, which is 256 KB. Longer text is refused without being parsed.","zh":"`parseMap()` 願意讀的映射文字長度上限：262144，也就是 256 KB。更長的文字不會解析，直接拒絕。"},"description":[{"en":"The limit is compared with the length of the text (characters), before any parsing, so a huge file cannot freeze the page. A map with more than 500 routes is refused as well, whatever its size.","zh":"這個上限是拿文字的長度（字元數）來比，在任何解析之前就比，所以很大的檔案不會讓頁面卡住。路由超過 500 條的映射檔也會被拒絕，不管大小。"}],"params":[],"notes":[],"examples":[],"related":["parseMap","MAP_FORMAT","MAP_VERSION"],"keywords":{"en":["map size","limit","256 KB","max routes","map file"],"zh":["映射檔大小","上限","256 KB","路由數量"]},"tbd":[],"claims":[]},
{"id":"MAP_VERSION","key":"MAP_VERSION","name":"MAP_VERSION","aliases":[],"kind":"constant","package":"@openav/mapping","category":"mapping","documented":true,"signature":"MAP_VERSION = 1","import":"import { MAP_VERSION } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":43,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L43"},"summary":{"en":"The newest map file version this build reads and writes: 1. A file with a higher version is refused with a message.","zh":"這個版本能讀也能寫的映射檔最新版本：1。版本更高的檔案會被拒絕並給出訊息。"},"description":[{"en":"[`serializeMap()`](#serializeMap) writes this number and [`parseMap()`](#parseMap) accepts any version from 1 up to it. A newer file gets the message \"this build of OAV reads up to version 1. Update OAV or re-export it.\" A file with no valid `version` is refused too. Only a bare array of routes counts as version 0.","zh":"[`serializeMap()`](#serializeMap) 寫入這個數字，[`parseMap()`](#parseMap) 接受從 1 到它之間的任何版本。更新的檔案會得到這句訊息：`this build of OAV reads up to version 1. Update OAV or re-export it.` 沒有合法 `version` 的檔案也會被拒絕。只有單純的路由陣列會被當成版本 0。"}],"params":[],"notes":[],"examples":[],"related":["MAP_FORMAT","MAP_MAX_BYTES","parseMap"],"keywords":{"en":["map version","version","map file","compatibility"],"zh":["映射檔版本","版本","相容"]},"tbd":[],"claims":[]},
{"id":"Mapper","key":"Mapper","name":"Mapper","aliases":[],"kind":"class","package":"@openav/mapping","category":"mapping","documented":true,"signature":"new Mapper({ signals, params, profile = 'default', onChange = null, onLearn = null })","import":"import { Mapper } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":146,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L146"},"summary":{"en":"Connects signals to params: it holds a list of routes (a window, a curve, smoothing) and writes the matching param whenever a signal arrives.","zh":"把訊號接到參數：它手上有一份路由清單（輸入窗、曲線、平滑），每當訊號到來，就寫入對應的參數。"},"description":[{"en":"The mapper is the only component that connects signals to params, and everything it knows is a list of **routes**, plain objects. `mapper.addRoute({ source: 'midi/cc/74', target: 'bloom' })` makes the param `bloom` follow the signal `midi/cc/74`. One signal may drive several params, one param may listen to several signals (the last writer wins), and learn adds routes, it never replaces them. In a show, [`createShow()`](#createShow) builds one, loads the saved routes (or adds the ones you declared in `routes`) and returns it as `show.mapper`.","zh":"映射是唯一把訊號接到參數的元件，它知道的一切就是一份**路由**清單，每條路由是個普通物件。`mapper.addRoute({ source: 'midi/cc/74', target: 'bloom' })` 讓參數 `bloom` 跟著訊號 `midi/cc/74` 走。一個訊號可以驅動好幾個參數，一個參數也可以聽好幾個訊號（最後寫入的算數），learn 只會新增路由，不會取代。在演出裡，[`createShow()`](#createShow) 會建好一個，載入存起來的路由（或加入你在 `routes` 宣告的），用 `show.mapper` 回傳。"},{"en":"A route needs only `source` and `target`; `addRoute()` fills in the rest and returns the stored route:\n\n```text\nsource, target      the signal name and the param key (required)\ninMin, inMax        0, 1   the part of the signal that counts; outside it is clamped\noutMin, outMax      the param's min and max AT THE MOMENT THE ROUTE IS ADDED (0 and 1 if the param does not exist yet)\ncurve               'linear' | 'exp' (x squared) | 'log' (square root) | 'smooth' (smoothstep); an unknown name is linear\ninvert              false   flip the value (1 - x) before the curve\nsmooth              0       glide time in seconds; 0 = instant\nenabled             absent = on; false mutes the route\nid                  assigned; used by removeRoute()\n```","zh":"一條路由只需要 `source` 和 `target`，其餘由 `addRoute()` 補上，並回傳存下來的路由：\n\n```text\nsource, target      the signal name and the param key (required)\ninMin, inMax        0, 1   the part of the signal that counts; outside it is clamped\noutMin, outMax      the param's min and max AT THE MOMENT THE ROUTE IS ADDED (0 and 1 if the param does not exist yet)\ncurve               'linear' | 'exp' (x squared) | 'log' (square root) | 'smooth' (smoothstep); an unknown name is linear\ninvert              false   flip the value (1 - x) before the curve\nsmooth              0       glide time in seconds; 0 = instant\nenabled             absent = on; false mutes the route\nid                  assigned; used by removeRoute()\n```"},{"en":"When a signal arrives, each enabled route with that `source` does five things in order: take the **window** (`(value - inMin) / (inMax - inMin)`, clamped to 0..1), **invert** if asked, apply the **curve**, **scale** into `outMin`..`outMax`, and **write**. With `smooth: 0` the param is overridden at once. With `smooth > 0` the result becomes a target that `update(dt)` glides toward: after `smooth` seconds the param has covered about 63% of the way. If the target param is a pulse param, the route fires it instead: on every event of a pulse signal, or when a continuous source rises past 0.5.","zh":"訊號到來的時候，每一條啟用中、`source` 相符的路由會依序做五件事：取**輸入窗**（`(value - inMin) / (inMax - inMin)`，限制在 0..1）、需要的話**反轉**、套用**曲線**、**縮放**到 `outMin`..`outMax`、然後**寫入**。`smooth: 0` 時參數立刻被覆寫。`smooth > 0` 時，結果變成一個目標值，由 `update(dt)` 慢慢滑過去：經過 `smooth` 秒，參數走完大約 63% 的距離。如果目標是脈衝參數，路由改成觸發它：脈衝訊號每來一次就觸發，連續訊號則是往上越過 0.5 的那一刻觸發。"},{"en":"**Learn** is generalised MIDI-learn: `mapper.learn('bloom')`, then move something. The first signal that moves by at least 5% of its declared range (a knob, a hand, a phone tilt), or the first pulse that fires (a key, a pad), becomes the source of a new route with the default settings. Calling `learn('bloom')` again cancels it. Because any pulse binds at once, stop the drum machine before you learn.","zh":"**Learn** 是通用版的 MIDI-learn：呼叫 `mapper.learn('bloom')`，然後動一個東西。第一個移動超過其宣告範圍 5% 的訊號（旋鈕、手、手機傾斜），或第一個觸發的脈衝（按鍵、打擊墊），就會成為一條使用預設值的新路由的來源。再呼叫一次 `learn('bloom')` 就取消。因為任何脈衝都會立刻被綁定，learn 之前請先把鼓機停掉。"}],"params":[{"name":"signals","type":"Signals","optional":false,"en":"The [`Signals`](#Signals) registry to listen to. The mapper subscribes to every signal at once, so a route works the moment it is added.","zh":"要監聽的 [`Signals`](#Signals) 登錄表。映射一建立就訂閱所有訊號，所以路由一加進去就生效。"},{"name":"params","type":"Params","optional":false,"en":"The [`Params`](#Params) to write into. A route writes its target as an override.","zh":"要寫入的 [`Params`](#Params)。路由會把目標參數寫成覆寫值。"},{"name":"profile","type":"String","optional":true,"default":"'default'","en":"The name this mapper saves its routes under in `localStorage` (`openav.map.<profile>`), and the `world` name in an exported map file. `createShow()` uses the world's name.","zh":"這個映射在 `localStorage` 儲存路由時用的名字（`openav.map.<profile>`），也是匯出的映射檔裡的 `world` 名稱。`createShow()` 用的是世界的名字。"},{"name":"onChange","type":"Function | null","optional":true,"default":"null","en":"Called with no arguments whenever the routes change or the import report changes (for a UI to redraw).","zh":"路由改變或匯入報告改變的時候呼叫，不帶參數（讓介面重畫用）。"},{"name":"onLearn","type":"Function | null","optional":true,"default":"null","en":"Called as `onLearn(route, signalName)` when learn mode binds a signal.","zh":"learn 模式綁定了一個訊號的時候，以 `onLearn(route, signalName)` 呼叫。"}],"needs":{"en":"Create the signals and the params first, then the mapper. A smoothed route (`smooth > 0`) writes its param only inside `update(dt)`, so call `mapper.update(dt)` every frame (`createShow()` does).","zh":"先建好訊號和參數，再建映射。平滑的路由（`smooth > 0`）只會在 `update(dt)` 裡寫入參數，所以每個畫面都要呼叫 `mapper.update(dt)`（`createShow()` 會做）。"},"notes":[{"en":"A route writes its param as an **override**, and overrides are sticky: as soon as a mapped signal moves, the timeline stops driving that param until `params.clearOverride(key)`.","zh":"路由把參數寫成**覆寫值**，而覆寫是黏住的：映射的訊號一動，時間軸就不再驅動那個參數，直到你呼叫 `params.clearOverride(key)`。"},{"en":"A smoothed route keeps writing its last target every frame once its signal has arrived, so its param stays overridden even after `clearOverride` and even when the route is muted with `enabled = false`. Remove the route to give the param back to the timeline.","zh":"平滑的路由只要訊號來過一次，之後每個畫面都會繼續寫入它最後的目標值，所以就算 `clearOverride`、甚至用 `enabled = false` 靜音，參數仍然是被覆寫的。要把參數還給時間軸，請移除那條路由。"},{"en":"With `createShow()`, routes are added before the sound engine registers its `sound/*` params, so a route to `sound/cutoff` needs an explicit `outMin: 100, outMax: 8000`.","zh":"在 `createShow()` 裡，路由加進去的時間比聲音引擎登記 `sound/*` 參數還早，所以接到 `sound/cutoff` 的路由要明確寫上 `outMin: 100, outMax: 8000`。"},{"en":"More, with the saved-profile rules: [Mapping](https://openaudiovisual.com/docs/#mapping).","zh":"更多內容與存檔規則：[映射](https://openaudiovisual.com/zh/docs/#mapping)。"}],"examples":[{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'bloom', min: 0, max: 100, def: 10 }]);\nconst mapper = new Mapper({ signals, params });\n\nmapper.addRoute({ source: 'midi/cc/74', target: 'bloom', curve: 'exp' });\nsignals.set('midi/cc/74', 0.5);\nconsole.log(params.resolve().bloom);   // 25: 0.5 through the exp curve is 0.25 of the range 0..100","caption":{"en":"The console prints `25`: the knob is halfway, the `exp` curve squares it to 0.25, and that is a quarter of the way along 0..100.","zh":"主控台印出 `25`：旋鈕在一半，`exp` 曲線把它平方成 0.25，也就是 0..100 的四分之一。"},"run":"module","lang":"js"},{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 0 }]);\nconst mapper = new Mapper({ signals, params });\n\nmapper.addRoute({ source: 'audio/rms', target: 'hue', inMax: 0.3, smooth: 0.5 });\nsignals.set('audio/rms', 0.3);                       // a voice at 0.3 is the whole range\nconsole.log(params.resolve().hue);                   // 0: nothing moved yet\nmapper.update(0.5);                                  // half a second = one time constant\nconsole.log(params.resolve().hue.toFixed(1));        // 227.6: about 63% of 360","caption":{"en":"A smoothed route only moves inside `update(dt)`: the console prints `0`, then `227.6` after half a second (63% of the way to 360).","zh":"平滑的路由只會在 `update(dt)` 裡動：主控台先印出 `0`，半秒之後印出 `227.6`（走了 360 的 63%）。"},"run":"module","lang":"js"},{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'bloom', min: 0, max: 100, def: 10 }]);\nconst mapper = new Mapper({ signals, params, onLearn: (route, name) => console.log('bound', name) });\n\nsignals.define('knob/a');\nsignals.set('knob/a', 0.5);\nmapper.learn('bloom');                  // arm learn for the param\nsignals.set('knob/a', 0.51);            // moved 1%: not enough\nsignals.set('knob/a', 0.8);             // moved 30%: bound\nconsole.log(mapper.routesFor('bloom').map((r) => r.source), mapper.learnTarget);","caption":{"en":"Learn waits for a real movement: the console prints `bound knob/a`, then `[ 'knob/a' ] null` (learn disarms itself once it has bound a signal).","zh":"learn 會等到真正的移動：主控台印出 `bound knob/a`，然後是 `[ 'knob/a' ] null`（綁定一個訊號之後，learn 會自己解除）。"},"run":"module","lang":"js"},{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\n\nfunction rig(profile) {\n  const signals = new Signals();\n  signals.define('audio/rms');\n  const params = new Params([{ key: 'bloom', min: 0, max: 100, def: 0 }]);\n  return { signals, params, mapper: new Mapper({ signals, params, profile }) };\n}\nconst a = rig('garden');\na.mapper.addRoute({ source: 'audio/rms', target: 'bloom', curve: 'log', inMax: 0.3 });\nconst file = a.mapper.exportMap();                    // the portable file\nconst report = rig('garden').mapper.importMap(file);  // on another machine or in another page\nconsole.log(file.format, file.version, report.ok, report.added, report.skipped);","caption":{"en":"A wiring travels as a file: the console prints `openav.map 1 true 1 []`.","zh":"一份接線可以變成檔案帶著走：主控台印出 `openav.map 1 true 1 []`。"},"run":"module","lang":"js"}],"related":["Signals","Params","LearnWizard","parseMap","serializeMap","loadMapUrl","createShow","approach"],"keywords":{"en":["mapper","mapping","route","routes","learn","midi learn","curve","smooth","invert","window","inMin","outMin","source target","bind","connect signal to param"],"zh":["映射","路由","接線","綁定","曲線","平滑","反轉","輸入窗","學習","訊號接參數","覆寫"]},"tbd":[],"claims":[],"members":[{"name":"addRoute","member":"method","signature":"r","doc":"","line":170},{"name":"removeRoute","member":"method","signature":"id","doc":"","line":182},{"name":"routesFor","member":"method","signature":"target","doc":"","line":188},{"name":"learn","member":"method","signature":"target","doc":"Toggle learn mode for a param.","line":191},{"name":"update","member":"method","signature":"dt","doc":"Call per frame — advances smoothed routes.","line":256},{"name":"toJSON","member":"method","signature":"","doc":"","line":267},{"name":"fromJSON","member":"method","signature":"arr","doc":"","line":268},{"name":"exportMap","member":"method","signature":"{ world = this.profile, now } = {}","doc":"The portable file for the current routes ({ format, version, world, routes }).","line":274},{"name":"importMap","member":"method","signature":"input, { mode = 'replace', source = 'file', address = '', name = '' } = {}","doc":"Put a map file on this mapper.","line":282},{"name":"clear","member":"method","signature":"","doc":"Remove every route (and forget it: the saved profile is emptied too).","line":312},{"name":"setReport","member":"method","signature":"rep","doc":"Remember what the last import said and tell the UI (a failure shows up here, never as an exception).","line":315},{"name":"save","member":"method","signature":"","doc":"","line":317},{"name":"load","member":"method","signature":"","doc":"","line":318},{"name":"dispose","member":"method","signature":"","doc":"","line":329}]},
{"id":"parseMap","key":"parseMap","name":"parseMap","aliases":[],"kind":"function","package":"@openav/mapping","category":"mapping","documented":true,"signature":"parseMap(input)","import":"import { parseMap } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":73,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L73"},"summary":{"en":"Reads and cleans a map file (JSON text or a parsed value) without touching any mapper; it never throws.","zh":"讀取並整理一份映射檔（JSON 文字或已解析的值），不會動到任何映射，也絕不丟出例外。"},"description":[{"en":"Use it to validate a file before you import it, or to show what is inside one. It refuses, with a plain-language `error`, text that is not JSON, text longer than [`MAP_MAX_BYTES`](#MAP_MAX_BYTES), an object whose `format` is not [`MAP_FORMAT`](#MAP_FORMAT), a missing or newer `version`, no `routes` list, and more than 500 routes.","zh":"用它在匯入之前檢查檔案，或看看檔案裡有什麼。下列情形它會用白話的 `error` 拒絕：文字不是 JSON、文字比 [`MAP_MAX_BYTES`](#MAP_MAX_BYTES) 長、物件的 `format` 不是 [`MAP_FORMAT`](#MAP_FORMAT)、`version` 缺少或比較新、沒有 `routes` 清單，以及超過 500 條路由。"},{"en":"What it accepts is cleaned: a route without a string `signal` and `param` is dropped (and counted in `dropped`), unknown keys disappear, numbers must be finite, an unknown `curve` becomes `'linear'`, `smooth` is held to 0..60 seconds, and only `invert: true` inverts. A route is on unless it says `enabled: false`.","zh":"接受的內容都會被整理過：沒有字串 `signal` 和 `param` 的路由會被丟掉（並算進 `dropped`），不認得的欄位會消失，數字必須有限，不認得的 `curve` 變成 `'linear'`，`smooth` 限制在 0 到 60 秒，只有 `invert: true` 才會反轉。路由除非寫了 `enabled: false`，否則是開著的。"}],"params":[{"name":"input","type":"String | Object | Array","en":"The JSON text of a map file, an already parsed map object, or a bare array of routes (version 0).","zh":"映射檔的 JSON 文字、已經解析好的映射物件，或單純的路由陣列（版本 0）。"}],"returns":{"type":"Object","en":"`{ ok: true, version, world, routes, dropped }` or `{ ok: false, error }`. Each route is `{ signal, param, curve, inMin, inMax, outMin?, outMax?, smooth, invert, enabled }`; `dropped` counts the entries that were not routes.","zh":"`{ ok: true, version, world, routes, dropped }` 或 `{ ok: false, error }`。每條路由是 `{ signal, param, curve, inMin, inMax, outMin?, outMax?, smooth, invert, enabled }`。`dropped` 是不算路由而被丟掉的項目數。"},"notes":[],"examples":[{"code":"import { parseMap } from '@openav/mapping';\n\nconst good = parseMap('{\"format\":\"openav.map\",\"version\":1,\"routes\":[{\"signal\":\"audio/rms\",\"param\":\"bloom\",\"curve\":\"wobble\"},{\"signal\":7}]}');\nconsole.log(good.ok, good.routes[0].curve, good.dropped);   // true linear 1\nconsole.log(parseMap('not json'));                          // { ok: false, error: 'not valid JSON' }\nconsole.log(parseMap('{\"format\":\"other\",\"version\":1}'));   // refused: not an OAV map","caption":{"en":"The console prints `true linear 1` (the unknown curve became linear, the broken route was dropped), then the two refusals with their reasons.","zh":"主控台印出 `true linear 1`（不認得的曲線變成 linear，壞掉的路由被丟掉），然後是兩個被拒絕的情形與原因。"},"run":"module","lang":"js"}],"related":["serializeMap","Mapper","MAP_FORMAT","MAP_VERSION","MAP_MAX_BYTES","loadMapUrl"],"keywords":{"en":["parse map","read map","validate map","import map","map file","json","check map"],"zh":["讀取映射","解析映射","檢查映射","匯入映射","映射檔","驗證"]},"tbd":[],"claims":[]},
{"id":"parseMapUrl","key":"parseMapUrl","name":"parseMapUrl","aliases":[],"kind":"function","package":"@openav/mapping","category":"mapping","documented":true,"signature":"parseMapUrl(value, base)","import":"import { parseMapUrl } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":109,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L109"},"summary":{"en":"Decides whether a `?map=` address may be fetched: same origin, or an absolute https URL, and nothing else.","zh":"判斷一個 `?map=` 的網址能不能去抓：同源的，或絕對的 https 網址，其他一律不行。"},"description":[{"en":"A map file named in the page address is somebody else's data, so the rules are strict. Accepted: an address on the same origin as `base` (any scheme, so plain `http://localhost` works for a page served from there), or an absolute `https:` address. Refused: an empty address, one longer than 2048 characters, one that is not a URL, one carrying a user name or password, `http:` to another host, and `data:`, `javascript:`, `file:`, `blob:` and every other scheme. [`loadMapUrl()`](#loadMapUrl) runs this check before it fetches anything.","zh":"網址列上指定的映射檔是別人的資料，所以規則很嚴。接受：跟 `base` 同源的網址（任何通訊協定，所以從 `http://localhost` 提供的頁面用 `http://localhost` 也行），或絕對的 `https:` 網址。拒絕：空網址、超過 2048 個字元、不是網址、帶有帳號或密碼、指向別的主機的 `http:`，以及 `data:`、`javascript:`、`file:`、`blob:` 和其他所有通訊協定。[`loadMapUrl()`](#loadMapUrl) 在抓任何東西之前都會先跑這個檢查。"}],"params":[{"name":"value","type":"String","en":"The address as typed: absolute, or relative to `base`.","zh":"輸入的網址：絕對網址，或相對於 `base` 的網址。"},{"name":"base","type":"String","en":"The page address that relative addresses resolve against, and that \"same origin\" is judged by. Usually `location.href`.","zh":"相對網址據以解析、「同源」據以判斷的頁面網址。通常是 `location.href`。"}],"returns":{"type":"Object","en":"`{ ok: true, url }` with the resolved address, or `{ ok: false, error }`.","zh":"`{ ok: true, url }`（解析後的網址），或 `{ ok: false, error }`。"},"notes":[],"examples":[{"code":"import { parseMapUrl } from '@openav/mapping';\n\nconst base = 'https://openaudiovisual.com/examples/01-hello-particles/';\nconsole.log(parseMapUrl('maps/stage-left.json', base));            // ok: same origin, resolved\nconsole.log(parseMapUrl('https://example.com/my-map.json', base));  // ok: https\nconsole.log(parseMapUrl('http://example.com/my-map.json', base));   // refused: http to another site\nconsole.log(parseMapUrl('javascript:alert(1)', base));              // refused: not http or https","caption":{"en":"The console prints two accepted results (the first with the full resolved address `https://openaudiovisual.com/examples/01-hello-particles/maps/stage-left.json`) and two refusals with their reasons.","zh":"主控台印出兩個接受的結果（第一個是完整解析後的網址 `https://openaudiovisual.com/examples/01-hello-particles/maps/stage-left.json`）和兩個被拒絕的情形與原因。"},"run":"module","lang":"js"}],"related":["loadMapUrl","parseMap","Mapper"],"keywords":{"en":["map url","?map=","url check","https","same origin","cors","load map from address"],"zh":["映射網址","?map=","網址檢查","https","同源","從網址載入"]},"tbd":[],"claims":[]},
{"id":"serializeMap","key":"serializeMap","name":"serializeMap","aliases":[],"kind":"function","package":"@openav/mapping","category":"mapping","documented":true,"signature":"serializeMap(routes, { world = 'default', now = new Date() } = {})","import":"import { serializeMap } from '@openav/mapping';","source":{"file":"packages/mapping/index.js","line":54,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mapping/index.js#L54"},"summary":{"en":"Builds the portable map file object from a list of routes: `{ format, version, world, exported, routes }`.","zh":"把一份路由清單做成可攜的映射檔物件：`{ format, version, world, exported, routes }`。"},"description":[{"en":"Routes in a map file are named by **signal** and **param**, never by id, so the file can travel between worlds. `mapper.exportMap()` calls this for you; use `serializeMap()` directly when you build routes by hand or want a fixed date. Missing `inMin`/`inMax` become 0 and 1, missing `curve` is `'linear'`, and the output range is 0..1 unless the route has `outMin`/`outMax`.","zh":"映射檔裡的路由是用**訊號**和**參數**的名字來指，不用 id，所以檔案可以在不同世界之間搬運。`mapper.exportMap()` 會替你呼叫它。自己手寫路由、或想要固定日期時，才需要直接用 `serializeMap()`。沒寫的 `inMin`/`inMax` 會變成 0 和 1，沒寫的 `curve` 是 `'linear'`，沒有 `outMin`/`outMax` 的話輸出範圍是 0..1。"}],"params":[{"name":"routes","type":"Array","en":"Routes in the mapper's own shape (what `mapper.toJSON()` returns: `source`, `target`, `inMin`, `outMax` …).","zh":"映射本身格式的路由（`mapper.toJSON()` 回傳的那種：`source`、`target`、`inMin`、`outMax` ……）。"},{"name":"world","type":"String","optional":true,"default":"'default'","en":"The world name written into the file. Importing into another world drops the output ranges.","zh":"寫進檔案的世界名稱。匯入到別的世界時，輸出範圍會被丟掉。"},{"name":"now","type":"Date | String","optional":true,"default":"new Date()","en":"The time written as `exported` (an ISO string). Pass a fixed date to get the same file twice.","zh":"寫成 `exported` 的時間（ISO 字串）。傳一個固定的日期，就能得到兩次相同的檔案。"}],"returns":{"type":"Object","en":"`{ format: 'openav.map', version: 1, world, exported, routes: [{ signal, param, curve, range: { in: [inMin, inMax], out: [outMin, outMax] }, smooth, invert }] }`. A muted route also carries `enabled: false`.","zh":"`{ format: 'openav.map', version: 1, world, exported, routes: [{ signal, param, curve, range: { in: [inMin, inMax], out: [outMin, outMax] }, smooth, invert }] }`。被靜音的路由會多一個 `enabled: false`。"},"notes":[],"examples":[{"code":"import { serializeMap } from '@openav/mapping';\n\nconst file = serializeMap(\n  [{ source: 'midi/cc/74', target: 'bloom', curve: 'exp', outMax: 100 }],\n  { world: 'garden', now: new Date('2026-10-19T00:00:00Z') },\n);\nconsole.log(JSON.stringify(file));","caption":{"en":"The console prints the whole file as one line of JSON, starting `{\"format\":\"openav.map\",\"version\":1,\"world\":\"garden\",\"exported\":\"2026-10-19T00:00:00.000Z\",\"routes\":[{\"signal\":\"midi/cc/74\",\"param\":\"bloom\",\"curve\":\"exp\",\"range\":{\"in\":[0,1],\"out\":[0,100]} …`.","zh":"主控台把整個檔案印成一行 JSON，開頭是 `{\"format\":\"openav.map\",\"version\":1,\"world\":\"garden\",\"exported\":\"2026-10-19T00:00:00.000Z\",\"routes\":[{\"signal\":\"midi/cc/74\",\"param\":\"bloom\",\"curve\":\"exp\",\"range\":{\"in\":[0,1],\"out\":[0,100]} …`。"},"run":"module","lang":"js"}],"related":["parseMap","Mapper","MAP_FORMAT","MAP_VERSION"],"keywords":{"en":["serialize map","export map","save map","map file","json","export routes"],"zh":["匯出映射","儲存映射","映射檔","序列化","json","匯出路由"]},"tbd":[],"claims":[]},
{"id":"Bus","key":"Bus","name":"Bus","aliases":[],"kind":"class","package":"@openav/core","category":"core","documented":true,"signature":"new Bus()","import":"import { Bus } from '@openav/core';","source":{"file":"packages/core/src/bus.js","line":2,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/core/src/bus.js#L2"},"summary":{"en":"A minimal event emitter: `on(event, cb)`, `emit(event, payload)`, `off(event, cb)`. No wildcards, no async.","zh":"最陽春的事件發射器：`on(event, cb)`、`emit(event, payload)`、`off(event, cb)`。沒有萬用字元，也不是非同步。"},"description":[{"en":"`new Bus()` is a tiny publish and subscribe helper for your own code: subscribe to an event name with `on()`, fire it with `emit()`, and every callback gets the one payload you pass. Callbacks run in the order they subscribed, synchronously, inside `emit()`.","zh":"`new Bus()` 是給你自己的程式用的小型發布與訂閱工具：用 `on()` 訂閱一個事件名稱，用 `emit()` 觸發，每個函式都會收到你傳的那一個內容。函式依訂閱順序同步執行，都在 `emit()` 裡面跑完。"},{"en":"It is not the thing shows talk through. Inputs and worlds talk through [`Signals`](#Signals) (named values with a range) and [`Params`](#Params); nothing else in the framework uses a `Bus`, and `createShow()` does not create one.","zh":"演出裡各部分溝通靠的不是它。輸入和世界是透過 [`Signals`](#Signals)（有範圍的具名值）和 [`Params`](#Params) 溝通，框架裡沒有別的東西用到 `Bus`，`createShow()` 也不會建立一個。"}],"params":[],"notes":[],"examples":[{"code":"import { Bus } from '@openav/core';\n\nconst bus = new Bus();\nconst off = bus.on('hit', (strength) => console.log('hit', strength));\nbus.emit('hit', 0.8);   // hit 0.8\noff();                  // unsubscribe\nbus.emit('hit', 0.9);   // nothing: nobody is listening","caption":{"en":"The console prints `hit 0.8` once; after unsubscribing, the second `emit` is silent.","zh":"主控台只印出一次 `hit 0.8`，取消訂閱之後，第二次 `emit` 沒有任何反應。"},"run":"module","lang":"js"}],"related":["Signals","Params"],"keywords":{"en":["bus","event emitter","events","on","emit","off","publish","subscribe","pubsub","listener"],"zh":["事件","發射器","匯流排","訂閱","發布","監聽","回呼"]},"tbd":[],"claims":[],"members":[{"name":"on","member":"method","signature":"event, cb","doc":"Subscribe.","line":6},{"name":"off","member":"method","signature":"event, cb","doc":"","line":12},{"name":"emit","member":"method","signature":"event, payload","doc":"","line":14}]},
{"id":"Loop","key":"Loop","name":"Loop","aliases":[],"kind":"class","package":"@openav/core","category":"core","documented":true,"signature":"new Loop(onFrame, { maxDt = 0.1 } = {})","import":"import { Loop } from '@openav/core';","source":{"file":"packages/core/src/loop.js","line":7,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/core/src/loop.js#L7"},"summary":{"en":"The frame clock: calls your function every animation frame with the seconds since the last one, and keeps ticking when the tab is hidden.","zh":"畫面的時鐘：每個動畫畫面呼叫你的函式一次，並告訴你距離上一次過了幾秒，分頁被藏起來時它也繼續跳。"},"description":[{"en":"`new Loop((dt) => { … }).start()` runs your function once per `requestAnimationFrame`, with `dt` in seconds. It is the one clock a show has: the timeline advances, mapped signals settle, the world draws and the backstage feed is sent from inside it, in the order [the architecture page](https://openaudiovisual.com/docs/#architecture-the-frame-loop) lists. [`createShow()`](#createShow) builds and starts one for you and returns it as `show.loop`.","zh":"`new Loop((dt) => { … }).start()` 每個 `requestAnimationFrame` 呼叫你的函式一次，`dt` 的單位是秒。這是一場演出唯一的時鐘：時間軸往前走、映射的訊號安頓下來、世界繪圖、後台資料送出，都在它裡面做，順序見[架構頁](https://openaudiovisual.com/zh/docs/#architecture-the-frame-loop)。[`createShow()`](#createShow) 會替你建好並啟動一個，用 `show.loop` 回傳。"},{"en":"Browsers freeze `requestAnimationFrame` in a hidden tab, but a performance must not stop because someone switched windows. So while the loop runs, a Web Worker beats every 33 ms; whenever no animation frame has arrived for 50 ms, the loop steps itself. A hidden tab therefore ticks at roughly 15 frames a second with a normal-sized `dt`, and show time stays real time. Where the page forbids workers (a strict content security policy) that fallback is simply absent.","zh":"瀏覽器會在分頁被藏起來的時候凍結 `requestAnimationFrame`，但演出不能因為有人切換視窗就停下來。所以迴圈在跑的時候，會有一個 Web Worker 每 33 毫秒跳一下。只要 50 毫秒內沒有收到動畫畫面，迴圈就自己往前走一步。被藏起來的分頁因此大約每秒 15 個畫面，`dt` 大小正常，演出時間仍是真實時間。如果頁面禁止 Worker（嚴格的內容安全政策），這個備援就不存在。"},{"en":"`loop.fps` is the frame rate, rounded, refreshed about every half second (the very first value comes from the first frame alone); it is 0 before the first frame. `loop.running` says whether it is going.","zh":"`loop.fps` 是四捨五入後的影格率，大約每半秒更新一次（第一個值只由第一個畫面算出來），在第一個畫面之前是 0。`loop.running` 告訴你它有沒有在跑。"}],"params":[{"name":"onFrame","type":"Function","en":"Called as `onFrame(dt, now)`: `dt` is the seconds since the previous frame (never more than `maxDt`), `now` the time in milliseconds. An error thrown inside is caught and logged as `[loop]`; the clock keeps going.","zh":"以 `onFrame(dt, now)` 呼叫：`dt` 是距離上一個畫面的秒數（不會超過 `maxDt`），`now` 是以毫秒計的時間。裡面丟出的錯誤會被接住、記成 `[loop]`，時鐘繼續跑。"},{"name":"maxDt","type":"Number","optional":true,"default":"0.1","en":"The longest `dt` you will ever be handed, in seconds. A stall (a long garbage collection, a dragged window) is cut to this, so a world never has to cope with a one-second jump.","zh":"你拿到的 `dt` 最大值，單位是秒。卡頓（長時間的記憶體回收、被拖動的視窗）會被截成這個值，世界不用處理一秒鐘的跳動。"}],"needs":{"en":"Call `start()`: nothing ticks until you do. It needs a browser (`requestAnimationFrame`, `performance`); in Node there is no clock to run it.","zh":"要呼叫 `start()`，在那之前什麼都不會跳。它需要瀏覽器（`requestAnimationFrame`、`performance`），在 Node 裡沒有時鐘可以讓它跑。"},"notes":[{"en":"Do not read the real clock inside a world: take time from `dt` (or from the timeline), so a show at 30 fps and one at 144 fps do the same thing.","zh":"不要在世界裡讀真實時間，時間請從 `dt`（或時間軸）來，這樣 30 fps 和 144 fps 的演出才會做一樣的事。"}],"examples":[{"code":"import { Loop } from '@openav/core';\n\nlet elapsed = 0;\nconst loop = new Loop((dt) => {\n  elapsed += dt;\n  document.getElementById('app').textContent = elapsed.toFixed(1) + ' s  ·  ' + loop.fps + ' fps';\n  if (elapsed > 3) loop.stop();\n});\nloop.start();","caption":{"en":"A clock counts up in the page and stops itself after three seconds.","zh":"頁面上有一個時鐘一直往上數，三秒後自己停下來。"},"run":"module","lang":"js"},{"code":"import { Loop } from '@openav/core';\n\nconst ctx = document.getElementById('c').getContext('2d');\nlet x = 0;\nconst loop = new Loop((dt) => {\n  x = (x + 120 * dt) % 400;          // 120 pixels a second, whatever the frame rate\n  ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 400, 300);\n  ctx.fillStyle = '#7ea6ff'; ctx.fillRect(x, 130, 40, 40);\n}, { maxDt: 0.05 });\nloop.start();","caption":{"en":"A square crosses the canvas at 120 pixels a second, the same speed at any frame rate because it moves by `dt`.","zh":"一個方塊以每秒 120 像素的速度橫過畫布，它是按 `dt` 移動的，所以不管畫面更新率多少，速度都一樣。"},"run":"module","lang":"js"}],"related":["createShow","Timeline","Signals","Params"],"keywords":{"en":["loop","frame","clock","requestAnimationFrame","raf","dt","delta time","fps","tick","hidden tab","animation"],"zh":["迴圈","畫面","時鐘","影格","更新率","fps","動畫","分頁","背景","時間差"]},"tbd":[],"claims":[],"members":[{"name":"start","member":"method","signature":"","doc":"","line":28},{"name":"stop","member":"method","signature":"","doc":"","line":51}]},
{"id":"Params","key":"Params","name":"Params","aliases":[],"kind":"class","package":"@openav/core","category":"core","documented":true,"signature":"new Params(schema = [])","import":"import { Params } from '@openav/core';","source":{"file":"packages/core/src/params.js","line":12,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/core/src/params.js#L12"},"summary":{"en":"The values a world listens to: a list of declared params, each resolved every frame from the timeline, unless a hand, a knob or a route has overridden it.","zh":"世界所聽的那些值：一份宣告好的參數清單，每個參數每個畫面都解析成一個數字，除非有手、旋鈕或路由把它蓋掉。"},"description":[{"en":"A param is the way a world takes continuous control: `{ key: 'bloom', min: 0, max: 1, def: 0.5 }`. The world never asks who set it. Every frame each param resolves to one number: the **override** if there is one, otherwise the **base** value (usually the [`Timeline`](#Timeline)'s automation at the current time), otherwise `def`. `resolve()` does exactly that and hands the world a `{ key: value }` object.","zh":"參數是世界取得連續控制的方式：`{ key: 'bloom', min: 0, max: 1, def: 0.5 }`。世界從來不問是誰設定的。每個畫面，每個參數都會解析成一個數字：有**覆寫值**（override）就用覆寫值，否則用**基準值**（通常是 [`Timeline`](#Timeline) 在目前時間的自動化曲線），再不然就用 `def`。`resolve()` 做的就是這件事，把 `{ key: value }` 物件交給世界。"},{"en":"An override is sticky. After `params.override('bloom', 0.8)` (what a slider, a MIDI knob or a mapped route does) the timeline stops moving `bloom` until `clearOverride('bloom')`. That matches the stage: when a person grabs a control, the score gives way. Overrides are clamped into `min`..`max` and snapped to `step` when they are written.","zh":"覆寫是黏住的。`params.override('bloom', 0.8)` 之後（滑桿、MIDI 旋鈕或映射的路由做的就是這個），時間軸就不再動 `bloom`，直到你呼叫 `clearOverride('bloom')`。這跟舞台上的情形一樣：有人抓住控制的時候，樂譜讓路。覆寫值在寫入的時候就會被夾進 `min`..`max`，並對齊 `step`。"},{"en":"A param with `pulse: true` is a trigger, not a level: nobody reads a value from it. Fire it with `firePulse(key)` (a pad, a route from a pulse signal, a console button) and listen with `onPulse(key, cb)`.","zh":"`pulse: true` 的參數是觸發器，不是水準值：沒有人從它讀值。用 `firePulse(key)` 觸發它（打擊墊、脈衝訊號的路由、主控台的按鈕），用 `onPulse(key, cb)` 聽它。"},{"en":"In a show, [`createShow()`](#createShow) builds one `Params` and registers every world's schema into it with `add()`, then returns it as `show.params`.","zh":"在演出裡，[`createShow()`](#createShow) 會建好一個 `Params`，用 `add()` 把每個世界的 schema 登記進去，再用 `show.params` 回傳。"}],"params":[{"name":"schema","type":"Array","optional":true,"default":"[]","en":"The param declarations: `{ key, label?, min, max, def, step?, pulse?, group? }`. `key` is required; `min`, `max` and `def` give the range and the value when nothing else sets it. The array is kept, not copied.","zh":"參數的宣告：`{ key, label?, min, max, def, step?, pulse?, group? }`。`key` 一定要有。`min`、`max`、`def` 給出範圍，以及沒人設定時的值。這個陣列是直接拿來用，不會複製。"}],"notes":[{"en":"`step` snaps with ordinary floating-point arithmetic: with `step: 0.1`, an override of 0.3 reads back as `0.30000000000000004`. On a timeline a stepped param also holds each keyframe instead of interpolating.","zh":"`step` 用一般的浮點數運算對齊：`step: 0.1` 時，覆寫值 0.3 讀回來是 `0.30000000000000004`。在時間軸上，有 `step` 的參數還會停在每個關鍵影格，而不是做內插。"},{"en":"`resolve()` does not clamp the base value, only overrides are clamped when written. Keep your automation inside `min`..`max`.","zh":"`resolve()` 不會夾基準值，只有覆寫值在寫入時會被夾住。自動化曲線請自己保持在 `min`..`max` 之內。"},{"en":"The fields a world may declare (`label`, `options`, `surface`, `group` …) are listed in [Writing a world](https://openaudiovisual.com/docs/#writing-a-world-params).","zh":"世界可以宣告的欄位（`label`、`options`、`surface`、`group` ……）列在[寫一個世界](https://openaudiovisual.com/zh/docs/#writing-a-world-params)。"}],"examples":[{"code":"import { Params } from '@openav/core';\n\nconst params = new Params([{ key: 'bloom', min: 0, max: 1, def: 0.5 }]);\nconsole.log(params.resolve().bloom);          // 0.5  (no automation, no override: def)\nparams.override('bloom', 0.8);                // what a slider or a mapped knob does\nconsole.log(params.resolve({ bloom: 0.1 }).bloom);   // 0.8  (the override beats the base)\nparams.clearOverride('bloom');\nconsole.log(params.resolve({ bloom: 0.1 }).bloom);   // 0.1  (the base is back)","caption":{"en":"The console prints `0.5`, `0.8`, `0.1`: def, then an override that beats the timeline, then the timeline again.","zh":"主控台印出 `0.5`、`0.8`、`0.1`：先是 def，接著是蓋過時間軸的覆寫值，最後時間軸回來了。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\n\nconst params = new Params([\n  { key: 'mode', min: 0, max: 3, def: 0, step: 1 },\n  { key: 'burst', pulse: true },\n]);\nparams.override('mode', 1.6);\nconsole.log(params.resolve().mode);            // 2  (clamped and snapped to the step)\nparams.onPulse('burst', (key) => console.log('fired', key));\nparams.firePulse('burst');                     // fired burst","caption":{"en":"A stepped param snaps (the console shows `2`) and a pulse param fires its listener (`fired burst`).","zh":"有 `step` 的參數會對齊（主控台顯示 `2`），脈衝參數會觸發它的監聽函式（`fired burst`）。"},"run":"module","lang":"js"},{"code":"import { Params } from '@openav/core';\n\nconst params = new Params([{ key: 'bloom', min: 0, max: 1, def: 0.5 }]);\nconsole.log(params.get('bloom'));                       // the declaration, not a value\nparams.override('bloom', 5);\nconsole.log(params.resolve().bloom);                    // 1  (clamped to max)\nparams.override('typo', 0.3);                           // unknown key: ignored\nconsole.log(params.isOverridden('typo'));               // false","caption":{"en":"The gotchas: `get()` returns the declaration `{ key, min, max, def }`, an override is clamped to the range (`1`), and an unknown key is silently ignored (`false`).","zh":"幾個陷阱：`get()` 回傳的是宣告 `{ key, min, max, def }`，覆寫值會被夾進範圍（`1`），不認識的 key 會被默默忽略（`false`）。"},"run":"module","lang":"js"}],"related":["Signals","Timeline","Mapper","Stage","createShow"],"keywords":{"en":["param","params","parameter","schema","override","resolve","clamp","step","pulse param","slider","min max def"],"zh":["參數","覆寫","解析","滑桿","範圍","預設值","步進","脈衝參數","宣告"]},"tbd":[],"claims":[],"members":[{"name":"add","member":"method","signature":"entries","doc":"","line":20},{"name":"get","member":"method","signature":"key","doc":"","line":25},{"name":"clamp","member":"method","signature":"key, v","doc":"","line":27},{"name":"override","member":"method","signature":"key, v","doc":"","line":34},{"name":"clearOverride","member":"method","signature":"key","doc":"","line":35},{"name":"clearAllOverrides","member":"method","signature":"","doc":"","line":36},{"name":"isOverridden","member":"method","signature":"key","doc":"","line":37},{"name":"firePulse","member":"method","signature":"key","doc":"Fire a pulse param (momentary trigger, e.g.","line":40},{"name":"onPulse","member":"method","signature":"key, cb","doc":"","line":44},{"name":"resolve","member":"method","signature":"base = {}","doc":"Resolve effective state: base (e.g.","line":51}]},
{"id":"Signals","key":"Signals","name":"Signals","aliases":[],"kind":"class","package":"@openav/core","category":"core","documented":true,"signature":"new Signals()","import":"import { Signals } from '@openav/core';","source":{"file":"packages/core/src/signals.js","line":19,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/core/src/signals.js#L19"},"summary":{"en":"The registry every input writes into and everything else reads from: named values that are either a level (continuous) or an event (pulse).","zh":"所有輸入都寫進去、其他東西都從這裡讀的登錄表：一個個有名字的值，不是持續的數值（continuous），就是一次性的事件（pulse）。"},"description":[{"en":"`new Signals()` makes an empty registry. In a show you rarely build it yourself: [`createShow()`](#createShow) creates one and returns it as `show.signals`. A signal is a name plus a value. Input packages publish with `set()` and `pulse()`; everything else reads with `get()` and `norm()`, or subscribes with `on()` and `onAny()`.","zh":"`new Signals()` 會做出一個空的登錄表。做演出的時候通常不用自己建：[`createShow()`](#createShow) 會建好一個，用 `show.signals` 回傳給你。一個訊號就是一個名字加一個值。輸入套件用 `set()` 和 `pulse()` 發布。其他東西用 `get()` 和 `norm()` 讀，或用 `on()` 和 `onAny()` 訂閱。"},{"en":"There are two kinds. A **continuous** signal (the default) always holds a current value, usually between 0 and 1: a knob, the loudness of the room, the height of a wrist. A **pulse** signal (`kind: 'pulse'`) is an event: its value is the payload of the last firing and it is `null` until the first one: a key struck, a kick heard, a pinch started. Read a level whenever you like; subscribe to an event with `on()`.","zh":"訊號分兩種。**連續**（continuous，預設）的訊號隨時都有一個目前的值，通常在 0 到 1 之間：旋鈕的位置、房間的音量、手腕的高度。**脈衝**（pulse，寫成 `kind: 'pulse'`）是事件：它的值是最近一次觸發帶的內容，在第一次觸發之前是 `null`：按下一個鍵、聽到一下大鼓、開始一次捏合。水準值想讀就讀。事件用 `on()` 訂閱。"},{"en":"Names are paths with the source first: `midi/cc/74`, `audio/rms`, `pose/hand/right/y`, `breath/pressure`. Declare a signal with its range before you publish it: `signals.define('breath/pressure', { min: 0, max: 1 })`. `set()` on a name nobody declared defines it on the spot as a continuous signal from 0 to 1, so declare first whenever your numbers are in degrees, MIDI notes or any other range.","zh":"名字是「來源在前」的路徑：`midi/cc/74`、`audio/rms`、`pose/hand/right/y`、`breath/pressure`。發布之前先用範圍宣告它：`signals.define('breath/pressure', { min: 0, max: 1 })`。對一個沒人宣告過的名字呼叫 `set()`，會當場把它宣告成 0 到 1 的連續訊號，所以你的數字如果是角度、MIDI 音符號碼或別的範圍，請先宣告。"},{"en":"A knob, a raised hand and a chord are equal citizens here. Whoever reads a signal (the [`Mapper`](#Mapper), a world that listens for events, the monitor) does not know and does not care what produced it.","zh":"旋鈕、舉起的手、一個和弦，在這裡地位相同。讀訊號的一方（[`Mapper`](#Mapper)、聽事件的世界、後台監看）不知道也不在乎是什麼產生它。"}],"params":[],"notes":[{"en":"`signals.meta` is a `Map` of name → declaration and `signals.values` a `Map` of name → last value. Read them if you must; write through `define()` and `set()` so subscribers hear about it.","zh":"`signals.meta` 是「名字 → 宣告」的 `Map`，`signals.values` 是「名字 → 最近的值」的 `Map`。需要的話可以讀，要寫請透過 `define()` 和 `set()`，訂閱者才聽得到。"},{"en":"A subscriber that throws is caught and logged as `[signals] <name>`; the other subscribers still run and `set()` does not throw.","zh":"訂閱者丟出錯誤會被接住、記成 `[signals] <名字>`，其他訂閱者照常執行，`set()` 也不會丟錯。"},{"en":"The names every built-in package publishes are listed in [the signals reference](https://openaudiovisual.com/docs/#signals).","zh":"每個內建套件發布的訊號名稱，都列在[訊號參考](https://openaudiovisual.com/zh/docs/#signals)。"}],"examples":[{"code":"import { Signals } from '@openav/core';\n\nconst signals = new Signals();\nsignals.define('breath/pressure', { min: 0, max: 1 });\nsignals.on('breath/pressure', (v) => console.log('pressure', v));\nsignals.set('breath/pressure', 0.4);   // logs: pressure 0.4","caption":{"en":"Open the console: it prints `pressure 0.4`, because the subscriber ran as soon as the value was set.","zh":"打開主控台：會印出 `pressure 0.4`，因為值一設定，訂閱者就跟著執行。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\n\nconst signals = new Signals();\nsignals.define('temp/room', { min: 10, max: 30, unit: '°C' });\nsignals.set('temp/room', 25);\nconsole.log(signals.get('temp/room'), signals.norm('temp/room'));   // 25 0.75\n\nsignals.define('breath/sigh', { kind: 'pulse' });\nsignals.on('breath/sigh', (payload) => console.log('sigh', payload));\nsignals.pulse('breath/sigh', { depth: 0.8 });                       // sigh { depth: 0.8 }","caption":{"en":"A level and an event side by side: the console shows `25 0.75`, then `sigh { depth: 0.8 }`.","zh":"一個水準值和一個事件並排：主控台先印出 `25 0.75`，再印出 `sigh { depth: 0.8 }`。"},"run":"module","lang":"js"},{"code":"import { Signals } from '@openav/core';\n\nconst signals = new Signals();\nsignals.set('wind/speed', 12);              // never declared, so it is 0..1\nconsole.log(signals.norm('wind/speed'));    // 1  (12 is far past the range)\nsignals.define('wind/speed', { min: 0, max: 20 });\nconsole.log(signals.norm('wind/speed'));    // 0.6","caption":{"en":"The gotcha: an undeclared signal is 0..1, so 12 reads as 1. Declare the range and the same value reads 0.6 (the console shows `1`, then `0.6`).","zh":"陷阱：沒宣告的訊號範圍是 0..1，所以 12 會讀成 1。把範圍宣告好，同一個值就讀成 0.6（主控台先印 `1`，再印 `0.6`）。"},"run":"module","lang":"js"}],"related":["Params","Loop","Mapper","createShow","Bus","ChordDetector"],"keywords":{"en":["signal","signals","registry","define","set","pulse","event","continuous","publish","subscribe","normalized","input","osc"],"zh":["訊號","信號","登錄表","輸入","脈衝","事件","連續","發布","訂閱","正規化","範圍"]},"tbd":[],"claims":[],"members":[{"name":"define","member":"method","signature":"name, meta = {}","doc":"Declare a signal.","line":29},{"name":"set","member":"method","signature":"name, value","doc":"Publish a value.","line":40},{"name":"pulse","member":"method","signature":"name, payload = 1","doc":"Fire a pulse signal (kind: 'pulse').","line":50},{"name":"get","member":"method","signature":"name","doc":"","line":52},{"name":"norm","member":"method","signature":"name","doc":"Normalized 0..1 view of a continuous signal (per its declared min/max).","line":55},{"name":"on","member":"method","signature":"name, cb","doc":"","line":62},{"name":"onAny","member":"method","signature":"cb","doc":"Subscribe to every signal (mapping layer / monitor use this).","line":69},{"name":"list","member":"method","signature":"","doc":"All signals as [{name, value, ...meta}] — monitor food.","line":72}]},
{"id":"createShow","key":"createShow","name":"createShow","aliases":[],"kind":"function","package":"@openav/show","category":"core","documented":true,"signature":"async createShow({ world = null, worlds = [], timeline: timelineCfg = { total: 120 }, routes = [], modules = {}, artwork = null, hint = '', profile = null, map = null, onFrame = null, mount = null, telemetry = true, score: scoreCfg = null, } = {})","import":"import { createShow } from '@openav/show';","source":{"file":"packages/show/index.js","line":44,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/index.js#L44"},"summary":{"en":"Builds a whole show around a world in one call: signals, params, stage, timeline, mapper, the modules you switch on, the console, the backstage feed and the frame loop.","zh":"一個呼叫就替世界組好整場演出：訊號、參數、舞台、時間軸、映射、你開啟的模組、控台、後台資料和影格迴圈。"},"description":[{"en":"Every example is a world plus one `createShow()` call. It builds the [`Signals`](#Signals) registry, the [`Params`](#Params), the [`Stage`](#Stage) with your world, the [`Timeline`](#Timeline), the [`Mapper`](#Mapper) (with your `routes`), the inputs and outputs you declare in `modules`, the director's desk ([`mountConsole()`](#mountConsole)), the backstage feed ([`MonitorFeed`](#MonitorFeed)) and the frame [`Loop`](#Loop), starts the loop, and hands you every part. Nothing is hidden: `show.signals`, `show.params`, `show.timeline`, `show.mapper`, `show.stage` and the rest are the very objects the frame loop runs. When the framework grows a module, every show can have it just by declaring it.","zh":"每個範例都是一個世界加上一次 `createShow()` 呼叫。它建好 [`Signals`](#Signals) 登錄表、[`Params`](#Params)、放著你的世界的 [`Stage`](#Stage)、[`Timeline`](#Timeline)、[`Mapper`](#Mapper)（帶著你的 `routes`）、你在 `modules` 宣告的輸入與輸出、導演控台（[`mountConsole()`](#mountConsole)）、後台資料（[`MonitorFeed`](#MonitorFeed)）和影格 [`Loop`](#Loop)，啟動迴圈，再把每個部分交給你。什麼都沒有藏起來：`show.signals`、`show.params`、`show.timeline`、`show.mapper`、`show.stage` 這些，就是影格迴圈實際在跑的那些物件。框架多了一個模組的時候，每場演出只要宣告就能用上。"},{"en":"The `modules` keys: `midi` (on by default, `false` turns it off, `{ controllers: true }` or `{ controllers: { profile, profiles, open } }` docks an on-screen MIDI controller), `keys` (the on-screen piano, QWERTY and the simulated performer: on by default, `false` turns them off, `{ base, octaves, sim, capture }` tunes them), `drums` (`true` or `{ engine, autoEnableEngine }`), `audio` (any truthy value: the microphone analyser, started from the 🎤 button), `chord` (`true` or `{ window }`: a [`ChordDetector`](#ChordDetector) wired to every played note), `hands` (`true`: [`HandTracker`](#HandTracker)), `pose` (`true`: [`PoseTracker`](#PoseTracker)), `leap` (`true` or `{ url, simulate, fingers, box }`: [`LeapInput`](#LeapInput); `?leap=sim` or `?leap=ws://…` in the address overrides), `remote` (`true` or `{ room, … }`: phones as controllers) and `sound` (`true`, an instrument id such as `'piano'`, or `{ instrument, remember, picker, engine }`: instruments in the page, loaded only when declared). Parts that fetch things load lazily, so a show that does not declare them pays nothing.","zh":"`modules` 的鍵：`midi`（預設開啟，`false` 關掉，`{ controllers: true }` 或 `{ controllers: { profile, profiles, open } }` 會在舞台上停靠一個螢幕上的 MIDI 控制器）、`keys`（螢幕鋼琴、電腦鍵盤和模擬演奏者：預設開啟，`false` 關掉，`{ base, octaves, sim, capture }` 可調整）、`drums`（`true` 或 `{ engine, autoEnableEngine }`）、`audio`（任何真值：麥克風分析器，從 🎤 按鈕啟動）、`chord`（`true` 或 `{ window }`：接上每個被彈奏的音的 [`ChordDetector`](#ChordDetector)）、`hands`（`true`：[`HandTracker`](#HandTracker)）、`pose`（`true`：[`PoseTracker`](#PoseTracker)）、`leap`（`true` 或 `{ url, simulate, fingers, box }`：[`LeapInput`](#LeapInput)，網址上的 `?leap=sim` 或 `?leap=ws://…` 可以覆寫）、`remote`（`true` 或 `{ room, … }`：把手機當控制器）和 `sound`（`true`、像 `'piano'` 這樣的樂器 id，或 `{ instrument, remember, picker, engine }`：頁面內的樂器，宣告了才會載入）。需要下載東西的部分都是延遲載入，所以沒宣告的演出不會付出任何代價。"},{"en":"The page shell is fixed once so every show is the same. Without `mount`, the page body becomes a two-column grid: the stage, and a 340 px side panel holding the piano and the console (an upright phone gets the stage on top and the panel below: see [`isStacked()`](#isStacked)). Every frame, in this order:\n\n```text\nkeys, drums, audio update   on-screen performers and the microphone\nonFrame(dt, show)           your hook\ntimeline.advance(dt)        time moves\ndirector.update(...)        with a score: segment modules enter, update, exit\nmapper.update(dt)           smoothed routes settle\nstage.frame(dt, state)      params resolve, world updates and renders\nsound, remote               sound params reach the synth, values echo to phones\nconsole.render(state)       the desk reflects reality\nmonitor.frame(snapshot)     the backstage hears about it\n```","zh":"頁面外殼只固定一次，所以每場演出都一樣。沒有 `mount` 的話，頁面的 body 會變成兩欄的格線：舞台，以及放著鋼琴和控台的 340 像素側邊面板（直立的手機則是舞台在上、面板在下：見 [`isStacked()`](#isStacked)）。每個畫面依這個順序做事：\n\n```text\nkeys, drums, audio update   on-screen performers and the microphone\nonFrame(dt, show)           your hook\ntimeline.advance(dt)        time moves\ndirector.update(...)        with a score: segment modules enter, update, exit\nmapper.update(dt)           smoothed routes settle\nstage.frame(dt, state)      params resolve, world updates and renders\nsound, remote               sound params reach the synth, values echo to phones\nconsole.render(state)       the desk reflects reality\nmonitor.frame(snapshot)     the backstage hears about it\n```"}],"params":[{"name":"world","type":"Object | null","optional":true,"default":"null","en":"The world to perform: `{ name, params, init, update, render, dispose }` (see [`Stage`](#Stage)). It is activated first, and `createShow` waits for its `init()`.","zh":"要表演的世界：`{ name, params, init, update, render, dispose }`（見 [`Stage`](#Stage)）。它會最先被啟用，`createShow` 會等它的 `init()` 完成。"},{"name":"worlds","type":"Array","optional":true,"default":"[]","en":"More worlds to register, after `world`. All of their params are registered up front; switch with `show.stage.activate(name)`.","zh":"`world` 之外要登記的更多世界。它們的參數會全部先登記好。用 `show.stage.activate(name)` 切換。"},{"name":"timeline","type":"Object","optional":true,"default":"{ total: 120 }","en":"The options of the [`Timeline`](#Timeline): `{ total, automation, scenes, start, loop, onCue }`. With a `score`, its scenes and total win.","zh":"[`Timeline`](#Timeline) 的選項：`{ total, automation, scenes, start, loop, onCue }`。有 `score` 的時候，以它的場景和總長為準。"},{"name":"routes","type":"Array","optional":true,"default":"[]","en":"Routes for the [`Mapper`](#Mapper). If a saved profile exists it is loaded, and any declared route whose `source → target` pair is missing from it is still added.","zh":"給 [`Mapper`](#Mapper) 的路由。如果有存起來的 profile 就會載入，其中沒有的 `source → target` 組合，仍然會把你宣告的路由補進去。"},{"name":"modules","type":"Object","optional":true,"default":"{}","en":"The inputs and outputs to switch on, by key (see the description): `midi`, `keys`, `drums`, `audio`, `chord`, `hands`, `pose`, `leap`, `remote`, `sound`.","zh":"要開啟的輸入與輸出，用鍵名指定（見說明）：`midi`、`keys`、`drums`、`audio`、`chord`、`hands`、`pose`、`leap`、`remote`、`sound`。"},{"name":"artwork","type":"Object | null","optional":true,"default":"null","en":"`{ title, artist, year, note }`: a credit line at the bottom right of the stage and a `<meta name=\"artwork\">` tag in the head.","zh":"`{ title, artist, year, note }`：舞台右下角的一行作品署名，以及 `<head>` 裡的 `<meta name=\"artwork\">` 標籤。"},{"name":"hint","type":"String","optional":true,"default":"''","en":"A line of **HTML** at the bottom left of the stage, for example `'play the piano · Space plays the timeline'`. It is inserted as HTML, so never put untrusted text in it.","zh":"舞台左下角的一行 **HTML**，例如 `'play the piano · Space plays the timeline'`。它是當成 HTML 插入的，所以千萬別放來路不明的文字。"},{"name":"profile","type":"String | false | null","optional":true,"default":"null","en":"The name the mapper saves its routes under in `localStorage` (default: the first world's name, else `'show'`). `false` ignores the saved routes when the show starts, so the routes come from your code on every load.","zh":"映射在 `localStorage` 儲存路由時用的名字（預設是第一個世界的名字，再不然是 `'show'`）。`false` 會在演出開始時忽略存起來的路由，所以每次載入路由都來自你的程式。"},{"name":"map","type":"String | false | null","optional":true,"default":"null","en":"A map file to load on start: a same-origin or https address (see [`loadMapUrl()`](#loadMapUrl)). Left out, `?map=<url>` in the page address is used; `false` ignores `?map=`.","zh":"開始時要載入的映射檔：同源或 https 的網址（見 [`loadMapUrl()`](#loadMapUrl)）。沒寫的話，使用頁面網址上的 `?map=<url>`。`false` 會忽略 `?map=`。"},{"name":"onFrame","type":"Function | null","optional":true,"default":"null","en":"Called as `onFrame(dt, show)` every frame, after the on-screen performers and the microphone update and **before** the timeline advances. The place for assembly-level logic such as envelopes, or `osc.flush()`.","zh":"每個畫面以 `onFrame(dt, show)` 呼叫，在螢幕上的演奏者和麥克風更新之後、時間軸前進**之前**。適合放組裝層級的邏輯，例如包絡，或 `osc.flush()`。"},{"name":"mount","type":"Object | null","optional":true,"default":"null","en":"`{ stage, side }`: elements or selectors to render into instead of the generated full-page layout.","zh":"`{ stage, side }`：要渲染進去的元素或選擇器，取代自動產生的全頁面版面。"},{"name":"telemetry","type":"Boolean","optional":true,"default":"true","en":"Allow one anonymous `oav_show_start` hit per page load. `false` sends nothing.","zh":"是否允許每次載入頁面送出一次匿名的 `oav_show_start`。`false` 什麼都不送。"},{"name":"score","type":"Object | null","optional":true,"default":"null","en":"`{ cuts, cut, modules, api, onStatus, onCue, maxStep, midi }`: the show's structure as segment lengths, performed by segment modules (see [`Score`](#Score) and [`Director`](#Director)). `?cut=<id>` in the page address picks the version.","zh":"`{ cuts, cut, modules, api, onStatus, onCue, maxStep, midi }`：以段落長度寫成的演出結構，由段落模組表演（見 [`Score`](#Score) 和 [`Director`](#Director)）。頁面網址上的 `?cut=<id>` 可以選版本。"}],"returns":{"type":"Promise<Object>","en":"The show: `{ signals, params, stage, timeline, mapper, midi, controllers, midiPanel, keys, drums, sound, audio, hands, pose, leap, chord, remote, score, director, scoreMidi, console, app, loop }`. A part you did not ask for is `null`. With a `score` there is also `show.show`, the rehearsal controls. The same object is `window.openav`.","zh":"演出物件：`{ signals, params, stage, timeline, mapper, midi, controllers, midiPanel, keys, drums, sound, audio, hands, pose, leap, chord, remote, score, director, scoreMidi, console, app, loop }`。沒有要求的部分是 `null`。有 `score` 的時候還有 `show.show`，也就是排練用的控制。同一個物件也是 `window.openav`。"},"needs":{"en":"`await` it: it is async because it waits for the world's `init()` (and for lazily loaded modules). It needs a page (`document`, `location`, `localStorage`), and an import map or bundler that resolves every `@openav/*` package it imports (copy the one from `examples/01-hello-particles/index.html`). Browsers start audio only after a click, so sound and the microphone start from the console's buttons.","zh":"要 `await` 它：它是非同步的，因為要等世界的 `init()`（和延遲載入的模組）完成。它需要一個頁面（`document`、`location`、`localStorage`），還要有能解析它所匯入的每個 `@openav/*` 套件的 import map 或打包工具（可以複製 `examples/01-hello-particles/index.html` 裡的那份）。瀏覽器要等點擊之後才會啟動聲音，所以聲音和麥克風要從控台的按鈕啟動。"},"notes":[{"en":"The mapper saves its routes under the profile name every 3 seconds, so a learned or edited route survives a reload and **wins over a changed declaration in your code** (the same `source → target` pair keeps its saved curve and smoothing). `profile: false` makes the routes come from code on every load, but the mapper still saves under the world's name from then on. To start clean, `localStorage.removeItem('openav.map.<world name>')`.","zh":"映射每 3 秒把路由存在 profile 名稱底下，所以學來的或改過的路由，重新載入之後還在，而且**會贏過你在程式裡改過的宣告**（同一個 `source → target` 組合會保留存起來的曲線和平滑）。`profile: false` 讓路由每次載入都來自程式，但映射之後仍然會用世界的名字存檔。要從頭開始，執行 `localStorage.removeItem('openav.map.<世界名稱>')`。"},{"en":"Routes are added before the sound engine registers its `sound/*` params, so a declared route to `sound/cutoff` needs an explicit `outMin: 100, outMax: 8000`.","zh":"路由加進去的時間比聲音引擎登記 `sound/*` 參數還早，所以宣告接到 `sound/cutoff` 的路由要明確寫上 `outMin: 100, outMax: 8000`。"},{"en":"`show.params.resolve()` without an argument gives the `def` of every param. To read what the world sees this frame, pass the timeline's base state: `show.params.resolve(show.timeline.state())`.","zh":"`show.params.resolve()` 不給參數的話，回傳的是每個參數的 `def`。想讀世界這個畫面看到的值，要傳入時間軸的基準狀態：`show.params.resolve(show.timeline.state())`。"},{"en":"Unless `telemetry: false`, one anonymous `oav_show_start` hit goes to Google Analytics per page load. Per the code it is silent on localhost, LAN addresses, `file://` and openaudiovisual.com itself, and it honours Do Not Track and Global Privacy Control. Nothing played, no path, no cookie and no stable id is sent.","zh":"除非 `telemetry: false`，每次載入頁面會有一次匿名的 `oav_show_start` 送到 Google Analytics。依程式碼，在 localhost、區域網路位址、`file://` 和 openaudiovisual.com 本身都不會送，也尊重 Do Not Track 和 Global Privacy Control。彈了什麼、頁面路徑、cookie 和固定的識別碼都不會送。"},{"en":"With a `score`, `window.openav.show` has `table()`, `goto('dusk')`, `goto('dusk.glow')`, `goto(2)`, `seek(T)`, `cue(name)`, `status()`, `next()`, `prev()` and `cuts`, for rehearsal from the devtools console.","zh":"有 `score` 的時候，`window.openav.show` 有 `table()`、`goto('dusk')`、`goto('dusk.glow')`、`goto(2)`、`seek(T)`、`cue(name)`、`status()`、`next()`、`prev()` 和 `cuts`，可以在開發者工具的主控台排練。"}],"examples":[{"code":"import { createShow } from '@openav/show';\nimport { createCanvas } from '@openav/stage';\n\nconst world = {\n  name: 'dot',\n  params: [{ key: 'size', label: 'Size', min: 0, max: 1, def: 0.3 }],\n  init({ container }) { this.view = createCanvas(container); },\n  update(dt, state) { this.size = state.size; },\n  render() {\n    const { ctx } = this.view, { w, h } = this.view.fit();\n    ctx.fillStyle = '#000'; ctx.fillRect(0, 0, w, h);\n    ctx.fillStyle = '#7ea6ff'; ctx.beginPath(); ctx.arc(w / 2, h / 2, this.size * h / 2, 0, Math.PI * 2); ctx.fill();\n  },\n  dispose() { this.view.dispose(); },\n};\n\nconst show = await createShow({\n  world,\n  timeline: { total: 60, automation: { size: [[0, 0.1], [60, 0.9]] } },\n  modules: { midi: false, keys: false },     // nothing to plug in\n  telemetry: false,\n});\nshow.timeline.play();                        // the dot grows over a minute\nconsole.log(Object.keys(show).length, show.timeline.playing);","caption":{"en":"A blue dot on the stage grows slowly while the timeline plays, with the desk (transport, layers, the `size` slider) on the right. The console prints `23 true`: twenty-three parts, and the timeline is playing.","zh":"舞台上的藍色圓點隨時間軸播放慢慢變大，右邊是控台（播放控制、圖層、`size` 推桿）。主控台印出 `23 true`：二十三個部分，時間軸正在播放。"},"run":"module","lang":"js"},{"code":"import { createShow } from '@openav/show';\n\nconst world = {\n  name: 'glow',\n  params: [{ key: 'size', min: 0, max: 1, def: 0.3 }, { key: 'burst', pulse: true }],\n  init() {},\n  update(dt, state) { this.last = state; },\n};\nconst show = await createShow({\n  world,\n  routes: [{ source: 'midi/cc/1', target: 'size' }, { source: 'pad/1', target: 'burst' }],\n  modules: { midi: false, keys: false },\n  profile: false,                            // routes come from this code on every load\n  telemetry: false,\n});\n\nshow.params.onPulse('burst', () => console.log('burst!'));\nshow.signals.set('midi/cc/1', 0.9);          // what a knob would do\nconsole.log(show.params.isOverridden('size'), show.params.resolve(show.timeline.state()).size);   // true 0.9\nshow.signals.set('pad/1', 0);\nshow.signals.set('pad/1', 1);                // a continuous source rising past 0.5 fires a pulse param\nsetTimeout(() => console.log('the world heard', world.last.size), 200);","caption":{"en":"A signal drives a param through a route: the console prints `true 0.9`, then `burst!` as the pad crosses 0.5, then `the world heard 0.9`.","zh":"一個訊號透過路由驅動一個參數：主控台印出 `true 0.9`，接著是墊子越過 0.5 時的 `burst!`，最後是 `the world heard 0.9`。"},"run":"module","lang":"js"},{"code":"import { createShow } from '@openav/show';\n\nconst world = { name: 'sky', params: [{ key: 'sun', min: 0, max: 1, def: 0 }], init() {}, update() {} };\nconst dusk = {\n  id: 'dusk',\n  enter() { console.log('dusk enters'); },\n  param: (key, value, ctx) => (key === 'sun' ? 1 - ctx.p : value),     // the segment bends the sun param\n};\nconst show = await createShow({\n  world,\n  modules: { midi: false, keys: false },\n  telemetry: false,\n  profile: false,\n  score: { cuts: [{ id: 'full', segments: [{ id: 'dawn', dur: 10 }, { id: 'dusk', dur: 10 }] }], modules: [dusk] },\n});\nconsole.log(show.score.total, show.timeline.total);     // 20 20: the timeline takes its length from the score\nshow.show.goto('dusk');                                  // rehearsal control: jump to a segment\nsetTimeout(() => console.log(show.show.status().segment.id, show.timeline.t), 300);","caption":{"en":"With a score the timeline takes its length from it: the console prints `20 20`, then `dusk enters` after the jump, and `dusk 10`.","zh":"有總譜的時候，時間軸的長度由它決定：主控台印出 `20 20`，跳轉之後印出 `dusk enters`，最後是 `dusk 10`。"},"run":"module","lang":"js"},{"code":"import { createShow } from '@openav/show';\n\nconst show = await createShow({\n  world,                                         // your world\n  timeline: { total: 120, automation, scenes },\n  routes: [{ source: 'audio/rms', target: 'energy', inMax: 0.3, smooth: 0.1 }],\n  modules: { keys: { base: 48 }, chord: true, audio: true, sound: 'piano' },\n  artwork: { title: 'Untitled', artist: 'You', year: 2026 },\n  hint: 'play the piano · Space plays the timeline',\n});\nshow.signals.on('chord/event', (a) => console.log(a.chordType));","caption":{"en":"The whole set: an on-screen piano, chord analysis, the microphone and a grand piano that sounds after a click on the console's sound button. Play a chord and the console prints its type (`major`).","zh":"完整的一套：螢幕鋼琴、和弦分析、麥克風，以及在控台上按一下聲音按鈕之後就會發聲的平台鋼琴。彈一個和弦，主控台就印出它的類型（`major`）。"},"run":"static","lang":"js"}],"related":["Signals","Params","Loop","Stage","Timeline","Mapper","mountConsole","MonitorFeed","Score","Director","ChordDetector","LeapInput","isStacked"],"keywords":{"en":["createShow","show","assembly","factory","one call","setup","modules","world","routes","timeline","console","window.openav","boot","start"],"zh":["createShow","演出","組裝","一個呼叫","設定","模組","世界","路由","時間軸","控台","window.openav","啟動"]},"tbd":[{"en":"The modules that need hardware, a permission or a download (microphone, camera, Leap, phones, the Tone.js sound engine, a MIDI device) were not run here; they are described from the code and the package docs. The examples switch `midi` and `keys` off so that they run anywhere.","zh":"需要硬體、權限或下載的模組（麥克風、鏡頭、Leap、手機、Tone.js 聲音引擎、MIDI 裝置）沒有在這裡跑過，說明是根據程式碼和套件文件寫的。範例把 `midi` 和 `keys` 關掉，這樣在哪裡都能跑。"}],"claims":[]},
{"id":"isStacked","key":"isStacked","name":"isStacked","aliases":[],"kind":"function","package":"@openav/show","category":"core","documented":true,"signature":"isStacked(width, height)","import":"import { isStacked } from '@openav/show/layout';","source":{"file":"packages/show/layout.js","line":16,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/layout.js#L16"},"summary":{"en":"Whether a window of this size gets the stacked layout (stage on top, panel below): upright and no wider than 820 pixels.","zh":"這個大小的視窗會不會用堆疊版面（舞台在上、面板在下）：直立的、而且寬度不超過 820 像素。"},"description":[{"en":"The shell of [`createShow()`](#createShow) has one layout rule: wide windows get the stage and a 340 px side panel next to each other, and an upright phone or small tablet gets the stage on top and the panel below it, scrolling. The rule is the media query [`STACK_QUERY`](#STACK_QUERY), and `isStacked()` is the very same decision as a plain function, so a test (or your own layout code) can use it without a browser. A phone on its side is never taller than wide, so it stays in the wide layout.","zh":"[`createShow()`](#createShow) 的外殼只有一條版面規則：寬的視窗是舞台和 340 像素的側邊面板並排，直立的手機或小平板則是舞台在上、面板在下並可以捲動。規則就是媒體查詢 [`STACK_QUERY`](#STACK_QUERY)，而 `isStacked()` 是同一個判斷的純函式版本，所以測試（或你自己的版面程式）不用瀏覽器也能用。橫放的手機高度不會比寬度大，所以仍然是寬版面。"}],"params":[{"name":"width","type":"Number","en":"The window width in pixels.","zh":"視窗寬度，單位是像素。"},{"name":"height","type":"Number","en":"The window height in pixels.","zh":"視窗高度，單位是像素。"}],"returns":{"type":"Boolean","en":"`true` when `width <= 820` and `height > width`.","zh":"`width <= 820` 而且 `height > width` 的時候是 `true`。"},"notes":[],"examples":[{"code":"import { isStacked, stageHeight } from '@openav/show/layout';\n\nconsole.log(isStacked(390, 844));     // true: a phone held upright\nconsole.log(isStacked(844, 390));     // false: the same phone on its side\nconsole.log(isStacked(820, 1180));    // true: a small tablet held upright\nconsole.log(isStacked(1024, 1366));   // false: wider than 820, even though it is upright\nconsole.log(stageHeight(844));        // 490: the stage keeps 58% of the window height","caption":{"en":"The console prints `true`, `false`, `true`, `false`, then `490`.","zh":"主控台印出 `true`、`false`、`true`、`false`，然後是 `490`。"},"run":"module","lang":"js"}],"related":["stageHeight","STACK_QUERY","STACK_MAX_WIDTH","STAGE_SHARE","SHELL_CSS","createShow"],"keywords":{"en":["stacked layout","responsive","phone layout","portrait","breakpoint","layout rule","820"],"zh":["堆疊版面","響應式","手機版面","直式","斷點","版面規則","820"]},"tbd":[],"claims":[]},
{"id":"SHELL_CSS","key":"SHELL_CSS","name":"SHELL_CSS","aliases":[],"kind":"constant","package":"@openav/show","category":"core","documented":true,"signature":"SHELL_CSS = \"\\n  body { margin: 0; background: #000; height: 100vh; display: grid;\\n    grid…\"","import":"import { SHELL_CSS } from '@openav/show/layout';","source":{"file":"packages/show/layout.js","line":25,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/layout.js#L25"},"summary":{"en":"The CSS text `createShow()` puts in the page: the two-column grid, the 340 px side panel, the hint and credit styles, and the stacked layout.","zh":"`createShow()` 放進頁面的 CSS 文字：兩欄格線、340 像素的側邊面板、提示與署名的樣式，以及堆疊版面。"},"description":[{"en":"A string. `createShow()` adds it once as `<style id=\"openav-shell-css\">` (unless one with that id is already there). It sets the page body to a grid of the stage and a 340 px side panel, with the classes `.oav-stage`, `.oav-side`, `.oav-keys-slot`, `.oav-desk`, `.oav-hint` and `.oav-credit`, and the stacked layout from [`STACK_QUERY`](#STACK_QUERY). Read it to see what the shell does to your page, or add a `<style id=\"openav-shell-css\">` of your own before calling `createShow()` to replace it entirely.","zh":"一個字串。`createShow()` 會把它用 `<style id=\"openav-shell-css\">` 加進頁面一次（除非已經有同 id 的）。它把頁面 body 設成舞台加上 340 像素側邊面板的格線，用到 `.oav-stage`、`.oav-side`、`.oav-keys-slot`、`.oav-desk`、`.oav-hint` 和 `.oav-credit` 這些 class，以及來自 [`STACK_QUERY`](#STACK_QUERY) 的堆疊版面。想知道外殼對你的頁面做了什麼就讀它。或在呼叫 `createShow()` 之前，自己先放一個 `<style id=\"openav-shell-css\">`，就能整個取代它。"}],"params":[],"notes":[],"examples":[{"code":"import { SHELL_CSS } from '@openav/show/layout';\n\nconsole.log(SHELL_CSS.includes('.oav-stage'), SHELL_CSS.includes('340px'), SHELL_CSS.length > 500);   // true true true\nconsole.log(SHELL_CSS.split('\\n').slice(1, 4).join('\\n'));      // the first rules: body is a grid","caption":{"en":"The console prints `true true true`, then the first lines of the stylesheet (the body grid).","zh":"主控台印出 `true true true`，接著是樣式表最前面的幾行（body 的格線）。"},"run":"module","lang":"js"}],"related":["createShow","STACK_QUERY","isStacked"],"keywords":{"en":["shell css","stylesheet","layout css","oav-stage","oav-side","grid","openav-shell-css"],"zh":["外殼 css","樣式表","版面 css","oav-stage","oav-side","格線","openav-shell-css"]},"tbd":[],"claims":[]},
{"id":"STACK_MAX_WIDTH","key":"STACK_MAX_WIDTH","name":"STACK_MAX_WIDTH","aliases":[],"kind":"constant","package":"@openav/show","category":"core","documented":true,"signature":"STACK_MAX_WIDTH = 820","import":"import { STACK_MAX_WIDTH } from '@openav/show/layout';","source":{"file":"packages/show/layout.js","line":10,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/layout.js#L10"},"summary":{"en":"The widest window that can get the stacked layout: 820 pixels (a small tablet held upright).","zh":"能用堆疊版面的最寬視窗：820 像素（直立的小平板）。"},"description":[{"en":"Used inside [`STACK_QUERY`](#STACK_QUERY) and [`isStacked()`](#isStacked), so the CSS rule and the function cannot drift apart. Above 820 pixels the stage and the 340 px panel always sit side by side.","zh":"用在 [`STACK_QUERY`](#STACK_QUERY) 和 [`isStacked()`](#isStacked) 裡面，所以 CSS 規則和函式不會彼此走樣。超過 820 像素，舞台和 340 像素的面板一律是並排的。"}],"params":[],"notes":[],"examples":[],"related":["STACK_QUERY","isStacked","STAGE_SHARE"],"keywords":{"en":["820","breakpoint","max width","stacked layout","tablet"],"zh":["820","斷點","最大寬度","堆疊版面","平板"]},"tbd":[],"claims":[]},
{"id":"STACK_QUERY","key":"STACK_QUERY","name":"STACK_QUERY","aliases":[],"kind":"constant","package":"@openav/show","category":"core","documented":true,"signature":"STACK_QUERY = \"(max-width: 820px) and (orientation: portrait)\"","import":"import { STACK_QUERY } from '@openav/show/layout';","source":{"file":"packages/show/layout.js","line":11,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/layout.js#L11"},"summary":{"en":"The media query behind the stacked layout: `(max-width: 820px) and (orientation: portrait)`.","zh":"堆疊版面背後的媒體查詢：`(max-width: 820px) and (orientation: portrait)`。"},"description":[{"en":"It is a string you can hand to `matchMedia()` to ask the browser the same question the shell's CSS asks: `matchMedia(STACK_QUERY).matches`. [`isStacked(width, height)`](#isStacked) is the same decision from numbers.","zh":"這是一個字串，可以交給 `matchMedia()`，問瀏覽器跟外殼的 CSS 同一個問題：`matchMedia(STACK_QUERY).matches`。[`isStacked(width, height)`](#isStacked) 是用數字做的同一個判斷。"}],"params":[],"notes":[],"examples":[{"code":"import { STACK_QUERY } from '@openav/show/layout';\n\nconst query = matchMedia(STACK_QUERY);\nconsole.log(STACK_QUERY, query.matches);          // the window this runs in is not an upright phone: false\nquery.addEventListener('change', (e) => console.log('stacked now?', e.matches));","caption":{"en":"The console prints the query and whether this window matches it (usually `false` on a desktop); the last line logs again whenever the window is resized across the rule.","zh":"主控台印出這個查詢，以及目前的視窗符不符合（在桌機上通常是 `false`）。最後一行在視窗大小跨過這條規則的時候會再印一次。"},"run":"module","lang":"js"}],"related":["isStacked","STACK_MAX_WIDTH","STAGE_SHARE","SHELL_CSS"],"keywords":{"en":["media query","matchMedia","portrait","stacked layout","responsive"],"zh":["媒體查詢","matchMedia","直式","堆疊版面","響應式"]},"tbd":[],"claims":[]},
{"id":"STAGE_SHARE","key":"STAGE_SHARE","name":"STAGE_SHARE","aliases":[],"kind":"constant","package":"@openav/show","category":"core","documented":true,"signature":"STAGE_SHARE = 58","import":"import { STAGE_SHARE } from '@openav/show/layout';","source":{"file":"packages/show/layout.js","line":13,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/layout.js#L13"},"summary":{"en":"How much of the window height the stage keeps when the layout is stacked: 58 (percent).","zh":"版面堆疊的時候，舞台保有視窗高度的多少：58（百分比）。"},"description":[{"en":"The stage must stay the main thing on a phone, so it keeps 58% of the height and the console below it scrolls. [`stageHeight()`](#stageHeight) turns this number into pixels.","zh":"在手機上，舞台必須仍然是主角，所以它保有 58% 的高度，下方的控台可以捲動。[`stageHeight()`](#stageHeight) 把這個數字換算成像素。"}],"params":[],"notes":[],"examples":[],"related":["stageHeight","isStacked","STACK_QUERY"],"keywords":{"en":["58","stage share","stage height","percent","stacked layout"],"zh":["58","舞台比例","舞台高度","百分比","堆疊版面"]},"tbd":[],"claims":[]},
{"id":"stageHeight","key":"stageHeight","name":"stageHeight","aliases":[],"kind":"function","package":"@openav/show","category":"core","documented":true,"signature":"stageHeight(height)","import":"import { stageHeight } from '@openav/show/layout';","source":{"file":"packages/show/layout.js","line":21,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/show/layout.js#L21"},"summary":{"en":"The stage height in pixels for a window of this height when the layout is stacked: 58% of it, rounded.","zh":"版面堆疊的時候，這個高度的視窗裡舞台的高度（像素）：它的 58%，四捨五入。"},"description":[{"en":"When the shell stacks, the stage must stay the main thing, so it takes [`STAGE_SHARE`](#STAGE_SHARE) percent of the window height (the CSS uses `58vh`, and `58dvh` where the browser has it) and the panel gets the rest. This is the same number as a function, for tests and your own layout code.","zh":"外殼堆疊的時候，舞台必須仍然是主角，所以它佔視窗高度的 [`STAGE_SHARE`](#STAGE_SHARE) 百分比（CSS 用 `58vh`，瀏覽器支援的話用 `58dvh`），面板拿剩下的。這是同一個數字的函式版本，給測試和你自己的版面程式用。"}],"params":[{"name":"height","type":"Number","en":"The window height in pixels.","zh":"視窗高度，單位是像素。"}],"returns":{"type":"Number","en":"A number: `Math.round(height * 58 / 100)`.","zh":"一個數字：`Math.round(height * 58 / 100)`。"},"notes":[],"examples":[{"code":"import { stageHeight } from '@openav/show/layout';\n\nfor (const height of [667, 844, 1180]) console.log(height, stageHeight(height));   // 667 387, 844 490, 1180 684","caption":{"en":"The console prints `667 387`, `844 490` and `1180 684`.","zh":"主控台印出 `667 387`、`844 490` 和 `1180 684`。"},"run":"module","lang":"js"}],"related":["isStacked","STAGE_SHARE","STACK_QUERY","createShow"],"keywords":{"en":["stage height","stacked","layout","58%","vh"],"zh":["舞台高度","堆疊","版面","58%","vh"]},"tbd":[],"claims":[]},
{"id":"createCanvas","key":"createCanvas","name":"createCanvas","aliases":[],"kind":"function","package":"@openav/stage","category":"core","documented":true,"signature":"createCanvas(container, { alpha = false } = {})","import":"import { createCanvas } from '@openav/stage';","source":{"file":"packages/stage/index.js","line":75,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/stage/index.js#L75"},"summary":{"en":"Makes a 2D canvas that fills its container and keeps its size in step with it, at up to 2 times the device pixel ratio.","zh":"做出一個填滿容器、並隨容器大小調整的 2D 畫布，解析度最高到裝置像素比的 2 倍。"},"description":[{"en":"A convenience for worlds that draw with the 2D canvas API; a world may use it or ignore it. The canvas is `display: block` and as big as the container. `fit()` sets its pixel size to the container size times the device pixel ratio (never more than 2) and scales the context so you keep drawing in CSS pixels. Call `fit()` at the top of `render()`: it only does work when the size changed, and it returns `{ w, h }` to draw with. A `ResizeObserver` also calls it when the container is resized.","zh":"給用 2D 畫布 API 繪圖的世界的小幫手。世界可以用，也可以不用。畫布是 `display: block`，跟容器一樣大。`fit()` 把它的像素大小設成容器大小乘以裝置像素比（最多 2），並縮放 context，讓你繼續用 CSS 像素繪圖。在 `render()` 開頭呼叫 `fit()`：只有大小改變時它才會做事，而且會回傳畫圖要用的 `{ w, h }`。容器改變大小時，`ResizeObserver` 也會呼叫它。"},{"en":"Call `dispose()` in the world's `dispose()`: it stops the observer and removes the canvas.","zh":"在世界的 `dispose()` 裡呼叫 `dispose()`：它會停掉觀察器並移除畫布。"}],"params":[{"name":"container","type":"HTMLElement","en":"The element to put the canvas in (a world's `container`, usually).","zh":"要放畫布的元素（通常是世界的 `container`）。"},{"name":"alpha","type":"Boolean","optional":true,"default":"false","en":"Whether the canvas may be transparent. The default `false` is faster and gives an opaque canvas.","zh":"畫布可不可以是透明的。預設 `false` 比較快，畫布是不透明的。"}],"returns":{"type":"Object","en":"`{ canvas, ctx, fit, dispose }`: the `<canvas>`, its 2D context, `fit()` and `dispose()`. `fit()` returns `{ w, h }`, the container size in CSS pixels.","zh":"`{ canvas, ctx, fit, dispose }`：`<canvas>`、它的 2D context、`fit()` 和 `dispose()`。`fit()` 回傳 `{ w, h }`，也就是容器以 CSS 像素計的大小。"},"notes":[{"en":"If the container has no height (an empty `<div>` in normal flow is 0 pixels tall), the canvas is 0 × 0 and nothing shows. Give the container a size, or use it inside a [`Stage`](#Stage), whose layer fills the stage.","zh":"如果容器沒有高度（一般排版流程裡的空 `<div>` 高度是 0 像素），畫布就是 0 × 0，什麼都看不到。請給容器一個大小，或是用在 [`Stage`](#Stage) 裡，它的圖層會填滿舞台。"}],"examples":[{"code":"import { createCanvas } from '@openav/stage';\n\nconst container = document.getElementById('app');\ncontainer.style.cssText = 'width: 100%; height: 200px; background: #111;';\nconst view = createCanvas(container);\n\nfunction render(time) {\n  const { w, h } = view.fit();                         // the size in CSS pixels; cheap when nothing changed\n  view.ctx.clearRect(0, 0, w, h);\n  view.ctx.fillStyle = '#7ea6ff';\n  view.ctx.fillRect(w / 2 - 30 + Math.sin(time / 500) * (w / 3), h / 2 - 30, 60, 60);\n  requestAnimationFrame(render);\n}\nrequestAnimationFrame(render);","caption":{"en":"A blue square slides left and right across a canvas that fills a 200-pixel-high strip of the page, and stays sharp when the window is resized.","zh":"一個藍色方塊在填滿頁面 200 像素高橫條的畫布上左右滑動，視窗大小改變時依然清晰。"},"run":"module","lang":"js"}],"related":["Stage","createShow"],"keywords":{"en":["canvas","2d canvas","createCanvas","fit","resize","device pixel ratio","retina","dpr","renderer helper"],"zh":["畫布","2d 畫布","createCanvas","調整大小","縮放","裝置像素比","retina","繪圖小幫手"]},"tbd":[],"claims":[]},
{"id":"Stage","key":"Stage","name":"Stage","aliases":[],"kind":"class","package":"@openav/stage","category":"core","documented":true,"signature":"new Stage({ container, params, signals, io = {} })","import":"import { Stage } from '@openav/stage';","source":{"file":"packages/stage/index.js","line":21,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/stage/index.js#L21"},"summary":{"en":"Runs one world at a time: registers worlds and their params, switches between them, and each frame resolves the params and calls the world's `update` and `render`.","zh":"一次執行一個世界：登記世界和它們的參數、在世界之間切換，並在每個畫面解析參數、呼叫世界的 `update` 和 `render`。"},"description":[{"en":"A **world** is any system with this shape, and every method is optional:\n\n```js\n{\n  name: 'garden',                                   // unique: the stage registers worlds by name\n  params: [{ key: 'growth', min: 0, max: 1, def: 0.5 }],   // what it can be performed with\n  init({ container, signals, params }) {},           // when it becomes active (may be async)\n  update(dt, state, io) {},                          // every frame: state = the resolved params\n  render() {},                                       // every frame, right after update\n  dispose() {},                                      // when another world is activated\n}\n```\n\nThe stage owns the lifecycle and the param aggregation and deliberately **not** a renderer: draw with a 2D canvas, p5, three.js, SVG, the DOM, whatever you like. The shell never knows the work: switching works means swapping the world and its data, and the shell stays.","zh":"**世界**就是任何符合這個形狀的系統，每個方法都是選填的：\n\n```js\n{\n  name: 'garden',                                   // unique: the stage registers worlds by name\n  params: [{ key: 'growth', min: 0, max: 1, def: 0.5 }],   // what it can be performed with\n  init({ container, signals, params }) {},           // when it becomes active (may be async)\n  update(dt, state, io) {},                          // every frame: state = the resolved params\n  render() {},                                       // every frame, right after update\n  dispose() {},                                      // when another world is activated\n}\n```\n\n舞台負責生命週期和參數的彙整，而刻意**不**負責繪圖：用 2D 畫布、p5、three.js、SVG、DOM，隨你高興。外殼永遠不知道作品是什麼：換作品就是換掉世界和它的資料，外殼不動。"},{"en":"A world takes continuous control from `state` (the params) and may subscribe to `signals` for discrete events (a note struck, a chord resolved). If it reads `signals.get('midi/cc/74')` inside `update()`, it works with your controller today, but the timeline, a hand or another controller can no longer drive it. In a show, [`createShow()`](#createShow) builds the stage from its `world` and `worlds` options and returns it as `show.stage`.","zh":"世界從 `state`（參數）取得連續控制，可以訂閱 `signals` 來接離散事件（按下一個音、一個和弦解決）。如果它在 `update()` 裡讀 `signals.get('midi/cc/74')`，今天用你的控制器是行的，但時間軸、手或別的控制器就再也驅動不了它。在演出裡，[`createShow()`](#createShow) 依 `world` 和 `worlds` 選項建好舞台，用 `show.stage` 回傳。"}],"params":[{"name":"container","type":"HTMLElement","en":"The element the worlds draw into. It becomes `position: relative` if it was static. The stage adds its own layer inside it, so overlays you put in the container (hints, start buttons) are not wiped when a world is switched.","zh":"世界要畫進去的元素。如果它原本是 static，會被改成 `position: relative`。舞台會在裡面加一個自己的圖層，所以你放在容器裡的覆蓋物（提示、開始按鈕）在切換世界的時候不會被清掉。"},{"name":"params","type":"Params","en":"The [`Params`](#Params) the worlds' schemas are absorbed into, and that `frame()` resolves.","zh":"世界的 schema 要併進去、也是 `frame()` 要解析的 [`Params`](#Params)。"},{"name":"signals","type":"Signals","en":"The [`Signals`](#Signals) registry handed to every world, so it can subscribe to events.","zh":"交給每個世界的 [`Signals`](#Signals) 登錄表，讓世界可以訂閱事件。"},{"name":"io","type":"Object","optional":true,"default":"{}","en":"Extra output handles for the worlds' `update(dt, state, io)`, such as `{ midi, osc }`. `signals` is always there.","zh":"給世界的 `update(dt, state, io)` 用的額外輸出把手，例如 `{ midi, osc }`。`signals` 一定在裡面。"}],"needs":{"en":"Register your worlds, then `await stage.activate(name)` once; until a world is active, `frame()` returns `null` and does nothing. Then call `stage.frame(dt, baseState)` every frame (a show's loop does).","zh":"先登記你的世界，再 `await stage.activate(name)` 一次。在有世界啟用之前，`frame()` 會回傳 `null`，什麼都不做。之後每個畫面呼叫 `stage.frame(dt, baseState)`（演出的迴圈會做）。"},"notes":[{"en":"Check the name before you activate: `activate('nope')` disposes the current world **first** and only then rejects, and the stage keeps pointing at the disposed world (`active` and `activeName` do not change), so the next `frame()` still calls its `update()` and `render()`.","zh":"啟用之前請先確認名字：`activate('nope')` 會**先**讓目前的世界 dispose，之後才被拒絕，而舞台仍然指著那個已經 dispose 的世界（`active` 和 `activeName` 沒有改變），所以下一次 `frame()` 還是會呼叫它的 `update()` 和 `render()`。"},{"en":"Fields to read: `active` (the active world object), `activeName`, `worlds` (a `Map` of name → world), `container`, `layer` (the element worlds are placed in) and `io` (`{ signals, ...io }`, what `update` receives; add handles later with `stage.io.osc = osc`).","zh":"可以讀的欄位：`active`（目前啟用的世界物件）、`activeName`、`worlds`（名字對應世界的 `Map`）、`container`、`layer`（世界被放進去的元素），以及 `io`（`{ signals, ...io }`，也就是 `update` 收到的東西，之後可以用 `stage.io.osc = osc` 補上把手）。"},{"en":"An exception in `init()` is not caught: `activate()` rejects. Exceptions in `update()` and `render()` are caught. A world must release what it takes (GPU objects, DOM, subscriptions) in `dispose()`, because worlds get switched live.","zh":"`init()` 裡的例外不會被接住：`activate()` 會被拒絕。`update()` 和 `render()` 裡的例外會被接住。世界必須在 `dispose()` 裡釋放它拿走的東西（GPU 物件、DOM、訂閱），因為世界是可以現場切換的。"}],"examples":[{"code":"import { Signals, Params } from '@openav/core';\nimport { Stage } from '@openav/stage';\n\nconst container = document.getElementById('app');\ncontainer.style.cssText = 'width: 400px; height: 120px;';          // give the stage some room\n\nconst stage = new Stage({ container, params: new Params(), signals: new Signals() });\nstage.register({\n  name: 'label',\n  params: [{ key: 'size', min: 10, max: 60, def: 20 }],\n  init({ container }) { this.el = document.createElement('div'); container.appendChild(this.el); },\n  update(dt, state) { this.size = state.size; },\n  render() { this.el.style.cssText = 'color: #7ea6ff; font: ' + this.size + 'px monospace'; this.el.textContent = 'size ' + this.size; },\n  dispose() { this.el.remove(); },\n});\nawait stage.activate('label');                      // init runs now\nconsole.log(stage.frame(0.016, { size: 40 }));      // { size: 40 }: the base state, since nothing overrides it\nconsole.log(stage.frame(0.016, {}));                // { size: 20 }: no base either, so def","caption":{"en":"The console prints `{ size: 40 }`, then `{ size: 20 }`, and the page shows the text `size 20` in blue: the world drew itself from the resolved params.","zh":"主控台印出 `{ size: 40 }`，然後是 `{ size: 20 }`，頁面上用藍色顯示文字 `size 20`：世界依解析後的參數畫出了自己。"},"run":"module","lang":"js"},{"code":"import { Signals, Params } from '@openav/core';\nimport { Stage } from '@openav/stage';\n\nconst signals = new Signals();\nconst stage = new Stage({ container: document.getElementById('app'), params: new Params(), signals });\nconst world = (name) => ({\n  name, params: [{ key: 'calm', min: 0, max: 1, def: 0.5 }],\n  init() { console.log(name, 'init'); },\n  dispose() { console.log(name, 'dispose'); },\n});\nstage.register(world('calm')).register(world('storm'));\nawait stage.activate('calm');                  // calm init\nawait stage.activate('storm');                 // calm dispose, then storm init\nawait stage.activate('storm');                 // already active: nothing happens\nconsole.log(stage.activeName, stage.params.schema.length);     // storm 1: both worlds share the one 'calm' param\nawait stage.activate('nope').catch((error) => console.log(error.message));   // unknown world: storm is disposed first, then this rejects","caption":{"en":"The console prints `calm init`, `calm dispose`, `storm init`, then `storm 1` (the shared param is registered once), then `storm dispose` and the error message for an unknown world: the failed switch has already disposed the world that was playing.","zh":"主控台印出 `calm init`、`calm dispose`、`storm init`，然後是 `storm 1`（共用的參數只登記一次），接著是 `storm dispose` 和未知世界的錯誤訊息：失敗的切換已經先讓正在跑的世界 dispose 了。"},"run":"module","lang":"js"}],"related":["createCanvas","createShow","Params","Signals","webtoeWorld","Timeline"],"keywords":{"en":["stage","world","worlds","lifecycle","init","update","render","dispose","activate","register","frame","renderer"],"zh":["舞台","世界","生命週期","初始化","更新","繪製","釋放","啟用","登記","影格","繪圖器"]},"tbd":[],"claims":[],"members":[{"name":"register","member":"method","signature":"world","doc":"Register a world and absorb its param schema.","line":46},{"name":"activate","member":"method","signature":"name","doc":"Switch active world (disposes the previous one).","line":53},{"name":"frame","member":"method","signature":"dt, baseState","doc":"Per-frame: resolve params → world.update → world.render.","line":65}]},
{"id":"webtoeWorld","key":"webtoeWorld","name":"webtoeWorld","aliases":[],"kind":"function","package":"@openav/world-webtoe","category":"core","documented":true,"signature":"webtoeWorld({ name = 'webtoe', app = 'https://webtoe.openaudiovisual.com/', project = null, params = [], extra = null, } = {})","import":"import { webtoeWorld } from '@openav/world-webtoe';","source":{"file":"packages/world-webtoe/index.js","line":20,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/world-webtoe/index.js#L20"},"summary":{"en":"Turns a WebToe node patch into a world: the patch runs in an iframe and every changed param is posted into it.","zh":"把一份 WebToe 節點圖變成世界：節點圖在 iframe 裡執行，每個改變的參數都會被送進去。"},"description":[{"en":"[WebToe](https://webtoe.openaudiovisual.com/) is the sister project: a web-native, node-based dataflow engine that speaks TouchDesigner. `webtoeWorld()` embeds its app in an iframe that fills the stage (with `allow=\"camera; microphone\"`), and each frame it posts the resolved params into the iframe as `{ type: 'webtoe:ext', values }`. Inside the patch, any parameter written as `ext('name', fallback)` follows the value called `name`, and uses `fallback` until one arrives.","zh":"[WebToe](https://webtoe.openaudiovisual.com/) 是姊妹專案：一個網頁原生、以節點為基礎的資料流引擎，講的是 TouchDesigner 的語言。`webtoeWorld()` 把它的應用程式嵌進一個填滿舞台的 iframe（帶著 `allow=\"camera; microphone\"`），每個畫面把解析後的參數用 `{ type: 'webtoe:ext', values }` 送進 iframe。節點圖裡，任何寫成 `ext('name', fallback)` 的參數，都會跟著名為 `name` 的值走，在值抵達之前用 `fallback`。"},{"en":"The contract stays pure: the world reads params and never inputs. Which knob, hand or timeline drives `speed` is the mapper's business, as for any world. To send less, the world posts only when at least one value differs from the last post.","zh":"約定維持純粹：世界讀的是參數，從不讀輸入。是哪個旋鈕、手或時間軸在驅動 `speed`，是映射的事，跟任何世界一樣。為了少送一點，世界只在至少有一個值跟上一次送出的不同時才會送。"}],"params":[{"name":"name","type":"String","optional":true,"default":"'webtoe'","en":"The world's name: what [`Stage`](#Stage) registers it under and what you pass to `stage.activate()`. Give each patch its own.","zh":"世界的名字：[`Stage`](#Stage) 用它來登記，`stage.activate()` 也是傳它。每份節點圖請取自己的名字。"},{"name":"app","type":"String","optional":true,"default":"'https://webtoe.openaudiovisual.com/'","en":"The WebToe app to embed in the iframe. Change it to run your own copy of the app.","zh":"要嵌進 iframe 的 WebToe 應用程式。改它就能跑你自己那一份。"},{"name":"project","type":"String | null","optional":true,"default":"null","en":"The address of a `.webtoe.json` patch file. It is handed to the app as `?project=<address>`, and the app fetches it, so the file's server must allow the app's origin (CORS). Make the address absolute, for example `new URL('./garden.webtoe.json', location.href).href`.","zh":"`.webtoe.json` 節點圖檔案的網址。它會用 `?project=<網址>` 交給應用程式，由應用程式去抓，所以檔案所在的伺服器必須允許應用程式的來源（CORS）。請寫成絕對網址，例如 `new URL('./garden.webtoe.json', location.href).href`。"},{"name":"params","type":"Array","optional":true,"default":"[]","en":"The param schema of the world: `[{ key, min, max, def }, …]`. The keys are the names the patch reads with `ext('name', fallback)`.","zh":"世界的參數 schema：`[{ key, min, max, def }, …]`。這些 key 就是節點圖用 `ext('name', fallback)` 讀取的名稱。"},{"name":"extra","type":"Function | null","optional":true,"default":"null","en":"`extra(state, out)`, called every frame: add derived values to `out` (a copy of the state) that the patch can read too, such as `out.hueRad = state.hue * Math.PI / 180`.","zh":"`extra(state, out)`，每個畫面呼叫：把衍生的值加進 `out`（狀態的複本），節點圖也讀得到，例如 `out.hueRad = state.hue * Math.PI / 180`。"}],"returns":{"type":"Object","en":"A world `{ name, params, init, update, render, dispose }` to pass to `createShow({ world })` or `stage.register()`.","zh":"一個世界 `{ name, params, init, update, render, dispose }`，可以交給 `createShow({ world })` 或 `stage.register()`。"},"needs":{"en":"The page must be able to load the WebToe app in an iframe (the network, by default `https://webtoe.openaudiovisual.com/`), and the patch file must be reachable by the app. The world does its work when it is activated, so use it with `createShow({ world })`, or `stage.register()` and `stage.activate()`.","zh":"頁面必須能把 WebToe 應用程式載進 iframe（要有網路，預設是 `https://webtoe.openaudiovisual.com/`），節點圖檔案也要讓應用程式抓得到。世界在被啟用的時候才開始做事，所以請搭配 `createShow({ world })`，或 `stage.register()` 加 `stage.activate()` 使用。"},"notes":[{"en":"The message goes to the iframe with the target origin `*`, so any page the iframe happens to be showing receives the values. If you point `app` at a site you do not trust, it can read your params.","zh":"訊息是用目標來源 `*` 送給 iframe，所以 iframe 當時顯示的任何頁面都收得到這些值。如果你把 `app` 指向你不信任的網站，它就能讀到你的參數。"}],"examples":[{"code":"import { webtoeWorld } from '@openav/world-webtoe';\n\n// a stand-in for the WebToe app, so this needs no network: it just shows what it receives\nconst patch = '<body style=\"margin:0;background:#111;color:#7ea6ff;font:14px monospace\"></body>'\n  + '<script>onmessage = (e) => { document.body.textContent = JSON.stringify(e.data); };<\\/script>';\nconst world = webtoeWorld({\n  app: 'data:text/html,' + encodeURIComponent(patch),\n  params: [{ key: 'speed', min: 0, max: 1, def: 0.5 }, { key: 'hue', min: 0, max: 360, def: 205 }],\n  extra: (state, out) => { out.hueRad = Math.round(state.hue * Math.PI / 180 * 100) / 100; },\n});\nconst container = document.getElementById('app');\ncontainer.style.cssText = 'position: relative; width: 400px; height: 120px;';\nworld.init({ container });\ncontainer.querySelector('iframe').onload = () => world.update(0.016, { speed: 0.8, hue: 180 });","caption":{"en":"An iframe fills the container and, once it has loaded, shows the message the world posted: `{\"type\":\"webtoe:ext\",\"values\":{\"speed\":0.8,\"hue\":180,\"hueRad\":3.14}}`.","zh":"iframe 填滿容器，載入完成之後顯示世界送出的訊息：`{\"type\":\"webtoe:ext\",\"values\":{\"speed\":0.8,\"hue\":180,\"hueRad\":3.14}}`。"},"run":"module","lang":"js"},{"code":"import { createShow } from '@openav/show';\nimport { webtoeWorld } from '@openav/world-webtoe';\n\nconst world = webtoeWorld({\n  name: 'garden-patch',\n  project: new URL('./garden.webtoe.json', location.href).href,   // the app fetches it: CORS must allow it\n  params: [\n    { key: 'speed', min: 0, max: 1, def: 0.5 },\n    { key: 'hue', min: 0, max: 360, def: 205 },\n  ],\n});\nconst show = await createShow({ world, routes: [{ source: 'midi/cc/1', target: 'speed' }] });","caption":{"en":"The patch from `garden.webtoe.json` fills the stage, and turning MIDI knob 1 changes its `speed` through the mapper. Every `ext('speed', …)` in the patch follows.","zh":"`garden.webtoe.json` 的節點圖填滿舞台，轉動 MIDI 旋鈕 1 會透過映射改變它的 `speed`。節點圖裡每個 `ext('speed', …)` 都會跟著動。"},"run":"static","lang":"js"}],"related":["Stage","createShow","Params","Mapper"],"keywords":{"en":["webtoe","touchdesigner","node patch","iframe world","ext","dataflow","patch","world","postMessage"],"zh":["webtoe","touchdesigner","節點圖","iframe 世界","ext","資料流","世界","postMessage"]},"tbd":[{"en":"Not run against the real WebToe app here (the examples use a stand-in page, with no network). Because a value is only posted when it changed, a patch that finishes loading after the first frame may not receive values that never move again until one of them changes; whether that happens with the live app was not checked.","zh":"這裡沒有對真正的 WebToe 應用程式跑過（範例用的是一個代替的頁面，不需要網路）。因為值只有在改變時才會送出，在第一個畫面之後才載入完成的節點圖，對於之後再也不動的值，可能要等到其中一個值改變才會收到。真正的應用程式會不會發生這種情況，沒有查證。"}],"claims":[]},
{"id":"aliasOf","key":"aliasOf","name":"aliasOf","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"aliasOf(name, alias = 'any')","import":"import { aliasOf } from '@openav/relay';","source":{"file":"packages/relay/signals.js","line":32,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/signals.js#L32"},"summary":{"en":"The \"latest phone\" name of a phone signal: `aliasOf('phone/ab12/tilt/x')` is `'phone/any/tilt/x'`. `null` for anything else.","zh":"手機訊號的「最新那支手機」名稱：`aliasOf('phone/ab12/tilt/x')` 是 `'phone/any/tilt/x'`。其他的名稱回傳 `null`。"},"description":[{"en":"A route written in a World file cannot know the id of a stranger's phone. The alias fixes that: every phone signal is also filed under `phone/any/…`, which always holds the value from whichever phone sent last, so `{ source: 'phone/any/tilt/x', target: 'hue' }` can be written before anyone has joined. [`bindSignals()`](#bindSignals) and [`localSensors()`](#localSensors) both file the alias for you.","zh":"寫在世界檔案裡的路由，不可能知道陌生人手機的 id。別名就是用來解決這件事：每個手機訊號也會歸一份到 `phone/any/…`，它永遠是最後送出數值的那支手機的值，所以 `{ source: 'phone/any/tilt/x', target: 'hue' }` 在還沒有人加入之前就能寫好。[`bindSignals()`](#bindSignals) 和 [`localSensors()`](#localSensors) 都會替你歸這份別名。"}],"params":[{"name":"name","type":"String","en":"A signal name. Only `phone/<id>/…` has an alias.","zh":"訊號名稱。只有 `phone/<id>/…` 有別名。"},{"name":"alias","type":"String","optional":true,"en":"The word that replaces the id. Leave it out for `any`.","zh":"用來取代 id 的字。不給就是 `any`。"}],"returns":{"type":"String | null","en":"The alias name, or `null` when `name` is not a phone signal or already uses the alias.","zh":"別名。`name` 不是手機訊號、或本來就用別名的時候是 `null`。"},"notes":[],"examples":[{"code":"import { aliasOf } from '@openav/relay';\n\ndocument.getElementById('app').innerHTML = [\n  aliasOf('phone/ab12/tilt/x'),\n  aliasOf('phone/any/tilt/x'),\n  aliasOf('midi/note/on'),\n  aliasOf('phone/ab12/knock', 'latest'),\n].map(String).join('<br>');","caption":{"en":"Prints phone/any/tilt/x, then null twice (already an alias; not a phone signal), then phone/latest/knock.","zh":"印出 phone/any/tilt/x，然後兩個 null（已經是別名、不是手機訊號），最後是 phone/latest/knock。"},"run":"module","lang":"js"}],"related":["bindSignals","localSensors","fileSignal"],"keywords":{"en":["alias","phone/any","latest phone","device id","route","stranger"],"zh":["別名","phone/any","最新的手機","裝置 id","路由","陌生人"]},"tbd":[],"claims":[]},
{"id":"attachRelay","key":"attachRelay","name":"attachRelay","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"attachRelay(server, { path = '/relay', log = () => {}, heartbeatMs = 20000 } = {})","import":"import { attachRelay } from '@openav/relay/server';","source":{"file":"packages/relay/server.js","line":20,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/server.js#L20"},"summary":{"en":"Attaches the room relay to a Node `http.Server`: WebSocket clients that connect to `path` are put in rooms and routed by role. Returns `{ hub, close() }`.","zh":"把房間中繼掛到 Node 的 `http.Server` 上：連到 `path` 的 WebSocket 用戶端會被放進房間，並依身分轉送訊息。回傳 `{ hub, close() }`。"},"description":[{"en":"This is the server half, in Node, with no dependencies (the WebSocket codec is in the package). `node serve.js` already does it for you at `/relay`. To use it in your own server: `attachRelay(server)` before or after `server.listen()`. Clients connect to `ws://host/relay?role=controller|runner|monitor&room=<name>&id=<device>`: a bad role becomes `controller`, a bad room name becomes `default`, and the id is cut to 64 characters.","zh":"這是伺服器那一半，跑在 Node 裡，沒有任何相依套件（WebSocket 編解碼器就在套件裡）。`node serve.js` 已經在 `/relay` 替你做好了。要用在你自己的伺服器上：在 `server.listen()` 之前或之後呼叫 `attachRelay(server)` 都行。用戶端連到 `ws://host/relay?role=controller|runner|monitor&room=<name>&id=<device>`：身分不合格會變成 `controller`，房間名不合格會變成 `default`，id 會被截成 64 個字元。"},{"en":"Who may talk to whom: a controller's `signal` and `batch` go to the runner and the monitors; a runner's go to the monitors only; a runner's `feedback` and `config` go to the controllers and monitors; a monitor says nothing. A `config` is kept per key and replayed to controllers that join later, and the room forgets it when its runner leaves. Rooms never hear each other. A message over 256 KB is ignored and a frame over 1 MB closes the connection.","zh":"誰能對誰說話：控制端的 `signal` 和 `batch` 送給演出端和監看端。演出端的只送給監看端。演出端的 `feedback` 和 `config` 送給控制端和監看端。監看端什麼都不能說。`config` 依 key 留著，重播給之後才加入的控制端，演出端離開時房間就忘掉它。房間之間互相聽不到。超過 256 KB 的訊息會被忽略，超過 1 MB 的 frame 會讓連線關閉。"},{"en":"There is no authentication: anyone who can reach the server and knows a room name can join as a controller. Use a closed network, or a room name nobody will guess.","zh":"沒有任何身分驗證：任何連得到伺服器、又知道房間名稱的人，都能以控制端身分加入。請用封閉的網路，或一個沒人猜得到的房間名稱。"}],"params":[{"name":"server","type":"http.Server","en":"A Node `http.Server` (or anything with the same `upgrade` event, such as an https server). The relay listens for its `upgrade` events.","zh":"Node 的 `http.Server`（或任何有同樣 `upgrade` 事件的東西，例如 https 伺服器）。中繼會聽它的 `upgrade` 事件。"},{"name":"path","type":"String","optional":true,"en":"The URL path clients connect to. Any other path is refused.","zh":"用戶端要連的網址路徑。其他路徑一律拒絕。"},{"name":"log","type":"Function","optional":true,"en":"`(line) => {}`, called with a line such as `+ controller ab12 @main` when a client joins and `- controller ab12 @main` when it leaves. The default says nothing.","zh":"`(line) => {}`，用戶端加入時以 `+ controller ab12 @main` 這樣的一行呼叫，離開時是 `- controller ab12 @main`。預設什麼都不說。"},{"name":"heartbeatMs","type":"Number","optional":true,"en":"How often to ping every client, in milliseconds. A client that has not answered by the next ping is dropped, so a dead phone goes within one to two periods.","zh":"多久對每個用戶端 ping 一次，單位毫秒。到下一次 ping 還沒回應的用戶端會被移除，所以斷線的手機會在一到兩個週期內消失。"}],"returns":{"type":"{ hub, close }","en":"`hub` is the `RoomHub` (`hub.rooms` is a `Map` of the rooms; `hub.counts(room)` gives `{ controllers, runners, monitors }`). `close()` drops every connected client; it does not close your server.","zh":"`hub` 是 `RoomHub`（`hub.rooms` 是房間的 `Map`，`hub.counts(room)` 回傳 `{ controllers, runners, monitors }`）。`close()` 把所有連線中的用戶端斷開，不會關掉你的伺服器。"},"needs":{"en":"A Node program, imported from `@openav/relay/server` (a different entry point from `@openav/relay`, which is the browser side). Standalone: `node packages/relay/server.js [port]` (default 7458; `/health` answers with the room names; `RELAY_HOST=127.0.0.1` binds to localhost behind a reverse proxy).","zh":"一支 Node 程式，從 `@openav/relay/server` 匯入（它跟 `@openav/relay` 是不同的進入點，後者是瀏覽器端）。單獨執行：`node packages/relay/server.js [port]`（預設 7458，`/health` 會回傳房間名稱，用 `RELAY_HOST=127.0.0.1` 可以綁在 localhost，後面接反向代理）。"},"notes":[],"examples":[{"code":"import http from 'node:http';\nimport { attachRelay } from '@openav/relay/server';\n\nconst server = http.createServer((req, res) => { res.writeHead(404); res.end(); });\nconst relay = attachRelay(server, { log: (line) => console.log('[relay]', line) });\nserver.listen(7458, () => console.log('relay at ws://localhost:7458/relay'));\n\n// later: relay.hub.counts('main') → { controllers: 1, runners: 1, monitors: 0 }","caption":{"en":"A relay on port 7458 in about five lines. Open a show page with `?relay=ws://localhost:7458/relay` and the terminal logs a line such as `[relay] + runner show-k3x9 @default` when it connects.","zh":"五行左右就有一個跑在 7458 埠的中繼。打開演出頁面時加上 `?relay=ws://localhost:7458/relay`，頁面連上去的時候，終端機會印出像 `[relay] + runner show-k3x9 @default` 這樣的一行。"},"run":"static","lang":"js"}],"related":["RelayClient","bindSignals","mountRemoteHost"],"keywords":{"en":["relay server","websocket server","http server","rooms","node","attach","serve.js","heartbeat","hub"],"zh":["中繼伺服器","websocket 伺服器","http 伺服器","房間","node","掛上","serve.js","心跳","hub"]},"tbd":[],"claims":[]},
{"id":"bindSignals","key":"bindSignals","name":"bindSignals","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"bindSignals(client, signals, { alias = 'any', meta = signalMeta } = {})","import":"import { bindSignals } from '@openav/relay';","source":{"file":"packages/relay/signals.js","line":65,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/signals.js#L65"},"summary":{"en":"Connects a runner's `RelayClient` to a `Signals` registry: every signal a phone sends is filed there, plus a `phone/any/…` copy. Returns an undo function.","zh":"把演出端的 `RelayClient` 接到 `Signals`：手機送來的每個訊號都歸進去，並多一份 `phone/any/…` 的複本。回傳一個復原函式。"},"description":[{"en":"Call it once on the show page, after making the runner's client: `bindSignals(show, signals)`. It sets `client.onSignal` so that each signal that arrives goes through [`fileSignal()`](#fileSignal), and a phone signal is filed twice, under its own name (`phone/ab12/tilt/x`) and under the alias (`phone/any/tilt/x`). Routes written at author time use the alias.","zh":"在演出頁面上，做好演出端的用戶端之後呼叫一次：`bindSignals(show, signals)`。它會設定 `client.onSignal`，讓每個收到的訊號都經過 [`fileSignal()`](#fileSignal)，手機訊號會歸兩份：自己的名稱（`phone/ab12/tilt/x`）和別名（`phone/any/tilt/x`）。寫作品時寫的路由用的是別名。"},{"en":"A callback that was already in `client.onSignal` keeps running after the filing, so you can still watch the raw messages.","zh":"`client.onSignal` 裡原本就有的回呼，會在歸檔之後繼續執行，所以你還是看得到原始訊息。"}],"params":[{"name":"client","type":"RelayClient","en":"The runner's client (any object with an `onSignal` field works).","zh":"演出端的用戶端（任何有 `onSignal` 欄位的物件都行）。"},{"name":"signals","type":"Signals","en":"The registry to fill (from `@openav/core`).","zh":"要填的註冊表（來自 `@openav/core`）。"},{"name":"alias","type":"String | null","optional":true,"en":"The word used in the copy, `phone/<alias>/…`. `null` or an empty string turns the copy off.","zh":"複本用的字，`phone/<alias>/…`。`null` 或空字串表示不做複本。"},{"name":"meta","type":"Function","optional":true,"en":"Replace [`signalMeta`](#signalMeta) with your own declarations, as in [`fileSignal()`](#fileSignal).","zh":"像 [`fileSignal()`](#fileSignal) 一樣，用你自己的宣告取代 [`signalMeta`](#signalMeta)。"}],"returns":{"type":"Function","en":"Call it to unbind: `client.onSignal` goes back to what it was before.","zh":"呼叫它就解除綁定：`client.onSignal` 回到綁定之前的樣子。"},"notes":[],"examples":[{"code":"import { bindSignals } from '@openav/relay';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst client = {};                               // anything with an onSignal field; a RelayClient in real use\nconst unbind = bindSignals(client, signals);\n\n// what the relay calls when a phone's message arrives\nclient.onSignal('phone/ab12/tilt/x', 0.4, {});\nclient.onSignal('phone/ab12/knock', { strength: 1 }, { pulse: true });\nunbind();                                        // client.onSignal is back to what it was\n\ndocument.getElementById('app').innerHTML = [\n  'phone/any/tilt/x = ' + signals.get('phone/any/tilt/x'),\n  'knock kind: ' + signals.meta.get('phone/any/knock').kind,\n  'unbound: ' + (client.onSignal === undefined),\n].join('<br>');","caption":{"en":"Prints the tilt under the alias (0.4), the knock declared as a pulse, and \"true\" once `unbind()` has put `onSignal` back.","zh":"印出別名底下的傾斜值（0.4）、被宣告成脈衝的 knock，以及 `unbind()` 把 `onSignal` 放回去之後的「true」。"},"run":"module","lang":"js"},{"code":"import { RelayClient, bindSignals } from '@openav/relay';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst show = new RelayClient({ role: 'runner', room: 'main' });\nconst seen = [];\nshow.onSignal = (name) => seen.push(name);       // our own listener first\nbindSignals(show, signals);                      // it still runs after the filing\nshow.onSignal('phone/ab12/tilt/y', 0.2, {});\n\ndocument.getElementById('app').textContent = seen.join(', ') + ' | ' + signals.get('phone/any/tilt/y');","caption":{"en":"A listener that was there before `bindSignals()` still hears every message: prints \"phone/ab12/tilt/y | 0.2\".","zh":"`bindSignals()` 之前就有的監聽函式，照樣聽得到每一則訊息：印出「phone/ab12/tilt/y | 0.2」。"},"run":"module","lang":"js"}],"related":["RelayClient","fileSignal","aliasOf","signalMeta","mountRemoteHost"],"keywords":{"en":["bind signals","connect relay to signals","phone/any","runner","show","receive phone","onSignal"],"zh":["綁定訊號","接中繼","phone/any","演出端","接收手機","onSignal"]},"tbd":[],"claims":[]},
{"id":"deviceId","key":"deviceId","name":"deviceId","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"deviceId(key = 'lab.deviceId')","import":"import { deviceId } from '@openav/relay';","source":{"file":"packages/relay/index.js","line":27,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/index.js#L27"},"summary":{"en":"The short random id of this browser, kept in `localStorage`, that names this phone in its signals, as in `phone/ab12/tilt/x`.","zh":"這個瀏覽器的隨機短 id，存在 `localStorage` 裡，用來在訊號名稱裡稱呼這支手機，例如 `phone/ab12/tilt/x`。"},"description":[{"en":"The first call makes an id and stores it under `key`; later calls read it back. [`RelayClient`](#RelayClient) uses it as its default `id`, so a phone keeps the same name across reloads and its signals stay `phone/<same id>/…`.","zh":"第一次呼叫會做一個 id 並存在 `key` 底下，之後的呼叫就把它讀回來。[`RelayClient`](#RelayClient) 把它當作預設的 `id`，所以手機重新載入之後名字不變，訊號也一直是 `phone/<同一個 id>/…`。"},{"en":"If `localStorage` is blocked (some private windows), nothing can be stored and every call makes a new id.","zh":"如果 `localStorage` 被擋掉（有些無痕視窗），就存不下來，每次呼叫都會做一個新的 id。"}],"params":[{"name":"key","type":"String","en":"The `localStorage` key. The default is the one the lab uses, so `/control` and `/remote` on one phone count as one device.","zh":"`localStorage` 的 key。預設是實驗室用的那一個，所以同一支手機上的 `/control` 和 `/remote` 算是同一個裝置。"}],"returns":{"type":"String","en":"A short id of about five letters and digits, such as `k3x9a`. The same one on every call, in the same browser.","zh":"大約五個字母和數字的短 id，例如 `k3x9a`。同一個瀏覽器裡，每次呼叫都一樣。"},"notes":[],"examples":[{"code":"import { deviceId } from '@openav/relay';\n\nconst first = deviceId();\nconst again = deviceId();\ndocument.getElementById('app').textContent =\n  `id ${first}, the same on the next call: ${first === again}`;","caption":{"en":"Prints a short id and \"true\": the second call reads back what the first one stored.","zh":"印出一個短 id 和「true」：第二次呼叫讀回第一次存下的值。"},"run":"module","lang":"js"}],"related":["RelayClient","PhoneSensors"],"keywords":{"en":["device id","phone id","localStorage","identity","unique id","random id"],"zh":["裝置 id","手機 id","localStorage","識別","隨機 id"]},"tbd":[],"claims":[]},
{"id":"fileSignal","key":"fileSignal","name":"fileSignal","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"fileSignal(signals, name, value, { pulse = false, meta = signalMeta, source = '' } = {})","import":"import { fileSignal } from '@openav/relay';","source":{"file":"packages/relay/signals.js","line":55,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/signals.js#L55"},"summary":{"en":"Files one incoming signal into a `Signals` registry: declares the name the first time, fills in the velocity, then sets it or pulses it.","zh":"把一個收到的訊號歸進 `Signals`：第一次聽到名稱就先宣告，補上力度，再設值或觸發脈衝。"},"description":[{"en":"This is the receiving half of the wire format, and every receiver files a message the same way through it: [`bindSignals()`](#bindSignals) for a show that has a relay, [`localSensors()`](#localSensors) for a show on a phone, and a host that runs its own socket. In order: if the registry has never heard the name, declare it with what [`signalMeta()`](#signalMeta) says (a pulse is never mistaken for a knob, and meters get their range); fill in the missing velocity spelling with [`normalizeValue()`](#normalizeValue); then `signals.pulse(name, value)` for a pulse or `signals.set(name, value)` for the rest.","zh":"這是傳輸格式的接收那一半，每個接收端都靠它用同樣的方式歸檔訊息：有中繼的演出用 [`bindSignals()`](#bindSignals)，跑在手機上的演出用 [`localSensors()`](#localSensors)，自己開 socket 的主機也一樣。順序是：註冊表沒聽過這個名稱的話，就照 [`signalMeta()`](#signalMeta) 的說法宣告（脈衝不會被當成旋鈕，音量表也拿得到範圍），用 [`normalizeValue()`](#normalizeValue) 補上缺的力度寫法，然後脈衝就 `signals.pulse(name, value)`，其他就 `signals.set(name, value)`。"}],"params":[{"name":"signals","type":"Signals","en":"The registry to file it in (from `@openav/core`).","zh":"要歸進去的註冊表（來自 `@openav/core`）。"},{"name":"name","type":"String","en":"The signal name. It must be a non-empty string of at most 128 characters, or nothing is filed.","zh":"訊號名稱。必須是非空字串，最長 128 個字元，否則什麼都不歸。"},{"name":"value","type":"any","en":"The payload.","zh":"訊號的內容。"},{"name":"pulse","type":"Boolean","optional":true,"en":"True when the message was sent as an event. A name that [`signalMeta()`](#signalMeta) knows to be a pulse (or was declared as one) is a pulse either way.","zh":"訊息是當作事件送來的話為 true。名稱若是 [`signalMeta()`](#signalMeta) 認得的脈衝（或之前就宣告成脈衝），不管這一項是什麼，都當作脈衝。"},{"name":"meta","type":"Function","optional":true,"en":"Replace [`signalMeta`](#signalMeta) with your own `(name, { pulse }) => metadata | null`, to declare names the relay does not know.","zh":"用你自己的 `(name, { pulse }) => 資料 | null` 取代 [`signalMeta`](#signalMeta)，用來宣告中繼不認得的名稱。"},{"name":"source","type":"String","optional":true,"en":"A label stored in the declared metadata (`signals.meta.get(name).source`), such as `'relay'` or `'local'`.","zh":"存進宣告資料裡的標籤（`signals.meta.get(name).source`），例如 `'relay'` 或 `'local'`。"}],"returns":{"type":"Boolean","en":"`true` when it was filed, `false` for a bad name.","zh":"歸好了是 `true`，名稱不合格是 `false`。"},"notes":[],"examples":[{"code":"import { fileSignal } from '@openav/relay';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nfileSignal(signals, 'midi/note/on', { note: 64, velocity: 100 });   // a known pulse name\nfileSignal(signals, 'phone/ab12/tilt/x', 0.3, { source: 'relay' });\nconst bad = fileSignal(signals, '', 1);\n\ndocument.getElementById('app').innerHTML = [\n  'note vel: ' + signals.get('midi/note/on').vel.toFixed(2),\n  'note kind: ' + signals.meta.get('midi/note/on').kind,\n  'tilt range: ' + signals.meta.get('phone/ab12/tilt/x').min + '..' + signals.meta.get('phone/ab12/tilt/x').max,\n  'empty name filed: ' + bad,\n].join('<br>');","caption":{"en":"Prints a velocity of 0.79 (100 / 127) that the sender never wrote, the kind \"pulse\", the range -1..1 for tilt, and \"false\" for the empty name.","zh":"印出送訊號的人沒寫過的力度 0.79（100 / 127）、種類「pulse」、傾斜的範圍 -1..1，以及空名稱的「false」。"},"run":"module","lang":"js"}],"related":["bindSignals","signalMeta","normalizeValue","localSensors","aliasOf"],"keywords":{"en":["file signal","receive","declare","signals","incoming","pulse","set","wire format"],"zh":["歸檔訊號","接收","宣告","signals","收到的訊號","脈衝","設值","傳輸格式"]},"tbd":[],"claims":[]},
{"id":"normalizeValue","key":"normalizeValue","name":"normalizeValue","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"normalizeValue(name, value)","import":"import { normalizeValue } from '@openav/relay';","source":{"file":"packages/relay/signals.js","line":43,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/signals.js#L43"},"summary":{"en":"Fills in the missing velocity spelling of a note message: `vel` (0..1) or `velocity` (1..127). Every other message passes through unchanged.","zh":"補上音符訊息缺的那一種力度寫法：`vel`（0..1）或 `velocity`（1..127）。其他訊息原樣通過。"},"description":[{"en":"`@openav/midi` publishes `vel` as 0..1, a hardware keyboard talks in `velocity` 1..127, and senders (an old phone page, a TouchOSC bridge, a sequencer) give one or the other. A sketch is written against one spelling, so the receiving end fills in the other: `velocity` becomes `vel = velocity / 127`, and `vel` becomes `velocity = Math.round(vel * 127)`.","zh":"`@openav/midi` 發布的 `vel` 是 0..1，硬體鍵盤說的是 1..127 的 `velocity`，而送訊號的人（舊的手機頁面、TouchOSC 橋接、編曲軟體）只會給其中一種。草圖通常只照一種寫法寫，所以接收端把另一種補上：`velocity` 會變成 `vel = velocity / 127`，`vel` 會變成 `velocity = Math.round(vel * 127)`。"},{"en":"You rarely call it yourself: [`fileSignal()`](#fileSignal) does it for every message it files.","zh":"通常不用自己呼叫：[`fileSignal()`](#fileSignal) 歸檔每一則訊息時都會做這件事。"}],"params":[{"name":"name","type":"String","en":"The signal name. Only `midi/note/on`, `midi/note/off` and the same with a device in the middle (`midi/<device>/note/on`) are touched.","zh":"訊號名稱。只有 `midi/note/on`、`midi/note/off`，以及中間多一個裝置名的寫法（`midi/<device>/note/on`）會被處理。"},{"name":"value","type":"any","en":"The payload. Only an object with `vel` or `velocity` is changed (a copy, never the original).","zh":"訊號的內容。只有帶 `vel` 或 `velocity` 的物件會被改（改的是複本，不動原件）。"}],"returns":{"type":"any","en":"The payload with both `vel` and `velocity`. If both were there already, the same object comes back untouched.","zh":"同時帶 `vel` 和 `velocity` 的內容。兩個本來就都有的話，原物件原樣傳回。"},"notes":[],"examples":[{"code":"import { normalizeValue } from '@openav/relay';\n\nconst rows = [\n  normalizeValue('midi/note/on', { note: 60, velocity: 127 }),\n  normalizeValue('midi/note/on', { note: 60, vel: 0.5 }),\n  normalizeValue('midi/minilab3/note/off', { note: 61, vel: 1 }),\n  normalizeValue('surface/main/pad/hit', { velocity: 3 }),\n];\ndocument.getElementById('app').innerHTML = rows.map((r) => JSON.stringify(r)).join('<br>');","caption":{"en":"The first two gain the spelling they lacked (`vel` 1 and `velocity` 64); the third works with a device in the name; the fourth is not a note and stays as it is.","zh":"前兩個補上缺的寫法（`vel` 是 1、`velocity` 是 64）。第三個名稱中間有裝置名也照樣處理。第四個不是音符，保持原樣。"},"run":"module","lang":"js"}],"related":["fileSignal","bindSignals"],"keywords":{"en":["velocity","vel","note on","midi","127","normalize","touchosc","keyboard"],"zh":["力度","vel","velocity","音符","midi","正規化","鍵盤"]},"tbd":[],"claims":[]},
{"id":"RelayClient","key":"RelayClient","name":"RelayClient","aliases":[],"kind":"class","package":"@openav/relay","category":"remote","documented":true,"signature":"new RelayClient({ role = 'controller', room = 'default', id = deviceId(), url = null, batchHz = 30, resendMs = 1000, onStatus, onSignal, onFeedback, onConfig } = {})","import":"import { RelayClient } from '@openav/relay';","source":{"file":"packages/relay/index.js","line":34,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/index.js#L34"},"summary":{"en":"The browser end of the room relay: sends signals to the other pages in a room and receives theirs, as a phone (controller), the show (runner) or a backstage monitor.","zh":"房間中繼的瀏覽器端：把訊號送給同一個房間裡的其他頁面，也接收它們的訊號，身分可以是手機（controller）、演出本身（runner）或後台監看（monitor）。"},"description":[{"en":"Make one client per page, give it a role and a room, and call `connect()`. A phone calls `phone.set('phone/me/tilt/x', 0.3)` for values that change continuously and `phone.send('midi/note/on', { note: 60 }, true)` for events. The show passes its client to [`bindSignals()`](#bindSignals), and every phone signal lands in its `Signals`. The relay itself is [`attachRelay()`](#attachRelay) on a Node server.","zh":"每個頁面做一個用戶端，給它身分和房間，然後呼叫 `connect()`。手機用 `phone.set('phone/me/tilt/x', 0.3)` 送連續變化的數值，用 `phone.send('midi/note/on', { note: 60 }, true)` 送事件。演出端把自己的用戶端交給 [`bindSignals()`](#bindSignals)，每個手機訊號就會進到它的 `Signals`。中繼本身是 Node 伺服器上的 [`attachRelay()`](#attachRelay)。"},{"en":"Continuous values are cheap on purpose: `set()` only stores the value (and does nothing if it is the same as the last), then every `1 / batchHz` seconds the changed ones leave in one `batch`, and every `resendMs` all of them leave again. Events are the opposite: `send()` leaves at once and is never batched or repeated, because a note-on delivered twice is a bug.","zh":"連續的數值刻意做得很省：`set()` 只是把值存起來（跟上一次一樣就什麼都不做），之後每 `1 / batchHz` 秒，有變動的值打包成一則 `batch` 送出，每 `resendMs` 毫秒，全部的值再送一遍。事件則相反：`send()` 馬上送出，不打包也不重送，因為一個 note-on 收到兩次就是錯。"},{"en":"It looks after the connection itself. When the socket closes it reconnects after 0.5 s, then 1 s, 2 s, 4 s and every 5 s, until you call `close()`. A ping goes out every 2 s and the answer becomes `latency` in milliseconds. `status` is `'connecting'`, `'open'` or `'closed'`, and `peers` is how many `controllers`, `runners` and `monitors` the room has.","zh":"連線它自己照顧。socket 關掉之後，它會在 0.5 秒、1 秒、2 秒、4 秒，之後每 5 秒重連一次，直到你呼叫 `close()`。每 2 秒送一個 ping，回應的時間差就是 `latency`（毫秒）。`status` 是 `'connecting'`、`'open'` 或 `'closed'`，`peers` 是房間裡有幾個 `controllers`、`runners`、`monitors`。"}],"params":[{"name":"role","type":"'controller' | 'runner' | 'monitor'","en":"Who this page is. A controller (a phone) sends signals to the runner and the monitors. A runner (the show) hears the controllers, and can send feedback and config back. A monitor only listens. The server treats any other word as `controller`.","zh":"這個頁面的身分。控制端（controller，也就是手機）把訊號送給演出端和監看端。演出端（runner，也就是演出本身）聽得到控制端，也能把 feedback 和 config 送回去。監看端（monitor）只聽不說。伺服器遇到其他字一律當成 `controller`。"},{"name":"room","type":"String","en":"The room name. Pages in different rooms never hear each other. 1 to 32 characters of `A–Z a–z 0–9 _ -`; the server turns anything else into `default`.","zh":"房間名稱。不同房間的頁面彼此聽不到。1 到 32 個字元，只能用 `A–Z a–z 0–9 _ -`，其他的伺服器會換成 `default`。"},{"name":"id","type":"String","en":"This device's id, the `<id>` in `phone/<id>/…`. The default is [`deviceId()`](#deviceId), which is kept in `localStorage`. The server keeps its first 64 characters.","zh":"這個裝置的 id，也就是 `phone/<id>/…` 裡的 `<id>`。預設是 [`deviceId()`](#deviceId)，存在 `localStorage` 裡。伺服器只留前 64 個字元。"},{"name":"url","type":"String","en":"The full WebSocket address, with the query already on it: `wss://host/relay?role=controller&room=main&id=ab12`. Nothing is added to it. Leave it out and the client uses `?relay=ws://…` from the page's address, or else the page's own `/relay` (`wss:` on an https page), and adds `role`, `room` and `id` itself.","zh":"完整的 WebSocket 位址，查詢字串要自己帶好：`wss://host/relay?role=controller&room=main&id=ab12`。它不會再補任何東西。不給的話，用頁面網址裡的 `?relay=ws://…`，再沒有就用頁面自己的 `/relay`（https 頁面用 `wss:`），並自動補上 `role`、`room`、`id`。"},{"name":"batchHz","type":"Number","en":"How many times a second the values you `set()` are flushed to the relay, as one `batch` message.","zh":"每秒把你 `set()` 的數值送出幾次，打包成一則 `batch` 訊息。"},{"name":"resendMs","type":"Number","en":"Every this many milliseconds, send every stored value again, not only the changed ones. This is what lets a phone that joins late, or a show page that reloads, catch up.","zh":"每隔這麼多毫秒，把存起來的每一個值再送一次，不只是有變動的。晚進來的手機、重新載入的演出頁面，就靠這個趕上現況。"},{"name":"onStatus","type":"Function","optional":true,"en":"`(client) => {}`, called when the connection opens or closes, when a `pong` gives a new `latency`, and when the peer counts change. Read `client.status`, `client.latency` and `client.peers` inside it.","zh":"`(client) => {}`，連線打開或關閉、`pong` 帶來新的 `latency`、連線數改變的時候呼叫。在裡面讀 `client.status`、`client.latency`、`client.peers`。"},{"name":"onSignal","type":"Function","optional":true,"en":"`(name, value, msg) => {}`, for every signal that arrives (a runner or monitor hears the controllers; a monitor also hears the runner). A `batch` calls it once per item. `msg.pulse` is true for a pulse. [`bindSignals()`](#bindSignals) fills this in for you.","zh":"`(name, value, msg) => {}`，每收到一個訊號就呼叫（演出端和監看端聽得到控制端，監看端也聽得到演出端）。一則 `batch` 會每個項目各呼叫一次。脈衝訊號的 `msg.pulse` 是 true。[`bindSignals()`](#bindSignals) 會替你填這一項。"},{"name":"onFeedback","type":"Function","optional":true,"en":"`(name, value) => {}`, on a controller: a value the runner sent with `feedback()`, to show in a meter or to move a fader.","zh":"`(name, value) => {}`，在控制端：演出端用 `feedback()` 送來的數值，拿去顯示在音量表或移動推桿。"},{"name":"onConfig","type":"Function","optional":true,"en":"`(key, data) => {}`, on a controller: a standing config the runner sent with `config()`, such as the surface layout. A phone that joins late gets the stored ones right away.","zh":"`(key, data) => {}`，在控制端：演出端用 `config()` 送來的常駐設定，例如控制面板的版面。晚進來的手機會立刻收到已儲存的那些。"}],"needs":{"en":"A relay server for it to reach, such as `node serve.js` (it hosts one at `/relay`) or `node packages/relay/server.js`. Until the socket is open, `set()` keeps the latest values and sends them when it opens, but `send()`, `feedback()` and `config()` messages are dropped, not queued.","zh":"要有一台中繼伺服器可以連，例如 `node serve.js`（它在 `/relay` 架了一個）或 `node packages/relay/server.js`。socket 還沒打開之前，`set()` 會記住最新的值，打開時送出，但 `send()`、`feedback()`、`config()` 的訊息會直接丟掉，不會排隊。"},"notes":[{"en":"Public fields: `role`, `room`, `id`, `url`, `batchHz`, `resendMs`, the callbacks, `status`, `latency` (milliseconds, `null` until the first pong), `peers` and `sent` (how many messages have gone out).","zh":"公開的欄位有 `role`、`room`、`id`、`url`、`batchHz`、`resendMs`、各個回呼、`status`、`latency`（毫秒，第一個 pong 回來之前是 `null`）、`peers` 和 `sent`（已經送出幾則訊息）。"},{"en":"After a reconnect the runner has to send its `config()` again: the client does not remember it. `mountRemoteHost()` does that for you.","zh":"重連之後，演出端要再送一次 `config()`：用戶端不會記得它。`mountRemoteHost()` 會替你做這件事。"}],"examples":[{"code":"import { RelayClient, bindSignals } from '@openav/relay';\nimport { Signals } from '@openav/core';\n\n// the show page: every phone signal lands in a Signals registry\nconst signals = new Signals();\nconst show = new RelayClient({ role: 'runner', room: 'main' }).connect();\nbindSignals(show, signals);\n\n// the phone page: a continuous value and an event\nconst phone = new RelayClient({ role: 'controller', room: 'main' }).connect();\nphone.set('phone/me/tilt/x', 0.3);\nphone.send('midi/note/on', { note: 60 }, true);","caption":{"en":"The two ends of one room, as they would sit on two pages (the show and a phone): once the socket is open, the show's signals hold `phone/me/tilt/x` and, as an alias, `phone/any/tilt/x`.","zh":"一個房間的兩端，實際上會放在兩個頁面（演出和手機）：socket 打開之後，演出端的 signals 裡就有 `phone/me/tilt/x`，以及別名 `phone/any/tilt/x`。"},"run":"static","lang":"js"},{"code":"import { RelayClient, bindSignals } from '@openav/relay';\nimport { Signals } from '@openav/core';\n\n// A stand-in for the relay so this runs without a server: a controller's messages reach the runner,\n// and the runner's feedback and config reach the controller (the same routing the real relay does).\nconst sockets = [];\nglobalThis.WebSocket = class {\n  constructor(url) {\n    this.role = new URL(url).searchParams.get('role');\n    this.readyState = 1;\n    sockets.push(this);\n    setTimeout(() => this.onopen?.());\n  }\n  send(text) {\n    const type = JSON.parse(text).type;\n    const to = this.role === 'controller' ? 'runner' : (type === 'feedback' || type === 'config') ? 'controller' : null;\n    for (const s of sockets) if (s.role === to) s.onmessage?.({ data: text });\n  }\n  close() {}\n};\n\nconst signals = new Signals();\nconst show = new RelayClient({ role: 'runner', url: 'ws://relay/x?role=runner&room=demo&id=show' }).connect();\nbindSignals(show, signals);\nconst got = [];\nconst phone = new RelayClient({\n  role: 'controller', url: 'ws://relay/x?role=controller&room=demo&id=ab12',\n  onFeedback: (name, value) => got.push('feedback ' + name + ' = ' + value),\n  onConfig: (key, data) => got.push('config ' + key + ' = ' + JSON.stringify(data)),\n}).connect();\n\nphone.set('phone/ab12/tilt/x', 0.3);\nshow.feedback('surface/main/level', 0.4);\nshow.config('surface', { title: 'demo' });\n\nsetTimeout(() => {\n  got.push('tilt in the show: ' + signals.get('phone/any/tilt/x'));\n  document.getElementById('app').innerHTML = got.join('<br>');\n  phone.close(); show.close();\n}, 200);","caption":{"en":"Prints the config and the feedback the phone got back from the show, then the phone's tilt as the show hears it (under its alias `phone/any/tilt/x`). Same calls as against a real relay.","zh":"印出手機從演出端收到的 config 和 feedback，再印出演出端聽到的手機傾斜（在別名 `phone/any/tilt/x` 底下）。呼叫的方式跟連真正的中繼完全一樣。"},"run":"module","lang":"js"}],"related":["bindSignals","attachRelay","deviceId","mountRemoteHost","mountRemote","Signals"],"keywords":{"en":["relay","websocket","room","phone","controller","runner","monitor","client","connect","send signal","feedback","config","reconnect","latency"],"zh":["中繼","websocket","房間","手機","控制端","演出端","監看端","連線","傳送訊號","回饋","設定","重連","延遲"]},"tbd":[{"en":"The reconnect delays (0.5 s, 1 s, 2 s, 4 s, then 5 s) are read from the code, not measured against a relay that goes down and comes back. The second example runs the client against a stand-in socket, not a real WebSocket server.","zh":"重連的間隔（0.5 秒、1 秒、2 秒、4 秒，之後 5 秒）是從程式碼讀來的，沒有拿一個斷線又恢復的中繼去實測。第二個範例是拿替身 socket 跑用戶端，不是真正的 WebSocket 伺服器。"}],"claims":[],"members":[{"name":"open","member":"getter","signature":"","doc":"","line":64},{"name":"connect","member":"method","signature":"","doc":"","line":68},{"name":"close","member":"method","signature":"","doc":"","line":92},{"name":"send","member":"method","signature":"name, value, pulse = false, extra","doc":"Immediate, un-coalesced.","line":112},{"name":"set","member":"method","signature":"name, value","doc":"Continuous value: only the latest per name survives a flush.","line":116},{"name":"feedback","member":"method","signature":"name, value","doc":"Runner → controllers: a value to display (meter) or echo (fader follows the timeline).","line":118},{"name":"config","member":"method","signature":"key, data","doc":"Runner → controllers: standing config (e.g.","line":120}]},
{"id":"signalMeta","key":"signalMeta","name":"signalMeta","aliases":[],"kind":"function","package":"@openav/relay","category":"remote","documented":true,"signature":"signalMeta(name, { pulse = false } = {})","import":"import { signalMeta } from '@openav/relay';","source":{"file":"packages/relay/signals.js","line":8,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/relay/signals.js#L8"},"summary":{"en":"What the relay knows about a signal name: its range and unit, or that it is a pulse. Returns `null` for names it does not know.","zh":"中繼對一個訊號名稱知道什麼：它的範圍和單位，或者它是脈衝。不認得的名稱回傳 `null`。"},"description":[{"en":"A `Signals` registry needs to know a name's range before a meter can scale it or a mapping can tell a knob from an event. [`fileSignal()`](#fileSignal) calls this the first time it hears a name. What it knows: `phone/<id>/tilt/*` is −1..1, `accel/*` is −20..20 m/s², `rot/*` is −360..360 °/s, `orient/*` is 0..360 °, `touch/count` is 0..10, `knock` is a pulse, and every other `phone/…` name (touch, light) is 0..1. `surface/…` names are 0..1 (`…/raw` and `…/delta` are unscaled, `{}`). `midi/note/on`, `midi/note/off`, `midi/virtual` and the `hit`, `on`, `off`, `last` names of a controller are pulses, and `midi/cc/*` is 0..1.","zh":"`Signals` 要先知道一個名稱的範圍，音量表才能縮放、映射才分得出旋鈕和事件。[`fileSignal()`](#fileSignal) 第一次聽到一個名稱就會呼叫它。它認得的有：`phone/<id>/tilt/*` 是 −1..1，`accel/*` 是 −20..20 m/s²，`rot/*` 是 −360..360 °/s，`orient/*` 是 0..360 °，`touch/count` 是 0..10，`knock` 是脈衝，其他的 `phone/…` 名稱（觸控、光線）是 0..1。`surface/…` 的名稱是 0..1（`…/raw` 和 `…/delta` 沒有縮放，回傳 `{}`）。`midi/note/on`、`midi/note/off`、`midi/virtual`，以及控制器的 `hit`、`on`、`off`、`last` 名稱是脈衝，`midi/cc/*` 是 0..1。"}],"params":[{"name":"name","type":"String","en":"A signal name such as `phone/ab12/tilt/x`, `surface/main/hue` or `midi/note/on`.","zh":"訊號名稱，例如 `phone/ab12/tilt/x`、`surface/main/hue` 或 `midi/note/on`。"},{"name":"pulse","type":"Boolean","optional":true,"en":"Pass `{ pulse: true }` when the message was sent as an event: the answer is then always `{ kind: 'pulse' }`.","zh":"訊息是當作事件送來的話，傳 `{ pulse: true }`：答案一律是 `{ kind: 'pulse' }`。"}],"returns":{"type":"Object | null","en":"Metadata for `signals.define()`: `{ min, max, unit? }`, `{ kind: 'pulse' }`, or `{}` for raw values. `null` when the name is not one it knows.","zh":"給 `signals.define()` 用的資料：`{ min, max, unit? }`、`{ kind: 'pulse' }`，原始值則是 `{}`。名稱不在它認得的範圍內時是 `null`。"},"notes":[],"examples":[{"code":"import { signalMeta } from '@openav/relay';\n\nconst names = ['phone/ab12/tilt/x', 'phone/ab12/accel/z', 'phone/ab12/knock', 'surface/main/hue', 'surface/main/hue/raw', 'midi/note/on', 'my/own/thing'];\ndocument.getElementById('app').innerHTML = names\n  .map((n) => `${n}: ${JSON.stringify(signalMeta(n))}`)\n  .join('<br>');","caption":{"en":"Prints what the relay knows about seven names, from `{\"min\":-1,\"max\":1}` for tilt to `null` for a name it has never heard of.","zh":"印出中繼對七個名稱的認識，從傾斜的 `{\"min\":-1,\"max\":1}`，到完全不認得的名稱的 `null`。"},"run":"module","lang":"js"}],"related":["fileSignal","bindSignals","aliasOf"],"keywords":{"en":["signal meta","range","min max","pulse","declare","unit","signal names","phone signals"],"zh":["訊號資料","範圍","最小最大","脈衝","宣告","單位","訊號名稱","手機訊號"]},"tbd":[],"claims":[]},
{"id":"attachTouchPad","key":"attachTouchPad","name":"attachTouchPad","aliases":[],"kind":"function","package":"@openav/remote","category":"remote","documented":true,"signature":"attachTouchPad(pad, io, { onMove, onEnd } = {})","import":"import { attachTouchPad } from '@openav/remote';","source":{"file":"packages/remote/sensors.js","line":138,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/remote/sensors.js#L138"},"summary":{"en":"Makes an element a multi-touch pad: each finger publishes `touch/x`, `touch/y` and `touch/down` (0..1 inside the element), and `touch/count` says how many are down.","zh":"把一個元素變成多點觸控板：每根手指發布 `touch/x`、`touch/y` 和 `touch/down`（在元素裡的 0..1），`touch/count` 是現在按著幾根。"},"description":[{"en":"The first finger down is slot 0 and publishes `touch/x`, `touch/y`, `touch/down`; the second is slot 1 and publishes `touch/1/x`, `touch/1/y`, `touch/1/down`, and so on. A slot is given back when its finger lifts, so the lowest free number is used next. `x` runs 0 at the left edge to 1 at the right, `y` runs 0 at the top to 1 at the bottom, both held inside the element and rounded to 4 decimals. `touch/down` is 1 or 0, and `touch/count` is the number of fingers down.","zh":"第一根按下去的手指是 slot 0，發布 `touch/x`、`touch/y`、`touch/down`。第二根是 slot 1，發布 `touch/1/x`、`touch/1/y`、`touch/1/down`，以此類推。手指離開就把 slot 還回去，下一根會用最小的空號。`x` 從左邊緣的 0 到右邊緣的 1，`y` 從上緣的 0 到下緣的 1，都限制在元素裡面，並四捨五入到小數點後 4 位。`touch/down` 是 1 或 0，`touch/count` 是按著的手指數。"},{"en":"It publishes `touch/down` 0 and `touch/count` 0 at once, so a receiver knows the pad is there before anyone touches it. It uses pointer events, so a mouse works too (one finger).","zh":"它一接上去就先發布 `touch/down` 0 和 `touch/count` 0，所以接收端在還沒有人碰之前就知道觸控板存在。它用的是 pointer 事件，所以滑鼠也能用（算一根手指）。"}],"params":[{"name":"pad","type":"HTMLElement","en":"The element to listen on. Give it `touch-action: none` in CSS, or the browser may take a drag for a scroll and cancel the touch.","zh":"要監聽的元素。在 CSS 裡給它 `touch-action: none`，不然瀏覽器可能把拖曳當成捲動，把觸碰取消掉。"},{"name":"io","type":"{ set(name, value) }","en":"Where the values go, as for [`PhoneSensors`](#PhoneSensors): only `set` is used, and the names have no `phone/<id>/` in front.","zh":"數值要送去哪裡，跟 [`PhoneSensors`](#PhoneSensors) 一樣：只用到 `set`，名稱前面沒有 `phone/<id>/`。"},{"name":"onMove","type":"Function","optional":true,"en":"`(slot, x, y, down) => {}`, when a finger touches and each time it moves. `x` and `y` are 0..1 and `down` is true. Use it to draw the finger.","zh":"`(slot, x, y, down) => {}`，手指按下去和每次移動的時候呼叫。`x`、`y` 是 0..1，`down` 是 true。拿來畫手指的位置。"},{"name":"onEnd","type":"Function","optional":true,"en":"`(slot) => {}`, when a finger lifts or the touch is cancelled.","zh":"`(slot) => {}`，手指離開或觸碰被取消的時候呼叫。"}],"notes":[{"en":"The sense tab uses this on its touch pad, with `io` writing to a [`RelayClient`](#RelayClient) under `phone/<id>/`.","zh":"感測 分頁的觸控板用的就是它，`io` 寫進 [`RelayClient`](#RelayClient)，名稱加上 `phone/<id>/`。"}],"examples":[{"code":"import { attachTouchPad } from '@openav/remote';\n\nconst pad = document.createElement('div');\npad.style.cssText = 'width:200px;height:200px;background:#222;touch-action:none';\ndocument.getElementById('app').append(pad);\n\nconst seen = [];\nattachTouchPad(pad, { set: (name, value) => seen.push(name + '=' + value) });\n\n// a finger lands a quarter of the way across, halfway down, then moves right\nconst r = pad.getBoundingClientRect();\nconst touch = (type, id, fx, fy) => pad.dispatchEvent(new PointerEvent(type, {\n  pointerId: id, clientX: r.left + fx * r.width, clientY: r.top + fy * r.height, bubbles: true,\n}));\ntouch('pointerdown', 1, 0.25, 0.5);\ntouch('pointermove', 1, 0.5, 0.5);\ntouch('pointerup', 1, 0.5, 0.5);\npad.after(Object.assign(document.createElement('p'), { innerHTML: seen.join('<br>') }));","caption":{"en":"A 200 px square appears, and under it the values a finger would publish: touch/down=0, touch/count=0 at the start, then x, y, down and count as it lands, moves and lifts.","zh":"畫面上出現一個 200 像素的方塊，下面列出一根手指會發布的數值：一開始的 touch/down=0、touch/count=0，然後是它落下、移動、離開時的 x、y、down 和 count。"},"run":"module","lang":"js"},{"code":"import { attachTouchPad } from '@openav/remote';\n\nconst pad = document.createElement('div');\npad.style.cssText = 'width:200px;height:100px;background:#222;touch-action:none';\ndocument.getElementById('app').append(pad);\n\nconst seen = [];\nattachTouchPad(pad, { set: (name, value) => { if (name.endsWith('/count') || name.endsWith('/down')) seen.push(name + '=' + value); } }, {\n  onMove: (slot, x, y) => seen.push('finger ' + slot + ' at ' + x.toFixed(2) + ', ' + y.toFixed(2)),\n  onEnd: (slot) => seen.push('finger ' + slot + ' lifted'),\n});\nconst r = pad.getBoundingClientRect();\nconst touch = (type, id, fx, fy) => pad.dispatchEvent(new PointerEvent(type, {\n  pointerId: id, clientX: r.left + fx * r.width, clientY: r.top + fy * r.height, bubbles: true,\n}));\ntouch('pointerdown', 1, 0.1, 0.5);   // slot 0\ntouch('pointerdown', 2, 0.9, 0.5);   // slot 1: touch/1/…\ntouch('pointerup', 1, 0.1, 0.5);     // slot 0 is free again\ntouch('pointerdown', 3, 0.5, 0.5);   // takes slot 0\npad.after(Object.assign(document.createElement('p'), { innerHTML: seen.join('<br>') }));","caption":{"en":"Two fingers get slots 0 and 1; after the first lifts, the next finger takes slot 0 again. The list shows the callbacks and the count.","zh":"兩根手指拿到 slot 0 和 1，第一根離開之後，下一根又拿回 slot 0。清單裡是各個回呼和手指數。"},"run":"module","lang":"js"}],"related":["PhoneSensors","mountRemote","RelayClient"],"keywords":{"en":["touch pad","multi-touch","fingers","pointer events","touch/x","touch/count","xy pad","phone"],"zh":["觸控板","多點觸控","手指","pointer 事件","touch/x","touch/count","xy 板","手機"]},"tbd":[],"claims":[]},
{"id":"localSensors","key":"localSensors","name":"localSensors","aliases":[],"kind":"function","package":"@openav/remote","category":"remote","documented":true,"signature":"localSensors(signals, { prefix = 'phone/local/', alias = 'any', source = 'local', hooks = {} } = {})","import":"import { localSensors } from '@openav/remote';","source":{"file":"packages/remote/sensors.js","line":163,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/remote/sensors.js#L163"},"summary":{"en":"For a show page that runs on the phone itself: the phone's own sensors go straight into a `Signals` as `phone/local/…`, with a `phone/any/…` copy.","zh":"給跑在手機本身上的演出頁面用：手機自己的感測器直接進到 `Signals`，名稱是 `phone/local/…`，另外多一份 `phone/any/…` 的複本。回傳 `PhoneSensors`。"},"description":[{"en":"A remote phone reaches the show through a relay. When the show page is open on the phone itself, there is no one to relay to, so this reads the sensors in place. The names and thresholds are the same as a remote phone's (`phone/local/tilt/x` is −1..1, `phone/local/knock` is a pulse), declared the same way by [`fileSignal()`](#fileSignal), and mirrored to `phone/any/…`, so a route written for \"the latest phone\", `phone/any/tilt/x`, works whether the phone is in your hand as a controller or is the show.","zh":"遠端的手機要透過中繼才找得到演出。演出頁面就開在手機上的時候，沒有人需要轉送，所以直接在原地讀感測器。名稱和門檻跟遠端手機一樣（`phone/local/tilt/x` 是 −1..1，`phone/local/knock` 是脈衝），用 [`fileSignal()`](#fileSignal) 一樣的方式宣告，並複製一份到 `phone/any/…`，所以替「最新的那支手機」寫的路由 `phone/any/tilt/x`，不論手機是拿在手上當控制端、還是它本身就是演出，都能用。"}],"params":[{"name":"signals","type":"Signals","en":"The registry to fill (from `@openav/core`).","zh":"要填的註冊表（來自 `@openav/core`）。"},{"name":"prefix","type":"String","optional":true,"en":"What goes in front of each sensor name.","zh":"放在每個感測器名稱前面的字。"},{"name":"alias","type":"String | null","optional":true,"en":"The word for the copy, `phone/<alias>/…`. `null` turns the copy off.","zh":"複本用的字，`phone/<alias>/…`。`null` 表示不做複本。"},{"name":"source","type":"String","optional":true,"en":"Stored as the signals' `source` in `signals.meta`.","zh":"存成 `signals.meta` 裡這些訊號的 `source`。"},{"name":"hooks","type":"Object","optional":true,"en":"The `hooks` of [`PhoneSensors`](#PhoneSensors): `onKnock`, `onSample`, `notify`.","zh":"[`PhoneSensors`](#PhoneSensors) 的 `hooks`：`onKnock`、`onSample`、`notify`。"}],"returns":{"type":"PhoneSensors","en":"Nothing is listening yet: call `.start()` from a tap.","zh":"還沒有開始聽：要在點擊時呼叫 `.start()`。"},"needs":{"en":"Call `.start()` on the result from a tap (iOS asks for permission only inside a gesture, and only on https pages). Until then nothing is published. See [`PhoneSensors`](#PhoneSensors).","zh":"拿到結果之後，要在點擊時呼叫 `.start()`（iOS 只在手勢裡才會問權限，而且只限 https 頁面）。在那之前什麼都不會發布。見 [`PhoneSensors`](#PhoneSensors)。"},"notes":[],"examples":[{"code":"import { localSensors } from '@openav/remote';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst own = localSensors(signals);\nawait own.start();   // on a phone: from a tap\n\nwindow.dispatchEvent(new DeviceOrientationEvent('deviceorientation', { alpha: 0, beta: 22.5, gamma: -45 }));\nown.stop();\ndocument.getElementById('app').innerHTML = [\n  'phone/local/tilt/x = ' + signals.get('phone/local/tilt/x'),\n  'phone/any/tilt/y = ' + signals.get('phone/any/tilt/y'),\n  'source: ' + signals.meta.get('phone/local/tilt/x').source,\n].join('<br>');","caption":{"en":"Prints the tilt under both names (-1 and 0.5) and the source \"local\". On a phone the events come from the sensors instead of from this made-up one.","zh":"印出兩個名稱底下的傾斜值（-1 和 0.5）和來源「local」。在手機上，事件是從感測器來的，而不是這個編出來的。"},"run":"module","lang":"js"},{"code":"import { localSensors } from '@openav/remote';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst own = localSensors(signals, { prefix: 'phone/me/', alias: null });   // no phone/any copy\nawait own.start();\nwindow.dispatchEvent(new DeviceOrientationEvent('deviceorientation', { alpha: 0, beta: 0, gamma: 9 }));\nown.stop();\ndocument.getElementById('app').textContent =\n  'phone/me/tilt/x = ' + signals.get('phone/me/tilt/x') + ', copy exists: ' + signals.meta.has('phone/any/tilt/x');","caption":{"en":"A different prefix and no alias: prints \"phone/me/tilt/x = 0.2, copy exists: false\".","zh":"換一個前綴、不做別名：印出「phone/me/tilt/x = 0.2, copy exists: false」。"},"run":"module","lang":"js"}],"related":["PhoneSensors","fileSignal","aliasOf","bindSignals"],"keywords":{"en":["local sensors","phone as the show","no relay","phone/local","tilt","motion","show on a phone"],"zh":["本機感測器","手機當演出","不用中繼","phone/local","傾斜","動作","演出跑在手機上"]},"tbd":[],"claims":[]},
{"id":"mountJoinCard","key":"mountJoinCard","name":"mountJoinCard","aliases":[],"kind":"function","package":"@openav/remote","category":"remote","documented":true,"signature":"mountJoinCard(parent, host)","import":"import { mountJoinCard } from '@openav/remote/host';","source":{"file":"packages/remote/host.js","line":90,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/remote/host.js#L90"},"summary":{"en":"Puts a small card on the show page with a QR code and the URL a phone should open, and a line that says whether the relay is reachable and how many phones are in.","zh":"在演出頁面上放一張小卡片，有 QR code 和手機該打開的網址，還有一行字說明中繼連不連得上、有幾支手機進來。"},"description":[{"en":"The URL is the phone page (`packages/remote/` next to this file) with `?room=<room>` on it, and with `?relay=…` as well when the show uses the public relay or a relay of your own, so the phone ends up in the same room on the same relay. On `localhost` it asks `node serve.js` for this machine's LAN address at `/__info` and puts that in the URL, because a phone cannot open `localhost`. The QR code is drawn from the URL; if the URL is too long for the encoder, the code is left out and the URL stays.","zh":"網址是手機頁面（這個檔案旁邊的 `packages/remote/`），後面接 `?room=<room>`，演出用公開中繼或你自己的中繼的話還會接上 `?relay=…`，這樣手機會進到同一個中繼的同一個房間。在 `localhost` 的時候，它會向 `node serve.js` 的 `/__info` 詢問這台電腦的區域網路位址並填進網址，因為手機打不開 `localhost`。QR code 是依網址畫出來的，網址太長、編碼器放不下時，就只留網址、不畫 QR code。"},{"en":"The status line reads `relay ok · 2 phones · room demo` when the connection is open (and adds `· public relay` on the public one), `relay offline — is it `node serve.js`?` when a local relay cannot be reached, and `can't reach the relay (…) — retrying` for a shared one. On a phone held upright the card starts folded, because the stage is small; tap it to open or fold it.","zh":"連線打開的時候，狀態那行寫 `relay ok · 2 phones · room demo`（用公開中繼的話後面加 `· public relay`），本機的中繼連不上時寫 `relay offline — is it `node serve.js`?`，共用的中繼連不上則寫 `can't reach the relay (…) — retrying`。直拿的手機上，卡片一開始是收起來的，因為舞台很小，點一下可以展開或收起。"}],"params":[{"name":"parent","type":"HTMLElement","en":"The element to add the card to. The card is `position: absolute` at its top left, so the parent should be positioned (`position: relative`) and big enough to show it.","zh":"要把卡片加進去的元素。卡片是 `position: absolute`，貼在左上角，所以父元素最好有定位（`position: relative`），而且夠大，放得下它。"},{"name":"host","type":"Object","en":"The host from [`mountRemoteHost()`](#mountRemoteHost). The card reads `host.room`, `host.relayMode` and `host.relayUrl`, and takes over `host.onStatus` to write the status line.","zh":"[`mountRemoteHost()`](#mountRemoteHost) 回傳的 host。卡片會讀 `host.room`、`host.relayMode`、`host.relayUrl`，並接管 `host.onStatus` 來寫狀態那行字。"}],"returns":{"type":"HTMLElement","en":"The card's element, already in `parent`. Remove it with `.remove()`.","zh":"卡片的元素，已經放進 `parent` 裡了。要移除就呼叫 `.remove()`。"},"notes":[{"en":"The words on the card are in English.","zh":"卡片上的字是英文。"}],"examples":[{"code":"import { mountRemoteHost, mountJoinCard } from '@openav/remote/host';\nimport { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\n\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 200 }]);\nconst signals = new Signals();\nconst mapper = new Mapper({ signals, params });\nconst host = mountRemoteHost({ signals, params, mapper, room: 'demo' });   // not connected\n\nconst card = mountJoinCard(document.getElementById('app'), host);\nhost.onStatus({ status: 'open', peers: { controllers: 2 } });   // pretend the relay answered","caption":{"en":"A card appears at the top left with a QR code, the phone URL ending in ?room=demo, and the line \"relay ok · 2 phones · room demo\".","zh":"左上角出現一張卡片，有 QR code、結尾是 ?room=demo 的手機網址，以及「relay ok · 2 phones · room demo」這行字。"},"run":"module","lang":"js"}],"related":["mountRemoteHost","mountRemote","RelayClient"],"keywords":{"en":["join card","qr code","phone url","room","scan","show page","public relay","status"],"zh":["加入卡片","qr code","手機網址","房間","掃描","演出頁面","公開中繼","狀態"]},"tbd":[],"claims":[]},
{"id":"mountRemote","key":"mountRemote","name":"mountRemote","aliases":[],"kind":"function","package":"@openav/remote","category":"remote","documented":true,"signature":"mountRemote(root, opts = {})","import":"import { mountRemote } from '@openav/remote';","source":{"file":"packages/remote/app.js","line":105,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/remote/app.js#L105"},"summary":{"en":"The whole phone app in one call: a status bar and the tabs sense, keys, control and MIDI, talking to the relay as a controller. `/packages/remote/` is exactly this.","zh":"一次呼叫就是整個手機 app：狀態列加上 感測、琴鍵、控制台、MIDI 這幾個分頁，以控制端身分跟中繼溝通。`/packages/remote/` 做的就是這件事。"},"description":[{"en":"It is what a phone opens: `lockViewport()` so nothing scrolls or zooms, `keepAwake()` so the screen stays on, one [`RelayClient`](#RelayClient) as a controller that every tab shares, and the tabs. Sense is a tilt ball, readouts, a multi-touch pad ([`attachTouchPad()`](#attachTouchPad)) and the sensors ([`PhoneSensors`](#PhoneSensors)) behind a start button. Keys is a piano that is one row in landscape and two stacked rows in portrait. Control is a [`Surface`](#Surface). MIDI is a virtual controller.","zh":"它就是手機打開的那個東西：`lockViewport()` 讓畫面不能捲動也不能縮放，`keepAwake()` 讓螢幕不暗掉，一個所有分頁共用的、身分是控制端的 [`RelayClient`](#RelayClient)，再加上幾個分頁。感測（sense）有一顆傾斜球、讀數、一塊多點觸控板（[`attachTouchPad()`](#attachTouchPad)），以及藏在開始按鈕後面的感測器（[`PhoneSensors`](#PhoneSensors)）。琴鍵（keys）是一台鋼琴，橫拿時一排，直拿時上下兩排。控制台（control）是一個 [`Surface`](#Surface)。MIDI 是一台虛擬控制器。"},{"en":"The control tab's layout comes from, in order: `surfaceUrl` or `?surface=`, `metaUrl` or `?meta=`, whatever layout the show publishes over the relay (the config `surface`), and finally a small generic panel so the page is never blank. The signals are `phone/<id>/…` (sense), `midi/note/on|off` and `midi/cc/64` (keys), and `surface/<page>/<id>` (control). The app opens on `?tab=`, else the tab used last in this browser, else sense.","zh":"控制台分頁的版面依序從這些地方找：`surfaceUrl` 或 `?surface=`、`metaUrl` 或 `?meta=`、演出透過中繼發布的版面（設定 `surface`），最後是一個小小的通用面板，頁面永遠不會空白。訊號是 `phone/<id>/…`（感測）、`midi/note/on|off` 和 `midi/cc/64`（琴鍵）、`surface/<page>/<id>`（控制台）。app 一開始打開 `?tab=` 指定的分頁，沒有就是這個瀏覽器上次用的分頁，再沒有就是感測。"}],"params":[{"name":"root","type":"HTMLElement","en":"Where to put the app. It fills the whole page (`position: fixed`), so pass `document.body`.","zh":"要把 app 放在哪裡。它會填滿整個頁面（`position: fixed`），所以傳 `document.body`。"},{"name":"opts","type":"Object","optional":true,"en":"`{ room, relayUrl, surfaceUrl, metaUrl }`, each optional. `room` is the relay room (else `?room=` in the address, else `window.__REMOTE__.room`, else `default`). `relayUrl` is a `ws://` or `wss://` relay (else `?relay=`; only those two schemes are accepted, so a link cannot point the phone anywhere else). `surfaceUrl` is a layout JSON for the control tab and `metaUrl` a JSON with a `params` array, which the control tab turns into [`autoSurface()`](#autoSurface). The address can also say `?tab=sense|keys|control|midi`.","zh":"`{ room, relayUrl, surfaceUrl, metaUrl }`，每一項都可省略。`room` 是中繼房間（沒給就用網址的 `?room=`，再沒有就用 `window.__REMOTE__.room`，再沒有就是 `default`）。`relayUrl` 是 `ws://` 或 `wss://` 的中繼（沒給就用 `?relay=`。只收這兩種，所以一個連結不能把手機指到別的地方）。`surfaceUrl` 是控制台分頁用的版面 JSON，`metaUrl` 是帶 `params` 陣列的 JSON，控制台分頁會把它變成 [`autoSurface()`](#autoSurface)。網址上也可以寫 `?tab=sense|keys|control|midi`。"}],"returns":{"type":"Object","en":"`{ relay, show(tab), surface, dispose() }`: the [`RelayClient`](#RelayClient), a function that switches tab, the control tab's [`Surface`](#Surface) (a getter, `null` until that tab has been opened), and `dispose()`, which closes the relay, unlocks the viewport, lets go of the wake lock and removes the app.","zh":"`{ relay, show(tab), surface, dispose() }`：[`RelayClient`](#RelayClient)、切換分頁的函式、控制台分頁的 [`Surface`](#Surface)（getter，那個分頁還沒打開過就是 `null`），以及 `dispose()`：關掉中繼、解除視窗鎖定、放掉螢幕喚醒鎖、移除整個 app。"},"needs":{"en":"A page of its own (it takes over the whole screen) and a relay to reach: the same origin's `/relay` unless `relayUrl` or `?relay=` says otherwise. Without a show in the room the header says it is waiting for the work to open.","zh":"一個專屬的頁面（它會佔滿整個螢幕），以及一個連得到的中繼：沒有 `relayUrl` 或 `?relay=` 的話就是同源的 `/relay`。房間裡沒有演出的時候，狀態列會寫在等作品開啟。"},"notes":[{"en":"The words on the app's tabs and header are in Chinese.","zh":"app 的分頁和狀態列上的字是中文。"}],"examples":[{"code":"import { mountRemote } from '@openav/remote';\n\n// the whole phone app: this is what /packages/remote/ does\nconst app = mountRemote(document.body, { room: 'main' });\n\n// later: app.show('keys') switches tab, app.dispose() takes everything down","caption":{"en":"On a phone: the status bar with the connection and latency, and the four tabs (sense, keys, control and MIDI, labelled in Chinese) along the bottom.","zh":"在手機上：有連線和延遲的狀態列，以及下方的 感測、琴鍵、控制台、MIDI 分頁。"},"run":"static","lang":"js"}],"related":["mountRemoteHost","PhoneSensors","attachTouchPad","Surface","RelayClient"],"keywords":{"en":["phone app","remote page","controller","tabs","sense","keys","control surface","ipad","touchosc","room"],"zh":["手機 app","遙控頁面","控制端","分頁","感測","琴鍵","控制面板","ipad","touchosc","房間"]},"tbd":[{"en":"Not opened on a real phone for this entry: how the tabs behave is read from `packages/remote/app.js` and docs/remote.md.","zh":"這一條沒有在真正的手機上打開過：分頁怎麼運作，是從 `packages/remote/app.js` 和 docs/remote.md 讀來的。"}],"claims":[]},
{"id":"mountRemoteHost","key":"mountRemoteHost","name":"mountRemoteHost","aliases":[],"kind":"function","package":"@openav/remote","category":"remote","documented":true,"signature":"mountRemoteHost({ signals, params, mapper, world = null, room = 'default', auto = {}, surface = null, feedbackHz = 10, url = null, mode = null, controllers = null } = {})","import":"import { mountRemoteHost } from '@openav/remote/host';","source":{"file":"packages/remote/host.js","line":41,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/remote/host.js#L41"},"summary":{"en":"The show side of the phone remote: joins the relay as the runner, files phone signals, builds the control panel from the params and echoes values back.","zh":"手機遙控在演出這一邊：以演出端（runner）身分加入中繼，歸檔手機訊號，依世界的參數建立控制面板，並把數值回送。回傳 host。"},"description":[{"en":"`createShow({ modules: { remote: true } })` calls this for you, so the world never has to know about phones. Calling it yourself does five things: it makes a [`RelayClient`](#RelayClient) as the room's runner; files every phone signal into `signals` with [`bindSignals()`](#bindSignals) (so `phone/any/…` always holds the latest phone); builds the layout from `surface`, `world.surface` or [`autoSurface()`](#autoSurface) of the params; adds the `surface/… → param` routes to the mapper; and, when you call `host.frame(dt, state)` every frame, sends the resolved param values back about `feedbackHz` times a second so the phones' faders follow the timeline.","zh":"`createShow({ modules: { remote: true } })` 會替你呼叫它，所以世界完全不必知道手機的存在。自己呼叫的話，它做五件事：先做一個 [`RelayClient`](#RelayClient) 當房間的演出端，再用 [`bindSignals()`](#bindSignals) 把每個手機訊號歸進 `signals`（所以 `phone/any/…` 永遠是最新那支手機的值），然後用 `surface`、`world.surface` 或參數的 [`autoSurface()`](#autoSurface) 建立版面，接著把 `surface/… → param` 路由加進映射器，最後，你每個畫面呼叫 `host.frame(dt, state)` 的話，它大約每秒 `feedbackHz` 次把算好的參數值送回去，讓手機上的推桿跟著時間軸動。"},{"en":"Making the host does not connect it: call `host.connect()` when you want the socket. On every (re)connection it publishes the layout as relay config `surface`, so a phone that joins later gets the right panel. `host.layout`, `host.routes` and `host.bindings` are there to look at straight away, and `host.dispose()` closes the socket.","zh":"做出 host 並不會連線：要連的時候呼叫 `host.connect()`。每次（重新）連上時，它會把版面當成中繼設定 `surface` 發布出去，所以之後才加入的手機也拿得到正確的面板。`host.layout`、`host.routes`、`host.bindings` 一做出來就能看，`host.dispose()` 會把 socket 關掉。"}],"params":[{"name":"signals","type":"Signals","en":"The registry that phone signals are filed in.","zh":"手機訊號要歸進去的註冊表。"},{"name":"params","type":"Params","en":"The world's params. Its `schema` is what the control surface is built from.","zh":"世界的參數。控制面板就是依它的 `schema` 建立的。"},{"name":"mapper","type":"Mapper","en":"The mapper that gets the `surface/… → param` routes (a route it already has is not added twice).","zh":"會收到 `surface/… → param` 路由的映射器（它已經有的路由不會重複加）。"},{"name":"world","type":"Object","optional":true,"en":"The world: its `params` are used when `params.schema` is empty, its `name` becomes the layout's title, and its `surface` (a hand-written layout) is used instead of a generated one.","zh":"世界物件：`params.schema` 是空的時候用它的 `params`，它的 `name` 會變成版面的標題，它的 `surface`（手寫的版面）會取代自動產生的。"},{"name":"room","type":"String","optional":true,"en":"The relay room to join.","zh":"要加入的中繼房間。"},{"name":"auto","type":"Object","optional":true,"en":"Options for [`autoSurface()`](#autoSurface): `pairs`, `meters`, `style`, `perPage`, `smooth`, `title`.","zh":"傳給 [`autoSurface()`](#autoSurface) 的選項：`pairs`、`meters`、`style`、`perPage`、`smooth`、`title`。"},{"name":"surface","type":"Object","optional":true,"en":"An explicit layout. It wins over `world.surface`.","zh":"指定的版面。它比 `world.surface` 優先。"},{"name":"feedbackHz","type":"Number","optional":true,"en":"How many times a second `host.frame()` sends the param values back to the phones.","zh":"`host.frame()` 每秒把參數值送回手機幾次。"},{"name":"url","type":"String","optional":true,"en":"A relay to use instead of this page's own `/relay`, such as `wss://host/relay`. `role`, `room` and `id` are added to it for you. The runner id is then `show-` plus four random characters, so two tabs of one show do not collide; without `url` it is `show`.","zh":"不用這個頁面自己的 `/relay`，改用的中繼，例如 `wss://host/relay`。`role`、`room`、`id` 會替你補上去。這時演出端的 id 是 `show-` 加四個隨機字元，所以同一個演出開兩個分頁也不會打架。沒給 `url` 的話就是 `show`。"},{"name":"mode","type":"'local' | 'public' | 'custom'","optional":true,"en":"What `pickRelay()` decided. Only the words of the join card depend on it; the host reports it as `host.relayMode`.","zh":"`pickRelay()` 的決定。只有加入卡片上的字會依它而變，host 會把它報成 `host.relayMode`。"},{"name":"controllers","type":"MidiControllers","optional":true,"en":"The on-screen MIDI controllers of `@openav/midi`: the phone's MIDI tab then shows the same device, and what is played on either screen shows on the other.","zh":"`@openav/midi` 的螢幕 MIDI 控制器：這樣手機的 MIDI 分頁會顯示同一台裝置，兩邊畫面上彈的音也會互相顯示。"}],"returns":{"type":"Object","en":"`{ relay, layout, routes, bindings, publish, room, relayUrl, relayMode, onStatus, frame(dt, state), connect(), dispose() }`. `connect()` returns the host again.","zh":"`{ relay, layout, routes, bindings, publish, room, relayUrl, relayMode, onStatus, frame(dt, state), connect(), dispose() }`。`connect()` 會再回傳 host 本身。"},"needs":{"en":"A relay for `connect()` to reach: `node serve.js` hosts one at `/relay`, a static site uses the public relay (see `pickRelay()`), or pass `url`. Building the host needs none of that.","zh":"要有中繼讓 `connect()` 連：`node serve.js` 在 `/relay` 架了一個，靜態網站用公開中繼（見 `pickRelay()`），或傳 `url`。單單做出 host 不需要這些。"},"notes":[],"examples":[{"code":"import { mountRemoteHost } from '@openav/remote/host';\nimport { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\n\nconst world = { name: 'Bloom', params: [\n  { key: 'hue', min: 0, max: 360, def: 200 },\n  { key: 'burst', pulse: true },\n] };\nconst signals = new Signals();\nconst params = new Params(world.params);\nconst mapper = new Mapper({ signals, params });\n\nconst host = mountRemoteHost({ signals, params, mapper, world, room: 'demo' });   // not connected yet\ndocument.getElementById('app').innerHTML = [\n  'title: ' + host.layout.title,\n  'widgets: ' + host.layout.pages[0].widgets.map((w) => w.id + ' (' + w.type + ')').join(', '),\n  'routes: ' + host.routes.map((r) => r.source + ' > ' + r.target).join(', '),\n  'in the mapper: ' + mapper.routes.length,\n].join('<br>');\nhost.dispose();","caption":{"en":"Prints what the host built from two params: a layout titled Bloom with a fader and a button, the two routes, and that the mapper now has them.","zh":"印出 host 用兩個參數建出來的東西：標題是 Bloom、有一支推桿和一顆按鈕的版面、兩條路由，以及映射器現在已經有這兩條。"},"run":"module","lang":"js"},{"code":"import { mountRemoteHost, mountJoinCard } from '@openav/remote/host';\n\n// signals, params, mapper and world as above\nconst host = mountRemoteHost({ signals, params, mapper, world, room: 'main' }).connect();\nmountJoinCard(document.body, host);   // the QR code and URL for phones\n\nfunction frame(dt, state) {\n  host.frame(dt, state);               // echo the params back to the phones\n}","caption":{"en":"The usual wiring on a show page: connect, show the join card, and call `host.frame()` from the frame loop. A phone that scans the card moves the faders.","zh":"演出頁面上的一般接法：連線、顯示加入卡片、在畫面迴圈裡呼叫 `host.frame()`。手機掃了卡片上的 QR code，就能推動推桿。"},"run":"static","lang":"js"}],"related":["mountJoinCard","RelayClient","bindSignals","autoSurface","routesFromLayout","feedbackFor","attachRelay","mountRemote"],"keywords":{"en":["remote host","show side","runner","phone remote","surface","feedback","relay","modules remote","createShow"],"zh":["遙控演出端","演出這一邊","runner","手機遙控","控制面板","回饋","中繼","modules remote","createShow"]},"tbd":[],"claims":[]},
{"id":"PhoneSensors","key":"PhoneSensors","name":"PhoneSensors","aliases":[],"kind":"class","package":"@openav/remote","category":"remote","documented":true,"signature":"new PhoneSensors(io, hooks = {})","import":"import { PhoneSensors } from '@openav/remote';","source":{"file":"packages/remote/sensors.js","line":24,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/remote/sensors.js#L24"},"summary":{"en":"Turns a phone's tilt, acceleration, rotation, knocks and front-camera light into named signals, in screen axes, and hands them to the `io` you give it.","zh":"把手機的傾斜、加速度、旋轉、敲擊和前鏡頭的光線，換成有名字的訊號（用螢幕座標軸），交給你給它的 `io`。"},"description":[{"en":"This is the phone half of the sense tab, with no page in it: make one with an `io`, call `start()` from a tap, and signals flow. From the phone's own events it produces `tilt/x` and `tilt/y` (−1..1, which is ±45° from the resting pose, x to the right and y toward you, in screen axes, so a phone turned sideways still reports \"tilt right is +x\"), `orient/alpha|beta|gamma` (degrees), `accel/x|y|z` (m/s², gravity included), `rot/alpha|beta|gamma` (degrees per second), `knock` (a pulse `{ strength, delta, t }`) and, if you turn the camera on, `light` (0..1).","zh":"這是 感測 分頁在手機這一半的邏輯，裡面不含頁面：給它一個 `io`，在使用者點一下時呼叫 `start()`，訊號就開始流動。它用手機自己的事件產生 `tilt/x` 和 `tilt/y`（−1..1，也就是離靜止姿勢 ±45°，x 向右、y 朝向你，用的是螢幕座標軸，所以手機橫著拿也還是「往右傾是 +x」）、`orient/alpha|beta|gamma`（度）、`accel/x|y|z`（m/s²，含重力）、`rot/alpha|beta|gamma`（每秒幾度）、`knock`（脈衝 `{ strength, delta, t }`），打開鏡頭的話還有 `light`（0..1）。"},{"en":"A knock is a jolt: when the acceleration vector changes by more than `threshold` (10 m/s² by default) between two motion samples, and at least 130 ms after the last knock, `io.send('knock', { strength, delta, t }, true)` fires. `delta` is the size of the change, and `strength` runs from 0.15 just over the threshold up to 1. The phone also vibrates for 10 ms where it can. Hold the phone how you want to play it and call `calibrate()` to make that pose the zero of the tilt.","zh":"敲擊就是猛地一震：兩筆動作樣本之間，加速度向量的變化超過 `threshold`（預設 10 m/s²），而且距離上次敲擊至少 130 毫秒，就會觸發 `io.send('knock', { strength, delta, t }, true)`。`delta` 是變化的大小，`strength` 從剛過門檻的 0.15 到 1。手機能震動的話也會震 10 毫秒。把手機拿成你想演奏的姿勢，呼叫 `calibrate()`，就把那個姿勢當成傾斜的零點。"},{"en":"For the show page itself running on a phone, [`localSensors()`](#localSensors) wraps this class and files straight into a `Signals`.","zh":"演出頁面本身就跑在手機上的話，用 [`localSensors()`](#localSensors)，它包住這個類別，直接把訊號歸進 `Signals`。"}],"params":[{"name":"io","type":"{ set(name, value), send(name, value, pulse?) }","en":"Where the signals go. `set` gets the continuous ones, `send` the knocks (with `pulse` true). The names have no `phone/<id>/` in front: `tilt/x`, `accel/z`. A [`RelayClient`](#RelayClient) with a prefix, or a `Signals` registry, can sit behind it.","zh":"訊號要送去哪裡。`set` 收連續的訊號，`send` 收敲擊（`pulse` 為 true）。名稱前面沒有 `phone/<id>/`，是 `tilt/x`、`accel/z` 這樣。後面可以接加了前綴的 [`RelayClient`](#RelayClient)，或是 `Signals`。"},{"name":"hooks","type":"{ onKnock?, onSample?, notify? }","en":"Optional callbacks. `onKnock(strength)` after each knock, `onSample(name, value)` for every continuous sample (to draw a readout), `notify(message)` with a one-line problem report such as a refused permission. The messages are written in Chinese.","zh":"選用的回呼。`onKnock(strength)` 在每次敲擊之後呼叫，`onSample(name, value)` 每個連續樣本都呼叫（拿來畫讀數），`notify(message)` 收一行問題說明，例如權限被拒絕。訊息是中文寫的。"}],"needs":{"en":"Call `start()` from a tap or click, not on page load. iOS asks for motion permission only inside a user gesture, and it only gives motion sensors to pages on `https://`; touch and keys work over plain `http://`. `start()` always resolves `true`: look at `denied` for a refusal. If no sensor data arrives within 2.5 s, `hooks.notify` says so (a desktop, or an iPhone page on http). `setCamera(true)` also needs a gesture and HTTPS on iOS.","zh":"要在點擊或觸碰時才呼叫 `start()`，不是載入頁面時。iOS 只在使用者手勢裡才會問動作權限，而且只把動作感測器開放給 `https://` 的頁面，觸控和琴鍵在一般的 `http://` 就能用。`start()` 一律回傳 `true`，被拒絕要看 `denied`。2.5 秒內沒有任何感測資料進來的話，`hooks.notify` 會說明（桌機，或 http 的 iPhone 頁面）。`setCamera(true)` 在 iOS 上同樣要手勢和 HTTPS。"},"notes":[{"en":"Public fields: `io`, `hooks`, `started`, `gotSensor` (true once any sensor data has arrived), `denied` (iOS said no; it stays no until the Settings app is changed), `rest` and `deg` (tilt in degrees), `threshold` (the knock threshold, 10) and `COOLDOWN` (130 ms).","zh":"公開的欄位有 `io`、`hooks`、`started`、`gotSensor`（收過任何感測資料就是 true）、`denied`（iOS 說不行，要到「設定」裡改才會變）、`rest` 和 `deg`（傾斜，單位度）、`threshold`（敲擊門檻，10）和 `COOLDOWN`（130 毫秒）。"}],"examples":[{"code":"import { PhoneSensors } from '@openav/remote';\n\nconst seen = [];\nconst sensors = new PhoneSensors({\n  set: (name, value) => seen.push(name + ' = ' + value),\n  send: (name, value) => seen.push(name + ' ' + JSON.stringify(value)),\n});\nawait sensors.start();   // on a phone: call this from a tap\n\n// the event a phone fires; here a made-up one, tipped 22.5° toward you and 45° to the left\nwindow.dispatchEvent(new DeviceOrientationEvent('deviceorientation', { alpha: 5, beta: 22.5, gamma: -45 }));\nsensors.stop();\ndocument.getElementById('app').innerHTML = seen.join('<br>');","caption":{"en":"Prints tilt/x = -1 (45° left is the end of the range), tilt/y = 0.5, then the three raw orient angles.","zh":"印出 tilt/x = -1（向左 45° 就是範圍的盡頭）、tilt/y = 0.5，然後是三個原始的 orient 角度。"},"run":"module","lang":"js"},{"code":"import { PhoneSensors } from '@openav/remote';\n\nconst seen = [];\nconst sensors = new PhoneSensors(\n  { set() {}, send: (name, v) => seen.push(name + ': strength ' + v.strength + ', delta ' + v.delta) },\n  { onKnock: (s) => seen.push('onKnock(' + s + ')') },\n);\nawait sensors.start();\nawait new Promise((r) => setTimeout(r, 200));   // the 130 ms knock cooldown counts from page load\nnavigator.vibrate = () => true;                 // a knock also buzzes the phone; a stand-in for a desktop page\n\nconst motion = (x) => window.dispatchEvent(new DeviceMotionEvent('devicemotion', {\n  accelerationIncludingGravity: { x, y: 0, z: 9.8 },\n}));\nmotion(0);    // at rest, gravity only\nmotion(15);   // a jolt: the acceleration changed by 15 m/s²\nmotion(15.5); // too small to count\nsensors.stop();\nseen.push('threshold now ' + sensors.cycleThreshold());\ndocument.getElementById('app').innerHTML = seen.join('<br>');","caption":{"en":"Prints one knock with strength 0.35 and delta 15 (and the onKnock call), then the new threshold 12 after cycleThreshold().","zh":"印出一次敲擊，strength 是 0.35、delta 是 15（還有 onKnock 的呼叫），然後是 cycleThreshold() 之後的新門檻 12。"},"run":"module","lang":"js"},{"code":"import { PhoneSensors } from '@openav/remote';\n\n// the same tilt (beta 10, gamma 20) as the screen turns: x is always \"to the right of the screen\"\nconst rows = [0, 90, 180, 270].map((angle) => {\n  const { x, y } = PhoneSensors.screenTilt(10, 20, angle);\n  return 'screen angle ' + angle + ': x ' + x + ', y ' + y;\n});\ndocument.getElementById('app').innerHTML = rows.join('<br>');","caption":{"en":"Prints x and y for the four screen angles: (20, 10), (10, -20), (-20, -10), (-10, 20).","zh":"印出四個螢幕角度的 x 和 y：(20, 10)、(10, -20)、(-20, -10)、(-10, 20)。"},"run":"module","lang":"js"}],"related":["localSensors","attachTouchPad","RelayClient","mountRemote","signalMeta"],"keywords":{"en":["phone sensors","tilt","accelerometer","gyroscope","deviceorientation","devicemotion","knock","shake","light sensor","ios permission","motion"],"zh":["手機感測器","傾斜","加速度","陀螺儀","動作感測","敲擊","搖晃","光線","ios 權限","動作"]},"tbd":[{"en":"Not tried on a real iPhone or Android phone for this entry: the iOS permission flow, the HTTPS requirement and the 2.5 s \"no data\" message are read from the code and from docs/remote.md. The examples feed the class real `DeviceOrientationEvent` and `DeviceMotionEvent` objects in a desktop browser instead.","zh":"這一條沒有用真正的 iPhone 或 Android 手機試過：iOS 的權限流程、HTTPS 的要求、2.5 秒的「沒有資料」訊息，都是從程式碼和 docs/remote.md 讀來的。範例是在桌面瀏覽器裡，把真正的 `DeviceOrientationEvent` 和 `DeviceMotionEvent` 餵給這個類別。"}],"claims":[],"members":[{"name":"start","member":"method","signature":"","doc":"MUST be called inside a user gesture (iOS permission rules).","line":43},{"name":"stop","member":"method","signature":"","doc":"","line":59},{"name":"calibrate","member":"method","signature":"","doc":"","line":65},{"name":"cycleThreshold","member":"method","signature":"","doc":"","line":66},{"name":"setCamera","member":"method","signature":"on","doc":"光照：front-camera mean luma, 32×24 samples","line":113},{"name":"screenTilt","member":"static method","signature":"beta, gamma, angle = 0","doc":"Rotate (gamma, beta) into screen axes using the current screen angle.","line":69}]},
{"id":"autoArrange","key":"autoArrange","name":"autoArrange","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"autoArrange(items, cols, fixed = [])","import":"import { autoArrange } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":95,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L95"},"summary":{"en":"Packs widgets into a grid first-fit, row by row, and never overlaps; `fixed` placements are kept and block their cells. Returns the placements and the rows used.","zh":"把元件依序排進格線，一列一列找第一個放得下的位置，絕不重疊，`fixed` 的位置會保留，並擋住它們的格子。回傳各元件的位置和用掉的列數。"},"description":[{"en":"Each item takes the first free spot found by scanning left to right, then top to bottom: what a person does laying out a controller. The grid grows downward as far as it must, so `rows` is a result, not a limit. [`resolvePage()`](#resolvePage) uses it for widgets without a position and for the automatic reflow.","zh":"每個項目都從左到右、再從上到下找第一個空位放進去：就是人在排控制器時的做法。格線需要多長就往下長多長，所以 `rows` 是結果，不是上限。[`resolvePage()`](#resolvePage) 用它來排沒有位置的元件，也用在自動重排。"}],"params":[{"name":"items","type":"{ id, w, h }[]","en":"What to place, in order. Every item needs a numeric `w` and `h` in cells, at least 1. A `w` wider than the grid is cut to `cols`.","zh":"要擺的東西，依序排。每一項都要有數字的 `w` 和 `h`（單位是格，至少 1）。`w` 比格線寬的話會被截成 `cols`。"},{"name":"cols","type":"Number","en":"How many columns the grid has.","zh":"格線有幾欄。"},{"name":"fixed","type":"{ id, x, y, w, h }[]","optional":true,"en":"Placements that are already decided. They come first in the result and the other items go around them.","zh":"已經決定的位置。它們會排在結果的最前面，其他項目繞著它們擺。"}],"returns":{"type":"{ placements, rows }","en":"`placements` is a list of `{ id, x, y, w, h }` (the fixed ones first), `rows` is how many rows they fill.","zh":"`placements` 是 `{ id, x, y, w, h }` 的清單（固定的排前面），`rows` 是它們佔了幾列。"},"notes":[{"en":"An item with no `w` (or a `w` that is not a number) never finds a spot and the function never returns. Give every item a `w` and an `h`; [`DEFAULT_SIZE`](#DEFAULT_SIZE) has the natural size of each widget type.","zh":"沒有 `w`（或 `w` 不是數字）的項目永遠找不到位置，函式也就永遠不會結束。每個項目都要給 `w` 和 `h`。[`DEFAULT_SIZE`](#DEFAULT_SIZE) 有每種元件類型的預設大小。"}],"examples":[{"code":"import { autoArrange } from '@openav/surface/layout';\n\nconst { placements, rows } = autoArrange([\n  { id: 'a', w: 4, h: 2 }, { id: 'b', w: 4, h: 2 }, { id: 'c', w: 8, h: 1 }, { id: 'd', w: 2, h: 2 },\n], 8);\ndocument.getElementById('app').innerHTML = placements\n  .map((p) => p.id + ': x ' + p.x + ', y ' + p.y + ', ' + p.w + '×' + p.h).join('<br>') + '<br>rows: ' + rows;","caption":{"en":"a and b share the first row, c takes a full row, d goes below: rows is 5.","zh":"a 和 b 共用第一列，c 佔滿一整列，d 排在下面：rows 是 5。"},"run":"module","lang":"js"},{"code":"import { autoArrange } from '@openav/surface/layout';\n\n// a button that must stay at the top left; the new knob goes around it\nconst { placements } = autoArrange(\n  [{ id: 'knob', w: 2, h: 1 }],\n  4,\n  [{ id: 'go', x: 0, y: 0, w: 2, h: 1 }],\n);\ndocument.getElementById('app').textContent = placements.map((p) => p.id + ' at ' + p.x + ',' + p.y).join('   ');","caption":{"en":"Prints \"go at 0,0   knob at 2,0\": the fixed placement comes first and the knob takes the first free cells.","zh":"印出「go at 0,0   knob at 2,0」：固定的位置排在最前面，旋鈕拿第一組空的格子。"},"run":"module","lang":"js"}],"related":["resolvePage","stretchRows","DEFAULT_SIZE"],"keywords":{"en":["auto arrange","pack","grid","first fit","layout algorithm","place widgets","no overlap"],"zh":["自動排列","打包","格線","第一個放得下","排版演算法","擺放元件","不重疊"]},"tbd":[],"claims":[]},
{"id":"autoSurface","key":"autoSurface","name":"autoSurface","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"autoSurface(params = [], { perPage = 8, style = 'auto', pairs = [], meters = [], smooth = 0.04, title = '' } = {})","import":"import { autoSurface } from '@openav/surface';","source":{"file":"packages/surface/auto.js","line":54,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/auto.js#L54"},"summary":{"en":"Turns a world's params into a control panel: a layout, the routes that drive the params, and the bindings for echoing values back.","zh":"把世界的參數變成控制面板：一份版面、驅動參數的路由，以及把數值回送用的綁定。"},"description":[{"en":"This is how a world that declares what it can be performed with gets a phone panel for free: `const { layout, routes, bindings } = autoSurface(world.params)`, then `routes.forEach((r) => mapper.addRoute(r))` and `new Surface(el, { layout, sink })`. Each param becomes the widget [`widgetTypeFor()`](#widgetTypeFor) picks. Each `group` of params gets its own page (a param without a group goes on `main`), long groups continue on `main2`, `main3` …, and a pair from `pairs` becomes one XY pad with two routes, `…/x` and `…/y`.","zh":"一個宣告了「可以拿什麼來演奏」的世界，就這樣免費得到手機面板：`const { layout, routes, bindings } = autoSurface(world.params)`，再 `routes.forEach((r) => mapper.addRoute(r))` 和 `new Surface(el, { layout, sink })`。每個參數變成 [`widgetTypeFor()`](#widgetTypeFor) 挑的元件。每個 `group` 各有一頁（沒有 group 的參數放在 `main`），太長的群組接著放到 `main2`、`main3` …，`pairs` 裡的一對會變成一塊有兩條路由（`…/x` 和 `…/y`）的 XY 板。"},{"en":"Widget ids are the param keys (anything outside `A–Z a–z 0–9 _ -` becomes `_`), colours go round cyan, amber, magenta, lime, violet, coral, and a radio's `options` come from the param or are the numbers. A pulse param gets no binding, since there is no value to show. The routes already scale the normalized signal to the param's `min` and `max`. A world that comes with its own layout uses [`routesFromLayout()`](#routesFromLayout) instead.","zh":"元件的 id 就是參數鍵（`A–Z a–z 0–9 _ -` 以外的字元會變成 `_`），顏色依序輪流用 cyan、amber、magenta、lime、violet、coral，radio 的 `options` 來自參數，沒有就用數字。脈衝參數沒有 binding，因為沒有值可以顯示。路由已經把正規化的訊號換算成參數的 `min` 和 `max`。自己帶版面的世界，改用 [`routesFromLayout()`](#routesFromLayout)。"}],"params":[{"name":"params","type":"Object[]","en":"The param declarations: `{ key, label?, min, max, def, step?, pulse?, group?, options?, surface? }`. `surface: { type, color, label }` on a param overrides the choice.","zh":"參數宣告：`{ key, label?, min, max, def, step?, pulse?, group?, options?, surface? }`。參數上的 `surface: { type, color, label }` 可以蓋過自動的選擇。"},{"name":"perPage","type":"Number","optional":true,"en":"How much fits on a page, counted in weights: a fader or a meter weighs 1, an XY pad 4, anything else 2. The default 8 is eight faders, four knobs or two XY pads. A page that scrolls is a page that fails, so a full page starts the next.","zh":"一頁放得下多少，以權重計算：推桿或音量表算 1，XY 板算 4，其他都算 2。預設的 8 就是八支推桿、四個旋鈕或兩塊 XY 板。要捲動的頁面就是失敗的頁面，所以滿了就開下一頁。"},{"name":"style","type":"'auto' | 'fader' | 'knob'","optional":true,"en":"Faders or knobs for the continuous params. `'auto'` picks faders up to six, knobs beyond.","zh":"連續參數用推桿還是旋鈕。`'auto'` 六個以內用推桿，超過就用旋鈕。"},{"name":"pairs","type":"string[][]","optional":true,"en":"Pairs of param keys to drive with one XY pad: `[['cx', 'cy']]`. The first key is x and the second is y.","zh":"要用同一塊 XY 板驅動的參數鍵配對：`[['cx', 'cy']]`。第一個鍵是 x，第二個是 y。"},{"name":"meters","type":"string[]","optional":true,"en":"Param keys to also show as read-only meters, on a page called Meters.","zh":"另外要顯示成唯讀音量表的參數鍵，放在一頁叫 Meters 的頁面。"},{"name":"smooth","type":"Number","optional":true,"en":"Seconds of smoothing on the continuous routes, to hide the steps of a 30 Hz network (zipper noise). Pulses, toggles and radios are never smoothed.","zh":"連續路由的平滑秒數，用來遮住 30 Hz 網路造成的階梯感（拉鍊雜訊）。脈衝、開關和單選不會被平滑。"},{"name":"title","type":"String","optional":true,"en":"The layout's title.","zh":"版面的標題。"}],"returns":{"type":"{ layout, routes, bindings }","en":"`layout` is a layout JSON for [`Surface`](#Surface). `routes` is a list of mapper routes `{ source, target, inMin, inMax, curve, smooth, outMin, outMax }`, from `surface/<page>/<id>` to each param. `bindings` is `{ param, name, page, widget, min, max, readonly? }` for [`feedbackFor()`](#feedbackFor).","zh":"`layout` 是給 [`Surface`](#Surface) 用的版面 JSON。`routes` 是映射器路由的清單 `{ source, target, inMin, inMax, curve, smooth, outMin, outMax }`，從 `surface/<page>/<id>` 到各個參數。`bindings` 是給 [`feedbackFor()`](#feedbackFor) 用的 `{ param, name, page, widget, min, max, readonly? }`。"},"notes":[],"examples":[{"code":"import { autoSurface } from '@openav/surface/auto';\n\nconst params = [\n  { key: 'hue', min: 0, max: 360, def: 200 },\n  { key: 'size', min: 1, max: 10, def: 4 },\n  { key: 'mode', min: 0, max: 3, step: 1, def: 1 },\n  { key: 'burst', pulse: true },\n];\nconst { layout, routes } = autoSurface(params, { title: 'Bloom' });\ndocument.getElementById('app').innerHTML = [\n  layout.pages.map((p) => p.id + ': ' + p.widgets.map((w) => w.id + ' ' + w.type).join(', ')).join('<br>'),\n  routes.map((r) => r.source + ' → ' + r.target + (r.smooth ? ' (smooth ' + r.smooth + ')' : '')).join('<br>'),\n].join('<br><br>');","caption":{"en":"Prints the one page main with a fader for hue and size, a radio for mode and a button for burst, then the four routes, smoothed by 0.04 s except the radio and the button.","zh":"印出一頁 main，有 hue 和 size 的推桿、mode 的單選、burst 的按鈕，然後是四條路由，除了單選和按鈕之外都平滑 0.04 秒。"},"run":"module","lang":"js"},{"code":"import { autoSurface } from '@openav/surface/auto';\n\nconst params = [\n  { key: 'cx', min: 0, max: 1, def: 0.5 }, { key: 'cy', min: 0, max: 1, def: 0.5 },\n  { key: 'density', min: 0, max: 100, def: 20 },\n  { key: 'glow', min: 0, max: 1, def: 0.3, group: 'look' },\n];\nconst { layout, bindings } = autoSurface(params, { pairs: [['cx', 'cy']], meters: ['density'] });\ndocument.getElementById('app').innerHTML = [\n  layout.pages.map((p) => p.id + ' (' + p.title + '): ' + p.widgets.map((w) => w.id + ' ' + w.type).join(', ')).join('<br>'),\n  'bindings: ' + bindings.map((b) => b.param + (b.readonly ? ' (read-only)' : '')).join(', '),\n].join('<br><br>');","caption":{"en":"cx and cy share one XY pad, density is a fader and also a read-only meter on its own page, and glow, in group look, gets a page of its own.","zh":"cx 和 cy 共用一塊 XY 板，density 是推桿、同時也是自己那一頁上的唯讀音量表，group 是 look 的 glow 另外有一頁。"},"run":"module","lang":"js"},{"code":"import { autoSurface } from '@openav/surface/auto';\n\n// twenty params do not fit on one page: more than six continuous ones become knobs, four to a page\nconst params = Array.from({ length: 20 }, (_, i) => ({ key: 'p' + i, min: 0, max: 1, def: 0 }));\nconst { layout } = autoSurface(params);\ndocument.getElementById('app').textContent = layout.pages\n  .map((p) => p.id + ': ' + p.widgets.length + ' ' + p.widgets[0].type + 's').join('   ');","caption":{"en":"Prints \"main1: 4 knobs   main2: 4 knobs …\" up to main5: the page limit counts a knob as 2, so four fit in the default 8.","zh":"印出「main1: 4 knobs   main2: 4 knobs …」一直到 main5：頁面上限把旋鈕算成 2，所以預設的 8 放得下四個。"},"run":"module","lang":"js"}],"related":["widgetTypeFor","feedbackFor","routesFromLayout","Surface","mountRemoteHost","DEFAULT_SIZE"],"keywords":{"en":["auto surface","params to panel","generate layout","control panel from params","routes","bindings","xy pairs","meters","phone panel"],"zh":["自動面板","參數變面板","產生版面","依參數建立面板","路由","綁定","xy 配對","音量表","手機面板"]},"tbd":[],"claims":[]},
{"id":"canFullscreen","key":"canFullscreen","name":"canFullscreen","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"canFullscreen()","import":"import { canFullscreen } from '@openav/surface';","source":{"file":"packages/surface/platform.js","line":35,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/platform.js#L35"},"summary":{"en":"True when the page can ask for fullscreen: the browser has `requestFullscreen` (or the WebKit one). The code treats iPhone Safari as not having it.","zh":"頁面可以要求全螢幕的時候是 true：瀏覽器有 `requestFullscreen`（或 WebKit 版的）。程式碼把 iPhone 的 Safari 當成沒有。"},"description":[{"en":"Use it to decide whether to show a fullscreen button: `if (canFullscreen()) button.hidden = false`. It only checks that the function exists; whether a request succeeds is up to the browser, which wants it to come from a tap ([`toggleFullscreen()`](#toggleFullscreen)).","zh":"用來決定要不要顯示全螢幕按鈕：`if (canFullscreen()) button.hidden = false`。它只檢查那個函式存不存在。要求會不會成功由瀏覽器決定，瀏覽器要求它必須來自一次點擊（[`toggleFullscreen()`](#toggleFullscreen)）。"}],"params":[],"returns":{"type":"Boolean","en":"`true` or `false`. `false` without a `document`.","zh":"`true` 或 `false`。沒有 `document` 的話是 `false`。"},"notes":[],"examples":[{"code":"import { canFullscreen } from '@openav/surface';\n\ndocument.getElementById('app').textContent = 'this browser can ask for fullscreen: ' + canFullscreen();","caption":{"en":"Prints \"true\" in desktop Chrome, Firefox and Safari, and in Android Chrome.","zh":"在桌面的 Chrome、Firefox、Safari 和 Android 的 Chrome 上印出「true」。"},"run":"module","lang":"js"}],"related":["toggleFullscreen","mountRemote"],"keywords":{"en":["fullscreen","can fullscreen","requestFullscreen","full screen button","ios safari"],"zh":["全螢幕","能不能全螢幕","requestFullscreen","全螢幕按鈕","ios safari"]},"tbd":[{"en":"Not checked on an iPhone or iPad: that iPhone Safari has no fullscreen function is what the code's own comment says, not something measured here.","zh":"沒有在 iPhone 或 iPad 上檢查過：iPhone 的 Safari 沒有全螢幕函式，這是程式碼自己的註解說的，不是在這裡量到的。"}],"claims":[]},
{"id":"COLOR_NAMES","key":"COLOR_NAMES","name":"COLOR_NAMES","aliases":[],"kind":"constant","package":"@openav/surface","category":"remote","documented":true,"signature":"COLOR_NAMES = array, 7 items","import":"import { COLOR_NAMES } from '@openav/surface';","source":{"file":"packages/surface/theme.js","line":30,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/theme.js#L30"},"summary":{"en":"The seven colour names a widget's `color` may be: cyan, amber, magenta, lime, violet, coral, white.","zh":"元件的 `color` 可以用的七個顏色名稱：cyan、amber、magenta、lime、violet、coral、white。"},"description":[{"en":"`{ id: 'hue', type: 'knob', color: 'amber' }` makes the widget's fill, its value readout and its pressed outline amber. A widget without a colour uses `--oav-accent` of [`THEME_VARS`](#THEME_VARS). [`autoSurface()`](#autoSurface) hands out the first six in turn (white is left for you). A name that is not in the list gets no colour of its own and falls back to the accent; [`validateLayout()`](#validateLayout) does not check it.","zh":"`{ id: 'hue', type: 'knob', color: 'amber' }` 會讓元件的填色、數值讀數和按下時的描邊都變成琥珀色。沒有顏色的元件用 [`THEME_VARS`](#THEME_VARS) 的 `--oav-accent`。[`autoSurface()`](#autoSurface) 會輪流發前六個（white 留給你用）。不在清單裡的名稱沒有自己的顏色，會退回強調色。[`validateLayout()`](#validateLayout) 不檢查它。"}],"value":{"type":"string[]","en":"`['cyan', 'amber', 'magenta', 'lime', 'violet', 'coral', 'white']`.","zh":"七個字串：`['cyan', 'amber', 'magenta', 'lime', 'violet', 'coral', 'white']`。"},"params":[],"notes":[],"examples":[{"code":"import { COLOR_NAMES } from '@openav/surface';\n\ndocument.getElementById('app').textContent = COLOR_NAMES.join(', ');","caption":{"en":"Prints cyan, amber, magenta, lime, violet, coral, white.","zh":"印出 cyan、amber、magenta、lime、violet、coral、white。"},"run":"module","lang":"js"}],"related":["THEME_VARS","autoSurface","WIDGET_TYPES"],"keywords":{"en":["color","colour","color names","widget color","cyan","amber","magenta","palette"],"zh":["顏色","色名","元件顏色","青色","琥珀","洋紅","調色盤"]},"tbd":[],"claims":[]},
{"id":"DEFAULT_SIZE","key":"DEFAULT_SIZE","name":"DEFAULT_SIZE","aliases":[],"kind":"constant","package":"@openav/surface","category":"remote","documented":true,"signature":"DEFAULT_SIZE = object, 14 keys","import":"import { DEFAULT_SIZE } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":31,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L31"},"summary":{"en":"The natural size of each widget type, in grid cells, as `{ w, h }`: a layout only has to give `x`, `y`, `w`, `h` when it cares.","zh":"每種元件類型在格線上的預設大小，格式是 `{ w, h }`（以格為單位）：版面只有在在意的時候才需要寫 `x`、`y`、`w`、`h`。"},"description":[{"en":"A widget without a `w` or `h` takes the size of its type, held to the number of columns of the grid. [`autoArrange()`](#autoArrange) and [`resolvePage()`](#resolvePage) use it, and so does [`autoSurface()`](#autoSurface) when it counts how many widgets fit on a page. A type it does not know counts as 2×2.","zh":"沒寫 `w` 或 `h` 的元件，會用它的類型的大小，並限制在格線的欄數以內。[`autoArrange()`](#autoArrange) 和 [`resolvePage()`](#resolvePage) 會用到它，[`autoSurface()`](#autoSurface) 計算一頁放得下幾個元件的時候也是。不認得的類型當作 2×2。"}],"value":{"type":"Object","en":"Fourteen keys, one per type: fader 1×4, knob 2×2, encoder 2×2, button 2×1, toggle 2×1, xy 4×4, bank 4×4, radio 4×1, pads 4×4, keyboard 8×4, number 2×1, text 4×1, label 2×1, meter 1×4 (width × height).","zh":"十四個鍵，每種類型一個：fader 1×4、knob 2×2、encoder 2×2、button 2×1、toggle 2×1、xy 4×4、bank 4×4、radio 4×1、pads 4×4、keyboard 8×4、number 2×1、text 4×1、label 2×1、meter 1×4（寬 × 高）。"},"params":[],"notes":[],"examples":[{"code":"import { DEFAULT_SIZE } from '@openav/surface';\n\nconst { w, h } = DEFAULT_SIZE.knob;\ndocument.getElementById('app').textContent = 'a knob is ' + w + ' cells wide and ' + h + ' tall; a fader is ' +\n  DEFAULT_SIZE.fader.w + ' by ' + DEFAULT_SIZE.fader.h;","caption":{"en":"Prints \"a knob is 2 cells wide and 2 tall; a fader is 1 by 4\".","zh":"印出「a knob is 2 cells wide and 2 tall; a fader is 1 by 4」。"},"run":"module","lang":"js"}],"related":["WIDGET_TYPES","autoArrange","resolvePage"],"keywords":{"en":["default size","widget size","grid cells","width height","w h"],"zh":["預設大小","元件大小","格","寬高","w h"]},"tbd":[],"claims":[]},
{"id":"feedbackFor","key":"feedbackFor","name":"feedbackFor","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"feedbackFor(state, bindings)","import":"import { feedbackFor } from '@openav/surface';","source":{"file":"packages/surface/auto.js","line":141,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/auto.js#L141"},"summary":{"en":"Turns the resolved param state into the normalized values the phone's widgets should show right now: `[{ name, value }]`.","zh":"把算好的參數狀態，換成手機上的元件現在該顯示的正規化數值：`[{ name, value }]`。"},"description":[{"en":"It is pure and cheap, so the show calls it about ten times a second and sends each result to the phones with `relay.feedback(name, value)`; `host.frame()` of [`mountRemoteHost()`](#mountRemoteHost) does exactly that. On the phone, [`Surface`](#Surface)'s `feedback()` moves the widget, so a fader follows the timeline and other controllers.","zh":"它是純函式、成本很低，所以演出端大約每秒呼叫十次，並用 `relay.feedback(name, value)` 把每一項送給手機，[`mountRemoteHost()`](#mountRemoteHost) 的 `host.frame()` 做的就是這件事。在手機上，[`Surface`](#Surface) 的 `feedback()` 會移動元件，所以推桿會跟著時間軸和其他控制器動。"}],"params":[{"name":"state","type":"Object","en":"The resolved param values, such as `params.resolve(…)`: `{ hue: 90, size: 5.5 }`.","zh":"算好的參數值，例如 `params.resolve(…)` 的結果：`{ hue: 90, size: 5.5 }`。"},{"name":"bindings","type":"Object[]","en":"The `bindings` that [`autoSurface()`](#autoSurface) returned.","zh":"[`autoSurface()`](#autoSurface) 回傳的 `bindings`。"}],"returns":{"type":"{ name, value }[]","en":"One entry per binding that has a number in `state`: `name` is the widget's signal, `value` is the param scaled to 0..1 and held in range. A param that is missing, not a number, or with `min` equal to `max` is skipped.","zh":"`state` 裡有數字的每個 binding 各一項：`name` 是元件的訊號，`value` 是縮放到 0..1 並限制在範圍內的參數值。缺少、不是數字、或 `min` 等於 `max` 的參數會跳過。"},"notes":[],"examples":[{"code":"import { autoSurface, feedbackFor } from '@openav/surface/auto';\n\nconst params = [{ key: 'hue', min: 0, max: 360, def: 200 }, { key: 'size', min: 1, max: 10, def: 4 }];\nconst { bindings } = autoSurface(params);\nconst echo = feedbackFor({ hue: 90, size: 5.5 }, bindings);\ndocument.getElementById('app').innerHTML = echo.map((f) => f.name + ' = ' + f.value).join('<br>');","caption":{"en":"Prints surface/main/hue = 0.25 (90 of 0 to 360) and surface/main/size = 0.5 (5.5 of 1 to 10).","zh":"印出 surface/main/hue = 0.25（0 到 360 裡的 90）和 surface/main/size = 0.5（1 到 10 裡的 5.5）。"},"run":"module","lang":"js"}],"related":["autoSurface","Surface","mountRemoteHost","RelayClient"],"keywords":{"en":["feedback","echo values","fader follows","bindings","normalize","send back to phone"],"zh":["回饋","回送數值","推桿跟著動","綁定","正規化","送回手機"]},"tbd":[],"claims":[]},
{"id":"gridOrientation","key":"gridOrientation","name":"gridOrientation","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"gridOrientation(g)","import":"import { gridOrientation } from '@openav/surface/layout';","source":{"file":"packages/surface/layout.js","line":41,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L41"},"summary":{"en":"Says whether a grid `{ cols, rows }` is `'landscape'` (at least as many columns as rows) or `'portrait'`.","zh":"判斷格線 `{ cols, rows }` 是 `'landscape'`（欄數不少於列數）還是 `'portrait'`。"},"description":[{"en":"[`resolvePage()`](#resolvePage) compares it with the orientation of the screen: a base grid of 8 by 4 is used as it is on a landscape screen and reflowed on a portrait one.","zh":"[`resolvePage()`](#resolvePage) 會拿它跟螢幕的方向比：8 乘 4 的基本格線在橫向螢幕上直接用，在直向螢幕上就重排。"}],"params":[{"name":"g","type":"{ cols, rows }","en":"The grid.","zh":"格線。"}],"returns":{"type":"'landscape' | 'portrait'","en":"`'landscape'` when `g.cols >= g.rows`, else `'portrait'`.","zh":"`g.cols >= g.rows` 是 `'landscape'`，否則是 `'portrait'`。"},"notes":[],"examples":[{"code":"import { gridOrientation } from '@openav/surface/layout';\n\ndocument.getElementById('app').innerHTML = [{ cols: 8, rows: 4 }, { cols: 4, rows: 8 }, { cols: 4, rows: 4 }]\n  .map((g) => g.cols + ' x ' + g.rows + ': ' + gridOrientation(g)).join('<br>');","caption":{"en":"Prints landscape for 8 x 4, portrait for 4 x 8, and landscape for 4 x 4.","zh":"印出 8 x 4 是 landscape、4 x 8 是 portrait、4 x 4 是 landscape。"},"run":"module","lang":"js"}],"related":["orientationOf","resolvePage"],"keywords":{"en":["grid orientation","portrait","landscape","cols rows","grid shape"],"zh":["格線方向","直向","橫向","欄 列","格線形狀"]},"tbd":[],"claims":[]},
{"id":"haptic","key":"haptic","name":"haptic","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"haptic(ms, enabled = true)","import":"import { haptic } from '@openav/surface';","source":{"file":"packages/surface/platform.js","line":43,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/platform.js#L43"},"summary":{"en":"Vibrates the phone for `ms` milliseconds where the browser can (Android Chrome); silent everywhere else, and `enabled` false turns it off.","zh":"瀏覽器做得到的地方（Android 的 Chrome）讓手機震動 `ms` 毫秒，其他地方什麼都不會發生，`enabled` 為 false 就關掉。"},"description":[{"en":"It calls `navigator.vibrate(ms)` when that exists and swallows any error. Browsers without it, such as Safari on iPhone, simply do nothing, and Chrome only vibrates after the person has touched the page. [`Surface`](#Surface) calls it for buttons, toggles and radios, and its `haptics: false` option turns that off.","zh":"它在 `navigator.vibrate(ms)` 存在的時候呼叫它，並吞掉任何錯誤。沒有它的瀏覽器，例如 iPhone 上的 Safari，就單純什麼都不做，而 Chrome 要等使用者碰過頁面之後才會震動。[`Surface`](#Surface) 會在按鈕、開關和單選時呼叫它，它的 `haptics: false` 選項可以把這個關掉。"}],"params":[{"name":"ms","type":"Number","en":"How long to vibrate, in milliseconds. A button press in the surface uses 10 to 14.","zh":"震動多久，單位毫秒。面板裡按一下按鈕用的是 10 到 14。"},{"name":"enabled","type":"Boolean","optional":true,"en":"Pass `false` to do nothing: a page can offer a \"haptics\" switch and hand it here.","zh":"傳 `false` 就什麼都不做：頁面可以提供一個「觸覺回饋」開關，把它的值交給這裡。"}],"notes":[],"examples":[{"code":"import { haptic } from '@openav/surface';\n\ndocument.querySelector('button').addEventListener('pointerdown', () => haptic(20));   // a short buzz on every press","caption":{"en":"On an Android phone the button buzzes for 20 ms each time it is pressed; elsewhere nothing happens.","zh":"在 Android 手機上，按一下按鈕就震 20 毫秒，其他地方什麼都不會發生。"},"run":"static","lang":"js"},{"code":"import { haptic } from '@openav/surface';\n\nconst log = [];\nnavigator.vibrate = (ms) => { log.push('vibrate ' + ms + ' ms'); return true; };   // a stand-in, so there is something to see\n\nhaptic(20);\nhaptic(12, false);    // switched off: nothing is called\nhaptic(30, true);\ndocument.getElementById('app').innerHTML = log.join('<br>');","caption":{"en":"Prints \"vibrate 20 ms\" and \"vibrate 30 ms\": the call with enabled false made no vibration.","zh":"印出「vibrate 20 ms」和「vibrate 30 ms」：enabled 為 false 的那一次沒有震動。"},"run":"module","lang":"js"}],"related":["Surface","keepAwake","lockViewport"],"keywords":{"en":["haptic","vibrate","vibration","navigator.vibrate","buzz","feedback","android"],"zh":["觸覺回饋","震動","振動","navigator.vibrate","嗡嗡","回饋","android"]},"tbd":[{"en":"Not felt on a real Android phone for this entry. The example uses a stand-in for `navigator.vibrate` to show when it is called.","zh":"這一條沒有在真正的 Android 手機上感受過。範例用了一個代替 `navigator.vibrate` 的替身，來示範什麼時候會被呼叫。"}],"claims":[]},
{"id":"injectTheme","key":"injectTheme","name":"injectTheme","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"injectTheme()","import":"import { injectTheme } from '@openav/surface';","source":{"file":"packages/surface/theme.js","line":164,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/theme.js#L164"},"summary":{"en":"Writes the surface's stylesheet into the page, once: the 15 theme variables on `:root` and the rules for every widget. Safe to call again.","zh":"把面板的樣式表寫進頁面，只寫一次：`:root` 上的 15 個主題變數，以及每個元件的樣式規則。重複呼叫也沒問題。"},"description":[{"en":"[`Surface`](#Surface) and `mountRemote()` call it for you, so you need it only when you build a [widget](#WIDGET_CLASSES) by yourself, or when you want the variables (`var(--oav-accent)`) for your own elements. It adds one `<style id=\"openav-surface-css\">` to the head, and does nothing if that already exists, or if there is no `document`. To change the look, set a variable after that: `document.documentElement.style.setProperty('--oav-accent', '#ff5fb3')`.","zh":"[`Surface`](#Surface) 和 `mountRemote()` 會替你呼叫它，所以只有自己建立 [元件](#WIDGET_CLASSES) 的時候，或想在自己的元素上用這些變數（`var(--oav-accent)`）的時候才需要它。它會在 head 裡加一個 `<style id=\"openav-surface-css\">`，這個已經存在、或根本沒有 `document` 的話，就什麼都不做。要改外觀，在那之後設變數：`document.documentElement.style.setProperty('--oav-accent', '#ff5fb3')`。"}],"params":[],"notes":[],"examples":[{"code":"import { injectTheme } from '@openav/surface';\n\nconst read = () => getComputedStyle(document.documentElement).getPropertyValue('--oav-accent').trim() || '(not set)';\nconst before = read();\ninjectTheme();\ninjectTheme();   // the second call does nothing\ndocument.getElementById('app').innerHTML = [\n  'accent before: ' + before,\n  'accent after: ' + read(),\n  'style tags: ' + document.querySelectorAll('#openav-surface-css').length,\n].join('<br>');","caption":{"en":"Prints \"(not set)\" before, #7ea6ff after, and exactly one style tag even though it was called twice.","zh":"印出之前是「(not set)」、之後是 #7ea6ff，而且就算呼叫了兩次，style 標籤也只有一個。"},"run":"module","lang":"js"},{"code":"import { Surface, injectTheme } from '@openav/surface';\n\ninjectTheme();\ndocument.documentElement.style.setProperty('--oav-accent', '#ff5fb3');   // re-skin: pink instead of blue\n\nconst box = document.getElementById('app');\nbox.style.cssText = 'position:relative;width:260px;height:160px';\nnew Surface(box, {\n  layout: { pages: [{ id: 'main', grid: { cols: 3, rows: 2 }, widgets: [\n    { id: 'a', type: 'fader', x: 0, y: 0, w: 1, h: 2, def: 0.7 }, { id: 'b', type: 'knob', x: 1, y: 0, w: 2, h: 2, def: 0.4 },\n  ] }] },\n  sink() {},\n});","caption":{"en":"A fader and a knob drawn in pink: they have no colour of their own, so they take the accent that was overridden.","zh":"一支推桿和一個旋鈕畫成粉紅色：它們沒有自己的顏色，所以用了被覆寫過的強調色。"},"run":"module","lang":"js"}],"related":["THEME_VARS","Surface","WIDGET_CLASSES","mountRemote"],"keywords":{"en":["inject theme","stylesheet","css","style tag","theme","skin","oav-accent"],"zh":["注入主題","樣式表","css","style 標籤","主題","換皮膚","oav-accent"]},"tbd":[],"claims":[]},
{"id":"keepAwake","key":"keepAwake","name":"keepAwake","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"keepAwake()","import":"import { keepAwake } from '@openav/surface';","source":{"file":"packages/surface/platform.js","line":25,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/platform.js#L25"},"summary":{"en":"Asks the browser to keep the screen on (a screen wake lock) and asks again when the tab comes back. Returns `{ release() }`.","zh":"請瀏覽器讓螢幕保持亮著（螢幕喚醒鎖），分頁回來時再要求一次。回傳 `{ release() }`。"},"description":[{"en":"A phone that is a controller dims and locks within a minute unless something holds the screen on. `keepAwake()` requests `navigator.wakeLock.request('screen')` right away if the page is visible. The operating system drops the lock whenever the page is hidden, so it asks again each time the page becomes visible. It is best effort and silent: a browser with no wake lock, or one that says no, just means the screen may still dim, and it never throws.","zh":"當控制器用的手機，一分鐘內就會暗掉、鎖定，除非有東西讓螢幕保持亮著。如果頁面看得到，`keepAwake()` 馬上要求 `navigator.wakeLock.request('screen')`。頁面一被藏起來，作業系統就會放掉這把鎖，所以每次頁面再變成可見，它都會再要求一次。它是盡力而為、而且不出聲：瀏覽器沒有喚醒鎖、或回答不行的話，就只是螢幕可能還是會暗掉，它絕不會丟出錯誤。"}],"params":[],"returns":{"type":"{ release }","en":"`release()` stops asking and lets go of the lock.","zh":"`release()` 停止再要求，並放掉喚醒鎖。"},"needs":{"en":"A page that is visible, on `https` or `localhost` (the wake lock only exists on secure pages). `mountRemote()` calls it for you.","zh":"頁面要看得到，而且在 `https` 或 `localhost` 底下（喚醒鎖只存在於安全的頁面）。`mountRemote()` 會替你呼叫它。"},"notes":[],"examples":[{"code":"import { keepAwake } from '@openav/surface';\n\nconst awake = keepAwake();            // the screen stays on while this page is visible\n\n// when the show is over:\n// awake.release();","caption":{"en":"On a phone or a laptop the screen stops dimming while the page is open and visible.","zh":"在手機或筆電上，頁面開著而且看得到的時候，螢幕不會再暗掉。"},"run":"static","lang":"js"},{"code":"import { keepAwake } from '@openav/surface';\n\nconst log = [];\n// a stand-in for the browser's wake lock, so there is something to see on any machine\nObject.defineProperty(navigator, 'wakeLock', { configurable: true, value: {\n  request: async (type) => { log.push('request ' + type); return { release() { log.push('lock released'); } }; },\n} });\n\nconst awake = keepAwake();\nawait new Promise((r) => setTimeout(r, 50));\nawake.release();\nlog.push('release() called');\ndocument.getElementById('app').innerHTML = log.join('<br>');","caption":{"en":"Prints \"request screen\", then \"lock released\" and \"release() called\": one request when the page is visible, and release() lets the lock go.","zh":"印出「request screen」，然後是「lock released」和「release() called」：頁面看得到時要求一次，release() 把鎖放掉。"},"run":"module","lang":"js"}],"related":["lockViewport","mountRemote"],"keywords":{"en":["wake lock","keep awake","screen on","no sleep","dim","phone screen","navigator.wakeLock"],"zh":["喚醒鎖","保持亮著","螢幕常亮","不休眠","變暗","手機螢幕","navigator.wakeLock"]},"tbd":[{"en":"Not tried on a real iPhone or iPad: Safari may refuse a wake-lock request that is not made inside a tap, in which case the screen can still dim. The second example only shows the calls, with a stand-in for `navigator.wakeLock`.","zh":"沒有在真正的 iPhone 或 iPad 上試過：Safari 可能會拒絕不在點擊裡發出的喚醒鎖要求，這時螢幕還是會暗掉。第二個範例只示範呼叫的順序，用了一個代替 `navigator.wakeLock` 的替身。"}],"claims":[]},
{"id":"lockViewport","key":"lockViewport","name":"lockViewport","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"lockViewport()","import":"import { lockViewport } from '@openav/surface';","source":{"file":"packages/surface/platform.js","line":7,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/platform.js#L7"},"summary":{"en":"Makes a page behave like an instrument panel instead of a web page: no scrolling, no pinch zoom, no rubber-banding. Returns a function that undoes most of it.","zh":"讓頁面像儀器面板，而不是網頁：不能捲動、不能雙指縮放、不會有橡皮筋回彈。回傳一個函式，可以復原其中大部分。"},"description":[{"en":"It sets `<meta name=\"viewport\">` to `width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover` (making the tag if there is none) and adds a style that gives `html` and `body` no margin, the full height, `overflow: hidden`, `overscroll-behavior: none` and `touch-action: none`. iOS ignores `user-scalable=no`, so it also cancels `gesturestart` and `gesturechange` (pinch zoom) and the default of every `touchmove`, except one that starts in an `input` or `textarea`, so a text box still works.","zh":"它把 `<meta name=\"viewport\">` 設成 `width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover`（沒有這個標籤就先做一個），並加一段樣式，讓 `html` 和 `body` 沒有邊界、高度填滿、`overflow: hidden`、`overscroll-behavior: none`、`touch-action: none`。iOS 會忽略 `user-scalable=no`，所以它也取消 `gesturestart` 和 `gesturechange`（雙指縮放），以及每個 `touchmove` 的預設行為，只有從 `input` 或 `textarea` 開始的例外，所以文字框還是能用。"},{"en":"This is for a page that is a control panel, such as `mountRemote()`, which calls it. Do not call it on a page that has to scroll. Without a `document` (in Node) it returns a function that does nothing.","zh":"這是給整個頁面就是控制面板的情況用的，例如 `mountRemote()` 就會呼叫它。要捲動的頁面不要呼叫它。沒有 `document`（在 Node 裡）的話，它回傳一個什麼都不做的函式。"}],"params":[],"returns":{"type":"Function","en":"Call it to remove the stylesheet and the listeners. The viewport `<meta>` stays as it was set.","zh":"呼叫它會移除樣式表和監聽。viewport 的 `<meta>` 維持設好的樣子，不會還原。"},"notes":[],"examples":[{"code":"import { lockViewport } from '@openav/surface';\n\nconst unlock = lockViewport();\nconst meta = document.querySelector('meta[name=viewport]');\nconst locked = getComputedStyle(document.body).overflow;\nunlock();\ndocument.getElementById('app').innerHTML = [\n  'viewport: ' + meta.content,\n  'body overflow while locked: ' + locked,\n  'body overflow after unlock: ' + getComputedStyle(document.body).overflow,\n].join('<br>');","caption":{"en":"Prints the viewport string, \"hidden\" while locked, and \"visible\" after unlock(): the stylesheet goes away, the meta tag stays.","zh":"印出 viewport 字串、鎖定時的「hidden」、unlock() 之後的「visible」：樣式表消失了，meta 標籤還在。"},"run":"module","lang":"js"}],"related":["keepAwake","mountRemote","Surface"],"keywords":{"en":["lock viewport","no scroll","no zoom","pinch zoom","overscroll","ios","instrument panel","user-scalable"],"zh":["鎖定視窗","不捲動","不縮放","雙指縮放","橡皮筋","ios","儀器面板","user-scalable"]},"tbd":[],"claims":[]},
{"id":"normalizeLayout","key":"normalizeLayout","name":"normalizeLayout","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"normalizeLayout(raw)","import":"import { normalizeLayout } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":70,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L70"},"summary":{"en":"Returns a copy of a layout with the defaults filled in: ids, titles, grids and the version. The input is not changed.","zh":"回傳一份補上預設值的版面複本：id、標題、格線和版本。原本的輸入不會被改動。"},"description":[{"en":"A page without an `id` becomes `page1`, `page2` …; without a `title` it gets its id (or `Page 1`); without a `grid` it gets `{ cols: 8, rows: 6 }`. A widget without an `id` becomes its type and its position in the list, `fader1`; an id already used on the page gets `_` added until it is unique. Every other field is kept as it is. [`Surface`](#Surface) normalizes the layout it is given, and so do [`signalNames()`](#signalNames) and the show side of the remote.","zh":"沒有 `id` 的頁面會變成 `page1`、`page2` …，沒有 `title` 就用它的 id（或 `Page 1`），沒有 `grid` 就是 `{ cols: 8, rows: 6 }`。沒有 `id` 的元件會變成「類型加在清單中的位置」，像 `fader1`。在同一頁已經用過的 id 會一直加上 `_` 直到不重複。其他欄位原樣保留。[`Surface`](#Surface) 會把拿到的版面正規化，[`signalNames()`](#signalNames) 和遙控的演出端也一樣。"}],"params":[{"name":"raw","type":"Object","en":"The layout JSON. `undefined` or an object without pages gives a layout with no pages.","zh":"版面 JSON。`undefined` 或沒有 pages 的物件，會得到沒有任何頁面的版面。"}],"returns":{"type":"Object","en":"`{ version: 1, title, pages }` with every page and widget complete.","zh":"`{ version: 1, title, pages }`，每個頁面和元件的欄位都補齊了。"},"notes":[{"en":"The default grid has 6 rows. [`autoSurface()`](#autoSurface) writes its own grids of 8 by 4, so this default only matters for layouts that leave the grid out.","zh":"預設格線是 6 列。[`autoSurface()`](#autoSurface) 自己寫的格線是 8 乘 4，所以這個預設值只對沒寫格線的版面有意義。"}],"examples":[{"code":"import { normalizeLayout } from '@openav/surface';\n\nconst layout = normalizeLayout({ pages: [{ widgets: [{ type: 'fader' }, { type: 'fader', id: 'fader1' }, { type: 'knob' }] }] });\nconst page = layout.pages[0];\ndocument.getElementById('app').innerHTML = [\n  'page: ' + page.id + ' \"' + page.title + '\", grid ' + page.grid.cols + 'x' + page.grid.rows,\n  'widgets: ' + page.widgets.map((w) => w.id).join(', '),\n].join('<br>');","caption":{"en":"Prints \"page: page1 \"Page 1\", grid 8x6\" and the widget ids fader1, fader1_ (the second fader1 was already taken), knob3.","zh":"印出「page: page1 \"Page 1\", grid 8x6」和元件 id：fader1、fader1_（第二個 fader1 已經被用了）、knob3。"},"run":"module","lang":"js"}],"related":["validateLayout","signalNames","resolvePage","Surface"],"keywords":{"en":["normalize","defaults","fill in","layout","ids","grid default","copy"],"zh":["正規化","預設值","補齊","版面","id","預設格線","複本"]},"tbd":[],"claims":[]},
{"id":"orientationOf","key":"orientationOf","name":"orientationOf","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"orientationOf(w, h)","import":"import { orientationOf } from '@openav/surface/layout';","source":{"file":"packages/surface/layout.js","line":40,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L40"},"summary":{"en":"Says whether a box is `'landscape'` or `'portrait'` from its width and height; a square counts as landscape.","zh":"從寬和高判斷一個框是 `'landscape'`（橫向）還是 `'portrait'`（直向），正方形算橫向。"},"description":[{"en":"[`Surface`](#Surface) calls it with the size of its container, not the size of the screen, so a panel in a half-width pane can be portrait while the phone is landscape. Use [`gridOrientation()`](#gridOrientation) for the shape of a grid.","zh":"[`Surface`](#Surface) 傳給它的是容器的大小，不是螢幕的大小，所以放在半寬窗格裡的面板可以是直向，而手機本身是橫向。要判斷格線的形狀，用 [`gridOrientation()`](#gridOrientation)。"}],"params":[{"name":"w","type":"Number","en":"The width, in any unit.","zh":"寬，什麼單位都可以。"},{"name":"h","type":"Number","en":"The height, in the same unit.","zh":"高，單位跟寬一樣。"}],"returns":{"type":"'landscape' | 'portrait'","en":"`'landscape'` when `w >= h`, else `'portrait'`.","zh":"`w >= h` 是 `'landscape'`，否則是 `'portrait'`。"},"notes":[],"examples":[{"code":"import { orientationOf } from '@openav/surface/layout';\n\ndocument.getElementById('app').innerHTML = [[844, 390], [390, 844], [500, 500]]\n  .map(([w, h]) => w + ' x ' + h + ': ' + orientationOf(w, h)).join('<br>');","caption":{"en":"Prints landscape for 844 x 390, portrait for 390 x 844, and landscape for the square.","zh":"印出 844 x 390 是 landscape、390 x 844 是 portrait，正方形是 landscape。"},"run":"module","lang":"js"}],"related":["gridOrientation","resolvePage","Surface"],"keywords":{"en":["orientation","portrait","landscape","width height","aspect","phone rotation"],"zh":["方向","直向","橫向","寬高","長寬比","手機轉向"]},"tbd":[],"claims":[]},
{"id":"planKeyboard","key":"planKeyboard","name":"planKeyboard","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"planKeyboard({ width, height, base = 48, minKeyPx = 38, maxWhites = 22, stackBelow = 2.2, forceRows = 0 })","import":"import { planKeyboard } from '@openav/surface';","source":{"file":"packages/surface/kbplan.js","line":41,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/kbplan.js#L41"},"summary":{"en":"Decides how many piano keys, in how many rows, fit a box: one wide row, or two stacked rows when the box is tall, so keys stay a playable size.","zh":"決定一個框裡放得下多少琴鍵、排成幾列：框很寬就一排，框很高就上下兩排，讓琴鍵維持好彈的大小。"},"description":[{"en":"A piano is two and a half metres wide and a phone held upright is six centimetres, so shrinking the keys makes a toy. Instead the keyboard changes shape with its box. A wide box gets one row with as many octaves as fit at `minKeyPx` or wider. A tall box gets two rows, the upper one continuing the pitch range where the lower stops, within reach of a thumb. The rows end on E or B and the next one starts on the next white key, so no black key is left alone at the seam. Notes stop at 127. The `keyboard` widget of [`Surface`](#Surface) calls this every time its box changes size.","zh":"一台鋼琴有兩公尺半寬，直拿的手機只有六公分，把琴鍵縮小只會變成玩具。所以改成讓鍵盤隨著框的形狀變形。很寬的框得到一排，放得下幾個八度就放幾個（白鍵不窄於 `minKeyPx`）。很高的框得到兩排，上排接著下排停下來的地方往高音延伸，拇指搆得到。每一排都結束在 E 或 B，下一排從下一個白鍵開始，所以接縫處不會留下孤單的黑鍵。音符最高到 127。[`Surface`](#Surface) 的 `keyboard` 元件在框的大小改變時，每次都會呼叫它。"}],"params":[{"name":"width","type":"Number","en":"The width of the box, in pixels.","zh":"框的寬，單位像素。"},{"name":"height","type":"Number","en":"The height of the box, in pixels.","zh":"框的高，單位像素。"},{"name":"base","type":"Number","optional":true,"en":"The MIDI note of the lowest key. 48 is C3 (C4 is 60).","zh":"最低音那個鍵的 MIDI 音符號碼。48 是 C3（C4 是 60）。"},{"name":"minKeyPx","type":"Number","optional":true,"en":"The narrowest a white key may be, in pixels. At least 3 white keys are kept.","zh":"白鍵最窄可以多寬，單位像素。至少會保留 3 個白鍵。"},{"name":"maxWhites","type":"Number","optional":true,"en":"The most white keys in a row. Two stacked rows are limited to 14 each.","zh":"一排最多幾個白鍵。上下兩排的話，每排最多 14 個。"},{"name":"stackBelow","type":"Number","optional":true,"en":"Stack into two rows when width ÷ height is below this.","zh":"寬 ÷ 高低於這個值就改成上下兩排。"},{"name":"forceRows","type":"1 | 2","optional":true,"en":"`1` or `2` to decide yourself; `0` lets the shape of the box decide.","zh":"`1` 或 `2` 自己決定，`0` 由框的形狀決定。"}],"returns":{"type":"{ stacked, whites, keyPx, rows }","en":"`stacked` is true for two rows, `whites` the white keys in a row, `keyPx` the width of a white key. `rows` is a list of `{ base, semitones, whites }`, **the top row first** (the higher notes), the way two keyboards are read: `base` is the MIDI note of the first key and `semitones` how many notes the row spans.","zh":"`stacked` 為 true 就是兩排，`whites` 是每排的白鍵數，`keyPx` 是白鍵的寬度。`rows` 是 `{ base, semitones, whites }` 的清單，**上面那一排在最前面**（音比較高），就像兩層鍵盤的讀法：`base` 是第一個鍵的 MIDI 音符號碼，`semitones` 是那一排涵蓋幾個半音。"},"notes":[],"examples":[{"code":"import { planKeyboard } from '@openav/surface';\n\nconst show = (label, plan) => label + ': ' + (plan.stacked ? 'two rows' : 'one row') + ', ' + plan.whites + ' white keys of ' +\n  Math.round(plan.keyPx) + ' px, ' + plan.rows.map((r) => 'notes ' + r.base + '–' + (r.base + r.semitones - 1)).join(' / ');\ndocument.getElementById('app').innerHTML = [\n  show('phone on its side (844 x 330)', planKeyboard({ width: 844, height: 330 })),\n  show('phone upright (390 x 640)', planKeyboard({ width: 390, height: 640 })),\n].join('<br>');","caption":{"en":"Prints one row of 21 white keys (notes 48 to 83) for the wide box, and two rows of 10 (notes 65 to 81 on top, 48 to 64 below) for the tall one.","zh":"寬的框印出一排 21 個白鍵（音符 48 到 83），高的框印出兩排各 10 個（上排 65 到 81、下排 48 到 64）。"},"run":"module","lang":"js"}],"related":["Surface","WIDGET_TYPES"],"keywords":{"en":["keyboard","piano keys","octaves","stacked rows","two rows","key width","phone piano","layout of keys"],"zh":["鍵盤","琴鍵","八度","上下兩排","兩排","琴鍵寬度","手機鋼琴","琴鍵排列"]},"tbd":[],"claims":[]},
{"id":"resolvePage","key":"resolvePage","name":"resolvePage","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"resolvePage(page, orientation)","import":"import { resolvePage } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":116,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L116"},"summary":{"en":"Works out where every widget of one page goes for one orientation: the grid and a cell for each widget, and which rule produced it.","zh":"算出某一頁在某個方向下每個元件要放哪裡：格線，以及每個元件的格子，還有是哪一條規則排出來的。"},"description":[{"en":"A phone gets turned, so a layout has to survive it. The rules, in order: 1. If the page has a `portrait` or `landscape` entry for this orientation, use its `grid` and its `place` (`{ widgetId: { x, y, w, h } }`); widgets it does not mention keep their own position, or are packed into free cells (`source` is `'explicit'`). 2. Else, if the base grid has the right shape (more columns than rows is landscape) and every widget has an `x` and a `y`, use the base (`'base'`). 3. Else reflow: the same widgets in reading order, packed again into half the columns (portrait, at least 2) or twice as many (landscape, up to 16), with a lone last row stretched to the edge by [`stretchRows()`](#stretchRows) (`'reflow'`).","zh":"手機會被轉來轉去，所以版面一定要撐得住。規則依序是：1. 頁面針對這個方向有 `portrait` 或 `landscape` 的項目，就用它的 `grid` 和 `place`（`{ 元件 id: { x, y, w, h } }`）。它沒提到的元件保留自己的位置，或被打包進空的格子（`source` 是 `'explicit'`）。2. 否則，基本格線的形狀對（欄數多於列數是橫向）、而且每個元件都有 `x` 和 `y`，就用基本格線（`'base'`）。3. 否則重排：同樣的元件依閱讀順序，重新打包進一半的欄數（直向，至少 2 欄）或兩倍的欄數（橫向，最多 16 欄），最後孤零零的一列由 [`stretchRows()`](#stretchRows) 拉到邊緣（`'reflow'`）。"},{"en":"Rows are a count, not pixels, so the result always fills the screen and nothing scrolls. A widget without `w` and `h` takes the size of its type from [`DEFAULT_SIZE`](#DEFAULT_SIZE). [`Surface`](#Surface) calls this for every page each time the orientation changes.","zh":"列數是個數，不是像素，所以結果永遠填滿螢幕，不會捲動。沒有 `w` 和 `h` 的元件，會用 [`DEFAULT_SIZE`](#DEFAULT_SIZE) 裡它的類型的大小。每次方向改變，[`Surface`](#Surface) 都會對每一頁呼叫它。"}],"params":[{"name":"page","type":"Object","en":"One page of a layout: `{ grid, widgets, portrait?, landscape? }`.","zh":"版面中的一頁：`{ grid, widgets, portrait?, landscape? }`。"},{"name":"orientation","type":"'portrait' | 'landscape'","en":"Which way the screen is held.","zh":"螢幕怎麼拿。"}],"returns":{"type":"{ grid, placements, source }","en":"`grid` is `{ cols, rows }`, `placements` a list of `{ id, x, y, w, h }`, `source` is `'explicit'`, `'base'` or `'reflow'`.","zh":"`grid` 是 `{ cols, rows }`，`placements` 是 `{ id, x, y, w, h }` 的清單，`source` 是 `'explicit'`、`'base'` 或 `'reflow'`。"},"notes":[],"examples":[{"code":"import { resolvePage } from '@openav/surface/layout';\n\n// eight faders in a row: right for a landscape screen, too wide for a portrait one\nconst page = {\n  grid: { cols: 8, rows: 4 },\n  widgets: Array.from({ length: 8 }, (_, i) => ({ id: 'f' + i, type: 'fader', x: i, y: 0, w: 1, h: 4 })),\n};\nconst out = ['landscape', 'portrait'].map((o) => {\n  const r = resolvePage(page, o);\n  return o + ': ' + r.source + ', ' + r.grid.cols + ' columns x ' + r.grid.rows + ' rows';\n});\ndocument.getElementById('app').innerHTML = out.join('<br>');","caption":{"en":"Prints \"landscape: base, 8 columns x 4 rows\" and \"portrait: reflow, 4 columns x 8 rows\": turned upright, the eight faders become two rows of four.","zh":"印出「landscape: base, 8 columns x 4 rows」和「portrait: reflow, 4 columns x 8 rows」：轉成直的之後，八支推桿變成兩列、每列四支。"},"run":"module","lang":"js"},{"code":"import { resolvePage } from '@openav/surface/layout';\n\n// a page that says where things go when the phone is upright\nconst page = {\n  grid: { cols: 8, rows: 4 },\n  widgets: [\n    { id: 'a', type: 'fader', x: 0, y: 0, w: 1, h: 4 },\n    { id: 'b', type: 'knob', x: 1, y: 0 },\n    { id: 'c', type: 'button' },\n  ],\n  portrait: { grid: { cols: 4, rows: 6 }, place: { a: { x: 3, y: 0, w: 1, h: 6 } } },\n};\nconst r = resolvePage(page, 'portrait');\ndocument.getElementById('app').innerHTML = [r.source, ...r.placements.map((p) => p.id + ': ' + p.x + ',' + p.y + ' ' + p.w + '×' + p.h)].join('<br>');","caption":{"en":"Prints \"explicit\": a goes where the portrait entry says, the knob and the button (which have no place) fill the free cells.","zh":"印出「explicit」：a 照 portrait 項目寫的位置放，旋鈕和按鈕（沒有位置）填進空的格子。"},"run":"module","lang":"js"}],"related":["autoArrange","stretchRows","orientationOf","gridOrientation","normalizeLayout","Surface"],"keywords":{"en":["resolve page","orientation","portrait","landscape","reflow","responsive","grid","placements","rotate phone"],"zh":["解析頁面","方向","直向","橫向","重排","響應式","格線","位置","轉手機"]},"tbd":[],"claims":[]},
{"id":"routesFromLayout","key":"routesFromLayout","name":"routesFromLayout","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"routesFromLayout(layout, params = [], { smooth = 0.04 } = {})","import":"import { routesFromLayout } from '@openav/surface';","source":{"file":"packages/surface/auto.js","line":156,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/auto.js#L156"},"summary":{"en":"Writes the mapper routes for a hand-written layout: every widget that names a param in `target` gets a route from its signal to that param.","zh":"替手寫的版面產生映射器路由：每個用 `target` 指定參數的元件，都會得到一條從它的訊號到那個參數的路由。"},"description":[{"en":"Give a widget a `target` and the layout file is enough to perform a world. `target` is a param key, or an array for the widgets with several signals: an `xy` sends x to the first and y to the second, a `bank` sends channel *i* to the *i*-th. Every other widget uses its first (or only) target. The routes already scale the 0..1 signal to the param's `min` and `max`. [`mountRemoteHost()`](#mountRemoteHost) uses this for a layout from `world.surface` or from its `surface` option.","zh":"給元件一個 `target`，光靠版面檔案就能演奏一個世界。`target` 是參數鍵，訊號不只一個的元件可以給陣列：`xy` 把 x 送給第一個、y 送給第二個，`bank` 把第 *i* 個頻道送給第 *i* 個。其他元件都用第一個（或唯一的）target。路由已經把 0..1 的訊號換算成參數的 `min` 和 `max`。[`mountRemoteHost()`](#mountRemoteHost) 遇到來自 `world.surface` 或 `surface` 選項的版面，就是用它。"}],"params":[{"name":"layout","type":"Object","en":"The layout JSON, whose widgets may have a `target`.","zh":"版面 JSON，裡面的元件可以有 `target`。"},{"name":"params","type":"Object[]","optional":true,"en":"The param declarations. A `target` that is not among them is skipped without a warning.","zh":"參數宣告。`target` 不在裡面的話，會不吭一聲地跳過。"},{"name":"smooth","type":"Number","optional":true,"en":"Seconds of smoothing on continuous routes. A widget's own `smooth` wins. Pulse params and button, toggle and radio widgets are never smoothed.","zh":"連續路由的平滑秒數。元件自己的 `smooth` 優先。脈衝參數，以及 button、toggle、radio 元件不會被平滑。"}],"returns":{"type":"Object[]","en":"Routes in the same shape [`autoSurface()`](#autoSurface) makes: `{ source, target, inMin, inMax, curve, smooth, outMin, outMax }`, with `curve` taken from the widget (`linear` if it has none).","zh":"格式跟 [`autoSurface()`](#autoSurface) 做出來的一樣的路由：`{ source, target, inMin, inMax, curve, smooth, outMin, outMax }`，`curve` 取自元件（沒有就是 `linear`）。"},"notes":[],"examples":[{"code":"import { routesFromLayout } from '@openav/surface/auto';\n\nconst params = [\n  { key: 'size', min: 1, max: 10 }, { key: 'cx', min: 0, max: 1 }, { key: 'cy', min: 0, max: 1 }, { key: 'burst', pulse: true },\n];\nconst layout = { pages: [{ id: 'p', widgets: [\n  { id: 'size', type: 'fader', target: 'size' },\n  { id: 'pos', type: 'xy', target: ['cx', 'cy'] },\n  { id: 'go', type: 'button', target: 'burst' },\n  { id: 'note', type: 'label' },\n] }] };\ndocument.getElementById('app').innerHTML = routesFromLayout(layout, params)\n  .map((r) => r.source + ' → ' + r.target).join('<br>');","caption":{"en":"Prints surface/p/size → size, surface/p/pos/x → cx, surface/p/pos/y → cy, surface/p/go → burst: the label has no target, so no route.","zh":"印出 surface/p/size → size、surface/p/pos/x → cx、surface/p/pos/y → cy、surface/p/go → burst：label 沒有 target，所以沒有路由。"},"run":"module","lang":"js"}],"related":["autoSurface","signalNames","mountRemoteHost","Surface"],"keywords":{"en":["routes from layout","target","hand-written layout","widget target","mapper routes","xy target","bank target"],"zh":["由版面產生路由","target","手寫版面","元件 target","映射器路由","xy target","bank target"]},"tbd":[],"claims":[]},
{"id":"signalNames","key":"signalNames","name":"signalNames","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"signalNames(layout)","import":"import { signalNames } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":172,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L172"},"summary":{"en":"Lists every signal name a layout can publish, so a mapping screen, a doc or a test can show them without touching the panel.","zh":"列出一份版面能發布的每一個訊號名稱，讓映射畫面、文件或測試不用碰面板就能列出它們。"},"description":[{"en":"It follows the table in [`WIDGET_TYPES`](#WIDGET_TYPES): an XY pad gives `/x`, `/y`, `/down`; a bank gives `/1` to `/count`; pads give `/hit` and `/0` to `/<rows × cols − 1>`; an encoder gives its name and `/delta`; a keyboard gives `midi/note/on`, `midi/note/off`, `midi/cc/64` and `midi/virtual`; a label or a meter gives nothing. The `…/raw` names that a widget with a range also sends are not in the list.","zh":"它依照 [`WIDGET_TYPES`](#WIDGET_TYPES) 裡的表：XY 板給 `/x`、`/y`、`/down`，bank 給 `/1` 到 `/count`，pads 給 `/hit` 和 `/0` 到 `/<列數 × 欄數 − 1>`，encoder 給它自己的名稱和 `/delta`，keyboard 給 `midi/note/on`、`midi/note/off`、`midi/cc/64` 和 `midi/virtual`，label 和 meter 什麼都不給。有範圍的元件另外送的 `…/raw` 名稱不在清單裡。"}],"params":[{"name":"layout","type":"Object","en":"The layout JSON (it is normalized first).","zh":"版面 JSON（會先正規化）。"}],"returns":{"type":"string[]","en":"Unique names such as `surface/main/hue`, `surface/main/pad/x`, `midi/note/on`, in page and widget order.","zh":"不重複的名稱，像 `surface/main/hue`、`surface/main/pad/x`、`midi/note/on`，依頁面和元件的順序。"},"notes":[],"examples":[{"code":"import { signalNames } from '@openav/surface/layout';\n\nconst names = signalNames({ pages: [{ id: 'main', widgets: [\n  { id: 'hue', type: 'knob' },\n  { id: 'pos', type: 'xy' },\n  { id: 'mix', type: 'bank', count: 2 },\n  { id: 'title', type: 'label' },\n] }] });\ndocument.getElementById('app').innerHTML = names.join('<br>');","caption":{"en":"Prints surface/main/hue, then pos/x, pos/y, pos/down, then mix/1 and mix/2: the label gives nothing.","zh":"印出 surface/main/hue，然後是 pos/x、pos/y、pos/down，再來是 mix/1 和 mix/2：label 什麼都不給。"},"run":"module","lang":"js"}],"related":["WIDGET_TYPES","normalizeLayout","routesFromLayout"],"keywords":{"en":["signal names","list signals","layout signals","outputs","what does it publish"],"zh":["訊號名稱","列出訊號","版面訊號","輸出","會發布什麼"]},"tbd":[],"claims":[]},
{"id":"signalSink","key":"signalSink","name":"signalSink","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"signalSink(signals)","import":"import { signalSink } from '@openav/surface';","source":{"file":"packages/surface/index.js","line":34,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/index.js#L34"},"summary":{"en":"Makes a `sink` for a `Surface` that writes straight into a `Signals` registry: for a panel on the same page as the show, with no phone and no relay.","zh":"做一個給 `Surface` 用的 `sink`，直接寫進 `Signals`：給跟演出在同一頁上的面板用，不需要手機，也不需要中繼。"},"description":[{"en":"A name it has not met is declared first: `{ kind: 'pulse' }` when `info.pulse` is set, otherwise `{ min: 0, max: 1 }`. Then a pulse fires with `signals.pulse()` and anything else is `signals.set()`. So a route in the mapper, `{ source: 'surface/main/hue', target: 'hue' }`, works whether the finger is on this page or on a phone.","zh":"還沒見過的名稱會先宣告：`info.pulse` 有設的話是 `{ kind: 'pulse' }`，不然是 `{ min: 0, max: 1 }`。然後脈衝用 `signals.pulse()` 觸發，其他都是 `signals.set()`。所以映射器裡的路由 `{ source: 'surface/main/hue', target: 'hue' }`，不論手指在這個頁面上還是在手機上，都能運作。"},{"en":"It does no scaling: the `…/raw` signal that a widget with a range sends is filed like any other name, with the range 0..1 it was declared with, so ignore those or give them their own declaration.","zh":"它不做任何縮放：有範圍的元件送出的 `…/raw` 訊號，會跟其他名稱一樣歸檔，宣告的範圍是 0..1，所以請忽略它們，或自己另外宣告。"}],"params":[{"name":"signals","type":"Signals","en":"The registry to write into (from `@openav/core`).","zh":"要寫進去的註冊表（來自 `@openav/core`）。"}],"returns":{"type":"Function","en":"A `(name, value, info) => {}` to pass as `Surface`'s `sink`.","zh":"一個 `(name, value, info) => {}`，傳給 `Surface` 的 `sink`。"},"notes":[],"examples":[{"code":"import { signalSink } from '@openav/surface';\nimport { Signals } from '@openav/core';\n\nconst signals = new Signals();\nconst sink = signalSink(signals);\n\nsink('surface/main/level', 0.4, {});               // a continuous value\nsink('surface/main/go', 1, { pulse: true });       // an event\n\ndocument.getElementById('app').innerHTML = [\n  'level = ' + signals.get('surface/main/level') + ', kind ' + signals.meta.get('surface/main/level').kind,\n  'go = ' + signals.get('surface/main/go') + ', kind ' + signals.meta.get('surface/main/go').kind,\n].join('<br>');","caption":{"en":"Prints \"level = 0.4, kind continuous\" and \"go = 1, kind pulse\": the sink declared each name the first time it saw it.","zh":"印出「level = 0.4, kind continuous」和「go = 1, kind pulse」：sink 第一次看到每個名稱時就替它宣告了。"},"run":"module","lang":"js"}],"related":["Surface","bindSignals","fileSignal"],"keywords":{"en":["sink","signals","same page","local surface","no relay","write signals"],"zh":["sink","訊號","同一頁","本機面板","不用中繼","寫入訊號"]},"tbd":[],"claims":[]},
{"id":"stretchRows","key":"stretchRows","name":"stretchRows","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"stretchRows(placements, cols)","import":"import { stretchRows } from '@openav/surface/layout';","source":{"file":"packages/surface/layout.js","line":154,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L154"},"summary":{"en":"Stretches a last row that stops short to the edge of the grid: five faders in four columns leave one lonely fader, and this makes it four columns wide.","zh":"把停在半途的最後一列拉到格線邊緣：四欄裡放五支推桿會剩下一支孤零零的，這個函式把它拉成四欄寬。"},"description":[{"en":"A row is stretched only when it is safe: its widgets are all the same height, side by side with no gaps from the left edge, and nothing else sits to their right. The extra columns are dealt out one at a time from left to right, so a short row of three in four columns gives the first widget two columns. A row that does not qualify is left alone, and nothing ever overlaps. [`resolvePage()`](#resolvePage) calls it at the end of a reflow.","zh":"只有安全的時候才會拉長一列：它的元件高度都一樣、從左邊緣開始一個接一個沒有空隙、右邊也沒有別的東西。多出來的欄數從左到右一欄一欄分下去，所以四欄裡只有三個元件的短列，第一個元件會拿到兩欄。不符合條件的列就不動，也絕不會重疊。[`resolvePage()`](#resolvePage) 在重排的最後會呼叫它。"}],"params":[{"name":"placements","type":"{ id, x, y, w, h }[]","en":"Placements, for example from [`autoArrange()`](#autoArrange). Not changed: copies come back.","zh":"位置清單，例如來自 [`autoArrange()`](#autoArrange)。不會被改動，回傳的是複本。"},{"name":"cols","type":"Number","en":"How many columns the grid has.","zh":"格線有幾欄。"}],"returns":{"type":"Object[]","en":"The placements, with the short rows widened.","zh":"位置清單，短的列已經加寬。"},"notes":[{"en":"Advanced. You rarely call it yourself.","zh":"進階用法。通常不用自己呼叫。"}],"examples":[{"code":"import { autoArrange, stretchRows } from '@openav/surface/layout';\n\nconst five = autoArrange(Array.from({ length: 5 }, (_, i) => ({ id: 'f' + i, w: 1, h: 4 })), 4).placements;\nconst stretched = stretchRows(five, 4);\nconst show = (list) => list.map((p) => p.id + ' w' + p.w).join(', ');\ndocument.getElementById('app').innerHTML = 'before: ' + show(five) + '<br>after: ' + show(stretched);","caption":{"en":"Prints f0 to f3 at width 1 both times, and f4 going from width 1 to width 4.","zh":"印出 f0 到 f3 的寬度前後都是 1，而 f4 的寬度從 1 變成 4。"},"run":"module","lang":"js"}],"related":["autoArrange","resolvePage"],"keywords":{"en":["stretch","last row","fill row","lonely widget","reflow","grid"],"zh":["拉長","最後一列","填滿一列","孤單的元件","重排","格線"]},"tbd":[],"claims":[]},
{"id":"Surface","key":"Surface","name":"Surface","aliases":[],"kind":"class","package":"@openav/surface","category":"remote","documented":true,"signature":"new Surface(container, { layout, sink, haptics = true, tabs = true, orientation = null, onPage = null } = {})","import":"import { Surface } from '@openav/surface';","source":{"file":"packages/surface/index.js","line":41,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/index.js#L41"},"summary":{"en":"A TouchOSC-style control panel: draws the pages and widgets of a layout in a container, and sends each touch out as a named signal.","zh":"像 TouchOSC 的控制面板：把版面裡的頁面和元件畫在容器裡，每一次觸碰都變成一個有名字的訊號送出去。"},"description":[{"en":"Pass `new Surface(container, { layout, sink })` and a panel appears: faders, knobs, an XY pad, pads, a keyboard, whatever the layout names, each with its live number. Widgets are dumb on purpose. Fingers go in, named normalized signals come out of `sink`, and the surface never knows what the work is. Run it on the show page with [`signalSink()`](#signalSink), or on a phone with a sink that sends to the relay: the same layout works for both. A world's params can grow the whole layout by themselves with [`autoSurface()`](#autoSurface).","zh":"呼叫 `new Surface(container, { layout, sink })`，面板就出現了：推桿、旋鈕、XY 板、打擊墊、琴鍵，版面裡寫什麼就有什麼，每個都顯示即時的數字。元件刻意做得很笨：手指進去，有名字的正規化訊號從 `sink` 出來，面板完全不知道作品是什麼。在演出頁面上搭配 [`signalSink()`](#signalSink) 用，或在手機上搭配送去中繼的 sink 用：同一份版面兩邊都行。世界的參數也能用 [`autoSurface()`](#autoSurface) 自己長出整份版面。"},{"en":"The grid always fills the container and nothing scrolls. When the container is turned, the page is laid out again for the new orientation by [`resolvePage()`](#resolvePage): a `portrait` or `landscape` variant in the layout if there is one, else the base grid, else the same widgets packed again into half or double the columns. Touching a widget outlines it in its colour, and a double-tap on a fader or knob returns it to its default.","zh":"格線一定填滿容器，不會有任何捲動。容器轉向的時候，頁面會由 [`resolvePage()`](#resolvePage) 依新的方向重新排：版面裡有 `portrait` 或 `landscape` 的版本就用它，沒有就用基本格線，再不行就把同樣的元件重新擺進一半或兩倍的欄數。碰到元件時，它會用自己的顏色描邊，在推桿或旋鈕上連點兩下就回到預設值。"},{"en":"Runner to widget goes the other way: [`feedback(name, value)`](#Surface) moves a fader to follow the timeline, or fills a meter. A widget under a finger ignores feedback while it is touched and for about half a second after, so it does not fight the hand (a button only while it is held).","zh":"從演出端到元件是另一條路：`feedback(name, value)` 讓推桿跟著時間軸動，或把音量表填起來。手指碰著的元件，在被碰著的時候和放開後大約半秒內，會忽略 feedback，不跟手搶（按鈕則只有按住的時候）。"}],"params":[{"name":"container","type":"HTMLElement","en":"Where the panel goes. The panel is `position: absolute` and fills it, so the container needs a position (`position: relative`) and a size, or the panel has none.","zh":"面板要放的地方。面板是 `position: absolute`，會填滿它，所以容器要有定位（`position: relative`）和大小，不然面板就沒有大小。"},{"name":"layout","type":"Object","en":"The layout JSON (see [`validateLayout()`](#validateLayout)): pages, a grid, widgets. A bad layout throws an error that lists every problem.","zh":"版面 JSON（見 [`validateLayout()`](#validateLayout)）：頁面、格線、元件。版面有問題會丟出錯誤，列出每一個問題。"},{"name":"sink","type":"Function","en":"`(name, value, info) => {}`, called for every signal a touch makes. `name` is like `surface/main/hue`, `value` is normalized 0..1 (an event is `info.pulse`, a string for a text box). When a widget's range is not 0..1, `info.raw` holds the unscaled value, and a second call follows with the name `…/raw`. [`signalSink()`](#signalSink) writes into a `Signals`; a phone passes one that calls `relay.set`.","zh":"`(name, value, info) => {}`，每一次觸碰產生訊號都會呼叫。`name` 像 `surface/main/hue`，`value` 是正規化的 0..1（事件是 `info.pulse`，文字框則是字串）。元件的範圍不是 0..1 的時候，`info.raw` 帶著沒縮放的值，接著還會再呼叫一次，名稱是 `…/raw`。[`signalSink()`](#signalSink) 會寫進 `Signals`，手機上則傳一個呼叫 `relay.set` 的函式。"},{"name":"haptics","type":"Boolean","en":"Buzz the phone briefly on a button, a toggle, a radio choice or a double-tap reset, where the browser can (Android Chrome). `false` turns it off.","zh":"在按下按鈕、切換開關、選了單選項目、雙擊重設的時候讓手機震一下，瀏覽器做得到的話（Android Chrome）。`false` 就關掉。"},{"name":"tabs","type":"Boolean","en":"Show a tab strip when the layout has more than one page. `false` hides it, and you switch with `setPage()`.","zh":"版面有一頁以上的時候，顯示分頁列。`false` 就隱藏，改用 `setPage()` 切換。"},{"name":"orientation","type":"'portrait' | 'landscape'","en":"Force the orientation. Left out, it follows the container's shape (wider than tall is landscape) and changes when the container does.","zh":"強制指定方向。不給的話，它跟著容器的形狀走（寬大於高是橫向），容器改變它也會跟著改。"},{"name":"onPage","type":"Function","en":"`(pageId) => {}`, when the page changes, and once at the start with the first page.","zh":"`(pageId) => {}`，換頁的時候呼叫，一開始也會用第一頁呼叫一次。"}],"needs":{"en":"A browser page (it builds DOM) and a container with a size. The panel injects its look once with [`injectTheme()`](#injectTheme). Nothing else to start: it works on the first touch. On a phone page, [`lockViewport()`](#lockViewport) stops the page from scrolling and zooming under the fingers.","zh":"要有瀏覽器頁面（它會建立 DOM），而且容器要有大小。面板會用 [`injectTheme()`](#injectTheme) 把它的外觀注入一次。不用另外啟動：第一次觸碰就能用。在手機頁面上，用 [`lockViewport()`](#lockViewport) 讓頁面不會在手指底下捲動或縮放。"},"notes":[{"en":"Public fields: `layout` (the normalized layout), `root` (the panel's element), `orientation`, `pageId`, `widgets` (a `Map` of `\"page/id\"` to widget), `sink`, `haptics`.","zh":"公開的欄位有 `layout`（正規化之後的版面）、`root`（面板的元素）、`orientation`、`pageId`、`widgets`（`\"page/id\"` 對應元件的 `Map`）、`sink`、`haptics`。"},{"en":"The constructor appends the panel to the container before it checks the layout, so a layout that throws there leaves an empty panel behind. Run [`validateLayout()`](#validateLayout) first if the layout comes from a file or a person.","zh":"建構函式在檢查版面之前就把面板加進容器了，所以版面在那裡丟出錯誤的話，會留下一個空的面板。版面來自檔案或使用者的話，請先跑 [`validateLayout()`](#validateLayout)。"}],"examples":[{"code":"import { Surface } from '@openav/surface';\n\nconst box = document.getElementById('app');\nbox.style.cssText = 'position:relative;width:360px;height:220px';   // the panel fills its container\n\nconst out = document.createElement('pre');\nbox.after(out);\nnew Surface(box, {\n  layout: { pages: [{ id: 'main', grid: { cols: 4, rows: 2 }, widgets: [\n    { id: 'size', type: 'fader', label: 'Size', x: 0, y: 0, w: 1, h: 2, min: 1, max: 10, def: 4 },\n    { id: 'pad', type: 'xy', x: 1, y: 0, w: 2, h: 2 },\n    { id: 'go', type: 'button', label: 'Go', x: 3, y: 0, w: 1, h: 2 },\n  ] }] },\n  sink: (name, value, info) => {\n    out.textContent = name + ' = ' + value + (info.raw !== undefined ? '  (raw ' + info.raw + ')' : '');\n  },\n});","caption":{"en":"A dark panel with a fader, an XY pad and a button. Touch one and the line under it shows the signal it sent, for example surface/main/size = 0.75 (raw 7.75).","zh":"一個深色面板，有一支推桿、一塊 XY 板和一顆按鈕。碰其中一個，下面那行就顯示它送出的訊號，例如 surface/main/size = 0.75 (raw 7.75)。"},"run":"module","lang":"js"},{"code":"import { Surface } from '@openav/surface';\n\nconst box = document.getElementById('app');\nbox.style.cssText = 'position:relative;width:360px;height:220px';\n\nconst surface = new Surface(box, {\n  layout: { pages: [{ id: 'main', grid: { cols: 4, rows: 2 }, widgets: [\n    { id: 'level', type: 'meter', label: 'Level', x: 0, y: 0, w: 1, h: 2 },\n    { id: 'echo', type: 'fader', label: 'Echo', x: 1, y: 0, w: 1, h: 2 },\n  ] }] },\n  sink() {},\n});\n\nfunction frame(ms) {\n  const v = 0.5 + 0.5 * Math.sin(ms / 500);\n  surface.feedback('surface/main/level', v);       // the meter fills\n  surface.feedback('surface/main/echo', 1 - v);    // the fader follows\n  requestAnimationFrame(frame);\n}\nrequestAnimationFrame(frame);","caption":{"en":"The meter rises and falls with a sine wave and the fader moves the opposite way: the show sending values back to the phone with feedback().","zh":"音量表隨著正弦波起落，推桿朝相反方向動：這是演出端用 feedback() 把數值送回手機。"},"run":"module","lang":"js"},{"code":"import { Surface } from '@openav/surface';\n\nconst box = document.getElementById('app');\nbox.style.cssText = 'position:relative;width:360px;height:220px';\nconst surface = new Surface(box, {\n  layout: { pages: [{ id: 'a', widgets: [{ id: 'x', type: 'fader' }] }] },\n  sink() {},\n});\n\ntry {\n  surface.setLayout({ pages: [{ id: 'a', widgets: [{ id: 'x', type: 'dial' }, { id: 'x', type: 'knob' }] }] });\n} catch (e) {\n  document.getElementById('app').after(Object.assign(document.createElement('pre'), { textContent: e.message }));\n}","caption":{"en":"A bad layout throws and lists every problem (an unknown type \"dial\", a duplicate id \"x\"); the panel that was already there stays as it was.","zh":"有問題的版面會丟出錯誤，並列出每個問題（不認得的類型「dial」、重複的 id「x」），原本的面板維持不變。"},"run":"module","lang":"js"}],"related":["signalSink","validateLayout","autoSurface","resolvePage","injectTheme","lockViewport","WIDGET_TYPES","mountRemote"],"keywords":{"en":["surface","control surface","touchosc","widgets","fader","knob","xy pad","pads","layout","phone controller","panel","touch","feedback"],"zh":["控制面板","touchosc","元件","推桿","旋鈕","xy 板","打擊墊","版面","手機控制器","面板","觸控","回饋"]},"tbd":[],"claims":[],"members":[{"name":"setLayout","member":"method","signature":"raw","doc":"(Re)build from layout JSON.","line":76},{"name":"setPage","member":"method","signature":"id","doc":"","line":150},{"name":"feedback","member":"method","signature":"name, value","doc":"Runner → widget.","line":160},{"name":"widget","member":"method","signature":"page, id","doc":"","line":162},{"name":"disposeWidgets","member":"method","signature":"","doc":"","line":164},{"name":"dispose","member":"method","signature":"","doc":"","line":168}]},
{"id":"THEME_VARS","key":"THEME_VARS","name":"THEME_VARS","aliases":[],"kind":"constant","package":"@openav/surface","category":"remote","documented":true,"signature":"THEME_VARS = object, 15 keys","import":"import { THEME_VARS } from '@openav/surface';","source":{"file":"packages/surface/theme.js","line":14,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/theme.js#L14"},"summary":{"en":"The look of every control as 15 CSS custom properties on `:root` (`--oav-bg`, `--oav-accent` …): override them to re-skin the whole surface.","zh":"每個控制元件的外觀，收成 `:root` 上的 15 個 CSS 自訂屬性（`--oav-bg`、`--oav-accent` …）：覆寫它們就能替整個面板換皮膚。"},"description":[{"en":"The design is a set of rules rather than decoration, because a controller is used in the dark, fast and half-looking: dark cards on near-black, the only saturated thing on screen is a value, every touch target at least 44 px (`--oav-touch-min`), the number you are changing always visible, and a pressed widget outlines itself in its colour. Everything is a CSS variable, so a work re-skins the surface by overriding a few of them, never by editing widget code.","zh":"設計是一組規則，不是裝飾，因為控制器是在黑暗中、很快、只用眼角餘光看的：近黑底上的深色卡片、畫面上唯一飽和的東西是數值、每個觸碰目標至少 44 像素（`--oav-touch-min`）、正在調的數字一定看得到、被按住的元件用自己的顏色描邊。所有東西都是 CSS 變數，所以作品只要覆寫其中幾個就能換皮膚，不必改元件的程式碼。"},{"en":"[`injectTheme()`](#injectTheme) writes these values into the page once. Override after that, on `document.documentElement` or in your own stylesheet, with a rule that comes later on `:root`.","zh":"[`injectTheme()`](#injectTheme) 會把這些值寫進頁面一次。要覆寫的話，在那之後，對 `document.documentElement` 設，或在你自己的樣式表裡用排在後面的 `:root` 規則。"}],"value":{"type":"Object","en":"Property name to its default value: `--oav-bg` #0a0c11, `--oav-surface` #141922, `--oav-surface-2` #1b2230, `--oav-line` #283044, `--oav-text` #e8ecf5, `--oav-dim` #8691a8, `--oav-accent` #7ea6ff, `--oav-ok` #3ddc84, `--oav-warn` #f5a524, `--oav-bad` #e5484d, `--oav-radius` 14px, `--oav-gap` 8px, `--oav-touch-min` 44px, `--oav-font` (the system font) and `--oav-mono` (a monospace font).","zh":"屬性名稱對應它的預設值：`--oav-bg` #0a0c11、`--oav-surface` #141922、`--oav-surface-2` #1b2230、`--oav-line` #283044、`--oav-text` #e8ecf5、`--oav-dim` #8691a8、`--oav-accent` #7ea6ff、`--oav-ok` #3ddc84、`--oav-warn` #f5a524、`--oav-bad` #e5484d、`--oav-radius` 14px、`--oav-gap` 8px、`--oav-touch-min` 44px、`--oav-font`（系統字型）和 `--oav-mono`（等寬字型）。"},"params":[],"notes":[],"examples":[{"code":"import { THEME_VARS } from '@openav/surface';\n\ndocument.getElementById('app').innerHTML = ['--oav-bg', '--oav-accent', '--oav-radius', '--oav-touch-min']\n  .map((k) => k + ': ' + THEME_VARS[k]).join('<br>');","caption":{"en":"Prints the background #0a0c11, the accent #7ea6ff, the corner radius 14px and the 44px minimum touch size.","zh":"印出背景 #0a0c11、強調色 #7ea6ff、圓角 14px 和最小觸碰尺寸 44px。"},"run":"module","lang":"js"}],"related":["injectTheme","COLOR_NAMES","Surface"],"keywords":{"en":["theme","css variables","custom properties","skin","colors","accent","dark","touch size","re-skin"],"zh":["主題","css 變數","自訂屬性","換皮膚","顏色","強調色","深色","觸碰尺寸"]},"tbd":[],"claims":[]},
{"id":"toggleFullscreen","key":"toggleFullscreen","name":"toggleFullscreen","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"toggleFullscreen()","import":"import { toggleFullscreen } from '@openav/surface';","source":{"file":"packages/surface/platform.js","line":36,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/platform.js#L36"},"summary":{"en":"Enters fullscreen if the page is not in it, and leaves it if it is. Call it from a tap, because browsers refuse otherwise.","zh":"頁面不在全螢幕就進入，已經在就離開。要在點擊裡呼叫，不然瀏覽器會拒絕。"},"description":[{"en":"It asks for the whole page (`document.documentElement`) with the browser's navigation bar hidden, and quietly does nothing on a browser without fullscreen (see [`canFullscreen()`](#canFullscreen)). The phone app puts it on a ⛶ button in the status bar.","zh":"它要求整個頁面（`document.documentElement`）進入全螢幕，並把瀏覽器的導覽列藏起來，瀏覽器沒有全螢幕功能（見 [`canFullscreen()`](#canFullscreen)）的話就安靜地什麼都不做。手機 app 把它放在狀態列的 ⛶ 按鈕上。"}],"params":[],"needs":{"en":"A user gesture: call it from a `click` or `pointerdown` handler. Called on page load it is refused by the browser.","zh":"需要使用者手勢：在 `click` 或 `pointerdown` 的處理函式裡呼叫。在頁面載入時呼叫，瀏覽器會拒絕。"},"notes":[],"examples":[{"code":"import { toggleFullscreen, canFullscreen } from '@openav/surface';\n\nconst button = document.createElement('button');\nbutton.textContent = '⛶ fullscreen';\nbutton.hidden = !canFullscreen();\nbutton.onclick = toggleFullscreen;\ndocument.body.append(button);","caption":{"en":"A button that puts the page into fullscreen and out again, and is hidden where the browser has no fullscreen.","zh":"一顆讓頁面進入、再離開全螢幕的按鈕，瀏覽器沒有全螢幕的地方就隱藏。"},"run":"static","lang":"js"}],"related":["canFullscreen","mountRemote"],"keywords":{"en":["fullscreen","toggle fullscreen","full screen","exit fullscreen","button"],"zh":["全螢幕","切換全螢幕","離開全螢幕","按鈕"]},"tbd":[],"claims":[]},
{"id":"validateLayout","key":"validateLayout","name":"validateLayout","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"validateLayout(layout)","import":"import { validateLayout } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":44,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L44"},"summary":{"en":"Checks a layout and returns a list of readable problems; an empty list means it is fine.","zh":"檢查一份版面，回傳一串看得懂的問題清單，清單是空的就表示沒問題。"},"description":[{"en":"It looks for: no object, or no non-empty `pages` array (it stops there); a page without an `id`, with a duplicate id or an id outside `A–Z a–z 0–9 _ -`; a widget whose `type` is not in [`WIDGET_TYPES`](#WIDGET_TYPES), without an `id`, with a duplicate id on its page or an id outside those characters (the id becomes part of a signal name); and a `min` equal to its `max`. It reports every problem it finds, not only the first.","zh":"它會檢查：不是物件、或沒有非空的 `pages` 陣列（到這裡就停），頁面沒有 `id`、id 重複、或 id 用了 `A–Z a–z 0–9 _ -` 以外的字元，元件的 `type` 不在 [`WIDGET_TYPES`](#WIDGET_TYPES) 裡、沒有 `id`、在同一頁 id 重複、或 id 用了那些以外的字元（id 會變成訊號名稱的一部分），還有 `min` 等於 `max`。它會報告找得到的每一個問題，不只是第一個。"},{"en":"It does not look at positions, grids or `target`s: a widget that overlaps another, or a `target` that is not a param, is not a problem here. [`Surface`](#Surface)'s `setLayout()` runs it and throws with the list when it is not empty.","zh":"它不看位置、格線或 `target`：元件互相重疊、或 `target` 不是任何參數，在這裡都不算問題。[`Surface`](#Surface) 的 `setLayout()` 會跑它，清單不是空的就丟出錯誤。"}],"params":[{"name":"layout","type":"Object","en":"The raw layout JSON.","zh":"原始的版面 JSON。"}],"returns":{"type":"string[]","en":"One sentence per problem, with where it is (`pages[0].widgets[2]: unknown type \"dial\"`). `[]` when the layout is fine.","zh":"每個問題一句話，附上位置（`pages[0].widgets[2]: unknown type \"dial\"`）。版面沒問題就是 `[]`。"},"notes":[],"examples":[{"code":"import { validateLayout } from '@openav/surface';\n\nconst good = validateLayout({ pages: [{ id: 'main', widgets: [{ id: 'size', type: 'fader' }] }] });\nconst bad = validateLayout({ pages: [{ id: 'main page', widgets: [\n  { id: 'a', type: 'dial' }, { id: 'a', type: 'fader' }, { type: 'knob', min: 1, max: 1 },\n] }] });\ndocument.getElementById('app').innerHTML = ['good: ' + JSON.stringify(good), ...bad].join('<br>');","caption":{"en":"Prints \"good: []\", then five problems for the bad layout: the page id, the unknown type, the duplicate id, the missing id and min equal to max.","zh":"印出「good: []」，然後是有問題的版面的五個問題：頁面 id、不認得的類型、重複的 id、缺少的 id、以及 min 等於 max。"},"run":"module","lang":"js"}],"related":["normalizeLayout","Surface","WIDGET_TYPES"],"keywords":{"en":["validate","layout","check layout","errors","problems","lint","layout json"],"zh":["驗證","版面","檢查版面","錯誤","問題","版面 json"]},"tbd":[],"claims":[]},
{"id":"WIDGET_CLASSES","key":"WIDGET_CLASSES","name":"WIDGET_CLASSES","aliases":[],"kind":"constant","package":"@openav/surface","category":"remote","documented":true,"signature":"WIDGET_CLASSES = object, 14 keys","import":"import { WIDGET_CLASSES } from '@openav/surface';","source":{"file":"packages/surface/widgets.js","line":510,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/widgets.js#L510"},"summary":{"en":"The widget classes, one per type, that `Surface` builds from a layout: `WIDGET_CLASSES.fader`, `.knob`, `.xy` and so on (advanced).","zh":"`Surface` 依版面建立的元件類別，每種類型一個：`WIDGET_CLASSES.fader`、`.knob`、`.xy` 等等（進階）。"},"description":[{"en":"Most pages never touch it: [`Surface`](#Surface) looks each `spec.type` up here. It is exported for code that wants a single widget without a whole panel. `spec` is the widget's entry of the layout (`{ id, type, label, min, max, … }`) and `ctx` is `{ page, out(name, value, info), haptic(ms) }`, where `out` is where the widget's signals go and `page` is the page id used in their names. A widget's element is `.el`; put it in the page yourself.","zh":"大部分頁面用不到它：[`Surface`](#Surface) 會在這裡查每個 `spec.type`。把它匯出來，是給只想要單一元件、不要整個面板的程式用的。`spec` 是元件在版面裡的那一項（`{ id, type, label, min, max, … }`），`ctx` 是 `{ page, out(name, value, info), haptic(ms) }`，`out` 就是元件的訊號送去的地方，`page` 是用在訊號名稱裡的頁面 id。元件的元素是 `.el`，要自己把它放進頁面。"},{"en":"Adding your own key here does not make a new layout type: [`validateLayout()`](#validateLayout) only accepts the names in [`WIDGET_TYPES`](#WIDGET_TYPES).","zh":"在這裡加你自己的鍵，並不會多出一種新的版面類型：[`validateLayout()`](#validateLayout) 只接受 [`WIDGET_TYPES`](#WIDGET_TYPES) 裡的名稱。"}],"value":{"type":"Object","en":"An object with the same 14 keys as [`WIDGET_TYPES`](#WIDGET_TYPES), each a class: `new WIDGET_CLASSES[type](spec, ctx)`.","zh":"一個物件，鍵跟 [`WIDGET_TYPES`](#WIDGET_TYPES) 一樣有 14 個，每個值是一個類別：`new WIDGET_CLASSES[type](spec, ctx)`。"},"params":[],"notes":[{"en":"Advanced. The `keyboard` widget wraps `KeysPiano` from `@openav/keys`.","zh":"進階用法。`keyboard` 元件包著 `@openav/keys` 的 `KeysPiano`。"}],"examples":[{"code":"import { WIDGET_CLASSES } from '@openav/surface';\n\nconst sent = [];\nconst ctx = { page: 'solo', out: (name, value) => sent.push(name + ' = ' + value), haptic() {} };\nconst toggle = new WIDGET_CLASSES.toggle({ id: 'mute', type: 'toggle', label: 'Mute' }, ctx);\ndocument.getElementById('app').append(toggle.el);\n\ntoggle.set(true);    // what a tap does\ntoggle.set(false);\ndocument.getElementById('app').append(Object.assign(document.createElement('pre'), { textContent: sent.join('\\n') }));","caption":{"en":"A lone toggle on the page, and under it what it sent when it was switched on and off: surface/solo/mute = 1, then 0.","zh":"頁面上有一個單獨的開關，下面是它被打開、關掉時送出的訊號：surface/solo/mute = 1，然後是 0。"},"run":"module","lang":"js"}],"related":["Surface","WIDGET_TYPES"],"keywords":{"en":["widget classes","single widget","fader class","knob class","build a widget","component"],"zh":["元件類別","單一元件","推桿類別","旋鈕類別","建立元件","元件"]},"tbd":[],"claims":[]},
{"id":"WIDGET_TYPES","key":"WIDGET_TYPES","name":"WIDGET_TYPES","aliases":[],"kind":"constant","package":"@openav/surface","category":"remote","documented":true,"signature":"WIDGET_TYPES = array, 14 items","import":"import { WIDGET_TYPES } from '@openav/surface';","source":{"file":"packages/surface/layout.js","line":38,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/layout.js#L38"},"summary":{"en":"The names of the 14 widget types a layout may use: fader, knob, encoder, button, toggle, xy, bank, radio, pads, keyboard, number, text, label, meter.","zh":"版面可以用的 14 種元件類型名稱：fader、knob、encoder、button、toggle、xy、bank、radio、pads、keyboard、number、text、label、meter。"},"description":[{"en":"Each widget publishes under `surface/<page>/<id>`, normalized 0..1; when its range is not 0..1 the unscaled value also goes out as `…/raw`. What each type sends:","zh":"每個元件都發布在 `surface/<page>/<id>` 底下，正規化成 0..1。範圍不是 0..1 的話，沒縮放的值也會以 `…/raw` 送出。各類型送出的東西："},{"en":"```text\nfader, knob, number   <id>\nbutton                 <id>  1 while held, 0 on release\ntoggle                 <id>  1 or 0\nencoder                <id> (phase 0..1, wraps) and <id>/delta (turns)\nxy                     <id>/x  <id>/y  <id>/down\nbank                   <id>/1 … <id>/N        (count, default 8)\nradio                  <id> = index / (n - 1), raw = index\npads                   <id>/hit {pad,row,col,vel}  and  <id>/<n> while held\nkeyboard               midi/note/on  midi/note/off  midi/cc/64\ntext                   <id>  a string, as a pulse\nlabel, meter           nothing: they only receive feedback\n```","zh":"```text\nfader, knob, number   <id>\nbutton                 <id>  1 while held, 0 on release\ntoggle                 <id>  1 or 0\nencoder                <id> (phase 0..1, wraps) and <id>/delta (turns)\nxy                     <id>/x  <id>/y  <id>/down\nbank                   <id>/1 … <id>/N        (count, default 8)\nradio                  <id> = index / (n - 1), raw = index\npads                   <id>/hit {pad,row,col,vel}  and  <id>/<n> while held\nkeyboard               midi/note/on  midi/note/off  midi/cc/64\ntext                   <id>  a string, as a pulse\nlabel, meter           nothing: they only receive feedback\n```\n\n[`signalNames()`](#signalNames) 可以替你的版面列出這些名稱。"},{"en":"Fields every widget accepts: `id`, `type`, `label`, `min`, `max`, `def`, `step`, `unit`, `color` (one of [`COLOR_NAMES`](#COLOR_NAMES)) and, for hand-written layouts, `target`, the param key that [`routesFromLayout()`](#routesFromLayout) turns into a route. Others by type: `orient` (`v`, `h` or `auto`) on a fader, `spring: true` on an XY pad, `count` and `labels` on a bank, `options` on a radio, `rows`, `cols` and `labels` on pads, `pulse` on a button.","zh":"每個元件都接受的欄位：`id`、`type`、`label`、`min`、`max`、`def`、`step`、`unit`、`color`（[`COLOR_NAMES`](#COLOR_NAMES) 其中之一），手寫版面還可以加 `target`，也就是 [`routesFromLayout()`](#routesFromLayout) 要變成路由的參數鍵。各類型另有：推桿的 `orient`（`v`、`h` 或 `auto`）、XY 板的 `spring: true`、bank 的 `count` 和 `labels`、radio 的 `options`、pads 的 `rows`、`cols` 和 `labels`。"}],"value":{"type":"string[]","en":"The keys of [`DEFAULT_SIZE`](#DEFAULT_SIZE), in that order. [`validateLayout()`](#validateLayout) rejects any `type` that is not in it.","zh":"[`DEFAULT_SIZE`](#DEFAULT_SIZE) 的鍵，順序也一樣。[`validateLayout()`](#validateLayout) 會拒絕不在裡面的 `type`。"},"params":[],"notes":[],"examples":[{"code":"import { WIDGET_TYPES, DEFAULT_SIZE } from '@openav/surface';\n\ndocument.getElementById('app').innerHTML = WIDGET_TYPES\n  .map((t) => t + ' ' + DEFAULT_SIZE[t].w + '×' + DEFAULT_SIZE[t].h)\n  .join('<br>');","caption":{"en":"Lists the 14 types with their natural size in grid cells, from fader 1×4 to meter 1×4.","zh":"列出 14 種類型和它們在格線上的預設大小，從 fader 1×4 到 meter 1×4。"},"run":"module","lang":"js"}],"related":["DEFAULT_SIZE","WIDGET_CLASSES","validateLayout","signalNames","COLOR_NAMES"],"keywords":{"en":["widget types","fader","knob","encoder","button","toggle","xy","bank","radio","pads","keyboard","number","text","label","meter"],"zh":["元件類型","推桿","旋鈕","無限旋鈕","按鈕","開關","xy","推桿組","單選","打擊墊","琴鍵","數字框","文字框","標籤","音量表"]},"tbd":[],"claims":[]},
{"id":"widgetTypeFor","key":"widgetTypeFor","name":"widgetTypeFor","aliases":[],"kind":"function","package":"@openav/surface","category":"remote","documented":true,"signature":"widgetTypeFor(p, { continuousCount = 0, style = 'auto' } = {})","import":"import { widgetTypeFor } from '@openav/surface';","source":{"file":"packages/surface/auto.js","line":32,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/surface/auto.js#L32"},"summary":{"en":"Picks the widget type for one param declaration: a button for a pulse, a toggle, a radio, a fader or a knob.","zh":"替一個參數宣告挑元件類型：脈衝是按鈕，其他是開關、單選、推桿或旋鈕。"},"description":[{"en":"The rules, first match wins: a `surface: { type }` hint on the param; `pulse` is a `button`; `step: 1` over a range of 1 (two values) is a `toggle`; `step: 1` over a range of 2 to 6 (three to seven values) is a `radio`; `style` `'knob'` or `'fader'` as asked; otherwise a `fader`, or a `knob` when `continuousCount` is above 6. A missing `min` is 0 and a missing `max` is 1. [`autoSurface()`](#autoSurface) calls it for every param.","zh":"規則依序比對，第一個符合的就用：參數上有 `surface: { type }` 提示就照它，`pulse` 是 `button`，`step: 1` 而範圍是 1（兩個值）是 `toggle`，`step: 1` 而範圍是 2 到 6（三到七個值）是 `radio`，`style` 要求 `'knob'` 或 `'fader'` 就照辦，其他是 `fader`，`continuousCount` 超過 6 時是 `knob`。沒寫 `min` 當 0，沒寫 `max` 當 1。[`autoSurface()`](#autoSurface) 會對每個參數呼叫它。"}],"params":[{"name":"p","type":"Object","en":"A param: `{ key, min?, max?, step?, pulse?, surface? }`.","zh":"一個參數：`{ key, min?, max?, step?, pulse?, surface? }`。"},{"name":"continuousCount","type":"Number","optional":true,"en":"How many continuous params the panel has in all. More than 6 turns faders into knobs.","zh":"整個面板一共有幾個連續參數。超過 6 個，推桿就改成旋鈕。"},{"name":"style","type":"'auto' | 'fader' | 'knob'","optional":true,"en":"Force faders or knobs for the continuous params.","zh":"連續參數強制用推桿或旋鈕。"}],"returns":{"type":"String","en":"One of the names in [`WIDGET_TYPES`](#WIDGET_TYPES).","zh":"[`WIDGET_TYPES`](#WIDGET_TYPES) 裡的其中一個名稱。"},"notes":[],"examples":[{"code":"import { widgetTypeFor } from '@openav/surface/auto';\n\nconst params = [\n  { key: 'hue', min: 0, max: 360 },\n  { key: 'mode', min: 0, max: 3, step: 1 },\n  { key: 'mirror', min: 0, max: 1, step: 1 },\n  { key: 'burst', pulse: true },\n];\nconst rows = params.map((p) => p.key + ' → ' + widgetTypeFor(p));\nrows.push('hue with 9 continuous params → ' + widgetTypeFor(params[0], { continuousCount: 9 }));\ndocument.getElementById('app').innerHTML = rows.join('<br>');","caption":{"en":"Prints hue → fader, mode → radio, mirror → toggle, burst → button, and a knob for hue once the panel has nine continuous params.","zh":"印出 hue → fader、mode → radio、mirror → toggle、burst → button，以及面板有九個連續參數時，hue 變成旋鈕。"},"run":"module","lang":"js"}],"related":["autoSurface","WIDGET_TYPES"],"keywords":{"en":["widget type","param to widget","fader or knob","toggle","radio","button","auto choose"],"zh":["元件類型","參數轉元件","推桿或旋鈕","開關","單選","按鈕","自動選擇"]},"tbd":[],"claims":[]},
{"id":"mountConsole","key":"mountConsole","name":"mountConsole","aliases":[],"kind":"function","package":"@openav/console","category":"backstage","documented":true,"signature":"mountConsole(root, app, opts = {})","import":"import { mountConsole } from '@openav/console';","source":{"file":"packages/console/index.js","line":39,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/console/index.js#L39"},"summary":{"en":"Mounts the director's desk into an element: transport and scrubber, layers overview, inputs, mapping, param sliders, signal meters and a fullscreen performance mode.","zh":"把導演控台掛進一個元素：播放控制與 scrubber、圖層總覽、輸入、映射、參數推桿、訊號音量表，以及全螢幕的演出模式。"},"description":[{"en":"The desk reads the same objects the loop runs and owns nothing. From top to bottom it has the transport (play, previous and next scene, reset, the clock) and a scrubber with a block for every scene (with a tick for every cue, and a dashed end for a hold, when the scenes have them); with a score, the Director panel; the **Layers** overview (L1 input, L2 mapping, L3 world, L4 output); **L1 · Input** (MIDI devices, 🎤 mic, 🖐 hands, 🕺 body, leap); **L2 · Mapping** (the routes with a mute box and ✕, **🎛 map knobs in order**, ⬇ export, ⬆ import, clear); **L3 · Params** (a slider per param with ✕ to clear the override and a *learn* chip; a yellow label means overridden); **L4 · sound** when there is a sound engine; and the **Signals** meters with the MIDI log. Every panel header collapses its section.","zh":"控台讀的是迴圈實際在跑的那些物件，自己什麼都不擁有。由上到下依序是：播放控制（播放、上一個與下一個場景、重設、時鐘）和一條 scrubber（每個場景一個區塊，場景有 cue 的話每個 cue 一個刻度、有 hold 的話尾端是虛線）。有總譜的話，Director 面板。**Layers** 總覽（L1 輸入、L2 映射、L3 世界、L4 輸出）。**L1 · Input**（MIDI 裝置、🎤 mic、🖐 hands、🕺 body、leap）。**L2 · Mapping**（路由清單，附靜音框與 ✕，還有 **🎛 map knobs in order**、⬇ export、⬆ import、clear）。**L3 · Params**（每個參數一支推桿，附清除覆寫的 ✕ 和 *learn* 標籤，黃色標籤代表被覆寫）。有聲音引擎的話是 **L4 · sound**。最後是 **Signals** 音量表和 MIDI 紀錄。每個面板的標題都可以收合它的區塊。"},{"en":"Keyboard, on the whole window: <kbd>Space</kbd> play and pause (at a hold: release it), <kbd>→</kbd> and <kbd>←</kbd> next and previous scene (→ at a hold releases it), <kbd>R</kbd> back to the start, <kbd>T</kbd> performance mode (a fullscreen teleprompter for the performer: scene title, note and acts, the clock, the next scene, a HOLD banner), <kbd>F</kbd> fullscreen, <kbd>Esc</kbd> stops the mapping wizard or leaves keyboard-piano capture. Typing in an input field, a held key and any combination with ⌘, Ctrl or Alt are ignored. While the on-screen piano captures the keyboard the **letters** (R, T, F) belong to the piano; Space, the arrows and Esc always work.","zh":"鍵盤，作用在整個視窗：<kbd>Space</kbd> 播放與暫停（在 hold 上則是放行）、<kbd>→</kbd> 和 <kbd>←</kbd> 下一個與上一個場景（在 hold 上，→ 是放行）、<kbd>R</kbd> 回到起點、<kbd>T</kbd> 演出模式（給表演者看的全螢幕提詞器：場景標題、備註與 acts、時鐘、下一個場景、HOLD 橫幅）、<kbd>F</kbd> 全螢幕、<kbd>Esc</kbd> 停止映射精靈，或離開鍵盤鋼琴的捕捉。在輸入欄位裡打字、按住不放的鍵，以及任何帶 ⌘、Ctrl 或 Alt 的組合都會被忽略。螢幕鋼琴捕捉鍵盤的時候，**字母鍵**（R、T、F）屬於鋼琴，Space、方向鍵和 Esc 則永遠有效。"}],"params":[{"name":"root","type":"HTMLElement","en":"The element to build the desk in. It gets the class `oav-console`, and the desk's styles are added once to the head.","zh":"要在裡面建出控台的元素。它會得到 `oav-console` 這個 class，控台的樣式也會一次加進 `<head>`。"},{"name":"app","type":"Object","en":"The objects the desk reads: `{ timeline, params, mapper, signals, stage }` are needed, and `midi`, `controllers`, `keys`, `sound`, `audio`, `hands`, `pose`, `leap`, `chord`, `remote`, `drums`, `loop`, `osc`, `score`, `director` are optional. A missing part hides its section. [`createShow()`](#createShow) passes `show.app`.","zh":"控台要讀取的物件：`{ timeline, params, mapper, signals, stage }` 一定要有，`midi`、`controllers`、`keys`、`sound`、`audio`、`hands`、`pose`、`leap`、`chord`、`remote`、`drums`、`loop`、`osc`、`score`、`director` 是選填的。缺少的部分，它的區塊就會隱藏。[`createShow()`](#createShow) 傳的是 `show.app`。"},{"name":"opts","type":"Object","optional":true,"default":"{}","en":"`{ layers: false }` and `{ signals: false }` leave out the Layers and Signals panels; `{ learnOrder: ['hue', 'size'] }` sets the order of the **🎛 map knobs in order** wizard (default: the params as declared, skipping hidden ones and any with `wizard: false`).","zh":"`{ layers: false }` 和 `{ signals: false }` 會省略 Layers 和 Signals 面板。`{ learnOrder: ['hue', 'size'] }` 設定 **🎛 map knobs in order** 精靈的順序（預設是依宣告順序的參數，略過 hidden 的和 `wizard: false` 的）。"}],"returns":{"type":"Object","en":"`{ render(state), perf, director, wizard, dispose() }`: `render` redraws the desk, `perf` is the performance mode (`toggle()`, `active`, `render()`), `director` the Director panel (or `null`), `wizard` the knob-mapping wizard, and `dispose()` removes the keyboard handler and empties the element.","zh":"`{ render(state), perf, director, wizard, dispose() }`：`render` 重畫控台，`perf` 是演出模式（`toggle()`、`active`、`render()`），`director` 是 Director 面板（或 `null`），`wizard` 是旋鈕映射精靈，`dispose()` 會移除鍵盤處理並清空這個元素。"},"needs":{"en":"Call `desk.render(state)` every frame after `stage.frame()` (a show's loop does): the desk only redraws when you tell it to. `state` is the resolved params, `params.resolve(timeline.state())`.","zh":"每個畫面在 `stage.frame()` 之後呼叫 `desk.render(state)`（演出的迴圈會做）：控台只在你叫它的時候才重畫。`state` 是解析後的參數，也就是 `params.resolve(timeline.state())`。"},"notes":[{"en":"The keyboard handler is on `window`, so two desks on one page both answer the keys. `dispose()` removes it. The performance mode's teleprompter is appended to `document.body`, not to `root`.","zh":"鍵盤處理是掛在 `window` 上的，所以同一頁有兩個控台的話，兩個都會回應按鍵。`dispose()` 會移除它。演出模式的提詞器是加在 `document.body` 底下，不是 `root`。"},{"en":"Touch a slider (or a mapped control) and that param leaves the timeline's hands until you press its ✕: the score gives way when a person grabs a control.","zh":"碰一下推桿（或被映射的控制），那個參數就離開時間軸的掌控，直到你按下它的 ✕：有人抓住控制項的時候，樂譜讓路。"}],"examples":[{"code":"import { Signals, Params, Loop } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\nimport { Timeline } from '@openav/timeline';\nimport { Stage } from '@openav/stage';\nimport { mountConsole } from '@openav/console';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'hue', label: 'Hue', min: 0, max: 360, def: 200 }, { key: 'size', min: 0, max: 1, def: 0.3 }]);\nconst mapper = new Mapper({ signals, params });\nconst timeline = new Timeline({ params, total: 60, automation: { hue: [[0, 200], [60, 320]] },\n  scenes: [{ id: 'dawn', t: 0, title: 'Dawn' }, { id: 'noon', t: 30, title: 'Noon' }] });\nconst stage = new Stage({ container: document.createElement('div'), params, signals });\n\nconst desk = mountConsole(document.getElementById('app'), { timeline, params, mapper, signals, stage });\ntimeline.play();\nnew Loop((dt) => {\n  timeline.advance(dt);\n  desk.render(params.resolve(timeline.state()));      // once per frame\n}).start();","caption":{"en":"The desk appears in the page: a transport with a clock running, a scrubber with two scene blocks (Dawn, Noon), the layers overview and a slider each for Hue and Size. The Hue slider follows the timeline until you drag it.","zh":"控台出現在頁面上：一個時鐘在走的播放控制、一條有兩個場景區塊（Dawn、Noon）的 scrubber、圖層總覽，以及 Hue 和 Size 各一支推桿。Hue 推桿會跟著時間軸走，直到你去拖它。"},"run":"module","lang":"js"},{"code":"import { Signals, Params } from '@openav/core';\nimport { Mapper } from '@openav/mapping';\nimport { Timeline } from '@openav/timeline';\nimport { Stage } from '@openav/stage';\nimport { mountConsole } from '@openav/console';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 200 }]);\nconst timeline = new Timeline({ params, total: 30 });\nconst app = { timeline, params, signals, mapper: new Mapper({ signals, params }), stage: new Stage({ container: document.createElement('div'), params, signals }) };\n\nconst desk = mountConsole(document.getElementById('app'), app, { layers: false, signals: false });\ndesk.render(params.resolve(timeline.state()));\nconsole.log(desk.perf.active, desk.perf.toggle(), desk.perf.active);    // false true true: the T key, in code\ndesk.perf.toggle();                                                     // back to the desk\ndesk.dispose();                                                         // removes the key handler and empties the element\nconsole.log(document.getElementById('app').children.length);            // 0","caption":{"en":"A desk without the Layers and Signals panels: the console prints `false true true` as performance mode is switched on from code, then `0` after `dispose()` has emptied the element.","zh":"一個沒有 Layers 和 Signals 面板的控台：用程式打開演出模式，主控台印出 `false true true`，`dispose()` 把元素清空之後印出 `0`。"},"run":"module","lang":"js"}],"related":["createShow","Timeline","Mapper","LearnWizard","Director","Params","MonitorFeed"],"keywords":{"en":["console","desk","director desk","control panel","sliders","transport","scrubber","performance mode","teleprompter","keyboard shortcuts","ui","learn"],"zh":["控台","導演控台","控制面板","推桿","播放控制","scrubber","演出模式","提詞器","鍵盤快速鍵","介面","learn"]},"tbd":[],"claims":[]},
{"id":"defaultMonitorUrl","key":"defaultMonitorUrl","name":"defaultMonitorUrl","aliases":[],"kind":"function","package":"@openav/monitor","category":"backstage","documented":true,"signature":"defaultMonitorUrl(loc = globalThis.location)","import":"import { defaultMonitorUrl } from '@openav/monitor';","source":{"file":"packages/monitor/index.js","line":17,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/monitor/index.js#L17"},"summary":{"en":"Where the backstage relay is unless told otherwise: the page's own host on port 7457 as a `ws://` address, and `null` on an https page or where there is no page.","zh":"沒有指定的時候後台中繼站在哪裡：頁面自己的主機加上 7457 連接埠，寫成 `ws://` 位址，在 https 頁面或沒有頁面的地方是 `null`。"},"description":[{"en":"It is the default `url` of [`MonitorFeed`](#MonitorFeed). Not on an https page: the browser blocks `ws://` there (mixed content, logged as an error), and a page published over https (openaudiovisual.com, a tunnel) has no relay beside it. A show that runs one behind TLS passes `{ url: 'wss://…' }` explicitly.","zh":"它是 [`MonitorFeed`](#MonitorFeed) 預設的 `url`。https 頁面上不用：瀏覽器在那裡會擋住 `ws://`（混合內容，會被記成錯誤），而且用 https 發布的頁面（openaudiovisual.com、通道）旁邊沒有中繼站。在 TLS 後面跑了一個中繼站的演出，要明確傳 `{ url: 'wss://…' }`。"}],"params":[{"name":"loc","type":"Object","optional":true,"default":"globalThis.location","en":"Anything with `protocol` and `hostname`, like `location`. Pass a fake one to test.","zh":"任何有 `protocol` 和 `hostname` 的東西，像 `location`。測試的時候可以傳一個假的。"}],"returns":{"type":"String | null","en":"`'ws://<hostname>:7457'`, or `null` when `loc` is missing or its protocol is `https:`.","zh":"`'ws://<hostname>:7457'`，`loc` 不存在、或它的 protocol 是 `https:` 的時候是 `null`。"},"notes":[],"examples":[{"code":"import { defaultMonitorUrl } from '@openav/monitor';\n\nconsole.log(defaultMonitorUrl({ protocol: 'http:', hostname: '192.168.1.20' }));        // ws://192.168.1.20:7457\nconsole.log(defaultMonitorUrl({ protocol: 'https:', hostname: 'openaudiovisual.com' })); // null\nconsole.log(defaultMonitorUrl(null));                                                    // null: no page","caption":{"en":"The console prints `ws://192.168.1.20:7457`, then `null` twice.","zh":"主控台印出 `ws://192.168.1.20:7457`，然後兩個 `null`。"},"run":"module","lang":"js"}],"related":["MonitorFeed","snapshotOf"],"keywords":{"en":["default url","monitor url","ws://","7457","https","mixed content","relay address"],"zh":["預設網址","監看網址","ws://","7457","https","混合內容","中繼站位址"]},"tbd":[],"claims":[]},
{"id":"MonitorFeed","key":"MonitorFeed","name":"MonitorFeed","aliases":[],"kind":"class","package":"@openav/monitor","category":"backstage","documented":true,"signature":"new MonitorFeed({ url = defaultMonitorUrl(), hz = 15 } = {})","import":"import { MonitorFeed } from '@openav/monitor';","source":{"file":"packages/monitor/index.js","line":22,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/monitor/index.js#L22"},"summary":{"en":"Streams snapshots of the running show to the backstage relay over a WebSocket, at most `hz` times a second, and quietly keeps trying when there is no relay.","zh":"把正在進行的演出的快照透過 WebSocket 串流到後台中繼站，每秒最多 `hz` 次，沒有中繼站的時候安靜地持續嘗試。"},"description":[{"en":"A show without a backstage is a rehearsal. Run the relay on the machine that serves the show (`node packages/monitor/server.js`, port 7457, which also serves the backstage page at `/` and `GET /health`), and a stage manager's phone or laptop on the same network opens `http://<that machine>:7457/` to see the clock, the scene, the world, the frame rate, the overrides and every signal and param live, with a red \"stage feed lost\" banner when the stage stops sending for 4 seconds. The show page joins as `?role=stage`; every other socket is a monitor. [`createShow()`](#createShow) already creates and connects one, so you only build a `MonitorFeed` for a show you assembled by hand.","zh":"沒有後台的演出只是排練。在提供演出頁面的那台機器上跑中繼站（`node packages/monitor/server.js`，連接埠 7457，它同時在 `/` 提供後台頁面，並有 `GET /health`），同一個網路上舞台監督的手機或筆電打開 `http://<那台機器>:7457/`，就能即時看到時鐘、場景、世界、影格率、覆寫，以及每個訊號和參數。舞台超過 4 秒沒有送資料，就會出現紅色的「stage feed lost」橫幅。演出頁面是以 `?role=stage` 加入的，其他所有 socket 都是監看端。[`createShow()`](#createShow) 已經會建好並連上一個，所以只有手工組裝的演出才需要自己建 `MonitorFeed`。"},{"en":"It is made to cost nothing when left on: with no relay it reconnects with a growing delay (about 5 seconds, growing by 1.6 each time up to 30), though the browser logs each refused connection, and that message is expected. `frame()` throttles itself, so call it every frame with a snapshot (see [`snapshotOf()`](#snapshotOf)).","zh":"它的設計是一直開著也不花什麼成本：沒有中繼站的時候，它用愈來愈長的間隔重連（大約 5 秒，每次乘以 1.6，最長 30 秒），不過瀏覽器會把每次被拒絕的連線記下來，那個訊息是預期中的。`frame()` 自己會節流，所以每個畫面都可以用一份快照呼叫它（見 [`snapshotOf()`](#snapshotOf)）。"}],"params":[{"name":"url","type":"String | null","optional":true,"default":"defaultMonitorUrl()","en":"The relay address, such as `'ws://192.168.1.20:7457'` (`?role=stage` is added for you). The default comes from [`defaultMonitorUrl()`](#defaultMonitorUrl): the page's own host on port 7457, and `null` on an https page. With `null`, `connect()` does nothing.","zh":"中繼站的位址，例如 `'ws://192.168.1.20:7457'`（`?role=stage` 會替你加上去）。預設值來自 [`defaultMonitorUrl()`](#defaultMonitorUrl)：頁面自己的主機加上 7457 連接埠，https 頁面則是 `null`。是 `null` 的時候，`connect()` 什麼都不做。"},{"name":"hz","type":"Number","optional":true,"default":"15","en":"The most snapshots sent per second.","zh":"每秒最多送出幾個快照。"}],"needs":{"en":"Call `connect()` once (a show does), then `frame(snapshot)` every frame. `frame()` does nothing while `connected` is false. The relay must be reachable from the page: on an https page the browser blocks `ws://` (mixed content), so give a `wss://` address explicitly.","zh":"呼叫一次 `connect()`（演出會做），之後每個畫面呼叫 `frame(snapshot)`。`connected` 是 false 的時候，`frame()` 什麼都不做。頁面必須連得到中繼站：在 https 頁面上，瀏覽器會擋住 `ws://`（混合內容），所以請明確給一個 `wss://` 位址。"},"notes":[{"en":"Fields to read and set: `url` (with `?role=stage` appended, or `null`), `hz`, `connected`, `ws`, and the hook `onStatus = (ok) => {}`.","zh":"可以讀、可以設的欄位：`url`（附上 `?role=stage`，或是 `null`）、`hz`、`connected`、`ws`，以及掛鉤 `onStatus = (ok) => {}`。"},{"en":"On a public https page a hand-assembled show that connects to `ws://127.0.0.1:7457` meets Chrome's Local Network Access prompt (from Chrome 147 for WebSockets, per the package README): press Allow, or open the page from `http://localhost`.","zh":"在公開的 https 頁面上，手工組裝的演出如果連 `ws://127.0.0.1:7457`，會遇到 Chrome 的區域網路存取詢問（依套件 README，WebSocket 從 Chrome 147 開始）：按允許，或從 `http://localhost` 開啟頁面。"}],"examples":[{"code":"import { MonitorFeed } from '@openav/monitor';\n\nclass FakeSocket {                                   // stands in for the relay, so this runs anywhere\n  constructor(url) { console.log('connecting', url); setTimeout(() => this.onopen(), 0); }\n  send(text) { console.log('sent', Object.keys(JSON.parse(text)).join(' ')); }\n}\nglobalThis.WebSocket = FakeSocket;\n\nconst monitor = new MonitorFeed({ url: 'ws://192.168.1.20:7457', hz: 15 });\nmonitor.onStatus = (ok) => console.log('connected', ok);\nmonitor.connect();\nsetTimeout(() => {\n  for (let i = 0; i < 5; i++) monitor.frame({ t: i, scene: 'Dawn' });     // five calls in the same instant\n}, 20);","caption":{"en":"The console prints `connecting ws://192.168.1.20:7457?role=stage`, `connected true`, and then only one `sent at t scene`: the other four calls came inside one throttle interval and were dropped.","zh":"主控台印出 `connecting ws://192.168.1.20:7457?role=stage`、`connected true`，然後只有一行 `sent at t scene`：其他四次呼叫都落在同一個節流間隔裡，被丟掉了。"},"run":"module","lang":"js"},{"code":"import { Loop } from '@openav/core';\nimport { MonitorFeed, snapshotOf } from '@openav/monitor';\n\n// a hand-assembled show: timeline, params, signals, stage and loop are yours\nconst monitor = new MonitorFeed({ url: 'ws://192.168.1.20:7457' });\nmonitor.connect();\nconst loop = new Loop((dt) => {\n  timeline.advance(dt);\n  const state = stage.frame(dt, timeline.state());\n  monitor.frame(snapshotOf({ timeline, params, signals, stage, loop }, state));   // throttled for you\n});\nloop.start();","caption":{"en":"With the relay running (`node packages/monitor/server.js`), a phone on the same network that opens `http://<this machine>:7457/` sees the clock, the scene, the frame rate and every signal and param move.","zh":"中繼站開著的時候（`node packages/monitor/server.js`），同一個網路上的手機打開 `http://<這台機器>:7457/`，就會看到時鐘、場景、影格率，以及每個訊號和參數在動。"},"run":"static","lang":"js"}],"related":["snapshotOf","defaultMonitorUrl","createShow","mountConsole"],"keywords":{"en":["monitor","backstage","stage manager","relay","websocket","snapshot","feed","stage feed lost","7457","remote view"],"zh":["監看","後台","舞台監督","中繼","websocket","快照","資料流","stage feed lost","7457","遠端檢視"]},"tbd":[{"en":"The relay itself (`packages/monitor/server.js`) and the backstage page were not run here; the examples use a stand-in socket.","zh":"中繼站本身（`packages/monitor/server.js`）和後台頁面沒有在這裡跑過。範例用的是代替的 socket。"}],"claims":[],"members":[{"name":"connect","member":"method","signature":"","doc":"","line":33},{"name":"frame","member":"method","signature":"snapshot","doc":"Call per frame; throttles itself.","line":49}]},
{"id":"snapshotOf","key":"snapshotOf","name":"snapshotOf","aliases":[],"kind":"function","package":"@openav/monitor","category":"backstage","documented":true,"signature":"snapshotOf({ timeline, params, signals, stage, loop }, state)","import":"import { snapshotOf } from '@openav/monitor';","source":{"file":"packages/monitor/index.js","line":59,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/monitor/index.js#L59"},"summary":{"en":"Builds the usual snapshot of a show from its parts: clock, scene, world, frame rate, resolved params, overrides and every signal.","zh":"從演出的各部分建出一般的快照：時鐘、場景、世界、影格率、解析後的參數、覆寫，以及每個訊號。"},"description":[{"en":"The data that [`MonitorFeed.frame()`](#MonitorFeed) sends and the backstage page draws. Each part is optional and read with a safe fallback, so a hand-assembled show can pass only what it has. The snapshot is plain JSON, so you can also log it, store it for a rehearsal recording or show it in your own page.","zh":"這是 [`MonitorFeed.frame()`](#MonitorFeed) 送出、後台頁面用來畫畫面的資料。每個部分都是選填的、讀取時有安全的預設值，所以手工組裝的演出只要傳它有的部分就行。快照是單純的 JSON，所以你也可以把它記下來、存成排練的錄影，或顯示在自己的頁面上。"}],"params":[{"name":"timeline","type":"Timeline","optional":true,"en":"The [`Timeline`](#Timeline): gives `t`, `playing`, `holding`, `total`, `sceneIndex` and `scene`.","zh":"[`Timeline`](#Timeline)：提供 `t`、`playing`、`holding`、`total`、`sceneIndex` 和 `scene`。"},{"name":"params","type":"Params","optional":true,"en":"The [`Params`](#Params): gives the list of keys that are overridden.","zh":"[`Params`](#Params)：提供被覆寫的參數 key 清單。"},{"name":"signals","type":"Signals","optional":true,"en":"The [`Signals`](#Signals): every signal as `{ n, v, k }` (name, value, kind).","zh":"[`Signals`](#Signals)：每個訊號整理成 `{ n, v, k }`（名字、值、類型）。"},{"name":"stage","type":"Stage","optional":true,"en":"The [`Stage`](#Stage): gives the active world's name.","zh":"[`Stage`](#Stage)：提供目前世界的名字。"},{"name":"loop","type":"Loop","optional":true,"en":"The [`Loop`](#Loop): gives the frame rate.","zh":"[`Loop`](#Loop)：提供影格率。"},{"name":"state","type":"Object","en":"The resolved param values of this frame (what `stage.frame()` returns). `{}` when you pass nothing.","zh":"這個畫面解析後的參數值（`stage.frame()` 的回傳值）。什麼都不傳的話是 `{}`。"}],"returns":{"type":"Object","en":"`{ t, playing, holding, total, sceneIndex, scene, world, fps, state, overrides, signals }`. `scene` is the current scene's title (or its id); `holding` is true while the show waits at a hold; `signals` is `[{ n, v, k }]` with numbers rounded to 3 decimals. A part you leave out gives zeros, empty strings and empty lists.","zh":"`{ t, playing, holding, total, sceneIndex, scene, world, fps, state, overrides, signals }`。`scene` 是目前場景的標題（沒有標題就用 id）。`holding` 在演出停在 hold 上等待的時候為 true。`signals` 是 `[{ n, v, k }]`，數字會四捨五入到小數三位。沒有給的部分，結果是 0、空字串和空清單。"},"notes":[],"examples":[{"code":"import { Signals, Params } from '@openav/core';\nimport { Timeline } from '@openav/timeline';\nimport { snapshotOf } from '@openav/monitor';\n\nconst signals = new Signals();\nconst params = new Params([{ key: 'hue', min: 0, max: 360, def: 200 }]);\nconst timeline = new Timeline({ params, total: 60, scenes: [{ id: 'dawn', t: 0, title: 'Dawn' }, { id: 'noon', t: 30 }] });\nsignals.set('midi/cc/74', 0.4567);\nparams.override('hue', 90);\ntimeline.seek(35);\n\nconst snapshot = snapshotOf({ timeline, params, signals, stage: { activeName: 'garden' }, loop: { fps: 60 } }, params.resolve(timeline.state()));\nconsole.log(snapshot);","caption":{"en":"The console prints the snapshot: `t: 35`, `scene: 'noon'` (a scene without a title shows its id), `world: 'garden'`, `fps: 60`, `state: { hue: 90 }`, `overrides: [ 'hue' ]` and `signals: [ { n: 'midi/cc/74', v: 0.457, k: 'continuous' } ]`.","zh":"主控台印出快照：`t: 35`、`scene: 'noon'`（沒有標題的場景會顯示它的 id）、`world: 'garden'`、`fps: 60`、`state: { hue: 90 }`、`overrides: [ 'hue' ]`，以及 `signals: [ { n: 'midi/cc/74', v: 0.457, k: 'continuous' } ]`。"},"run":"module","lang":"js"}],"related":["MonitorFeed","Timeline","Params","Signals","Stage","Loop"],"keywords":{"en":["snapshot","monitor snapshot","state","backstage data","overrides","signals list","serialize show"],"zh":["快照","監看快照","狀態","後台資料","覆寫","訊號清單","序列化演出"]},"tbd":[],"claims":[]},
{"id":"candidateUrls","key":"candidateUrls","name":"candidateUrls","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"candidateUrls(kind, opts = {})","import":"import { candidateUrls } from '@openav/fab';","source":{"file":"packages/fab/kernel.js","line":10,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/kernel.js#L10"},"summary":{"en":"The list of URLs [`loadKernel()`](#loadKernel) tries for a kernel, in order: your `urls`, then `window.__LAB__.vendor`, then the unpkg default.","zh":"[`loadKernel()`](#loadKernel) 載入核心時依序嘗試的網址清單：你給的 `urls`、`window.__LAB__.vendor`、最後是 unpkg 預設的。"},"description":[{"en":"The order lets a page choose where its kernels come from without touching the code that uses them: pass `urls` for one page, set `window.__LAB__.vendor` for a whole host (the lab does, to serve the kernels itself), and the unpkg default is the last resort. Calling it yourself is useful to see where a page would look.","zh":"這個順序讓頁面可以決定核心從哪裡來，而不用動到使用它們的程式：單一頁面傳 `urls`，整個主機就設 `window.__LAB__.vendor`（實驗室就是這樣做，自己提供核心），unpkg 預設是最後的辦法。自己呼叫它，可以看出一個頁面會去哪些地方找。"}],"params":[{"name":"kind","type":"'smlib' | 'manifold'","en":"Which kernel.","zh":"哪一個核心。"},{"name":"opts","type":"Object","optional":true,"en":"`urls`: `{ smlib, manifold }`, each a URL or a list of URLs.","zh":"`urls`：`{ smlib, manifold }`，每一項是一個網址或一串網址。"}],"returns":{"type":"string[]","en":"The URLs, first to try first, without repeats.","zh":"網址清單，最先要試的排最前面，沒有重複。"},"notes":[],"examples":[{"code":"import { candidateUrls } from '@openav/fab';\n\nwindow.__LAB__ = { vendor: { smlib: 'https://lab.example/vendor/smlib.js' } };   // what a host may set\nconst rows = [\n  'mine first: ' + candidateUrls('smlib', { urls: { smlib: 'https://mine.example/smlib.js' } }).join('  →  '),\n  'manifold: ' + candidateUrls('manifold').join('  →  '),\n];\ndelete window.__LAB__;\ndocument.getElementById('app').innerHTML = rows.join('<br>');","caption":{"en":"Prints three smlib URLs in order (yours, the host's, unpkg) and only the unpkg URL for manifold, which nobody configured.","zh":"依序印出 smlib 的三個網址（你的、主機的、unpkg），以及 manifold 只有 unpkg 的網址，因為沒有人設定它。"},"run":"module","lang":"js"}],"related":["loadKernel","DEFAULT_URLS","Fab"],"keywords":{"en":["kernel urls","candidate urls","vendor","__LAB__","where to load","unpkg","cdn"],"zh":["核心網址","候選網址","vendor","__LAB__","從哪載入","unpkg","cdn"]},"tbd":[],"claims":[]},
{"id":"checkMesh","key":"checkMesh","name":"checkMesh","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"checkMesh(input, opts = {})","import":"import { checkMesh } from '@openav/fab';","source":{"file":"packages/fab/checks.js","line":236,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/checks.js#L236"},"summary":{"en":"Measures a triangle mesh for 3D printing: closed or not, volume, mass, overhang, thinnest wall, fit on the bed, with a plain warning for each problem.","zh":"替三角形網格量 3D 列印要看的數字：封不封閉、體積、重量、懸空、最薄的壁、放不放得進列印床，每個問題有一句白話警告。"},"description":[{"en":"Pure and synchronous, with no DOM. Duplicate vertices are merged first (within 1e-6 of the bounding-box diagonal), because without that every un-indexed mesh (three.js `toNonIndexed()`, an STL, any triangle soup) reads as open. In the report: `triangles` (after merging; a face that collapses is dropped), `bbox` as `{ min, max, size }` in mm, `volume` in mm³ (always positive; a mesh wound inside out is measured the right way round), `watertight` (every edge used by exactly two faces, in opposite directions) with the counts `openEdges`, `nonManifoldEdges` and `inconsistentEdges` behind it, and `grams`: `{ PETG, PLA }`, the weight of the volume as solid plastic (1.27 and 1.24 g/cm³).","zh":"純函式、同步，不碰 DOM。重複的頂點會先合併（距離在包圍盒對角線的 1e-6 以內），因為不這樣做，每個沒有索引的網格（three.js 的 `toNonIndexed()`、STL、任何三角形湯）都會被看成有開口。報告裡有：`triangles`（合併之後的數量，塌掉的面會被丟掉）、`bbox`（`{ min, max, size }`，單位毫米）、`volume`（單位 mm³，一定是正的，繞法反了的網格也會用正確的方向去量）、`watertight`（每條邊剛好被兩個面用到、而且方向相反），背後是 `openEdges`、`nonManifoldEdges`、`inconsistentEdges` 三個計數，還有 `grams`：`{ PETG, PLA }`，把體積當成實心塑膠算出的重量（1.27 和 1.24 g/cm³）。"},{"en":"`overhang` is `{ angle, area, ratio }`: the area (mm²) of faces that look down steeper than `angle` allows, and its share of the whole surface. Faces that rest on the lowest 0.2 mm (the bed) do not count. `minWall` is `{ value, min, median, samples }` or `null`: rays are shot into the part from sampled faces, and `value` is the 5th percentile of the thicknesses found, which is how thin most of the wall is. `min` is the single thinnest sample, which on a part with holes drilled at an angle is a knife edge and says little about the wall, so it has its own milder note.","zh":"`overhang` 是 `{ angle, area, ratio }`：朝下的程度超過 `angle` 容許範圍的面的面積（mm²），以及它佔整個表面的比例。貼在最低 0.2 毫米內（也就是貼在床上）的面不算。`minWall` 是 `{ value, min, median, samples }` 或 `null`：從取樣的面朝零件裡面射線，`value` 是量到的厚度的第 5 百分位，也就是大部分壁有多薄。`min` 是單一最薄的樣本，在有斜著鑽孔的零件上，它會是刀口，對壁厚說不了什麼，所以另外有比較溫和的提示。"},{"en":"`warnings` is a list of one sentence each, in Chinese: not watertight (with the three counts), overhang above `overhangWarn`, a wall thinner than `minWall`, a knife edge thinner than half of it, and a size beyond `bed`. An empty list means nothing to report. `fab.check(x)` is `checkMesh(meshOf(x), opts)`.","zh":"`warnings` 是一串警告，每項一句話：網格不水密（附三個計數）、懸空超過 `overhangWarn`、壁比 `minWall` 薄、刀口比它的一半還薄、尺寸超出 `bed`。清單是空的就表示沒有要回報的事。`fab.check(x)` 就是 `checkMesh(meshOf(x), opts)`。"}],"params":[{"name":"input","type":"TriMesh","en":"A mesh `{ vertices, triangles, units? }`: `vertices` a `Float32Array` or `Float64Array` of x, y, z, x, y, z …, `triangles` a `Uint32Array` with three vertex numbers per face, counter-clockwise seen from outside, Z up, in millimetres. Use [`meshOf()`](#meshOf) to get one from something else.","zh":"網格 `{ vertices, triangles, units? }`：`vertices` 是 x, y, z, x, y, z … 的 `Float32Array` 或 `Float64Array`，`triangles` 是每個面三個頂點編號的 `Uint32Array`，從外面看逆時針，Z 向上，單位毫米。要從別的東西得到網格，用 [`meshOf()`](#meshOf)。"},{"name":"opts","type":"Object","optional":true,"en":"`angle` (45): the overhang angle in degrees. `minWall` (1.2): the thinnest wall allowed, in mm. `bed` (`[320, 320, 325]`): the size of the print bed in mm. `wallSamples` (2000): how many faces to measure the wall from; fewer is faster. `overhangWarn` (0.15): the share of overhanging area that earns a warning. `weld` (`true`): merge duplicate vertices first; `false` skips it.","zh":"`angle`（45）：懸空角度，單位度。`minWall`（1.2）：允許的最薄壁，單位毫米。`bed`（`[320, 320, 325]`）：列印床的大小，單位毫米。`wallSamples`（2000）：量壁厚時取樣的面數，少一點比較快。`overhangWarn`（0.15）：懸空面積佔多少比例才警告。`weld`（`true`）：先把重複的頂點合併，`false` 就跳過。"}],"returns":{"type":"Object","en":"`{ units, triangles, bbox, volume, watertight, openEdges, nonManifoldEdges, inconsistentEdges, overhang, minWall, grams, warnings }`. See the description.","zh":"`{ units, triangles, bbox, volume, watertight, openEdges, nonManifoldEdges, inconsistentEdges, overhang, minWall, grams, warnings }`。見說明。"},"notes":[{"en":"`grams` is the weight of a solid part: it does not know about infill or wall count, so a sliced print weighs less.","zh":"`grams` 是實心零件的重量：它不知道填充率或外殼層數，所以切片列印出來的會比較輕。"}],"examples":[{"code":"import { checkMesh } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n  units: 'mm',\n};\nconst r = checkMesh(tetra);\ndocument.getElementById('app').innerHTML = [\n  'closed: ' + r.watertight + ' (open edges ' + r.openEdges + ')',\n  'size: ' + r.bbox.size.join(' x ') + ' mm',\n  'volume: ' + r.volume.toFixed(2) + ' mm³, PLA ' + r.grams.PLA + ' g',\n  'thinnest wall (5th percentile): ' + r.minWall.value.toFixed(2) + ' mm',\n].join('<br>');","caption":{"en":"Prints closed: true, size 10 x 10 x 10 mm, volume 166.67 mm³, PLA 0.207 g and a wall of 3.33 mm.","zh":"印出 closed: true、尺寸 10 x 10 x 10 mm、體積 166.67 mm³、PLA 0.207 公克，壁厚 3.33 mm。"},"run":"module","lang":"js"},{"code":"import { checkMesh } from '@openav/fab';\n\n// the same tetrahedron with its bottom face left out: a hole in the mesh\nconst open = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 1, 3, 0, 3, 2, 1, 2, 3]),\n  units: 'mm',\n};\nconst r = checkMesh(open, { bed: [5, 5, 5] });   // and a printer bed that is too small\ndocument.getElementById('app').innerHTML = [\n  'watertight: ' + r.watertight + ', open edges: ' + r.openEdges,\n  'warnings: ' + r.warnings.length,\n].join('<br>');","caption":{"en":"Prints watertight: false with 3 open edges, and 2 warnings: the open mesh and the 10 mm part that does not fit a 5 mm bed.","zh":"印出 watertight: false、3 條開放邊，以及 2 個警告：網格有開口，以及 10 毫米的零件放不進 5 毫米的床。"},"run":"module","lang":"js"},{"code":"import { checkMesh } from '@openav/fab';\n\n// the same tetrahedron, but with every triangle listed again for each corner: 12 vertices, none shared (a \"triangle soup\")\nconst tetra = [0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10];\nconst faces = [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]];\nconst soup = {\n  vertices: Float64Array.from(faces.flatMap((f) => f.flatMap((i) => tetra.slice(i * 3, i * 3 + 3)))),\n  triangles: Uint32Array.from({ length: 12 }, (_, i) => i),\n  units: 'mm',\n};\ndocument.getElementById('app').innerHTML = [\n  'merged first (default): ' + checkMesh(soup).watertight,\n  'weld: false: ' + checkMesh(soup, { weld: false }).watertight,\n].join('<br>');","caption":{"en":"Prints true with the default merge and false with weld: false: a soup of loose triangles looks open until its vertices are merged.","zh":"預設會合併，印出 true，用 weld: false 印出 false：一堆散的三角形，在頂點合併之前看起來是開的。"},"run":"module","lang":"js"}],"related":["Fab","weldMesh","meshOf","toStlBytes"],"keywords":{"en":["check mesh","watertight","manifold","volume","weight","grams","overhang","wall thickness","print bed","3d print","report","mesh quality"],"zh":["檢查網格","水密","流形","體積","重量","公克","懸空","壁厚","列印床","3d 列印","報告","網格品質"]},"tbd":[],"claims":[]},
{"id":"DEFAULT_URLS","key":"DEFAULT_URLS","name":"DEFAULT_URLS","aliases":[],"kind":"constant","package":"@openav/fab","category":"fab","documented":true,"signature":"DEFAULT_URLS = object, 2 keys","import":"import { DEFAULT_URLS } from '@openav/fab';","source":{"file":"packages/fab/kernel.js","line":5,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/kernel.js#L5"},"summary":{"en":"Where the CAD kernels are loaded from when nothing else says so: smlibwasm 0.3.0 and manifold-3d 3.5.4, both from unpkg.","zh":"沒有別的指定時，CAD 核心從哪裡載入：smlibwasm 0.3.0 和 manifold-3d 3.5.4，都是從 unpkg。"},"description":[{"en":"The last entry of [`candidateUrls()`](#candidateUrls). Versions are pinned, so the kernels a page gets do not change under it. To use another copy, pass `urls` to [`Fab`](#Fab) or [`loadKernel()`](#loadKernel) rather than changing this object.","zh":"[`candidateUrls()`](#candidateUrls) 的最後一項。版本是釘死的，所以頁面拿到的核心不會在它底下悄悄改變。要用別的副本，請把 `urls` 傳給 [`Fab`](#Fab) 或 [`loadKernel()`](#loadKernel)，不要去改這個物件。"}],"value":{"type":"Object","en":"`{ smlib: 'https://unpkg.com/@classcad/smlibwasm@0.3.0/dist/index.js', manifold: 'https://unpkg.com/manifold-3d@3.5.4/manifold.js' }`.","zh":"兩個網址：`{ smlib: 'https://unpkg.com/@classcad/smlibwasm@0.3.0/dist/index.js', manifold: 'https://unpkg.com/manifold-3d@3.5.4/manifold.js' }`。"},"params":[],"notes":[],"examples":[{"code":"import { DEFAULT_URLS } from '@openav/fab';\n\ndocument.getElementById('app').innerHTML = Object.entries(DEFAULT_URLS).map(([kind, url]) => kind + ': ' + url).join('<br>');","caption":{"en":"Prints the two unpkg addresses, with the versions in them.","zh":"印出兩個 unpkg 位址，位址裡有版本號。"},"run":"module","lang":"js"}],"related":["candidateUrls","loadKernel","Fab"],"keywords":{"en":["default urls","unpkg","smlibwasm version","manifold version","cdn","kernel location"],"zh":["預設網址","unpkg","smlibwasm 版本","manifold 版本","cdn","核心位置"]},"tbd":[],"claims":[]},
{"id":"exportAs","key":"exportAs","name":"exportAs","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"async exportAs(x, fmt, opts = {}, kernel = null)","import":"import { exportAs } from '@openav/fab';","source":{"file":"packages/fab/export.js","line":95,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/export.js#L95"},"summary":{"en":"Exports a shape as STL, OBJ, GLB, STEP or DXF and resolves to `{ bytes, mime, ext, report }`: the same as `fab.export()`, without the `Fab` class.","zh":"把形狀輸出成 STL、OBJ、GLB、STEP 或 DXF，結果是 `{ bytes, mime, ext, report }`：跟 `fab.export()` 一樣，只是不用 `Fab` 類別。"},"description":[{"en":"`stl`, `obj` and `glb` come from the mesh: [`toStlBytes()`](#toStlBytes), [`toObjText()`](#toObjText), [`toGlbBytes()`](#toGlbBytes). `step` and `dxf` come straight from the smlibwasm kernel (`toStep`, `toDxf`) and are refused for anything that is not a kernel shape, because a mesh has no curved surfaces to write; an error says so. A kernel solid's `glb` uses the kernel's `toGlb` when it has one. The report is made on the way, so a file and its check always agree.","zh":"`stl`、`obj`、`glb` 由網格產生：[`toStlBytes()`](#toStlBytes)、[`toObjText()`](#toObjText)、[`toGlbBytes()`](#toGlbBytes)。`step` 和 `dxf` 直接來自 smlibwasm 核心（`toStep`、`toDxf`），不是核心形狀的東西會被拒絕，因為網格沒有曲面可以寫，錯誤訊息會這樣說。核心實體的 `glb` 在核心有 `toGlb` 的時候會用它。報告是順便做出來的，所以檔案和它的檢查結果一定一致。"}],"params":[{"name":"x","type":"Object","en":"The shape: anything [`meshOf()`](#meshOf) accepts, and for `step` a smlibwasm `Part`, for `dxf` a smlibwasm `Shape` or `Sketch`.","zh":"形狀：[`meshOf()`](#meshOf) 接受的任何東西。`step` 要用 smlibwasm 的 `Part`，`dxf` 要用 smlibwasm 的 `Shape` 或 `Sketch`。"},{"name":"fmt","type":"String","en":"`'stl'`, `'obj'`, `'step'`, `'dxf'` or `'glb'`, in any case. `'stp'` means step and `'gltf'` means glb. Another word throws an `Error` that lists the formats.","zh":"`'stl'`、`'obj'`、`'step'`、`'dxf'` 或 `'glb'`，大小寫都可以。`'stp'` 等於 step，`'gltf'` 等於 glb。其他的字會丟出列出可用格式的 `Error`。"},{"name":"opts","type":"Object","optional":true,"en":"The options of [`meshOf()`](#meshOf) (`scale`, `tessellator`) and of [`checkMesh()`](#checkMesh) (`angle`, `minWall`, `bed` …), and `name`, the object name of an OBJ file.","zh":"[`meshOf()`](#meshOf) 的選項（`scale`、`tessellator`）、[`checkMesh()`](#checkMesh) 的選項（`angle`、`minWall`、`bed` …），以及 `name`，也就是 OBJ 檔裡的物件名稱。"},{"name":"kernel","type":"{ smlib }","optional":true,"en":"`{ smlib: () => Promise<module> }`, where to get the smlibwasm module for `step`, `dxf`, and the `glb` of a `Part`. `fab.export()` passes its own.","zh":"`{ smlib: () => Promise<module> }`，`step`、`dxf` 和 `Part` 的 `glb` 要從哪裡拿 smlibwasm 模組。`fab.export()` 會傳它自己的。"}],"returns":{"type":"Promise<Object>","en":"`bytes` is a `Uint8Array`, `mime` is from [`MIME`](#MIME), `ext` is the extension without the dot (`'step'` even if you asked for `'stp'`), and `report` is the [`checkMesh()`](#checkMesh) report of the mesh (for `step` and `dxf` it can be `null` when the shape cannot be meshed, such as a 2D sketch).","zh":"`bytes` 是 `Uint8Array`，`mime` 來自 [`MIME`](#MIME)，`ext` 是不含點的副檔名（就算你要的是 `'stp'` 也是 `'step'`），`report` 是這個網格的 [`checkMesh()`](#checkMesh) 報告（`step` 和 `dxf` 在形狀切不成網格，例如 2D 草圖時，可能是 `null`）。"},"notes":[],"examples":[{"code":"import { exportAs } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst rows = [];\nfor (const fmt of ['STL', 'obj', 'gltf']) {\n  const out = await exportAs(tetra, fmt);\n  rows.push(fmt + ' → ' + out.ext + ', ' + out.mime + ', ' + out.bytes.length + ' bytes, watertight ' + out.report.watertight);\n}\ndocument.getElementById('app').innerHTML = rows.join('<br>');","caption":{"en":"Prints three exports: STL as stl (284 bytes), obj, and the alias gltf as glb, each with the report's verdict.","zh":"印出三個輸出：STL 變成 stl（284 位元組）、obj，以及別名 gltf 變成 glb，每個都附上報告的結論。"},"run":"module","lang":"js"},{"code":"import { exportAs } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nlet step, xyz;\ntry { await exportAs(tetra, 'step'); } catch (e) { step = e.message; }\ntry { await exportAs(tetra, 'xyz'); } catch (e) { xyz = e.message; }\ndocument.getElementById('app').innerHTML = [step, xyz].join('<br>');","caption":{"en":"Two refusals, each with a message in Chinese: a mesh cannot become STEP (it has no curved surfaces), and \"xyz\" is not a format.","zh":"兩個拒絕，各有一則中文訊息：網格不能變成 STEP（它沒有曲面），「xyz」也不是格式。"},"run":"module","lang":"js"},{"code":"import { exportAs } from '@openav/fab';\n\n// a stand-in for the smlibwasm kernel: STEP only works for a kernel solid, and the kernel writes it\nconst solid = { faces: () => [], bbox: () => ({}), tessellate: () => ({ positions: [0, 0, 0, 10, 0, 0, 0, 10, 0], indices: [0, 1, 2] }) };\nconst kernel = { smlib: async () => ({ toStep: () => 'ISO-10303-21;' }) };\nconst out = await exportAs(solid, 'stp', {}, kernel);\ndocument.getElementById('app').textContent = out.ext + ' (' + out.mime + '): ' + new TextDecoder().decode(out.bytes);","caption":{"en":"Prints \"step (model/step): ISO-10303-21;\": the alias stp was understood and the bytes came from the kernel's toStep.","zh":"印出 `step (model/step): ISO-10303-21;`，表示別名 stp 被認得了，位元組來自核心的 toStep。"},"run":"module","lang":"js"}],"related":["Fab","MIME","toStlBytes","toObjText","toGlbBytes","checkMesh","loadKernel"],"keywords":{"en":["export","export as","stl","obj","glb","step","dxf","format","bytes","mime","cad export"],"zh":["輸出","匯出","stl","obj","glb","step","dxf","格式","位元組","mime","cad 輸出"]},"tbd":[],"claims":[]},
{"id":"Fab","key":"Fab","name":"Fab","aliases":[],"kind":"class","package":"@openav/fab","category":"fab","documented":true,"signature":"new Fab(opts = {})","import":"import { Fab } from '@openav/fab';","source":{"file":"packages/fab/index.js","line":16,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/index.js#L16"},"summary":{"en":"Checks a shape for 3D printing and exports it as STL, OBJ, GLB, STEP or DXF; takes a mesh, a three.js object or a smlibwasm solid.","zh":"檢查一個形狀能不能 3D 列印，並輸出成 STL、OBJ、GLB、STEP 或 DXF，吃網格、three.js 物件或 smlibwasm 的實體。"},"description":[{"en":"Make one `Fab` and hand it whatever you made: `fab.check(x)` returns a manufacturability report (is it closed, how much material, how much overhang, how thin is the thinnest wall, how many grams), and `await fab.export(x, 'stl')` returns the file as bytes. `x` can be a plain triangle mesh `{ vertices, triangles }`, a three.js `Mesh`, `Object3D` or `BufferGeometry` (recognised by its shape, three.js is never imported), or a smlibwasm `Part` or `Shape`. [`meshOf()`](#meshOf) says exactly what is accepted.","zh":"做一個 `Fab`，把你做出來的東西交給它：`fab.check(x)` 回傳製造性報告（封不封閉、用了多少材料、有多少懸空、最薄的壁多薄、幾公克），`await fab.export(x, 'stl')` 以位元組回傳檔案。`x` 可以是單純的三角形網格 `{ vertices, triangles }`、three.js 的 `Mesh`、`Object3D` 或 `BufferGeometry`（靠形狀辨認，從不 import three.js），或 smlibwasm 的 `Part` 或 `Shape`。[`meshOf()`](#meshOf) 說明了到底接受什麼。"},{"en":"Formats: `stl`, `obj` and `glb` work for any input; `step` only for a smlibwasm `Part`, and `dxf` only for a smlibwasm `Shape` or `Sketch`, because a mesh has no curved-surface information. The CAD kernels are not bundled: the first time one is needed, `fab.smlib()` or `fab.manifold()` imports it from a URL (unpkg by default) and remembers it.","zh":"格式：`stl`、`obj`、`glb` 任何輸入都行。`step` 只能從 smlibwasm 的 `Part` 輸出，`dxf` 只能從 smlibwasm 的 `Shape` 或 `Sketch` 輸出，因為網格沒有曲面資訊。CAD 核心沒有打包在裡面：第一次需要時，`fab.smlib()` 或 `fab.manifold()` 才從網址（預設是 unpkg）import 進來，並且記住。"},{"en":"`fab.save()` writes the file through `fab.saver` if you set one (the lab's dev server does), and otherwise as a browser download. Zero runtime dependencies: nothing is fetched until you ask for a kernel.","zh":"`fab.save()` 如果你設了 `fab.saver`（實驗室的開發伺服器就會設）就透過它寫檔，沒有的話就當成瀏覽器下載。沒有任何執行期相依套件：在你要求核心之前，不會抓任何東西。"}],"params":[{"name":"opts","type":"Object","en":"`{ scale, urls, importer, download, context }`, all optional. `scale` is millimetres per unit for a three.js object (`10` means one three.js unit is 10 mm). `urls` is `{ smlib, manifold }`, a URL or a list of them, to load the CAD kernels from. `importer(url)` replaces the dynamic `import()` that loads a kernel. `download(bytes, info)` replaces the browser download that `save()` falls back to. `context()` returns `{ id, params }` for `save()` to put in `info`.","zh":"`{ scale, urls, importer, download, context }`，每一項都可省略。`scale` 是 three.js 物件每個單位多少毫米（`10` 表示 three.js 的一個單位是 10 毫米）。`urls` 是 `{ smlib, manifold }`，一個網址或一串網址，用來載入 CAD 核心。`importer(url)` 取代載入核心用的動態 `import()`。`download(bytes, info)` 取代 `save()` 退而求其次用的瀏覽器下載。`context()` 回傳 `{ id, params }`，讓 `save()` 放進 `info`。"}],"needs":{"en":"Nothing for meshes and three.js objects. A smlibwasm `Part` has to be tessellated by the kernel: call `await fab.smlib()` once before the synchronous `check()`, or use `checkAsync()`, `meshOfAsync()` and `export()`, which load it for you. Loading needs a network, or `urls` / `window.__LAB__.vendor` pointing somewhere that has the kernel.","zh":"網格和 three.js 物件不需要任何前置動作。smlibwasm 的 `Part` 要由核心來切成網格：在同步的 `check()` 之前先 `await fab.smlib()` 一次，或改用會自動載入的 `checkAsync()`、`meshOfAsync()` 和 `export()`。載入需要網路，或是 `urls`、`window.__LAB__.vendor` 指向有這個核心的地方。"},"notes":[{"en":"Public fields: `opts` and `saver` (set it to `async (bytes, info) => ({ file, … })`; it starts as `null`). The file name for `save()` is `name`, else the `id` from `context()`, else `fab`.","zh":"公開的欄位有 `opts` 和 `saver`（設成 `async (bytes, info) => ({ file, … })`，一開始是 `null`）。`save()` 的檔名是 `name`，沒有的話是 `context()` 給的 `id`，再沒有就是 `fab`。"}],"examples":[{"code":"import { Fab } from '@openav/fab';\n\n// a tetrahedron: three 10 mm edges meeting at the origin (x, y, z), counter-clockwise faces seen from outside\nconst mesh = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n  units: 'mm',\n};\nconst report = new Fab().check(mesh);\ndocument.getElementById('app').innerHTML = [\n  'watertight: ' + report.watertight,\n  'volume: ' + report.volume.toFixed(1) + ' mm³',\n  'PETG: ' + report.grams.PETG + ' g',\n  'overhang: ' + Math.round(report.overhang.ratio * 100) + '%',\n  'warnings: ' + report.warnings.length,\n].join('<br>');","caption":{"en":"Prints watertight: true, volume 166.7 mm³ (10 × 10 × 10 ÷ 6), 0.212 g of PETG, no overhang and no warnings.","zh":"印出 watertight: true、體積 166.7 mm³（10 × 10 × 10 ÷ 6）、PETG 0.212 公克、沒有懸空、沒有警告。"},"run":"module","lang":"js"},{"code":"import { Fab } from '@openav/fab';\n\nconst mesh = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n  units: 'mm',\n};\nconst fab = new Fab();\nconst stl = await fab.export(mesh, 'stl');\nconst obj = await fab.export(mesh, 'obj', { name: 'tetra' });\ndocument.getElementById('app').innerHTML = [\n  stl.ext + ' (' + stl.mime + '): ' + stl.bytes.length + ' bytes',\n  obj.ext + ' (' + obj.mime + '): ' + new TextDecoder().decode(obj.bytes).split('\\n').slice(0, 4).join(' | '),\n].join('<br>');","caption":{"en":"Prints the STL as 284 bytes (84 + 50 × 4) and the first lines of the OBJ: a comment, \"o tetra\" and the first two vertices.","zh":"印出 STL 是 284 位元組（84 + 50 × 4），以及 OBJ 的前幾行：註解、「o tetra」和前兩個頂點。"},"run":"module","lang":"js"},{"code":"import { Fab } from '@openav/fab';\n\nconst mesh = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n  units: 'mm',\n};\nconst seen = [];\nconst fab = new Fab({ download: (bytes, info) => seen.push('download ' + info.name + '.' + info.ext + ' (' + bytes.length + ' bytes)') });\n\nconst first = await fab.save(mesh, 'stl', { name: 'tetra' });          // no saver: a download\nfab.saver = async (bytes, info) => ({ file: info.name + '.' + info.ext, n: 1 });   // e.g. the lab's /api/fab\nconst second = await fab.save(mesh, 'obj', { name: 'tetra' });         // the saver wins\nseen.push('first: ' + JSON.stringify({ downloaded: first.downloaded, file: first.file }));\nseen.push('second: ' + JSON.stringify({ file: second.file, n: second.n }));\ndocument.getElementById('app').innerHTML = seen.join('<br>');","caption":{"en":"Prints the download of tetra.stl (the stand-in `download` replaces the browser's), then the saver's answer for tetra.obj.","zh":"印出 tetra.stl 的下載（替身 `download` 取代了瀏覽器的下載），然後是 saver 對 tetra.obj 的回答。"},"run":"module","lang":"js"}],"related":["checkMesh","exportAs","meshOf","loadKernel","toStlBytes"],"keywords":{"en":["fab","3d print","manufacturability","export stl","export obj","export glb","step","dxf","three.js","smlibwasm","manifold","check mesh","save file"],"zh":["數位製造","3d 列印","可製造性","輸出 stl","輸出 obj","輸出 glb","step","dxf","three.js","smlibwasm","manifold","檢查網格","存檔"]},"tbd":[{"en":"The STEP and DXF export, and the tessellation of a real smlibwasm `Part`, were not run for this entry: they need the real kernel from unpkg. The examples use a stand-in kernel to show the routing.","zh":"這一條沒有實際跑過 STEP 和 DXF 輸出，以及真正的 smlibwasm `Part` 切網格：它們需要 unpkg 上的真正核心。範例用一個替身核心來示範流程。"}],"claims":[],"members":[{"name":"smlib","member":"method","signature":"","doc":"","line":30},{"name":"manifold","member":"method","signature":"","doc":"","line":31},{"name":"meshOf","member":"method","signature":"x, opts = {}","doc":"","line":35},{"name":"meshOfAsync","member":"method","signature":"x, opts = {}","doc":"","line":36},{"name":"check","member":"method","signature":"x, opts = {}","doc":"","line":37},{"name":"checkAsync","member":"method","signature":"x, opts = {}","doc":"","line":38},{"name":"export","member":"method","signature":"x, fmt, opts = {}","doc":"","line":40},{"name":"save","member":"method","signature":"x, fmt, { name, params, note, ...rest } = {}","doc":"","line":44}]},
{"id":"fromMeshLike","key":"fromMeshLike","name":"fromMeshLike","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"fromMeshLike(m, scale = 1)","import":"import { fromMeshLike } from '@openav/fab';","source":{"file":"packages/fab/mesh.js","line":22,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/mesh.js#L22"},"summary":{"en":"Turns a mesh-like object (positions and indices under any of the usual names) into a triangle mesh, multiplying the positions by `scale`.","zh":"把像網格的物件（位置和索引用常見的任何名稱）變成三角形網格，位置會乘上 `scale`。"},"description":[{"en":"This is what [`meshOf()`](#meshOf) uses for a kernel's tessellation result, whichever way the kernel spells it. It throws an `Error` (in Chinese) when `m` is empty or has no positions. It does not check that the faces point outward or that the mesh is closed: that is [`checkMesh()`](#checkMesh).","zh":"[`meshOf()`](#meshOf) 處理核心的切網格結果時用的就是它，不管核心怎麼拼那些名稱。`m` 是空的、或沒有位置的話，會丟出 `Error`（中文訊息）。它不檢查面是不是朝外、網格有沒有封閉：那是 [`checkMesh()`](#checkMesh) 的工作。"}],"params":[{"name":"m","type":"Object","en":"An object with the points under `positions`, `vertices`, `points` or `position`, and the faces under `indices`, `triangles`, `faces`, `index` or `triVerts`. Points may be flat numbers, nested arrays (`[[x, y, z], …]`) or objects (`{ x, y, z }`). Without faces, every three points are one triangle. `m.units` is kept (default `'mm'`).","zh":"一個物件，點放在 `positions`、`vertices`、`points` 或 `position` 底下，面放在 `indices`、`triangles`、`faces`、`index` 或 `triVerts` 底下。點可以是攤平的數字、巢狀陣列（`[[x, y, z], …]`）或物件（`{ x, y, z }`）。沒有面的話，每三個點是一個三角形。`m.units` 會保留（預設 `'mm'`）。"},{"name":"scale","type":"Number","optional":true,"en":"Multiplies every position, for example millimetres per unit.","zh":"乘在每個位置上，例如每個單位多少毫米。"}],"returns":{"type":"TriMesh","en":"`{ vertices: Float64Array, triangles: Uint32Array, units }`.","zh":"回傳 `{ vertices: Float64Array, triangles: Uint32Array, units }`。"},"notes":[],"examples":[{"code":"import { fromMeshLike } from '@openav/fab';\n\n// no faces given: every three points are one triangle; positions in centimetres, scaled to millimetres\nconst mesh = fromMeshLike({ positions: [0, 0, 0, 1, 0, 0, 0, 1, 0] }, 10);\ndocument.getElementById('app').textContent =\n  'vertices: ' + [...mesh.vertices].join(', ') + ' | triangles: ' + [...mesh.triangles].join(', ') + ' | ' + mesh.units;","caption":{"en":"Prints \"vertices: 0, 0, 0, 10, 0, 0, 0, 10, 0 | triangles: 0, 1, 2 | mm\".","zh":"印出「vertices: 0, 0, 0, 10, 0, 0, 0, 10, 0 | triangles: 0, 1, 2 | mm」。"},"run":"module","lang":"js"}],"related":["meshOf","meshOfAsync","checkMesh"],"keywords":{"en":["mesh like","positions indices","convert mesh","flatten","scale","kernel mesh"],"zh":["像網格","位置 索引","轉換網格","攤平","縮放","核心網格"]},"tbd":[],"claims":[]},
{"id":"fromStlBytes","key":"fromStlBytes","name":"fromStlBytes","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"fromStlBytes(bytes)","import":"import { fromStlBytes } from '@openav/fab';","source":{"file":"packages/fab/export.js","line":33,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/export.js#L33"},"summary":{"en":"Reads a binary STL file back into a triangle mesh: every face has its own three vertices, in 32-bit floats.","zh":"把二進位 STL 檔讀回三角形網格：每個面有自己的三個頂點，32 位元浮點數。"},"description":[{"en":"It reads what [`toStlBytes()`](#toStlBytes) writes, or any binary STL: the normals and the two attribute bytes are skipped, and the result is a triangle soup. Give it to [`checkMesh()`](#checkMesh), which merges the vertices itself, or to [`weldMesh()`](#weldMesh). The text (ASCII) form of STL, which starts with `solid`, is not supported and would read as garbage.","zh":"它讀的是 [`toStlBytes()`](#toStlBytes) 寫出的東西，或任何二進位 STL：法線和兩個屬性位元組會跳過，結果是一堆三角形湯。把它交給會自己合併頂點的 [`checkMesh()`](#checkMesh)，或交給 [`weldMesh()`](#weldMesh)。以 `solid` 開頭的文字（ASCII）格式 STL 不支援，讀進來會是亂碼。"}],"params":[{"name":"bytes","type":"Uint8Array","en":"The contents of a binary STL file. The face count is read from the header; the file's length is not checked against it.","zh":"二進位 STL 檔的內容。面數從檔頭讀，不會拿檔案長度去對照。"}],"returns":{"type":"TriMesh","en":"`{ vertices: Float32Array (9 per face), triangles: Uint32Array (0, 1, 2, 3, 4, 5 …), units: 'mm' }`.","zh":"`{ vertices: Float32Array（每個面 9 個）, triangles: Uint32Array（0, 1, 2, 3, 4, 5 …）, units: 'mm' }`。"},"notes":[],"examples":[{"code":"import { toStlBytes, fromStlBytes, checkMesh } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst back = fromStlBytes(toStlBytes(tetra));   // a round trip through the file format\ndocument.getElementById('app').innerHTML = [\n  'faces: ' + back.triangles.length / 3 + ', vertices: ' + back.vertices.length / 3,\n  'still closed after the check merges them: ' + checkMesh(back).watertight,\n].join('<br>');","caption":{"en":"Prints 4 faces and 12 vertices (no sharing), and \"true\": the check welds them and finds the shape closed.","zh":"印出 4 個面和 12 個頂點（沒有共用），以及「true」：檢查時把它們合併後，發現形狀是封閉的。"},"run":"module","lang":"js"}],"related":["toStlBytes","weldMesh","checkMesh"],"keywords":{"en":["read stl","load stl","parse stl","binary stl","triangle soup","import stl"],"zh":["讀取 stl","載入 stl","解析 stl","二進位 stl","三角形湯","匯入 stl"]},"tbd":[],"claims":[]},
{"id":"loadKernel","key":"loadKernel","name":"loadKernel","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"async loadKernel(kind, opts = {})","import":"import { loadKernel } from '@openav/fab';","source":{"file":"packages/fab/kernel.js","line":17,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/kernel.js#L17"},"summary":{"en":"Imports a CAD kernel (smlibwasm or manifold-3d) from the first URL that works, and resolves to the ready module.","zh":"從第一個能用的網址 import CAD 核心（smlibwasm 或 manifold-3d），結果是可以直接用的模組。"},"description":[{"en":"The kernels are not bundled, because open-audiovisual has no dependencies and no build step: the first time a page needs one, it is imported from a URL. The order is [`candidateUrls()`](#candidateUrls): `opts.urls[kind]`, then `window.__LAB__.vendor[kind]`, then unpkg ([`DEFAULT_URLS`](#DEFAULT_URLS)). If every URL fails it rejects with an `Error` (in Chinese) that names each URL and why it failed. It does not remember anything: `fab.smlib()` and `fab.manifold()` keep the promise for you, so call those unless you want to manage it yourself.","zh":"核心沒有打包，因為 open-audiovisual 沒有相依套件、也沒有建置步驟：頁面第一次需要的時候，才從網址 import 進來。順序是 [`candidateUrls()`](#candidateUrls)：`opts.urls[kind]`，然後 `window.__LAB__.vendor[kind]`，最後是 unpkg（[`DEFAULT_URLS`](#DEFAULT_URLS)）。每個網址都失敗的話，它會用 `Error`（中文）reject，訊息列出每個網址和失敗原因。它自己什麼都不記：`fab.smlib()` 和 `fab.manifold()` 會替你留著 promise，除非你想自己管理，不然呼叫那兩個。"}],"params":[{"name":"kind","type":"'smlib' | 'manifold'","en":"Which kernel: `'smlib'` for smlibwasm (solid modelling, STEP and DXF) or `'manifold'` for manifold-3d (fast mesh booleans).","zh":"哪一個核心：`'smlib'` 是 smlibwasm（實體建模、STEP 和 DXF），`'manifold'` 是 manifold-3d（快速的網格布林運算）。"},{"name":"opts","type":"Object","optional":true,"en":"`urls`: `{ smlib, manifold }`, a URL or a list of URLs for each kernel. `importer`: `async (url) => module`, to use instead of the dynamic `import()`.","zh":"`urls`：`{ smlib, manifold }`，每個核心一個網址或一串網址。`importer`：`async (url) => module`，用來取代動態的 `import()`。"}],"returns":{"type":"Promise<Object>","en":"The smlibwasm module as it came, or the manifold-3d module with its WebAssembly started (`setup()` called).","zh":"smlibwasm 模組原樣，或已經把 WebAssembly 啟動（呼叫過 `setup()`）的 manifold-3d 模組。"},"needs":{"en":"A network, or `urls` pointing at a place that serves the kernel (the lab uses `window.__LAB__.vendor`), or an `importer` that returns one.","zh":"要有網路，或是 `urls` 指向有提供核心的地方（實驗室用的是 `window.__LAB__.vendor`），或是一個會回傳核心的 `importer`。"},"notes":[],"examples":[{"code":"import { loadKernel } from '@openav/fab';\n\nconst tried = [];\n// a stand-in importer so nothing is fetched: the first URL fails, the second one works\nconst importer = async (url) => {\n  tried.push(url);\n  if (url.includes('mirror-down')) throw new Error('503');\n  return { name: 'manifold stand-in', default: async () => ({ setup() { tried.push('setup() called'); } }) };\n};\nconst wasm = await loadKernel('manifold', { urls: { manifold: ['https://mirror-down.example/m.js', 'https://mirror.example/m.js'] }, importer });\ndocument.getElementById('app').innerHTML = tried.join('<br>') + '<br>result has setup: ' + (typeof wasm.setup);","caption":{"en":"Prints the two URLs in the order they were tried, then \"setup() called\": manifold's WebAssembly is started for you.","zh":"依嘗試的順序印出兩個網址，然後是「setup() called」：manifold 的 WebAssembly 會替你啟動。"},"run":"module","lang":"js"},{"code":"import { loadKernel } from '@openav/fab';\n\nlet message;\ntry {\n  await loadKernel('smlib', { urls: { smlib: 'https://nowhere.example/a.js' }, importer: async () => { throw new Error('no network'); } });\n} catch (e) { message = e.message; }\ndocument.getElementById('app').textContent = message;","caption":{"en":"When every URL fails the error names each one, yours first and then the unpkg default, with the reason (in a Chinese sentence).","zh":"每個網址都失敗的時候，錯誤訊息會一個一個列出來，你的在前、unpkg 預設的在後，並附上原因（用中文寫的句子）。"},"run":"module","lang":"js"}],"related":["candidateUrls","DEFAULT_URLS","Fab","meshOfAsync"],"keywords":{"en":["load kernel","smlibwasm","manifold-3d","wasm","import","cad kernel","lazy load","unpkg","cdn"],"zh":["載入核心","smlibwasm","manifold-3d","wasm","import","cad 核心","延遲載入","unpkg","cdn"]},"tbd":[],"claims":[]},
{"id":"meshOf","key":"meshOf","name":"meshOf","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"meshOf(x, opts = {})","import":"import { meshOf } from '@openav/fab';","source":{"file":"packages/fab/mesh.js","line":83,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/mesh.js#L83"},"summary":{"en":"Turns whatever you made into a triangle mesh `{ vertices, triangles, units }`: a mesh, a three.js object, a mesh-like object or a smlibwasm shape.","zh":"把你做的東西變成三角形網格 `{ vertices, triangles, units }`：網格、three.js 物件、像網格的物件，或 smlibwasm 的形狀。"},"description":[{"en":"Everything is recognised by its shape and nothing is imported, so three.js and smlibwasm are never loaded for you. A three.js object has its `matrixWorld` applied (a mirrored object keeps its faces facing outward), indexed or not, and Y up is turned into Z up as `(x, y, z) → (x, −z, y)`, then everything is multiplied by `scale`. A missing input, or something it cannot read, throws an `Error` whose message (in Chinese) says what to do.","zh":"每種東西都靠形狀辨認，什麼都不 import，所以 three.js 和 smlibwasm 絕對不會被替你載入。three.js 物件會套用它的 `matrixWorld`（鏡射的物件仍會讓面朝外），有沒有索引都行，Y 向上會換成 Z 向上，換法是 `(x, y, z) → (x, −z, y)`，然後全部乘上 `scale`。沒有給輸入、或它讀不懂的東西，會丟出 `Error`，訊息（中文）會說明該怎麼辦。"},{"en":"It is synchronous, so it cannot wait for a kernel: a smlibwasm solid without `opts.tessellator` throws a message that tells you to load the kernel first, and a `tessellate()` that returns a promise throws too. Use [`meshOfAsync()`](#meshOfAsync) for those.","zh":"它是同步的，所以不能等核心：沒有 `opts.tessellator` 的 smlibwasm 實體會丟出一則叫你先載入核心的訊息，`tessellate()` 回傳 promise 也會丟出錯誤。這些情況請用 [`meshOfAsync()`](#meshOfAsync)。"}],"params":[{"name":"x","type":"Object","en":"The thing to convert. Accepted, tried in this order: a mesh `{ vertices, triangles }` (returned as it is); a three.js `Mesh`, `Object3D` (every visible mesh below it) or `BufferGeometry`; anything with a `tessellate()` method; an object with `positions` / `vertices` / `points` and, optionally, `indices` / `triangles` / `faces` / `index`; a smlibwasm solid or shape (given `opts.tessellator`).","zh":"要轉換的東西。接受的類型，依這個順序比對：網格 `{ vertices, triangles }`（原樣傳回），three.js 的 `Mesh`、`Object3D`（底下每個看得見的網格）或 `BufferGeometry`，任何有 `tessellate()` 方法的東西，有 `positions`／`vertices`／`points`，以及可有可無的 `indices`／`triangles`／`faces`／`index` 的物件，smlibwasm 的實體或形狀（要給 `opts.tessellator`）。"},{"name":"opts","type":"Object","optional":true,"en":"`scale`: millimetres per unit for a three.js or mesh-like input (default 1). A mesh `{ vertices, triangles }` is never scaled. `tessellator(shape)`: a function that turns a smlibwasm shape into a mesh (the `Fab` class passes the kernel's once it is loaded). `tessellate`: an argument passed on to the shape's own `tessellate()`.","zh":"`scale`：three.js 或像網格的輸入，每個單位多少毫米（預設 1）。`{ vertices, triangles }` 的網格不會被縮放。`tessellator(shape)`：把 smlibwasm 形狀變成網格的函式（`Fab` 類別在核心載入後會傳入核心的那個）。`tessellate`：傳給形狀自己的 `tessellate()` 的參數。"}],"returns":{"type":"TriMesh","en":"`{ vertices, triangles, units }`: counter-clockwise faces seen from outside, Z up, millimetres.","zh":"`{ vertices, triangles, units }`：從外面看逆時針的面，Z 向上，單位毫米。"},"notes":[],"examples":[{"code":"import { meshOf } from '@openav/fab';\n\n// what a three.js Mesh looks like to fab: a triangle in the XY plane, no index, an identity matrixWorld\nconst threeLike = {\n  isMesh: true,\n  geometry: { attributes: { position: { array: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), itemSize: 3, count: 3 } }, index: null },\n  matrixWorld: { elements: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] },\n};\nconst m = meshOf(threeLike, { scale: 10 });   // one three.js unit is 10 mm\ndocument.getElementById('app').textContent = 'vertices: ' + [...m.vertices].join(', ') + ' (' + m.units + ')';","caption":{"en":"Prints \"vertices: 0, 0, 0, 10, 0, 0, 0, 0, 10 (mm)\": scaled by 10, and three.js's up (y = 1) became fab's up (z = 10).","zh":"印出「vertices: 0, 0, 0, 10, 0, 0, 0, 0, 10 (mm)」：乘上 10，three.js 的向上（y = 1）變成 fab 的向上（z = 10）。"},"run":"module","lang":"js"},{"code":"import { meshOf } from '@openav/fab';\n\n// a mesh-like object: nested points and faces, as some libraries hand them out\nconst m = meshOf({ points: [[0, 0, 0], [20, 0, 0], [0, 20, 0]], faces: [[0, 1, 2]] });\n// the vertices come back flat, as numbers: x, y, z, x, y, z …\ndocument.getElementById('app').textContent = [...m.vertices].join(' ') + ' | faces: ' + [...m.triangles].join(' ');","caption":{"en":"Prints the nine numbers of the three corners and the face \"0 1 2\": nested arrays are flattened.","zh":"印出三個角的九個數字，以及面「0 1 2」：巢狀陣列會被攤平。"},"run":"module","lang":"js"}],"related":["meshOfAsync","fromMeshLike","checkMesh","Fab"],"keywords":{"en":["mesh of","convert to mesh","three.js","BufferGeometry","Object3D","tessellate","triangle mesh","y up z up","scale"],"zh":["轉成網格","three.js","BufferGeometry","Object3D","切網格","三角形網格","y 向上 z 向上","縮放"]},"tbd":[],"claims":[]},
{"id":"meshOfAsync","key":"meshOfAsync","name":"meshOfAsync","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"async meshOfAsync(x, opts = {})","import":"import { meshOfAsync } from '@openav/fab';","source":{"file":"packages/fab/mesh.js","line":102,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/mesh.js#L102"},"summary":{"en":"Like `meshOf()`, but it waits for a `tessellate()` that returns a promise, and can load the smlibwasm kernel to tessellate a solid.","zh":"跟 `meshOf()` 一樣，但它會等回傳 promise 的 `tessellate()`，也能載入 smlibwasm 核心來切實體。"},"description":[{"en":"Use it where the conversion may have to wait: a shape whose `tessellate()` is asynchronous, or a smlibwasm `Part` when the kernel is not loaded yet. With `opts.kernel.smlib` it awaits the module and uses its `tessellate(part)`; an error says so if the module has none. Everything else is passed on to [`meshOf()`](#meshOf). `fab.meshOfAsync()` fills in `kernel` for you.","zh":"轉換可能得等的時候用它：`tessellate()` 是非同步的形狀，或核心還沒載入的 smlibwasm `Part`。有給 `opts.kernel.smlib` 的話，它會等模組載好，用它的 `tessellate(part)`。模組沒有這個函式的話會丟出說明的錯誤。其他東西都交給 [`meshOf()`](#meshOf)。`fab.meshOfAsync()` 會替你填好 `kernel`。"}],"params":[{"name":"x","type":"Object","en":"The thing to convert; see [`meshOf()`](#meshOf).","zh":"要轉換的東西，見 [`meshOf()`](#meshOf)。"},{"name":"opts","type":"Object","optional":true,"en":"The options of [`meshOf()`](#meshOf), and `kernel`: `{ smlib: () => Promise<module> }`, where to get the smlibwasm module from when a solid has to be tessellated and no `tessellator` was given.","zh":"[`meshOf()`](#meshOf) 的選項，另外有 `kernel`：`{ smlib: () => Promise<module> }`，遇到實體要切、又沒給 `tessellator` 時，從哪裡拿 smlibwasm 模組。"}],"returns":{"type":"Promise<TriMesh>","en":"The same mesh [`meshOf()`](#meshOf) would give.","zh":"跟 [`meshOf()`](#meshOf) 會給的一樣的網格。"},"notes":[],"examples":[{"code":"import { meshOfAsync } from '@openav/fab';\n\n// a shape whose tessellate() answers later, the way a kernel running in a worker would\nconst shape = { tessellate: async () => ({ positions: [0, 0, 0, 5, 0, 0, 0, 5, 0], indices: [0, 1, 2] }) };\nconst mesh = await meshOfAsync(shape);\ndocument.getElementById('app').textContent = mesh.vertices.length / 3 + ' vertices, ' + mesh.triangles.length / 3 + ' face';","caption":{"en":"Prints \"3 vertices, 1 face\": the promise was awaited and the result turned into a mesh.","zh":"印出「3 vertices, 1 face」：promise 被等到了，結果也轉成了網格。"},"run":"module","lang":"js"},{"code":"import { meshOfAsync } from '@openav/fab';\n\n// a stand-in for the smlibwasm module: a solid has no tessellate() of its own, the module's function cuts it\nconst part = { faces: () => [], bbox: () => ({}) };\nconst kernel = { smlib: async () => ({ tessellate: (p) => ({ vertices: [0, 0, 0, 5, 0, 0, 0, 5, 0], triangles: [0, 1, 2] }) }) };\nconst mesh = await meshOfAsync(part, { kernel });\ndocument.getElementById('app').textContent = 'tessellated by the kernel: ' + mesh.triangles.length / 3 + ' face';","caption":{"en":"Prints \"tessellated by the kernel: 1 face\": the module was fetched through `kernel.smlib()` and its `tessellate` did the work.","zh":"印出「tessellated by the kernel: 1 face」：透過 `kernel.smlib()` 取得模組，由它的 `tessellate` 完成。"},"run":"module","lang":"js"}],"related":["meshOf","Fab","loadKernel"],"keywords":{"en":["async mesh","tessellate","smlibwasm","promise","kernel","load on demand","worker"],"zh":["非同步網格","切網格","smlibwasm","promise","核心","需要時載入","worker"]},"tbd":[],"claims":[]},
{"id":"MIME","key":"MIME","name":"MIME","aliases":[],"kind":"constant","package":"@openav/fab","category":"fab","documented":true,"signature":"MIME = object, 5 keys","import":"import { MIME } from '@openav/fab';","source":{"file":"packages/fab/export.js","line":5,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/export.js#L5"},"summary":{"en":"The media type of each export format: `MIME.stl` is `'model/stl'`, and `obj`, `step`, `dxf` and `glb` the same way.","zh":"每種輸出格式的媒體類型：`MIME.stl` 是 `'model/stl'`，`obj`、`step`、`dxf`、`glb` 也一樣。"},"description":[{"en":"For the `type` of a `Blob` or the `Content-Type` of an upload when you handle the bytes yourself: `new Blob([bytes], { type: MIME.stl })`. [`exportAs()`](#exportAs) already puts the right one in its result.","zh":"自己處理位元組的時候，拿來當 `Blob` 的 `type` 或上傳的 `Content-Type`：`new Blob([bytes], { type: MIME.stl })`。[`exportAs()`](#exportAs) 的結果裡已經附上正確的那一個。"}],"value":{"type":"Object","en":"`{ stl: 'model/stl', obj: 'model/obj', step: 'model/step', dxf: 'image/vnd.dxf', glb: 'model/gltf-binary' }`. Its keys are the formats [`exportAs()`](#exportAs) accepts.","zh":"`{ stl: 'model/stl', obj: 'model/obj', step: 'model/step', dxf: 'image/vnd.dxf', glb: 'model/gltf-binary' }`。它的鍵就是 [`exportAs()`](#exportAs) 接受的格式。"},"params":[],"notes":[],"examples":[{"code":"import { MIME, toStlBytes } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst blob = new Blob([toStlBytes(tetra)], { type: MIME.stl });\ndocument.getElementById('app').textContent = Object.keys(MIME).join(', ') + ' | blob: ' + blob.type + ', ' + blob.size + ' bytes';","caption":{"en":"Prints the five formats and a blob of type model/stl, 284 bytes.","zh":"印出五種格式，以及一個類型是 model/stl、284 位元組的 blob。"},"run":"module","lang":"js"}],"related":["exportAs","toStlBytes","Fab"],"keywords":{"en":["mime","media type","content type","blob type","model/stl","gltf-binary"],"zh":["mime","媒體類型","content type","blob 類型","model/stl","gltf-binary"]},"tbd":[],"claims":[]},
{"id":"toGlbBytes","key":"toGlbBytes","name":"toGlbBytes","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"toGlbBytes(mesh)","import":"import { toGlbBytes } from '@openav/fab';","source":{"file":"packages/fab/export.js","line":54,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/export.js#L54"},"summary":{"en":"Writes a triangle mesh as a minimal binary glTF (`.glb`): one mesh with welded positions and indices, in millimetres.","zh":"把三角形網格寫成最精簡的二進位 glTF（`.glb`）：一個網格，位置已合併並帶索引，單位毫米。"},"description":[{"en":"Vertices are welded first with [`weldMesh()`](#weldMesh). The file has `POSITION` and indices only: no normals, no materials, no colours, so a viewer shades it flat or works the normals out itself. The positions stay in millimetres and Z up, and `asset.extras` says `{ units: 'mm', up: 'Z' }`. glTF itself assumes Y up, so a viewer that does not read the extras shows the part lying on its side.","zh":"頂點會先用 [`weldMesh()`](#weldMesh) 合併。檔案裡只有 `POSITION` 和索引：沒有法線、材質、顏色，所以檢視器會用平面著色，或自己算法線。位置維持毫米、Z 向上，`asset.extras` 寫著 `{ units: 'mm', up: 'Z' }`。glTF 本身假設 Y 向上，所以不讀 extras 的檢視器會把零件顯示成躺在側面。"},{"en":"`await fab.export(part, 'glb')` of a smlibwasm `Part` uses the kernel's own `toGlb` when it has one, and this function otherwise.","zh":"smlibwasm 的 `Part` 用 `await fab.export(part, 'glb')` 的話，核心有自己的 `toGlb` 就用它，沒有才用這個函式。"}],"params":[{"name":"mesh","type":"TriMesh","en":"A mesh `{ vertices, triangles }`, in millimetres.","zh":"網格 `{ vertices, triangles }`，單位毫米。"}],"returns":{"type":"Uint8Array","en":"A glTF 2.0 binary: the `glTF` header, one JSON chunk, one binary chunk with 32-bit float positions and 32-bit indices. The length is a multiple of 4.","zh":"glTF 2.0 二進位檔：`glTF` 檔頭、一個 JSON 區塊、一個放 32 位元浮點數位置和 32 位元索引的二進位區塊。長度是 4 的倍數。"},"notes":[],"examples":[{"code":"import { toGlbBytes } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst glb = toGlbBytes(tetra);\nconst dv = new DataView(glb.buffer);\nconst json = JSON.parse(new TextDecoder().decode(glb.subarray(20, 20 + dv.getUint32(12, true))));\ndocument.getElementById('app').innerHTML = [\n  'magic: ' + new TextDecoder().decode(glb.subarray(0, 4)) + ', ' + glb.length + ' bytes',\n  'vertices: ' + json.accessors[0].count + ', indices: ' + json.accessors[1].count,\n  'bounds: ' + json.accessors[0].max.join(' x '),\n].join('<br>');","caption":{"en":"Prints the magic \"glTF\" and the size, 4 vertices with 12 indices, and the bounds 10 x 10 x 10.","zh":"印出魔術字串「glTF」和大小、4 個頂點配 12 個索引，以及範圍 10 x 10 x 10。"},"run":"module","lang":"js"}],"related":["toStlBytes","toObjText","exportAs","weldMesh"],"keywords":{"en":["glb","gltf","export glb","binary gltf","3d viewer","web 3d","write glb"],"zh":["glb","gltf","輸出 glb","二進位 gltf","3d 檢視器","網頁 3d","寫出 glb"]},"tbd":[],"claims":[]},
{"id":"toObjText","key":"toObjText","name":"toObjText","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"toObjText(mesh, name = 'fab')","import":"import { toObjText } from '@openav/fab';","source":{"file":"packages/fab/export.js","line":45,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/export.js#L45"},"summary":{"en":"Writes a triangle mesh as OBJ text: welded vertices (`v x y z`), then faces (`f a b c`), one object, in millimetres.","zh":"把三角形網格寫成 OBJ 文字：先合併頂點（`v x y z`），再寫面（`f a b c`），一個物件，單位毫米。"},"description":[{"en":"Vertices are welded first with [`weldMesh()`](#weldMesh), so a triangle soup comes out with shared vertices and a small file. There are no normals, no texture coordinates and no materials. `await fab.export(x, 'obj', { name })` is the same thing with the conversions done for you.","zh":"頂點會先用 [`weldMesh()`](#weldMesh) 合併，所以三角形湯寫出來有共用的頂點，檔案也小。沒有法線、沒有貼圖座標、沒有材質。`await fab.export(x, 'obj', { name })` 是同一件事，並且替你做了轉換。"}],"params":[{"name":"mesh","type":"TriMesh","en":"A mesh `{ vertices, triangles }`, in millimetres.","zh":"網格 `{ vertices, triangles }`，單位毫米。"},{"name":"name","type":"String","optional":true,"en":"The object name on the `o` line. Spaces become underscores.","zh":"`o` 那一行的物件名稱。空格會變成底線。"}],"returns":{"type":"String","en":"The text: a comment line, `o <name>`, one `v` line per welded vertex (6 decimals at most) and one `f` line per face, vertex numbers counting from 1.","zh":"文字內容：一行註解、`o <name>`、每個合併後的頂點一行 `v`（最多 6 位小數）、每個面一行 `f`，頂點編號從 1 開始。"},"notes":[],"examples":[{"code":"import { toObjText } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\ndocument.getElementById('app').innerHTML = toObjText(tetra, 'my part').trim().split('\\n').join('<br>');","caption":{"en":"Prints the comment, \"o my_part\", four v lines and four f lines: \"f 1 3 2\" is the first face.","zh":"印出註解、「o my_part」、四行 v 和四行 f：「f 1 3 2」是第一個面。"},"run":"module","lang":"js"}],"related":["toStlBytes","toGlbBytes","exportAs","weldMesh"],"keywords":{"en":["obj","wavefront","export obj","text mesh","write obj"],"zh":["obj","wavefront","輸出 obj","文字網格","寫出 obj"]},"tbd":[],"claims":[]},
{"id":"toStlBytes","key":"toStlBytes","name":"toStlBytes","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"toStlBytes(mesh)","import":"import { toStlBytes } from '@openav/fab';","source":{"file":"packages/fab/export.js","line":9,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/export.js#L9"},"summary":{"en":"Writes a triangle mesh as a binary STL file: 84 + 50 bytes per face, with a normal worked out for every face.","zh":"把三角形網格寫成二進位 STL 檔：84 位元組加上每個面 50 位元組，每個面的法線都會算出來。"},"description":[{"en":"Every face becomes its own 50 bytes: STL has no shared vertices. The normal comes from the order of the corners (right-hand rule), so counter-clockwise faces seen from outside give outward normals. Positions are written as they are, as float32: nothing is converted to millimetres, so give it a mesh that is already in mm. `await fab.export(x, 'stl')` is the same thing with the conversions and the check done for you.","zh":"每個面各自佔 50 位元組：STL 沒有共用頂點。法線是從角的順序（右手定則）算出來的，所以從外面看逆時針的面，法線就朝外。位置原樣寫成 float32，不會換算成毫米，所以請給它已經是毫米的網格。`await fab.export(x, 'stl')` 是同一件事，並且替你做了轉換和檢查。"}],"params":[{"name":"mesh","type":"TriMesh","en":"A mesh `{ vertices, triangles }`, in millimetres.","zh":"網格 `{ vertices, triangles }`，單位毫米。"}],"returns":{"type":"Uint8Array","en":"The file: an 80-byte header (the text `open-audiovisual fab STL (mm)`), the face count as a little-endian 32-bit integer, then per face the normal and three corners as 32-bit floats and two zero bytes.","zh":"檔案內容：80 位元組的檔頭（文字 `open-audiovisual fab STL (mm)`）、面數（小端序的 32 位元整數），然後每個面是法線和三個角（32 位元浮點數）加上兩個零位元組。"},"notes":[],"examples":[{"code":"import { toStlBytes } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst bytes = toStlBytes(tetra);\nconst faces = new DataView(bytes.buffer).getUint32(80, true);\ndocument.getElementById('app').textContent =\n  bytes.length + ' bytes = 84 + 50 × ' + faces + ' faces, header \"' + new TextDecoder().decode(bytes.subarray(0, 29)) + '\"';","caption":{"en":"Prints 284 bytes = 84 + 50 × 4 faces, and the header text.","zh":"印出 284 位元組 = 84 + 50 × 4 個面，以及檔頭文字。"},"run":"module","lang":"js"},{"code":"import { toStlBytes } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst bytes = toStlBytes(tetra);\nconst link = document.createElement('a');\nlink.href = URL.createObjectURL(new Blob([bytes], { type: 'model/stl' }));\nlink.download = 'tetra.stl';\nlink.textContent = 'download tetra.stl (' + bytes.length + ' bytes)';\ndocument.getElementById('app').append(link);","caption":{"en":"A link that downloads the file: open it in a slicer or a 3D viewer to see a small pyramid with a right-angle corner.","zh":"一個下載檔案的連結：用切片軟體或 3D 檢視器打開，會看到一個有直角的小金字塔。"},"run":"module","lang":"js"}],"related":["fromStlBytes","exportAs","toObjText","toGlbBytes","Fab"],"keywords":{"en":["stl","binary stl","export stl","3d print file","slicer","write stl","bytes"],"zh":["stl","二進位 stl","輸出 stl","3d 列印檔","切片軟體","寫出 stl","位元組"]},"tbd":[],"claims":[]},
{"id":"weldMesh","key":"weldMesh","name":"weldMesh","aliases":[],"kind":"function","package":"@openav/fab","category":"fab","documented":true,"signature":"weldMesh(mesh, tolerance)","import":"import { weldMesh } from '@openav/fab';","source":{"file":"packages/fab/checks.js","line":22,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/fab/checks.js#L22"},"summary":{"en":"Merges vertices that sit closer together than a tolerance and drops the faces that collapse; returns a new mesh with shared vertices.","zh":"把靠得比容差還近的頂點合併，並丟掉塌掉的面，回傳一個頂點有共用的新網格。"},"description":[{"en":"A triangle soup, where every face has three vertices of its own (what an STL file or three.js `toNonIndexed()` gives), has no neighbours as far as topology goes: every edge looks open. Welding gives the faces shared vertices, so [`checkMesh()`](#checkMesh) can see that they join. [`checkMesh()`](#checkMesh), [`toObjText()`](#toObjText) and [`toGlbBytes()`](#toGlbBytes) call it themselves; [`toStlBytes()`](#toStlBytes) does not need it.","zh":"三角形湯，也就是每個面都有自己的三個頂點（STL 檔或 three.js 的 `toNonIndexed()` 給的就是這樣），在拓撲上沒有鄰居：每條邊都看起來是開的。合併頂點讓面共用頂點，[`checkMesh()`](#checkMesh) 才看得出它們是接在一起的。[`checkMesh()`](#checkMesh)、[`toObjText()`](#toObjText) 和 [`toGlbBytes()`](#toGlbBytes) 會自己呼叫它，[`toStlBytes()`](#toStlBytes) 用不到。"}],"params":[{"name":"mesh","type":"TriMesh","en":"A mesh `{ vertices, triangles }`. It is not changed.","zh":"網格 `{ vertices, triangles }`。不會被改動。"},{"name":"tolerance","type":"Number","optional":true,"en":"Vertices closer than this (in each of x, y and z) become one, in the mesh's units. Left out: 1e-6 times the diagonal of the bounding box.","zh":"距離（x、y、z 各自）小於這個值的頂點會變成一個，單位跟網格一樣。不給的話是包圍盒對角線的 1e-6 倍。"}],"returns":{"type":"TriMesh","en":"`{ vertices: Float64Array, triangles: Uint32Array, units }` with each position once, and no face that has two corners on the same vertex.","zh":"`{ vertices: Float64Array, triangles: Uint32Array, units }`，每個位置只出現一次，也沒有任何一個面有兩個角落在同一個頂點上。"},"notes":[],"examples":[{"code":"import { weldMesh, fromStlBytes, toStlBytes } from '@openav/fab';\n\nconst tetra = {\n  vertices: new Float64Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 10]),\n  triangles: new Uint32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3]),\n};\nconst soup = fromStlBytes(toStlBytes(tetra));   // reading an STL gives every face its own vertices\nconst welded = weldMesh(soup);\ndocument.getElementById('app').textContent =\n  'soup: ' + soup.vertices.length / 3 + ' vertices, welded: ' + welded.vertices.length / 3 + ' vertices, ' + welded.triangles.length / 3 + ' faces';","caption":{"en":"Prints \"soup: 12 vertices, welded: 4 vertices, 4 faces\".","zh":"印出「soup: 12 vertices, welded: 4 vertices, 4 faces」。"},"run":"module","lang":"js"}],"related":["checkMesh","fromStlBytes","toObjText","toGlbBytes"],"keywords":{"en":["weld","merge vertices","triangle soup","dedupe","shared vertices","tolerance","stl"],"zh":["合併頂點","焊接","三角形湯","去重複","共用頂點","容差","stl"]},"tbd":[],"claims":[]},
{"id":"list_controllers","key":"list_controllers","name":"list_controllers","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"list_controllers","source":{"file":"packages/mcp/server.js","line":77,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L77"},"summary":{"en":"Lists every MIDI controller profile that ships with the framework: id, name, maker, kind, control mix, how many controls are unverified, and its page URL.","zh":"列出框架內建的每一份 MIDI 控制器設定檔：id、名稱、廠牌、種類、控制元件的組成、還有幾個沒驗證、以及它的頁面網址。"},"description":[{"en":"No input: send `\"arguments\": {}`. It reads the profiles in `packages/midi/profiles/`, the same ones the controller pages and `<oav-controller>` use. Use it to find the `id` for [`read_profile`](#read_profile), or to check that a controller is already covered before writing a new profile with [`scaffold_profile`](#scaffold_profile).","zh":"不需要輸入：送 `\"arguments\": {}`。它讀的是 `packages/midi/profiles/` 裡的設定檔，跟控制器頁面和 `<oav-controller>` 用的是同一批。用它找 [`read_profile`](#read_profile) 要的 `id`，或在用 [`scaffold_profile`](#scaffold_profile) 寫新設定檔之前，先確認這台控制器是不是已經有了。"}],"params":[],"returns":{"type":"Object","en":"`{ count, controllers }` as JSON text. Each controller is `{ id, short, name, maker, kind, controls, unverified, url }`: `id` such as `arturia-minilab3`, `short` the signal namespace (`midi/<short>/…`), `kind` `'device'` or `'generic'`, `controls` the mix as `{ knob: 8, pad: 8, keys: 25 }` (a keyboard counts one per key), `unverified` how many of those controls still carry `\"verified\": false`, `url` like `https://openaudiovisual.com/controllers/<id>/`.","zh":"`{ count, controllers }`，格式是 JSON 文字。每個控制器是 `{ id, short, name, maker, kind, controls, unverified, url }`：`id` 像 `arturia-minilab3`，`short` 是訊號的命名空間（`midi/<short>/…`），`kind` 是 `'device'` 或 `'generic'`，`controls` 是組成，像 `{ knob: 8, pad: 8, keys: 25 }`（鍵盤每個鍵算一個），`unverified` 是其中還帶著 `\"verified\": false` 的控制元件數，`url` 像 `https://openaudiovisual.com/controllers/<id>/`。"},"notes":[],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 5,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"list_controllers\",\n    \"arguments\": {}\n  }\n}","caption":{"en":"The reply lists every shipped profile, starting with arturia-minilab3 (8 knobs, 4 faders, 8 pads, 25 keys …), each with its page URL.","zh":"回應列出每一份內建的設定檔，從 arturia-minilab3（8 個旋鈕、4 支推桿、8 個打擊墊、25 個琴鍵 …）開始，每個都附上頁面網址。"},"run":"static","lang":"json"}],"related":["read_profile","validate_profile","scaffold_profile"],"keywords":{"en":["list controllers","midi controllers","profiles","minilab","launchpad","devices","mcp tool","controller list"],"zh":["列出控制器","midi 控制器","設定檔","minilab","launchpad","裝置","mcp 工具","控制器清單"]},"tbd":[],"claims":[]},
{"id":"list_examples","key":"list_examples","name":"list_examples","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"list_examples","source":{"file":"packages/mcp/server.js","line":55,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L55"},"summary":{"en":"Lists the example works of this checkout: for each one its folder, its page title and the first comment line of its `main.js`.","zh":"列出這份檔案庫裡的範例作品：每個範例的資料夾、頁面標題，以及它的 `main.js` 第一行註解。"},"description":[{"en":"The first call an agent makes in an unfamiliar checkout: what works already exist, so it can pick a donor for [`scaffold_world`](#scaffold_world). It takes no input: send `\"arguments\": {}`.","zh":"代理進到陌生的檔案庫時的第一個呼叫：已經有哪些作品，這樣它才能替 [`scaffold_world`](#scaffold_world) 挑一個範本。它不需要任何輸入：送 `\"arguments\": {}`。"}],"params":[],"returns":{"type":"Object[]","en":"`[{ dir, title, summary }]`, one per folder in `examples/`, as JSON text. `dir` is like `examples/01-hello-particles`, `title` is the `<title>` of its `index.html`, `summary` is the first line of its `main.js` that starts with `//` (the slashes taken off). A file that is missing gives an empty string.","zh":"`[{ dir, title, summary }]`，`examples/` 底下每個資料夾一項，格式是 JSON 文字。`dir` 像 `examples/01-hello-particles`，`title` 是它的 `index.html` 的 `<title>`，`summary` 是它的 `main.js` 第一行以 `//` 開頭的內容（去掉斜線）。缺檔就給空字串。"},"needs":{"en":"The server running: `node packages/mcp/server.js`, which Claude Code starts from the `.mcp.json` of the repository (server name `openav`). Other clients need the same `command` and `args` in their MCP settings. An MCP client sends `initialize` once before the first call. Over stdio every message is one line of JSON: the request bodies on this page are spread over several lines to be read, and a client writes them on one line for you.","zh":"伺服器要在跑：`node packages/mcp/server.js`，Claude Code 會依儲存庫的 `.mcp.json` 啟動它（伺服器名稱 `openav`）。其他用戶端要在自己的 MCP 設定裡寫同樣的 `command` 和 `args`。MCP 用戶端在第一次呼叫之前會先送一次 `initialize`。透過 stdio 時每則訊息是一行 JSON：這一頁的請求內容為了好讀而分成好幾行，用戶端會替你把它們寫成一行。"},"notes":[],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"list_examples\",\n    \"arguments\": {}\n  }\n}","caption":{"en":"Send this to the server over stdio: the reply carries one text item, a JSON list of the examples with their folders, titles and one-line summaries.","zh":"把這個送給伺服器（透過 stdio）：回應裡有一個文字項目，是範例清單的 JSON，附上資料夾、標題和一行摘要。"},"run":"static","lang":"json"}],"related":["scaffold_world","read_doc"],"keywords":{"en":["list examples","examples","works","mcp tool","agent","what exists","donor"],"zh":["列出範例","範例","作品","mcp 工具","代理","有哪些","範本"]},"tbd":[],"claims":[]},
{"id":"read_doc","key":"read_doc","name":"read_doc","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"read_doc","source":{"file":"packages/mcp/server.js","line":60,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L60"},"summary":{"en":"Returns the text of one framework document: the agent briefing, a handbook chapter or the README, chosen by name.","zh":"回傳一份框架文件的文字：代理的簡介、手冊的某一章，或 README，用名稱選。"},"description":[{"en":"Input: `doc` (required), one of `architecture`, `writing-a-world`, `signals`, `show-control`, `score`, `roadmap`, `agents`, `readme`, `introduction`, `getting-started`, `concepts`, `inputs`, `mapping`, `controllers`, `remote`, `packages`, `agents-guide`, `troubleshooting`, `lab-integration`. The tool's own description tells an agent to start with `agents` (the conventions and the two unbreakable rules), then `writing-a-world`.","zh":"輸入：`doc`（必填），下面其中之一：`architecture`、`writing-a-world`、`signals`、`show-control`、`score`、`roadmap`、`agents`、`readme`、`introduction`、`getting-started`、`concepts`、`inputs`、`mapping`、`controllers`、`remote`、`packages`、`agents-guide`、`troubleshooting`、`lab-integration`。工具自己的說明叫代理先讀 `agents`（慣例和兩條不能破的規則），再讀 `writing-a-world`。"},{"en":"Most names are `docs/<name>.md`. Two are not, and they are easy to mix up: `agents` is `AGENTS.md` at the root of the repository (the briefing), `agents-guide` is `docs/agents.md` (the handbook chapter), and `readme` is `README.md`.","zh":"大部分名稱就是 `docs/<name>.md`。有兩個不是，而且很容易搞混：`agents` 是儲存庫根目錄的 `AGENTS.md`（簡介），`agents-guide` 是 `docs/agents.md`（手冊的章節），`readme` 則是 `README.md`。"}],"params":[],"returns":{"type":"String","en":"The Markdown of the file, as it is on disk, in one text item: not JSON. The whole file comes back, which can be tens of kilobytes.","zh":"檔案原樣的 Markdown，放在一個文字項目裡，不是 JSON。整份檔案都會回來，可能有好幾十 KB。"},"notes":[{"en":"A name that is not in the list, or no `doc` at all, is an error with a plain Node message (`The \"path\" argument must be of type string. Received undefined`), not a list of the valid names. A client that follows the input schema cannot send one: the schema has the names as an `enum`.","zh":"不在清單裡的名稱、或根本沒給 `doc`，會得到一個 Node 的原始錯誤訊息（`The \"path\" argument must be of type string. Received undefined`），而不是合法名稱的清單。照著輸入 schema 的用戶端不會送出這種值：schema 裡的名稱是 `enum`。"}],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 2,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"read_doc\",\n    \"arguments\": {\n      \"doc\": \"agents\"\n    }\n  }\n}","caption":{"en":"Asks for AGENTS.md: the reply is its Markdown text, which begins \"# AGENTS.md — for AI coding agents working in this repo\".","zh":"要 AGENTS.md：回應就是它的 Markdown 文字，開頭是「# AGENTS.md — for AI coding agents working in this repo」。"},"run":"static","lang":"json"}],"related":["list_examples","scaffold_world"],"keywords":{"en":["read doc","documentation","handbook","AGENTS.md","readme","architecture","writing a world","mcp tool","agent briefing"],"zh":["讀文件","文件","手冊","AGENTS.md","readme","架構","寫一個世界","mcp 工具","代理簡介"]},"tbd":[],"claims":[]},
{"id":"read_profile","key":"read_profile","name":"read_profile","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"read_profile","source":{"file":"packages/mcp/server.js","line":82,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L82"},"summary":{"en":"Reads one controller profile in full, plus a readable MIDI map: for each control its type, the exact message, the signal name and whether the number is verified.","zh":"完整讀出一份控制器設定檔，另外附上好讀的 MIDI 對照表：每個控制元件的類型、確切的訊息、訊號名稱，以及這個號碼有沒有驗證過。"},"description":[{"en":"Input: `id` (required), a profile id or its `short`, for example `arturia-minilab3` or `minilab3`. Where a control says `verified: false`, its number is a placeholder or a guess that no source has confirmed yet. An unknown id is an error that lists every id it knows. [`list_controllers`](#list_controllers) shows what is there.","zh":"輸入：`id`（必填），設定檔的 id 或它的 `short`，例如 `arturia-minilab3` 或 `minilab3`。控制元件寫著 `verified: false` 的地方，它的號碼是還沒有任何來源證實的預留值或猜測。不認得的 id 會得到一個列出它所有已知 id 的錯誤。[`list_controllers`](#list_controllers) 可以看有哪些。"}],"params":[],"returns":{"type":"Object","en":"`{ id, url, unverified, profile, mapping }` as JSON text. `profile` is the JSON as shipped. `mapping` has one row per control: `{ control, type, message, signal, verified }`, where `message` reads like `CC 74, ch 1`, `note 36, ch 10` or `notes 48–72 (25 keys), ch 1`, and `signal` is `midi/<short>/<id>` (for a keyboard `midi/<short>/<id>/on · /off · n<note>`); a row may also carry `led` and `note`.","zh":"`{ id, url, unverified, profile, mapping }`，格式是 JSON 文字。`profile` 是原樣的 JSON。`mapping` 每個控制元件一列：`{ control, type, message, signal, verified }`，`message` 讀起來像 `CC 74, ch 1`、`note 36, ch 10` 或 `notes 48–72 (25 keys), ch 1`，`signal` 是 `midi/<short>/<id>`（鍵盤是 `midi/<short>/<id>/on · /off · n<note>`），有的列還會帶 `led` 和 `note`。"},"notes":[],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 6,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"read_profile\",\n    \"arguments\": {\n      \"id\": \"minilab3\"\n    }\n  }\n}","caption":{"en":"Asks for the MiniLab 3 by its short name: the reply holds its full profile and a mapping whose first knob reads \"CC …, ch 1\" with the signal midi/minilab3/knob1.","zh":"用簡稱要 MiniLab 3：回應裡有完整的設定檔，還有一份對照表，其中第一個旋鈕寫成「CC …, ch 1」、訊號是 midi/minilab3/knob1。"},"run":"static","lang":"json"}],"related":["list_controllers","validate_profile","scaffold_profile"],"keywords":{"en":["read profile","controller profile","midi map","cc number","signal name","verified","minilab3","mcp tool"],"zh":["讀設定檔","控制器設定檔","midi 對照表","cc 號碼","訊號名稱","驗證","minilab3","mcp 工具"]},"tbd":[],"claims":[]},
{"id":"run_checks","key":"run_checks","name":"run_checks","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"run_checks","source":{"file":"packages/mcp/server.js","line":117,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L117"},"summary":{"en":"Runs the framework's whole test suite (`node --test` on every file in `tests/`), the same gate CI uses, and says whether it passed.","zh":"跑整個框架的測試（對 `tests/` 裡的每個檔案執行 `node --test`），也就是 CI 用的那道關卡，並回報有沒有通過。"},"description":[{"en":"No input: send `\"arguments\": {}`. An agent calls it after every change to know whether the repository is still green. It runs `node --test` over every `tests/*.test.js` of the checkout the server lives in, waits for it to finish and gives up after 60 seconds.","zh":"不需要輸入：送 `\"arguments\": {}`。代理每改一次就呼叫它，確認儲存庫還是綠的。它對伺服器所在的那份檔案庫的每個 `tests/*.test.js` 執行 `node --test`，等它跑完，60 秒就放棄。"}],"params":[],"returns":{"type":"Object","en":"When the run finishes cleanly: `{ pass, fail, ok }` (numbers from the report, and `ok` true only when `fail` is 0), as JSON text. Otherwise `{ ok: false, output }`, where `output` holds the lines of the report that say `not ok` or give the totals.","zh":"跑完順利的時候：`{ pass, fail, ok }`（數字取自報告，`fail` 是 0 時 `ok` 才是 true），格式是 JSON 文字。其他情況是 `{ ok: false, output }`，`output` 裡是報告中寫著 `not ok` 或給出總數的那幾行。"},"notes":[{"en":"At the time of writing, in the checkout this was written from, the whole suite takes about 50 seconds and prints about 3.6 MB of report, while the tool reads at most 1 MiB of it. Past that the child process is stopped, and the tool answers `{ ok: false, output }` with only the failing lines found in that first megabyte, even if every test passes. Until that changes, do not read `ok: false` with an empty `output` as a failing test: run `node --test tests/*.test.js` in a terminal.","zh":"寫這一條的時候，在寫它所用的那份檔案庫裡，整套測試大約要跑 50 秒，印出大約 3.6 MB 的報告，而工具最多只讀它的 1 MiB。超過之後，子行程會被停掉，工具回答 `{ ok: false, output }`，裡面只有那第一個百萬位元組裡找到的失敗行，就算每個測試都通過也一樣。在這個狀況改變之前，不要把 `ok: false` 加上空的 `output` 當成有測試失敗：請在終端機裡直接跑 `node --test tests/*.test.js`。"}],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 4,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"run_checks\",\n    \"arguments\": {}\n  }\n}","caption":{"en":"Runs the suite: the reply is `{ pass, fail, ok }` when it ends cleanly, or `{ ok: false, output }` with the failing lines. Allow about a minute.","zh":"跑整套測試：順利跑完時回應是 `{ pass, fail, ok }`，不然是附上失敗行的 `{ ok: false, output }`。要等大約一分鐘。"},"run":"static","lang":"json"}],"related":["scaffold_world","validate_profile"],"keywords":{"en":["run checks","run tests","node --test","ci","test suite","verify","green","mcp tool"],"zh":["跑檢查","跑測試","node --test","ci","測試","驗證","全綠","mcp 工具"]},"tbd":[],"claims":[]},
{"id":"scaffold_profile","key":"scaffold_profile","name":"scaffold_profile","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"scaffold_profile","source":{"file":"packages/mcp/server.js","line":92,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L92"},"summary":{"en":"Generates a valid skeleton profile for a new controller from maker, model and control counts. Every number is an unverified placeholder; nothing is written.","zh":"依廠牌、型號和控制元件的數量，替一台新控制器產生一份有效的設定檔骨架。每個號碼都是標成未驗證的預留值，不會寫入磁碟。"},"description":[{"en":"Input, all required: `maker` (such as `Arturia`), `model` (such as `MiniLab 3`) and `controls`, a list of `{ type, count, from? }` that describes the faceplate, for example `[{\"type\":\"knob\",\"count\":8},{\"type\":\"pad\",\"count\":8},{\"type\":\"keys\",\"count\":25,\"from\":48}]`. `type` is one of `knob`, `encoder`, `fader`, `pad`, `button`, `keys`, `wheel`, `strip`; `count` is a whole number from 1 to 128; `from` is the lowest note of a `keys` section (default 48).","zh":"輸入全部必填：`maker`（例如 `Arturia`）、`model`（例如 `MiniLab 3`）和 `controls`，一串描述面板的 `{ type, count, from? }`，例如 `[{\"type\":\"knob\",\"count\":8},{\"type\":\"pad\",\"count\":8},{\"type\":\"keys\",\"count\":25,\"from\":48}]`。`type` 是 `knob`、`encoder`、`fader`、`pad`、`button`、`keys`、`wheel`、`strip` 之一，`count` 是 1 到 128 的整數，`from` 是 `keys` 區段最低的音符（預設 48）。"},{"en":"The skeleton passes `validateProfile()` and it is deliberately honest: every control has `\"verified\": false`, the source list holds one `TODO` entry, and the numbers are placeholders. Knobs, faders and strips get consecutive CC numbers from 20, pads get notes from 36 (skipping a `keys` range), buttons get momentary CCs, encoders get relative CCs, the first wheel is a pitch bend and any further wheel falls back to a CC. The id comes from maker and model (`acme-pad-pro-16`), `short` is the model without hyphens, and `match.ports` is a pattern made of the model's words.","zh":"骨架會通過 `validateProfile()`，而且刻意很誠實：每個控制元件都是 `\"verified\": false`，來源清單裡只有一條 `TODO`，號碼都是預留值。旋鈕、推桿、觸控條從 20 開始依序拿 CC 號碼，打擊墊從 36 開始拿音符（會跳過 `keys` 的範圍），按鈕拿瞬間式的 CC，編碼器拿相對式的 CC，第一個滾輪是彈音，之後的滾輪改用 CC。id 由廠牌和型號組成（`acme-pad-pro-16`），`short` 是去掉連字號的型號，`match.ports` 是由型號各個字組成的規則。"},{"en":"Wrong input is an error with the reason: no maker or model, `controls` empty or not a list, an unknown `type`, a `count` outside 1..128, `keys` outside notes 0..127, or more controls than there are CC numbers (about 108) or pad notes. After that the agent sources every number from the maker's MIDI chart, runs [`validate_profile`](#validate_profile), and saves the file in `packages/midi/profiles/`; `next_steps` lists these.","zh":"輸入不對會得到說明原因的錯誤：沒有廠牌或型號、`controls` 是空的或不是清單、不認得的 `type`、`count` 不在 1..128、`keys` 超出音符 0..127，或控制元件多到 CC 號碼（約 108 個）或打擊墊音符不夠用。之後代理要從廠商的 MIDI 對照表查證每個號碼，跑 [`validate_profile`](#validate_profile)，再把檔案存進 `packages/midi/profiles/`。`next_steps` 列出的就是這些。"}],"params":[],"returns":{"type":"Object","en":"`{ profile, problems, unverified, next_steps }` as JSON text, plus `warnings` when the id or short name collides with a shipped profile. `profile` is the skeleton, `problems` is what `validateProfile()` says about it (empty), `unverified` is how many controls are placeholders (all of them), `next_steps` is what to do to turn it into a real profile.","zh":"`{ profile, problems, unverified, next_steps }`，格式是 JSON 文字，id 或簡稱跟內建設定檔撞名的話還會有 `warnings`。`profile` 是骨架，`problems` 是 `validateProfile()` 對它的意見（是空的），`unverified` 是有多少控制元件是預留值（全部），`next_steps` 是把它變成正式設定檔要做的事。"},"notes":[],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 8,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"scaffold_profile\",\n    \"arguments\": {\n      \"maker\": \"Acme\",\n      \"model\": \"Pad Pro 16\",\n      \"controls\": [\n        { \"type\": \"knob\", \"count\": 8 },\n        { \"type\": \"pad\", \"count\": 16 },\n        { \"type\": \"keys\", \"count\": 25, \"from\": 48 }\n      ]\n    }\n  }\n}","caption":{"en":"Asks for a skeleton for an invented \"Acme Pad Pro 16\": the reply has id acme-pad-pro-16, 25 control entries (the keyboard is one entry of 25 keys, so `unverified` is 49), no problems, and the next steps.","zh":"替虛構的「Acme Pad Pro 16」要一份骨架：回應裡 id 是 acme-pad-pro-16、有 25 個控制元件項目（鍵盤是一項、含 25 個鍵，所以 `unverified` 是 49）、沒有問題，並附上接下來的步驟。"},"run":"static","lang":"json"}],"related":["validate_profile","read_profile","list_controllers"],"keywords":{"en":["scaffold profile","new controller","skeleton","controller profile","maker model","placeholder","unverified","mcp tool"],"zh":["建立設定檔","新控制器","骨架","控制器設定檔","廠牌 型號","預留值","未驗證","mcp 工具"]},"tbd":[],"claims":[]},
{"id":"scaffold_world","key":"scaffold_world","name":"scaffold_world","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"scaffold_world","source":{"file":"packages/mcp/server.js","line":65,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L65"},"summary":{"en":"Creates a new numbered example by copying a donor example's files; then the agent edits the World in `main.js`. This tool writes to disk.","zh":"複製一個範本範例的檔案，建立一個新的、有編號的範例，之後代理再去編輯 `main.js` 裡的世界。這個工具會寫入磁碟。"},"description":[{"en":"Input: `slug` (required), a kebab-case name made of lower-case letters, digits and hyphens, such as `jellyfish-bloom`; and `donor` (optional, default `01-hello-particles`), the name of a folder in `examples/` to copy. Good donors: `01-hello-particles` (a minimal canvas), `02-chord-garden` (driven by chords), `03-pose-field` (the camera), `05-prebiotic-flake` (p5 and sound). The new folder is `examples/<number>-<slug>`, where the number is the highest one already in `examples/` plus 1, with two digits (`28`).","zh":"輸入：`slug`（必填），由小寫字母、數字和連字號組成的 kebab-case 名稱，例如 `jellyfish-bloom`，以及 `donor`（選填，預設 `01-hello-particles`），要複製的 `examples/` 裡的資料夾名稱。好用的範本有：`01-hello-particles`（最小的畫布）、`02-chord-garden`（由和弦驅動）、`03-pose-field`（鏡頭）、`05-prebiotic-flake`（p5 加聲音）。新資料夾是 `examples/<編號>-<slug>`，編號是 `examples/` 裡現有最大的編號加 1，兩位數（`28`）。"},{"en":"It copies every file of the donor folder (the donors have no sub-folders, and a sub-folder would stop the copy with an error), so the new example starts as a working copy. A `slug` with anything else in it, or an unknown `donor`, or a folder that already exists is an error (`slug must be kebab-case`, `unknown donor: …`, `already exists: …`).","zh":"它複製範本資料夾裡的每個檔案（現有的範本都沒有子資料夾，遇到子資料夾複製會出錯而中斷），所以新範例一開始就是能跑的副本。`slug` 含有其他字元、`donor` 不存在、或資料夾已經存在，都會是錯誤（`slug must be kebab-case`、`unknown donor: …`、`already exists: …`）。"}],"params":[],"returns":{"type":"Object","en":"`{ created, next_steps }` as JSON text: `created` is the new folder (`examples/28-jellyfish-bloom`), `next_steps` is three lines: edit the World in `main.js`, edit the `<title>` and hint in `index.html`, then open it with `node serve.js`.","zh":"`{ created, next_steps }`，格式是 JSON 文字：`created` 是新資料夾（`examples/28-jellyfish-bloom`），`next_steps` 是三行：編輯 `main.js` 裡的世界、編輯 `index.html` 的 `<title>` 和提示文字、然後用 `node serve.js` 打開。"},"notes":[{"en":"Send `slug` every time. It is marked required in the input schema, but the tool does not check that it is there: without it the folder is created with the name `examples/<number>-undefined`.","zh":"每次都要送 `slug`。輸入 schema 把它標成必填，但工具本身沒有檢查它在不在：沒給的話，資料夾會以 `examples/<編號>-undefined` 的名字建立。"},{"en":"`donor` is used as a folder name without further checks, so give it a plain name from `examples/`.","zh":"`donor` 會直接當成資料夾名稱使用，沒有再檢查，所以請給 `examples/` 裡的單純名稱。"}],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 3,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"scaffold_world\",\n    \"arguments\": {\n      \"slug\": \"jellyfish-bloom\",\n      \"donor\": \"02-chord-garden\"\n    }\n  }\n}","caption":{"en":"Creates examples/<next number>-jellyfish-bloom from the chord garden; the reply says where, and what to edit next.","zh":"用和弦花園建立 examples/<下一個編號>-jellyfish-bloom，回應會說明建在哪裡，以及接下來要編輯什麼。"},"run":"static","lang":"json"}],"related":["list_examples","read_doc","run_checks"],"keywords":{"en":["scaffold world","new example","create example","copy donor","jellyfish","start a work","mcp tool","new performance"],"zh":["建立世界","新範例","建立範例","複製範本","開始一件作品","mcp 工具","新演出"]},"tbd":[],"claims":[]},
{"id":"validate_profile","key":"validate_profile","name":"validate_profile","aliases":[],"kind":"tool","package":"@openav/mcp","category":"mcp","documented":true,"signature":"validate_profile","source":{"file":"packages/mcp/server.js","line":87,"url":"https://github.com/frank890417/open-audiovisual/blob/main/packages/mcp/server.js#L87"},"summary":{"en":"Checks a controller profile with `validateProfile()` and against the shipped ones: same id, same short name, or port patterns that claim the same device.","zh":"用框架自己的 `validateProfile()` 檢查一份控制器設定檔，並跟內建的設定檔比對：id 相同、簡稱相同，或連接埠規則會搶到同一台裝置。"},"description":[{"en":"Input: `profile` (required), the profile as an object or as a JSON string. A string that is not valid JSON does not raise an error: the answer is `{ ok: false, problems: ['not valid JSON: …'], conflicts: [] }`. The checks cover the id and short name (lower-case, and `short` must not be one of the reserved generic names), a name and a kind (`device` or `generic`), a source for every device profile, the port regular expressions, the faceplate size, every control (a known type and message, `cc` or `note` in 0..127, two controls never on the same message, an encoder's relative mode) and that every control is shown on the faceplate.","zh":"輸入：`profile`（必填），可以是物件，也可以是 JSON 字串。不是合法 JSON 的字串不會引發錯誤：答案是 `{ ok: false, problems: ['not valid JSON: …'], conflicts: [] }`。檢查的範圍有：id 和簡稱（小寫，而且 `short` 不能是保留的通用名稱）、名稱和種類（`device` 或 `generic`）、每份 device 設定檔都要有來源、連接埠的正規表示式、面板尺寸、每個控制元件（已知的類型和訊息、`cc` 或 `note` 在 0..127、兩個控制元件不能用同一則訊息、編碼器的相對模式），以及每個控制元件都有顯示在面板上。"},{"en":"The conflicts are what the validator cannot see by itself: `id` and `short` are taken by a shipped profile (the `short` becomes the signal namespace `midi/<short>/…`), and `port` means a regular expression in `match.ports` is identical to a shipped one or matches its name, so the framework could not tell the two devices apart when one is plugged in. A profile for a new device passes when `ok` is true; a clone of a shipped one comes back with all three kinds.","zh":"衝突是驗證器自己看不到的事：`id` 和 `short` 已經被內建的設定檔用掉了（`short` 會變成訊號命名空間 `midi/<short>/…`），`port` 則是 `match.ports` 裡的某條正規表示式跟內建的一模一樣、或對得上它的名稱，這樣接上其中一台時，框架分不出兩台裝置。新裝置的設定檔在 `ok` 是 true 的時候就算通過。複製一份內建設定檔去驗證，會三種衝突全都得到。"}],"params":[],"returns":{"type":"Object","en":"`{ ok, problems, conflicts }` as JSON text. `problems` is a list of sentences from `validateProfile()` (`controls[0] (a): cc must be 0..127`). `conflicts` is a list of `{ with, kind, detail }`: `with` is the shipped profile, `kind` is `'id'`, `'short'` or `'port'`. `ok` is true only when both are empty.","zh":"`{ ok, problems, conflicts }`，格式是 JSON 文字。`problems` 是 `validateProfile()` 給的一串句子（`controls[0] (a): cc must be 0..127`）。`conflicts` 是 `{ with, kind, detail }` 的清單：`with` 是內建的那份設定檔，`kind` 是 `'id'`、`'short'` 或 `'port'`。兩個都是空的，`ok` 才是 true。"},"notes":[],"examples":[{"code":"{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 7,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"validate_profile\",\n    \"arguments\": {\n      \"profile\": {\n        \"id\": \"Bad Id\",\n        \"kind\": \"device\",\n        \"controls\": [\n          {\n            \"id\": \"a\",\n            \"type\": \"knob\",\n            \"msg\": \"cc\",\n            \"cc\": 200\n          }\n        ]\n      }\n    }\n  }\n}","caption":{"en":"Sends a profile with several mistakes: the reply has `ok: false` and a list of problems, among them the id that is not lower-case and \"cc must be 0..127\".","zh":"送一份有好幾個錯誤的設定檔：回應是 `ok: false` 和一串問題，其中有不是小寫的 id，以及「cc must be 0..127」。"},"run":"static","lang":"json"}],"related":["scaffold_profile","read_profile","list_controllers"],"keywords":{"en":["validate profile","check profile","validateProfile","conflicts","port pattern","collision","controller profile","mcp tool"],"zh":["驗證設定檔","檢查設定檔","validateProfile","衝突","連接埠規則","撞名","控制器設定檔","mcp 工具"]},"tbd":[],"claims":[]}
]}
