音源素材ゼロ。numpyのsinとノイズだけで27秒のBGMを作ってみた

 Python 

 numpy 

作成日:2026/11/13

はじめに

宇宙を飛んでいて、カウントダウンのあとにワープして、どこか別の場所に着く。そんなイメージの短い曲が欲しくなりました。

静かに始まって、だんだん高まって、「5、4、3、2、1」で「ドン!」と鳴り、シュワーッとワープして到着する、という流れです。

フリーのBGMを探してもよかったのですが、「ここで盛り上がる」「ここでドン」という秒数まで思ったとおりの曲は、なかなか見つかりません。

それなら、音を全部コードで書いてしまおう、と試してみたら、思ったよりいい感じになりました。

できたのがこちらです。音が出るので注意してください。

🎧 完成したBGM(27秒)音源素材は0個。全部 numpy の計算結果です
完成した27秒のBGMの波形。しずか、高まる、カウントダウン、ドン、ワープ、到着の6つの場面に色分けされている
完成した曲の波形。右に行くほど音が大きくなり、19秒で「ドン!」(図はタップで大きく開きます)
0個音源素材
167行Pythonのコード
約2秒書き出しにかかる時間
2つ使ったもの(numpy と wave)
💡
この記事で分かること ・音を「数字の列」として作る、いちばん小さいコード ・サイン波から、パッド・ベル・キック・スネア・「シュワー」という盛り上げ音を作る方法 ・「19秒でドン」のように、決めた秒数どおりに曲を組み立てるやり方と、気をつけたところ

コードは Claude に書いてもらい、自分は聴いて「もっと『これから始まる』ワクワクが高まる感じに」と注文を付ける係でした。この記事では、そのコードの中身を1つずつ見ていきます。

全体の流れ

やっていることは、最初から最後までこれだけです。

🎼 音の高さドレミ → Hz
▶
〰️ 波を作るsin とノイズ
▶
📈 形をつけるエンベロープ
▶
➕ 足し合わせる秒数を決めて置く
▶
🔁 仕上げエコー・tanh
▶
💾 WAVに保存wave モジュール

シンセサイザーのライブラリも、音声処理のライブラリも使いません。配列を作って、足して、掛けるだけです。

音は、ただの数字の列

スピーカーは、渡された数字のとおりに前後に動きます。その数字を1秒に44,100個ならべたものが、音のデータです。

「ラ」の音(440Hz)なら、1秒に440回ゆれるサイン波を計算すれば終わりです。

import numpy as np

SR = 44100                       # 1秒あたりの数字の個数
t = np.arange(int(SR * 1.5)) / SR   # 0秒〜1.5秒の「時刻」の配列
x = np.sin(2 * np.pi * 440 * t)     # 440Hz のサイン波
440Hzのサイン波を5ミリ秒ぶん拡大した図。なめらかな波の上に、1個ずつのサンプルが点で並んでいる
最初の5ミリ秒を拡大。点が220個ある
🔊 440Hzのサイン波時報の「ポーン」のような、まじりけのない音
ことば意味この記事での値
サンプリングレート1秒に何個の数字で音を表すか44,100個(CDと同じ)
周波数(Hz)1秒に何回ゆれるか。大きいほど高い音45Hz〜1万Hzくらい
振幅ゆれの大きさ。大きいほど大きな音−1〜1

ドレミを周波数に変える

曲を書くときに「329.63Hz」のように考えるのは大変です。そこで、鍵盤の番号(MIDIノート番号)から周波数を出す関数を1つ用意します。

def hz(m):
    return 440.0 * 2 ** ((m - 69) / 12)

69番が440Hzの「ラ」で、番号が1つ上がると半音、12上がると1オクターブ(周波数が2倍)です。

番号音周波数
45ラ(低い)110.0Hz
57ラ220.0Hz
60ド261.6Hz
64ミ329.6Hz
69ラ440.0Hz
81ラ(高い)880.0Hz

