コンテンツにスキップ

ライフサイクル

▶ 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 度上のハモリ

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_TIMEbeat / bpm * 60(秒)
登録ゼロ時ループ自体スキップ(コストゼロ)

クリップ長が 4 拍なら、onBeat16 回 呼ばれます(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 = 解像度。既定 16setCcResolution() で上書き可)
引数(ctx: { beat, time, phase })phase = beat / CLIP_LENGTH(クリップ全体で 1 周期の LFO に便利)
CLIP_BEATその beat と同じ
CLIP_TIMEbeat / 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_TIMECLIP_BEAT / bpm * 60フェーズごとに更新
CLIP_LENGTHクリップ長(ビート数)不変(コンパイル時固定)

[!CAUTION] onInitCLIP_BEAT を読んでも 常に 0 です。クリップ末尾の処理をしたければ onBeatb を見るか、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
2onInit(CLIP_BEAT=0)init 0 0
3onBeat(beat=0)beat 0 0 0
4onBeat(beat=0.25)beat 0.25 0.25 0.125
5onBeat(beat=0.5)beat 0.5 0.5 0.25
6onBeat(beat=0.75)beat 0.75 0.75 0.375
7onNote(C4=60, startBeat=0.5)note 60 0.5 0.25
8onCc(beat=0)cc 0 0 0
9onCc(beat=0.25)cc 0.25 0.25 0.125
10onCc(beat=0.5)cc 0.5 0.5 0.25
11onCc(beat=0.75)cc 0.75 0.75 0.375

各フェーズは 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 してください。

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.jsrandom() / randomInt() / pick() / chance() / weighted() / shuffle() / urn() / drunk()Math.random() ベース で、シードの影響を受けません。再現性が必要なら seededRandom() を直接使ってください

▶ Render ボタン(または Render タブの「再レンダー」)を押した時にだけ実行されます。元ノート・コード・lib が変わると Render タブにスタールバナーが出ます — 自動再実行はされません

これは意図的な仕様です:

  • 重い処理を書いてもタイピングごとに走らない
  • エラーで Render が壊れても前回結果が残るので、編集を続けられる
  • 同じコードでも再現性のある結果になる