# p5.js 一行接上

任何一份 p5.js 草圖（OpenProcessing、p5 編輯器、你自己的網頁）在最前面加一行，就能用 MIDI 控制器、
螢幕上的控制器或手機來彈：

```js
document.head.prepend(Object.assign(document.createElement('script'), { src: 'https://openaudiovisual.com/p5/oav-p5.js' }));
```

在 OpenProcessing 也可以不寫這行，改把網址貼到右側 Libraries 的 **＋**（自訂函式庫）。p5 1.x、2.x 的全域模式都能用，
`setup()` 裡不用呼叫任何東西。
帶著這一行、現在就能彈的草圖：[範例 15](https://openaudiovisual.com/examples/15-p5-one-line/)。

## 像讀滑鼠一樣讀它

```js
function draw() {
  background(knob1 * 255);                 // knob 1, 0..1, like mouseX
  circle(width / 2, height / 2, 50 + knob2 * 300);
}

function notePressed(note, velocity) {     // like keyPressed(): any key, or a pad
  circle(random(width), random(height), velocity * 100);
}
```

| 讀 | 值 |
|---|---|
| `knob1` … `knob8`、`knob(n)` | 0 到 1，編號照控制器上印的順序 |
| `fader1` … `fader8`、`fader(n)` | 0 到 1 |
| `pad1` … `pad16`、`pad(n)` | 按著的時候是力度，放開回到 0 |
| `note`、`velocity` | 最後按的鍵（MIDI 編號）和多用力（0 到 1） |
| `noteIsPressed`、`noteIsDown(60)` | 跟 `keyIsPressed`、`keyIsDown()` 一樣 |

| 寫這個函式 | 什麼時候被叫 |
|---|---|
| `notePressed(note, velocity)`、`noteReleased(note)` | 按下、放開一個鍵（打擊墊也算，除非你寫了 `padPressed`） |
| `knobMoved(n, value)`、`faderMoved(n, value)` | 轉旋鈕、推推桿 |
| `padPressed(n, velocity)`、`padReleased(n)` | 打擊墊按下、放開 |

旋鈕 1 就是你手上那台的旋鈕 1：[控制器設定檔](controllers.md)記著每顆旋鈕送哪個訊息（MiniLab 3 的旋鈕 1 是 CC 74，
LPD8 是 CC 70）。OAV 沒有設定檔的控制器，你先轉的那顆就是 `knob1`，再轉的是 `knob2`。草圖裡自己已經有
`note`、`velocity`、`knob1` 這些名字的話，原本的會留著。

## 音名

收音符號碼的地方，也都能直接寫音名（0.8）：

```js
function draw() {
  if (noteIsDown('C4')) circle(width / 2, height / 2, 100);   // the same as noteIsDown(60)
}

function mousePressed() {
  midi.play('C#4', 0.8);                    // sends note 61, as a key would
}
```

| 寫 | 得到 |
|---|---|
| `noteNumber('C4')` | `60`；給數字就原樣回來，夾在 0 到 127 |
| `noteName(61)` | `'C#4'`；`noteName(61, { flats: true })` 是 `'Db4'`，`noteName(61, { octave: false })` 是 `'C#'` |
| `noteFreq('A4')`、`noteFreq(69)` | `440`（赫茲），給自己合成聲音的草圖用；`noteFreq(69.5)` 是高四分之一音 |
| `noteIsDown('C4')`、`midi.isDown('C4')`、`midi.play('C#4', 0.8)` | 跟寫 `60`、`61` 一樣 |

音名是字母 A 到 G（大小寫都行）、可有可無的 `#` 或 `b`（`♯`、`♭` 也行），再加八度數字：`C4`、`c#4`、`Db3`、`F♯2`，
從 `C-1`（0）到 `G9`（127）。**C4 是 60**，跟課程模組用的科學音高記法一致；有些軟體（Ableton、Yamaha）把 60 叫 C3，
從那邊抄來的音名會差一個八度。這也是 Strudel 的寫法（`c4 e4 g4`，升記號 `#`，降記號 `b`）。
其他寫法，像 `H4`、`C-2`，會停下來並跳出一張一行的卡片，告訴你正確怎麼寫。
`notePressed(note, velocity)` 拿到的仍然是號碼：`noteName(note)` 把它寫回音名。

## 搭配 OP Configurator

[OP Configurator 3000](https://github.com/msawired/OPC)（OPC，OpenProcessing 內建）幫變數取名字、定範圍。
頁面上有這一行，控制器就照宣告順序接上這些變數，不用一個一個對應：

```js
OPC.slider('radius', 60, 10, 300);   // knob 1
OPC.slider('speed', 1, 0, 5, 0.1);   // knob 2
OPC.toggle('trails', true);          // pad 1 flips it
```

旋鈕依序接 slider，推桿接在旋鈕後面，打擊墊接 toggle 和 button。轉旋鈕時 OPC 面板上的滑桿跟著動，
`parameterChanged()` 照常會被叫。OpenProcessing 的 OPC 面板自己也能學實體控制器（面板上的 MIDI 開關），
一看到它的對應在動，實體控制器就交給它，這一行繼續接螢幕上的控制器和手機（這兩個 OpenProcessing 看不到）。
OPC 變數不要取 p5 已經有的函式名稱（`hue`、`fill`），p5 1.x 會撞名。

**一顆旋鈕只做一件事。** 草圖自己也直接讀 `knob1` 的話，旋鈕 1 就歸草圖：OPC 的第一條拉桿不再跟著它（改在 OPC 面板上拉），
其他旋鈕照接，並跳一次提示說明。🎹 面板的第一行就是接線表，例如「接線：旋鈕1 → knob1（草圖；radius 改用滑桿） · 旋鈕2 → speed（OPC）」。

## 麥克風

同一行也接得上麥克風，草圖讀它的方式一樣：讀變數。

```js
function draw() {
  background(230, 40, 10);
  fill(200 + bass * 160, 70, 100);                      // bass: the low end, 0..1
  circle(width / 2, height / 2, 50 + micLevel * 300);   // micLevel: how loud, 0..1
}

function beatDetected(level) {                          // like keyPressed(): a beat just landed
  // flash, spawn, jump to the next scene
}
```

| 讀 | 值 |
|---|---|
| `micLevel` | 0 到 1，聽起來多大聲：房間本身的底噪是 0，說話大約 0.4–0.6，大喊接近 1 |
| `bass`、`mid`、`treble` | 0 到 1，20–250 Hz、250–2000 Hz、2000–8000 Hz 三段的能量 |
| `beat` | 每個拍子有一格是 `true`，像按著滑鼠時的 `mouseIsPressed` |
| `micIsOn` | 麥克風有沒有在跑 |
| `startMic()`、`stopMic()` | 用程式開、關（回傳 promise） |
| `micSensitivity(2)` | 靈敏度變兩倍；`micSensitivity()` 回傳現在的值 |

| 寫這個函式 | 什麼時候被叫 |
|---|---|
| `beatDetected(level)` | 一個拍子：音量比房間剛才的樣子突然跳起來（level 0 到 1） |
| `kickDetected(level)`、`snareDetected(level)`、`hatDetected(level)` | 低、中、高頻段各自打一下（20–120 Hz、150–800 Hz、6–14 kHz） |

**草圖開口要，它才開。** 頁面載入時什麼都不會要。草圖第一次讀到 `micLevel`、`bass`、`mid`、`treble`、`beat`，
或呼叫 `startMic()`，瀏覽器才會問你要不要給麥克風；答案回來之前，每個值都是 `0`。只寫了 `beatDetected()`、
什麼值都沒讀的草圖，會在第一次點擊或按鍵時開。🎹 面板裡的 **🎤** 按鈕可以手動開關、顯示音量；瀏覽器擋下麥克風時，
它會寫出來（從網址列左邊重新允許之後，再按一次）。

- **瀏覽器要你先點一下的話。** 瀏覽器在頁面被點過之前不讓聲音動起來。這個檔在 `draw()` 一讀到值就去要，
  瀏覽器擋住的話，第一次點擊或按鍵再開一次，所以不用寫 `userStartAudio()`。
- **跟 p5.sound 並存。** 分析用的是 [`@openav/audio`](packages.md)（一個 `AnalyserNode`），第一次用到時從這個檔旁邊載入，
  有自己的 `AudioContext`。同一頁可以放 `p5.sound`（`p5.AudioIn`、`p5.FFT`），兩邊的麥克風互不相干，這裡新增的名字
  也沒有一個跟 p5 或 p5.sound 的名字重複。p5 1.x、2.x 的全域模式都能用。
- **多大聲算大聲。** `micLevel` 是以分貝算的「聽起來的大小」，從房間量起：最低點跟著房間自己的底噪走（一兩秒內就定下來，
  往上每秒最多 1.5 dB，所以一直講話不會被當成新的安靜），−10 dBFS（對著筆電大喊）是 1。所以 `50 + micLevel * 300`
  在有人說話時就看得出變大。麥克風太小聲或離太遠：`micSensitivity(3)`（`micLevel` 多 9.5 dB、三個頻段乘三，上限還是 1）。
  原始數值在 `OAVp5.mic.rms`（0 到 1）、`OAVp5.mic.db`、`OAVp5.mic.floor`（房間底噪，dBFS）。
  `OAVp5.mic.on('beat', fn)` 跟定義 `beatDetected` 一樣，回傳一個可以取消的函式。
- **你自己的名字優先。** 草圖裡已經有 `bass`、`mid`（`let`、函式、別的函式庫）的話，原本的會留著，
  這個檔也不會為了它去開麥克風。
- **關掉自動開。** `?mic=click`（或 `window.OAV_P5 = { mic: 'click' }`）：只有 🎤 和 `startMic()` 能開；
  `mic=off` 就什麼都開不起來（名字還在，值一直是 0）。跟所有用麥克風的頁面一樣，要 `https` 或 `localhost`。
- 現成可開的麥克風草圖：`p5/examples/mic/`。

## 聲音：彈什麼就聽到什麼

```js
function setup() {
  createCanvas(windowWidth, windowHeight);
  startSound('piano');          // every key is heard: a keyboard, the 🎹 on screen, a phone
}
```

| 呼叫／讀 | 值 |
|---|---|
| `startSound(instrument)` | 開聲音（預設 `'piano'`）；開著的時候再叫一次就換樂器 |
| `stopSound()` | 關聲音 |
| `soundIsOn` | 現在彈下去有沒有聲音 |

樂器來自 [`@openav/sound`](packages.md)：`piano`（Salamander 平台鋼琴，第一次要載 2 MB 的取樣，載入中先用電鋼琴頂著，
載不到就一直用電鋼琴）、`epiano`、`organ`、`strings`、`choir`、`harp`、`marimba`、`vibraphone`、`xylophone`、
`glockenspiel`、`music-box`，還有 `synth/…` 一系列合成器。🎹 面板上的 **🔊** 可以開關聲音，打開後有樂器選單和載入進度。

- **沒叫它就不載。** 呼叫 `startSound()` 或按 🔊 之前什麼都不載；之後從這個檔旁邊載 `@openav/sound`，從 jsDelivr 載 Tone.js 15
  （選項 `tone=<網址>` 可以改從別的地方載）。
- **先點一下。** 瀏覽器要網頁被點過才讓它出聲，按 MIDI 鍵盤不算。`setup()` 裡呼叫 `startSound()` 的草圖，
  第一次點擊或按鍵之後才聽得到；在那之前 🎹 按鈕上的 🔊 是黃的。
- **打擊墊是鼓。** 第 10 軌的音（MiniLab 3、LPD8 的打擊墊，General MIDI 的鼓軌）打的是鼓組，不會彈成鋼琴。延音踏板會讓音延長。
- **一行程式都不用寫。** 頁面網址加 `?oav_sound=piano`（任何樂器名；OpenProcessing 用 `?sketch_oav_sound=piano`），
  任何草圖點一下之後就有聲音；`sound=off` 則保持安靜。
- 現成可開、邊彈邊畫的草圖：`p5/examples/keys/`。

## 鼓機

```js
function setup() {
  startDrums('breakbeat', 110);   // a 16-step grid on screen; click a cell to change the beat
}

function kickDetected(level) {    // the microphone's own callback
  // swell, flash, spawn
}

function drumHit(lane, velocity) {   // every hit: 'kick', 'snare', 'hat' or 'clap'
}
```

| 讀 | 值 |
|---|---|
| `drumsAreOn` | 鼓機有沒有在打 |
| `drumStep` | 0 到 15，剛打到第幾格 |
| `drumBar` | 1、2、3……，從 `startDrums()` 開始數第幾小節 |
| `drumBpm` | 速度（格子上的滑桿也會改它） |
| `drumPhase` | 0 到 1，這一小節走到哪，連續的、照聽到的時間算：`rotate(drumPhase * TWO_PI)` 一小節轉一圈 |
| `beatPhase` | 0 到 1，這一拍（四分音符）走到哪：`1 - beatPhase` 每一拍一開始最大，慢慢縮到下一拍 |
| `startDrums(pattern, bpm)`、`stopDrums()` | 開始（打著的時候再叫一次就換節奏、換速度）、停 |

| 寫這個函式 | 什麼時候被叫 |
|---|---|
| `drumHit(lane, velocity)` | 每打一下，每一種鼓都叫（一行節奏裡的碎音鈸、疊音鈸、中鼓也是） |
| `kickDetected(level)`、`snareDetected(level)`、`hatDetected(level)` | 大鼓；小鼓、拍手或鼓邊；鈸（閉鈸、開鈸、踩鈸） |
| `beatDetected(level)` | 大鼓或小鼓，每一格最多一次（`beat` 也會有一格是 true） |
| `rhythmChanged(src, how)` | 那一行換了：`startDrums()`、點格子、打字、↶（0.7） |

**寫給麥克風的草圖，換成鼓機照樣動。** 鼓機每打一下，叫的就是麥克風會叫的那幾個函式，所以麥克風草圖不用改就跟著鼓機動。
草圖開了鼓機之後，只是寫了 `kickDetected()` 不會在第一次點擊時去開麥克風（有讀 `micLevel` 的草圖還是會問，那時兩邊都會叫它）。

- **節奏。** `'four on floor'`、`breakbeat`、`'half time'`、`latin`、`sparse`，自己寫的格子
  `startDrums({ kick: 'x...x...x...x...', snare: '....x.......x...', hat: 'x.x.x.x.x.x.x.x.' })`（沒寫的那軌不打），
  或一行節奏（下面一節）。
- **鼓機卡。** 中文瀏覽器的軌道名是中文（大鼓、小鼓、鈸、拍手），速度旁邊顯示現在第幾小節，正在響的那一欄是黃框
  （跟深色卡片的對比 13:1，投影也看得清楚）。
- **格子放在哪。** 畫面上方正中間：在 OpenProcessing 是 🎹 和 OPC 圖示中間，不擋到下面的 console；🎹 面板打開時移到面板下面。
  「–」收起格子，「×」關掉鼓機；🎹 面板裡的 **🥁** 不寫程式也能開關。🥁 打開的鼓機先不播放，勾格子上的播放框才會出聲（草圖自己寫的 `startDrums()` 會直接播放）。
- **鼓組。** [`@openav/drums`](packages.md) 的合成鼓組（沒有取樣），跟聲音一樣點一下之後才聽得到；回呼從一開始就會被叫。

**畫面再重，鼓都不歪。** 鼓是照音訊時鐘提前排好的：Web Worker 裡的心跳每 25 毫秒醒一次，把接下來 0.1 秒內要打的鼓，
照精確的時間交給鼓組。用鼓組自己的輸出量（`draw()` 每一格故意忙 30 毫秒）：大鼓到大鼓的間隔，差異是 0.000 毫秒；
0.5 版在畫面輪到那一格才打，差異是 15 毫秒。回呼還是在畫面的某一格叫，叫的是聲音**聽得到**的那一格（排定的時間加上
瀏覽器回報的輸出延遲），在 `draw()` 之前，順序跟以前一樣。

- **切到別的分頁**：聲音照響、照數小節，`drumBar` 跟牆上的時間一致，cue 照樣觸發，`barStarted` 每小節叫一次。
  每一下的鼓和 `stepStarted` 在沒人看得到的時候先不叫，回到這個分頁也不補，所以「每打一下就加一顆粒子」的草圖不會一回來就爆出一大堆。
- **`draw()` 一格超過 0.1 秒**：鼓機從那之後提前 0.3 秒排（🩺 會寫）；代價是點格子改節奏，最多晚 0.3 秒才聽到。
- **藍牙喇叭**會晚 0.15 到 0.25 秒；瀏覽器有回報時回呼會等它，也可以用 `?oav_drumlatency=200`（毫秒）自己設。
- **保險**：`?oav_drums=frame`（OpenProcessing 是 `?sketch_oav_drums=frame`）退回 0.5 版逐幀推進的鼓機。

### 一行節奏

```js
function setup() {
  startDrums("bd*4, [~ sd]*2, hh*8", 110);   // a kick on every beat, a snare on 2 and 4, eight hats
}
```

不是節奏名稱的字串，就當成一行節奏；節奏名稱、自己寫的格子、數字，行為跟以前一模一樣。這一行字會顯示在格子上方，
正在響的那個字會亮起來、再淡掉。

| 寫 | 意思 | 例 |
|---|---|---|
| 一個字 | 一種鼓：`bd` 大鼓、`sd` 小鼓、`hh` 鈸、`oh` 開鈸、`cp` 拍手、`rim` 鼓邊、`lt` `mt` `ht` 中鼓、`cr` 碎音鈸、`rd` 疊音鈸、`cb` 牛鈴 | `"bd sd bd sd"` |
| `~` | 休息 | `"bd ~ sd ~"` |
| 空白 | 幾個字平分一小節 | `"bd hh sd hh"` |
| `[ ]` | 一組擠進一格 | `"bd [sd sd]"` |
| `*n` | 在那一格打 n 下 | `"hh*8"` |
| `,` | 再疊一行 | `"bd*4, hh*8"` |
| `< >` | 每小節輪一個 | `"<bd*4 [bd ~ bd ~]>"` |
| `(k,n)` | n 格裡平均打 k 下 | `"bd(3,8)"` |

完整的寫法在 [`@openav/pattern`](packages.md)，跟鼓機一起從 `oav-p5.js` 旁邊載入。格子上沒有那一軌的鼓（碎音鈸、疊音鈸、
中鼓、開鈸）會多一列；不在 16 格上的音（三連音）畫在它落在那一格裡的位置。`startDrums("…")` **寫錯了**會跳一張卡（照頁面
語言）說第幾個字、為什麼、怎麼改，鼓機先打 `'four on floor'`；只寫 `'x...x...'`（格子的一軌）會提醒你放進 `{ kick: … }`。

**從 strudel.cc 複製來的也能用**：`startDrums('s("bd*4, [~ sd]*2").fast(2)')` 打的是 `[bd*4, [~ sd]*2]*2`。讀得懂
`s("…")`、`sound("…")`（`"`、`'`、`` ` `` 都行；好幾個 `$: s(…)` 會疊在一起）、`.fast(n)`、`.slow(n)`；`setcpm(110/4)` 或
`setcps(…)` 在 `startDrums()` 沒給速度時拿來當速度。`.bank("…")` 先略過（OAV 用自己合成的鼓組），鏈上其他的（`.jux(rev)`、
`.every(…)`、`.gain(…)`）會跳一張卡說 OAV 還不支援、請按「在 Strudel 打開」；其餘照打。

一行節奏的寫法來自 TidalCycles／Strudel（[strudel.cc](https://strudel.cc)），OAV 是依公開文件獨立實作。

### 格子和那一行互相改

0.7 起，格子上方那一行一直都在（節奏名稱也會顯示它的格子寫成一行的樣子：`'four on floor'` 就是 `bd*4, [~ sd]*2, hh*8`），
兩個方向都能改：

- **點格子**：那一行照人會寫的樣子改寫，鼓機馬上照新的那一行打。整齊的寫成 `bd*4`／`hh*8`，每組一下的寫成 `[~ sd]*2`，
  歐幾里得節奏寫成 `bd(3,8)`（含旋轉：`bd(3,8,2)` 晚兩格開始），其他的一拍一拍寫：`bd [~ ~ ~ bd] bd*2 ~`；各軌用 `, ` 疊起來。
  如果那一行有 16 格寫不出來的東西（三連音、`< >`、`/2`、`?`、`bd:2`），第一次點只跳一張卡說哪些會被換掉，再點一次才改。
- **直接打字**：點那一行就能改；按 Enter 或停手 400 ms 就套用，格子跟著變。打錯的字標紅、跳一張卡說哪裡錯，
  **鼓機繼續打上一個能用的版本**（不會跳回 `'four on floor'`）；Esc 放棄還沒套用的部分。打的字不會觸發草圖的 `keyPressed()`。
- 卡上的 **↶**（或碰過鼓機卡之後按 ⌘Z／Ctrl+Z）回到上一版；`OAVp5.drums.history` 是改過的每一版。
- **複製**：把 `startDrums("…", 110)` 放進剪貼簿，直接貼進 `setup()`。
- **在 Strudel 打開**：用新分頁在 strudel.cc 打開同一行，寫成 `s("…")`，前面加 `setcpm(bpm/4)`（OAV 一小節是 Strudel
  的一個 cycle、四拍；strudel.cc 文件寫的是 `setcpm(bpm/bpc)`）。`OAVp5.drums.strudelUrl` 是那個連結。

```js
function rhythmChanged(src, how) {   // how: 'code' · 'cell' · 'type' · 'undo' · 'menu'
  line = src;                        // e.g. draw the line on the canvas, so a recording shows it
}
```

`OAVp5.drums.src` 是正在打的那一行，`OAVp5.drums.edit("…")` 像打字一樣換一行，`OAVp5.drums.undo()` 回上一版。按鍵換節奏、上一步、開到 Strudel 的草圖：`p5/examples/rhythm-lab/`。

### 把節奏畫在畫布上

鼓機卡是網頁的一部分、不在畫布上，所以錄影錄不到它。`drawRhythm()` 把節奏畫在畫布上：

```js
function draw() {
  background(0);
  drawRhythm(20, 20, 360, 90, 'grid');          // the sixteen steps, a playhead
  drawRhythm(20, 130, 300, 300, 'circle');      // a ring of 16 per drum, a hand turned by drumPhase
  drawRhythm(340, 130, 300, 300, 'spiral', { bars: 4, colors: { kick: '#ff7b54' } });   // the last 4 bars, wound outwards
}
```

`drawRhythm(x, y, 寬, 高, 樣式, 選項)`：樣式是 `'grid'`、`'circle'` 或 `'spiral'`；每一下在**聽到的那一刻**亮起來（跟
`drumPhase` 同一個時鐘），三連音畫在它落下的位置。選項：`colors`（`{ kick: '#f66', … }`，p5 認得的顏色都行）、`color`
（全部同一色）、`off`（空的格子）、`playhead`（顏色，或 `false` 不畫）、`bg`、`bars`（螺旋幾小節）。在 `push()`／`pop()`
裡用 p5 自己的圖形畫，2D 和 WEBGL、p5 1.x 和 2.x 都行；**WEBGL 的 (0, 0) 在畫布正中間**：
`drawRhythm(-width / 2, -height / 2, width, height, 'circle')`。不給位置就畫在底部一條（`'grid'`）或正中間一個正方形。
三種一起用的草圖：`p5/examples/rhythm-visual/`。

## 時間軸：小節與 cue

鼓機也是時鐘：一小節十六格，從 `startDrums()` 開始數。宣告第幾小節要發生什麼：

```js
function setup() {
  startDrums('breakbeat', 110);
  cue(1, 'calm', 12);             // bar 1, and again every 12 bars
  cue(5, 'bloom', 12);
  cue(9, 'storm', 12);
}

function cueFired(name, bar) {    // or read cueName in draw()
  look = SCENES[name];
}
```

| 呼叫／讀／定義 | |
|---|---|
| `cue(bar, name, every)` | 第 `bar` 小節（1 是第一小節，`4.5` 是第 4 小節的一半）；有給 `every` 就每隔那麼多小節再來一次 |
| `cueName` | 最近一次觸發的 cue（第一次之前是 `''`） |
| `cueFired(name, bar)` | 到了某個 cue 的那一小節 |
| `barStarted(bar)`、`stepStarted(step, bar)` | 每一小節的第一格；每一格 |

同樣的 `cue()` 叫兩次不會多一個，所以放在 `draw()` 裡也可以。同一格上先照宣告順序觸發 cue，再來 `barStarted`、`stepStarted`、
那一格的鼓。只要時鐘不要聲音：`startDrums({})`。全部用上的草圖：`p5/examples/drums/`；一行節奏加 `drumPhase`、`beatPhase`：`p5/examples/rhythm/`；把節奏畫在畫布上：`p5/examples/rhythm-visual/`。

## 數值隨時間：lag、env、speed、spring、trail

旋鈕和 `micLevel` 是跳著變的；好看的是它在兩次跳動之間怎麼動。TouchDesigner 用 CHOP 做這件事；這裡有同樣的運算子，
在 `draw()` 裡一個一行，不用宣告；秒就是秒（30 fps 和 144 fps 畫出同一條曲線）：

```js
circle(width / 2, height / 2, 50 + lag(micLevel, 0.03, 0.4) * 300);   // a VU meter: opens at once, closes slowly
scale(1 + env('kick', 0.01, 0.3));                                     // the drum machine: every kick swells it, gone in 0.3 s
rotate(spring(knob1, 100, 6) * TWO_PI);                                // flick a knob: it overshoots and settles
```

第一行像 VU 表：一出聲立刻撐開、安靜下來慢慢收。第二行：鼓機每打一下大鼓就撐開一下，0.3 秒收回。第三行：旋鈕一甩，會衝過頭再彈回來停下。

| TouchDesigner | oav-p5 | |
|---|---|---|
| Lag CHOP（Lag 1／Lag 2） | `lag(value, up, down)` | 跟著 `value` 走：往上用 `up` 秒、往下用 `down` 秒（不給 `down` 就等於 `up`；預設 0.1） |
| Trigger CHOP（attack、decay） | `env('kick', attack, decay)`、`trigger('boom')` | 0..1：每個事件之後 `attack` 秒升到 1，`decay` 秒回到 0（預設 0.01、0.3）。事件：`'kick'` `'snare'` `'hat'` `'beat'`（麥克風或鼓機，跟 `kickDetected` 聽到的是同一套）、`'note'`（任何琴鍵）、`'bar'` `'step'`（鼓機）、cue 的名字、草圖自己的 `trigger('boom')`；也可以給開關：`env(mouseIsPressed, …)` |
| Speed CHOP | `speed(rate)` | 每秒加 `rate`：`rotate(speed(TWO_PI))` 一秒轉一圈 |
| Spring CHOP | `spring(value, stiffness, damping)` | 像掛在彈簧上的東西一樣跟著走（100、6：彈兩三下；damping 20：不彈） |
| Trail CHOP | `trail(value, seconds)` | 最近 `seconds` 秒（2）的值，一個陣列，每秒 60 個，舊的在前、最新的在最後 |
| Slope CHOP | `slope(value)` | 每秒變化多少 |
| Count CHOP | `count(beat)`、`count(beat, 4)`、`count('kick')` | 打開過幾次；給 4 就 0 1 2 3 0 … 循環；給名字：那個事件到現在幾次 |
| CHOP 的檢視器 | `scope(value, 'label')` | 頁面角落的小示波器（見下面） |
| | `drawTrail(values, x, y, w, h)` | 同樣的曲線畫在畫布上，作品裡看得到、錄影也錄得到 |

**秒數跟 TouchDesigner 一樣。** lag 的秒數就是 Lag CHOP 的秒數：大約是跟上一次跳動 90% 所需的時間，所以這裡的
`lag(x, 0.1)` 動起來就像 Lag CHOP 填 0.1（[Lag CHOP 文件](https://docs.derivative.ca/Lag_CHOP)）。底下是一個時間常數為
「秒數 ÷ 2.3（ln 10）」的單極平滑；`@openav/mapping` 的 `smooth` 仍照它原本 63% 的時間常數。attack、decay 兩邊都是長度。

**不用宣告。** `draw()` 裡第一個 `lag()` 記第一個 lag 的值、第二個記第二個，每一幀都一樣；`spring()`、`speed()`、`trail()` …
各自分開數。把它放進 `if`，或放在長度會變的迴圈裡，順序就會錯開：畫面上會有一張提示說明，之後 oav-p5 改成照「寫在哪一行」對應。
最穩的是給它名字，最後一個參數放字串：`lag(p.size, 0.1, 0.5, 'p' + p.id)`。有名字的在哪裡呼叫都是同一個，`draw()`、`setup()`、
`mousePressed()` 都行；沒名字的在 `draw()` 外面沒有東西可以對，直接回傳原值（`speed` 回 0、`trail` 回 `[]`），也會有提示。
`env()`、`trigger()`、`count('kick')` 是跟著事件走的，在哪裡都能用。草圖自己有 `speed` 或 `count` 變數的話照用自己的；
`OAVp5.chop.speed(…)` 是同一個函式換個地方拿。

**看得見的值。** `scope(value, 'label')` 會把 `value` 原樣回傳，所以可以直接包住一段算式：
`circle(x, y, scope(lag(micLevel, 0.03, 0.4), 'lag') * 300)`。每個標籤是一條線，畫在 🎹 對面角落的小示波器裡：最近 4 秒和現在的值；
旁邊再放一個 `scope(micLevel, 'mic')`，就看得到 lag 做了什麼。它在網頁上、不在畫布上：不會錄進影片和 📷，也不會擋到點擊；
`hints=off` 或 `scope=off` 就不顯示。要畫進作品裡：`stroke(255); drawTrail(trail(micLevel, 3), 0, height - 100, width, 100)`
（或 `drawTrail(micLevel, …)`，它自己記 2 秒）：框的底是 0、頂是 1（`h` 後面可以給 `lo`、`hi`），用草圖的 `stroke()`（`noStroke()` 時用白色）。

數學在 [@openav/chop](packages.md#openav-chop)，第一次呼叫時從這支檔案旁邊載入；載好之前（幾幀）每個函式都直接回傳原值。
參數意義參考 TouchDesigner 官方文件（Derivative），程式由 OAV 自行實作。原值和 lag 後並排、配 scope 的範例：`p5/examples/lag/`；用 `env`、`spring`、`speed`、`count`、`slope` 做一個大鼓撐開的圓盤（只靠 `startDrums()`，不用麥克風）：`p5/examples/chop/`。

## 錄影：畫面和聲音一起錄

```js
function keyPressed() {
  if (key === 'r') recordingIsOn ? stopRecording() : startRecording();   // startRecording(10): ten seconds, then it stops
}

function recordingSaved(info) {   // info.name, info.seconds, info.bytes, info.format ('mp4' or 'webm')
}
```

| 呼叫／讀／定義 | |
|---|---|
| `startRecording()`、`startRecording(秒數)`、`startRecording({ seconds, fps, mic, name, canvas, bitrate })` | 錄下畫布和作品的聲音；給秒數就錄那麼久自動停；一段最長 5 分鐘。`canvas` 指定要錄哪個畫布（p5 的畫布或 `createGraphics()`，預設是頁面上最大的 p5 畫布），`bitrate` 是每秒位元數（預設依畫布大小和幀率算：4 到 16 Mb/s，幀率高過 30 時最高 24 Mb/s） |
| `stopRecording()` | 停止，檔案自動下載 |
| `recordingIsOn` | 現在有沒有在錄 |
| `recordingSaved(info)` | 存好了：`name`、`seconds`、`bytes`、`format`、`width`、`height`、`audio`、`sources`、`warnings`、`blob`、`url` |

🎹 面板上的 **⏺** 不寫程式也能開始、停止；錄影中 🎹 按鈕上有紅點和時間（點紅點就停）。**📷** 把畫布存成一張 PNG。

- **影片裡有什麼。** p5 的畫布，用它實際的像素（Retina 螢幕是兩倍），每秒 30 格；加上這個檔發出的所有聲音：`startSound()`、
  鼓機和打擊墊、開著的麥克風、p5.sound 1.x，混成一軌，喇叭照常出聲。🎹、鼓機格子、提示卡片、OPC 面板都不在畫布上，所以不會進影片。
- **檔案。** 瀏覽器能錄 MP4（H.264＋AAC）就錄 MP4：Chrome 126 以後、Edge、Safari，IG、YouTube、剪輯軟體都直接吃。
  其他瀏覽器（Firefox、舊版 Chrome）錄成 WebM，卡片會說 IG 不收。檔名像 `oav-20261015-183005.mp4`；電腦上直接存到「下載」，
  手機上卡片有 **分享** 按鈕，可以存進相簿或直接傳到 IG。
- **每錄完一段都會檢查。** 卡片寫出錄到什麼（格式、長度、大小、有沒有聲音）；有問題就說發生什麼、為什麼：開著聲音卻什麼都沒錄進去
  （還沒點過畫面、沒有彈）、錄成 WebM、檔案幾乎是空的、畫布有畫面但影片全黑（Safari 錄 WebGL 畫布，WebKit 229611）、
  畫布上有別的網站的圖（沒有 CORS）、這個瀏覽器不能錄。
- **麥克風**開著就會一起錄：戴耳機，不然喇叭的聲音會被錄兩次。`record=nomic` 不收麥克風。
- **p5.sound。** 給 p5 1.x 的 p5.sound 1.x 會一起錄。給 p5 2.x 的 p5.sound 把每個聲音直接接到喇叭，沒有總輸出可以接進錄影：
  錄不到，卡片會說。草圖改用 p5 1.11，或改用 `startSound()`、`startDrums()`。草圖自己用 Tone.js、Web Audio 做的聲音也錄不到。
- **靜態圖。** 📷（或 `OAVp5.record.shot()`）只存畫布，檔名 `oav-<日期>-<時間>.png`，不含 🎹 和卡片；
  在程式裡用 p5 自己的 `saveCanvas('名字', 'png')` 也一樣。
- **選項。** `record=nomic`（不收麥克風）、`record=off`（沒有 ⏺ 和 📷，`startRecording()` 什麼都不做）、`recfps=60`、
  `reclimit=600`（秒）、`title=作品名`（放進檔名）；只改這一段：`startRecording({ seconds, fps, mic: false, name, canvas, bitrate })`。
- 全部用上的草圖：`p5/examples/record/`。

## 三種輸入

| 輸入 | 怎麼用 |
|---|---|
| 實體控制器 | Chrome、Edge、Firefox 的 Web MIDI，瀏覽器問一次就記住 |
| 螢幕上的控制器 | 按 🎹 打開 `<oav-controller>`，接上哪台控制器就換成哪台 |
| 手機 | 面板上的 📱 顯示 QR code，手機[遙控頁](remote.md)的 MIDI 分頁就能彈這份草圖 |

三種輸入進來都是同一串 MIDI，草圖分不出是誰彈的（想知道的話看 `midi.last.source`）。Safari 和 iPad 沒有
Web MIDI，螢幕上的控制器和手機照樣能用。草圖自己寫了 Web MIDI（`navigator.requestMIDIAccess`、WebMidi.js）的話，
螢幕和手機會變成多出來的一個輸入，名字叫 **OAV Virtual MIDI**。

別的程式是第四種輸入（0.7）。程式送進 OAV Virtual MIDI **輸出**的東西——Strudel 的 `.midi('OAV Virtual MIDI')`——會在它
標的那個時刻到草圖（`midi.last.source` 是 `virtual-out`）；外部 MIDI 時鐘推動 `drumPhase`、`drumBar` 和鼓機；
`link=<房間>` 把另一個分頁、另一台電腦的程式接進這份草圖。全部寫在[和 Strudel 一起用](strudel.md)。

## 出狀況的時候

彈的時候沒人會去看 OpenProcessing 畫布下面的 console，所以出狀況會直接寫在畫面上：🎹 旁邊一張小卡片，不擋畫布中間，
用瀏覽器的語言，同一則只跳一次，按 × 關掉。每張都寫三件事：**發生什麼、為什麼、下一步按哪裡**，console 也同步寫一樣的話。

- **麥克風**：被擋下（網址列左邊的鎖頭 → 允許 → 回來點一下畫面）、被電腦的系統設定擋住、在沒有 `allow="microphone"` 的嵌入框裡、
  找不到麥克風、被別的程式（Zoom、OBS）占用、網址不是 https、瀏覽器還在等你按允許。
- **MIDI**：瀏覽器沒有 Web MIDI（Safari、iPad：改用 🎹 或手機）、MIDI 權限被擋。只對有用 MIDI 的草圖說；
  什麼功能都沒用到的草圖不會跳任何卡片。
- **你自己的程式**：`notePressed()`、`kickDetected()`、`drumHit()` 這些回呼裡出錯，卡片寫出錯誤訊息、哪個檔第幾行，草圖照樣繼續跑。
  草圖自己宣告了 `note`、`knob1`（`let`、`var`、`function`），原本的會留著，卡片告訴你改讀什麼（`midi.last.note`、`knob(1)`）。
- **聲音和鼓機**：要先點一下畫面、鋼琴還在載入（先用電鋼琴頂著）、某個樂器載不到（改用備用的）、Tone.js 連不上、
  鼓機在打但還沒點過畫面所以沒聲音。
- **錄影**：開始錄了（錄到哪些聲音、怎麼停）、錄好了（格式、長度、大小、有沒有聲音）、錄到的影片沒有聲音、錄成 WebM（IG 不收）、
  整段黑、檔案幾乎是空的、畫布上有別的網站的圖、這個瀏覽器不能錄。

`?oav_hints=off`（或 `window.OAV_P5 = { hints: 'off' }`）只寫在 console；`OAVp5.hints` 列出說過的提示。

**🩺 體檢。** 🎹 面板上的 🩺（或 `OAVp5.check()`）把版本、頁面、瀏覽器、麥克風、MIDI、聲音、鼓機、錄影（這裡能錄 MP4 還是 WebM、上一段）、接線、草圖自己的名字、
目前為止的提示整理成一張卡，截圖貼到 Discord 就能遠端看；有「複製文字」按鈕，也連到完整的[裝置檢查](https://openaudiovisual.com/zh/check/)。

## 在 Hydra 裡用

[Hydra](https://hydra.ojack.xyz) 有自己的載入函式；同一個檔在那裡也能用，Hydra 的箭頭函式每一格都會重讀變數：

```js
await loadScript('https://openaudiovisual.com/p5/oav-p5.js')
osc(10, 0.1, () => knob1 * 2).out()                     // knob 1 sets the colour offset
```

```js
osc(10, 0.1, () => 1 + micLevel * 3).out()              // the microphone, asked for when this line first runs
```

`note`、`velocity`、`knob1` … `knob8`、`fader1` …、`pad1` …、`drumStep`，還有 🎹 螢幕上的控制器，都一樣在。
Hydra 不是 p5，沒有可以定義的 `notePressed()` 回呼，改用訂閱：
`midi.on('notePressed', (note, velocity) => { … })` 或 `OAVp5.drums.on('hit', (lane, velocity) => { … })`（都回傳一個取消訂閱的函式）。測試用的是 hydra-synth 1.4.0，
也就是編輯器跑的那個函式庫（`p5/test/hydra-smoke.cjs`）。

## 細節

- **為什麼用 `prepend`**：p5 的友善錯誤檢查會把頁面上最後一個 `<script>` 當成你的草圖。這個檔放在最前面，
  就不會被誤當成你的程式。
- **為什麼非同步載入的一行就夠**：p5 等到視窗的 `load` 事件才開始全域模式，而頁面載入中插進來的 script 會讓
  `load` 等它跑完，所以 `setup()` 裡就已經有 `knob1`。
- **這一行沒載到時**（沒網路、網址打錯），讀 `knob1` 的程式會停在「knob1 is not defined」。
- **嵌到別的網站**：OpenProcessing 自己的頁面允許草圖用 MIDI 和麥克風，它給的嵌入碼沒有。要讓嵌入的草圖聽實體控制器，
  在那個 `<iframe>` 加上 `allow="midi"`；要它聽麥克風，加 `allow="microphone"`。
- **選項、MIDI 層的寫法和測試**：[`p5/README.md`](https://github.com/frank890417/open-audiovisual/tree/main/p5)。
  範例：`p5/examples/particles/`、`p5/examples/opc/`、`p5/examples/mic/`、`p5/examples/keys/`、`p5/examples/drums/`、`p5/examples/rhythm/`、`p5/examples/rhythm-visual/`、`p5/examples/rhythm-lab/`、`p5/examples/record/`、`p5/examples/lag/`、`p5/examples/chop/`、`p5/examples/with-strudel/`、`p5/examples/note-names/`。
