keep learning blog(キープラーニングブログ)

自分が興味を持ったことを備忘録として残すブログです。

7.ギターの音を人工的に作ってみよう

――一本のギターより美しいものなどありはしない。おそらく二本のギターを除いてはね。――

フレデリック・フランソワ・ショパン

 

 

ギターの音を人工的に作ってみよう

内容としては独立ですが、プログラミング関連なので第6回の記事の続きです。今回も、Pythonで簡単に実装できる音声処理のアルゴリズムをご紹介します。

keep-learning.hatenablog.jp

 

ギターの弦とおさかな理論

音とは空気の振動であり、空気の振動は音源(ギターの弦や太鼓の膜)の振動と二人三脚で発生します。物理では、波が1秒間に振動する回数のことを「周波数」と呼び、人間は高い周波数の音を高い音として知覚します*1。空気と音源は二人三脚なので、音の周波数と音源の周波数は一致します。

これから、ギター風の音を人工的に発生させるアルゴリズムを実装するということもあり、まずは弦の振動に関する基礎を解説します。別に教育関係のブログではないので一から説明する必要はないのですが、塾講師をやっていたときにこの手の説明はよくしました。少しお付き合いください。

f:id:yuki0718:20190802022331p:plain

©Luc MahlerによるPixabayからの画像

ギターとは、一定の張力で6本の弦を張ったカブトムシみたいな形の弦楽器です。カブトムシの角の側には、弦に対して垂直に細いパイプが並んでおり、業界の方はフレットと呼ぶそうです。そして、指で弦をフレットに押し当ててから弾くと、高い音が鳴ります。ギタリストは様々なフレットを渡り歩くように、華麗に指を動かします。

物理では、弦の上に安定して発生する振動のことを定常波と呼びます。定常波は、ちょうど以下のように弦の上を泳ぐお魚のような形をしています。そして、弦の長さLに対して、お魚のサイズの2倍(お魚2匹分)を波長と呼びます。以下の図だと波長は 2L です。

f:id:yuki0718:20190730044231j:plain

ギターの弦の両端は固定されているので、弦の端には必ずお魚の頭かおしりが来なければいけません(固定端境界条件)。言い換えると、どんなサイズのお魚でも良いわけではなく、弦の上を泳げるお魚のサイズは限定されています。以下の図のように、弦の長さLに対して、お魚が2匹なら波長は 2L/2 、3匹なら波長は 2L/3 です。

f:id:yuki0718:20190730044250j:plain

勘の良い方であれば、お魚が4匹なら波長は 2L/4 、5匹なら波長は 2L/5 、X匹なら波長は 2L/X となることが予想できると思います。この波長という量は、物理的にたいへん重要な数値です。

というのも、波動力学によれば、周波数は波長の逆数に比例するからです。波長は 2L/X でしたから、その逆数は X/2L になります。すなわち、音の高さ(周波数)はお魚の数Xに比例し、弦の長さLに反比例します。ギタリストが弦をフレットに押さえつけるのは、お魚の泳げる長さLを調節して、音の高さを変えるためだったのです。

 

有周波数

お魚の泳げる範囲を指で華麗に調節するギタリスト、なんだかギターがカブトムシではなく巨大なカジキマグロに見えてきたのではないでしょうか。

さて、ここで一つ強調しておきたいことは、「弦をフレットに押しつけると、Lが短くなって音の高さを変えられるけど、お魚の数Xはコントロールできない」という点です。一般的に弦楽器の奏でる音は、決して1匹のカジキマグロではなく、ブリからサバからシラスまで、多種多様なサイズのお魚(様々な高さの音)が勝手に混じりあってできています。

そして、この音の高さの基準となる、1番大きいお魚に対応する音の周波数のことを、弦の有周波数(Natural Frequency) F_0 と呼びます。お魚が何匹いようと、音の高さは固有周波数の整数倍(X匹)で表現されるので、他の音は 2F_0, 3F_0, 4F_0, ・・・と書くことができます。

