Wizard Notes

Pythonを使った音楽信号分析の技術録、作曲活動に関する雑記

Pythonでゲーム音楽(チップチューン)の基本波形を生成(サイン波,矩形波,のこぎり波,三角波,白色雑音)

シンセサイザ(オシレータ)の基本波形として一般的なサイン波,矩形波,のこぎり波,三角波,白色雑音は,ファミコンのBGMのようなゲーム音楽チップチューン)の作成や音信号処理のテストでよく利用されます.

サイン波は基本周波数(音高)をf_oとすると,

f:id:Kurene:20210924065437p:plain:w200

として簡単に書けますが,矩形波・ のこぎり波・三角波は若干面倒です

ただ,Pythonだと scipy.signal.sawtooth, scipy.signal.square を使えばサイン波と同様の形式でプログラミングできるので非常に楽です.

この記事では,Pythonでnumpy や scipy を使って基本波形5種を生成する方法を紹介します.

f:id:Kurene:20210924061351p:plain:w500

基本波形の生成

準備

まずはパラメタとして波形の音高長さサンプリング周波数を決めます.

長さ,サンプリング周波数が決まれば各サンプル点の時刻tが算出できます.

duration = 0.025 # 25 msec
sr = 44100 # サンプリング周波数
fo = 261.63 # 基本周波数(C4)

t  = np.arange(0, sr*duration) / sr # サンプル点の時間

サイン波 (正弦波)

Pythonでは numpy.sin を使って1行で生成できます.

x = np.sin(2*np.pi*fo*t)

矩形波 (方形波)

矩形波scipy.signal.square を使うことでサイン波と同様の形式で生成できます.

x = scipy.signal.square(2*np.pi*fo*t)

なおscipy.signal.squareは引数dutyデューティー比(パルス幅)を変更できます.

x = scipy.signal.square(2*np.pi*fo*t, duty=duty)

f:id:Kurene:20210924061021p:plain:w500

のこぎり波(鋸歯状)

のこぎり波も scipy.signal.sawtooth を使うことでサイン波と同様の形式で生成できます.

x = scipy.signal.sawtooth(2*np.pi*fo*t)

なお,引数widthのこぎり波の形状を変更することができます.

デフォルトはwidth=1.0です.

x = scipy.signal.sawtooth(2*np.pi*fo*t, width=width)

f:id:Kurene:20210924061333p:plain:w500

三角波

三角波は,scipy.signal.sawtoothwidth=0.5に相当します.

x = scipy.signal.sawtooth(2*np.pi*fo*t, width=0.5)

白色雑音 (ホワイトノイズ)

白色雑音は正規分布を発生させます.Pythonの場合は np.random.normal を使えば1行で書けます.

ただし,最大/最小値が制限されていないため,用途によっては正規化や値域の制限すべき場合があります.

正規化は平均パワー/RMSが所望の値になっているか,クリッピングする場合は波形が歪んで音質に影響していないかなどに注意が必要です.

# -1.0 ~ 1.0 に制限
# 方法1. 正規化
x = 2*(x-np.min(x))/(np.max(x)-np.min(x)) - 1.0

# 方法2. クリッピング
x[x<-1.0] = -1.0
x[x>=1.0] = 1.0
x = np.random.normal(0.0, 1.0, len(t))

まとめ

Pythonゲーム音楽や音信号処理で利用される基本波形(サイン波,矩形波,のこぎり波,三角波,白色雑音)を生成する方法を紹介しました.

なお,作成した音波形を聞いたりオーディオファイルとして書き出すのは,Pythonではpython-soundcardsoundfileといったライブラリを使えば簡単にできます.

詳しくは以下の記事をご覧ください.

www.wizard-notes.com

www.wizard-notes.com

プロット用コード

import sys
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal

def plot(x, c="c", label=""):
    plt.plot(x, c=c, label=label, alpha=0.8)
    plt.ylim(-1.5, 1.5)
    plt.legend(loc='upper right')
    plt.grid()


duration = 0.025 # 25 msec
sr = 44100 # サンプリング周波数
t  = np.arange(0, sr*duration) / sr # サンプル点の時間
fo = 261.63 # 基本周波数(C4)


# サイン波
x = np.sin(2*np.pi*fo*t)
plt.subplot(5,1,1)
plot(x, "c", "Sine wave")

# 矩形波
x = scipy.signal.square(2*np.pi*fo*t)
plt.subplot(5,1,2)
plot(x, "m", "Square wave")

# のこぎり波(鋸歯状)
x = scipy.signal.sawtooth(2*np.pi*fo*t)
plt.subplot(5,1,3)
plot(x, "g", "Sawtooth wave")

# 三角波
x = scipy.signal.sawtooth(2*np.pi*fo*t, width=0.5)
plt.subplot(5,1,4)
plot(x, "b", "Triangle wave")

# 白色雑音
x = np.random.normal(0.0, 0.25, len(t))
x[x<-1.0] = -1.0
x[x>=1.0] = 1.0
plt.subplot(5,1,5)
plot(x, "k", "Whitenoise")

plt.tight_layout()
plt.show()


# 矩形波 デューティー比変更
cmap = plt.get_cmap("tab10")
duties = np.arange(0.1, 1.0, 0.2)
plt.clf()
for k, duty in enumerate(duties):
    plt.subplot(len(duties), 1, k+1)
    x = scipy.signal.square(2*np.pi*fo*t, duty=duty)
    plot(x, cmap(k), f"Square wave (duty={duty:0.1f})")


# 三角:鋸歯状波 width比変更
cmap = plt.get_cmap("tab10")
width_list = np.arange(0.0, 1.1, 0.2)
plt.clf()
for k, width in enumerate(width_list):
    plt.subplot(len(width_list), 1, k+1)
    x = scipy.signal.sawtooth(2*np.pi*fo*t, width=width)
    plot(x, cmap(k), f"Sawtooth wave (width={width:0.1f})")

plt.tight_layout()
plt.show()