Improves microphone audio capture.

- Uses audio monitoring during recording.
- Records the audio in a WAV file.
This commit is contained in:
Jonas Luz Jr. 2025-11-25 10:08:39 -03:00
parent d0ff6aac6b
commit 15db99b032
3 changed files with 232 additions and 20 deletions

View File

@ -3,7 +3,7 @@
<ui:VisualElement name="VE_Panel" style="flex-grow: 1;"> <ui:VisualElement name="VE_Panel" style="flex-grow: 1;">
<ui:Button text="Iniciar sessão" name="B_Session" /> <ui:Button text="Iniciar sessão" name="B_Session" />
<ui:Label text="Chat:" /> <ui:Label text="Chat:" />
<ui:TextField placeholder-text="Digite sua mensagem" name="TF_ChatInput" hide-mobile-input="false" class="expandable" /> <ui:TextField placeholder-text="Digite sua mensagem" name="TF_ChatInput" class="expandable" />
<ui:Button text="Enviar mensagem" name="B_SendChat" /> <ui:Button text="Enviar mensagem" name="B_SendChat" />
<ui:ProgressBar value="0" title="Processando... Por favor, espere..." name="PB_Progress" enabled="true" /> <ui:ProgressBar value="0" title="Processando... Por favor, espere..." name="PB_Progress" enabled="true" />
<ui:TextField placeholder-text="response" multiline="true" readonly="true" name="TF_ChatOutput" class="expandable" /> <ui:TextField placeholder-text="response" multiline="true" readonly="true" name="TF_ChatOutput" class="expandable" />

View File

@ -440,6 +440,18 @@ PrefabInstance:
serializedVersion: 3 serializedVersion: 3
m_TransformParent: {fileID: 0} m_TransformParent: {fileID: 0}
m_Modifications: m_Modifications:
- target: {fileID: 796320736253214861, guid: 004ea84725223334994cf72d2115c180, type: 3}
propertyPath: OutputAudioMixerGroup
value:
objectReference: {fileID: -7600268988427216071, guid: b4346293b16e5254aadce52e891ff5e9, type: 2}
- target: {fileID: 1938294408165125026, guid: 004ea84725223334994cf72d2115c180, type: 3}
propertyPath: _fileName
value: RecordedAudio.wav
objectReference: {fileID: 0}
- target: {fileID: 1938294408165125026, guid: 004ea84725223334994cf72d2115c180, type: 3}
propertyPath: _selectedMicrophoneIndex
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3050228793350555746, guid: 004ea84725223334994cf72d2115c180, type: 3} - target: {fileID: 3050228793350555746, guid: 004ea84725223334994cf72d2115c180, type: 3}
propertyPath: m_Name propertyPath: m_Name
value: -- GAME MANAGER -- value: -- GAME MANAGER --

View File

@ -1,3 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine; using UnityEngine;
@ -17,43 +21,239 @@ namespace PPGIA.X540.Project3
[Header("Audio Capture Settings")] [Header("Audio Capture Settings")]
[SerializeField] [SerializeField]
private SampleRate _sampleRateInHz = SampleRate.Hz44100; private SampleRate _sampleRateInHz = SampleRate.Hz44100;
[SerializeField] [SerializeField]
private bool _playingBack = false; 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<string> OnRecordingSaved; // path
public event Action OnRecordingStarted;
public event Action OnRecordingStopped;
private AudioSource _audioSource; private AudioSource _audioSource;
private AudioClip _audioClip; private bool _isRecording;
private List<short> _capturedSamples =
new List<short>(1024 * 32); // filled only on Stop
private int _channels = 1; // microphone channel count (Unity usually mono)
private AudioClip _recordingClip;
private string _currentDevice;
void Awake() private void Awake()
{ {
_audioSource = GetComponent<AudioSource>(); _audioSource = GetComponent<AudioSource>();
_microphones = Microphone.devices;
} }
void OnDestroy() private void OnDestroy()
{ {
Stop(); StopRecording();
} }
[ContextMenu("Record Audio")] [ContextMenu("Start recording audio")]
public void Record() public void StartRecording()
{ {
_audioClip = Microphone.Start(null, true, 1, (int)_sampleRateInHz); if (_isRecording)
_audioSource.clip = _audioClip;
_audioSource.loop = true;
// Wait until the microphone starts recording
while (!(Microphone.GetPosition(null) > 0)) { }
if (_playingBack)
{ {
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(); _audioSource.Play();
} }
} }
[ContextMenu("Stop Audio")] [ContextMenu("Stop recording audio")]
public void Stop() public void StopRecording()
{ {
Microphone.End(null); 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;
} }
} }
} }