地図/信号/ノイズとゆらぎ

確率で音を選ぶ — スケールに縛る

一様乱数で音の高さを直に決めると、半音だらけの羅列になって音程の塊が散らばる。二つの縛りを入れると音符の並びになる。使う音をスケール(音階)の中だけに限るのが一つ、どの音を選ぶかに確率の重みをつけるのがもう一つ。

スケール(音階)。1オクターブ12半音から特定の音だけを選んだ集合。ペンタトニックは5音で、隣り合う音の間隔が広く、どの二音を重ねても不協和になりにくい。重み付き抽選は、各候補に選ばれやすさを与え、その重みを足し上げた累積分布の上に乱数を落とす。累積の区間幅が確率に比例するので、重い候補ほど当たりやすい。乱数を分布へ写すこの手口が逆関数法(inverse transform sampling)。

スケールに縛るほうだけ入れる。scale に5音のペンタトニックを半音オフセットで持ち、一様乱数でそのどれかを選ぶ。scale[(Math.random() * 5) | 0] で、12 半音のうち5つだけを使う。横が時間、縦が音の高さ。

薄い横線がスケールの段で、音符は必ずそのどれかに乗る。一様乱数で選んでいても、外れた半音は出てこない。5音が等確率なので、どの音も同じ頻度で並ぶ。各音に重みをつけると頻度が偏る。重みを足し上げた累積分布を作り、Math.random がどの区間に落ちたかで音を決めると、重い音ほど広い区間を取って当たる。

// 重み付き抽選: 累積を作り、乱数がどの区間に落ちたかで選ぶ
const weights = [5, 2, 3, 4, 2] // 主音(0)と5度(7)を重めに
const cum = []
let acc = 0
for (const x of weights) {
  acc += x
  cum.push(acc)
}
const pick = () => {
  const r = Math.random() * acc
  for (let i = 0; i < cum.length; i++) if (r < cum[i]) return i
  return cum.length - 1
}

重み付き抽選に、たまに1オクターブ上へ跳ばす遊びと Math.random() < restProb で引く休符を足す。休符は音を置かずに間を作る。下が全部入り。

重みを変えると、ある音が頻繁に顔を出したり引っ込んだりして、並びの偏りが変わる。weights[5, 2, 3, 4, 2] は主音と5度を重くしているので、その二音が多く出る。休符を混ぜると音が詰まらず、間の空いた並びになる。一様乱数で散っていた音の高さが、スケールと重みの二つの縛りでメロディの形に寄る。Math.random を種から決まる rng() に差し替えれば、同じ種から同じ並びが何度でも出る。