😤

呼吸音をプロシージャル生成する

2023/05/03に公開

ゲームやインタラクティブコンテンツにおいて、キャラクターの呼吸音は強い臨場感を与えてくれますが、不自然になりやすい要素のひとつです。

呼吸音が不自然になる理由は「録音された変化のない音」を使っているためです。ゲームのプレイ中、同じような呼吸音が何度も鳴って残念に感じたことはありませんか? もし呼吸音にゆらぎや変化があれば、この違和感を解消できるはずです。

そこで、究極的な解決方法である呼吸音のプロシージャル生成にトライしてみます。

サンプル

ここでは女の子のキャラクターの呼吸音を作ってみました。だいぶそれらしい質感がありませんか?

https://www.youtube.com/watch?v=6fuu3cUS_qc

呼吸音生成のコード

呼吸音生成を音響プログラミング言語のFaustで実装してみました。

Faustはやや知名度の低い言語なので読者の中には初めて使う方もいるかもしれませんが、試すのは簡単です。

  1. ブラウザでFaust IDEを開く
  2. エディタ欄に以下のコードをペーストする
  3. Runボタン(またはCtrl+R) を押して実行する

Faust IDE上で音を聴きながらスライダー調整が可能です。いろんなパラメータ設定を試してみてください。

declare name "AutoBreath";
declare license "Unlicense";

import("stdfaust.lib");

envParam = environment {
    rate = hslider("v:envelope/[0]respiratoryRate[unit:/min]", 20, 10, 100, 0.1);
    gainRatio = hslider("v:envelope/[1]gainRatio", 0.7, -1, 1, 0.01);
    timeRatio = hslider("v:envelope/[2]timeRatio", 0.32, 0.01, 0.99, 0.01);
    inhaleAttack = hslider("v:envelope/[3]inhaleAttack", 0.70, 0.01, 0.99, 0.01);
    inhaleRatio  = hslider("v:envelope/[4]inhaleRatio",  0.94, 0.01, 0.99, 0.01);
    exhaleAttack = hslider("v:envelope/[5]exhaleAttack", 0.30, 0.01, 0.99, 0.01);
    exhaleRatio  = hslider("v:envelope/[6]exhaleRatio",  0.70, 0.01, 0.99, 0.01);
};

easeLinear(x) = x * select2(((x < 0) || (x >= 1)), 1, 0);
easeInQuad(x) = easeLinear(x) ^ 2;
easeOutQuad(x) = 1 - (1 - easeLinear(x)) ^ 2;
envelope = envInhaleAttack(time) + envInhaleRelease(time) + envExhaleAttack(time) + envExhaleRelease(time)
with {
    time = os.lf_sawpos(envParam.rate / 60);
    gainInhale = select2((envParam.gainRatio < 0), 1, (1 + envParam.gainRatio));
    gainExhale = select2((envParam.gainRatio > 0), 1, (1 - envParam.gainRatio));
    timeInhaleAttack = envParam.timeRatio * envParam.inhaleAttack * envParam.inhaleRatio;
    timeInhaleStop = envParam.timeRatio * envParam.inhaleRatio;
    timeExhaleAttack = (1 - envParam.timeRatio) * envParam.exhaleAttack * envParam.exhaleRatio + envParam.timeRatio;
    timeExhaleTotal = (1 - envParam.timeRatio) * envParam.exhaleRatio + envParam.timeRatio;
    envInhaleAttack(t)  = gainInhale * easeLinear(t / timeInhaleAttack);
    envInhaleRelease(t) = gainInhale * easeInQuad(1 - (t - timeInhaleAttack) / (timeInhaleStop - timeInhaleAttack));
    envExhaleAttack(t)  = gainExhale * easeInQuad((t - envParam.timeRatio) / (timeExhaleAttack - envParam.timeRatio));
    envExhaleRelease(t) = gainExhale * easeOutQuad(1 - (t - timeExhaleAttack) / (timeExhaleTotal - timeExhaleAttack));
};

srcParam = environment {
    gain = hslider("v:source/[0]gain", 0.05, 0, 1, 0.01);
    combFreq = hslider("v:source/[1]combFrequency[unit:Hz]", 3100, 2500, 3500, 0.01);
    combStrength = hslider("v:source/[2]combStrength", 0.9, 0, 1, 0.01);
    notchFreq = hslider("v:source/[3]notchFrequency", 2.5, 0.5, 4.0, 0.01);
    notchStrength = hslider("v:source/[4]notchStrength", 0.75, 0, 1, 0.01);
    peakFreq = hslider("v:source/[5]peakFrequency[unit:Hz]", 1850, 1000, 4000, 0.01);
    peakGain = hslider("v:source/[6]peakGain[unit:dB]", 7, 0, 15, 0.01);
};
noise = no.velvet_noise(1, 8000);
breath = noise : peak : comb : comb : notch
with {
    maxdelay = 2 ^ ceil(ma.log2(ma.SR / 1000));
    delay = ma.SR / srcParam.combFreq;
    comb = fi.ff_fcomb(maxdelay, delay, 1.0, -1 * srcParam.combStrength);
    notch = fi.notchw(srcParam.combFreq / 2, srcParam.combFreq * srcParam.notchFreq);
    peak = fi.peak_eq(srcParam.peakGain, srcParam.peakFreq, 4000);
};

output = srcParam.gain * envelope * breath : fi.dcblocker;
process = output <: _, _;

このコードは Unlicense です。ご自由にお使いください。

FaustのいいところはUnity用オーディオプラグインを出力できるところです。この出力機能を使えば、そのままゲームにも組み込めそうですね。

生成の仕組み

