using System; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; namespace PPGIA.X540.Project3 { [RequireComponent(typeof(AudioSource))] public class AudioCapture : MonoBehaviour { public enum SampleRate { Hz16000 = 16000, Hz44100 = 44100, Hz48000 = 48000, Hz96000 = 96000 } [Header("Audio Capture Settings")] [SerializeField] private SampleRate _sampleRateInHz = SampleRate.Hz44100; [SerializeField] private int _maxRecordingSeconds = 300; // safety cap [SerializeField] private string _fileName = "RecordedAudio.wav"; [SerializeField, Range(0f, 1f)] private float _playbackVolume = 1f; // volume while monitoring [SerializeField] private bool _enableMonitoring = true; // if true, playback your mic [SerializeField, Tooltip("Latency in milliseconds to offset monitoring playback from the microphone write head")] private int _monitorLatencyMs = 80; [SerializeField] private string[] _microphones; [SerializeField] private int _selectedMicrophoneIndex = 0; public string SelectedDevice => _microphones.Length > 0 && _selectedMicrophoneIndex < _microphones.Length && _selectedMicrophoneIndex >= 0 ? _microphones[_selectedMicrophoneIndex] : null; public bool IsRecording => _isRecording; public string LastSavedFilePath { get; private set; } public event Action OnRecordingSaved; // path public event Action OnRecordingStarted; public event Action OnRecordingStopped; private AudioSource _audioSource; private bool _isRecording; private List _capturedSamples = new List(1024 * 32); // filled only on Stop private int _channels = 1; // microphone channel count (Unity usually mono) private AudioClip _recordingClip; private string _currentDevice; private void Awake() { _audioSource = GetComponent(); _microphones = Microphone.devices; } private void OnDestroy() { StopRecording(); } [ContextMenu("Start recording audio")] public void StartRecording() { if (_isRecording) { Debug.LogWarning("Already recording."); return; } if (_microphones == null || _microphones.Length == 0) { Debug.LogError("No microphone devices found."); return; } _currentDevice = SelectedDevice; int frequency = GetSupportedFrequency(_currentDevice, (int)_sampleRateInHz); // start as looped for safe monitoring without underruns _recordingClip = Microphone.Start( _currentDevice, true, _maxRecordingSeconds, frequency); if (_recordingClip == null) { Debug.LogError("Failed to start microphone recording."); return; } _channels = Mathf.Max(1, _recordingClip.channels); _capturedSamples.Clear(); _isRecording = true; StartCoroutine(WaitAndStartMonitoring()); OnRecordingStarted?.Invoke(); } private IEnumerator WaitAndStartMonitoring() { // Wait until microphone has started providing data while (Microphone.GetPosition(_currentDevice) <= 0) { yield return null; } // Recording might have been stopped early if (!_isRecording) yield break; if (_enableMonitoring) { _audioSource.loop = true; _audioSource.clip = _recordingClip; _audioSource.volume = _playbackVolume; // Start playback slightly behind the microphone write position to avoid underruns/noise int pos = Microphone.GetPosition(_currentDevice); int latency = Mathf.Clamp((int)(_recordingClip.frequency * (_monitorLatencyMs / 1000f)), 0, _recordingClip.samples - 1); int start = pos - latency; if (start < 0) start += _recordingClip.samples; // wrap around for looped clip _audioSource.timeSamples = start % _recordingClip.samples; _audioSource.Play(); } } [ContextMenu("Stop recording audio")] public void StopRecording() { if (!_isRecording) { Debug.LogWarning("Not currently recording."); return; } if (_audioSource.isPlaying) _audioSource.Stop(); var position = Microphone.GetPosition(_currentDevice); Microphone.End(_currentDevice); _isRecording = false; // Capture only the recorded portion (GetPosition tells how many samples per channel were written) if (position <= 0) { Debug.LogWarning("Microphone position is zero; no data captured."); } else { try { int samplesToCopy = position * _recordingClip.channels; float[] floatData = new float[samplesToCopy]; _recordingClip.GetData(floatData, 0); for (int i = 0; i < floatData.Length; i++) { float clamped = Mathf.Clamp(floatData[i], -1f, 1f); short sample = (short)(clamped * short.MaxValue); _capturedSamples.Add(sample); } } catch (Exception ex) { Debug.LogError($"Failed to read microphone data: {ex.Message}"); } } OnRecordingStopped?.Invoke(); SaveWavFile(); } private void SaveWavFile() { if (_capturedSamples.Count == 0) { Debug.LogWarning("No audio samples captured; skipping file save."); return; } string filePath = Path.Combine(Application.persistentDataPath, _fileName); try { using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) using (var writer = new BinaryWriter(fs)) { int sampleRate = _recordingClip != null ? _recordingClip.frequency : (int)_sampleRateInHz; int bitsPerSample = 16; int channels = _recordingClip != null ? _recordingClip.channels : _channels; int byteRate = sampleRate * channels * bitsPerSample / 8; byte[] dataBytes = new byte[_capturedSamples.Count * 2]; Buffer.BlockCopy(_capturedSamples.ToArray(), 0, dataBytes, 0, dataBytes.Length); int subchunk2Size = dataBytes.Length; int chunkSize = 36 + subchunk2Size; // RIFF header writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF")); writer.Write(chunkSize); writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVE")); // fmt subchunk writer.Write(System.Text.Encoding.ASCII.GetBytes("fmt ")); writer.Write(16); // PCM header length writer.Write((short)1); // Audio format = PCM writer.Write((short)channels); writer.Write(sampleRate); writer.Write(byteRate); writer.Write((short)(channels * bitsPerSample / 8)); // block align writer.Write((short)bitsPerSample); // data subchunk writer.Write(System.Text.Encoding.ASCII.GetBytes("data")); writer.Write(subchunk2Size); writer.Write(dataBytes); } LastSavedFilePath = filePath; Debug.Log($"Audio saved to: {filePath}"); OnRecordingSaved?.Invoke(filePath); } catch (Exception ex) { Debug.LogError($"Failed to save WAV file: {ex.Message}"); } finally { _capturedSamples.Clear(); } } [ContextMenu("Refresh microphone list")] public void RefreshMicrophones() { _microphones = Microphone.devices; if (_microphones == null || _microphones.Length == 0) { Debug.LogWarning("No microphones detected after refresh."); _selectedMicrophoneIndex = -1; } else if ( _selectedMicrophoneIndex < 0 || _selectedMicrophoneIndex >= _microphones.Length) { _selectedMicrophoneIndex = 0; } } private int GetSupportedFrequency(string device, int requested) { int min, max; Microphone.GetDeviceCaps(device, out min, out max); // When both are 0, any frequency is supported if (min == 0 && max == 0) return requested; if (requested < min) return min; if (requested > max) return max; return requested; } } }