🔉

USoundWaveにツール等で生成した音データを入れて再生する(UE5.0~5.5まとめ)

2024/12/20に公開

Unreal Engine (UE) Advent Calendar 2024、シリーズ1、20日目の記事です。

前に書いたUSoundWaveにツール等で生成した音データを入れて再生するの続編です。
C++でVOICEVOX等の外部ツールから生成した音データを元にUSwondWaveを作成する場合、5.0、5.1~5.3、5.4以降で結構変わっているため、改めて記事を作成します。

前提条件

  • Unreal C++オンリーです。ある程度C++が分かる人向けの記事です。
  • UE5.0~5.5で検証を行いました。
  • UE4.27以下は内容が違う可能性が高いです。
  • UE5.6以上で大幅に変わる可能性が高いです。

開発環境

  • Windows11
  • Unreal Engine 5.0~5.5
  • JetBrains Rider 2024.3

C++で音データを再生のみしたい場合(USoundWaveProcedural)

C++で音データを再生処理だけ行いたい、アセットとして保存する必要が無い場合はUSwondWaveの派生クラス、USoundWaveProceduralを利用します。

再生だけであれば、USwondWaveを利用することも可です。
ただし、USwondWaveは内部が5.0から5.5でかなり変わっており、プロジェクトのアップデートのたびに変更点を修正するのは非常に大変なので、アセットとして保存する必要が無ければUSoundWaveProceduralを使用するのが基本的に良さそうです。

フォーラムで非常にわかりやすい回答があったので引用します。

The regular USoundWave isn’t well-suited for dynamically filling in audio data, but is more geared towards serialization on the editor side for later runtime use, so this runtime sound wave creation behavior can vary between different UE versions. From my experience, it’s common for its behavior to change across versions, including differences in how it handles compressed audio buffers and the ways for getting and populating audio data.

I recommend decoding the audio data to PCM manually and then queuing it into USoundWaveProcedural (which is inherited from USoundWave), as this approach maintains consistent behavior across different engine versions. For example, RuntimeAudioImporter uses the same approach and works consistently and correctly from UE 4.24 through the latest version, 5.4, and it can handle WAV formats. You can check it out here: https://www.fab.com/listings/66e0d72e-982f-4d9e-aaaf-13a1d22efad1

以下、モノラルでのサンプルコードです。

FString ErrorMessage = "";

if (FWaveModInfo WaveInfo; WaveInfo.ReadWaveInfo(PCMData.GetData(), PCMData.Num(), &ErrorMessage))
{
    USoundWaveProcedural* Sound = NewObject<USoundWaveProcedural>(USoundWaveProcedural::StaticClass());
    const int32 ChannelCount = *WaveInfo.pChannels;
    const int32 SizeOfSample = *WaveInfo.pBitsPerSample / 8;
    const int32 NumSamples = WaveInfo.SampleDataSize / SizeOfSample;
    const int32 NumFrames = NumSamples / ChannelCount;
    
    Sound->RawPCMDataSize = WaveInfo.SampleDataSize;
    Sound->QueueAudio(WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);
    
    Sound->Duration = static_cast<float>(NumFrames) / *WaveInfo.pSamplesPerSec;
    Sound->SetSampleRate(*WaveInfo.pSamplesPerSec);
    Sound->NumChannels = ChannelCount;
    Sound->TotalSamples = *WaveInfo.pSamplesPerSec * Sound->Duration;
    Sound->SoundGroup = SOUNDGROUP_Default;
}

以下は自分が趣味で開発しているVoicevoxEngineForUEプラグインから音声データを生成してUSoundWaveProceduralで返すBlueprint公開ノードの例です。


	UFUNCTION(BlueprintCallable, Category="VOICEVOX Engine", meta=(Keywords="voicevox", DisplayName = "VoicevoxQueryAssetOutput"))
	static UPARAM(DisplayName="Sound") USoundWave* VoicevoxQueryOutput(UVoicevoxQuery* VoicevoxQuery, bool bEnableInterrogativeUpspeak = true);

