この記事では音量に合わせて口パク(リップシンク)を実装する方法について解説します
目次
口パクの実装方法
素材データの作成と登録
まずは口パクを行うキャラクターの画像と音声を用意します。
素材データとして以下のものを用意しました。
最低限2パターン(口を閉じる・口を開く)あれば問題ないですが、今回は3パターン用意しています
- 口を閉じている: ch01_1.png
- 口を少し開いている: ch01_2.png
- 口を大きく開いている: ch01_3.png
プロジェクトを作成し、これらの画像を追加しておきます。
それとボイスデータを追加します。
これは音読さんという音声作成ツールで作成したものとなります
これらもプロジェクトに追加しておきます
なお、mp3ファイルを使用する場合、自動的にループ設定がされているので、「インポート」タブから Loop のチェックを外して「再インポート」をクリックすると、ループ設定が無効となります
AnimatedSpriteで口パクアニメーションを作成する
Node2Dを作成し、ノード名を「Main」にリネームしておきます。
そして「AnimatedSprite」を追加します。
インスペクターから「Frames > [空]」をクリックして「新規 SpriteFrames」を選びます。
作成した「SpriteFrames」をクリックして、アニメーションフレームを表示します。
default には「ch01_1.png」のみ登録します。
次にアニメーションを追加してアニメーション名を「lipsync1」にリネームし、「ch01_1.png」と「ch01_2.png」を登録します。
速度は10フレームにしておきます。
続けてアニメーションを追加してアニメーション名を「lipsync2」にリネームし、「ch01_1.png」と「ch01_3.png」を登録します。
こちらも速度は10フレームにしておきます。
インスペクターから「Playing」にチェックを入れると口パクアニメーションが確認できます。
オーディオバスの作成とエフェクトの追加
次にボイス再生用のオーディオバスを作成します。
画面下部にある「オーディオ」をクリックしてレイアウトを表示して、「バスを追加」をクリックします。
すると新しいバスが作られるので、名前を「Voice」にリネームしておきます。
なお、ボイス用にバスを分ける理由は、Godotは波形データから直接音量を取得することができず、バス経由で音量を取得します。その際にバスを分けておかないと、他のサウンドの音量も拾ってしまうためとなります。
次に「エフェクト」をクリックして「SpectrumAnalyzer」を選びます。
音量を引っ張るためには「SpectrumAnalyzer」が都合良いみたいでしたので、これを使います。
AudioStreamPlayer2Dの追加
ボイスを再生するためのプレイヤー「AudioStreamPlayer2D」ノードを追加します。
追加したら、インスペクタから「Bus」を「Voice」に変更します。
これによりこのプレイヤーで再生したサウンドは「Voice」バスに送られます。
Labelの追加
音量を数値で確認するために Labelノードを追加しておきます
これはデバッグ用なので、リップシンクに必要というわけではありません
スクリプトの実装
Mainノードにスクリプトを
extends Node2D
const FREQ_MAX = 11050.0 # 最大周波数
const VU_COUNT = 16 # 周波数の分解度
const MIN_DB = 60
var spectrum = null # スペクトラムエフェクト
var db_now = 0.0 # 現在の音量
var past_time = 0.0 # 経過時間
var db_list = [] # 音量の平均を取るためのリスト
onready var player = $AudioStreamPlayer2D
onready var label = $Label
onready var sprite = $AnimatedSprite
func _ready():
# スペクトラムエフェクトを取得
# バス番号1 (Voice)
# エフェクト番号0 (Spectrum)
spectrum = AudioServer.get_bus_effect_instance(1, 0)
func _process(_delta):
if Input.is_action_just_pressed("ui_accept"):
# ボイス再生
player.stream = load("res://test1.mp3")
#player.stream = load("res://test2.mp3")
player.play()
# 最大音量を取得.
var max_energy = _get_max_energy()
db_list.append(max_energy)
# 経過時間を加算
past_time += _delta
if past_time > 0.2:
# 0.2秒ごとに平均を求める
_calc_db_average()
past_time = 0
if player.playing == false:
# 再生していなければ音量を0にする
db_now = 0
# リップシンク更新
_update_lipsync()
label.text = "Volume: %3.2f"%db_now
func _calc_db_average():
# 音量の平均を求める
var sum = 0
for v in db_list:
sum += v
# 平均を求める
db_now = sum / db_list.size()
db_list.clear()
func _get_max_energy():
# 音量の最大値を取得する
var prev_hz = 0
var max_energy = 0
# 各周波数ごとの音量から最大値を取り出す
for i in range(1, VU_COUNT+1):
var hz = i * FREQ_MAX / VU_COUNT;
var magnitude: float = spectrum.get_magnitude_for_frequency_range(prev_hz, hz).length()
var energy = clamp((MIN_DB + linear2db(magnitude)) / MIN_DB, 0, 1)
if energy > max_energy:
max_energy = energy # 最大音量
prev_hz = hz
return max_energy
func _update_lipsync():
if db_now < 0.1:
# 口パク停止
sprite.play("default")
else:
if db_now > 0.4:
# 大きい口パク
sprite.play("lipsync2")
else:
# 小さい口パク
sprite.play("lipsync1")
実行して動作を確認します。
口パクアニメーションの速度が 10フレーム だと少し速いような気がしたので、7フレームに減らしました。
Enter/Spaceキーを押すとボイスが再生され、それに合わせてキャラクターが口パクします。
スクリプトの簡単な説明ですが、音量を計算する際に、0.2秒ごとにその区間の音量の平均を取っていますが、これには理由があります。
音量というのは小さい区間では上下の変動が大きいです。なので平均を取らずに口の大きさを決めてしまうと、今回のような口の大きさのパターンが少ない場合は口の動きが激しくなってしまいます。
そのため、0.2秒くらいで口パクの変動を行うのが良いようです。
完成プロジェクト
今回作成したプロジェクトファイルを添付しておきます