ライフサイクル
▶ Render ボタンを押すと、ユーザコードは トップレベル → onInit → onBeat → onNote → onCc の順に実行されます。本ページでは各フェーズで何が起きるか、CLIP_BEAT などのグローバル変数がどう変化するかを 1 つの具体的なクリップを追いながら説明します。
▶ Render ボタン │ ├─ 1. lib のロード(factory.js + 有効化中の libs/codes/*.js) │ ├─ 2. new Function() でラッパーをコンパイル │ (構文エラーはここで Compile error: ... となる) │ ├─ 3. ラッパー実行(= ユーザコードのトップレベル) │ ・関数定義・グローバル変数の用意 │ ・onInit/onBeat/onNote/onCc を「登録」する │ ├─ 4. ① onInit ─ CLIP_BEAT = 0 │ ├─ 5. ② onBeat ─ 0, 1/4, 2/4, ..., (CLIP_LENGTH - 1/4) │ (onBeat を 1 つも登録していなければスキップ) │ ├─ 6. ③ onNote ─ 元ノート events を startBeat 昇順で │ (events が空 / onNote 未登録ならスキップ) │ ├─ 7. ④ onCc ─ 固定解像度のサンプリンググリッドで毎刻 │ (onCc 未登録ならループごとスキップ。元 CC レーンの有無は不問) │ └─ 8. 結果(notes[], ccLanes[], log[])を Render タブへ反映それぞれのフェーズの詳細を見ていきます。
トップレベルとコールバックの関係
Section titled “トップレベルとコールバックの関係”ユーザコードのトップレベルは ▶ Render のたびに 1 度だけ 実行されます。onInit/onBeat/onNote/onCc はそこで「コールバックを 登録」する役割で、呼び出されるのはあとのフェーズです。
// トップレベル(最初に 1 度だけ走る)let counter = 0;console.log("compiled"); // (1) 必ず最初に出る
onInit(() => { console.log("init"); // (2) フェーズ ① で出る counter = 0;});
onBeat((b) => { counter += 1; setNote({ pitch: 60, startBeat: b, lengthBeats: 0.25 });});[!TIP] 同じフェーズに複数のコールバックを登録できます。登録順 に呼ばれます。
onNote((n) => setNote({ ...n })); // 元ノート保持onNote((n) => setNote({ ...n, pitch: n.pitch + 7 })); // 5 度上のハモリ
4 フェーズの詳細
Section titled “4 フェーズの詳細”① onInit — クリップの先頭で 1 回
Section titled “① onInit — クリップの先頭で 1 回”| 項目 | 値 |
|---|---|
| いつ呼ばれる | レンダーの最初に 1 回 |
| 引数 | なし |
CLIP_BEAT | 常に 0 |
CLIP_TIME | 常に 0 |
| 登録ゼロ時 | スキップ(実害なし) |
用途: コード全体で使う変数の初期化、onInit 内だけで完結する一括ノート出力(コード進行の生成など)。
const arp = chord("maj7", 60);onInit(() => { arp.forEach((p, i) => { setNote({ pitch: p, startBeat: i * 0.5, lengthBeats: 0.5 }); });});② onBeat — 16 分グリッドで反復
Section titled “② onBeat — 16 分グリッドで反復”| 項目 | 値 |
|---|---|
| いつ呼ばれる | 0, 1/4, 2/4, ... を CLIP_LENGTH 未満まで(既定 stepsPerBeat = 4) |
| 引数 | (beat: number) — 現在のグリッド位置 |
CLIP_BEAT | その beat と同じ |
CLIP_TIME | beat / bpm * 60(秒) |
| 登録ゼロ時 | ループ自体スキップ(コストゼロ) |
クリップ長が 4 拍なら、onBeat は 16 回 呼ばれます(i=0..15、beat = 0, 0.25, …, 3.75)。
// 4 拍ごとにキック、裏拍ごとにスネアonBeat((b) => { if (Number.isInteger(b)) noteOn(36, 0.1, 100, b); // 1, 2, 3, 4 拍 if (Math.abs((b % 1) - 0.5) < 1e-6) noteOn(38, 0.1, 90, b); // 裏拍});[!CAUTION] グリッドは 16 分音符固定 です。三連符など 16 分以外の解像度を使いたい場合は
bを見て間引いてください。例:if (Math.abs((b * 3) % 1) < 1e-6) { ... }で 1/3 拍ごと。
③ onNote — 元ノートを順に処理
Section titled “③ onNote — 元ノートを順に処理”| 項目 | 値 |
|---|---|
| いつ呼ばれる | 元ノート(PianoRoll タブ)の各ノートに対して 1 回ずつ |
| 引数 | (note: MidiNote) — そのノートのコピー |
| 順序 | startBeat 昇順 |
CLIP_BEAT | そのノートの startBeat と同じ |
| 登録ゼロ時 | スキップ |
| 元ノート空 / 未登録 時 | スキップ |
用途: 元ノートをそのまま通したり、変換したり、レイヤーを足したりする「変換器」。
// 元ノートを保持しつつ、5 度上を 0.25 拍遅らせて重ねるonNote((n) => { setNote({ ...n }); setNote({ pitch: n.pitch + 7, velocity: Math.round(n.velocity * 0.7), startBeat: n.startBeat + 0.25, lengthBeats: n.lengthBeats, });});[!CAUTION]
onNote内でsetNote()を呼ばないと、その元ノートは Render から消えます(PianoRoll の元データは残ります)。ノートを残したいならsetNote({ ...n })を必ず通すこと。
④ onCc — 固定解像度のサンプリンググリッド
Section titled “④ onCc — 固定解像度のサンプリンググリッド”| 項目 | 値 |
|---|---|
| いつ呼ばれる | 0, 1/r, 2/r, ... を CLIP_LENGTH 未満まで(r = 解像度。既定 16、setCcResolution() で上書き可) |
| 引数 | (ctx: { beat, time, phase }) — phase = beat / CLIP_LENGTH(クリップ全体で 1 周期の LFO に便利) |
CLIP_BEAT | その beat と同じ |
CLIP_TIME | beat / bpm * 60(秒) |
| 登録ゼロ時 | ループ自体スキップ(コストゼロ) |
入力 ccLanes の有無 | 無関係 — 空でも掃引は走る(ゼロから LFO を生成可能) |
入力 CC を読みたいときは sampleCc(ccNumber, beat) で線形補間値を取得します(ホスト API)。
// CC1 にサイン LFO を 1 周期かける(解像度 32 で滑らかに)onInit(() => setCcResolution(32));onCc(({ phase }) => { setCc({ ccNumber: 1, value: 64 + 32 * Math.sin(phase * 2 * Math.PI) });});
// 既存 CC7 にゆらぎを足すonCc(({ beat, phase }) => { const base = sampleCc(7, beat); setCc({ ccNumber: 7, value: clamp(base + 10 * lfo(phase, "sin"), 0, 127) });});[!CAUTION] 解像度 × クリップ長 が 32,768 を超える場合、Render は
Runtime error (onCc): grid sweep would emit ... ticks, exceeding cap (32768).を返します。setCcResolution()を下げるかクリップを短くしてください。
[!TIP]
setCcResolution(n)は トップレベルまたはonInit内 から呼ぶと反映されます。onBeat / onNote / onCcの中で呼んでも黙って無視されます(グリッドは onCc 掃引の直前にスナップショットされるため)。
CLIP_BEAT / CLIP_TIME / CLIP_LENGTH の更新メカニズム
Section titled “CLIP_BEAT / CLIP_TIME / CLIP_LENGTH の更新メカニズム”これら 3 つは ラッパー内部の let バインディングとしてユーザコードに公開されています。各コールバック呼び出しの 直前 に内部関数 __sync() がそれらを書き換え、ユーザのクロージャは 呼び出し時点の値 を見ます。
const log = [];onInit(() => log.push(["init", CLIP_BEAT])); // ["init", 0]onBeat(() => log.push(["beat", CLIP_BEAT])); // ["beat", 0], ["beat", 0.25], ...onNote((n) => log.push(["note", CLIP_BEAT])); // ノートの startBeat| 名前 | 内容 | レンダー中の変化 |
|---|---|---|
CLIP_BEAT | 現在のフェーズに対応するビート | フェーズごとに更新 |
CLIP_TIME | CLIP_BEAT / bpm * 60 秒 | フェーズごとに更新 |
CLIP_LENGTH | クリップ長(ビート数) | 不変(コンパイル時固定) |
[!CAUTION]
onInitでCLIP_BEATを読んでも 常に 0 です。クリップ末尾の処理をしたければonBeatのbを見るか、CLIP_LENGTH - epsilonと比較してください。
トレース例:1 ノートのクリップ
Section titled “トレース例:1 ノートのクリップ”長さ 1 拍・bpm 120 のクリップに、PianoRoll で C4 の元ノート(startBeat=0.5, length=0.25)を 1 つだけ置いた場合:
console.log("top-level");onInit(() => console.log("init", CLIP_BEAT, CLIP_TIME));onInit(() => setCcResolution(4)); // 例示用に粗いグリッドonBeat((b) => console.log("beat", b, CLIP_BEAT, CLIP_TIME));onNote((n) => console.log("note", n.pitch, CLIP_BEAT, CLIP_TIME));onCc(({ beat }) => console.log("cc", beat, CLIP_BEAT, CLIP_TIME));実行順とコンソール出力は次のようになります(CLIP_LENGTH = 1、ccStepsPerBeat = 4):
| # | 処理 | コンソール |
|---|---|---|
| 1 | トップレベル | top-level |
| 2 | onInit(CLIP_BEAT=0) | init 0 0 |
| 3 | onBeat(beat=0) | beat 0 0 0 |
| 4 | onBeat(beat=0.25) | beat 0.25 0.25 0.125 |
| 5 | onBeat(beat=0.5) | beat 0.5 0.5 0.25 |
| 6 | onBeat(beat=0.75) | beat 0.75 0.75 0.375 |
| 7 | onNote(C4=60, startBeat=0.5) | note 60 0.5 0.25 |
| 8 | onCc(beat=0) | cc 0 0 0 |
| 9 | onCc(beat=0.25) | cc 0.25 0.25 0.125 |
| 10 | onCc(beat=0.5) | cc 0.5 0.5 0.25 |
| 11 | onCc(beat=0.75) | cc 0.75 0.75 0.375 |
エラーが起きたら何が残るか
Section titled “エラーが起きたら何が残るか”各フェーズは try/catch で 個別に 捕捉されています。エラーが起きたフェーズは中断されますが、それまでに setNote / setCc で出した結果は捨てられません。
| プレフィックス | どこで | 部分結果は残るか |
|---|---|---|
Compile error: ... | new Function() のコンパイル | 残らない(実行前) |
Runtime error (top-level): ... | トップレベル実行中 | 残らない(フェーズ未到達) |
Runtime error (onInit): ... | onInit 内 | onInit でそれまでに setNote した分は残る |
Runtime error (onBeat): ... | onBeat 内 | onInit + onBeat でそれまでに出した分は残る |
Runtime error (onNote): ... | onNote 内 | onInit + onBeat + onNote でそれまでに出した分は残る |
Runtime error (onCc): ... | onCc 内 | 全フェーズの累積結果が残る |
ただし Render タブには反映されません — エラー時は前回成功時の Render 結果がそのまま表示されたままになります。コンソールでエラーを確認してから再 Render してください。
再現性とデフォルトシード
Section titled “再現性とデフォルトシード”seededRandom() のシードは、明示しない限り次のハッシュから生成されます。
hash(code + JSON.stringify(events) + libSources.join(" "))つまり コード・元ノート・lib のいずれかが変わるとシードが変わる ため、同じ条件下では何度 Render しても同じ結果になります。
// このコードは毎回同じノートを置くseed("variation-A");onInit(() => { for (let i = 0; i < 8; i++) { setNote({ pitch: 60 + Math.floor(seededRandom() * 12), startBeat: i * 0.5, }); }});[!CAUTION]
factory.jsのrandom()/randomInt()/pick()/chance()/weighted()/shuffle()/urn()/drunk()はMath.random()ベース で、シードの影響を受けません。再現性が必要ならseededRandom()を直接使ってください。
再レンダー条件
Section titled “再レンダー条件”▶ Render ボタン(または Render タブの「再レンダー」)を押した時にだけ実行されます。元ノート・コード・lib が変わると Render タブにスタールバナーが出ます — 自動再実行はされません。
これは意図的な仕様です:
- 重い処理を書いてもタイピングごとに走らない
- エラーで Render が壊れても前回結果が残るので、編集を続けられる
- 同じコードでも再現性のある結果になる