USoundWave* UVoicevoxBlueprintLibrary::VoicevoxQueryOutput(UVoicevoxQuery* VoicevoxQuery, bool bEnableInterrogativeUpspeak)
{
	if (VoicevoxQuery == nullptr) return nullptr;

    // VOICEVOXから音データを生成
	if (const TArray<uint8> OutputWAV = GEngine->GetEngineSubsystem<UVoicevoxCoreSubsystem>()->RunSynthesis(*VoicevoxQuery, bEnableInterrogativeUpspeak); !OutputWAV.IsEmpty())
	{
                FString ErrorMessage = "";
	
        	if (FWaveModInfo WaveInfo; WaveInfo.ReadWaveInfo(OutputWAV.GetData(), OutputWAV.Num(), &ErrorMessage))
        	{
        		USoundWaveProcedural* Sound = NewObject<USoundWaveProcedural>(USoundWaveProcedural::StaticClass());
        		const int32 ChannelCount = *WaveInfo.pChannels;
        		const int32 SizeOfSample = *WaveInfo.pBitsPerSample / 8;
        		const int32 NumSamples = WaveInfo.SampleDataSize / SizeOfSample;
        		const int32 NumFrames = NumSamples / ChannelCount;
        		
        		Sound->RawPCMDataSize = WaveInfo.SampleDataSize;
        		Sound->QueueAudio(WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);
        		
        		Sound->Duration = static_cast<float>(NumFrames) / *WaveInfo.pSamplesPerSec;
        		Sound->SetSampleRate(*WaveInfo.pSamplesPerSec);
        		Sound->NumChannels = ChannelCount;
        		Sound->TotalSamples = *WaveInfo.pSamplesPerSec * Sound->Duration;
        		Sound->SoundGroup = SOUNDGROUP_Default;
        		
        		return Sound;
        	}
	}

	return nullptr;
}

Blueprint公開ノードでも、USwondWaveの戻り値でUSoundWaveProceduralを戻しても問題なく再生します

重要なことは前回の記事に記載した通り、

  • RawPCMDataSizeとUSoundWaveはRawPCMData、USoundWaveProceduralはQueueAudioWavのデータを直接入れない
    • 再生開始時にポップノイズが高確率で発生するため
    • UE用のWavデータはReadWaveInfoでデータを読み込ませ、WaveInfo.SampleDataStart、WaveInfo.SampleDataSizeの範囲をUSoundWaveはRawPCMData、USoundWaveProceduralはQueueAudioに格納する

上記が最低限守れれば、USoundWaveProceduralの再生処理は実装出来ると思います。

C++でUSwondWaveを作成(またはアセットとして保存)したい場合

音データをc++でUSwondWaveをアセットで保存する処理を実装したい場合、基本的にはそれぞれのアセットに対応したFactoryクラスを経由して作成します。
USwondWaveもUSoundFactoryクラスがありますが、エディターのインポートしか対応していません

以下、エンジンソースコードからの引用ですが、USwondWaveを生成するCreateObjectがprivateになっているからですね。

private:
	void UpdateTemplate();

	TWeakObjectPtr<USoundWave> TemplateSoundWave;


	UObject* CreateObject(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, const TCHAR* FileType, const uint8*& Buffer, const uint8* BufferEnd, FFeedbackContext* Warn);

普通の使い方していれば音データはエディターにドラッグ&ドロップで済むので、CreateObjectが隠蔽されているのは当たり前ですね。
そのため、USoundFactoryは音データをSwondWaveアセットに作成して保存したい場合は、Factoryクラスを自作する必要があります。

自作するならFactoryクラスでUSwondWaveを生成する処理を実装しないといけないですが、USwondWaveはバージョンアップごとに大幅に変更するため、バージョンごとに検証が必要になります。

UE5では、以下のバージョンで大幅に更新が入っています。

  • 5.0
  • 5.1
  • 5.4

UE5.0

