コンテンツにスキップ

ヘルパー:数値・乱数・モジュレーション

リズム・音楽理論以外のヘルパーをまとめます。数値ユーティリティ、配列操作、乱数、LFO/envelope、チューニング変換、ステートフルなジェネレータ。

v[min, max] の範囲に切り詰めます。

clamp(150, 0, 127); // 127
clamp(-5, 0, 127); // 0
clamp(60, 0, 127); // 60

実用例: ピッチや velocity を MIDI 範囲に収める。

onNote((n) => {
setNote({ ...n, pitch: clamp(n.pitch + 7, 0, 127) });
});

モジュラに [min, max) に折り返します(max は含まない)。clamp と違って 値を保ったまま範囲内に収める

wrap(7, 0, 5); // 2
wrap(-1, 0, 5); // 4
wrap(135, 0, 128); // 7

実用例: ピッチをオクターブ内に折り畳む。

onNote((n) => {
setNote({ ...n, pitch: wrap(n.pitch, 60, 72) }); // C4 オクターブ内に収める
});

線形補間。t = 0at = 1b

lerp(0, 100, 0.5); // 50
lerp(60, 72, 0.25); // 63

v[ai, bi] から [ao, bo] に線形リマップ。音楽プログラミングの定番。

// 0..1 の正規化値を CC 範囲(0..127)へ
mapRange(0.5, 0, 1, 0, 127); // 63.5
// velocity 60..120 を表現 0.4..1.0 に
mapRange(80, 60, 120, 0.4, 1.0); // 0.6

実用例: 元 velocity を CC1(modulation)に。

onNote((n) => {
setNote({ ...n });
setCc({
ccNumber: 1,
beat: n.startBeat,
value: mapRange(n.velocity, 0, 127, 30, 100),
});
});
関数シグネチャ内容
range(start, end, step = 1)等差数列。step 負も可range(0, 4) → [0,1,2,3]
rotate(arr, n)左回転(負で右回転)。新配列rotate([1,2,3,4], 1) → [2,3,4,1]
rev(arr)反転コピーrev([1,2,3]) → [3,2,1]
interleave(...arrays)ジッパーマージinterleave([1,2],[3,4]) → [1,3,2,4]
shuffle(arr)Fisher-Yates シャッフル(コピー)(要:再現性なし)
unique(arr)順序保存で重複除去unique([1,2,1,3]) → [1,2,3]
thin(arr, n)n 個飛ばしで間引くthin([1,2,3,4,5], 2) → [1,3,5]

実用例

// オクターブ違いをジッパーで混ぜる
const lo = [60, 62, 64, 65];
const hi = transpose(lo, 12);
const mixed = interleave(lo, hi); // [60, 72, 62, 74, 64, 76, 65, 77]
mixed.forEach((p, i) => setNote({ pitch: p, startBeat: i * 0.25 }));
// スケールを逆順に並べてからローテーション
const sc = rev(scale("minor", 60));
const rot = rotate(sc, 2);

ここの関数群はすべて Math.random() ベース で、シードの影響を受けません。再現性が必要な場合は seededRandom() を直接使ってください。

random(); // [0, 1) の浮動小数点
randomInt(0, 11); // 0..11 の整数(両端含む)

配列から 1 つ選ぶ。空配列は例外

const sc = scale("major", 60);
onBeat((b) => {
if (chance(0.3))
setNote({ pitch: pick(sc), startBeat: b, lengthBeats: 0.25 });
});

確率 p(0..1)で true。範囲外は自動でクランプ。

if (chance(0.5)) setNote({ pitch: 60 }); // コインフリップ
if (chance(0.1)) setNote({ pitch: 84 }); // 10% の確率で高音

重み付きランダムピック。重みの合計で正規化 されるので相対値で OK。

// 50% / 33% / 17%
const note = weighted([60, 64, 67], [3, 2, 1]);
// 拍ごとに違う確率分布で音を選ぶ
onBeat((b) => {
const p = weighted([36, 38, 42], [4, 1, 8]); // ハイハットが多め
setNote({ pitch: p, startBeat: b, lengthBeats: 0.1 });
});

[!CAUTION] 重みが全部 0 だと一様分布にフォールバックします。重みの長さが items と違うと例外。

正規化位相 phase(0..1 の小数。範囲外は内部で自動折り返し)に対する LFO 波形値。戻り値は -1..1

shape波形
"sin" / "sine"サイン波
"tri" / "triangle"三角波
"saw"のこぎり波(-1 → 1)
"square" / "sq"矩形波(-1 か 1)

