멈추지 않고 끈질기게

[Unreal] 최종 프로젝트 2일차 - wav 파일 런타임에 출력하기 본문

포트폴리오

[Unreal] 최종 프로젝트 2일차 - wav 파일 런타임에 출력하기

sam0308 2024. 5. 13. 18:18

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

※ 해당 포스팅은 청년취업사관학교 교육 과정의 최종 프로젝트에 관한 내용을 포함하고 있습니다.

 

※ 해당 포스팅은 Unreal 5.4.1 버전을 기준으로 작성되었습니다.

 

 

 

0. 서론

 STT는 생각보다는 금방 구현되었고, 기왕 마이크를 사용하는 김에 NPC의 대화까지 TTS로 음성으로 출력하기로 얘기가 되었습니다. 챗봇을 담당하신 분이 TTS로 음성 파일을 생성하는 부분을 작업하시기로 했고, 저는 언리얼에서 런타임 중에 음성 파일을 읽어 출력하는 로직을 구현하기로 했습니다.

 

 

1. 레퍼런스(블루프린트)

 일단 블루프린트 기반으로 wav 파일을 읽어오는 로직의 레퍼런스는 금방 찾을 수 있었습니다.

https://www.youtube.com/watch?v=SmDuNgZZCwU

 

MediaSoundComponent와 MediaPlayer 객체를 통해 실행하는 방식으로, 그대로 따라해보니 금방 wav 파일을 실행할 수 있었습니다. 해당 로직을 별도의 함수로 만든 다음에, 게임모드에서 STT를 통해 NPC에게 말을 걸고 답변을 받는 부분에서 실행하도록 했습니다.

 

 

2. C++로 옮기기

 블루프린트로 옮겨놓고 보니 C++로 옮기는 작업이 크게 어렵지 않아보여서, 해당 작업까지 진행하였습니다. 유의할 점은 Build.cs 파일에 MediaAssets 모듈을 추가하는 정도였습니다.

ANPCBase::ANPCBase()
{
	/* 기타 내용 */

	// Media Sound Component
	MediaComp = CreateDefaultSubobject<UMediaSoundComponent>(TEXT("MediaComp"));
}

#pragma region TTS
void ANPCBase::LoadSpeechFileAndPlay(const FString& FilePath)
{
	if (MediaPlayer->OpenFile(FilePath))
	{
		MediaComp->SetMediaPlayer(MediaPlayer);
		UE_LOG(LogTemp, Warning, TEXT("Open File Success : %s"), *FilePath);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Open File Failed : %s"), *FilePath);
	}
}
#pragma endregion

 

그런데 해당 코드로 wav 파일을 읽어와서 실행하려고 하니 음성이 출력되지 않는 이슈가 발생하였습니다. C++로 옮기는 과정에서 생긴 이슈인가 싶어서 우선 레퍼런스와 똑같이 BeginPlay()에서 실행해보았는데, 이 경우에는 또 잘 실행되었습니다. 그래서 BeginPlay()에서 먼저 한번 실행하고, 음성 입력을 받을 때마다 실행하게 했더니 잘 실행되었습니다. 초기화 관련 이슈인 듯 한데 관련 내용은 아직 찾지 못했습니다.

 

ANPCBase::ANPCBase()
{
	/* 기타 내용 */

	// Media Sound Component
	MediaComp = CreateDefaultSubobject<UMediaSoundComponent>(TEXT("MediaComp"));
}

void ANPCBase::BeginPlay()
{
	Super::BeginPlay();

	MediaPlayer = NewObject<UMediaPlayer>();
	LoadSpeechFileAndPlay(FilePath);
}

#pragma region TTS
void ANPCBase::LoadSpeechFileAndPlay(const FString& FilePath)
{
	if (MediaPlayer->OpenFile(FilePath))
	{
		MediaComp->SetMediaPlayer(MediaPlayer);
		UE_LOG(LogTemp, Warning, TEXT("Open File Success : %s"), *FilePath);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Open File Failed : %s"), *FilePath);
	}
}
#pragma endregion

 

 그런데 이번엔 다른 이슈가 발생했습니다. 같은 말만 반복해서 발견을 못했었는데, 음성이 새로 입력한 내용으로 덮어씌워지지 않고 있었습니다. 로그를 확인해보니 MediaPlayer에서 이미 Open중인 파일을 덮어씌우려고 해서 에러가 발생하는 것으로 보였습니다.

 

 

3. 델리게이트 설정(OnEndReached)

 해당 이슈를 해결하려면 실행이 끝난 후에 MediaPlayer가 자동으로 파일을 Close 해줄 필요가 있어 보였습니다. 다행히 스트림을 닫는 함수는 아주 직관적으로 Close() 함수가 있어 금방 찾았습니다. 실행이 완료된 시점에 호출될 델리게이트가 다소 이름이 직관적이지 않아 찾는데 시간이 좀 걸렸는데, OnEndReached라는 이름으로 존재했습니다. 아래와 같이 코드를 수정하고 나서 wav 파일이 제대로 덮어씌워지는 것을 확인할 수 있었습니다. 

 

ANPCBase::ANPCBase()
{
	/* 기타 내용 */

	// Media Sound Component
	MediaComp = CreateDefaultSubobject<UMediaSoundComponent>(TEXT("MediaComp"));
}

void ANPCBase::BeginPlay()
{
	Super::BeginPlay();

	MediaPlayer = NewObject<UMediaPlayer>();
	MediaPlayer->OnEndReached.AddDynamic(this, &ANPCBase::OnPlayEnded);

	LoadSpeechFileAndPlay(FilePath);
}

#pragma region TTS
void ANPCBase::LoadSpeechFileAndPlay(const FString& FilePath)
{
	if (MediaPlayer->OpenFile(FilePath))
	{
		MediaComp->SetMediaPlayer(MediaPlayer);
		UE_LOG(LogTemp, Warning, TEXT("Open File Success : %s"), *FilePath);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Open File Failed : %s"), *FilePath);
	}
}

void ANPCBase::OnPlayEnded()
{
	MediaPlayer->Close();
}
#pragma endregion

 

 BeginPlay()에서 MediaPlayer 객체를 생성한 후에, OnEndReached에 OnPlayEnded() 함수를 바인딩 해주었습니다. OnPlayEnded()는 단순히 MediaPlayer 의 Close() 함수를 호출하여 파일을 Close 하도록 작성했습니다.

 

 다만 BeginPlay()에서 LoadSpeechFileAndPlay() 함수를 1번 호출하는 바람에 게임 시작 후에 무의미한 음성 출력이 1번 발생해버리는데, 해당 부분이 예상 외로 해결이 되지 않았습니다. BeginPlay()의 호출 부분을 제외하면 아예 음성 출력이 안되어버리고,  MediaPlayer의 SetNativeVolume() 함수로 볼륨을 조절하여 제어해보려고도 했는데 결국 성공하지 못했습니다. 일단 내일은 챗봇을 담당한 분의 구현 결과와 합치는 작업을 진행할 예정이라 차후에 수정해야 할 듯 싶습니다.