これで、和音は  [45, 57, 60, 64, 71]  のような数字のリストで書けるようになります。

倍音を足して、音色を作る

サイン波だけだと、きれいですが味気ない音です。

楽器の音には、もとの高さの2倍、3倍、4倍……の高さの音(倍音)が少しずつ混ざっています。その混ぜ方が、音色の違いになります。

f = 220
x = (np.sin(2 * np.pi * f * t)
     + 0.5  * np.sin(2 * np.pi * 2 * f * t)
     + 0.25 * np.sin(2 * np.pi * 3 * f * t)
     + 0.12 * np.sin(2 * np.pi * 4 * f * t))
220Hzのサイン波に、2倍、3倍、4倍の高さのサイン波を足していくと、のこぎりのようなギザギザした波形になることを示す図
4本のサイン波を足すと、のこぎりのような形になる
🔊 サイン波だけ → 倍音を足した音前半と後半で、同じ高さなのに明るさが違う

今回の曲では、倍音の量を  bright  という引数にして、曲が進むほど大きくしています。同じ和音でも、後半ほど明るく、前に出てくる音になります。

少しずらして重ねると、厚くなる

もう1つ、音を「それっぽく」する定番があります。同じ音を、高さをほんの少しだけずらして重ねることです。

for det in (-0.004, 0.0, 0.005):          # −0.4%、0%、+0.5%
    ph = 2 * np.pi * f * (1 + det) * tt + rng.uniform(0, 6.28)
    out += np.sin(ph)
サイン波1本は音量が一定だが、わずかに高さの違う3本を重ねると音量がゆっくり波打つことを示す図
1本だと一定。3本重ねると、ゆっくりうねる
🔊 1本だけ → 3本重ね後半は、音がゆらゆらと広がって聞こえる

何人かで同じ音を歌うと、ぴったり同じ高さにはならず、それが合唱らしい厚みになります。それと同じことを計算でやっています。

エンベロープで、楽器の「形」を決める

ここまでの音は、鳴り始めから終わりまで同じ大きさでした。

実際の楽器は、ピアノなら「ポーン」と鳴ってすぐ小さくなり、弦楽器なら「ふわー」と立ち上がります。この音量の変化の形を、エンベロープと呼びます。

作り方はかんたんで、波に「0〜1の間で変化する配列」を掛けるだけです。

def ss(a, b, x):
    """a で 0、b で 1 になる、なめらかな坂"""
    u = np.clip((x - a) / (b - a), 0, 1)
    return u * u * (3 - 2 * u)

# パッド:1.5秒かけて立ち上がり、2秒かけて消える
env = ss(0, att, tt) * (1 - ss(dur, dur + rel, tt))

# プラック:一瞬で鳴って、すっと消える
env = np.exp(-tt * 6) * ss(0, 0.004, tt)
パッドはゆっくり立ち上がりゆっくり消える台形のエンベロープ、プラックは一瞬で最大になり指数的に消えるエンベロープを持つことを示す図
黄色い線がエンベロープ。中の波はどちらもサイン波の足し算
🔊 パッド(和音)うしろでふわっと鳴り続ける役
🔊 プラック(アルペジオ)ベルのような「ポロポロ」。メロディ役

プラックのほうは、倍音にも別々のエンベロープを掛けています。高い倍音ほど早く消えるようにすると、「カーン」と鳴った直後だけ明るく、あとは丸い音が残って、ベルらしくなります。

出だしのほんの少しのフェードイン

プラックの式の最後に付いている  ss(0, 0.004, tt)  は、4ミリ秒だけのフェードインです。

これがないと、音がいきなり高い位置から始まって、「プチッ」というノイズが乗ります。

音がいきなり高い値から始まる波形と、4ミリ秒かけてゼロからなめらかに立ち上がる波形の比較
左はスピーカーが一瞬で飛び出すことになる。右はゼロから動き出す

