25#include "Sound/SoundWaveProcedural.h"
26#include "GameFramework/PlayerState.h"
29UVoiceConversationSystem::UVoiceConversationSystem()
31 PrimaryComponentTick.bCanEverTick =
false;
34void UVoiceConversationSystem::InitSystem(
APlayerActor* InOwner)
36 this->Owner = InOwner;
38 BroadcastManager = UBroadcastManager::Get(GetWorld());
41void UVoiceConversationSystem::EndPlay(
const EEndPlayReason::Type EndPlayReason)
43 if (AudioCapture.IsValid())
44 AudioCapture->CloseStream();
45 Super::EndPlay(EndPlayReason);
50void UVoiceConversationSystem::StartRecording()
52 if (bIsRecording || bIsProcessing)
54 PRINTLOG( TEXT(
"[VoiceConversation] Already recording or processing."));
59 if (UWorld* World = GetWorld())
61 if (
auto SoundManager = UGameSoundManager::Get(World))
63 if (SoundManager->IsConversationVoicePlaying())
65 SoundManager->StopConversationVoice();
68 World->GetTimerManager().ClearTimer(VoiceFinishTimerHandle);
70 OnVoiceAudioFinished();
78 AudioCapture = MakeUnique<Audio::FAudioCapture>();
81 TArray<Audio::FCaptureDeviceInfo> DeviceInfos;
82 AudioCapture->GetCaptureDevicesAvailable(DeviceInfos);
84 PRINTLOG(TEXT(
"[VoiceConversation] Available Audio Devices:"));
85 for (int32 i = 0; i < DeviceInfos.Num(); ++i)
87 PRINTLOG(TEXT(
" [%d] %s (Channels: %d, SampleRate: %d, bSupportsHardwareAEC: %d)"),
89 *DeviceInfos[i].DeviceName,
90 DeviceInfos[i].InputChannels,
91 DeviceInfos[i].PreferredSampleRate,
92 DeviceInfos[i].bSupportsHardwareAEC ? 1 : 0);
95 Audio::FAudioCaptureDeviceParams Params;
96 Params.DeviceIndex = 0;
97 Params.NumInputChannels = 1;
99 const bool bStreamOpened = AudioCapture->OpenAudioCaptureStream(
101 [
this](
const void* InAudio, int32 NumFrames, int32 InNumChannels, int32 InSampleRate,
double StreamTime,
bool bOverFlow)
104 static bool bLoggedOnce =
false;
107 PRINTLOG(TEXT(
"[VoiceConversation] Audio Capture Settings: SampleRate=%d, Channels=%d, Frames=%d"),
108 InSampleRate, InNumChannels, NumFrames);
111 HandleOnCapture(
static_cast<const float*
>(InAudio), NumFrames, InNumChannels, InSampleRate);
116 if (!bStreamOpened || !AudioCapture->StartStream() )
118 if (
auto DM = UDialogManager::Get(GetWorld()))
120 DM->ShowToast(TEXT(
"연결된 마이크가 없습니다"));
129 BroadcastManager->SendAudioCapture(
true);
130 PRINTLOG( TEXT(
"[VoiceConversation] Recording started."));
134void UVoiceConversationSystem::HandleOnCapture(
const float* InAudio, int32 InNumFrames, int32 InNumChannels, int32 InSampleRate)
136 LastSampleRate = InSampleRate;
137 LastNumChannels = InNumChannels;
139 const int32 SampleCount = InNumFrames * InNumChannels;
140 PCMData.Reserve(PCMData.Num() + SampleCount *
sizeof(int16));
142 float TotalVolume = 0.f;
144 for (int32 i = 0; i < SampleCount; ++i)
146 float Sample = InAudio[i];
147 Sample = FMath::Clamp(Sample, -1.0f, 1.0f);
149 TotalVolume += FMath::Abs(Sample);
151 int16 Int16Sample =
static_cast<int16
>(Sample * 32767.0f);
152 const uint8* SampleBytes =
reinterpret_cast<const uint8*
>(&Int16Sample);
154 PCMData.Append(SampleBytes,
sizeof(int16));
157 const float AverageVolume = (SampleCount > 0) ? (TotalVolume / SampleCount) : 0.f;
158 const float AmplifiedVolume = AverageVolume * 10.f;
160 BroadcastManager->SendAudioSpectrum(FMath::Clamp(AmplifiedVolume, 0.f, 1.f));
163void UVoiceConversationSystem::StopRecording()
167 PRINTLOG( TEXT(
"[VoiceConversation] Not currently recording."));
171 bIsRecording =
false;
172 bIsProcessing =
true;
174 AudioCapture->StopStream();
175 AudioCapture->CloseStream();
177 BroadcastManager->SendAudioCapture(
false);
179 PRINTLOG(TEXT(
"[VoiceConversation] Recording stopped. Original: SampleRate=%d, Channels=%d, PCM Size=%d bytes"),
180 LastSampleRate, LastNumChannels, PCMData.Num());
183 TArray<uint8> ProcessedPCM = PCMData;
184 int32 ProcessedChannels = LastNumChannels;
186 if (LastNumChannels == 2)
189 ProcessedChannels = 1;
193 int32 TargetSampleRate = 16000;
195 if (LastSampleRate != TargetSampleRate)
203 PRINTLOG(TEXT(
"[VoiceConversation] Recording saved to: %s"), *LastRecordedFilePath);
204 if (LastRecordedFilePath.IsEmpty())
206 PRINTLOG( TEXT(
"[VoiceConversation] FilePath is Empty") );
213 PRINTLOG( TEXT(
"HttpSystem을 찾을 수 없습니다."));
214 bIsProcessing =
false;
220 bool bUseSpeakJudges =
false;
225 APlayerState* LocalPlayerState =
nullptr;
228 APlayerController* PC = Cast<APlayerController>(Owner->GetController());
231 LocalPlayerState = PC->GetPlayerState<APlayerState>();
239 if (CurrentSpeaker && LocalPlayerState && CurrentSpeaker == LocalPlayerState)
241 bUseSpeakJudges =
true;
250 DM->ShowToast(TEXT(
"The officer is reviewing your answer"));
254 SpeakStageActor->GetCurrentQuestion(),
255 LastRecordedFilePath,
256 FResponseSpeakingJudesDelegate::CreateUObject(
this, &UVoiceConversationSystem::OnResponseSpeakingsJudges));
258 else if (
auto* DailyStudyPopup = Cast<UPopup_DailyStudy>(UPopupManager::Get(GetWorld())->GetCurrentPopupWidget()))
262 DM->ShowToast(TEXT(
"The officer is reviewing your answer"));
265 DailyStudyPopup->GetCurrentQuestionText(),
266 LastRecordedFilePath,
267 FResponseSpeakingJudesDelegate::CreateUObject(
this, &UVoiceConversationSystem::OnResponseSpeakingsJudges));
274 DM->ShowToast(TEXT(
"Processing your voice message..."));
278 FString ChatContext = TEXT(
"You are a helpful assistant.");
281 APlayerController* PC = Cast<APlayerController>(Owner->GetController());
286 ChatContext = PS->GetChatContext();
294 LastRecordedFilePath,
295 FResponseChatAnswersDelegate::CreateUObject(
this, &UVoiceConversationSystem::OnResponseChatAnswers));
301 bIsProcessing =
false;
305 if (
auto* DailyStudyPopup = Cast<UPopup_DailyStudy>(UPopupManager::Get(GetWorld())->GetCurrentPopupWidget()))
307 DailyStudyPopup->OnResponseSpeakingsJudges(Response);
313 Owner->Server_NotifySpeakJudgeComplete(Response);
317 PRINTLOG( TEXT(
"--- Network Response Received (FAIL) ---"));
321void UVoiceConversationSystem::OnResponseChatAnswers(
FResponseChatAnswers& Response,
bool bSuccess)
323 bIsProcessing =
false;
328 if (
auto* GS = GetWorld()->GetGameState<ALingoGameState>())
330 APlayerControl* PC = Cast<APlayerControl>(Owner->GetController());
337 if (
auto playerActor = Cast<APlayerActor>(PC->GetPawn()))
339 playerActor->GetMiniOwlBot()->UpdateText(Response.
answer);
342 FText PlayerQuestion = FText::FromString(Response.
question);
346 FText AIAnswer = FText::FromString(Response.
answer);
356 PRINTLOG( TEXT(
"--- Chat Answers Response Received (FAIL) ---"));
360bool UVoiceConversationSystem::PlayVoiceAudio(
const TArray<uint8>& AudioData)
365 PRINTLOG(TEXT(
"[VoiceConversation] TTS playback blocked: recording in progress"));
370 if (AudioData.Num() == 0)
372 PRINTLOG(TEXT(
"[VoiceConversation] TTS playback failed: empty audio data"));
378 if (!IsValid(SoundWave))
380 PRINTLOG(TEXT(
"[VoiceConversation] TTS playback failed: invalid sound wave"));
385 auto SoundManager = UGameSoundManager::Get(GetWorld());
388 PRINTLOG(TEXT(
"[VoiceConversation] TTS playback failed: could not get sound manager"));
392 CurVoiceAudio = SoundManager->PlayConversationVoice(SoundWave);
399 const float Duration = SoundWave->Duration;
400 PRINTLOG(TEXT(
"[VoiceConversation] TTS audio playing (duration: %.2f seconds)"), Duration);
403 if (
auto World = GetWorld())
405 World->GetTimerManager().ClearTimer(VoiceFinishTimerHandle);
408 World->GetTimerManager().SetTimer(
409 VoiceFinishTimerHandle,
411 &UVoiceConversationSystem::OnVoiceAudioFinished,
420void UVoiceConversationSystem::OnVoiceAudioFinished()
423 if (
auto World = GetWorld())
424 World->GetTimerManager().ClearTimer(VoiceFinishTimerHandle);
427 CurVoiceAudio =
nullptr;
Declares the player-controlled character actor.
APlayerControl 선언에 대한 Doxygen 주석을 제공합니다.
YiSan 전반에서 사용하는 공용 인터페이스를 선언합니다.
#define PRINTLOG(fmt,...)
Chat 대화 기록을 GConfig를 이용하여 관리하는 컴포넌트입니다.
UDialogManager 클래스를 선언합니다.
UGameSoundManager 클래스를 선언합니다.
KLingo API 요청을 담당하는 서브시스템을 선언합니다.
STT·GPT·TTS 파이프라인을 연결하는 음성 대화 컴포넌트를 선언합니다.
음성 데이터 처리와 명령 파싱을 위한 블루프린트 함수 라이브러리를 선언합니다.
Main character driven directly by the player.
class UChatHistorySystem * GetChatHistorySystem() const
Chat History System에 접근합니다.
const FResponseUserMe & GetUserInfo() const
토스트 메시지와 같은 간단한 다이얼로그 위젯의 표시를 관리하는 LocalPlayer 서브시스템입니다.
KLingo 서버와의 HTTP 요청을 중재하는 게임 인스턴스 서브시스템입니다.
void RequestSpeakingJudges(const FString &Question, const FString &AudioPath, FResponseSpeakingJudesDelegate InDelegate)
Speaking 평가 요청을 전송합니다.
void RequestChatAudio(const FString &Context, const FString &AudioPath, FResponseChatAnswersDelegate InDelegate)
Chat 답변을 요청합니다 (음성 질문).
static class ASpeakStageActor * GetSpeakStageActor(const UObject *WorldContextObject)
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 포맷으로 감쌉니다.
Speaking Questions 응답 구조체입니다.