全体としてはエンベロープノイズフィルタという構成です。それぞれ見ていきましょう。

エンベロープ

呼吸音は音量変化が大事です。音量のエンベロープ曲線を変えることで、様々な呼吸表現ができます。

ここでは以下の曲線で音量変化をさせています。

この曲線は4つのカーブと2つの無音で構成されています。
これら4つのカーブはそれぞれ Linear, InQuad, InQuad, OutQuad のイージング曲線です。

Pythonでも実装してみるとこんな感じです。

def ease_linear(x):
    return x

def ease_inquad(x):
    return x ** 2

def ease_outquad(x):
    return 1 - (1 - x) ** 2

def envelope(t, gain_ratio, time_ratio, inhale_attack, inhale_ratio, exhale_attack, exhale_ratio):
    gain_inhale = (1 + gain_ratio) if gain_ratio < 0 else 1.0
    gain_exhale = (1 - gain_ratio) if gain_ratio > 0 else 1.0
    time_inhale_attack = time_ratio * inhale_attack * inhale_ratio
    time_inhale_stop = time_ratio * inhale_ratio
    time_exhale_attack = (1 - time_ratio) * exhale_attack * exhale_ratio + time_ratio
    time_exhale_total = (1 - time_ratio) * exhale_ratio + time_ratio

    # Stop
    if t < 0:
        return 0.0

    # Inhale Attack
    if t < time_inhale_attack:
        u = t / time_inhale_attack
        return gain_inhale * ease_linear(u)

    # Inhale Release
    if t < time_inhale_stop:
        u = (t - time_inhale_attack) / (time_inhale_stop - time_inhale_attack)
        return gain_inhale * ease_inquad(1 - u)

    # Stop
    if t < time_ratio:
        return 0.0

    # Exhale Attack
    if t < time_exhale_attack:
        u = (t - time_ratio) / (time_exhale_attack - time_ratio)
        return gain_exhale * ease_inquad(u)

    # Exhale Release
    if t < time_exhale_total:
        u = (t - time_exhale_attack) / (time_exhale_total - time_exhale_attack)
        return gain_exhale * ease_outquad(1 - u)

    # Stop
    return 0.0

ノイズ

ベルベットノイズと呼ばれるノイズを使っています。ベルベットノイズは、通常のノイズよりもなめらかに聴こえる特徴があるランダム信号です。ノイズのなめらかさを制御する「周期」のパラメータがありますが、これは 8000 Hz 以上が良いとされています。

フィルタ

ノイズにフィルタをかけて音にディティールを加えています。

フィルタの構成内容は、呼吸音のスペクトログラムを見ながら決めました。以下は音声素材を分析にかけた結果です。

吸気音のスペクトログラム

こうした呼吸音のスペクトログラムの特徴を挙げてみます。

  • ある倍数の周波数の振幅が下がる (この図では約 3.3, 6.6, 9.9, ... kHz)
  • 特定帯域の振幅が大きく下がる (この図では約 6–7 kHz 付近)
  • 中高域成分が盛り上がる (この図では約 2–3 kHz 付近)
  • 高域成分が下がる (この図では約 15 kHz 以上)

これらを実現するには、それぞれ以下のフィルタが使えます。

  • 特定倍数の振幅を下げる: Combフィルタ
  • 特定帯域をピンポイントで下げる: Notchフィルタ
  • 特定帯域を盛り上げる: Peakingフィルタ
  • 高域をカットする: Lowpassフィルタ

上述のコードでは Comb, Notch, Peaking の3つのフィルタでそれっぽく整えてあります。
実際に試すと Lowpass はほとんど音の印象に影響しなかったため、ここでは省きました(15 kHz 以上は人間の可聴帯域限界に近く、音味の違いが表れにくいところがあります)。

Tips

エンベロープ 呼吸周期は、普通の安静時は毎分 12~20 回だそうです(Wikipedia)。これを毎分 60~80 回程度に設定すると、走っている感じになります。
細かいパラメータはいじりながら調整するのが早いと思います。

ノイズ 上述の実装ではベルベットノイズを使っていますが、音質が劇的に良くなるわけでもありません。そもそも呼吸音は大音量で鳴らさないので、違いは分かりづらいと思います。

フィルタ 鼻呼吸っぽくするには、Combフィルタをなくすか弱める (CombStrengthを下げる) とよいです。
走ったときの呼吸などで上ずった感じを出したいときは、CombフィルタやPeakingフィルタの周波数をわずかに上げます。
あとはフィルタのパラメータを微妙にゆっくり揺らしてやると、さらにリアルになります。

リアルタイムのフィルタ処理はCPU負荷がかかります。パフォーマンスが厳しい場合は、あらかじめ数秒間のフィルタ済ノイズ信号をオーディオアセットとして持っておいて、ノイズをループ再生しながら音量制御でエンベロープをかけるやりかたが良いと思います。

おわりに

呼吸音のプロシージャル生成のお話でした。ゲームのプロシージャルというと、シェーダー芸はたくさん見かけますが、音をリアルタイム生成するものはあまり見かけない気がします。これからさらにゲームオーディオの世界が広がってくれたらなと思っています。

前述したように、Faust では Unity Native Audio Plugin が出力できます。さらに SteamAudio などの3Dオーディオプラグインを併用するとかなりASMRっぽくなります。

ちなみに、世の中には呼吸モーションを作ってくれる BreathController という便利スクリプトがあります。組み合わせると良い感じになるかもしれませんね。

私はこの呼吸音生成を活用したゲームを作ろうとしてエターナったので、誰かが組み込んで素敵な作品を作ってくれないかなーと期待しています(他力本願)。

Discussion