ドラムも、sin とノイズでできる

キック

バスドラムの「ドン」は、高い音から低い音へ一瞬ですべり落ちるサイン波です。

def kick(amp=1.0):
    n = int(0.45 * SR); tt = np.arange(n) / SR
    f = 45 + 110 * np.exp(-tt * 28)                 # 155Hz → 45Hz
    return amp * np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-tt * 9)
キックの周波数が155Hzから45Hzへ急に下がるグラフと、その結果の波形。右側にはノイズと190Hzのサイン波でできたスネアの波形
左:キックの高さの変化と波形 右:スネアの波形
🔊 キック4回 → キック+スネアどちらも sin と乱数だけ

高さが変わる音は、cumsum で作る

キックのコードで大事なのは  np.cumsum(f) / SR  のところです。

高さが変わらない音なら  np.sin(2π * f * t)  で作れます。でも、f が時間で変わるときに同じ書き方をすると、ねらった高さになりません。

周波数を積み上げて位相を作った場合はねらいどおり45Hzに落ち着くが、f×tをそのまま使うと30Hzあたりまで下がりすぎることを示すグラフ
緑がねらった高さ。ピンクは f * t をそのまま sin に入れたときに実際に鳴る高さ

サイン波の中身(位相)は「これまでに何回ゆれたか」を表しています。高さが変わるなら、その瞬間ごとの周波数を、最初から順に足していく必要があります。それをやってくれるのが  np.cumsum (累積和)です。

あとで出てくる「上がっていく音」も、同じ方法で作っています。

スネア

スネアの「タン」は、乱数で作ったホワイトノイズから低い成分を引いたものに、190Hzの短いサイン波を足しています。

def snare(amp=1.0):
    n = int(0.16 * SR); tt = np.arange(n) / SR
    nz = rng.standard_normal(n); nz = nz - lp(nz, 1800)     # 高い成分だけ残す
    return amp * (nz * 0.6 * np.exp(-tt * 30)
                  + 0.4 * np.sin(2 * np.pi * 190 * tt) * np.exp(-tt * 40))

ここで使っている  lp  は、次に出てくるローパスフィルターです。

盛り上げ役は、ノイズのフタを開けていく

サビ前の「シュワーーー」と上がっていく音(ライザー)は、ホワイトノイズにローパスフィルターを掛けて作ります。

ローパスフィルターは、ある高さより上の音を通さない「フタ」のようなものです。いちばん単純なものは、1行の式を繰り返すだけで書けます。

def lp(x, cutoff):
    """cutoff より高い音を弱める。cutoff は配列でもよい"""
    cutoff = np.broadcast_to(np.asarray(cutoff, dtype=float), x.shape)
    a = 1 - np.exp(-2 * np.pi * cutoff / SR)
    y = np.empty_like(x); s = 0.0
    for i in range(len(x)):
        s += a[i] * (x[i] - s)      # 前の値に、少しだけ近づく
        y[i] = s
    return y

def noise_sweep(dur, f0, f1, shape=2.0):
    n = int(dur * SR); tt = np.arange(n) / SR; u = tt / dur
    cut = f0 * (f1 / f0) ** u                      # フタの位置を f0 → f1 へ
    return lp(rng.standard_normal(n), cut) * u ** shape

フタの位置を300Hzから9,000Hzへ少しずつ上げていくと、こもった「ゴー」が、だんだん明るい「シャー」に変わっていきます。

ライザーのスペクトログラム。時間が進むにつれてノイズが高い周波数まで広がり、その中を1本のサイン波が上がっていく
ノイズが上のほうへ広がっていく。斜めの線は、一緒に鳴らしている「上がっていくサイン波」
🔊 ライザー「何かが始まりそう」という気分は、ほぼこの音が作っている

部品を並べて、曲にする

部品がそろったら、あとは「何秒に、何を、どのくらいの音量で置くか」を決めるだけです。

