▍AI Dev Lab
RainLull

RainLull ができるまで — 雨音を 4 レイヤーで合成する

雨音ヒーリングサウンドの開発ログ。ノイズのレイヤー合成、しずく音の先読みスケジューラ、procedural な遠雷、畳み込み残響をWeb Audioで重ねた設計を記録します。

雨の音で眠りたい、集中したい——という需要は大きく、アプリもたくさんあります。ただ、その多くは録音した雨の音をループ再生しているだけです。

録音ループには 2 つの弱点があります。ひとつは、数十秒〜数分で一巡するので、集中して聴くと「あ、またここだ」と繰り返しに気づいてしまうこと。もうひとつは、音源ファイルが数 MB〜数十 MB あって、読み込みが重いこと。

RainLull は、雨の音を録音ではなく その場で合成します。ループの継ぎ目がなく、無限に続き、音源ファイルは 1 バイトもダウンロードしません。全部ブラウザの Web Audio が生成しています。


#🎯 なぜ合成にこだわったのか

「雨の音なんて録音で十分では?」と言われそうですが、合成にする理由は明確です。

  • 継ぎ目がない — 乱数から作るので、同じ瞬間は二度と来ない。何時間流しても繰り返しに気づかない
  • 軽い — 音源ファイルがないので、ページを開いた瞬間に鳴らせる
  • 雨脚を自在に変えられる — 小雨から土砂降りまで、パラメータを動かすだけで連続的に変わる

録音だと「小雨の音源」「本降りの音源」と別々に用意することになりますが、合成なら 1 つの仕組みで全部の強さを作れます。


#🏗️ 雨を「層」で組み立てる

現実の雨の音を分解すると、いくつかの成分が重なっています。RainLull はそれをレイヤーとして別々に作り、混ぜています。

bed … 雨脚そのもの (ピンクノイズ)
fine … きらめく高音の細かさ (白ノイズ + ハイパス)
rumble … 遠くの低いゴーッという轟き (ブラウンノイズ + ローパス)
droplets… 個別の「ぴちょん」という雫
thunder … 遠雷
reverb … 全体を包む残響

面白いのは、ノイズの「色」を使い分けているところです。

ts
if (type === "white") {
 for (let i = 0; i < length; i++) data[i] = Math.random * 2 - 1
} else if (type === "pink") {
 // Voss-McCartney 近似で 1/f 特性を作る

 let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0
 for (let i = 0; i < length; i++) {
 const w = Math.random * 2 - 1
 b0 = 0.99886 * b0 + w * 0.0555179
 // ... 複数の係数で低域を持ち上げる

 const pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362
 data[i] = Math.max(-1, Math.min(1, pink * 0.11))
 }
}
  • 白ノイズ は全周波数が均等で、シャーッという硬い音。細かいきらめき(fine 層)に使う
  • ピンクノイズ は低いほど強い自然な分布で、雨脚(bed 層)の芯になる
  • ブラウンノイズ はさらに低域寄りで、遠くの轟き(rumble 層)を作る

自然界の音の多くはピンクノイズに近い分布を持つと言われていて、雨の「地の音」をピンクノイズにしたのはそれが理由です。白ノイズだけだと、雨というより砂嵐みたいな耳障りな音になります。


#💧 「ぴちょん」を一粒ずつ作る

雨の質感を決めるのは、実は地の音より個別の雫です。RainLull は雫を録音ではなく、1 粒ずつ合成しています。しかも、シーンによって雫の作り方そのものを変えています。

ts
// patter = 普通 の 雨だれ (柔らかい ノイズ tick)

// glass = ガラス を 弾く 硬く 高い 短い click

// plop = 水面 の ぽちゃ (上昇 する 共鳴 bloop + 飛沫、 残響 たっぷり)

// metal = トタン の 叩きつけ (鋭い transient + 高 Q 金属 リング 2 倍音)

// thud = 傘 / テント の 鈍い こもった bot (低音 ローパス、 飛沫 なし)

// leaf = 森 の 葉 を 打つ 柔らかい 中音

同じ「雨」でも、当たる対象で音はまったく違います。ガラス窓なら硬い高音、傘なら鈍い低音、水面ならぽちゃんと跳ねる残響。これを「雫の芯の周波数」「共鳴の Q 値」「減衰時間」「飛沫ノイズの量」の組み合わせで作り分けています。

各シーンは、これらのパラメータの組として定義されています。

ts
{
 id: "window", jp: "夜の窓辺",
 dropType: "glass",
 dropPitchMin: ..., dropPitchMax: ..., // 芯の周波数の幅

 dropQ: ..., // 共鳴の鋭さ

 dropDecay: ..., // 余韻の長さ

 reverb: ..., // 残響

}

