広告/統計/アニメ/映画 等に関するブログ

広告/統計/アニメ/映画 等に関するブログ

FM音源でランダムに音色を作る

FM音源でランダムに音色を作る

FM音源とは、Frequency Modulation(周波数変調)を使った音色合成で、ヤマハが発明し過去は特許となっていた。

FM音源 - Wikipedia

サイン波の周波数部分を更にサイン関数で変動させることで様々な音色を作ることができる。特に、金属系の音色を作ることに長けているようだ。

今回は、構造自体がとても簡素である点に着目して、FM音源の仕組みでランダムに音色を作ってみる。

FM音源の作り方はこの本を参照した。ここまで初心者向けに丁寧な音の説明とプログラムの用意がある本はなかなかないだろう。

下準備

wavファイルの取り扱いや音の再生、グラフ表示などで必要となる関数をインポートする

import numpy as np #sin関数を作るのに必要
#import scipy.signal as sp #sin関数を作るのに必要
import wave as wave #wavファイルに吐き出したり(読み込んだりするのに必要)
import sounddevice as sd #jupyter notebook上で音を再生するために必要
import random as random #ランダムに数値を発生させるために使う
import matplotlib.pyplot as plt #波形を確認しておきたいのでmatplotlibも使う
%matplotlib inline

この辺りはこの本であったり、Web上の諸先輩方を参考にする。

基本となるデータや空の行列の用意

秒数やサンプリング周波数などを決めておく

totalduration = 4.0 #時間、秒
fs = 44100 #サンプリング周波数。sfでなくてfsと書く習慣になった理由はよくわからない

秒数は時点に変えておく

t = np.arange(0,fs*totalduration)/fs #時間を時点に変換
t
array([0.00000000e+00, 2.26757370e-05, 4.53514739e-05, ...,
       3.99993197e+00, 3.99995465e+00, 3.99997732e+00])

FM音源

メインのサイン関数がキャリア。サイン関数の中で変動させる周波数部分がモジュレーターと呼ばれる。 それぞれの音の大きさ(振幅・amplitude)を格納するnumpy行列を容易しておく。

ac = np.zeros(len(t)) #キャリア
am = np.zeros(len(t)) #モジュレータ

キャリア側振幅の設定

音色のどこを調整するか(エンベロープ)については、「Attack(立ち上がり)、Decay(減衰)、Sustain(減衰後の保持)、Release(余韻)」の考え方を利用する。まだ自分の理解が足りていないのだが、物体の振動数の特徴を再現するのによく考えられた仕組みだと思う。

ADSR - Wikipedia

先程の本に則って金属系の音を出してみる。勿論、エンベロープをかけることは必須ではない。

ADSRの初期値

この辺りの関数も先程の本の関数を借用する。ここに詳しい紹介がある。Cを前提にプログラムが書かれているが、シンプルなのでどの言語でもできる。著者がコードをこちらで公開している。

サウンドプログラミング2

gate = fs*4 #単音の減衰が始まるまでの秒数
duration = fs*4 #単音の秒数。ここでは4秒
A = 0
D = fs*4
S = 0.0
R = fs*4

ADSRによるエンベロープの関数を定義

def ADSR(e,A,D,S,R,gate,duration):
    if A!=0:
        for i in np.arange(0,A,1):
            e[i]=1.0-np.exp(-5.0*i/A)
    elif D!=0:
        for i in np.arange(A,gate,1):
            e[i] = S+(1-S)*np.exp(-5.0*(i-A)/D)
    else:
        for i in np.arange(A,gate,1):
            e[i] = S
    if R!=0:
        for i in np.arange(gate,duration,1):
            e[i] = e[gate-1]*np.exp(-5.0*(i-gate+1)/R)
    return e

キャリア振幅にエンベロープを適用

ac = ADSR(ac,A,D,S,R,gate,duration)
ac
array([1.        , 0.99997166, 0.99994331, ..., 0.00673852, 0.00673833,
       0.00673814])

モジュレータ側振幅の設定

こちらも同様にエンベロープをかけておく

gate = fs*4
duration = fs*4
A = 0
D = fs*2
S = 0.0
R = fs*2
am = ADSR(am,A,D,S,R,gate,duration)
am
array([1.00000000e+00, 9.99943312e-01, 9.99886628e-01, ...,
       4.54076515e-05, 4.54050774e-05, 4.54025035e-05])

合成

FM音源のポイント。キャリアとモジュレーターの周波数の比率を調整することで、FM音源は様々な音色を見せる。

キャリア周波数

キャリア側の周波数を決めておく。中央のA4のラは440kHzなのでそれを採用してみた。

fc  = 440.0 #キャリア周波数

モジュレーターの周波数

乱数の発生

