공부중

[UE] WAV파일을 DownSampleing, Stereo To Mono로 변경 해보자. 본문

Programing/UnrealEngine

[UE] WAV파일을 DownSampleing, Stereo To Mono로 변경 해보자.

곤란 2024. 7. 29. 05:12
반응형

요거 카테고리를 어디에 배치 해야 할까 하다가 단순히 언리얼엔진에서 작업한거라 언리얼로 분류 했다.

물론 이를 참조해서 다른 엔진 그리고 다른언어에서도 적용 가능하다.

 


이 글은 WAV파일을 읽어서 DownSampleing 그리고 스테레오를 모노로 변경(Stereo To Mono)하는 내용을 구현한뒤 내용을 정리해보려고 작성하는 글이다.

과거 WAV파일 DownSampleing, 그리고 스테레오를 모노로 변경(Stereo To Mono) 작업을 사내에서 사용한 경험을 토대로 다시 작성하여 나중에 잊어버리면 내가 다시 볼 목적으로 적어놓는 글이며 전문적인 프로그램과는 다른 결과를 불러올 수 있으므로 자세하고 깊은 wav파일 지식은 작성하지 않으려고 한다.(더 좋은 내용의 글들이 다른곳에 더 많을것이므로...)

여기 이 글에서 말하는 Sample은(정확히는 Sampling Frequency) WAV파일에 저장된 소리로부터 초당 샘플링한 횟수를 의미한다. 쉽게 말하면 높으면 음질이 좋다!

아마 내 기억속에서의 언리얼은 녹음시 기본으로 48000Hz를 사용 했던것으로 기억한다.

프로젝트 세팅에서 각 플랫폼마다 확인할 수 있고 수정도 할 수 있다.

사내에서 관련 작업을 할 때 이를 임의로 수정하기보다는 내가 DownSampleing을 통해서 적절히 변경하는것이 나중에 사이드 이펙트가 줄지 않을까라는 생각이 들어서 작업하다보니 이런 작업도 하게 되었다 하하..

 

작성된 코드는 이전 언리얼엔진에서 마이크를 통해 wav파일로 저장해보자 글에서 작성된 코드의 연장으로 작업 되어있다.

더보기

 

 

그리고 이 작업을 하면서 참고했던 사이트들은 아래와 같다.

https://infograph.tistory.com/333

 

01. WAV 파일 구조

컴퓨터에서 소리를 담는 가장 기본적인 파일 구조가 WAV 파일 구조이다. 1999년 경부터 Microsoft와 IBM에 의해 파일 구조가 정의되어 사용되었고, PCM 방식으로 인코딩 된 디지털 신호를 압축되지 않

infograph.tistory.com

https://anythingcafe.tistory.com/2

 

WAV 파일의 헤더 구조와 Raw Data(pcm data)

우리는 .mp3 형식의 오디오 파일엔 익숙하고, .wav 형식의 오디오 파일은 다소 생소한 느낌이 있다. 과연 wav 파일은 무엇일까? Wav 는 Waveform audio format의 준말로 개인용 컴퓨터에서 오디오를 재생하

anythingcafe.tistory.com

https://psychoria.tistory.com/entry/DirectSound-1-Wave-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EA%B5%AC%EC%A1%B0

 

[DirectSound] 1. Wave 파일의 구조

DirectSound는 DirectX 중에서 음악의 재생 및 녹음과 같은 기능을 담당합니다.DirectSound를 하기 전에 기본적으로 wav 파일의 구조를 알아보겠습니다.간단하게 wav 파일은 헤더(Header) 정보 + PCM 데이터로

psychoria.tistory.com

 

그리고 아예 코드로도 만들어 놓은 외국인 형도 있었는데 위에서 헤더파일을 좀 보고 코드를 고쳐서 사용했다.

https://stackoverflow.com/questions/69085916/ue4-convert-audio-from-48-stereo-to-16-mono

 

ue4 Convert audio from 48 stereo to 16 mono

