Godot GDEXTENSION での動的トーン生成

Godot

Godot GDEXTENSIONで動的生成したトーンを鳴動させる

何がやりたいのか

画像や点群を、GDEXTENSIONを用いて表示する方法が分かった。次は動的に生成した音を鳴動させる方法を試す。

任意のPCMデータを鳴動させることができれば、Godot エンジンを音声系信号処理のデモ環境として使うことができるようになる。

Godot(GDEXTENSIONではない)で動的生成トーンを鳴動させる

Node2Dをシーンとして設置し、名前を Prototype に変更する。その下に AudioStreamPlayer を設置し、名前を ToneGen に変更する。

ToneGen に以下のスクリプトをアタッチする。

ToneGen.gd

extends AudioStreamPlayer

var frequency = 440.0 # Frequency of A4 note
var mix_rate = 44100.0 # Sampling frq. It's also the samples num par sec.
var buffer_length = 0.1 # Buffer length in seconds
var buf_samples = int(mix_rate*buffer_length)
var phase
var increment

# Called when the node enters the scene tree for the first time.
func _ready():
	stream = AudioStreamGenerator.new()
	stream.mix_rate = mix_rate
	stream.buffer_length = buffer_length
	phase = 0.0
	increment = (2.0 * PI * frequency) / mix_rate
	play()

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	if is_playing(): 
		var playback = get_stream_playback()
		if playback.can_push_buffer(buf_samples/2): 
#			print("feed!", phase)
			var frames = PackedVector2Array()
			for i in range(0, buf_samples/2): 
				var sample = sin(phase)
				phase += increment
				frames.append(Vector2(sample, sample))
			playback.push_buffer(frames)

AudioStreamPlayer の基本的な使い方は、stream プロパティに wave 形式などの音源ファイルを指定し、play() で鳴動させるものであるが、AudioStreamGenerator のインスタンスを指定すると、PCM データを直接鳴動させることができる。AudioStreamGenerator は buffer_length という鳴動待ち PCM データバッファを時間単位で指定することができ、このバッファが枯渇する前に新しいPCMデータを供給すれば、途切れなく音を鳴動させ続けることができる。

func _ready():
	stream = AudioStreamGenerator.new()
	stream.mix_rate = mix_rate
	stream.buffer_length = buffer_length

AudioStreamGenerator インスタンスの mix_rate プロパティに 44100.0 Hzを設定している。また、buffer_length に 0.1 sec を指定する。44100.0 Hz の 0.1 sec 分であるので、サンプル数は 4410 個となる。buffer_length は値を小さくすると鳴動遅延が少なくなる代わりに新規 PCM データの供給間隔が短くなり、供給が間に合わなくなる危険性が増える。供給が間に合わなければ音が途切れることになる。逆に buffer_length の値を大きくすると供給が間に合わなくなる危険を減らすことができる代わりに、鳴動遅延が大きくなる。このトレードオフがあるので、必要に応じて設計する必要がある。

buffer に PCM データを供給可能であるか否かは、can_push_buffer() 関数で確認する。つまり、_process() 関数内などの周期実行関数から can_push_buffer() を用いて buffer の空き確認のポーリングを行い、供給可能であれば、後続 PCM データを供給する。

func _process(delta):
	if is_playing(): 
		var playback = get_stream_playback()
		if playback.can_push_buffer(buf_samples/2): 
#			print("feed!", phase)

この例では buf_samples/2 は 2205であるので、2205 個のデータ(つまり 0.05 sec 分)の PCM データが供給可能かを確認している。buf_samples/2 > delta が満たされる限り、音は途切れない。

今回の例では 位相 phase を 2πf (f=440 Hz)進めて周波数 f のサイン波 PCM データを生成させ、push_buffer() 関数を用いて buffer に供給している。

			var frames = PackedVector2Array()
			for i in range(0, buf_samples/2): 
				var sample = sin(phase)
				phase += increment
				frames.append(Vector2(sample, sample))
			playback.push_buffer(frames)

“F6” キーを押してこのシーンを実行すると灰色の画面が現れるとともに、「ポー」という音が鳴動する。

Godot GDEXTENSION で動的生成トーンを鳴動させる

上記とほぼ同じことを C++ を用いて行えるかを試す。

