Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
218 lines (153 sloc) 10.2 KB

ライドシンバル合成の試み

ZynAddSubFXのDrums -> Natural Drum KitのハイハットがSUBsynthで合成されているのを見てシンバルを加算合成できる気がしました。特にライドシンバルは「カーン」というトーンの成分が多めに聞こえるので加算合成しやすそうに思えました。

始めは周波数成分を手作業で入力するつもりでしたが、無理そうだったのでKernel Density Estimationを応用してある程度ランダムに合成することにしました。

結果はあまり良くないものでしたが、シンバルの合成に関する知見が得られました。

PADsynth

加算合成にPADsynthを使います。PADsynthは長さ2^18前後のバッファに周波数分布のデータを書き込んで逆フーリエ変換することで音を作ります。

以下はPython3での実装です。位相を扱えるように変更しています。

import cmath
import math
import numpy
import random

from pyfftw.interfaces.numpy_fft import fft, ifft


def profile(fi, bwi):
    x = fi / bwi
    return math.exp(-x * x) / bwi


def normalize(sound, amp=1.0):
    amp /= max([max(sound), -min(sound)])
    return [value * amp for value in sound]


def padsynth(samplerate, frequencies, band_width=10, random_phase=2):
    """
    PadSynth from ZynAddSubFX
    http://zynaddsubfx.sourceforge.net/doc/PADsynth/PADsynth.htm

    frequencies = [(freq, gain, phase), ...]

    profile_size_half の定数6は以下を参照。値を大きくすると遅くなるかわりに精度が上がる。
    https://en.wikipedia.org/wiki/Normal_distribution#Standard_deviation_and_coverage
    """
    table = numpy.zeros(2**16, dtype=numpy.complex)

    for freq, gain, phase in frequencies:
        band_width_hz = (math.pow(2, band_width / 1200) - 1.0) * freq
        band_width_i = band_width_hz / (2.0 * samplerate)

        sigma = math.sqrt(math.pow(band_width_i, 2.0) / (2.0 * math.pi))
        profile_size_half = max(int(6 * len(table) * sigma), 1)

        freq_i = freq / samplerate

        center = int(freq_i * len(table))
        start = max(center - profile_size_half, 0)
        end = min(center + profile_size_half, len(table))

        for index in range(start, end):
            table[index] += cmath.rect(
                gain * profile(index / len(table) - freq_i, band_width_i),
                phase + random_phase * random.random() * math.pi)

    table[0] = 0 * 1j  # 直流を除去。

    sound_ifft = ifft(table, planner_effort='FFTW_ESTIMATE', threads=1)
    sound_flat = normalize(sound_ifft.real)

    return sound_flat

Kernel Density Estimation

PADsynthのやっていることは加算合成なので、周波数、音量、位相のデータを上手く生成できればバッファ長以下のどんな音でも合成できます。今回はKernel density estimation (KDE) という手法を用いてサンプル集合と似たようなデータの生成を試しました。

以下は作成したシステムの大まかな処理の流れです。

  1. サンプルを入力して特徴抽出。
  2. 抽出した特徴からKDEで確率密度関数を作成。
  3. 確率密度関数からPADsynthに渡すパラメータを生成。
  4. パラメータをPADsynthに渡して音を合成。

特徴抽出

例として以下のサンプルから特徴を抽出します。

./fig/fig_sample.png

サンプルの音量を平坦にします。

./fig/fig_flat.png

平坦にしたサンプルをフーリエ変換して音量 (パワー) と位相を得ます。周波数が100Hzより低い成分は捨てています。

  • パワースペクトラム

./fig/power_spectrum.png

  • 位相

./fig/fig_phase.png

計算時間を減らしたいのでデータの数を減らします。

周波数領域のデータは (周波数, 音量, 位相) という組の集まりとして扱うことができます。音量についてソートして大きいほうから、例えば1024組のデータを取り出します。

  • 数を減らしたパワースペクトラム

./fig/reduced_spectrum.png

  • 位相

./fig/fig_reduced_phase.png

KDE

各サンプルの音量がn番目に大きい組を取り出して、周波数、音量、位相それぞれでKDEを行って確率密度関数を作ります。

コードにすると以下のようになります。

# python3
data = [
    [(freq, gain, phase), (freq, gain, phase), ...]
    [(freq, gain, phase), (freq, gain, phase), ...]
    [(freq, gain, phase), (freq, gain, phase), ...]
    ...
]

pf = [] # probability function
for i in range(len(data[0])):
    pf.append((
        kde([d[i][0] for d in data]), # freq
        kde([d[i][1] for d in data]), # gain
        kde([d[i][2] for d in data]), # phase
    ))

データを生成する時は以下のようなコードになります。

generated = [
    (
        p[0].random(),  # freq
        p[1].random(),  # gain
        p[2].random(),  # phase
    ) for p in pf
]

実験と結果

実験で使うサンプルはfreesound.orgから取得しました。

実験で使ったシンバル系のPackです。

シンバルでないPackです。freesound.orgのトップページからSounds -> More Sounds -> Give me a random sound!とたどってランダムに取得しました。

結果として得られた合成音です。

合成音のファイル名は <種類>_<Pack>_<番号>.wav となっています。以下は種類の意味です。

  • flat: 周波数と音量のデータを使用。位相はランダム。
  • flat_phase: 周波数、音量、位相のデータを使用。
  • out: flatに減衰のみのエンベロープを音量に適用。

outのエンベロープはただの指数関数です。パラメータは音を平らにするときに得られた値からscipyのcurve_fitで推定しました。

乱数

位相がランダムの場合、入力に関係なく似たような音になる気がしたので周波数と音量もランダム生成してPADsynthに入力しました。

size = 1024
freq = numpy.random.uniform(100, 12000, size)
gain = numpy.random.uniform(1e-5, 1, size)
phase = numpy.random.uniform(0, 2.0 * numpy.pi, size)
generated = [(freq[i], gain[i], phase[i]) for i in range(size)]

出力です。

分かったこと

シンバルの「カーン」というトーンの部分を加算合成するとき、位相はランダムにしたほうがいいことがわかりました。

考察

後知恵としてはPADsynthだけでのシンバルの合成は難しいと思います。

作った後に調べてみるとModal Sound Synthesisという手法を用いた研究を見つけました。各周波数成分の減衰を考慮しているようです。

シンバルの音は宙吊りにされた金属とスティックの衝突音です。従って、「カチッ」という衝突のインパルスを入力すると「シャー」「カーン」といったシンバルの音が出てくるシステムと捉えることができます。

このアプローチではDigital Waveguide Synthesisが応用できます。”digital waveguide hihat” でグーグル検索したら既に作っている方もいました。

参考文献