長さ27秒ぶんの空の配列を左右2本用意して、そこに足していきます。

N = int(SR * 27.0)
L = np.zeros(N); R = np.zeros(N)

def add(sig, start, pan=0.0, gain=1.0):
    i = int(start * SR)
    n = min(len(sig), N - i)
    L[i:i+n] += sig[:n] * gain * (1 - max(pan, 0))
    R[i:i+n] += sig[:n] * gain * (1 + min(pan, 0))

add(kick(0.9), 14.0, gain=0.55)     # 14.0秒にキックを置く

 pan  は左右の位置です。プラックの音を1つずつ左右に振ると、音が頭のまわりを飛び回るように聞こえます。

27秒の設計図

最初に、「19秒でドン!、22秒からワープ、24.45秒で到着」という筋書きを決めました。音は、その秒数に合わせて置いていきます。

0秒914192227秒
しずか0〜9秒パッド+まばらなベル
高まる9〜14秒ベース・ライザー
カウントダウン14〜19秒毎秒キック
ドン!19〜22秒いちばんの山
ワープ22〜24.45秒上がっていく音
到着24.45〜27秒2回目のドン
時間場面鳴っているものねらい
0〜9秒しずかパッド、まばらなベル待っている時間。じゃまをしない
9〜14秒高まるベル2倍、ベース、ライザー開始「そろそろかな」
14〜19秒カウントダウン毎秒のキックと「ピッ」、加速するスネア5、4、3、2、1
19〜22秒ドン!低い「ドン」、明るい和音、きらきらいちばんの山
22〜24.45秒ワープノイズと、70Hzから一気に上がる音場面が切り替わる
24.45〜27秒到着2回目の「ドン」、別の和音新しい場所に着いた

和音で「空気」を変える

和音の進み方も、数字のリストで書いてあります。

秒和音番号のリスト感じ
0Am945, 57, 60, 64, 71少しさびしい、宇宙っぽい
4.5F41, 53, 57, 60, 67ふわっと広がる
9C48, 55, 60, 64, 67明るくなり始める
11.5G43, 55, 59, 62, 69前に進む
14Am9 → F(くり返し)緊張を保つ
19A(メジャー)45, 57, 61, 64, 69 …ぱっと晴れる(ドン!)
24.45Dm38, 50, 57, 62, 65, 69別の世界に着いた

ずっとマイナー(暗め)の和音で引っぱっておいて、「ドン!」の瞬間にだけメジャー(明るい)に変えています。60番(ド)を61番(ド♯)に、1つ上げただけです。それだけで、聴いたときの「ぱっと開けた」感じが出ます。

「ドン」の直前に、一瞬だけ無音を入れる

もう1つ効いているのが、「ドン!」の直前の0.1秒ほどの無音です。

gate = 1 - ss(18.84, 18.87, t) * (1 - ss(18.985, 19.0, t))
L *= gate; R *= gate

盛り上がりきったところで音を全部止めて、すぐに「ドン」。ジャンプの前に一度しゃがむようなものです。上の波形の図でも、19秒の手前に細い切れ込みが見えます。

できあがりを絵で見る

完成した曲を、スペクトログラム(横が時間、縦が音の高さ)にしたものです。

完成した曲のスペクトログラム。前半は横線状の和音とベルだけで、9秒から低音とノイズが加わり、19秒と24.45秒に縦の強い線が入っている
横線=和音やベル 下の明るい帯=ベースとキック 上に広がるもや=ライザー 縦線=「ドン」

設計図の表が、そのまま絵になっています。9秒から下のほうが急ににぎやかになり、上のほうにもやが広がって、19秒でぷつっと切れてから縦に1本の線が入ります。

音を聴きながらこの絵を見ると、「いま鳴っているのはこの線か」と分かっておもしろいです。

仕上げ:エコーと tanh

左右に跳ね返るエコー