“C:\dev\gdextension_sample\sample_03” というディレクトリに GDEXTENSION のサンプルを作成することとする。設置ファイルツリー構造は以下の4つ。

  • gdexample.cpp
  • gdexample.h
  • register_types.cpp
  • register_types.h

各ソースコードファイルの内容は以下とする。

gdexample.h

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H
#include <godot_cpp/classes/audio_stream_player.hpp>
#include <godot_cpp/classes/audio_stream_generator.hpp>
#include <godot_cpp/classes/audio_stream_generator_playback.hpp>
namespace godot {

class ToneGen : public AudioStreamPlayer {
    GDCLASS(ToneGen, AudioStreamPlayer);

    double frequency = 440.0; // Frequency of A4 note.
    double mix_rate = 44100.0; // Sampling frq. It's also the samples num par sec.
    double buffer_length = 0.1; // Buffer length in seconds.
    int32_t buf_samples = int32_t(mix_rate*buffer_length);
    double phase;
    double increment;

	double time_passed;
protected:
    static void _bind_methods();
public:
    ToneGen();
    ~ToneGen();
    void _process(double delta);
    void _ready();
};

}

#endif

このプロジェクトでは AudioStreamPlayer クラスを継承する ToneGen クラスを GDEXTENSION として定義している。サンプルであるため、全て _process() と _ready() メソッド内で完結させている。

gdexample.cpp

#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>

#ifdef DEBUG_ENABLED
#include <godot_cpp/variant/utility_functions.hpp> // for "UtilityFunctions::print()".
#endif // DEBUG_ENABLED

#include <filesystem>

using namespace godot;

void ToneGen::_bind_methods()
{
}

ToneGen::ToneGen()
{
	time_passed = 0;
}

ToneGen::~ToneGen()
{
}

void ToneGen::_ready() {
    Ref<AudioStreamGenerator> stream = new AudioStreamGenerator();
    set_stream(stream);
    stream->set_mix_rate(mix_rate);
    stream->set_buffer_length(buffer_length);
    phase = 0.0;
    increment = (2.0 * Math_PI * frequency) / mix_rate;
    play(phase);
}

void ToneGen::_process(double delta) {
    time_passed += delta;
    if (is_playing()) {
        Ref<AudioStreamGeneratorPlayback> playback = get_stream_playback();
        if (playback->can_push_buffer(buf_samples/2)) {
#ifdef DEBUG_ENABLED
            UtilityFunctions::print("feed!", phase);
#endif // DEBUG_ENABLED
            PackedVector2Array frames = PackedVector2Array();
            for (int i =0; i < buf_samples/2; i++) {
                double sample = sin(phase);
                phase += increment;
                frames.append(Vector2(sample, sample));
            }
            playback->push_buffer(frames);
        }
    }
}

_ready() と _process() メソッドの実装では、上記 ToneGen.gd スクリプトでやっていることとほぼ同じ。

register_types.h

#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H

void initialize_example_module();
void uninitialize_example_module();

#endif // GDEXAMPLE_REGISTER_TYPES_H

このファイルははテンプレートのまま。

register_types.cpp

#include "register_types.h"

#include "gdexample.h"

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/godot.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }

    ClassDB::register_class<ToneGen>();
}

void uninitialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
}

extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
    godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);

    init_obj.register_initializer(initialize_example_module);
    init_obj.register_terminator(uninitialize_example_module);
    init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);

    return init_obj.init();
}
}

このファイルもほぼ雛型のまま。ToneGen の 登録(下記参照)と初期化タイミングの指定を行っている。

void initialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }

    ClassDB::register_class<ToneGen>();
}

ビルド

SConstruct ファイル

Sconstruct ファイルの例を以下に示す。今回は単なるお試しであるのでWindows 64bit ターゲットのみ。

#!/usr/bin/env python
import os
import sys
 
project_name = 'libgdexample'
env = SConscript('./godot-cpp-4.2.1-stable/SConstruct')
env.Append(CPPPATH=["src/"])
sources = Glob("src/*.cpp")
 
if env['platform'] == "windows" and env['use_mingw'] == True and env['arch'] == "x86_64":
    env.Append(LINKFLAGS = ['-static-libgcc', '-static-libstdc++','-static','-pthread'])
