KLingo Project Documentation 1.0.0
Unreal Engine 5.6 C++ Project Documentation
로딩중...
검색중...
일치하는것 없음
UVoiceFunctionLibrary.cpp
이 파일의 문서화 페이지로 가기
1// Copyright (c) 2025 Doppleddiggong. All rights reserved. Unauthorized copying, modification, or distribution of this file, via any medium is strictly prohibited. Proprietary and confidential.
2
8#include "GameLogging.h"
9#include "Sound/SoundWaveProcedural.h"
10
11#define VOICE_LOG TEXT("VoiceLogs")
12
13static uint32 ReadUInt32(const uint8* Data, int32 Offset)
14{
15 return Data[Offset] |
16 (Data[Offset + 1] << 8) |
17 (Data[Offset + 2] << 16) |
18 (Data[Offset + 3] << 24);
19}
20
21static uint16 ReadUInt16(const uint8* Data, int32 Offset)
22{
23 return Data[Offset] | (Data[Offset + 1] << 8);
24}
25
27 const TArray<uint8>& InPCMData,
28 const int32 InSampleRate,
29 const int32 InChannel,
30 const int32 InBitsPerSample)
31{
32 TArray<uint8> WavData;
33
34 const int32 ByteRate = InSampleRate * InChannel * InBitsPerSample / 8;
35 const int32 BlockAlign = InChannel * InBitsPerSample / 8;
36 const int32 DataSize = InPCMData.Num();
37 const int32 ChunkSize = 36 + DataSize;
38
39 // RIFF 헤더
40 WavData.Append(reinterpret_cast<const uint8*>("RIFF"), 4);
41 WavData.Append(reinterpret_cast<const uint8*>(&ChunkSize), 4);
42 WavData.Append(reinterpret_cast<const uint8*>("WAVE"), 4);
43
44 // fmt chunk
45 WavData.Append(reinterpret_cast<const uint8*>("fmt "), 4);
46 int32 SubChunk1Size = 16;
47 int16 AudioFormat = 1;
48
49 WavData.Append(reinterpret_cast<const uint8*>(&SubChunk1Size), 4);
50 WavData.Append(reinterpret_cast<const uint8*>(&AudioFormat), 2);
51 WavData.Append(reinterpret_cast<const uint8*>(&InChannel), 2);
52 WavData.Append(reinterpret_cast<const uint8*>(&InSampleRate), 4);
53 WavData.Append(reinterpret_cast<const uint8*>(&ByteRate), 4);
54 WavData.Append(reinterpret_cast<const uint8*>(&BlockAlign), 2);
55 WavData.Append(reinterpret_cast<const uint8*>(&InBitsPerSample), 2);
56 WavData.Append(reinterpret_cast<const uint8*>("data"), 4);
57 WavData.Append(reinterpret_cast<const uint8*>(&DataSize), 4);
58
59 WavData.Append(InPCMData);
60 return WavData;
61}
62
63
64FString UVoiceFunctionLibrary::SaveWavToFile(TArray<uint8>& InWavData, const FString& InFileName )
65{
66 if (InWavData.Num() == 0)
67 {
68 PRINTLOG( TEXT("WavData is empty, nothing to save."));
69 return FString();
70 }
71
72 FString FileName = InFileName;
73 if (FileName.IsEmpty())
74 {
75 // 날짜 기반 파일명 생성
76 const FDateTime Now = FDateTime::Now();
77 FileName = FString::Printf(TEXT("Voice_%04d%02d%02d_%02d%02d%02d.wav"),
78 Now.GetYear(), Now.GetMonth(), Now.GetDay(),
79 Now.GetHour(), Now.GetMinute(), Now.GetSecond()
80 );
81 }
82
83 FString FolderPath = FPaths::ProjectSavedDir() / VOICE_LOG;
84 // 폴더 없으면 생성
85 IFileManager::Get().MakeDirectory(*FolderPath, true);
86
87 FString FullPath = FolderPath / FileName;
88 if (FFileHelper::SaveArrayToFile(InWavData, *FullPath))
89 {
90 // 절대 경로로 변환하여 반환
91 FString AbsolutePath = FPaths::ConvertRelativePathToFull(FullPath);
92 PRINTLOG( TEXT("Saved WAV file: %s"), *AbsolutePath);
93 return AbsolutePath;
94 }
95 else
96 {
97 PRINTLOG( TEXT("Failed to save WAV file: %s"), *FullPath);
98 return FString();
99 }
100}
101
102USoundWave* UVoiceFunctionLibrary::CreateSoundWaveFromWavData(const TArray<uint8>& WavData)
103{
104 if (WavData.Num() < 44)
105 {
106 PRINTLOG( TEXT("Invalid WAV data (too small)"));
107 return nullptr;
108 }
109
110 const uint8* RawData = WavData.GetData();
111
112 // WAV Header Parsing
113 // ChunkID "RIFF" (0~3)
114 // Format "WAVE" (8~11)
115 // Subchunk1ID "fmt " (12~15)
116 // AudioFormat, NumChannels, SampleRate, ByteRate, BlockAlign, BitsPerSample
117 uint16 AudioFormat = ReadUInt16(RawData, 20);
118 uint16 NumChannels = ReadUInt16(RawData, 22);
119 uint32 SampleRate = ReadUInt32(RawData, 24);
120 uint16 BitsPerSample = ReadUInt16(RawData, 34);
121
122 // Subchunk2ID "data"는 고정 위치가 아니므로 탐색
123 int32 DataChunkOffset = 36;
124 while (DataChunkOffset + 8 < WavData.Num())
125 {
126 uint32 ChunkID =
127 RawData[DataChunkOffset] |
128 (RawData[DataChunkOffset + 1] << 8) |
129 (RawData[DataChunkOffset + 2] << 16) |
130 (RawData[DataChunkOffset + 3] << 24);
131
132 uint32 ChunkSize = ReadUInt32(RawData, DataChunkOffset + 4);
133
134 // 'data' 체크
135 if (ChunkID == 'atad') // 'data'를 리틀엔디언 'atad'로 읽음
136 {
137 break;
138 }
139
140 // 다음 Chunk로 이동
141 DataChunkOffset += (8 + ChunkSize);
142 }
143
144 if (DataChunkOffset + 8 >= WavData.Num())
145 {
146 PRINTLOG( TEXT("WAV data chunk not found"));
147 return nullptr;
148 }
149
150 const int32 DataStart = DataChunkOffset + 8;
151 const int32 DataSize = WavData.Num() - DataStart;
152
153 // SoundWave 생성
154 USoundWave* SoundWave = NewObject<USoundWave>();
155 if (!SoundWave)
156 {
157 PRINTLOG( TEXT("Failed to create USoundWave"));
158 return nullptr;
159 }
160
161 SoundWave->SoundGroup = ESoundGroup::SOUNDGROUP_Voice;
162 SoundWave->DecompressionType = EDecompressionType::DTYPE_Procedural;
163 SoundWave->bLooping = false;
164
165 SoundWave->NumChannels = NumChannels;
166 SoundWave->Duration = (float)DataSize / (SampleRate * NumChannels * (BitsPerSample / 8));
167 SoundWave->SetSampleRate(SampleRate);
168 SoundWave->RawPCMDataSize = DataSize;
169
170 // RawPCMData에 복사
171 uint8* PCMData = (uint8*)FMemory::Malloc(DataSize);
172 FMemory::Memcpy(PCMData, RawData + DataStart, DataSize);
173 SoundWave->RawPCMData = PCMData;
174
175 return SoundWave;
176}
177
178USoundWaveProcedural* UVoiceFunctionLibrary::CreateProceduralSoundWaveFromWavData(const TArray<uint8>& AudioData)
179{
180 // VoiceLogs 폴더에 타임스탬프 형식으로 저장
181 const FDateTime Now = FDateTime::Now();
182 const FString FileName = FString::Printf(TEXT("TTS_Output_%04d%02d%02d_%02d%02d%02d.wav"),
183 Now.GetYear(), Now.GetMonth(), Now.GetDay(),
184 Now.GetHour(), Now.GetMinute(), Now.GetSecond()
185 );
186 const FString FolderPath = FPaths::ProjectSavedDir() / VOICE_LOG;
187 IFileManager::Get().MakeDirectory(*FolderPath, true);
188 const FString SavePath = FolderPath / FileName;
189
190 if (!FFileHelper::SaveArrayToFile(AudioData, *SavePath))
191 {
192 PRINTLOG(TEXT("TTS WAV 저장 실패"));
193 return nullptr;
194 }
195
196 PRINTLOG(TEXT("TTS WAV 저장 완료: %s"), *SavePath);
197
198 if (AudioData.Num() < 44)
199 {
200 PRINTLOG(TEXT("Invalid WAV data (too small)"));
201 return nullptr;
202 }
203
204 const uint8* RawData = AudioData.GetData();
205
206 // --- WAV Header Parsing ---
207 uint16 NumChannels = ReadUInt16(RawData, 22);
208 uint32 SampleRate = ReadUInt32(RawData, 24);
209 uint16 BitsPerSample = ReadUInt16(RawData, 34);
210
211 // Find the 'data' chunk, as it's not always at a fixed position.
212 int32 DataChunkOffset = 36; // Typically after the 'fmt ' chunk.
213 while (DataChunkOffset + 8 < AudioData.Num())
214 {
215 // Read ChunkID as a 4-character string
216 const char* ChunkIDStr = (const char*)(RawData + DataChunkOffset);
217
218 if (strncmp(ChunkIDStr, "data", 4) == 0)
219 {
220 break; // Found it
221 }
222
223 // If not 'data', skip to the next chunk
224 uint32 ChunkSize = ReadUInt32(RawData, DataChunkOffset + 4);
225 DataChunkOffset += (8 + ChunkSize);
226 }
227
228 if (DataChunkOffset + 8 >= AudioData.Num())
229 {
230 PRINTLOG(TEXT("WAV 'data' chunk not found"));
231 return nullptr;
232 }
233
234 const uint32 DataSize = ReadUInt32(RawData, DataChunkOffset + 4);
235 const int32 DataStart = DataChunkOffset + 8;
236
237 if (DataStart + (int32)DataSize > AudioData.Num())
238 {
239 PRINTLOG(TEXT("Invalid WAV data chunk size"));
240 return nullptr;
241 }
242 // --- End of WAV Header Parsing ---
243
244 USoundWaveProcedural* SoundWave = NewObject<USoundWaveProcedural>();
245 if (!SoundWave)
246 {
247 PRINTLOG(TEXT("Failed to create USoundWaveProcedural"));
248 return nullptr;
249 }
250
251 // Calculate actual duration for one-shot playback
252 const float BytesPerSample = BitsPerSample / 8.0f;
253 const float ActualDuration = (float)DataSize / (SampleRate * NumChannels * BytesPerSample);
254
255 SoundWave->SetSampleRate(SampleRate);
256 SoundWave->NumChannels = NumChannels;
257 SoundWave->Duration = ActualDuration;
258 SoundWave->SoundGroup = ESoundGroup::SOUNDGROUP_Voice;
259 SoundWave->bLooping = false;
260
261 // Queue the raw PCM data for playback
262 SoundWave->QueueAudio(RawData + DataStart, DataSize);
263 return SoundWave;
264}
265
266TArray<uint8> UVoiceFunctionLibrary::ResampleAudio(const TArray<uint8>& InPCMData, int32 InSampleRate, int32 OutSampleRate, int32 InNumChannels)
267{
268 if (InSampleRate == OutSampleRate)
269 return InPCMData;
270
271 // PCM 데이터는 int16 형식
272 const int32 NumSamples = InPCMData.Num() / sizeof(int16);
273 const int16* InSamples = reinterpret_cast<const int16*>(InPCMData.GetData());
274
275 // 리샘플링 비율 계산
276 const double ResampleRatio = static_cast<double>(OutSampleRate) / static_cast<double>(InSampleRate);
277 const int32 OutNumSamples = FMath::CeilToInt(NumSamples * ResampleRatio);
278
279 TArray<uint8> OutPCMData;
280 OutPCMData.Reserve(OutNumSamples * sizeof(int16));
281
282 // Linear interpolation 리샘플링
283 for (int32 i = 0; i < OutNumSamples; ++i)
284 {
285 const double SourceIndex = i / ResampleRatio;
286 const int32 Index0 = FMath::FloorToInt(SourceIndex);
287 const int32 Index1 = FMath::Min(Index0 + 1, NumSamples - 1);
288 const double Fraction = SourceIndex - Index0;
289
290 // Linear interpolation
291 const int16 Sample0 = InSamples[Index0];
292 const int16 Sample1 = InSamples[Index1];
293 const int16 InterpolatedSample = FMath::RoundToInt(Sample0 + (Sample1 - Sample0) * Fraction);
294
295 // 결과 버퍼에 추가
296 const uint8* SampleBytes = reinterpret_cast<const uint8*>(&InterpolatedSample);
297 OutPCMData.Append(SampleBytes, sizeof(int16));
298 }
299
300 return OutPCMData;
301}
302
303TArray<uint8> UVoiceFunctionLibrary::ConvertStereoToMono(const TArray<uint8>& InStereoPCMData)
304{
305 // PCM 데이터는 int16 형식 (16bit), 스테레오는 [L0, R0, L1, R1, ...] 형태
306 const int32 NumSamples = InStereoPCMData.Num() / sizeof(int16);
307
308 if (NumSamples % 2 != 0)
309 {
310 PRINTLOG(TEXT("[ConvertStereoToMono] Invalid stereo PCM data size (not even number of samples)"));
311 return InStereoPCMData;
312 }
313
314 const int16* InSamples = reinterpret_cast<const int16*>(InStereoPCMData.GetData());
315 const int32 NumMonoSamples = NumSamples / 2;
316
317 TArray<uint8> MonoPCMData;
318 MonoPCMData.Reserve(NumMonoSamples * sizeof(int16));
319
320 // 좌우 채널 평균으로 모노 생성
321 for (int32 i = 0; i < NumMonoSamples; ++i)
322 {
323 const int16 LeftSample = InSamples[i * 2]; // 왼쪽 채널
324 const int16 RightSample = InSamples[i * 2 + 1]; // 오른쪽 채널
325
326 // 평균값 계산 (오버플로우 방지)
327 const int32 AverageSample = (static_cast<int32>(LeftSample) + static_cast<int32>(RightSample)) / 2;
328 const int16 MonoSample = static_cast<int16>(FMath::Clamp(AverageSample, -32768, 32767));
329
330 // 결과 버퍼에 추가
331 const uint8* SampleBytes = reinterpret_cast<const uint8*>(&MonoSample);
332 MonoPCMData.Append(SampleBytes, sizeof(int16));
333 }
334
335 PRINTLOG(TEXT("[ConvertStereoToMono] Converted %d stereo samples to %d mono samples"), NumSamples, NumMonoSamples);
336 return MonoPCMData;
337}
YiSan 전반에서 사용하는 공용 인터페이스를 선언합니다.
#define PRINTLOG(fmt,...)
Definition GameLogging.h:30
#define VOICE_LOG
static uint32 ReadUInt32(const uint8 *Data, int32 Offset)
static uint16 ReadUInt16(const uint8 *Data, int32 Offset)
음성 데이터 처리와 명령 파싱을 위한 블루프린트 함수 라이브러리를 선언합니다.
static USoundWave * CreateSoundWaveFromWavData(const TArray< uint8 > &WavData)
WAV 데이터를 기반으로 사운드 웨이브 객체를 생성합니다.
static FString SaveWavToFile(TArray< uint8 > &InWavData, const FString &InFileName=TEXT(""))
WAV 데이터를 파일로 저장합니다.
static TArray< uint8 > ConvertStereoToMono(const TArray< uint8 > &InStereoPCMData)
스테레오 PCM 데이터를 모노로 변환합니다. (좌우 채널 평균)
static TArray< uint8 > ResampleAudio(const TArray< uint8 > &InPCMData, int32 InSampleRate, int32 OutSampleRate, int32 InNumChannels)
PCM 오디오 데이터를 다른 샘플레이트로 리샘플링합니다.
static USoundWaveProcedural * CreateProceduralSoundWaveFromWavData(const TArray< uint8 > &AudioData)
절차형 사운드 웨이브를 생성해 스트리밍 재생에 사용합니다.
static TArray< uint8 > ConvertPCM2WAV(const TArray< uint8 > &InPCMData, int32 InSampleRate, int32 InChannel, int32 InBitsPerSample)
PCM 데이터를 WAV 포맷으로 감쌉니다.