7.ギターの音を人工的に作ってみよう
――一本のギターより美しいものなどありはしない。おそらく二本のギターを除いてはね。――
ギターの音を人工的に作ってみよう
内容としては独立ですが、プログラミング関連なので第6回の記事の続きです。今回も、Pythonで簡単に実装できる音声処理のアルゴリズムをご紹介します。
ギターの弦とおさかな理論
音とは空気の振動であり、空気の振動は音源(ギターの弦や太鼓の膜)の振動と二人三脚で発生します。物理では、波が1秒間に振動する回数のことを「周波数」と呼び、人間は高い周波数の音を高い音として知覚します*1。空気と音源は二人三脚なので、音の周波数と音源の周波数は一致します。
これから、ギター風の音を人工的に発生させるアルゴリズムを実装するということもあり、まずは弦の振動に関する基礎を解説します。別に教育関係のブログではないので一から説明する必要はないのですが、塾講師をやっていたときにこの手の説明はよくしました。少しお付き合いください。
ギターとは、一定の張力で6本の弦を張ったカブトムシみたいな形の弦楽器です。カブトムシの角の側には、弦に対して垂直に細いパイプが並んでおり、業界の方はフレットと呼ぶそうです。そして、指で弦をフレットに押し当ててから弾くと、高い音が鳴ります。ギタリストは様々なフレットを渡り歩くように、華麗に指を動かします。
物理では、弦の上に安定して発生する振動のことを定常波と呼びます。定常波は、ちょうど以下のように弦の上を泳ぐお魚のような形をしています。そして、弦の長さLに対して、お魚のサイズの2倍(お魚2匹分)を波長と呼びます。以下の図だと波長はです。
ギターの弦の両端は固定されているので、弦の端には必ずお魚の頭かおしりが来なければいけません(固定端境界条件)。言い換えると、どんなサイズのお魚でも良いわけではなく、弦の上を泳げるお魚のサイズは限定されています。以下の図のように、弦の長さLに対して、お魚が2匹なら波長は、3匹なら波長はです。
勘の良い方であれば、お魚が4匹なら波長は、5匹なら波長は、X匹なら波長はとなることが予想できると思います。この波長という量は、物理的にたいへん重要な数値です。
というのも、波動力学によれば、周波数は波長の逆数に比例するからです。波長はでしたから、その逆数はになります。すなわち、音の高さ(周波数)はお魚の数Xに比例し、弦の長さLに反比例します。ギタリストが弦をフレットに押さえつけるのは、お魚の泳げる長さLを調節して、音の高さを変えるためだったのです。
固有周波数
お魚の泳げる範囲を指で華麗に調節するギタリスト、なんだかギターがカブトムシではなく巨大なカジキマグロに見えてきたのではないでしょうか。
さて、ここで一つ強調しておきたいことは、「弦をフレットに押しつけると、Lが短くなって音の高さを変えられるけど、お魚の数Xはコントロールできない」という点です。一般的に弦楽器の奏でる音は、決して1匹のカジキマグロではなく、ブリからサバからシラスまで、多種多様なサイズのお魚(様々な高さの音)が勝手に混じりあってできています。
そして、この音の高さの基準となる、1番大きいお魚に対応する音の周波数のことを、弦の固有周波数(Natural Frequency)と呼びます。お魚が何匹いようと、音の高さは固有周波数の整数倍(X匹)で表現されるので、他の音は・・・と書くことができます。
固有周波数(固有振動数と言う人もいる)の説明については、Youtubeの映像授業「Try it」でたいへん分かりやすい講義がアップされていました*2。ご参考まで。
Karplus-Strong string synthesis
以上で私のつまらないうんちくは終わりです。ようするに、ギター風の音を作るためには、①ギターの弦の固有周波数を知り、②固有周波数の整数倍の音を人工的に作り出し*3、③適度な割合で混じり合わせる、という過程が必要だと理解してもらえたでしょうか。
実は、第6回の記事で紹介したくし形フィルタ(Comb Filter)がここで役に立ちます。くし形フィルタは、ド⇒ミ⇒ソ⇒シ⇒・・・といった一定の間隔で、櫛のようなピークが並んだフィルタでした。このピークの間隔をちょうどギターの固有周波数と同じになるようにフィルタを設計すれば、①~②のプロセスはクリアできます。
残る問題は③です。ギターの音はたくさんのお魚が玉石混交で構成されていましたが、実はメインとなるのは大きい(波長の長い)魚です。周波数は波長の逆数ですから、周波数の低い成分ほど強調されるべきです。これはローパスフィルタ(Low-Pass Filter)というフィルタで実現できます。
デジタルなローパスフィルタの設計方法はいろいろあります。その中で最もシンプルな形がこれです。周波数が低いところでは何もせず、周波数が高くなるにつれて音をかき消すように働くフィルタです。
以上、まるで自分が開発したかのように偉そうに語りましたが、このギター風の音を再現するフィルタ設計方法をKarplus-Strong string synthesisと呼びます。カープラスさんとストロングさんは人の名前で、string synthesisは弦の(音)合成という意味です。
Pythonで実装
またもや、いきなりソースコード載せます。実行結果だけでも聴いてみてください。Pythonの文法(syntax)を知りたい場合には、私のような素人が解説するより豊富なネット情報の方が有用です。
import IPython.display as ipd #jupyter notebook用の再生ライブラリ import matplotlib.pyplot as plt import numpy as np from scipy import signal as sg #■Karplus-Strong_string_algorithmでギター風の音を生成する関数を定義■ def guitar_sound(fret, offset, Fs): #二番目の弦の基本周波数Aは開放弦で110Hz、弦を押さえる位置(fret)に従って音が高くなる A = 110*2**((fret+offset)/12) #フィードバック遅延はサンプリングと基本周波数の比(=400.909…) #遅延は整数でなければならないので丸める D = round(Fs/A) #フィルタの伝達関数の分子b分母aを定義 #分子bは最もシンプルなLow-Passフィルタ(1/Fsに向かって緩やかに減衰) b = [0.5, 0.5] #分母aはpoles(極点)が基本周波数に一致するCombフィルタ #すなわちフィードバック遅延D-1個の数だけゼロをパディング a = [1] a.extend([0]*(D-1)) a.extend([-0.5, -0.5]) #プロット用の横軸を1000Hzまで2^12=4096点定義する F = np.linspace(1/Fs, 1000, 2**12) #1000Hzまでの範囲でフィルターの振幅H&周波数Wの応答を得る W, H = sg.freqz(b, a, worN=F, fs=Fs) #フィルタ応答の振幅をlogスケールのdBに変換 P = abs(H)**2 P = 10*np.log10(P) #matplotを使ってフィルタ応答関数のグラフを表示 plt.plot(W, P) #ギターの音の元となる6秒間ゼロが並んだベクトルを定義する x = [0]*(Fs*6) #フィルタの初期ディレイとして乱数を持つ状態ベクトルを生成 #iniの長さは分子または分母の最大次数-1と等しくないといけない #(参考)https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.lfilter.html ini = np.random.rand(np.amax([len(b), len(a)])-1) #初期状態iniを使用してxをフィルター処理 Result = sg.lfilter(b, a, x, zi=ini)[0] #ini=Noneでは結果が配列で返るがiniを入れるとネスト配列なので[0]が必要 #平均を引いて最大値で割って正規化 Result = Result - np.mean(Result) Result = Result / np.amax(abs(Result)) #戻り値として音の振幅を返す return Result #■メイン処理■ if __name__ == "__main__": #グラフの描画領域を用意しておく plt.figure(figsize=(16, 5)) plt.title('Frequency Response of the Comb-Low-Pass Filter', fontsize=12) plt.xlabel('Frequency [Hz]') plt.ylabel('Power [dB]') #サンプリング周波数44.1kHz Fs = 44100 #手前から二番目の弦(110Hz)を基準としたオフセット offset = [-5, 0, 5, 10, 14, 19] #弦ごとの音量のバランス(重み) balance = [0.1, 0.1, 0.1, 0.1, 0.3, 0.3] #いろいろな和音を用意します fret_C = [3, 3, 2, 0, 1, 3] #Cコード fret_F = [1, 3, 3, 2, 1, 1] #Fコード fret_G = [3, 2, 0, 0, 0, 3] #Gコード #★押さえる弦の位置fretとして上のいずれかのコードを選びます★ fret = fret_G #和音を生成する Chord = 0 for i in range(6): #自己定義関数を呼び出してi番目の弦を鳴らす sound = guitar_sound(fret[i], offset[i], Fs) #IPythonライブラリを使ってjupyter notebook上に再生バーを表示 print('No.' + str(i+1) + ' string') ipd.display(ipd.Audio(data=sound, rate=Fs)) #和音は各弦の単音の重み付き和で表現できる Chord = Chord + balance[i] * sound #IPythonライブラリを使ってjupyter notebook上に再生バーを表示 print('Chord (Sum of them)') ipd.display(ipd.Audio(data=Chord, rate=Fs)) #フーリエ変換の最大次数(2の累乗にするとFFTになる) fftsize = 2**12 #横軸用に0~Fsまでをfftsize個に分割 F = np.linspace(0, Fs, fftsize) #最大次数まで離散フーリエ変換を実行 dft = np.fft.fft(Chord, fftsize) #縦軸用に振幅を二乗してdB単位に変換 P = abs(dft)**2 P = 10*np.log10(P) #データがFs個もあると見にくいので1000Hzまでに限定 F = F[0 : round(1000*fftsize/Fs)] P = P[0 : round(1000*fftsize/Fs)] #matplotlibを使って和音のグラフを別表示 plt.figure(figsize=(16, 5)) plt.plot(F, P) plt.title('DFT Power(Generated Chord)', fontsize=12) plt.xlabel('Frequency [Hz]') plt.ylabel('Power [dB]') plt.show()
実行結果
私のプログラムでは、ギターの和音(Chord)を各弦の音の重ね合わせとして表現したいと思い、人工的にコードの音階に合わせた音を作り出しました。そのために設計したフィルタの応答関数(周波数特性)は以下のとおりです。
いろんな色のグラフが重なって見にくいですが、青い色のグラフが、ギターを担いだときに顔に一番近い弦の音を再現するフィルタです。次に近い弦がオレンジ、緑、赤、紫、茶色と続きます。横軸は周波数で、左から最初に出てくるピークが固有周波数(お魚1匹のとき)です。この整数倍の位置にピークが続いています*4。
それでは、このフィルタを通して人工的に発生した音を、ギターを担いだときに顔に近い側の弦から順にサンプルで紹介します。
ギターの弦は6本なので、6つの音を生成しました。さて、これらを足し合わせた音はこんな感じでした。
個人的には、「素人実装の割にすごいギターっぽい!」と思ったのですが、いかがでしょうか。教科書や論文を読んでアルゴリズムさえ理解すれば、結果が正しいかどうかは音で聴いて判断できるので、音声処理は(趣味でやっても)なかなかおもしろいタスクです。
今日のところは以上です。今回も勉強寄りの内容でした。おもしろ食べ歩き紀行とかじゃなくてすみません。そのうちスーパーの商品別の価格を考察したり、オーロラ観測に行って写真をアップしたりしたいな、と思っています。
稚文をお読みいただきありがとうございました。