22using System ;
33using UnityEngine ;
44using System . Collections . Generic ;
5+ using ProjectVG . Infrastructure . Audio ;
56
67namespace ProjectVG . Core . Audio
78{
9+ /// <summary>
10+ /// 정확한 시간 기반 음성 녹음 시스템
11+ /// 녹음 시작/중지 시간을 기반으로 정확한 길이의 오디오를 생성합니다.
12+ /// </summary>
813 public class AudioRecorder : Singleton < AudioRecorder >
914 {
1015 [ Header ( "Recording Settings" ) ]
1116 [ SerializeField ] private int _sampleRate = 44100 ;
1217 [ SerializeField ] private int _channels = 1 ;
13- [ SerializeField ] private int _maxRecordingLength = 30 ;
18+ [ SerializeField ] private int _maxRecordingLength = 30 ; // 최대 녹음 시간 (초)
19+ 20+ [ Header ( "Audio Processing" ) ]
21+ [ SerializeField ] private bool _enableNoiseReduction = false ; // 노이즈 제거 비활성화
22+ [ SerializeField ] private float _silenceThreshold = 0.001f ; // 무음 임계값 낮춤
1423
1524 private AudioClip ? _recordingClip ;
1625 private bool _isRecording = false ;
1726 private float _recordingStartTime ;
18- private List < float > _audioBuffer ;
19- 20- public bool IsRecording => _isRecording ;
21- public float RecordingDuration => _isRecording ? Time . time - _recordingStartTime : 0f ;
22- public bool IsRecordingAvailable => Microphone . devices . Length > 0 ;
27+ private float _recordingEndTime ;
28+ private string _currentDevice = null ;
2329
30+ // 이벤트
2431 public event Action ? OnRecordingStarted ;
2532 public event Action ? OnRecordingStopped ;
2633 public event Action < AudioClip > ? OnRecordingCompleted ;
2734 public event Action < string > ? OnError ;
35+ public event Action < float > ? OnRecordingProgress ; // 녹음 진행률 (0-1)
36+ 37+ // 프로퍼티
38+ public bool IsRecording => _isRecording ;
39+ public float RecordingDuration => _isRecording ? Time . time - _recordingStartTime : 0f ;
40+ public bool IsRecordingAvailable => Microphone . devices . Length > 0 ;
41+ public float RecordingProgress => _isRecording ? Mathf . Clamp01 ( RecordingDuration / _maxRecordingLength ) : 0f ;
2842
2943 #region Unity Lifecycle
3044
3145 protected override void Awake ( )
3246 {
3347 base . Awake ( ) ;
34- _audioBuffer = new List < float > ( ) ;
48+ InitializeMicrophone ( ) ;
3549 }
3650
3751 private void Update ( )
3852 {
39- if ( _isRecording && RecordingDuration >= _maxRecordingLength )
53+ if ( _isRecording )
4054 {
41- StopRecording ( ) ;
55+ // 녹음 진행률 이벤트 발생
56+ OnRecordingProgress ? . Invoke ( RecordingProgress ) ;
57+ 58+ // 최대 녹음 시간 체크
59+ if ( RecordingDuration >= _maxRecordingLength )
60+ {
61+ StopRecording ( ) ;
62+ }
4263 }
4364 }
4465
@@ -54,6 +75,10 @@ private void OnDestroy()
5475
5576 #region Public Methods
5677
78+ /// <summary>
79+ /// 음성 녹음 시작
80+ /// </summary>
81+ /// <returns>녹음 시작 성공 여부</returns>
5782 public bool StartRecording ( )
5883 {
5984 if ( _isRecording )
@@ -73,10 +98,11 @@ public bool StartRecording()
7398 {
7499 _isRecording = true ;
75100 _recordingStartTime = Time . time ;
76- _audioBuffer . Clear ( ) ;
77101
78- _recordingClip = Microphone . Start ( null , false , _maxRecordingLength , _sampleRate ) ;
102+ // 최대 녹음 시간만큼 버퍼 할당
103+ _recordingClip = Microphone . Start ( _currentDevice , false , _maxRecordingLength , _sampleRate ) ;
79104
105+ Debug . Log ( $ "[AudioRecorder] 녹음 시작 - 최대 시간: { _maxRecordingLength } 초, 샘플레이트: { _sampleRate } Hz") ;
80106 OnRecordingStarted ? . Invoke ( ) ;
81107
82108 return true ;
@@ -90,6 +116,10 @@ public bool StartRecording()
90116 }
91117 }
92118
119+ /// <summary>
120+ /// 음성 녹음 중지
121+ /// </summary>
122+ /// <returns>처리된 AudioClip</returns>
93123 public AudioClip ? StopRecording ( )
94124 {
95125 if ( ! _isRecording )
@@ -101,17 +131,22 @@ public bool StartRecording()
101131 try
102132 {
103133 _isRecording = false ;
134+ _recordingEndTime = Time . time ;
135+ float actualRecordingDuration = _recordingEndTime - _recordingStartTime ;
104136
105- Microphone . End ( null ) ;
137+ Microphone . End ( _currentDevice ) ;
106138
107139 if ( _recordingClip != null )
108140 {
109- ProcessRecordingClip ( ) ;
110- OnRecordingCompleted ? . Invoke ( _recordingClip ) ;
141+ AudioClip processedClip = ProcessRecordingClip ( actualRecordingDuration ) ;
142+ if ( processedClip != null )
143+ {
144+ Debug . Log ( $ "[AudioRecorder] 녹음 완료 - 실제 녹음 시간: { actualRecordingDuration : F2} 초, 샘플: { processedClip . samples } ") ;
145+ OnRecordingCompleted ? . Invoke ( processedClip ) ;
146+ }
111147 }
112148
113149 OnRecordingStopped ? . Invoke ( ) ;
114- 115150 return _recordingClip ;
116151 }
117152 catch ( Exception ex )
@@ -123,72 +158,175 @@ public bool StartRecording()
123158 }
124159 }
125160
126- public byte [ ] AudioClipToBytes ( AudioClip audioClip )
161+ /// <summary>
162+ /// AudioClip을 WAV 바이트 배열로 변환
163+ /// </summary>
164+ public byte [ ] AudioClipToWavBytes ( AudioClip audioClip )
127165 {
128166 if ( audioClip == null )
129- return new byte [ 0 ] ;
130- 167+ return Array . Empty < byte > ( ) ;
131168 try
132169 {
133- float [ ] samples = new float [ audioClip . samples * audioClip . channels ] ;
134- audioClip . GetData ( samples , 0 ) ;
135- 136- byte [ ] audioBytes = new byte [ samples . Length * 2 ] ;
137- for ( int i = 0 ; i < samples . Length ; i ++ )
170+ return WavEncoder . FromAudioClip ( audioClip ) ;
171+ }
172+ catch ( Exception ex )
173+ {
174+ Debug . LogError ( $ "[AudioRecorder] WAV 변환 실패: { ex . Message } ") ;
175+ return Array . Empty < byte > ( ) ;
176+ }
177+ }
178+ 179+ /// <summary>
180+ /// 녹음 파일 저장 (디버깅용)
181+ /// </summary>
182+ public bool SaveRecordingToFile ( AudioClip audioClip , string fileName = "recording" )
183+ {
184+ if ( audioClip == null )
185+ {
186+ Debug . LogError ( "[AudioRecorder] 저장할 AudioClip이 null입니다." ) ;
187+ return false ;
188+ }
189+ 190+ try
191+ {
192+ byte [ ] wavData = AudioClipToWavBytes ( audioClip ) ;
193+ if ( wavData . Length == 0 )
138194 {
139- short sample = ( short ) ( samples [ i ] * short . MaxValue ) ;
140- BitConverter . GetBytes ( sample ) . CopyTo ( audioBytes , i * 2 ) ;
195+ Debug . LogError ( "[AudioRecorder] WAV 데이터 변환 실패" ) ;
196+ return false ;
141197 }
198+ 199+ string filePath = System . IO . Path . Combine ( Application . persistentDataPath , $ "{ fileName } .wav") ;
200+ System . IO . File . WriteAllBytes ( filePath , wavData ) ;
201+ 202+ Debug . Log ( $ "[AudioRecorder] 녹음 파일 저장 완료: { filePath } ") ;
203+ Debug . Log ( $ "[AudioRecorder] 파일 크기: { wavData . Length } bytes") ;
204+ Debug . Log ( $ "[AudioRecorder] AudioClip 정보 - 샘플: { audioClip . samples } , 채널: { audioClip . channels } , 주파수: { audioClip . frequency } ") ;
142205
143- return audioBytes ;
206+ return true ;
144207 }
145208 catch ( Exception ex )
146209 {
147- Debug . LogError ( $ "[AudioRecorder] AudioClip을 byte 배열로 변환 실패: { ex . Message } ") ;
148- return new byte [ 0 ] ;
210+ Debug . LogError ( $ "[AudioRecorder] 파일 저장 실패: { ex . Message } ") ;
211+ return false ;
149212 }
150213 }
151214
215+ /// <summary>
216+ /// 사용 가능한 마이크 목록 반환
217+ /// </summary>
152218 public string [ ] GetAvailableMicrophones ( )
153219 {
154220 return Microphone . devices ;
155221 }
156222
223+ /// <summary>
224+ /// 기본 마이크 반환
225+ /// </summary>
157226 public string GetDefaultMicrophone ( )
158227 {
159228 string [ ] devices = Microphone . devices ;
160229 return devices . Length > 0 ? devices [ 0 ] : string . Empty ;
161230 }
162231
232+ /// <summary>
233+ /// 현재 마이크 설정
234+ /// </summary>
235+ public void SetMicrophone ( string deviceName )
236+ {
237+ if ( Array . Exists ( Microphone . devices , device => device == deviceName ) )
238+ {
239+ _currentDevice = deviceName ;
240+ Debug . Log ( $ "[AudioRecorder] 마이크 설정 변경: { deviceName } ") ;
241+ }
242+ else
243+ {
244+ Debug . LogWarning ( $ "[AudioRecorder] 존재하지 않는 마이크: { deviceName } ") ;
245+ }
246+ }
247+ 163248 #endregion
164249
165250 #region Private Methods
166251
167- private void ProcessRecordingClip ( )
252+ /// <summary>
253+ /// 마이크 초기화
254+ /// </summary>
255+ private void InitializeMicrophone ( )
256+ {
257+ string [ ] devices = Microphone . devices ;
258+ if ( devices . Length > 0 )
259+ {
260+ _currentDevice = devices [ 0 ] ;
261+ Debug . Log ( $ "[AudioRecorder] 기본 마이크 설정: { _currentDevice } ") ;
262+ }
263+ else
264+ {
265+ Debug . LogError ( "[AudioRecorder] 사용 가능한 마이크가 없습니다." ) ;
266+ }
267+ }
268+ 269+ /// <summary>
270+ /// 녹음된 AudioClip 처리
271+ /// </summary>
272+ private AudioClip ? ProcessRecordingClip ( float actualDuration )
168273 {
169274 if ( _recordingClip == null )
170- return ;
275+ return null ;
171276
172- int recordedLength = Microphone . GetPosition ( null ) ;
173- if ( recordedLength <= 0 )
277+ // 실제 녹음 시간을 기반으로 샘플 수 계산
278+ int actualSamples = Mathf . RoundToInt ( actualDuration * _sampleRate ) ;
279+ 280+ // 최대 샘플 수 제한 (버퍼 크기)
281+ int maxSamples = _recordingClip . samples ;
282+ actualSamples = Mathf . Min ( actualSamples , maxSamples ) ;
283+ 284+ Debug . Log ( $ "[AudioRecorder] 실제 녹음 길이: { actualSamples } 샘플, 전체 버퍼: { _recordingClip . samples } 샘플, 실제 시간: { actualDuration : F2} 초") ;
285+ 286+ if ( actualSamples <= 0 )
174287 {
175288 Debug . LogWarning ( "[AudioRecorder] 녹음된 데이터가 없습니다." ) ;
176- return ;
289+ return null ;
177290 }
178291
292+ // 실제 녹음된 길이만큼만 새로운 AudioClip 생성
179293 AudioClip processedClip = AudioClip . Create (
180294 "RecordedAudio" ,
181- recordedLength ,
295+ actualSamples ,
182296 _recordingClip . channels ,
183297 _recordingClip . frequency ,
184298 false
185299 ) ;
186300
187- float [ ] samples = new float [ recordedLength * _recordingClip . channels ] ;
301+ float [ ] samples = new float [ actualSamples * _recordingClip . channels ] ;
188302 _recordingClip . GetData ( samples , 0 ) ;
189- processedClip . SetData ( samples , 0 ) ;
190303
304+ // 노이즈 리덕션 적용
305+ if ( _enableNoiseReduction )
306+ {
307+ ApplyNoiseReduction ( samples ) ;
308+ }
309+ 310+ processedClip . SetData ( samples , 0 ) ;
191311 _recordingClip = processedClip ;
312+ 313+ Debug . Log ( $ "[AudioRecorder] 처리된 AudioClip - 샘플: { _recordingClip . samples } , 채널: { _recordingClip . channels } , 주파수: { _recordingClip . frequency } ") ;
314+ 315+ return _recordingClip ;
316+ }
317+ 318+ /// <summary>
319+ /// 노이즈 리덕션 적용
320+ /// </summary>
321+ private void ApplyNoiseReduction ( float [ ] audioData )
322+ {
323+ for ( int i = 0 ; i < audioData . Length ; i ++ )
324+ {
325+ if ( Mathf . Abs ( audioData [ i ] ) < _silenceThreshold )
326+ {
327+ audioData [ i ] = 0f ;
328+ }
329+ }
192330 }
193331
194332 #endregion
0 commit comments