FString ErrorMessage = "";
if (FWaveModInfo WaveInfo; WaveInfo.ReadWaveInfo(OutputWAV.GetData(), OutputWAV.Num(), &ErrorMessage))
{
    // Factoryクラスで生成したと仮定した場合
    USoundWave* Sound = NewObject<USoundWave>(InParent, InClass, InName, Flags, Context);
    // StaticClassでも可、単に再生したい場合はこちら
    //USoundWave* Sound = NewObject<USoundWave>(USoundWave::StaticClass());

    const int32 ChannelCount = *WaveInfo.pChannels;
    const int32 SizeOfSample = *WaveInfo.pBitsPerSample / 8;
    const int32 NumSamples = WaveInfo.SampleDataSize / SizeOfSample;
    const int32 NumFrames = NumSamples / ChannelCount;

    Sound->RawPCMDataSize = WaveInfo.SampleDataSize;
    Sound->RawPCMData = static_cast<uint8*>(FMemory::Malloc(WaveInfo.SampleDataSize));
    FMemory::Memmove(Sound->RawPCMData, WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);

    Sound->RawData.Lock(LOCK_READ_WRITE);
    void* LockedData = Sound->RawData.Realloc(OutputWAV.Num());
    FMemory::Memcpy(LockedData, OutputWAV.GetData(), OutputWAV.Num());
    Sound->RawData.Unlock();

    Sound->Duration = static_cast<float>(NumFrames) / *WaveInfo.pSamplesPerSec;
    Sound->SetSampleRate(*WaveInfo.pSamplesPerSec);
    Sound->NumChannels = ChannelCount;
    Sound->TotalSamples = *WaveInfo.pSamplesPerSec * Sound->Duration;
    Sound->SoundGroup = SOUNDGROUP_Default;
}

最低限上記の変数にセットすれば再生処理、アセット保存が可能です。
アセットとして保存する場合はRawDataにWavデータを格納してください。RawDataが空の場合、再度プロジェクトを開いた時に音データを読み込むことが出来ず、無音のデータになってしまいます。

UE5.1~5.3

5.1でUSwondWaveに結構な変更が入りました。
5.1~5.3でUSwondWaveを作成(またはアセットとして保存)したい場合は以下の通りです。

FString ErrorMessage = "";
if (FWaveModInfo WaveInfo; WaveInfo.ReadWaveInfo(OutputWAV.GetData(), OutputWAV.Num(), &ErrorMessage))
{
    // Factoryクラスで生成したと仮定した場合
    USoundWave* Sound = NewObject<USoundWave>(InParent, InClass, InName, Flags, Context);
    // StaticClassでも可、単に再生したい場合はこちら
    //USoundWave* Sound = NewObject<USoundWave>(USoundWave::StaticClass());

    const int32 ChannelCount = *WaveInfo.pChannels;
    const int32 SizeOfSample = *WaveInfo.pBitsPerSample / 8;
    const int32 NumSamples = WaveInfo.SampleDataSize / SizeOfSample;
    const int32 NumFrames = NumSamples / ChannelCount;

    Sound->RawPCMDataSize = WaveInfo.SampleDataSize;
    Sound->RawPCMData = static_cast<uint8*>(FMemory::Malloc(WaveInfo.SampleDataSize));
    FMemory::Memmove(Sound->RawPCMData, WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);

    const FSharedBuffer UpdatedBuffer = FSharedBuffer::Clone(OutputWAV.GetData(), OutputWAV.Num());
    Sound->RawData.UpdatePayload(UpdatedBuffer);
    
    Sound->Duration = static_cast<float>(NumFrames) / *WaveInfo.pSamplesPerSec;
    Sound->SetSampleRate(*WaveInfo.pSamplesPerSec);
    Sound->NumChannels = ChannelCount;
    Sound->TotalSamples = *WaveInfo.pSamplesPerSec * Sound->Duration;
    Sound->SoundGroup = SOUNDGROUP_Default;
}

変更部分は以下の通りです。

#if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 0)
		Sound->RawData.Lock(LOCK_READ_WRITE);
		void* LockedData = Sound->RawData.Realloc(OutputWAV.Num());
		FMemory::Memcpy(LockedData, OutputWAV.GetData(), OutputWAV.Num());
		Sound->RawData.Unlock();
#else
		const FSharedBuffer UpdatedBuffer = FSharedBuffer::Clone(OutputWAV.GetData(), OutputWAV.Num());
		Sound->RawData.UpdatePayload(UpdatedBuffer);
#endif

これは5.1以降からRawDataのが変更された影響です。

// 5.0
// Uncompressed wav data 16 bit in mono or stereo - stereo not allowed for multichannel data
FByteBulkData RawData;
// 5.1以降(EDITER用変数に変更)
UE::Serialization::FEditorBulkData RawData;

5.0まではRawDataに格納する前にLockとUnlockを実行する必要がありましたが、5.1以降ではFShardBufferに音データをコピー後、RawDataに書き込むように変更されました。
シンプルに書けるようになって良いですね。

