// %BANNER_BEGIN% // --------------------------------------------------------------------- // %COPYRIGHT_BEGIN% // Copyright (c) 2022-2023 Magic Leap, Inc. All Rights Reserved. // Use of this file is governed by the Magic Leap 2 Software License Agreement, located here: https://www.magicleap.com/software-license-agreement-ml2 // Terms and conditions applicable to third-party materials accompanying this distribution may also be found in the top-level NOTICE file appearing herein. // %COPYRIGHT_END% // --------------------------------------------------------------------- // %BANNER_END% using AOT; using System; using System.Collections.Concurrent; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.XR.MagicLeap; namespace MagicLeap.Spectator { #region C API public class MLVirtualMicAPI { protected static readonly ulong ML_INVALID_HANDLE = 0xFFFFFFFFFFFFFFFF; protected delegate void MLAudioBufferCallback(ulong handle, IntPtr context); protected enum Result { NOT_INITIALIZED = -1, FAIL = 0, SUCCESS = 1, }; #if !UNITY_EDITOR && UNITY_ANDROID private const string MLVirtualMicLib = "VirtualMic"; [DllImport(MLVirtualMicLib, CallingConvention = CallingConvention.Cdecl)] protected static extern ulong CreateVirtualMic(MLAudioBufferCallback callback); [DllImport(MLVirtualMicLib, CallingConvention = CallingConvention.Cdecl)] protected static extern Result DestroyVirtualMic(ref ulong mic_handle); [DllImport(MLVirtualMicLib, CallingConvention = CallingConvention.Cdecl)] protected static extern Result StartVirtualMic(ulong mic_handle); [DllImport(MLVirtualMicLib, CallingConvention = CallingConvention.Cdecl)] protected static extern Result StopVirtualMic(ulong mic_handle); [DllImport(MLVirtualMicLib, CallingConvention = CallingConvention.Cdecl)] protected static extern Result VirtualMicGetInputBuffer(ulong mic_handle, ref IntPtr data, ref int size); [DllImport(MLVirtualMicLib, CallingConvention = CallingConvention.Cdecl)] protected static extern Result VirtualMicReleaseInputBuffer(ulong mic_handle); #else protected static ulong CreateVirtualMic(MLAudioBufferCallback callback) => ML_INVALID_HANDLE; protected static Result DestroyVirtualMic(ref ulong mic_handle) => Result.NOT_INITIALIZED; protected static Result StartVirtualMic(ulong mic_handle) => Result.NOT_INITIALIZED; protected static Result StopVirtualMic(ulong mic_handle) => Result.NOT_INITIALIZED; protected static Result VirtualMicGetInputBuffer(ulong mic_handle, ref IntPtr data, ref int size) => Result.NOT_INITIALIZED; protected static Result VirtualMicReleaseInputBuffer(ulong mic_handle) => Result.NOT_INITIALIZED; #endif } #endregion #region C# API public class MLVirtualMic : MLVirtualMicAPI, IDisposable { #region Static private static ConcurrentDictionary micMap = new(); [MonoPInvokeCallback(typeof(MLAudioBufferCallback))] private static void AudioBufferCallback(ulong handle, IntPtr context) { if (micMap.TryGetValue(handle, out MLVirtualMic mic)) { IntPtr data = IntPtr.Zero; int size = 0; if (VirtualMicGetInputBuffer(handle, ref data, ref size) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: AudioBufferCallback() - VirtualMicGetInputBuffer() failed"); return; } mic.PushData(data, size); if (VirtualMicReleaseInputBuffer(handle) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: AudioBufferCallback() - VirtualMicReleaseInputBuffer() failed"); } } } #endregion #region Private variables private ulong handle = ML_INVALID_HANDLE; private short[] rawSamples = null; private float[] samples = new float[2048]; private bool isSilence = true; private int index = 0; private bool _waitingOnPermission = false; private object waitLock = new(); private bool waitingOnPermission { get { lock (waitLock) { return _waitingOnPermission; } } set { lock (waitLock) { _waitingOnPermission = value; } } } private bool _running = false; private object runLock = new(); private bool running { get { lock (runLock) { return _running; } } set { lock (runLock) { _running = value; } } } #endregion #region Public properties public int SampleRate => 48000; public int Channels => 2; public bool Active => running; #endregion #region Public events public event Action OnBufferReceived = null; #endregion #region Constructor / Destructor public MLVirtualMic() { Debug.Log("MLVirtualMic: MLVirutalMic() - Creating virtual mic..."); try { if (!MLPermissions.CheckPermission(MLPermission.RecordAudio).IsOk) { Debug.Log($"MLVirtualMic: Requesting MLPermission.RecordAudio"); var callbacks = new MLPermissions.Callbacks(); callbacks.OnPermissionGranted += Initialize; waitingOnPermission = true; MLPermissions.RequestPermission(MLPermission.RecordAudio, callbacks); } else Initialize(MLPermission.RecordAudio); } catch (Exception ex) { Debug.LogError($"MLVirtualMic: MLVirtualMic() - {ex.Message}"); } } private void Initialize(string permission) { Debug.Log($"MLVirtualMic: Initialize() - Permission {permission} granted"); handle = CreateVirtualMic(AudioBufferCallback); if (handle == ML_INVALID_HANDLE) { Debug.LogError("MLVirtualMic: Initialize() - CreateVirtualMic() failed"); return; } Debug.Log("MLVirtualMic: Initialize() - CreateVirtualMic() succeeded"); if (!micMap.TryAdd(handle, this)) { Debug.LogError("MLVirtualMic: Initialize() - Failed to add handle to mic map"); } if (running) { if (StartVirtualMic(handle) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: Initialize() - StartVirtualMic() failed"); return; } Debug.Log("MLVirtualMic: Initialize() - StartVirtualMic() succeeded"); } waitingOnPermission = false; } ~MLVirtualMic() { Dispose(); } #endregion #region Private methods private void PushData(IntPtr data, int size) { int numSamples = size / sizeof(short); if (rawSamples == null || rawSamples.Length < numSamples) rawSamples = new short[numSamples]; Marshal.Copy(data, rawSamples, 0, numSamples); for (int i = 0; i < numSamples;) { for (; i < numSamples && index < samples.Length; ++i, ++index) { samples[index] = (float)rawSamples[i] / short.MaxValue; isSilence &= rawSamples[i] == 0; } if (index == samples.Length) { ulong utcNow = (ulong)(DateTime.UtcNow.Ticks * 100); OnBufferReceived?.Invoke(utcNow, samples, samples.Length, isSilence); isSilence = true; index = 0; } } } #endregion #region Public methods public void Start() { if (running) { Debug.LogWarning("MLVirtualMic: Start() - Already running"); return; } if (waitingOnPermission) { Debug.LogWarning("MLVirtualMic: Start() - Waiting on permission"); running = true; return; } if (StartVirtualMic(handle) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: Start() - StartVirtualMic() failed"); return; } Debug.Log("MLVirtualMic: Start() - StartVirtualMic() succeeded"); running = true; } public void Stop() { if (!running) { Debug.LogWarning("MLVirtualMic: Stop() - Not running"); return; } if (waitingOnPermission) { Debug.LogWarning("MLVirtualMic: Stop() - Waiting on permission"); running = false; return; } if (StopVirtualMic(handle) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: Stop() - StopVirtualMic() failed"); return; } Debug.Log("MLVirtualMic: Stop() - StopVirtualMic() succeeded"); running = false; } public void Dispose() { if (handle == ML_INVALID_HANDLE) return; if (running && StopVirtualMic(handle) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: Dispose() - StopVirtualMic() failed"); } running = false; Debug.Log("MLVirtualMic: Dispose() - StopVirtualMic() succeeded"); if (DestroyVirtualMic(ref handle) != Result.SUCCESS) { Debug.LogError("MLVirtualMic: Dispose() - DestroyVirtualMic() failed"); return; } Debug.Log("MLVirtualMic: Dispose() - DestroyVirtualMic() succeeded"); micMap.TryRemove(handle, out MLVirtualMic mic); } #endregion } #endregion }