エコーは、「少し前の自分を、小さくして足す」だけです。配列をずらして足します。

ここでは、左の音を右に、右の音を左に返しています。音が左右に跳ね返るように聞こえるので、ピンポンディレイと呼ばれます。

d = int(0.375 * SR)                       # 0.375秒 遅らせる
Le = L.copy(); Re = R.copy()
Le[d:] += 0.22 * R[:-d]                   # 右の音が、遅れて左から
Re[d:] += 0.22 * L[:-d]                   # 左の音が、遅れて右から

d2 = int(0.75 * SR)                       # さらに 0.75秒後に、同じ側へ小さく
Le[d2:] += 0.10 * L[:-d2]
Re[d2:] += 0.10 * R[:-d2]
◀ 左
右 ▶
♪ もとの音
♪ 0.375秒後(22%)
♪ 0.75秒後(10%)
左で鳴った音が、小さくなりながら右へ、また左へ

0.375秒という数字は、ベルを鳴らす間隔(0.25秒)の1.5倍です。エコーが、もとの音と音の間にちょうど入るので、音の数が増えたように聞こえます。

tanh で音量をまるく収める

たくさんの音を足していくと、合計が1を超えます。そのまま保存すると、はみ出した部分が切り取られて「バリバリ」という音になります。

そこで最後に、全体を  np.tanh  に通します。

M = np.stack([Le, Re], 1)
M = np.tanh(M * 1.5 / np.max(np.abs(M))) * 0.89
入力と出力の関係を示すグラフ。tanhは小さい入力ではほぼ直線で、大きい入力ではなめらかに上限へ近づく
小さい音はほぼそのまま、大きい音だけがなめらかに押さえられる

小さい音が相対的に持ち上がるので、全体の音が詰まって、聞こえる音量も上がります。

WAV に保存する

保存は、Python に最初から入っている  wave  モジュールで足ります。−1〜1 の数字を、16ビットの整数(−32767〜32767)に直して書き込みます。

import wave

with wave.open('out.wav', 'wb') as w:
    w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR)
    w.writeframes((M * 32767).astype('<i2').tobytes())

やってみて分かったこと

内容
👍 秒数がぴったり合う「19.0秒にドン」と書けば、19.0秒に鳴る。構成を変えたくなったら、数字を直すだけ
👍 何度でも同じ音が出る乱数の種を固定してあるので、実行するたびに同じ曲ができる
👍 直すのが速い「ベルをもう少し小さく」は数字を1つ変えて、2秒待つだけ
👍 持ち運びが楽素材ファイルがない。コード1つで曲が再現できる
⚠️ 耳で確かめるしかない数字が正しくても、聴いて気持ちいいかは別。何度も聴いて直すことになる
⚠️ 生楽器の音は苦手ピアノやギターのような音は、サイン波の足し算だけでは遠い。シンセらしい音に向いている
⚠️ 音量の足し算に注意部品ごとの  gain  を決めずに足すと、すぐ音が割れる

特に、位相を cumsum で作ること、出だしに数ミリ秒のフェードインを入れること、最後に tanh を通すこと。この3つは、知らないと「なんだか変な音」のまま原因が分からなくなりそうなところでした。

まとめ

音は、1秒に44,100個ならんだ数字。 np.sin  で作れる

倍音を足すと音色、少しずらして重ねると厚み、エンベロープを掛けると楽器らしさが出る

キックは「すべり落ちるサイン波」、スネアやライザーは「フィルターを掛けたノイズ」

曲は、空の配列に「何秒に何を置くか」を足していくだけ

仕上げは、ずらして足すエコーと、tanh

音楽の知識があまりなくても、「数字を1つ変えて、聴いて、また変える」をくり返すうちに、それらしい形になっていくのが楽しいところでした。

「こういう流れの曲が欲しい」というイメージがあるときは、探すかわりにコードで書いてみるのも、おもしろい選択肢だと思います。