// ----------------------------------------------------------------------------
//
// Photon Voice for Unity - Copyright (C) 2018 Exit Games GmbH
//
//
// Component representing outgoing audio stream in scene.
//
// developer@photonengine.com
// ----------------------------------------------------------------------------
#if WINDOWS_UWP || ENABLE_WINMD_SUPPORT
#define PHOTON_MICROPHONE_WSA
#endif
#if PHOTON_MICROPHONE_WSA || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
#define PHOTON_MICROPHONE_ENUMERATOR
#endif
#if UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN || UNITY_ANDROID || UNITY_IOS || UNITY_WSA || UNITY_SWITCH
#define PHOTON_MICROPHONE_SUPPORTED_PLATFORM
#endif
#if UNITY_EDITOR_OSX || UNITY_EDITOR_WIN
#define PHOTON_MICROPHONE_SUPPORTED_EDITOR
#endif
using System;
using System.Linq;
using POpusCodec.Enums;
using UnityEngine;
using UnityEngine.Serialization;
namespace Photon.Voice.Unity
{
///
/// Component representing outgoing audio stream in scene.
///
[AddComponentMenu("Photon Voice/Recorder")]
[HelpURL("https://doc.photonengine.com/en-us/voice/v2/getting-started/recorder")]
[DisallowMultipleComponent]
public class Recorder : VoiceComponent
{
public const int MIN_OPUS_BITRATE = 6000;
public const int MAX_OPUS_BITRATE = 510000;
#region Private Fields
private static readonly Array samplingRateValues = Enum.GetValues(typeof(SamplingRate));
[SerializeField]
private bool voiceDetection;
[SerializeField]
private float voiceDetectionThreshold = 0.01f;
[SerializeField]
private int voiceDetectionDelayMs = 500;
private object userData;
private LocalVoice voice = LocalVoiceAudioDummy.Dummy;
#if UNITY_EDITOR
[SerializeField]
#endif
private string unityMicrophoneDevice;
#if UNITY_EDITOR
[SerializeField]
#endif
private int photonMicrophoneDeviceId = -1;
private IAudioDesc inputSource;
private VoiceClient client;
private VoiceConnection voiceConnection;
[SerializeField]
[FormerlySerializedAs("audioGroup")]
private byte interestGroup;
[SerializeField]
private bool debugEchoMode;
[SerializeField]
private bool reliableMode;
[SerializeField]
private bool encrypt;
[SerializeField]
private bool transmitEnabled;
[SerializeField]
private SamplingRate samplingRate = SamplingRate.Sampling24000;
[SerializeField]
private OpusCodec.FrameDuration frameDuration = OpusCodec.FrameDuration.Frame20ms;
[SerializeField, Range(MIN_OPUS_BITRATE, MAX_OPUS_BITRATE)]
private int bitrate = 30000;
[SerializeField]
private InputSourceType sourceType;
[SerializeField]
private MicType microphoneType;
[SerializeField]
private AudioClip audioClip;
[SerializeField]
private bool loopAudioClip = true;
private bool isRecording;
private Func inputFactory;
[Obsolete]
private static IDeviceEnumerator photonMicrophoneEnumerator;
private IAudioInChangeNotifier photonMicChangeNotifier;
[SerializeField]
private bool reactOnSystemChanges;
private bool subscribedToSystemChangesPhoton;
private bool subscribedToSystemChangesUnity;
[SerializeField]
private bool autoStart = true;
#if UNITY_IOS || UNITY_EDITOR
[SerializeField]
private IOS.AudioSessionParameters audioSessionParameters = IOS.AudioSessionParametersPresets.Game;
#pragma warning disable 649
[SerializeField]
private bool useCustomAudioSessionParameters;
[SerializeField]
private int audioSessionPresetIndex;
#pragma warning restore 649
#endif
#if UNITY_ANDROID || UNITY_EDITOR
[SerializeField]
private NativeAndroidMicrophoneSettings nativeAndroidMicrophoneSettings = new NativeAndroidMicrophoneSettings();
#endif
[SerializeField]
private bool recordOnlyWhenEnabled;
[SerializeField]
private bool skipDeviceChangeChecks;
private bool wasRecordingBeforePause;
private bool isPausedOrInBackground;
[SerializeField]
private bool stopRecordingWhenPaused;
[SerializeField]
private bool useOnAudioFilterRead;
[SerializeField]
private bool trySamplingRateMatch;
[SerializeField]
private bool useMicrophoneTypeFallback = true;
[SerializeField]
private bool recordOnlyWhenJoined = true;
private bool recordingStoppedExplicitly;
private IDeviceEnumerator photonMicrophonesEnumerator;
private AudioInEnumerator unityMicrophonesEnumerator;
#if PHOTON_MICROPHONE_WSA
private string photonMicrophoneDeviceIdString;
#endif
private object microphoneDeviceChangeDetectedLock = new object();
internal bool microphoneDeviceChangeDetected;
#endregion
#region Properties
internal bool MicrophoneDeviceChangeDetected
{
get
{
lock (this.microphoneDeviceChangeDetectedLock)
{
return this.microphoneDeviceChangeDetected;
}
}
set
{
lock (this.microphoneDeviceChangeDetectedLock)
{
if (this.microphoneDeviceChangeDetected == value)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Unexpected: MicrophoneDeviceChangeDetected to be overriden with same value: {0}", value);
}
return;
}
this.microphoneDeviceChangeDetected = value;
}
}
}
private bool subscribedToSystemChanges
{
get
{
return this.subscribedToSystemChangesUnity || this.subscribedToSystemChangesPhoton;
}
}
/// Enumerator for the available microphone devices gathered by the Photon plugin.
[Obsolete("Use the generic unified non-static MicrophonesEnumerator")]
public static IDeviceEnumerator PhotonMicrophoneEnumerator
{
get
{
if (photonMicrophoneEnumerator == null)
{
photonMicrophoneEnumerator = CreatePhotonDeviceEnumerator(new VoiceLogger("PhotonMicrophoneEnumerator"));
}
return photonMicrophoneEnumerator;
}
}
/// If true, this Recorder has been initialized and is ready to transmit to remote clients. Otherwise call .
public bool IsInitialized
{
get { return this.client != null; }
}
[Obsolete("Renamed to RequiresRestart")]
public bool RequiresInit
{
get { return this.RequiresRestart; }
}
/// Returns true if something has changed in the Recorder while recording that won't take effect unless recording is restarted using .
/// Think of this as a "isDirty" flag.
public bool RequiresRestart { get; protected set; }
/// If true, audio transmission is enabled.
public bool TransmitEnabled
{
get { return this.transmitEnabled; }
set
{
if (value != this.transmitEnabled)
{
this.transmitEnabled = value;
if (this.voice != LocalVoiceAudioDummy.Dummy)
{
this.voice.TransmitEnabled = value;
}
}
}
}
/// If true, voice stream is sent encrypted.
public bool Encrypt
{
get { return this.encrypt; }
set
{
if (this.encrypt == value)
{
return;
}
this.encrypt = value;
this.voice.Encrypt = value;
}
}
/// If true, outgoing stream routed back to client via server same way as for remote client's streams.
public bool DebugEchoMode
{
get
{
if (this.debugEchoMode && this.InterestGroup != 0)
{
this.voice.DebugEchoMode = false;
this.debugEchoMode = false;
}
return this.debugEchoMode;
}
set
{
if (this.debugEchoMode == value)
{
return;
}
if (this.InterestGroup != 0)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Cannot enable DebugEchoMode when InterestGroup value ({0}) is different than 0.", this.interestGroup);
}
return;
}
this.debugEchoMode = value;
this.voice.DebugEchoMode = value;
}
}
/// If true, stream data sent in reliable mode.
public bool ReliableMode
{
get
{
return this.reliableMode;
}
set
{
if (this.voice != LocalVoiceAudioDummy.Dummy)
{
this.voice.Reliable = value;
}
this.reliableMode = value;
}
}
/// If true, voice detection enabled.
public bool VoiceDetection
{
get
{
this.GetStatusFromDetector();
return this.voiceDetection;
}
set
{
this.voiceDetection = value;
if (this.VoiceDetector != null)
{
this.VoiceDetector.On = value;
}
}
}
/// Voice detection threshold (0..1, where 1 is full amplitude).
public float VoiceDetectionThreshold
{
get
{
this.GetThresholdFromDetector();
return this.voiceDetectionThreshold;
}
set
{
if (this.voiceDetectionThreshold.Equals(value))
{
return;
}
if (value < 0f || value > 1f)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Value out of range: VAD Threshold needs to be between [0..1], requested value: {0}", value);
}
return;
}
this.voiceDetectionThreshold = value;
if (this.VoiceDetector != null)
{
this.VoiceDetector.Threshold = this.voiceDetectionThreshold;
}
}
}
/// Keep detected state during this time after signal level dropped below threshold. Default is 500ms
public int VoiceDetectionDelayMs
{
get
{
this.GetActivityDelayFromDetector();
return this.voiceDetectionDelayMs;
}
set
{
if (this.voiceDetectionDelayMs == value)
{
return;
}
this.voiceDetectionDelayMs = value;
if (this.VoiceDetector != null)
{
this.VoiceDetector.ActivityDelayMs = value;
}
}
}
/// Custom user object to be sent in the voice stream info event.
public object UserData
{
get { return this.userData; }
set
{
if (this.userData != value)
{
this.userData = value;
if (this.IsRecording)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "UserData");
}
}
}
}
}
/// Set the method returning new Voice.IAudioDesc instance to be assigned to a new voice created with Source set to Factory
public Func InputFactory
{
get { return this.inputFactory; }
set
{
if (this.inputFactory != value)
{
this.inputFactory = value;
if (this.IsRecording && this.SourceType == InputSourceType.Factory)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "InputFactory");
}
}
}
}
}
/// Returns voice activity detector for recorder's audio stream.
public AudioUtil.IVoiceDetector VoiceDetector
{
get { return this.voiceAudio != null ? this.voiceAudio.VoiceDetector : null; }
}
/// Set or get Unity microphone device used for streaming.
public string UnityMicrophoneDevice
{
get
{
if (!IsValidUnityMic(this.unityMicrophoneDevice))
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("\"{0}\" is not a valid Unity microphone device, switching to default", this.unityMicrophoneDevice);
}
this.unityMicrophoneDevice = null;
#if !UNITY_WEBGL
if (UnityMicrophone.devices.Length > 0)
{
this.unityMicrophoneDevice = UnityMicrophone.devices[0];
}
#endif
}
return this.unityMicrophoneDevice;
}
set
{
if (!IsValidUnityMic(value))
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("\"{0}\" is not a valid Unity microphone device", value);
}
return;
}
if (!CompareUnityMicNames(this.unityMicrophoneDevice, value))
{
this.unityMicrophoneDevice = value;
#if !UNITY_WEBGL
if (string.IsNullOrEmpty(this.unityMicrophoneDevice) && UnityMicrophone.devices.Length > 0)
{
this.unityMicrophoneDevice = UnityMicrophone.devices[0];
}
#endif
if (this.IsRecording && this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Unity)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "UnityMicrophoneDevice");
}
}
this.CheckAndSetSamplingRate();
}
}
}
/// Set or get photon microphone device used for streaming.
public int PhotonMicrophoneDeviceId
{
get
{
#if !PHOTON_MICROPHONE_ENUMERATOR
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Photon microphone device IDs are not supported on this platform {0}.", CurrentPlatform);
}
this.photonMicrophoneDeviceId = -1;
#else
if (!this.IsValidPhotonMic())
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("\"{0}\" is not a valid Photon microphone device ID, switching to default (-1)", this.photonMicrophoneDeviceId);
}
this.photonMicrophoneDeviceId = -1;
}
#endif
return this.photonMicrophoneDeviceId;
}
set
{
#if !PHOTON_MICROPHONE_ENUMERATOR
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Setting a Photon microphone device ID is not supported on this platform {0}.", CurrentPlatform);
}
#else
if (!this.IsValidPhotonMic(value))
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("\"{0}\" is not a valid Photon microphone device ID", value);
}
return;
}
if (this.photonMicrophoneDeviceId != value)
{
this.photonMicrophoneDeviceId = value;
if (this.IsRecording && this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "PhotonMicrophoneDeviceId");
}
}
}
#endif
}
}
#if PHOTON_MICROPHONE_WSA
/// Set or get photon microphone device used for streaming.
public string PhotonMicrophoneDeviceIdString
{
get
{
#if !PHOTON_MICROPHONE_ENUMERATOR
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Photon microphone device IDs (string) are not supported on this platform {0}.", CurrentPlatform);
}
this.photonMicrophoneDeviceIdString = string.Empty;
#else
if (!this.IsValidPhotonMic())
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("\"{0}\" is not a valid Photon microphone device ID, switching to default (string.Empty)", this.photonMicrophoneDeviceIdString);
}
this.photonMicrophoneDeviceIdString = string.Empty;
}
#endif
return this.photonMicrophoneDeviceIdString;
}
set
{
#if !PHOTON_MICROPHONE_ENUMERATOR
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Setting a Photon microphone device ID (string) is not supported on this platform {0}.", CurrentPlatform);
}
#else
if (!this.IsValidPhotonMic(value))
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("\"{0}\" is not a valid Photon microphone device ID (string)", value);
}
return;
}
if (!string.Equals(this.photonMicrophoneDeviceIdString, value))
{
this.photonMicrophoneDeviceIdString = value;
if (this.IsRecording && this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "PhotonMicrophoneDeviceIdString");
}
}
}
#endif
}
}
#endif
/// Target interest group that will receive transmitted audio.
/// If AudioGroup != 0, recorder's audio data is sent only to clients listening to this group.
[Obsolete("Use InterestGroup instead")]
public byte AudioGroup
{
get { return this.InterestGroup; }
set { this.InterestGroup = value; }
}
/// Target interest group that will receive transmitted audio.
/// If InterestGroup != 0, recorder's audio data is sent only to clients listening to this group.
public byte InterestGroup
{
get
{
if (this.isRecording && this.voice.InterestGroup != this.interestGroup)
{
// interest group probably set via GlobalInterestGroup!
this.interestGroup = this.voice.InterestGroup;
if (this.debugEchoMode && this.interestGroup != 0)
{
this.debugEchoMode = false;
}
}
return this.interestGroup;
}
set
{
if (this.interestGroup == value)
{
return;
}
if (this.debugEchoMode && value != 0)
{
this.debugEchoMode = false;
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("DebugEchoMode disabled because InterestGroup changed to {0}. DebugEchoMode works only with Interest Group 0.", value);
}
}
this.interestGroup = value;
this.voice.InterestGroup = value;
}
}
/// Returns true if audio stream broadcasts.
public bool IsCurrentlyTransmitting
{
get { return this.IsRecording && this.TransmitEnabled && this.voice.IsCurrentlyTransmitting; }
}
/// Level meter utility.
public AudioUtil.ILevelMeter LevelMeter
{
get { return this.voiceAudio != null ? this.voiceAudio.LevelMeter : null; }
}
/// If true, voice detector calibration is in progress.
public bool VoiceDetectorCalibrating { get { return this.voiceAudio != null && this.TransmitEnabled && this.voiceAudio.VoiceDetectorCalibrating; } }
protected ILocalVoiceAudio voiceAudio { get { return this.voice as ILocalVoiceAudio; } }
/// Audio data source.
public InputSourceType SourceType
{
get { return this.sourceType; }
set
{
if (this.sourceType != value)
{
this.sourceType = value;
if (this.IsRecording)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "Source");
}
}
this.CheckAndSetSamplingRate();
}
}
}
/// Which microphone API to use when the Source is set to Microphone.
public MicType MicrophoneType
{
get
{
#if !PHOTON_MICROPHONE_SUPPORTED_PLATFORM
if (this.microphoneType == MicType.Photon)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Photon microphone type is not supported on this platform {0}, switching to Unity microphone type.", CurrentPlatform);
}
this.microphoneType = MicType.Unity;
}
#endif
return this.microphoneType;
}
set
{
if (this.microphoneType != value)
{
#if !PHOTON_MICROPHONE_SUPPORTED_PLATFORM
if (value == MicType.Photon)
{
#if PHOTON_MICROPHONE_SUPPORTED_EDITOR
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Photon microphone type is not supported on this platform {0}. Microphone type will be automatically reverted to Unity in build.", CurrentPlatform);
}
#else
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Photon microphone type is not supported on this platform {0}", CurrentPlatform);
}
return;
#endif
}
#endif
this.microphoneType = value;
if (this.IsRecording && this.SourceType == InputSourceType.Microphone)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "MicrophoneType");
}
}
this.CheckAndSetSamplingRate();
}
}
}
#pragma warning disable 618
/// Force creation of 'short' pipeline and convert audio data to short for 'float' audio sources.
[Obsolete("No longer used. Implicit conversion is done internally when needed.")]
public SampleTypeConv TypeConvert { get; set; }
#pragma warning restore 618
/// Source audio clip.
public AudioClip AudioClip
{
get { return this.audioClip; }
set
{
if (this.audioClip != value)
{
this.audioClip = value;
if (this.IsRecording && this.SourceType == InputSourceType.AudioClip)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "AudioClip");
}
}
this.CheckAndSetSamplingRate();
}
}
}
/// Loop playback for audio clip sources.
public bool LoopAudioClip
{
get { return this.loopAudioClip; }
set
{
if (this.loopAudioClip != value)
{
this.loopAudioClip = value;
if (this.IsRecording && this.SourceType == InputSourceType.AudioClip)
{
AudioClipWrapper wrapper = this.inputSource as AudioClipWrapper;
if (wrapper != null)
{
wrapper.Loop = value;
}
else if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Unexpected: Recorder inputSource is not of AudioClipWrapper type or is null.");
}
}
}
}
}
/// Outgoing audio stream sampling rate.
public SamplingRate SamplingRate
{
get { return this.samplingRate; }
set
{
this.CheckAndSetSamplingRate(value);
}
}
/// Outgoing audio stream encoder delay.
public OpusCodec.FrameDuration FrameDuration
{
get { return this.frameDuration; }
set
{
if (this.frameDuration != value)
{
this.frameDuration = value;
if (this.IsRecording)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "FrameDuration");
}
}
}
}
}
/// Outgoing audio stream bitrate.
public int Bitrate
{
get { return this.bitrate; }
set
{
if (this.bitrate != value)
{
if (value < MIN_OPUS_BITRATE || value > MAX_OPUS_BITRATE)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Unsupported bitrate value {0}, valid range: {1}-{2}", value, MIN_OPUS_BITRATE, MAX_OPUS_BITRATE);
}
}
else
{
this.bitrate = value;
if (this.IsRecording)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "Bitrate");
}
}
}
}
}
}
/// Gets or sets whether this Recorder is actively recording audio to be transmitted.
public bool IsRecording
{
get
{
return this.isRecording;
}
set
{
if (this.isRecording != value)
{
if (this.isRecording)
{
this.StopRecording();
}
else
{
this.StartRecording();
}
}
}
}
/// If true, the Recorder will automatically restart recording to recover from audio device changes.
///
/// By default, the Recorder will restart recording only when the is
/// and the device being used is no longer available or valid, in some cases you may need to force restarts even if the device in use did not change.
/// To enable this set to true.
///
public bool ReactOnSystemChanges
{
get { return this.reactOnSystemChanges; }
set
{
if (this.reactOnSystemChanges != value)
{
this.reactOnSystemChanges = value;
if (this.IsRecording)
{
if (this.reactOnSystemChanges)
{
if (!this.subscribedToSystemChanges)
{
this.SubscribeToSystemChanges();
}
}
else if (this.subscribedToSystemChanges)
{
this.UnsubscribeFromSystemChanges();
}
}
}
}
}
/// If true, automatically start recording when initialized.
public bool AutoStart
{
get { return this.autoStart; }
set
{
if (this.autoStart != value)
{
this.autoStart = value;
this.CheckAndAutoStart();
}
}
}
/// If true, component will work only when enabled and active in hierarchy.
public bool RecordOnlyWhenEnabled
{
get { return this.recordOnlyWhenEnabled; }
set
{
if (this.recordOnlyWhenEnabled != value)
{
this.recordOnlyWhenEnabled = value;
if (this.recordOnlyWhenEnabled)
{
if (!this.isActiveAndEnabled && this.IsRecording)
{
this.StopRecordingInternal();
}
}
else
{
this.CheckAndAutoStart();
}
}
}
}
/// If true, restarts recording without checking if audio config/device changes affected recording.
/// To be used when is true.
public bool SkipDeviceChangeChecks
{
get { return this.skipDeviceChangeChecks; }
set { this.skipDeviceChangeChecks = value; }
}
/// If true, stop recording when paused resume/restart when un-paused.
public bool StopRecordingWhenPaused
{
get { return this.stopRecordingWhenPaused; }
set { this.stopRecordingWhenPaused = value; }
}
/// If true, recording will make use of Unity's OnAudioFitlerRead callback from a muted local AudioSource.
/// If enabled, 3D sounds and voice positioning can be lost.
public bool UseOnAudioFilterRead
{
get
{
return this.useOnAudioFilterRead;
}
set
{
if (this.useOnAudioFilterRead != value)
{
this.useOnAudioFilterRead = value;
if (this.IsRecording && this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Unity)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "UseOnAudioFilterRead");
}
}
}
}
}
/// If true, Recorder will try to match sampling rates of microphone device and Opus encoder to avoid re sampling of audio input.
public bool TrySamplingRateMatch
{
get
{
return this.trySamplingRateMatch;
}
set
{
if (this.trySamplingRateMatch != value)
{
this.trySamplingRateMatch = value;
if (this.trySamplingRateMatch)
{
this.CheckAndSetSamplingRate();
}
}
}
}
/// If true, if recording fails to start with Unity microphone type, Photon microphone type is used -if available- as a fallback and vice versa.
public bool UseMicrophoneTypeFallback
{
get
{
return this.useMicrophoneTypeFallback;
}
set
{
this.useMicrophoneTypeFallback = value;
}
}
/// If true, recording can start only when client is joined to a room. Auto start is also delayed until client is joined to a room.
public bool RecordOnlyWhenJoined
{
get
{
return this.recordOnlyWhenJoined;
}
set
{
if (this.recordOnlyWhenJoined != value)
{
this.recordOnlyWhenJoined = value;
if (this.recordOnlyWhenJoined)
{
if (this.IsRecording && this.voiceConnection.Client != null && !this.voiceConnection.Client.InRoom)
{
this.StopRecordingInternal();
}
}
else
{
this.CheckAndAutoStart();
}
}
}
}
public IDeviceEnumerator MicrophonesEnumerator
{
get
{
return this.GetMicrophonesEnumerator(this.MicrophoneType);
}
}
public DeviceInfo MicrophoneDevice
{
get
{
switch (this.MicrophoneType)
{
case MicType.Unity:
{
string deviceId = this.UnityMicrophoneDevice;
if (string.IsNullOrEmpty(deviceId))
{
return this.MicrophonesEnumerator.First();
}
return this.GetDeviceById(deviceId);
}
case MicType.Photon:
{
#if !PHOTON_MICROPHONE_ENUMERATOR
return DeviceInfo.Default;
#else
#if PHOTON_MICROPHONE_WSA
string id = this.PhotonMicrophoneDeviceIdString;
if (!string.IsNullOrEmpty(id))
{
return this.GetDeviceById(id);
}
#else
int id = this.PhotonMicrophoneDeviceId;
if (id != -1)
{
return this.GetDeviceById(id);
}
#endif
break;
#endif
}
}
return DeviceInfo.Default;
}
set
{
switch (this.MicrophoneType)
{
case MicType.Unity:
{
this.UnityMicrophoneDevice = value.IDString;
break;
}
case MicType.Photon:
{
#if !PHOTON_MICROPHONE_ENUMERATOR
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Setting a Photon microphone device is not supported on this platform {0}.", CurrentPlatform);
}
#elif PHOTON_MICROPHONE_WSA
this.PhotonMicrophoneDeviceIdString = value.IDString;
#else
this.PhotonMicrophoneDeviceId = value.IDInt;
#endif
break;
}
}
}
}
#endregion
#region Public Methods
///
/// Initializes the Recorder component to be able to transmit audio.
///
/// The VoiceConnection to be used with this Recorder.
public void Init(VoiceConnection connection)
{
if (connection == null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("voiceConnection is null.");
}
return;
}
if (!this.IgnoreGlobalLogLevel)
{
this.LogLevel = connection.GlobalRecordersLogLevel;
}
if (this.IsInitialized)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recorder already initialized.");
}
return;
}
if (connection.VoiceClient == null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("voiceConnection.VoiceClient is null.");
}
return;
}
this.voiceConnection = connection;
this.client = connection.VoiceClient;
this.voiceConnection.AddInitializedRecorder(this);
this.CheckAndAutoStart();
}
[Obsolete("Renamed to RestartRecording")]
public void ReInit()
{
this.RestartRecording();
}
///
/// Restarts recording if something has changed that requires this.
///
/// Set to true if you want to restart even if this is not required (RequiresRestart = false)
public void RestartRecording(bool force = false)
{
if (!force && !this.RequiresRestart)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recorder does not require restart.");
}
return;
}
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Restarting recording, RequiresRestart?={0} forcedRestart?={1}", this.RequiresRestart, force);
}
this.StopRecording();
this.StartRecording();
}
/// Trigger voice detector calibration process.
/// While calibrating, keep silence. Voice detector sets threshold basing on measured background noise level.
///
/// Duration of calibration in milliseconds.
/// Callback when VAD calibration ends.
public void VoiceDetectorCalibrate(int durationMs, Action detectionEndedCallback = null)
{
if (this.voiceAudio != null)
{
if (!this.TransmitEnabled)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Cannot start voice detection calibration when transmission is not enabled");
}
return;
}
this.voiceAudio.VoiceDetectorCalibrate(durationMs, newThreshold =>
{
this.GetThresholdFromDetector();
if (detectionEndedCallback != null)
{
detectionEndedCallback(this.voiceDetectionThreshold);
}
});
}
}
/// Starts recording.
public void StartRecording()
{
if (this.IsRecording)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recorder is already started.");
}
return;
}
if (!this.IsInitialized)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recording can't be started if Recorder is not initialized. Call Recorder.Init(VoiceConnection) first.");
}
return;
}
if (this.RecordOnlyWhenEnabled && !this.isActiveAndEnabled)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recording can't be started because RecordOnlyWhenEnabled is true and Recorder is not enabled or its GameObject is not active in hierarchy.");
}
return;
}
if (this.RecordOnlyWhenJoined && this.voiceConnection.Client != null && !this.voiceConnection.Client.InRoom)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recording can't be started because RecordOnlyWhenJoined is true and voice networking client is not joined to a room.");
}
return;
}
this.StartRecordingInternal();
}
/// Stops recording.
public void StopRecording()
{
this.wasRecordingBeforePause = false; // in case StopRecording is called after this.OnApplicationPause(true) or this.OnApplicationFocus(false)
if (!this.IsRecording)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Recorder is not started.");
}
return;
}
this.StopRecordingInternal();
this.recordingStoppedExplicitly = true;
}
#if UNITY_EDITOR || UNITY_IOS
///
/// Sets the AudioSessionParameters for iOS audio initialization when Photon MicrophoneType is used.
///
/// You can use custom value or one from presets,
/// If a change has been made.
public bool SetIosAudioSessionParameters(IOS.AudioSessionParameters asp)
{
return this.SetIosAudioSessionParameters(asp.Category, asp.Mode, asp.CategoryOptions);
}
///
/// Sets the AudioSessionParameters for iOS audio initialization when Photon MicrophoneType is used.
///
/// Audio session category to be used.
/// Audio session mode to be used.
/// Audio session category options to be used
/// If a change has been made.
public bool SetIosAudioSessionParameters(IOS.AudioSessionCategory category, IOS.AudioSessionMode mode, IOS.AudioSessionCategoryOption[] options)
{
int opt = 0;
if (options != null)
{
for (int i = 0; i < options.Length; i++)
{
opt |= (int)options[i];
}
}
if (this.audioSessionParameters.Category != category ||
this.audioSessionParameters.Mode != mode ||
this.audioSessionParameters.CategoryOptionsToInt() != opt)
{
this.audioSessionParameters.Category = category;
this.audioSessionParameters.Mode = mode;
this.audioSessionParameters.CategoryOptions = options;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Changing iOS audioSessionParameters = {0}", this.audioSessionParameters);
}
#if !UNITY_EDITOR
if (this.IsRecording && this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "iOSAudioSessionParameters");
}
}
#endif
return true;
}
return false;
}
#endif
#if UNITY_EDITOR || UNITY_ANDROID
///
/// Sets the native Android audio input settings when the Photon microphone type is used.
///
/// The settings to be applied
/// If a change has been made.
public bool SetAndroidNativeMicrophoneSettings(NativeAndroidMicrophoneSettings nams)
{
return this.SetAndroidNativeMicrophoneSettings(nams.AcousticEchoCancellation, nams.AutomaticGainControl,
nams.NoiseSuppression);
}
///
/// Sets the native Android audio input settings when the Photon microphone type is used.
///
/// Acoustic Echo Cancellation
/// Automatic Gain Control
/// Noise Suppression
/// If a change has been made.
public bool SetAndroidNativeMicrophoneSettings(bool aec = false, bool agc = false, bool ns = false)
{
if (this.nativeAndroidMicrophoneSettings.AcousticEchoCancellation != aec ||
this.nativeAndroidMicrophoneSettings.AutomaticGainControl != agc ||
this.nativeAndroidMicrophoneSettings.NoiseSuppression != ns)
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Changing Android native microphone settings to aec = {0}, agc = {1}, ns = {2}", aec, agc, ns);
}
#if !UNITY_EDITOR
if (this.IsRecording && this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "nativeAndroidMicrophoneSettings");
}
}
#endif
return true;
}
return false;
}
#endif
/// Resets audio session and parameters locally to fix broken recording due to system configuration modifications or audio interruptions or audio routing changes.
/// If reset is done.
public bool ResetLocalAudio()
{
if (this.inputSource != null && this.inputSource is IResettable)
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Resetting local audio.");
}
(this.inputSource as IResettable).Reset();
return true;
}
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("InputSource is null or not resettable.");
}
return false;
}
public static bool CompareUnityMicNames(string mic1, string mic2)
{
if (IsDefaultUnityMic(mic1) && IsDefaultUnityMic(mic2))
{
return true;
}
if (mic1 != null && mic1.Equals(mic2))
{
return true;
}
return false;
}
public static bool IsDefaultUnityMic(string mic)
{
#if UNITY_WEBGL
return false;
#else
return string.IsNullOrEmpty(mic) || Array.IndexOf(UnityMicrophone.devices, mic) == 0;
#endif
}
#endregion
#region Private Methods
private void Setup()
{
this.voice = this.CreateLocalVoiceAudioAndSource();
if (this.voice == LocalVoiceAudioDummy.Dummy)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Local input source setup and voice stream creation failed. No recording or transmission will be happening. See previous error log messages for more details.");
}
if (this.inputSource != null)
{
this.inputSource.Dispose();
this.inputSource = null;
}
if (this.MicrophoneDeviceChangeDetected)
{
this.MicrophoneDeviceChangeDetected = false;
}
return;
}
this.SubscribeToSystemChanges();
if (this.VoiceDetector != null)
{
this.VoiceDetector.Threshold = this.voiceDetectionThreshold;
this.VoiceDetector.ActivityDelayMs = this.voiceDetectionDelayMs;
this.VoiceDetector.On = this.voiceDetection;
}
this.voice.InterestGroup = this.InterestGroup;
this.voice.DebugEchoMode = this.DebugEchoMode;
this.voice.Encrypt = this.Encrypt;
this.voice.Reliable = this.ReliableMode;
this.RequiresRestart = false;
this.isRecording = true;
this.SendPhotonVoiceCreatedMessage();
this.voice.TransmitEnabled = this.TransmitEnabled;
}
private LocalVoice CreateLocalVoiceAudioAndSource()
{
SamplingRate effectiveSamplingRate = this.samplingRate;
int samplingRateInt = (int)effectiveSamplingRate;
switch (this.SourceType)
{
case InputSourceType.Microphone:
{
#if UNITY_WEBGL
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Photon Voice 2 does not support WebGL but we made sure code compiles for WebGL at least.");
}
return LocalVoiceAudioDummy.Dummy;
#else
if (!this.CheckIfThereIsAtLeastOneMic())
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("No microphone detected.");
}
return LocalVoiceAudioDummy.Dummy;
}
#endif
bool fallbackMicrophone = false;
switch (this.MicrophoneType)
{
case MicType.Unity:
{
string micDev = this.UnityMicrophoneDevice;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to Unity microphone device {0}", micDev);
}
// mic can ignore passed sampling rate and set its own
if (this.UseOnAudioFilterRead)
{
this.inputSource = new MicWrapperPusher(micDev, this.transform, samplingRateInt, this.Logger);
}
else
{
this.inputSource = new MicWrapper(micDev, samplingRateInt, this.Logger);
}
if (this.inputSource != null)
{
if (this.inputSource.Error != null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Unity microphone input source creation failure: {0}", this.inputSource.Error);
}
}
else
{
break;
}
}
#if PHOTON_MICROPHONE_SUPPORTED_PLATFORM || PHOTON_MICROPHONE_SUPPORTED_EDITOR
if (this.UseMicrophoneTypeFallback && !fallbackMicrophone)
{
fallbackMicrophone = true;
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Unity microphone failed. Falling back to Photon microphone");
}
goto case MicType.Photon;
}
#endif
}
break;
case MicType.Photon:
{
#if PHOTON_MICROPHONE_ENUMERATOR
DeviceInfo hwMicDev = this.MicrophoneDevice;
int hwMicDevId = hwMicDev.IsDefault ? -1 : hwMicDev.IDInt;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to Photon microphone device={0}", hwMicDev);
}
#else
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to Photon microphone device");
}
#endif
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR || UNITY_EDITOR_WIN
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to WindowsAudioInPusher");
}
this.inputSource = new Windows.WindowsAudioInPusher(hwMicDevId, this.Logger);
#elif PHOTON_MICROPHONE_WSA
int channels = 1;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to UWP.AudioInPusher(channels={0})", channels);
}
this.inputSource = new Voice.UWP.AudioInPusher(this.Logger, samplingRateInt, channels, hwMicDev.IDString);
#elif UNITY_IOS && !UNITY_EDITOR
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to IOS.AudioInPusher with session {0}", audioSessionParameters);
}
this.inputSource = new IOS.AudioInPusher(audioSessionParameters, this.Logger);
#elif UNITY_STANDALONE_OSX && !UNITY_EDITOR || UNITY_EDITOR_OSX
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to MacOS.AudioInPusher");
}
this.inputSource = new MacOS.AudioInPusher(hwMicDevId, this.Logger);
#elif UNITY_SWITCH && !UNITY_EDITOR
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to Switch.AudioInPusher");
}
this.inputSource = new Switch.AudioInPusher(this.Logger);
#elif UNITY_ANDROID && !UNITY_EDITOR
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Setting recorder's source to UnityAndroidAudioInAEC");
}
this.inputSource = new AndroidAudioInAEC(this.Logger, this.nativeAndroidMicrophoneSettings.AcousticEchoCancellation, this.nativeAndroidMicrophoneSettings.AutomaticGainControl, this.nativeAndroidMicrophoneSettings.NoiseSuppression);
#else
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Photon microphone type is not supported for the current platform {0}.", CurrentPlatform);
}
#endif
if (this.inputSource != null)
{
if (this.inputSource.Error != null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Photon microphone input source creation failure: {0}", this.inputSource.Error);
}
}
else
{
break;
}
}
if (this.UseMicrophoneTypeFallback && !fallbackMicrophone)
{
fallbackMicrophone = true;
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Photon microphone failed. Falling back to Unity microphone");
}
goto case MicType.Unity;
}
break;
}
default:
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("unknown MicrophoneType value {0}", this.MicrophoneType);
}
return LocalVoiceAudioDummy.Dummy;
}
}
break;
case InputSourceType.AudioClip:
{
if (this.AudioClip == null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("AudioClip property must be set for AudioClip audio source");
}
return LocalVoiceAudioDummy.Dummy;
}
AudioClipWrapper audioClipWrapper = new AudioClipWrapper(this.AudioClip); // never fails, no need to check Error
audioClipWrapper.Loop = this.LoopAudioClip;
this.inputSource = audioClipWrapper;
}
break;
case InputSourceType.Factory:
{
if (this.InputFactory == null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Recorder.InputFactory must be specified if Recorder.Source set to Factory");
}
return LocalVoiceAudioDummy.Dummy;
}
this.inputSource = this.InputFactory();
if (this.inputSource.Error != null && this.Logger.IsErrorEnabled)
{
this.Logger.LogError("InputFactory creation failure: {0}.", this.inputSource.Error);
}
}
break;
default:
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("unknown Source value {0}", this.SourceType);
}
return LocalVoiceAudioDummy.Dummy;
}
if (this.inputSource == null || this.inputSource.Error != null)
{
return LocalVoiceAudioDummy.Dummy;
}
if (this.inputSource.Channels == 0)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("inputSource.Channels is zero");
}
return LocalVoiceAudioDummy.Dummy;
}
if (this.TrySamplingRateMatch && this.inputSource.SamplingRate != samplingRateInt)
{
effectiveSamplingRate = this.GetSupportedSamplingRate(this.inputSource.SamplingRate);
if (effectiveSamplingRate != this.samplingRate && this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Sampling rate requested ({0}Hz) is not used, input source is expecting {1}Hz instead so switching to the closest supported value: {1}Hz.", samplingRateInt, this.inputSource.SamplingRate, (int)effectiveSamplingRate);
}
}
AudioSampleType audioSampleType = AudioSampleType.Source;
WebRtcAudioDsp dsp = this.GetComponent();
if (dsp != null && dsp.enabled)
{
audioSampleType = AudioSampleType.Short;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Type Conversion set to Short. Audio samples will be converted if source samples types differ.");
}
samplingRateInt = (int) effectiveSamplingRate;
if (Array.IndexOf(WebRTCAudioProcessor.SupportedSamplingRates, samplingRateInt) < 0)
{
switch (effectiveSamplingRate)
{
case SamplingRate.Sampling12000:
case SamplingRate.Sampling24000:
effectiveSamplingRate = SamplingRate.Sampling48000;
break;
}
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Sampling rate requested ({0}Hz) is not supported by WebRTC Audio DSP, switching to the closest supported value: {1}Hz.", samplingRateInt, (int)effectiveSamplingRate);
}
this.SamplingRate = SamplingRate.Sampling48000;
}
switch (this.FrameDuration)
{
case OpusCodec.FrameDuration.Frame2dot5ms:
case OpusCodec.FrameDuration.Frame5ms:
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Frame duration requested ({0}ms) is not supported by WebRTC Audio DSP (it needs to be N x 10ms), switching to the closest supported value: {1}Hz.", (int)this.FrameDuration / 1000, 10);
}
this.FrameDuration = OpusCodec.FrameDuration.Frame10ms;
break;
}
}
VoiceInfo voiceInfo = VoiceInfo.CreateAudioOpus(effectiveSamplingRate, this.inputSource.Channels, this.FrameDuration, this.Bitrate, this.UserData);
return this.client.CreateLocalVoiceAudioFromSource(voiceInfo, this.inputSource, audioSampleType);
}
protected virtual void SendPhotonVoiceCreatedMessage()
{
this.gameObject.SendMessage("PhotonVoiceCreated", new Unity.PhotonVoiceCreatedParams { Voice = this.voice, AudioDesc = this.inputSource }, SendMessageOptions.DontRequireReceiver);
}
private void OnDestroy()
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Recorder is about to be destroyed, removing local voice.");
}
this.RemoveVoice();
if (this.IsInitialized)
{
this.voiceConnection.RemoveInitializedRecorder(this);
}
}
private void RemoveVoice()
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("RemovingVoice()");
}
if (this.subscribedToSystemChanges)
{
this.UnsubscribeFromSystemChanges();
}
this.GetThresholdFromDetector();
this.GetStatusFromDetector();
this.GetActivityDelayFromDetector();
if (this.voice != LocalVoiceAudioDummy.Dummy)
{
this.interestGroup = this.voice.InterestGroup;
if (this.debugEchoMode && this.interestGroup != 0)
{
this.debugEchoMode = false;
}
this.voice.RemoveSelf();
this.voice = LocalVoiceAudioDummy.Dummy;
}
if (this.inputSource != null)
{
this.inputSource.Dispose();
this.inputSource = null;
}
this.gameObject.SendMessage("PhotonVoiceRemoved", SendMessageOptions.DontRequireReceiver);
this.isRecording = false;
this.RequiresRestart = false;
}
private void OnAudioConfigChanged(bool deviceWasChanged)
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("OnAudioConfigChanged deviceWasChanged={0}", deviceWasChanged);
}
if (this.SkipDeviceChangeChecks || deviceWasChanged)
{
this.MicrophoneDeviceChangeDetected = true;
}
}
private void PhotonMicrophoneChangeDetected()
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Microphones change detected by Photon native plugin");
}
this.MicrophoneDeviceChangeDetected = true;
}
internal void HandleDeviceChange()
{
if (!this.MicrophoneDeviceChangeDetected && this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Unexpected: HandleDeviceChange called while MicrophoneDeviceChangedDetected is false.");
}
#if PHOTON_MICROPHONE_ENUMERATOR
#pragma warning disable 612
if (photonMicrophoneEnumerator != null)
{
photonMicrophoneEnumerator.Refresh();
}
#pragma warning restore 612
if (this.photonMicrophonesEnumerator != null)
{
this.photonMicrophonesEnumerator.Refresh();
}
#endif
if (this.unityMicrophonesEnumerator != null)
{
this.unityMicrophonesEnumerator.Refresh();
}
if (this.IsRecording)
{
bool restart = false;
if (this.SkipDeviceChangeChecks)
{
restart = true;
}
else if (this.SourceType == InputSourceType.Microphone)
{
if (this.MicrophoneType == MicType.Photon)
{
#if !PHOTON_MICROPHONE_ENUMERATOR
restart = true;
#elif PHOTON_MICROPHONE_WSA
restart = string.IsNullOrEmpty(this.photonMicrophoneDeviceIdString) || !this.IsValidPhotonMic();
#else
restart = this.photonMicrophoneDeviceId == -1 || !this.IsValidPhotonMic();
#endif
}
else
{
restart = string.IsNullOrEmpty(this.unityMicrophoneDevice) || !IsValidUnityMic(this.unityMicrophoneDevice);
}
}
if (restart)
{
if (this.ResetLocalAudio())
{
this.MicrophoneDeviceChangeDetected = false;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Local audio reset as a result of audio config/device change.");
}
}
else
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Restarting Recording as a result of audio config/device change.");
}
this.RestartRecording();
}
}
}
else
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("A microphone device may have been made available: will check auto start conditions and if all good will attempt to start recording.");
}
this.CheckAndAutoStart(true);
}
}
private void SubscribeToSystemChanges()
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Subscribing to system (audio) changes.");
}
if (!this.ReactOnSystemChanges)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("ReactOnSystemChanges is false, not subscribed to system (audio) changes.");
}
return;
}
if (this.subscribedToSystemChanges)
{
if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Already subscribed to system (audio) changes.");
}
return;
}
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS)
if (this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon)
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("ReactOnSystemChanges ignored when using Photon microphone type as this is handled internally for iOS and Android via native plugins.");
}
return;
}
#endif
this.photonMicChangeNotifier = Platform.CreateAudioInChangeNotifier(this.PhotonMicrophoneChangeDetected, this.Logger);
if (this.photonMicChangeNotifier.IsSupported)
{
if (this.photonMicChangeNotifier.Error == null)
{
this.subscribedToSystemChangesPhoton = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Subscribed to audio in change notifications via Photon plugin.");
}
return;
}
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Error creating instance of photonMicChangeNotifier: {0}", this.photonMicChangeNotifier.Error);
}
}
this.photonMicChangeNotifier.Dispose();
this.photonMicChangeNotifier = null;
AudioSettings.OnAudioConfigurationChanged += this.OnAudioConfigChanged;
this.subscribedToSystemChangesUnity = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Subscribed to audio configuration changes via Unity callback.");
}
}
private void UnsubscribeFromSystemChanges()
{
if (this.subscribedToSystemChangesUnity)
{
AudioSettings.OnAudioConfigurationChanged -= this.OnAudioConfigChanged;
this.subscribedToSystemChangesUnity = false;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Unsubscribed from audio configuration changes via Unity callback.");
}
}
if (this.subscribedToSystemChangesPhoton)
{
if (this.photonMicChangeNotifier == null)
{
if (this.Logger.IsErrorEnabled)
{
this.Logger.LogError("Unexpected: photonMicChangeNotifier is null while subscribedToSystemChangesPhoton is true.");
}
}
else
{
this.photonMicChangeNotifier.Dispose();
this.photonMicChangeNotifier = null;
}
this.subscribedToSystemChangesPhoton = false;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Unsubscribed from audio in change notifications via Photon plugin.");
}
}
}
private void GetThresholdFromDetector()
{
if (this.IsRecording && this.VoiceDetector != null && !this.voiceDetectionThreshold.Equals(this.VoiceDetector.Threshold))
{
if (this.VoiceDetector.Threshold <= 1f && this.VoiceDetector.Threshold >= 0f)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("VoiceDetectionThreshold automatically changed from {0} to {1}", this.voiceDetectionThreshold, this.VoiceDetector.Threshold);
}
this.voiceDetectionThreshold = this.VoiceDetector.Threshold;
}
else if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("VoiceDetector.Threshold has unexpected value {0}", this.VoiceDetector.Threshold);
}
}
}
private void GetActivityDelayFromDetector()
{
if (this.IsRecording && this.VoiceDetector != null && this.voiceDetectionDelayMs != this.VoiceDetector.ActivityDelayMs)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("VoiceDetectionDelayMs automatically changed from {0} to {1}", this.voiceDetectionDelayMs, this.VoiceDetector.ActivityDelayMs);
}
this.voiceDetectionDelayMs = this.VoiceDetector.ActivityDelayMs;
}
}
private void GetStatusFromDetector()
{
if (this.IsRecording && this.VoiceDetector != null && this.voiceDetection != this.VoiceDetector.On)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("VoiceDetection automatically changed from {0} to {1}", this.voiceDetection, this.VoiceDetector.On);
}
this.voiceDetection = this.VoiceDetector.On;
}
}
private static bool IsValidUnityMic(string mic)
{
#if UNITY_WEBGL
return false;
#else
return string.IsNullOrEmpty(mic) || UnityMicrophone.devices.Contains(mic);
#endif
}
private void OnEnable()
{
this.wasRecordingBeforePause = false;
this.isPausedOrInBackground = false;
this.CheckAndAutoStart();
}
private void OnDisable()
{
if (this.RecordOnlyWhenEnabled && this.IsRecording)
{
this.StopRecordingInternal();
}
}
private bool IsValidPhotonMic()
{
#if !PHOTON_MICROPHONE_WSA
return this.IsValidPhotonMic(this.photonMicrophoneDeviceId);
#else
return this.IsValidPhotonMic(this.photonMicrophoneDeviceIdString);
#endif
}
private bool CheckIfMicrophoneIdIsValid(IDeviceEnumerator audioInEnumerator, int id)
{
if (id == -1) // default
{
return true;
}
if (audioInEnumerator.IsSupported && audioInEnumerator.Error == null)
{
foreach (DeviceInfo deviceInfo in audioInEnumerator)
{
if (deviceInfo.IDInt == id)
{
return true;
}
}
}
return false;
}
private bool IsValidPhotonMic(int id)
{
return this.CheckIfMicrophoneIdIsValid(this.GetMicrophonesEnumerator(MicType.Photon), id);
}
#if PHOTON_MICROPHONE_WSA
private bool CheckIfMicrophoneIdIsValid(IDeviceEnumerator audioInEnumerator, string id)
{
if (string.IsNullOrEmpty(id)) // default
{
return true;
}
if (audioInEnumerator.IsSupported && audioInEnumerator.Error == null)
{
foreach (DeviceInfo deviceInfo in audioInEnumerator)
{
if (string.Equals(deviceInfo.IDString, id))
{
return true;
}
}
}
return false;
}
private bool IsValidPhotonMic(string id)
{
return this.CheckIfMicrophoneIdIsValid(this.GetMicrophonesEnumerator(MicType.Photon), id);
}
#endif
private void OnApplicationPause(bool paused)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("OnApplicationPause({0})", paused);
}
this.HandleApplicationPause(paused);
}
private void OnApplicationFocus(bool focused)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("OnApplicationFocus({0})", focused);
}
this.HandleApplicationPause(!focused);
}
private void HandleApplicationPause(bool paused)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("App paused?= {0}, isPausedOrInBackground = {1}, wasRecordingBeforePause = {2}, StopRecordingWhenPaused = {3}, IsRecording = {4}", paused, this.isPausedOrInBackground, this.wasRecordingBeforePause, this.StopRecordingWhenPaused, this.IsRecording);
}
if (this.isPausedOrInBackground == paused) // OnApplicationFocus and OnApplicationPause both called
{
return;
}
if (paused)
{
this.wasRecordingBeforePause = this.IsRecording;
this.isPausedOrInBackground = true;
if (this.StopRecordingWhenPaused && this.IsRecording)
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Stopping recording as application went to background or paused");
}
this.RemoveVoice();
}
}
else
{
if (!this.StopRecordingWhenPaused)
{
if (this.ResetLocalAudio() && this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Local audio reset as application is back from background or unpaused");
}
}
else if (this.wasRecordingBeforePause)
{
if (!this.IsRecording)
{
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Starting recording as application is back from background or unpaused");
}
this.Setup();
}
else if (this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Unexpected: Application back from background or unpaused, isPausedOrInBackground = true, wasRecordingBeforePause = true, StopRecordingWhenPaused = true, IsRecording = true");
}
}
this.wasRecordingBeforePause = false;
this.isPausedOrInBackground = false;
}
}
private SamplingRate GetSupportedSamplingRate(int requested)
{
if (Enum.IsDefined(typeof(SamplingRate), requested))
{
return (SamplingRate)requested;
}
int diff = int.MaxValue;
SamplingRate res = SamplingRate.Sampling48000;
foreach (SamplingRate sr in samplingRateValues)
{
int sri = (int) sr;
int d = Math.Abs(sri - requested);
if (d < diff)
{
diff = d;
res = sr;
}
}
return res;
}
private SamplingRate GetSupportedSamplingRateForUnityMicrophone(SamplingRate requested)
{
int minFreq, maxFreq;
UnityMicrophone.GetDeviceCaps(this.UnityMicrophoneDevice, out minFreq, out maxFreq);
return this.GetSupportedSamplingRate(requested, minFreq, maxFreq);
}
private SamplingRate GetSupportedSamplingRate(SamplingRate requested, int minFreq, int maxFreq)
{
SamplingRate res = requested;
int requestedSamplingRateInt = (int) this.samplingRate;
if (requestedSamplingRateInt < minFreq || maxFreq != 0 && requestedSamplingRateInt > maxFreq)
{
if (Enum.IsDefined(typeof(SamplingRate), maxFreq))
{
res = (SamplingRate)maxFreq;
}
else
{
requestedSamplingRateInt = maxFreq;
int diff = int.MaxValue;
foreach (SamplingRate sr in samplingRateValues)
{
int sri = (int) sr;
if (sri < minFreq || maxFreq != 0 && sri > maxFreq)
{
continue;
}
int d = Math.Abs(sri - requestedSamplingRateInt);
if (d < diff)
{
diff = d;
res = sr;
}
}
}
}
return res;
}
private SamplingRate GetSupportedSamplingRate(SamplingRate sR)
{
switch (this.SourceType)
{
case InputSourceType.Microphone:
switch (this.MicrophoneType)
{
case MicType.Unity:
return this.GetSupportedSamplingRateForUnityMicrophone(sR);
case MicType.Photon:
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR || UNITY_EDITOR_WIN
return SamplingRate.Sampling16000;
#elif UNITY_IOS && !UNITY_EDITOR
return SamplingRate.Sampling48000;
#elif UNITY_STANDALONE_OSX && !UNITY_EDITOR || UNITY_EDITOR_OSX
return SamplingRate.Sampling48000;
#elif UNITY_ANDROID && !UNITY_EDITOR
return SamplingRate.Sampling48000;
#else
return sR;
#endif
default:
throw new ArgumentOutOfRangeException();
}
case InputSourceType.AudioClip:
if (this.AudioClip != null)
{
return this.GetSupportedSamplingRate(this.AudioClip.frequency);
}
break;
case InputSourceType.Factory:
break;
default:
throw new ArgumentOutOfRangeException();
}
return sR;
}
private void CheckAndSetSamplingRate(SamplingRate sR)
{
if (this.TrySamplingRateMatch)
{
SamplingRate closest = this.GetSupportedSamplingRate(sR);
if (closest != this.samplingRate)
{
if (closest != sR && this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("Sampling rate requested ({0}Hz) not supported using closest value ({1}Hz)", (int)sR, (int)closest);
}
this.samplingRate = closest;
}
else
{
return;
}
}
else if (sR != this.samplingRate)
{
this.samplingRate = sR;
}
else
{
return;
}
if (this.IsRecording)
{
this.RequiresRestart = true;
if (this.Logger.IsInfoEnabled)
{
this.Logger.LogInfo("Recorder.{0} changed, Recorder requires restart for this to take effect.", "SamplingRate");
}
}
}
private void CheckAndSetSamplingRate()
{
this.CheckAndSetSamplingRate(this.samplingRate);
}
internal void StopRecordingInternal()
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Stopping recording");
}
this.wasRecordingBeforePause = false;
this.RemoveVoice();
if (this.MicrophoneDeviceChangeDetected)
{
this.MicrophoneDeviceChangeDetected = false;
}
}
internal void CheckAndAutoStart()
{
this.CheckAndAutoStart(this.autoStart);
}
internal void CheckAndAutoStart(bool autoStartFlag)
{
bool canAutoStart = true;
if (!autoStartFlag)
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: autoStart flag is false.");
}
}
if (!this.IsInitialized)
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: recorder not initialized.");
}
}
if (this.isRecording)
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: recorder is already started.");
}
}
if (this.recordingStoppedExplicitly)
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: recorder was previously stopped explicitly.");
}
}
if (this.recordOnlyWhenEnabled && !this.isActiveAndEnabled)
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: recorder not enabled and this is required.");
}
}
if (this.recordOnlyWhenJoined && (this.voiceConnection == null || this.voiceConnection.Client == null || !this.voiceConnection.Client.InRoom))
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: voice client not joined to a room yet and this is required.");
}
}
if (!this.CheckIfThereIsAtLeastOneMic())
{
canAutoStart = false;
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Auto start check failure: no microphone detected.");
}
}
if (canAutoStart)
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("AutoStart requirements met: going to auto start recording");
}
this.StartRecordingInternal();
}
else if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("AutoStart requirements NOT met: NOT going to auto start recording");
}
}
internal void StartRecordingInternal()
{
if (this.Logger.IsDebugEnabled)
{
this.Logger.LogDebug("Starting recording");
}
this.wasRecordingBeforePause = false;
this.recordingStoppedExplicitly = false;
this.Setup();
}
private IDeviceEnumerator GetMicrophonesEnumerator(MicType micType)
{
switch (micType)
{
case MicType.Unity:
{
if (this.unityMicrophonesEnumerator == null)
{
this.unityMicrophonesEnumerator = new AudioInEnumerator(this.Logger);
if (!this.unityMicrophonesEnumerator.IsSupported && this.Logger.IsWarningEnabled)
{
this.Logger.LogWarning("UnityMicrophonesEnumerator is not supported on this platform {0}.", CurrentPlatform);
}
else if (this.unityMicrophonesEnumerator.Error != null && this.Logger.IsErrorEnabled)
{
this.Logger.LogError(this.unityMicrophonesEnumerator.Error);
}
}
return this.unityMicrophonesEnumerator;
}
case MicType.Photon:
{
//#if PHOTON_MICROPHONE_ENUMERATOR
if (this.photonMicrophonesEnumerator == null)
{
this.photonMicrophonesEnumerator = CreatePhotonDeviceEnumerator(this.Logger);
}
//#endif
return this.photonMicrophonesEnumerator;
}
}
return null;
}
private DeviceInfo GetDeviceById(int id)
{
foreach (DeviceInfo deviceInfo in this.MicrophonesEnumerator)
{
if (deviceInfo.IDInt == id)
{
return deviceInfo;
}
}
return DeviceInfo.Default;
}
private DeviceInfo GetDeviceById(string id)
{
foreach (DeviceInfo deviceInfo in this.MicrophonesEnumerator)
{
if (string.Equals(deviceInfo.IDString, id))
{
return deviceInfo;
}
}
return DeviceInfo.Default;
}
private bool CheckIfThereIsAtLeastOneMic()
{
#if !UNITY_EDITOR && UNITY_SWITCH
return true;
#else
#if PHOTON_MICROPHONE_ENUMERATOR
if (this.MicrophoneType == MicType.Photon)
{
IDeviceEnumerator enumerator = this.MicrophonesEnumerator;
if (enumerator != null)
{
return enumerator.Any();
}
}
#endif
// todo: check if this code causes issues on some platforms.
return UnityMicrophone.devices.Length > 0;
#endif
}
private static IDeviceEnumerator CreatePhotonDeviceEnumerator(VoiceLogger voiceLogger)
{
IDeviceEnumerator enumerator = Platform.CreateAudioInEnumerator(voiceLogger);
if (!enumerator.IsSupported && voiceLogger.IsWarningEnabled)
{
voiceLogger.LogWarning("PhotonMicrophonesEnumerator is not supported on this platform {0}.", CurrentPlatform);
}
else if (enumerator.Error != null && voiceLogger.IsErrorEnabled)
{
voiceLogger.LogError(enumerator.Error);
}
return enumerator;
}
#endregion
public enum InputSourceType
{
Microphone,
AudioClip,
Factory
}
public enum MicType
{
Unity,
Photon
}
[Obsolete("No longer needed. Implicit conversion is done internally when needed.")]
public enum SampleTypeConv
{
None,
Short
}
[Obsolete("Use Photon.Voice.Unity.PhotonVoiceCreatedParams")]
public class PhotonVoiceCreatedParams : Unity.PhotonVoiceCreatedParams
{
}
}
}