공부중

[UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 1 본문

Programing/UnrealEngine

[UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 1

곤란 2024. 7. 15. 21:22
반응형

들어가기전 잡담을 좀 하자면 6월초에 수술을 받고 6월 내내 누워 지내다가 이제서야 어느정도 회복이 되어서 앉을수 있게 되었다 하하하하하하 건강챙기자.. -_-...


이전 회사에서 했던 작업중 마이크를 통해 전달받은 음성 데이터를 wav로 저장해야 하는 일이 있었다.

이 wav파일을 음질변환 같은것도 해야 했었고.

wav파일을 동적으로 파일을 읽어서 재생도 해야했다.

이 글은 그중 일단 마이크를 통해 받은 음성 데이터를 wav로 저장하는 방법에 대해서 적으려고 한다.

 

일단 {프로젝트명}.Build.cs파일에서 "AudioMixer" 모듈을 추가해 주어야 한다.

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class VoiceRecordProj : ModuleRules
{
	public VoiceRecordProj(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { 
			"Core", 
			"CoreUObject", 
			"Engine", 
			"InputCore", 
			"EnhancedInput", 
			"UMG",
			"AudioMixer",
		});
	}
}

여기서 내가 만든 프로젝트 명은 VoiceRecordProj 이므로 VoiceRecordProj.Build.cs 파일에서 위와같이 AudioMixer를 추가해 주었다.

이제 ComponentClass를 만들어주자.

AudioCaptureComponent를 상속받아서 클래스를 만들어 주자.

나는 클래스 이름을 VoiceRecordComponent으로 명명했다.

UAudioCaptureComponent를 상속받은 클래스가 생성되면 아래와 같이 자동으로 뭔가 만들어진 코드들이 있다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "AudioCaptureComponent.h"
#include "DSP/Osc.h"
#include "VoiceRecordComponent.generated.h"

// ========================================================================
// UVoiceRecordComponent
// Synth component class which implements USynthComponent
// This is a simple hello-world type example which generates a sine-wave
// tone using a DSP oscillator class and implements a single function to set
// the frequency. To enable example:
// 1. Ensure "SignalProcessing" is added to project's .Build.cs in PrivateDependencyModuleNames
// 2. Enable macro below that includes code utilizing SignalProcessing Oscilator
// ========================================================================

#define SYNTHCOMPONENT_EX_OSCILATOR_ENABLED 0

UCLASS(ClassGroup = Synth, meta = (BlueprintSpawnableComponent))
class VOICERECORDPROJ_API UVoiceRecordComponent : public UAudioCaptureComponent
{
	GENERATED_BODY()
	
	// Called when synth is created
	virtual bool Init(int32& SampleRate) override;

	// Called to generate more audio
	virtual int32 OnGenerateAudio(float* OutAudio, int32 NumSamples) override;

	// Sets the oscillator's frequency
	UFUNCTION(BlueprintCallable, Category = "Synth|Components|Audio")
	void SetFrequency(const float FrequencyHz = 440.0f);

protected:
#if SYNTHCOMPONENT_EX_OSCILATOR_ENABLED
	// A simple oscillator class. Can also generate Saw/Square/Tri/Noise.
	Audio::FOsc Osc;
#endif // SYNTHCOMPONENT_EX_OSCILATOR_ENABLED
};

일단 이렇게 만든 Component를 사용할 Actor를 간단하게 만들어보자.

UCLASS()
class VOICERECORDPROJ_API ARecordActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARecordActor();

protected:

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VoiceRecordComponent")
	TObjectPtr<UVoiceRecordComponent>			_VoiceRecordComponent;

};

//===============================================================

#include "RecordActor.h"
#include "Component/VoiceRecordComponent.h"

// Sets default values
ARecordActor::ARecordActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;

	_VoiceRecordComponent = CreateDefaultSubobject<UVoiceRecordComponent>(TEXT("VoiceRecordComponent"));
	if (!_VoiceRecordComponent)
	{
		UE_LOG(LogTemp, Warning, TEXT("CreateDefaultSubobject UVoiceRecordComponent Fail"));
	}

}