How to change sampling rate from 48000 (ue4 default) to 16000 samples and stereo to mono in a wav recording in ue4? I have searched in BPs but not lack. The image below shows what I have done with ...

stackoverflow.com

 

일단 내가 작성한 코드는 아래와 같다.

USTRUCT()
struct FWaveInfo
{
	GENERATED_BODY()

	// Pointers to variables in the in-memory WAVE file.
	uint32	SamplesPerSec	= 0;
	uint32	AvgBytesPerSec	= 0;
	uint16	BlockAlign	= 0;
	uint16	BitsPerSample	= 0;
	uint16	Channels	= 0;
	uint16	FormatTag	= 0;

	uint32	WaveDataSize	= 0;
	uint32	MasterSize	= 0;
	uint8	SampleDataStart	= 0;
	uint8	SampleDataEnd	= 0;
	uint32	SampleDataSize	= 0;
	uint8	WaveDataEnd	= 0;

	uint32	NewDataSize	= 0;

	// 생성자, 복사생성자 operator= 생략.

};

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	if (ReadWavFileData.Num() == 0)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, "Stereo Bytes is empty");
		return;
	}

	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)
		{
			short NumChannelCount = ConvertWavInfo.Channels;
			OutConvertResultFileData.Append((uint8*)&NumChannelCount, sizeof(NumChannelCount));
			i++;
		}
		else if (i == 24)	//SamplingRate starts from 24 to 27
		{
			int OriginalSamplingRate = (*(int*)&ReadWavFileData[i]);
			int SamplingRate = OriginalSamplingRate / ((OriginalSamplingRate / ConvertWavInfo.SamplesPerSec));

			OutConvertResultFileData.Append((uint8*)&SamplingRate, sizeof(SamplingRate));
			i += 3;
		}
		else if (i == 28)	//ByteRate starts from 28 to 32
		{
			int OriginalByteRate = (*(int*)&ReadWavFileData[i]);
			int ByteRate = OriginalByteRate / ((OriginWavInfo.SamplesPerSec / ConvertWavInfo.SamplesPerSec) * (OriginWavInfo.Channels / ConvertWavInfo.Channels));

			OutConvertResultFileData.Append((uint8*)&ByteRate, sizeof(ByteRate));
			i += 3;
		}
		else if (i == 32)	//BlockAlign starts from 32 to 34
		{
			short BlockAlign = (*(short*)&ReadWavFileData[i]) / (OriginWavInfo.Channels / ConvertWavInfo.Channels);
			OutConvertResultFileData.Append((uint8*)&BlockAlign, sizeof(BlockAlign));
			i++;
		}
		else if (i == 40)
		{
			int SubChunkSize = (*(int*)&ReadWavFileData[i]) / (OriginWavInfo.Channels / ConvertWavInfo.Channels);
			OutConvertResultFileData.Append((uint8*)&SubChunkSize, sizeof(SubChunkSize));
			i += 3;
		}
		else
		{
			OutConvertResultFileData.Add(ReadWavFileData[i]);
		}
	}

	short BlockAlign = (OriginWavInfo.BlockAlign * (OriginWavInfo.Channels / ConvertWavInfo.Channels)) * (OriginWavInfo.SamplesPerSec / ConvertWavInfo.SamplesPerSec);

	for (int i = 44; i < ReadWavFileData.Num(); i += BlockAlign)
	{
		OutConvertResultFileData.Add(ReadWavFileData[i]);
		OutConvertResultFileData.Add(ReadWavFileData[i + 1]);
	}

}

일단 먼저 FWaveInfo라는 구조체는 내가 임의로 만든것이고

Engine/Public/Audio.h에 FWaveModInfo라는 구조체 내부를 보고 멤버 똑같이 작성하고 깊은복사 되도록 적어놓은것 뿐이다.

 

파라미터로 전달받은 내용은 아래와 같다.

원래의 wav파일 정보를 OriginWavInfo

변경하려고 하는 wav파일 정보를 ConvertWavInfo

원본 wav파일을 읽어들인것은 ReadWavFileData

변경된 wav파일 내용은 OutConvertResultFileData

 