else:
    print("not suppoted conbination!")
    print("platform",env["platform"])
    print("use_mingw",env["use_mingw"])
    print("use_clang_cl",env["use_clang_cl"])
    print("use_static_cpp",env["use_static_cpp"])
    print("android_api_level",env["android_api_level"])
    print("ios_simulator",env["ios_simulator"])
    print("arch",env["arch"])
    sys.exit(1)
 
print("CPPDEFINES",env['CPPDEFINES'])
library = env.SharedLibrary(
    "project/bin/{}{}{}".format(project_name,env["suffix"], env["SHLIBSUFFIX"]),
    source=sources,
)
 
Default(library)

設置

ソースコードと SConstruct ファイルを以下のように設置する。

C:\dev\gdextension_sample\sample_03
│  SConstruct
│
└─src
        gdexample.cpp
        gdexample.h
        register_types.cpp
        register_types.h

godot-cpp はサブモジュールとして直下の ‘./godot-cpp-4.2.1-stable に設置する。

Microsoft Windows [Version 10.0.22631.2861]
(c) Microsoft Corporation. All rights reserved.

c:\Users\user>mingw64.bat
c:\dev>cd gdextension_sample

c:\dev\gdextension_sample>cd sample_03
c:\dev\gdextension_sample\sample_03>git clone --depth 1 https://github.com/godotengine/godot-cpp.git -b godot-4.2.1-stable godot-cpp-4.2.1-stable

以上でビルドの準備は整った。これを Windows ターゲットデバッグビルドを行う。

Windows プラットフォーム向けビルド

新しいコマンドプロンプトを開き以下を実行する。

Microsoft Windows [Version 10.0.22631.3007]
(c) Microsoft Corporation. All rights reserved.

C:\Users\user>.\mingw64.bat

c:\dev>cd gdextension_sample\sample_03

Windows ターゲットのデバッグビルドは以下を実行する。

c:\dev\gdextension_sample\sample_03>scons platform=windows use_mingw=yes target=template_debug

ビルドが成功すると以下の生成物を得る。

Godot エンジンプロジェクトの作成

先ず Godot エンジンプロジェクト用のディレクトリを作成する。新しいコマンドプロンプトを開き、以下を実行する。

Microsoft Windows [Version 10.0.22631.3007]
(c) Microsoft Corporation. All rights reserved.

C:\Users\user>.\mingw64.bat

c:\dev>cd demos
c:\dev\demos>mkdir demo_08
c:\dev\demos>cd demo_08

空の Godot エンジンプロジェクトを作成。

c:\dev\demos\demo_08>copy nul .\project.godot

bin ディレクトリを作成し、GDEXTENSION のダイナミックリンクライブラリを設置する。

c:\dev\demos\demo_08>mkdir bin
c:\dev\demos\demo_08>cd bin
c:\dev\demos\demo_08\bin>copy C:\dev\gdextension_sample\sample_03\project\bin\*.dll .

c:\dev\demos\demo_08\bin ディレクトリに以下のような gdextension ファイルを作成する。

c:\dev\demos\demo_08\bin\gdexample.gdextension

[configuration]

entry_symbol = "example_library_init"
compatibility_minimum = "4.1"

[libraries]
windows.debug.x86_64   = "res://bin/libgdexample.windows.template_debug.x86_64.dll"

ここまでの手順で、ディレクトリ構成は以下のようになる。

C:\dev\demos\demo_08
│  project.godot
│
└─bin\
        gdexample.gdextension
        libgdexample.windows.template_debug.x86_64.dll"

Godot エンジンプロジェクトを起動する。

c:\dev\demos\demo_08>godot.windows.editor.x86_64.exe -e .

名無しのプロジェクトが起動する。

Node2Dをシーンとして設置し、名前を Prototype に変更する。その下に今回作成したDGEXTENSION ノードである ToneGen ノードを設置する。この段階で「ポー」という音が鳴動する。

本稿の振り返り

Godot のトーン出力処理として、任意の PCM データを動的に生成し、それを鳴動させられることが確認できた。また、同様のことが GDEXTENSION C++ を用いて実現することができることも確認できた。

今回のサンプルは最小構成のものであるが、生成する PCM データを任意に変えていけば、様々に応用できるだろう。一応自分自身の備忘のため記録を残しておく。

コメント

タイトルとURLをコピーしました