2025-09-24 19:24:19 +08:00

509 lines
18 KiB
C#

//#define FORCE_WEBGL_MIC
// C# 10 feature
//global using Microphone = FrostweepGames.MicrophonePro.Microphone;
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
using System.Runtime.InteropServices;
#endif
using System;
using System.Linq;
using System.Globalization;
using System.Collections.Generic;
using UnityEngine;
using AOT;
#if UNITY_ANDROID
using UnityEngine.Android;
#endif
namespace FrostweepGames.MicrophonePro
{
public sealed class Microphone
{
/// <summary>
/// Uses for debugging native commands. Works only in WebGL
/// </summary>
public static bool Logging = false;
/// <summary>
/// Fire when permission for microphone was changed. In WebGL it does automatically. On Android/IOS fire only when requested permission via RequestPermission() function
/// </summary>
public static event Action<bool> PermissionChangedEvent;
/// <summary>
/// Fire when receiving stream chunk from native recorder. Works only in WebGL
/// </summary>
public static event Action<StreamData> RecordStreamDataEvent;
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
private delegate void NativeCommand(string json);
#region __Internal
[DllImport("__Internal")]
private static extern int init(NativeCommand handler);
[DllImport("__Internal")]
private static extern int initSamplesMemoryData(float[] array, int length, int left);
[DllImport("__Internal")]
private static extern int isRecording();
[DllImport("__Internal")]
private static extern string devicesData();
[DllImport("__Internal")]
private static extern string getDeviceCaps();
[DllImport("__Internal")]
private static extern int getPosition();
[DllImport("__Internal")]
private static extern void start(string deviceId, int frequency, int loop, int lengthSec);
[DllImport("__Internal")]
private static extern void end();
[DllImport("__Internal")]
private static extern void dispose();
[DllImport("__Internal")]
private static extern int isPermissionGranted();
[DllImport("__Internal")]
private static extern void setLeapSync(int enabled);
#endregion
private static float[] _leftChannelBuffer;
private static float[] _rightChannelBuffer;
private static float[] _mainBuffer;
private static List<Device> _devices;
private static int _channels = 2;
#endif
private static AudioClip _microphoneClip;
/// <summary>
/// <para>A list of available microphone devices, identified by name.</para>
/// </summary>
public static string[] devices
{
get
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
_devices = Deserialize<SimpleDevicesData>(devicesData()).devices;
return _devices.Select(item => item.label).ToArray();
#else
return UnityEngine.Microphone.devices;
#endif
}
}
static Microphone()
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
_devices = new List<Device>();
init(HandleNativeCommandExecution);
#endif
}
/// <summary>
/// <para>Start Recording with device.</para>
/// </summary>
/// <param name="deviceName">The name of the device.</param>
/// <param name="loop">Indicates whether the recording should continue recording if lengthSec is reached, and wrap around and record from the beginning of the AudioClip.</param>
/// <param name="lengthSec">Is the length of the AudioClip produced by the recording.</param>
/// <param name="frequency">The sample rate of the AudioClip produced by the recording.</param>
/// <returns>
/// <para>The function returns null if the recording fails to start.</para>
/// </returns>
public static AudioClip Start(string deviceName, bool loop, int lengthSec, int frequency, bool leapSync = false)
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
string microphoneDeviceIDFromName = GetMicrophoneDeviceIDFromName(deviceName);
if (string.IsNullOrEmpty(microphoneDeviceIDFromName))
throw new ArgumentException("Couldn't acquire device ID for device name " + deviceName);
if (lengthSec <= 0)
throw new ArgumentException("Length of recording must be greater than zero seconds (was: " + lengthSec + " seconds)");
if (lengthSec > 600)
throw new ArgumentException("Length of recording must be less than 10 minutes (was: " + lengthSec + " seconds)");
if (frequency <= 0)
throw new ArgumentException("Frequency of recording must be greater than zero (was: " + frequency + " Hz)");
if (frequency > 48000)
throw new ArgumentException("Frequency of recording must be less than 48000 (was: " + frequency + " Hz)");
if (IsRecording(deviceName))
return _microphoneClip;
if (_microphoneClip != null)
MonoBehaviour.Destroy(_microphoneClip);
_microphoneClip = AudioClip.Create("Microphone", frequency * lengthSec, _channels, frequency, false);
_leftChannelBuffer = new float[frequency * lengthSec];
_rightChannelBuffer = new float[frequency * lengthSec];
_mainBuffer = new float[frequency * lengthSec * _channels];
initSamplesMemoryData(_leftChannelBuffer, _leftChannelBuffer.Length, 0);
initSamplesMemoryData(_rightChannelBuffer, _rightChannelBuffer.Length, 1);
setLeapSync(leapSync ? 1 : 0);
start(microphoneDeviceIDFromName, frequency, loop ? 1 : 0, lengthSec);
return _microphoneClip;
#else
if (_microphoneClip != null)
MonoBehaviour.Destroy(_microphoneClip);
_microphoneClip = UnityEngine.Microphone.Start(deviceName, loop, lengthSec, frequency);
return _microphoneClip;
#endif
}
/// <summary>
/// <para>Query if a device is currently recording.</para>
/// </summary>
/// <param name="deviceName">The name of the device.</param>
public static bool IsRecording(string deviceName)
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
return isRecording() == 1;
#else
return UnityEngine.Microphone.IsRecording(deviceName);
#endif
}
public static void GetDeviceCaps(string deviceName, out int minFreq, out int maxfreq)
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
int[] array = Deserialize<SimpleDeviceCapsData>(getDeviceCaps()).caps;
minFreq = array[0];
maxfreq = array[1];
#else
UnityEngine.Microphone.GetDeviceCaps(deviceName, out minFreq, out maxfreq);
#endif
}
/// <summary>
/// <para>Get the position in samples of the recording.</para>
/// </summary>
/// <param name="deviceName">The name of the device.</param>
public static int GetPosition(string deviceName)
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
return getPosition();
#else
return UnityEngine.Microphone.GetPosition(deviceName);
#endif
}
/// <summary>
/// <para>Stops recording.</para>
/// </summary>
/// <param name="deviceName">The name of the device.</param>
public static void End(string deviceName)
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
end();
Interleave(_mainBuffer, 0);
_microphoneClip.SetData(_mainBuffer, 0);
#else
UnityEngine.Microphone.End(deviceName);
#endif
}
/// <summary>
/// <para>Request that the user grant access to a Microphone.</para>
/// </summary>
public static void RequestPermission()
{
#if UNITY_ANDROID
PermissionCallbacks permissionCallbacks = new PermissionCallbacks();
permissionCallbacks.PermissionGranted += (permission) => { PermissionChangedEvent?.Invoke(true); };
permissionCallbacks.PermissionDenied += (permission) => { PermissionChangedEvent?.Invoke(false); };
permissionCallbacks.PermissionDeniedAndDontAskAgain += (permission) => { PermissionChangedEvent?.Invoke(false); };
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
Permission.RequestUserPermission(Permission.Microphone, permissionCallbacks);
else
PermissionChangedEvent?.Invoke(true);
#elif UNITY_IOS
if (!Application.HasUserAuthorization(UserAuthorization.Microphone))
{
var operation = Application.RequestUserAuthorization(UserAuthorization.Microphone);
operation.completed += (operation) => { PermissionChangedEvent?.Invoke(Application.HasUserAuthorization(UserAuthorization.Microphone)); };
}
else
PermissionChangedEvent?.Invoke(true);
#elif (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
if (isPermissionGranted() == 0)
{
// TODO: do nothing
}
#endif
}
/// <summary>
/// <para>Fills an array with sample data from the clip.</para>
/// </summary>
/// <param name="data"></param>
/// <param name="offsetSamples"></param>
public static bool GetData(float[] data, int offsetSamples)
{
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
return Interleave(data, offsetSamples);
#else
if (_microphoneClip == null || !_microphoneClip)
return false;
return _microphoneClip.GetData(data, offsetSamples);
#endif
}
/// <summary>
/// <para>Resamples of samples array from sourceFrequency into targetFrequency.</para>
/// </summary>
/// <param name="samples"></param>
/// <param name="sourceFrequency"></param>
/// <param name="targetFrequency"></param>
/// <returns></returns>
public static float[] ResampleData(float[] data, int sourceFrequency, int targetFrequency)
{
if (targetFrequency == sourceFrequency)
return data;
int sampleRateRatio = sourceFrequency / targetFrequency;
int newLength = data.Length / sampleRateRatio;
float[] result = new float[newLength];
int offsetResult = 0;
int offsetBuffer = 0;
while (offsetResult < result.Length)
{
int nextOffsetBuffer = (offsetResult + 1) * sampleRateRatio;
float accum = 0,
count = 0;
for (int i = offsetBuffer; i < nextOffsetBuffer && i < data.Length; i++)
{
accum += data[i];
count++;
}
result[offsetResult] = accum / count;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return result;
}
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
[MonoPInvokeCallback(typeof(NativeCommand))]
private static void HandleNativeCommandExecution(string json)
{
try
{
Command<object> command = Deserialize<SimpleCommandData<object>>(json).command;
switch ((CommandType)Enum.Parse(typeof(CommandType), command.command))
{
case CommandType.PermissionChanged:
{
var internalCommand = Deserialize<SimpleCommandData<bool>>(json).command;
PermissionChangedEvent?.Invoke(internalCommand.data);
}
break;
case CommandType.StreamChunkReceived:
{
var internalCommand = Deserialize<SimpleCommandData<string>>(json).command;
string[] split = internalCommand.data.Split(':');
int index = int.Parse(split[0]);
int length = int.Parse(split[1]);
List<float[]> channelsData = new List<float[]>();
float[] array;
float[] chunk;
int pointer;
int positionPointer;
for (int i = 0; i < _channels; i++)
{
array = i == 0 ? _leftChannelBuffer : _rightChannelBuffer;
chunk = new float[length];
pointer = 0;
positionPointer = index;
for (int j = index; j < index + length; j++)
{
chunk[pointer] = array[positionPointer];
pointer++;
positionPointer++;
if (positionPointer >= array.Length)
positionPointer = 0;
}
channelsData.Add(chunk);
}
RecordStreamDataEvent?.Invoke(new StreamData(channelsData));
}
break;
}
if (Logging)
{
UnityEngine.Debug.Log($"Executed command: {command.command}");
}
}
catch (Exception ex)
{
if (Logging)
{
Debug.LogException(ex);
}
}
}
private static T Deserialize<T>(string json)
{
if (Logging)
{
UnityEngine.Debug.Log($"Deserialize: {json}");
}
return UnityEngine.JsonUtility.FromJson<T>(json);
}
private static bool Interleave(float[] result, int offset)
{
if (_leftChannelBuffer == null || _leftChannelBuffer.Length == 0)
{
result = null;
return false;
}
int length = _leftChannelBuffer.Length + _rightChannelBuffer.Length;
int index = 0, inputIndex = Mathf.Clamp(offset, 0, _leftChannelBuffer.Length - 1);
while (index < length)
{
if (index >= result.Length || inputIndex >= _leftChannelBuffer.Length)
{
result = null;
return false;// tried to move over array bounds
}
result[index++] = _leftChannelBuffer[inputIndex];
result[index++] = _rightChannelBuffer[inputIndex];
inputIndex++;
}
return true;
}
private static string GetMicrophoneDeviceIDFromName(string deviceName)
{
return _devices?.Find(device => device.label == deviceName)?.deviceId;
}
#endif
public class StreamData
{
/// <summary>
/// Returns channels data, first left then right channel
/// </summary>
public readonly List<float[]> ChannelsData;
#if UNITY_2018_4_OR_NEWER
[UnityEngine.Scripting.Preserve]
#endif
public StreamData()
{
ChannelsData = new List<float[]>();
}
#if UNITY_2018_4_OR_NEWER
[UnityEngine.Scripting.Preserve]
#endif
public StreamData(List<float[]> channelsData)
{
ChannelsData = channelsData;
}
}
#if (UNITY_WEBGL && !UNITY_EDITOR) || FORCE_WEBGL_MIC
[Serializable]
private class Command<T>
{
public string command;
public T data;
#if UNITY_2018_4_OR_NEWER
[UnityEngine.Scripting.Preserve]
#endif
public Command()
{
}
#if UNITY_2018_4_OR_NEWER
[UnityEngine.Scripting.Preserve]
#endif
public Command(string command, T data)
{
this.command = command;
this.data = data;
}
}
[Serializable]
private class Device
{
public string deviceId;
public string kind;
public string label;
public string groupId;
#if UNITY_2018_4_OR_NEWER
[UnityEngine.Scripting.Preserve]
#endif
public Device()
{
}
#if UNITY_2018_4_OR_NEWER
[UnityEngine.Scripting.Preserve]
#endif
public Device(string deviceId, string kind, string label, string groupId)
{
this.deviceId = deviceId;
this.kind = kind;
this.label = label;
this.groupId = groupId;
}
}
[Serializable]
private class SimpleDevicesData
{
public List<Device> devices;
}
[Serializable]
private class SimpleDeviceCapsData
{
public int[] caps;
}
[Serializable]
private class SimpleCommandData<T>
{
public Command<T> command;
}
private enum CommandType
{
PermissionChanged,
StreamChunkReceived
}
#endif
}
}