すべての波形は phase=0 と phase=0.5 でゼロクロスする設計なので、クロスフェード可能(位相がずれない)。

最小例

lfo(0, "sin"); // 0
lfo(0.25, "sin"); // 1
lfo(0.5, "sin"); // 0
lfo(0.75, "sin"); // -1

実用例:クリップ全体で 1 周期の正弦波で velocity を揺らす

onNote((n) => {
const phase = n.startBeat / CLIP_LENGTH; // 0..1
setNote({ ...n, velocity: 90 + 20 * lfo(phase, "sin") });
});

実用例:CC1 を 2 Hz で揺らす(120 BPM 想定)

onBeat((b) => {
// b は beat 単位なので、bpm 120 だと b=2 で 1 秒
// 2 Hz = 1 秒に 2 周期 = beat に対して 4 周期/拍
setCc({ ccNumber: 1, beat: b, value: 64 + 32 * lfo(b * 4, "sin") });
});

時間-値ペアの配列 [[t0, v0], [t1, v1], ...] を時間でソートし、t における 線形補間値 を返します。範囲外は端点に張り付き。

const env = [
[0, 0],
[2, 100],
[4, 60],
];
envelope(env, 0); // 0
envelope(env, 1); // 50 (0 → 100 の中間)
envelope(env, 3); // 80 (100 → 60 の中間)
envelope(env, 5); // 60 (範囲外、最終点に張り付き)

実用例:4 拍クリップの ADSR

const adsr = [
[0, 0],
[0.5, 100],
[1, 80],
[3, 80],
[4, 0],
];
onBeat((b) => {
setCc({ ccNumber: 11, beat: b, value: envelope(adsr, b) });
});

[!TIP] 点は時間順でなくても OK — 内部でソートされます。ただし [t1, v1]t1 同士が同じ値だと、後ろの v に張り付くので注意。

MIDI ピッチ → 周波数(Hz)。mtof(69) = 440(A4)。

mtof(69); // 440
mtof(60); // 261.625... (C4)
mtof(72); // 523.251... (C5)

実用例: 周波数情報を console.log でデバッグ表示。

onNote((n) => {
console.log(n.pitch, "Hz:", mtof(n.pitch).toFixed(2));
});

周波数 → MIDI ピッチ(小数。整数化したいなら Math.round)。

ftom(440); // 69
ftom(442); // 69.078... (チューニング微調整想定)
Math.round(ftom(437)); // 69 (A4 にスナップ)

[!CAUTION] ftom(0) 以下を渡すと -Infinity を返します。

ステートフルなジェネレータ(Max 系)

Section titled “ステートフルなジェネレータ(Max 系)”

これらは呼び出すと クロージャを返します。各クロージャは独立した内部状態を持つので、複数並列で使えます。

[0, n)非反復ランダム整数。すべての値を 1 度ずつ返したら再シャッフル。Max の urn 相当。

いつ使うか: 「12 音すべてを 1 度ずつ使い切ってから繰り返す」のように、一様性を保ちつつランダムにしたい とき。

const u = urn(7);
const sc = scale("minor", 60);
onBeat((b) => {
if (Number.isInteger(b * 2)) {
// 8 分ごと
setNote({ pitch: sc[u()], startBeat: b, lengthBeats: 0.5 });
}
});

整数の 有界ランダムウォーク。各呼び出しで [-step, step] の整数差分を加算し [min, max] にクランプ。Max の drunk 相当 — 「滑らかなランダム」 の代名詞。

const d = drunk(60, 2, 48, 84); // 中央 60、最大 ±2、48..84 範囲
onBeat((b) => {
setNote({ pitch: d(), startBeat: b, lengthBeats: 0.25 });
});

[!TIP] step を大きくすると暴れた線、小さくすると緩やかな線になります。step=1 だと半音単位で這う。

counter(start, end, stepBy = 1, wrap = true)

Section titled “counter(start, end, stepBy = 1, wrap = true)”

モジュラカウンタ。現在値を返してから stepBy だけ進む。end を超えたら wrap=truestart へ巻き戻し、false で範囲内最終値に張り付き。

いつ使うか: 周期的な分散和音、決定的なステップ進行。

const c = counter(0, 4); // 0, 1, 2, 3, 0, 1, 2, 3, ...
const ch = chord("maj7", 60);
onBeat((b) => {
if (Number.isInteger(b * 2)) {
setNote({ pitch: ch[c()], startBeat: b, lengthBeats: 0.5 });
}
});
// stepBy=2 で 2 つ飛ばし、wrap=false で末端張り付き
const c = counter(0, 8, 2, false); // 0, 2, 4, 6, 6, 6, ...