공부중
[UE] WAV파일을 DownSampling, Stereo To Mono로 변경 해보자. 본문
요거 카테고리를 어디에 배치 해야 할까 하다가 단순히 언리얼엔진에서 작업한거라 언리얼로 분류 했다.
물론 이를 참조해서 다른 엔진 그리고 다른언어에서도 적용 가능하다.
이 글은 WAV파일을 읽어서 DownSampling 그리고 스테레오를 모노로 변경(Stereo To Mono)하는 내용을 구현한뒤 내용을 정리해보려고 작성하는 글이다.
과거 WAV파일 DownSampling, 그리고 스테레오를 모노로 변경(Stereo To Mono) 작업을 사내에서 사용한 경험을 토대로 다시 작성하여 나중에 잊어버리면 내가 다시 볼 목적으로 적어놓는 글이며 전문적인 프로그램과는 다른 결과를 불러올 수 있으므로 자세하고 깊은 wav파일 지식은 작성하지 않으려고 한다.(더 좋은 내용의 글들이 다른곳에 더 많을것이므로...)
여기 이 글에서 말하는 Sample은(정확히는 Sampling Frequency) WAV파일에 저장된 소리로부터 초당 샘플링한 횟수를 의미한다. 쉽게 말하면 높으면 음질이 좋다!
아마 내 기억속에서의 언리얼은 녹음시 기본으로 48000Hz를 사용 했던것으로 기억한다.
프로젝트 세팅에서 각 플랫폼마다 확인할 수 있고 수정도 할 수 있다.
사내에서 관련 작업을 할 때 이를 임의로 수정하기보다는 내가 DownSampling을 통해서 적절히 변경하는것이 나중에 사이드 이펙트가 줄지 않을까라는 생각이 들어서 작업하다보니 이런 작업도 하게 되었다 하하..
작성된 코드는 이전 언리얼엔진에서 마이크를 통해 wav파일로 저장해보자 글에서 작성된 코드의 연장으로 작업 되어있다.
그리고 이 작업을 하면서 참고했던 사이트들은 아래와 같다.
https://infograph.tistory.com/333
https://anythingcafe.tistory.com/2
그리고 아예 코드로도 만들어 놓은 외국인 형도 있었는데 위에서 헤더파일을 좀 보고 코드를 고쳐서 사용했다.
https://stackoverflow.com/questions/69085916/ue4-convert-audio-from-48-stereo-to-16-mono
일단 내가 작성한 코드는 아래와 같다.
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 함수를 보는것도 좋을것 같다.
'Programing > UnrealEngine' 카테고리의 다른 글
[UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 3 (0) | 2024.07.16 |
---|---|
[UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 2 (3) | 2024.07.16 |
[UE] 마이크로 전달한 음성 데이터를 wav파일로 저장해보자. - 1 (0) | 2024.07.15 |
[UE] 엔진 코드 빌드에러, Generate Header [x86-64] NearestNeighborOptimizedNetwork.ispc > fopen: No such file or directory (0) | 2024.05.14 |
[UE] Editor에서 WebBrowser사용시 FConstBitReference operator[] check에 걸리는 경우(임시 해결 방법) (0) | 2024.05.08 |