그냥 Actor를 상속받아 만들고 BeginPlay와 Tick은 지웠다.

 

이제 본격적인 녹음관련 코드를 작성해보자 위에서 만든 VoiceRecordComponent에 아래 내용을 추가해준다.

#include "Sound/SampleBufferIO.h"	// Audio::FAudioRecordingData
// 위의 include를 추가해준다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnStartVoiceRecord);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnStopVoiceRecord);

// 생략..

UCLASS(ClassGroup = Synth, meta = (BlueprintSpawnableComponent))
class VOICERECORDPROJ_API UVoiceRecordComponent : public UAudioCaptureComponent
{
	GENERATED_BODY()
	
    /// 생략
    
public:
	UFUNCTION(BlueprintCallable, Category = "VoiceRecordComponent")
	void			StartRecord();

	UFUNCTION(BlueprintCallable, Category = "VoiceRecordComponent")
	void			StopRecord(const bool bOnlyStop = false);

	UFUNCTION(BlueprintCallable, Category = "VoiceRecordComponent")
	const bool		IsRecording() const;

public:
	FOnStartVoiceRecord& GetOnStartVoiceRecord() { return _OnStartVoiceRecord; }
	FOnStopVoiceRecord& GetOnStopVoiceRecord() { return _OnStopVoiceRecord; }

protected:
	UPROPERTY(EditAnywhere, Category = "VoiceRecordComponent")
	TObjectPtr<USoundSubmix>		_VoiceRecordSoundSubmix;

	TUniquePtr<Audio::FAudioRecordingData>	_RecordingData;

protected:

	FOnStartVoiceRecord							_OnStartVoiceRecord;
	FOnStopVoiceRecord							_OnStopVoiceRecord;

};

일단 cpp에는 빈칸으로 정의해두고

이제 에디터로 잠시 이동해서 서브믹스(Submixes)를 만들어보자.

서브믹스가 뭔지는 아래의 공식 문서를 읽어보는것으로..

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/overview-of-submixes-in-unreal-engine

 

Sound폴더를 만들어주고

"마우스 오른쪽 버튼 - 오디오 - 믹스 - 사운드 서브믹스"에서 서브믹스를 만들 수 있다.

 

서브믹스의 이름은 Record로 명명했다.

위에서 만든 서브믹스를 열어보면..

만들었던 서브믹스의 이름과 같은 노드가 있는데 여기서 출력을 드래그해보자

새 사운드 서브 믹스를 추가하는 노드가 나오는데 여기서 이름을 "RecordRecv"로 명명했다.

제데로 만들었다면 Record의 출력이 RecordRecv의 입력으로 들어갈 것이다.

만일 연결이 안되어있으면 연결해주면 된다.

연결이 되면 Rocord와 RecordRecv의 부모 자식 관계를 볼 수있다.

그리고 RecordRecv에서 "서브믹스 레벨"이 있는데 여기서의 출력 볼륨(db)웻 레벨(db)을 조절해 줄 수 있다.

기본 값은 0으로 되어있는데 이 상태에서는 마이크로 들어간 목소리가 그대로 스피커에 다시 나오게 된다.

마이크로 들어간 목소리를 스피커에서 재생 시켜주지 않으려면 출력볼룸과 웻 레벨을 적절하게 낮추어주면 된다.

 

다시 코드로 돌아와서..

bool UVoiceRecordComponent::Init(int32& SampleRate)
{
	NumChannels = 1;

#if SYNTHCOMPONENT_EX_OSCILATOR_ENABLED
	// Initialize the DSP objects
	Osc.Init(SampleRate);
	Osc.SetFrequency(440.0f);
	Osc.Start();
#endif // SYNTHCOMPONENT_EX_OSCILATOR_ENABLED

/// Add >>>>>>>
	if (_VoiceRecordSoundSubmix)
	{
		this->SoundSubmix = _VoiceRecordSoundSubmix;
	}
/// Add <<<<<<<

	return true;
}