有周波数(固有振動数と言う人もいる)の説明については、Youtubeの映像授業「Try it」でたいへん分かりやすい講義がアップされていました*2。ご参考まで。

www.youtube.com

 

Karplus-Strong string synthesis

以上で私のつまらないうんちくは終わりです。ようするに、ギター風の音を作るためには、①ギターの弦の固有周波数 F_0 を知り、②固有周波数の整数倍の音を人工的に作り出し*3、③適度な割合で混じり合わせる、という過程が必要だと理解してもらえたでしょうか。

実は、第6回の記事で紹介したくし形フィルタ(Comb Filter)がここで役に立ちます。くし形フィルタは、ド⇒ミ⇒ソ⇒シ⇒・・・といった一定の間隔で、櫛のようなピークが並んだフィルタでした。このピークの間隔をちょうどギターの固有周波数 F_0 と同じになるようにフィルタを設計すれば、①~②のプロセスはクリアできます。

残る問題は③です。ギターの音はたくさんのお魚が玉石混交で構成されていましたが、実はメインとなるのは大きい(波長の長い)魚です。周波数は波長の逆数ですから、周波数の低い成分ほど強調されるべきです。これはローパスフィルタ(Low-Pass Filter)というフィルタで実現できます。

ローパスフィルタの差分方程式 y_{n} = 0.5x_{n} + 0.5x_{n-1}

デジタルなローパスフィルタの設計方法はいろいろあります。その中で最もシンプルな形がこれです。周波数が低いところでは何もせず、周波数が高くなるにつれて音をかき消すように働くフィルタです。

以上、まるで自分が開発したかのように偉そうに語りましたが、このギター風の音を再現するフィルタ設計方法を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)を各弦の音の重ね合わせとして表現したいと思い、人工的にコードの音階に合わせた音を作り出しました。そのために設計したフィルタの応答関数(周波数特性)は以下のとおりです。

f:id:yuki0718:20190802020110p:plain

弦ごとのくし形フィルタの応答関数

いろんな色のグラフが重なって見にくいですが、青い色のグラフが、ギターを担いだときに顔に一番近い弦の音を再現するフィルタです。次に近い弦がオレンジ、緑、赤、紫、茶色と続きます。横軸は周波数で、左から最初に出てくるピークが固有周波数(お魚1匹のとき)です。この整数倍の位置にピークが続いています*4

それでは、このフィルタを通して人工的に発生した音を、ギターを担いだときに顔に近い側の弦から順にサンプルで紹介します。

ギターの弦は6本なので、6つの音を生成しました。さて、これらを足し合わせた音はこんな感じでした。

f:id:yuki0718:20190802020817p:plain

ギターコード風のフーリエ変換データ

個人的には、「素人実装の割にすごいギターっぽい!」と思ったのですが、いかがでしょうか。教科書や論文を読んでアルゴリズムさえ理解すれば、結果が正しいかどうかは音で聴いて判断できるので、音声処理は(趣味でやっても)なかなかおもしろいタスクです。

 

今日のところは以上です。今回も勉強寄りの内容でした。おもしろ食べ歩き紀行とかじゃなくてすみません。そのうちスーパーの商品別の価格を考察したり、オーロラ観測に行って写真をアップしたりしたいな、と思っています。

稚文をお読みいただきありがとうございました。

*1:つまり、平均的には男性の声よりも女性の声の方が周波数は高いということになります。

*2:私が子供のころは、田舎だと学校の教科書や参考書を読むことしか選択肢がありませんでしたが、今はインターネット経由で優れた教材を探せます。今の子供たちは幸せですね。

*3:お魚の喩えを続けるなら、人工マグロの養殖とでも言いましょうか。

*4:例えば、青い色のグラフは、100Hz、200Hz、300Hz・・・という位置にピークが出ています。