Files
Kizza00232Jera 17532b524f [unity] avatars
2025-06-11 14:45:45 +02:00

381 lines
10 KiB
C#

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#nullable enable
#if UNITY_WEBGL || OVR_DISABLE_MICROPHONE
#define DISABLE_MICROPHONE
#endif
using System;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using Oculus.Avatar2;
using UnityEngine;
using Debug = UnityEngine.Debug;
#if UNITY_ANDROID && !UNITY_EDITOR
using UnityEngine.Android;
#endif
public enum LipSyncMicInputMode
{
HoldToSpeak, // Constantly hold a button to enable the mic
PushToSpeak, // Press a button to enable the mic; press again to disable
ConstantSpeak, // Mic is always on
Manual // Call StartMicrophone and StopMicrophone to control externally
}
[RequireComponent(typeof(AudioSource))]
public class LipSyncMicInput : MonoBehaviour
{
// Serialized Members
[Tooltip("Manual specification of Audio Source. Default will use any attached to the same object.")]
[SerializeField]
private AudioSource? _audioSource;
[Range(0f, 1f)]
[Tooltip("Microphone input volume control.")]
[SerializeField]
private float _micInputVolume = 1f;
[SerializeField]
private LipSyncMicInputMode _micInputMode = LipSyncMicInputMode.ConstantSpeak;
[Tooltip("Button name used to drive HoldToSpeak and PushToSpeak methods of control.")]
[SerializeField]
private string _inputButtonName = "";
[SerializeField]
private bool _stopRecordingWhilePaused = true;
[SerializeField]
private bool _stopRecordingWhileUnfocused = true;
[Tooltip("The max amount of time to wait for mic recording to start.")]
[SerializeField]
private float _micCaptureTimeout = 5f;
[SerializeField]
private bool _preferOculusMic = true;
// Public Properties
public AudioSource? audioSource => _audioSource;
public int MicFrequency => _micFrequency;
public float MicInputVolume
{
get { return _micInputVolume; }
set
{
_micInputVolume = Mathf.Clamp01(value);
if (_audioSource is not null)
{
_audioSource.volume = _micInputVolume;
}
else
{
OvrAvatarLog.LogError("No audio source found");
}
}
}
public bool micSelected => _selectedDevice?.Length > 0;
// Active will be true whenever the mic should be recording according to the InputMode
// Active can still be true when the mic is not recording due to lack of focus or because the app is paused
public bool active { get; private set; }
// Other Private members
private bool _initialized;
private bool _askingPermission;
private int _micFrequency = 48000;
private bool _focused = true;
private bool _paused;
private string? _selectedDevice;
private int _minFreq, _maxFreq;
// Core Private Functions
private void Awake()
{
if (_audioSource == null)
{
_audioSource = GetComponent<AudioSource>();
}
}
private void Start()
{
if (_audioSource is not null)
{
_audioSource.loop = true;
_audioSource.mute = false;
_audioSource.volume = _micInputVolume;
}
else
{
OvrAvatarLog.LogError("Audio source not set up");
}
}
private void Update()
{
// Lazy Microphone initialization (needed on Android)
if (!_initialized)
{
#if UNITY_ANDROID && !UNITY_EDITOR
if( Permission.HasUserAuthorizedPermission("android.permission.RECORD_AUDIO") )
{
InitializeMicrophone();
}
else if(!_askingPermission)
{
_askingPermission = true;
OvrAvatarManager.Instance.RequestMicPermission();
return;
}
else
{
return;
}
#else
InitializeMicrophone();
#endif
}
ProcessMicActivity();
}
// Events
private void OnApplicationFocus(bool focus)
{
_focused = focus;
if (!_focused && _stopRecordingWhileUnfocused) StopMicrophone_Internal();
}
private void OnApplicationPause(bool pauseStatus)
{
_paused = pauseStatus;
if (_paused && _stopRecordingWhilePaused) StopMicrophone_Internal();
}
// Other Private Functions
private void InitializeMicrophone()
{
#if !DISABLE_MICROPHONE
if (_initialized) return;
if (Microphone.devices.Length == 0) return;
_selectedDevice = Microphone.devices[0];
if (_preferOculusMic)
{
foreach (var device in Microphone.devices)
{
if (device.Contains("Oculus") || device.Contains("Rift"))
{
_selectedDevice = device;
break;
}
}
}
Debug.Log($"Selected microphone {_selectedDevice}");
GetMicCaps();
_initialized = true;
#endif
}
private void ProcessMicActivity()
{
#if !DISABLE_MICROPHONE
//Hold To Speak
if (_micInputMode == LipSyncMicInputMode.HoldToSpeak)
{
active = Input.GetButton(_inputButtonName);
}
//Push To Talk
else if (_micInputMode == LipSyncMicInputMode.PushToSpeak)
{
if (Input.GetButtonDown(_inputButtonName))
{
active = !active;
}
}
//Constant Speak
else if (_micInputMode == LipSyncMicInputMode.ConstantSpeak)
{
active = true;
}
// Activation
if (active)
{
if (CanStartMic() && !Microphone.IsRecording(_selectedDevice))
{
StartMicrophone_Internal();
}
}
else if (Microphone.IsRecording(_selectedDevice))
{
StopMicrophone_Internal();
}
#endif
}
private void GetMicCaps()
{
#if !DISABLE_MICROPHONE
if (micSelected == false) return;
//Gets the frequency of the device
Microphone.GetDeviceCaps(_selectedDevice, out _minFreq, out _maxFreq);
if (_minFreq == 0 && _maxFreq == 0)
{
Debug.Log("OvrAvatarLipSyncMicInput: GetMicCaps warning - min and max frequencies are 0");
_minFreq = 48000;
_maxFreq = 48000;
}
if (_micFrequency > _maxFreq)
{
_micFrequency = _maxFreq;
}
#endif
}
private bool CanStartMic()
{
if (!micSelected) return false;
if (!_focused && _stopRecordingWhileUnfocused) return false;
if (_paused && _stopRecordingWhilePaused) return false;
return true;
}
private void StartMicrophone_Internal()
{
#if !DISABLE_MICROPHONE
Debug.Log($"Starting microphone recording with frequency {_micFrequency}");
if (_audioSource is not null)
{
//Starts recording
_audioSource.clip = Microphone.Start(_selectedDevice, true, 10, _micFrequency);
_audioSource.loop = true;
Stopwatch timer = Stopwatch.StartNew();
// Wait until the recording has started
while (!(Microphone.GetPosition(_selectedDevice) > 0) && timer.Elapsed.TotalMilliseconds < _micCaptureTimeout)
{
Thread.Sleep(5);
}
var samplesRecorded = Microphone.GetPosition(_selectedDevice);
if (samplesRecorded <= 0)
{
throw new Exception("Timeout initializing microphone " + _selectedDevice);
}
// Play the audio source
var latency = (float)samplesRecorded / _micFrequency;
Debug.Log($"Microphone recording started with latency {latency * 1000.0} ms");
_audioSource.Play();
}
else
{
OvrAvatarLog.LogError("No audio source found");
}
#endif
}
private void StopMicrophone_Internal()
{
#if !DISABLE_MICROPHONE
if (micSelected == false) return;
Debug.Log($"Stopping microphone recording");
// Don't stop the audio source if it is overridden with a clip to play
if (_audioSource != null &&
_audioSource.clip != null &&
_audioSource.clip.name == "Microphone")
{
_audioSource.Stop();
}
Microphone.End(_selectedDevice);
#endif
}
//:: Public Functions
public void SetMode(LipSyncMicInputMode newMode)
{
_micInputMode = newMode;
active = false;
if (_initialized) ProcessMicActivity();
}
public void StartMicrophone()
{
#if !DISABLE_MICROPHONE
if (_micInputMode != LipSyncMicInputMode.Manual)
{
Debug.LogWarning("Starting Microphone while not in Manual Input Mode.");
}
active = true;
if (CanStartMic() && !Microphone.IsRecording(_selectedDevice))
{
StartMicrophone_Internal();
}
#endif
}
public void StopMicrophone()
{
#if !DISABLE_MICROPHONE
if (_micInputMode != LipSyncMicInputMode.Manual)
{
Debug.LogWarning("Starting Microphone while not in Manual Input Mode.");
}
active = false;
if (Microphone.IsRecording(_selectedDevice))
{
StopMicrophone_Internal();
}
#endif
}
}