이제 코드를 살펴보면..

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	//Change wav headers
	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)
		{
			short NumChannelCount = ConvertWavInfo.Channels;

			OutConvertResultFileData.Append((uint8*)&NumChannelCount, sizeof(NumChannelCount));
			i++;
		}

	// ....

위에서 참고한 사이트들의 정보를 모으고 모아 offset의 22 이전값은 특별히 변경할 필요가 없는 내용들이라 건너뛰었다.

Channel은 여기서 단순히 모노(Mono) 또는 스테레오(Stereo)만 정해주면되므로 1 또는 2를 전달받은것을 넣어준다.

다음은 Sample Rate를 기입해 주어야 한다.

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	//Change wav headers
	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)	{ /* 생략 */ }
		else if (i == 24)
        	{
                	int OriginalSamplingRate = (*(int*)&ReadWavFileData[i]);
        		int SamplingRate = OriginalSamplingRate / ((OriginalSamplingRate / ConvertWavInfo.SamplesPerSec));
			OutConvertResultFileData.Append((uint8*)&SamplingRate, sizeof(SamplingRate));
			i += 3;
        	}

	// ....

위의 코드에서는 SamplingRate값을 단순히 전달받은 ConvertWavInfo.SamplesPerSec를 넣어주면 끝이 아닌가?

하지만 11025같은 값이 들어오면 음성이 늘어지도록 재생되는 문제가 있어서 일단 위와같이 두었다.(그러면 12000이 된다.)

더보기

음악파일 편집 프로그램(GoldWave)에서 기본 Preset에서 12000이 아닌 11025가 되어있길래 이 값을 넣었다가 위와 같은 문제를 발견 했다.

사내에서 이 내용을 개발할 당시 필요한값이 8000과 12000이었기 때문에 해당 문제에 대해서 깊게 따로 파진 않았다. (만들어야 하는 것이 완벽한 음성데이터 조작이 아니었기 때문에 여기에 시간을 많이 할애할 필요가 없기 때문. 이와 같은 문제가 발생 할 수 있다정도를 주석에 달아놓았다.)

 

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	//Change wav headers
	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)	{ /* 생략 */ }
		else if (i == 24)	{ /* 생략 */ }
        	else if (i == 28)	
		{
        		int OriginalByteRate = (*(int*)&ReadWavFileData[i]);
			
        		int ByteRate = OriginalByteRate / (
                		(OriginWavInfo.SamplesPerSec / ConvertWavInfo.SamplesPerSec) * 
                		(OriginWavInfo.Channels / ConvertWavInfo.Channels)
                    	);
			
        		OutConvertResultFileData.Append((uint8*)&ByteRate, sizeof(ByteRate));
        		i += 3;
		}


	// ....

ByteRate부분인데 여기 값은 (SampleRate * NumChannels * BitPerSample / 8)로 이루어진 값이다.

OriginalByteRate에서 변경된 값은 SampleRate와 NumChannels만 변경되므로 원래의 값에서 변경될 값이 얼마만큼 줄어든건지를 계산하려고 위와 같이 ByteRate를 계산했다.

 

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	//Change wav headers
	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)	{ /* 생략 */ }
		else if (i == 24)	{ /* 생략 */ }
        	else if (i == 28)	{ /* 생략 */ }
		else if (i == 32)
		{
			short BlockAlign = (*(short*)&ReadWavFileData[i]) / (OriginWavInfo.Channels / ConvertWavInfo.Channels);
			
			OutConvertResultFileData.Append((uint8*)&BlockAlign, sizeof(BlockAlign));
			i++;
		}



	// ....

BlockAlign은 (NumChannels * BitsPerSample / 8) 으로 이루어진 값이다.