「静かな小雨」「本降り」「傘」「雷雨」「森の雨」「水面」「トタン屋根」「テント泊」「土砂降り」——11 のシーンは、この雫の作り方と各層のバランスの違いで生まれています。


#⏱️ 雫のタイミング:先読みスケジューリング

ここが Web Audio で一番の技術的ポイントです。

雫を「鳴らしたい瞬間に setTimeout で鳴らす」と、必ずガタつきます。JavaScript のタイマーは数十 ms 単位でずれるので、雨だれのような細かい音では、そのズレが不規則なもたつきとして聞こえてしまう。

そこで look-ahead スケジューリングを使っています。考え方はこうです。

  1. 定期的に「これから少し先まで」を見る
  2. その区間に鳴らすべき雫を、Web Audio の正確なタイムスタンプ付きで予約する
  3. 実際の発音は Web Audio のオーディオクロックが担当する

JavaScript は「いつ何を予約するか」だけを担当し、正確な発音タイミングはオーディオエンジンに任せる。この分業で、JS タイマーのブレとは無関係に、雫がサンプル精度で正しく鳴ります。密度(1 秒あたりの雫の数)を上げれば土砂降り、下げれば小雨になります。


#🌫️ 残響もファイルなしで作る

雨の音には、空間の広がり(残響)が欠かせません。普通、残響は「インパルス応答」という録音ファイルを畳み込んで作りますが、それだとまたファイルが必要になる。

RainLull は残響用のインパルス応答も合成しています。

ts
export function createImpulseResponse(ctx, seconds = 2.6, decay = 2.4): AudioBuffer {
 const rate = ctx.sampleRate
 const len = Math.max(1, Math.floor(rate * seconds))
 const ir = ctx.createBuffer(2, len, rate)
 for (let ch = 0; ch < 2; ch++) {
 const data = ir.getChannelData(ch)
 for (let i = 0; i < len; i++) {
 data[i] = (Math.random * 2 - 1) * Math.pow(1 - i / len, decay)
 }
 }
 return ir
}

ノイズを指数関数で減衰させただけのものを ConvolverNode に食わせています。物理的に正確な部屋の残響ではありませんが、雨を包む「空間っぽさ」を出すには十分で、しかもファイルが要らない。実用の質と「ゼロダウンロード」を両立させるための割り切りです。


#😓 苦労したところ

#ブラウザは勝手に音を鳴らせない

現代のブラウザは、ユーザーの操作なしに音を鳴らすことを禁じています(勝手に音が出る広告を防ぐため)。なので AudioContext は最初「停止」状態で作られ、再生ボタンのクリックで初めて起動する形にしています。ページを開いた瞬間に鳴らせないのは、この制約のためです。

#音の「大きすぎ」を防ぐ

複数のノイズ層と大量の雫を重ねると、ピークが積み上がって音が割れます。各層の生成時点で Math.max(-1, Math.min(1, ...)) とクリップを入れ、さらに全体のゲインを絞ってあります。合成音は「思ったより簡単に破綻する」ので、各段でレベルを抑えるのが地道に効きました。

#耳で確かめるしかない

これは正直に書いておきます。音は、コードが「動く」ことと「気持ちよく聞こえる」ことが別物です。パラメータが技術的に正しく反映されていても、実際に長時間聴くと耳障り、ということが起こる。数値のテストでは「鳴っている」ことしか確認できず、心地よさは実機で耳で聴くしかない。ここは自動化しきれない領域として残っています。


#🔭 今後の拡張

  • 雫の当たる対象のミックス — 「窓」と「傘」を同時に、のような合成
  • 風の層 — 雨に風のうなりを重ねる
  • イコライザー — 高音が苦手な人向けに全体のトーンを調整
  • プリセットの保存 — 自分好みに調整した雨を名前を付けて残す

#💡 このサービスから言えること

「録音してループ」で済ませていたものを、合成に置き換えると質が上がる。 継ぎ目のなさ、軽さ、連続的に変えられる自由度——これらは録音では原理的に得られないものです。雨の音のような「自然に揺らぎ続けるもの」ほど、合成の相性が良い。

技術的な核心は look-ahead スケジューリングでした。「JS タイマーは不正確だが、オーディオクロックは正確」という性質を理解して、役割を分ける。これは音を扱う Web アプリすべてに効く考え方で、メトロノームでも同じ手を使っています。

そして、音・映像といった感覚に訴えるものは、自動テストが通っても完成ではない。「鳴っている」と「心地よい」の間には、耳で確かめるしかない距離があります。数値で検証できる範囲と、人が確認するしかない範囲を分けて考えるようになったのは、このサービスからの一番の学びです。

[ ./next_action ]

読んだら、 RainLull を実際に動かす。

この開発ログは RainLull をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →