-
Notifications
You must be signed in to change notification settings - Fork 0
Feature: recode voice #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # .coderabbit.yaml | ||
| language: "ko-KR" | ||
| early_access: false | ||
| reviews: | ||
| profile: "chill" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAMLlint 오류: 후행 공백 제거 필요 Line 5 끝에 공백이 있어 linter가 에러를 보고합니다. 아래처럼 공백을 제거하세요. - profile: "chill" + profile: "chill" 📝 Committable suggestion
Suggested change
profile: "chill"
profile: "chill"
🧰 Tools🪛 YAMLlint (1.37.1)[error] 5-5: trailing spaces (trailing-spaces) 🤖 Prompt for AI Agents |
||
| request_changes_workflow: false | ||
| high_level_summary: true | ||
| poem: true | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAMLlint 오류: 후행 공백 제거 필요 Line 8 끝에 공백이 있어 linter가 에러를 보고합니다. 아래처럼 공백을 제거하세요. - poem: true + poem: true 📝 Committable suggestion
Suggested change
poem: true
poem: true
🧰 Tools🪛 YAMLlint (1.37.1)[error] 8-8: trailing spaces (trailing-spaces) 🤖 Prompt for AI Agents |
||
| review_status: true | ||
| collapse_walkthrough: false | ||
| auto_review: | ||
| enabled: true | ||
| drafts: false | ||
| chat: | ||
| auto_reply: true | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. YAMLlint 오류: 파일 끝 개행 추가 필요 파일 끝에 개행(newline)이 없어 linter가 에러를 보고합니다. 개행을 하나 추가해 주세요. - auto_reply: true + auto_reply: true + 📝 Committable suggestion
Suggested change
auto_reply: true
auto_reply: true
🧰 Tools🪛 YAMLlint (1.37.1)[error] 15-15: no new line character at the end of file (new-line-at-end-of-file) 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,43 +2,64 @@ | |
| using System; | ||
| using UnityEngine; | ||
| using System.Collections.Generic; | ||
| using ProjectVG.Infrastructure.Audio; | ||
|
|
||
| namespace ProjectVG.Core.Audio | ||
| { | ||
| /// <summary> | ||
| /// 정확한 시간 기반 음성 녹음 시스템 | ||
| /// 녹음 시작/중지 시간을 기반으로 정확한 길이의 오디오를 생성합니다. | ||
| /// </summary> | ||
| public class AudioRecorder : Singleton<AudioRecorder> | ||
| { | ||
| [Header("Recording Settings")] | ||
| [SerializeField] private int _sampleRate = 44100; | ||
| [SerializeField] private int _channels = 1; | ||
| [SerializeField] private int _maxRecordingLength = 30; | ||
| [SerializeField] private int _maxRecordingLength = 30; // 최대 녹음 시간 (초) | ||
|
|
||
| [Header("Audio Processing")] | ||
| [SerializeField] private bool _enableNoiseReduction = false; // 노이즈 제거 비활성화 | ||
| [SerializeField] private float _silenceThreshold = 0.001f; // 무음 임계값 낮춤 | ||
|
|
||
| private AudioClip? _recordingClip; | ||
| private bool _isRecording = false; | ||
| private float _recordingStartTime; | ||
| private List<float> _audioBuffer; | ||
|
|
||
| public bool IsRecording => _isRecording; | ||
| public float RecordingDuration => _isRecording ? Time.time - _recordingStartTime : 0f; | ||
| public bool IsRecordingAvailable => Microphone.devices.Length > 0; | ||
| private float _recordingEndTime; | ||
| private string? _currentDevice = null; | ||
|
|
||
| // 이벤트 | ||
| public event Action? OnRecordingStarted; | ||
| public event Action? OnRecordingStopped; | ||
| public event Action<AudioClip>? OnRecordingCompleted; | ||
| public event Action<string>? OnError; | ||
| public event Action<float>? OnRecordingProgress; // 녹음 진행률 (0-1) | ||
|
|
||
| // 프로퍼티 | ||
| public bool IsRecording => _isRecording; | ||
| public float RecordingDuration => _isRecording ? Time.time - _recordingStartTime : 0f; | ||
| public bool IsRecordingAvailable => Microphone.devices.Length > 0; | ||
| public float RecordingProgress => _isRecording ? Mathf.Clamp01(RecordingDuration / _maxRecordingLength) : 0f; | ||
|
|
||
| #region Unity Lifecycle | ||
|
|
||
| protected override void Awake() | ||
| { | ||
| base.Awake(); | ||
| _audioBuffer = new List<float>(); | ||
| InitializeMicrophone(); | ||
| } | ||
|
|
||
| private void Update() | ||
| { | ||
| if (_isRecording && RecordingDuration >= _maxRecordingLength) | ||
| if (_isRecording) | ||
| { | ||
| StopRecording(); | ||
| // 녹음 진행률 이벤트 발생 | ||
| OnRecordingProgress?.Invoke(RecordingProgress); | ||
|
|
||
| // 최대 녹음 시간 체크 | ||
| if (RecordingDuration >= _maxRecordingLength) | ||
| { | ||
| StopRecording(); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+53
to
64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Update() method performs unnecessary work every frame The Throttle the update frequency: +private float _lastProgressUpdate = 0f; +private const float PROGRESS_UPDATE_INTERVAL = 0.1f; // Update 10 times per second private void Update() { if (_isRecording) { - // 녹음 진행률 이벤트 발생 - OnRecordingProgress?.Invoke(RecordingProgress); + // Throttle progress updates + if (Time.time - _lastProgressUpdate >= PROGRESS_UPDATE_INTERVAL) + { + OnRecordingProgress?.Invoke(RecordingProgress); + _lastProgressUpdate = Time.time; + } // 최대 녹음 시간 체크 if (RecordingDuration >= _maxRecordingLength) { StopRecording(); } } } 📝 Committable suggestion
Suggested change
if (_isRecording)
{
StopRecording();
// 녹음 진행률 이벤트 발생
OnRecordingProgress?.Invoke(RecordingProgress);
// 최대 녹음 시간 체크
if (RecordingDuration >= _maxRecordingLength)
{
StopRecording();
}
}
}
// Add these at class scope
private float _lastProgressUpdate = 0f;
private const float PROGRESS_UPDATE_INTERVAL = 0.1f; // Update 10 times per second
private void Update()
{
if (_isRecording)
{
// Throttle progress updates
if (Time.time - _lastProgressUpdate >= PROGRESS_UPDATE_INTERVAL)
{
OnRecordingProgress?.Invoke(RecordingProgress);
_lastProgressUpdate = Time.time;
}
// 최대 녹음 시간 체크
if (RecordingDuration >= _maxRecordingLength)
{
StopRecording();
}
}
}
🤖 Prompt for AI Agents |
||
|
|
||
|
|
@@ -54,6 +75,10 @@ private void OnDestroy() | |
|
|
||
| #region Public Methods | ||
|
|
||
| /// <summary> | ||
| /// 음성 녹음 시작 | ||
| /// </summary> | ||
| /// <returns>녹음 시작 성공 여부</returns> | ||
| public bool StartRecording() | ||
| { | ||
| if (_isRecording) | ||
|
|
@@ -73,10 +98,11 @@ public bool StartRecording() | |
| { | ||
| _isRecording = true; | ||
| _recordingStartTime = Time.time; | ||
| _audioBuffer.Clear(); | ||
|
|
||
| _recordingClip = Microphone.Start(null, false, _maxRecordingLength, _sampleRate); | ||
| // 최대 녹음 시간만큼 버퍼 할당 | ||
| _recordingClip = Microphone.Start(_currentDevice ?? string.Empty, false, _maxRecordingLength, _sampleRate); | ||
|
|
||
| Debug.Log($"[AudioRecorder] 음성 녹음 시작됨 (최대 {_maxRecordingLength}초, {_sampleRate}Hz)"); | ||
| OnRecordingStarted?.Invoke(); | ||
|
|
||
| return true; | ||
|
|
@@ -90,6 +116,10 @@ public bool StartRecording() | |
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 음성 녹음 중지 | ||
| /// </summary> | ||
| /// <returns>처리된 AudioClip</returns> | ||
| public AudioClip? StopRecording() | ||
| { | ||
| if (!_isRecording) | ||
|
|
@@ -101,18 +131,25 @@ public bool StartRecording() | |
| try | ||
| { | ||
| _isRecording = false; | ||
| _recordingEndTime = Time.time; | ||
| float actualRecordingDuration = _recordingEndTime - _recordingStartTime; | ||
|
|
||
| Microphone.End(null); | ||
| Microphone.End(_currentDevice ?? string.Empty); | ||
|
|
||
| if (_recordingClip != null) | ||
| { | ||
| ProcessRecordingClip(); | ||
| OnRecordingCompleted?.Invoke(_recordingClip); | ||
| AudioClip processedClip = ProcessRecordingClip(actualRecordingDuration); | ||
| if (processedClip != null) | ||
| { | ||
| Debug.Log($"[AudioRecorder] 음성 녹음 완료됨 ({actualRecordingDuration:F1}초, {processedClip.samples} 샘플)"); | ||
| OnRecordingCompleted?.Invoke(processedClip); | ||
| OnRecordingStopped?.Invoke(); | ||
| return processedClip; | ||
| } | ||
| } | ||
|
|
||
| OnRecordingStopped?.Invoke(); | ||
|
|
||
| return _recordingClip; | ||
| return null; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
|
|
@@ -123,72 +160,186 @@ public bool StartRecording() | |
| } | ||
| } | ||
|
|
||
| public byte[] AudioClipToBytes(AudioClip audioClip) | ||
| /// <summary> | ||
| /// AudioClip을 WAV 바이트 배열로 변환 | ||
| /// </summary> | ||
| public byte[] AudioClipToWavBytes(AudioClip audioClip) | ||
| { | ||
| if (audioClip == null) | ||
| return new byte[0]; | ||
|
|
||
| return Array.Empty<byte>(); | ||
| try | ||
| { | ||
| float[] samples = new float[audioClip.samples * audioClip.channels]; | ||
| audioClip.GetData(samples, 0); | ||
|
|
||
| byte[] audioBytes = new byte[samples.Length * 2]; | ||
| for (int i = 0; i < samples.Length; i++) | ||
| return WavEncoder.FromAudioClip(audioClip); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Debug.LogError($"[AudioRecorder] WAV 변환 실패: {ex.Message}"); | ||
| return Array.Empty<byte>(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 녹음 파일 저장 (디버깅용) | ||
| /// </summary> | ||
| public bool SaveRecordingToFile(AudioClip audioClip, string fileName = "recording") | ||
| { | ||
| if (audioClip == null) | ||
| { | ||
| Debug.LogError("[AudioRecorder] 저장할 AudioClip이 null입니다."); | ||
| return false; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| byte[] wavData = AudioClipToWavBytes(audioClip); | ||
| if (wavData.Length == 0) | ||
| { | ||
| short sample = (short)(samples[i] * short.MaxValue); | ||
| BitConverter.GetBytes(sample).CopyTo(audioBytes, i * 2); | ||
| Debug.LogError("[AudioRecorder] WAV 데이터 변환 실패"); | ||
| return false; | ||
| } | ||
|
|
||
| string filePath = System.IO.Path.Combine(Application.persistentDataPath, $"{fileName}.wav"); | ||
| System.IO.File.WriteAllBytes(filePath, wavData); | ||
|
|
||
| return audioBytes; | ||
| Debug.Log($"[AudioRecorder] 녹음 파일 저장됨: {filePath} ({wavData.Length} bytes)"); | ||
|
|
||
| return true; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Debug.LogError($"[AudioRecorder] AudioClip을 byte 배열로 변환 실패: {ex.Message}"); | ||
| return new byte[0]; | ||
| Debug.LogError($"[AudioRecorder] 파일 저장 실패: {ex.Message}"); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 사용 가능한 마이크 목록 반환 | ||
| /// </summary> | ||
| public string[] GetAvailableMicrophones() | ||
| { | ||
| return Microphone.devices; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 기본 마이크 반환 | ||
| /// </summary> | ||
| public string GetDefaultMicrophone() | ||
| { | ||
| string[] devices = Microphone.devices; | ||
| return devices.Length > 0 ? devices[0] : string.Empty; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 현재 마이크 설정 | ||
| /// </summary> | ||
| public void SetMicrophone(string deviceName) | ||
| { | ||
| if (_isRecording) | ||
| { | ||
| Debug.LogError("[AudioRecorder] 녹음 중에는 마이크를 변경할 수 없습니다."); | ||
| return; | ||
| } | ||
|
|
||
| if (Array.Exists(Microphone.devices, device => device == deviceName)) | ||
| { | ||
| _currentDevice = deviceName; | ||
| Debug.Log($"[AudioRecorder] 마이크 변경됨: {deviceName}"); | ||
| } | ||
| else | ||
| { | ||
| Debug.LogWarning($"[AudioRecorder] 존재하지 않는 마이크: {deviceName}"); | ||
| } | ||
| } | ||
|
|
||
| #endregion | ||
|
|
||
| #region Private Methods | ||
|
|
||
| private void ProcessRecordingClip() | ||
| /// <summary> | ||
| /// 마이크 초기화 | ||
| /// </summary> | ||
| private void InitializeMicrophone() | ||
| { | ||
| string[] devices = Microphone.devices; | ||
| if (devices.Length > 0) | ||
| { | ||
| _currentDevice = devices[0]; | ||
| Debug.Log($"[AudioRecorder] 마이크 초기화됨: {_currentDevice}"); | ||
| } | ||
| else | ||
| { | ||
| Debug.LogError("[AudioRecorder] 사용 가능한 마이크가 없습니다."); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 녹음된 AudioClip 처리 | ||
| /// </summary> | ||
| private AudioClip? ProcessRecordingClip(float actualDuration) | ||
| { | ||
| if (_recordingClip == null) | ||
| return; | ||
| return null; | ||
|
|
||
| int recordedLength = Microphone.GetPosition(null); | ||
| if (recordedLength <= 0) | ||
| // 실제 녹음 시간을 기반으로 샘플 수 계산 | ||
| int actualSamples = Mathf.RoundToInt(actualDuration * _sampleRate); | ||
|
|
||
| // 최대 샘플 수 제한 (버퍼 크기) | ||
| int maxSamples = _recordingClip.samples; | ||
| actualSamples = Mathf.Min(actualSamples, maxSamples); | ||
|
|
||
| Debug.Log($"[AudioRecorder] 녹음 데이터 처리 중 ({actualSamples}/{_recordingClip.samples} 샘플, {actualDuration:F1}초)"); | ||
|
|
||
| if (actualSamples <= 0) | ||
| { | ||
| Debug.LogWarning("[AudioRecorder] 녹음된 데이터가 없습니다."); | ||
| return; | ||
| return null; | ||
| } | ||
|
|
||
| // 실제 녹음된 길이만큼만 새로운 AudioClip 생성 | ||
| AudioClip processedClip = AudioClip.Create( | ||
| "RecordedAudio", | ||
| recordedLength, | ||
| actualSamples, | ||
| _recordingClip.channels, | ||
| _recordingClip.frequency, | ||
| false | ||
| ); | ||
|
|
||
| float[] samples = new float[recordedLength * _recordingClip.channels]; | ||
| float[] samples = new float[actualSamples * _recordingClip.channels]; | ||
| _recordingClip.GetData(samples, 0); | ||
|
|
||
| // 노이즈 리덕션 적용 | ||
| if (_enableNoiseReduction) | ||
| { | ||
| ApplyNoiseReduction(samples); | ||
| } | ||
|
|
||
| processedClip.SetData(samples, 0); | ||
|
|
||
| // 원본 AudioClip 정리하여 메모리 누수 방지 | ||
| if (_recordingClip != null) | ||
| { | ||
| DestroyImmediate(_recordingClip); | ||
| } | ||
|
|
||
| _recordingClip = processedClip; | ||
|
|
||
| Debug.Log($"[AudioRecorder] AudioClip 생성 완료 ({_recordingClip.samples} 샘플, {_recordingClip.channels} 채널, {_recordingClip.frequency}Hz)"); | ||
|
|
||
| return _recordingClip; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 노이즈 리덕션 적용 | ||
| /// </summary> | ||
| private void ApplyNoiseReduction(float[] audioData) | ||
| { | ||
| for (int i = 0; i < audioData.Length; i++) | ||
| { | ||
| if (Mathf.Abs(audioData[i]) < _silenceThreshold) | ||
| { | ||
| audioData[i] = 0f; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #endregion | ||
|
|
||