위와 마찬가지로 원래 BlockAlign값에서 변경된 NumChannels값만 변경하면 되므로 채널값만 변경된것을 적용해 주었다.

 

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	//Change wav headers
	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)	{ /* 생략 */ }
		else if (i == 24)	{ /* 생략 */ }
        	else if (i == 28)	{ /* 생략 */ }
		else if (i == 32)	{ /* 생략 */ }
		else if (i == 40)
		{
			int SubChunkSize = (*(int*)&ReadWavFileData[i]) / (OriginWavInfo.Channels / ConvertWavInfo.Channels);
			
			OutConvertResultFileData.Append((uint8*)&SubChunkSize, sizeof(SubChunkSize));
			i += 3;
		}
		else
		{
			OutConvertResultFileData.Add(ReadWavFileData[i]);
		}
	}



	// ....

여기는 SubChunkSize 내용이다.

(BitsPerSample / 8) * NumChannels * 실제 샘플수 로 계산이 되는데 역시 여기서 변경되는건 NumChannels만 있으므로 위에 BlockAlign과 똑같이 채널만 조절되도록 수정했다.

 

나머지 else에 해당되는 부분은 모두 값 그대로 넣어주었다.

이제 헤더부분은 끝이 났고 이제 Data부분이다.

void UVoiceRecordComponent::ConvertWaveFileData(const FWaveInfo& OriginWavInfo, const FWaveInfo& ConvertWavInfo, TArray<uint8>& OutConvertResultFileData, const TArray<uint8>& ReadWavFileData)
{
	//Change wav headers
	for (int i = 0; i < 44; i++)
	{
		//NumChannels starts from 22 to 24
		if (i == 22)	{ /* 생략 */ }
		else if (i == 24)	{ /* 생략 */ }
        	else if (i == 28)	{ /* 생략 */ }
		else if (i == 32)	{ /* 생략 */ }
		else if (i == 40)	{ /* 생략 */ }
		else	{ /* 생략 */ }
	}

	short BlockAlign = OriginWavInfo.BlockAlign * 
			(OriginWavInfo.Channels / ConvertWavInfo.Channels) *
			(OriginWavInfo.SamplesPerSec / ConvertWavInfo.SamplesPerSec);
                        
	for (int i = 44; i < ReadWavFileData.Num(); i += BlockAlign)
	{
		OutConvertResultFileData.Add(ReadWavFileData[i]);
		OutConvertResultFileData.Add(ReadWavFileData[i + 1]);
	}
    
}

여기서는 음성 데이터를 직접적으로 수정하는것이 아닌 건너뛰는 형식으로 음성 데이터를 넣었다.

이러한 방법이 실제 음악편집 프로그램의 방식인지는 모르겠다. 위에서 적어놓은 페이지를 참고해서 구현하고 이해했을뿐

이러한것을 GPT에게 물어봐도 외부 라이브러리를 써야 제데로된 샘플링 조절이 된다고 말하고 있다.

 

아무튼 원래의 BlockAlign에서 채널이 스테레오인지 모노인지에 따라서 건너뛸 크기가 결정되고 Sample에 따라서 얼마나 건너뛰어야 할지가 결정되므로 해당 내용을 계산해서 for문에서 건너뛰는 식으로 했다.

Sample이 원래에 비해서 어떤 비율로 낮아졌는지, 그리고 스테레오에서 모노로 변경되면 left 혹은 right만 가져와야 할것이므로 더 건너뛰어야 한다.

위는 GoldWave라는 편집프로그램을 통해서 본 내용이다.

16bit 스테레오 48000hz wav파일을 16bit mono 12000hz로 변경한 모습이다.

 

다른 전문 프로그램과 비교하자면 정확하게 다운샘플이 된건지는 모르겠지만 그래도 야매로 이렇게 구현해 본 경험을 정리해 보았다.

물론 내가 지금도 이걸 작성하면서 잘못 알고있는 부분이 있을수 있으므로

완벽한 정답은 아니더라도 미래의 나 혹은 이 글을 읽을 사람들이 참고만 했으면 해서 적어놓았다.

 

이 글을 작성하면서 Wav헤더에 관한 내용은 Engine/Public/Audio.h와 Engine/Private/Audio.cpp에서

FWaveModInfo 클래스 그리고 void SerializeWaveFile 함수를 보는것도 좋을것 같다.

반응형