int32 UVoiceRecordComponent::OnGenerateAudio(float* OutAudio, int32 NumSamples)
{
#if SYNTHCOMPONENT_EX_OSCILATOR_ENABLED
	// Perform DSP operations here
	for (int32 Sample = 0; Sample < NumSamples; ++Sample)
	{
		OutAudio[Sample] = Osc.Generate();
	}
#endif // SYNTHCOMPONENT_EX_OSCILATOR_ENABLED

	return Super::OnGenerateAudio(OutAudio, NumSamples);
}

기본으로 만들어진 Init에 Add 부분을 추가했다.

그리고 OnGenerateAudio 에서 원래 return NumSamples; 코드를 return Super::OnGenerateAudio(OutAudio, NumSamples); 으로 변경해준다.

 

void UPPVoiceRecordComponent::StartRecord()
{
	this->Start();
	UAudioMixerBlueprintLibrary::StartRecordingOutput(GetWorld(), 0.0f, _VoiceRecordSoundSubmix.Get());
    
    	_OnStartVoiceRecord.Broadcast();
}

const bool UPPVoiceRecordComponent::IsRecording() const
{
	// StartRecord의 this->Start() 내부에서 Play를 해주고 있다.
	// 녹음 시작시 this->Start() 호출
	// 녹음 종료시 	this->Stop() 호출
	return IsPlaying();
}

StartRecord와 IsRecording을 작성해 준다.

StartRecording에서 _OnStartVoiceRecord Delegate를 Broadcast 해주고 있다.

void UVoiceRecordComponent::StopRecord(const bool bOnlyStop)
{
	if (!IsRecording())
	{
		return;
	}

	if (!bOnlyStop)
	{
		FString fileDirectoryPath = FPaths::ProjectSavedDir();
		// 아래 내용은 UAudioMixerBlueprintLibrary::StopRecordingOutput를 참조..
		Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(GetWorld());
		if (MixerDevice)
		{
			float OutSampleRate;
			float OutChannelCount;

			Audio::FAlignedFloatBuffer& RecordedBuffer = MixerDevice->StopRecording(_VoiceRecordSoundSubmix.Get(), OutChannelCount, OutSampleRate);

			if (RecordedBuffer.Num() == 0)
			{
				UE_LOG(LogTemp, Warning, TEXT("No audio data. Did you call Start Recording Output?"));
				return;
			}

			_RecordingData.Reset(new Audio::FAudioRecordingData());
			_RecordingData->InputBuffer = Audio::TSampleBuffer<int16>(RecordedBuffer, OutChannelCount, OutSampleRate);

			_RecordingData->Writer.BeginWriteToWavFile(_RecordingData->InputBuffer, TEXT("RecordVoiceFile"), fileDirectoryPath, [this]()
				{
					if (_VoiceRecordSoundSubmix && _VoiceRecordSoundSubmix->OnSubmixRecordedFileDone.IsBound())
					{
						_VoiceRecordSoundSubmix->OnSubmixRecordedFileDone.Broadcast(nullptr);
					}

					// I'm gonna try this, but I do not feel great about it.
					_RecordingData.Reset();
				});
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext Return Value is nullptr"));
		}
	}
	this->Stop();
    	_OnStopVoiceRecord.Broadcast();
}

StopRecord에서는 녹음중일때 Wav파일로 저장하는 역활을 한다.

일단 코드를 읽어보면 알겠지만 파일 경로는 save 폴더에 RecordVoiceFile이라는 이름으로 wav 파일 저장하는 역활을 한다.

녹음을 중지하면 _OnStopVoiceRecord를 Broadcast한다.

이제 다시 빌드 후 에디터를 실행해 BP와 녹음 위젯을 만들어보자

위에서 만든 RecordActor를 상속받는 BP를 만들자.

 

BP_RecordActor를 만들었으면 열어보자

 

BP_RecordActor를 열고 코드에서 작성했던 VoiceRecordSoundSubmix에 위에서 만들었던 Record SoundSubmix를 넣어준다.

 

너무 길어서 여기까지 작성하고 다음 글에서 위젯을 생성하고 녹음되는지 확인해보자.

반응형