今回はその周波数比を自分で考える代わりに、適当に乱数で比率を決めてみる。再現性の確認のため、randomseedを使う。2倍なら1オクターブ上の倍音が、0.5倍なら1オクターブ下の倍音が出るということになるのだが、どれくらいの幅までやってみるかは好みではないかと思う。非整数倍の音を出したいので、今回は、1~99までの整数を発生させて10で割ることにする。

random.seed(42) #よく42が使われるのだが、「銀河ヒッチハイク・ガイド」のネタだと思う
ratio = random.randint(1,99)/10
print(ratio)
8.2

seed値を使わなけば、実行する度に様々な音色が生まれる

周波数の設定

上記の比率をキャリア周波数にかけてモジュレーター側の周波数を決める

fm = fc*ratio
print(fm)
3607.9999999999995

音源化

事前情報は揃ったので、キャリア・モジュレーターの振幅と周波数でサイン波の合成を行う。先に行列の箱を作ってfor文で格納しているのだが、本当はもっと良い方法があるような気がする。

z = np.zeros(len(t))
for n in np.arange(0,len(t),1):
    z[n] = ac[n]*np.sin(2*np.pi*fc*n/fs)*(1.0+am[n]*np.sin(2*np.pi*fm*n/fs))
print(z)
[ 0.          0.09344876  0.23210877 ... -0.00125979 -0.0008426
 -0.00042212]

音の確認

sounddeviceライブラリを使うとjupyter notenook上でも音が再生できる

sd.play(z,fs)
print("再生中")
status=sd.wait()
再生中

WAVEファイルへの書き出し

wavファイルには整数値で入れる必要があるが、Pythonの整数値には上限がある。上限を超えると音が狂うので、範囲に収まるようにスケーリングする。16bitで量子化するのでint16の上限値以内に収まるようにする。

print(np.iinfo(np.int16).max)
32767
z2 = (z/z.max())*np.iinfo(np.int16).max #スケールを整数のマックスに

整数値に変換

z2 = z2.astype(np.int16) #2バイトデータ化

書き出しには、waveライブラリを使用する。

wave_out = wave.open("./FM_220326_seed42.wav","w")
wave_out.setnchannels(1) #モノラル音源
wave_out.setsampwidth(2) #16bitなので2byte
wave_out.setframerate(fs)
wave_out.writeframes(z2)
wave_out.close()

波形をグラフ化

作った音がどんな波形なのかは気になると思うので、確認する

fig,ax = plt.subplots(1,1,figsize=(15,3))
ax.plot(t,z) 
ax.set_xlim(0,0.5)
plt.savefig("FM_220326_seed42_graf_001.png")
plt.show()

f:id:yyhhyy:20220326184444p:plain

表示する区間が長過ぎると潰れてよくわからないのでもっと短い秒数で確認する

fig,ax = plt.subplots(1,1,figsize=(15,3))
ax.plot(t,z) 
ax.set_xlim(0,0.01)
plt.savefig("FM_220326_seed42_graf_002.png")
plt.show()

f:id:yyhhyy:20220326184504p:plain

エンベロープなし

エンベロープをかけないシンプルな音だとどうなったも確認してみたい。振幅をとりあえず1にしておく。

amp = 1.0
z_simple = np.zeros(len(t))
for n in np.arange(0,len(t),1):
    z_simple[n] = amp*np.sin(2*np.pi*fc*n/fs)*(1.0+amp*np.sin(2*np.pi*fm*n/fs))

結論から言うと余り面白い音にはならなかった。エンベロープのテクニックがわりと重要に思える。

sd.play(z_simple,fs)
print("再生中")
status=sd.wait()
再生中
z_simple_2 = (z_simple/z_simple.max())*np.iinfo(np.int16).max 
z_simple_2 = z_simple_2.astype(np.int16) #2バイトデータ化
wave_out = wave.open("./FM_220326_seed42_simple.wav","w")
wave_out.setnchannels(1) #モノラル音源
wave_out.setsampwidth(2) #16bitなので2byte
wave_out.setframerate(fs)
wave_out.writeframes(z_simple_2 )
wave_out.close()

番外、ffmpegによる音の映像化

音声ファイルだけをYoutubeにアップロードすることはできない。適当に画像を貼って映像化するなら映像編集ソフトを立ち上げるよりffmpegで変換する方が楽だろう。

ffmpeg -loop 1 -i FM_220326_seed42_graf_001.png -i FM_220326_seed42.wav -vcodec libx264 -pix_fmt yuv420p -shortest FM_220326_seed42.mp4

ffmpeg -loop 1 -i FM_220326_seed42_simple_graf_002.png -i FM_220326_seed42_simple.wav -vcodec libx264 -pix_fmt yuv420p -shortest FM_220326_seed42_simple.mp4

dev.classmethod.jp