もう一つ変更点として、5.1からEditor用の変数になりました。
RawDataは元の音声データを格納する変数であり、再生処理では一切使用しないのでEditor専用の変数になるのも納得です。

UE5.4~5.5

5.4でまた大きな変更が加わりました。
5.4~5.5でUSwondWaveを作成(またはアセットとして保存)したい場合は以下の通りです。

FString ErrorMessage = "";
if (FWaveModInfo WaveInfo; WaveInfo.ReadWaveInfo(OutputWAV.GetData(), OutputWAV.Num(), &ErrorMessage))
{
    // Factoryクラスで生成したと仮定した場合
    USoundWave* Sound = NewObject<USoundWave>(InParent, InClass, InName, Flags, Context);
    
    const int32 ChannelCount = *WaveInfo.pChannels;
    const int32 NumSamples = Audio::SoundFileUtils::GetNumSamples(OutputWAV);
    const int32 NumFrames = NumSamples / ChannelCount;
    
    Sound->InvalidateCompressedData(true, false);
    const FSharedBuffer UpdatedBuffer = FSharedBuffer::Clone(OutputWAV.GetData(), OutputWAV.Num());
    Sound->RawData.UpdatePayload(UpdatedBuffer);
    
    Sound->RawPCMDataSize = WaveInfo.SampleDataSize;
    Sound->RawPCMData = static_cast<uint8*>(FMemory::Malloc(WaveInfo.SampleDataSize));
    FMemory::Memmove(Sound->RawPCMData, WaveInfo.SampleDataStart, WaveInfo.SampleDataSize);
    
    Sound->Duration = static_cast<float>(NumFrames) / *WaveInfo.pSamplesPerSec;
    Sound->SetImportedSampleRate(*WaveInfo.pSamplesPerSec);
    Sound->SetSampleRate(*WaveInfo.pSamplesPerSec);
    Sound->NumChannels = ChannelCount;
    Sound->TotalSamples = *WaveInfo.pSamplesPerSec * Sound->Duration;
    Sound->SoundGroup = SOUNDGROUP_Default;

    const bool bRebuildStreamingChunks = FPlatformCompressionUtilities::IsCurrentPlatformUsingStreamCaching();
    Sound->InvalidateCompressedData(true, bRebuildStreamingChunks);
    if (bRebuildStreamingChunks && Sound->IsStreaming(nullptr))
    {
        Sound->LoadZerothChunk();
    }
    
    GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(this, Sound);
    Sound->PostImport();
    Sound->SetRedrawThumbnail(true);
}

5.4から二点変更が加わりました。

  • Audio::SoundFileUtils追加
  • InvalidateCompressedDataの実行が必須

Audio::SoundFileUtils追加

5.4からAudio::SoundFileUtilsが追加されました。
5.5までは4つの関数があり、その中でユーザーが使用できそうなのは以下の二つです。

  • GetNumSamples
    • サンプルレートを取得する関数です。
      これを使用することで簡単にサンプルレートを取得できます。
      サンプルレートのための計算式を書かなくてよくなるので便利です。
  • ConvertAudioToWav
    • 音データをWavファイルへ変換する関数です。
      こちらは恐らく外部からogg等の音ファイルをインポートした時に使用すると思われるので、生成した音データがWav、もしくは生データであれば使用しなくて大丈夫です。

InvalidateCompressedDataの実行が必須

5.4以降はUSwondWave生成時に必ずInvalidateCompressedDataを実行しないといけません

InvalidateCompressedDataを実行しない場合、最初の数秒で再生が強制的にストップしてしまいます
上記はアセット生成時、もしくはUSwondWaveを保存せずにプログラム上で再生した時に発生し、エディタを再起動すれば正常に再生されるようになります

5.4以降の再生処理の変更点は把握してないですが、恐らく再生時に圧縮データから見るように変更が加えられていると思われます。

参考資料

今回の記事を書くのに参考にした資料一覧です。

FSoundWavePCMWriter not working in UE 5.4.3 ?

UE5.1, How to access SoundWave’s ResourceData in packaged Build?

Runtime Audio Importing

How can I use USoundWaveStreaming?

How to Reliably Access USoundWave PCM Data (with or without FAsyncAudioDecompress)

AudioComponent not playing sound in PIE.

Discussion