// ---------------------------------------------------------------------------- // // Photon Voice for Unity - Copyright (C) 2018 Exit Games GmbH // // // Component representing outgoing audio stream in scene. // // developer@photonengine.com // ---------------------------------------------------------------------------- using System; using POpusCodec.Enums; using UnityEngine; using UnityEngine.Serialization; using System.Linq; 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 [SerializeField] private bool voiceDetection; [SerializeField] private float voiceDetectionThreshold = 0.01f; [SerializeField] private int voiceDetectionDelayMs = 500; private object userData; private LocalVoice voice = LocalVoiceAudioDummy.Dummy; private IAudioDesc inputSource; private VoiceConnection voiceConnection; [SerializeField] [FormerlySerializedAs("audioGroup")] private byte interestGroup; [SerializeField] private bool useTargetPlayers; // to distinguish between null and empty targetPlayers array [SerializeField] private int[] targetPlayers; [SerializeField] private bool debugEchoMode; [SerializeField] private bool reliableMode; [SerializeField] private bool encrypt; [SerializeField] private bool transmitEnabled = true; [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; [SerializeField] private bool recordingEnabled = true; private Func inputFactory; [SerializeField] private IOS.AudioSessionParameters audioSessionParameters = IOS.AudioSessionParametersPresets.Game; // stores the preset for editor, the microphone initialization used only the field above #if UNITY_EDITOR public enum EditorIosAudioSessionPreset { Custom, Game, VoIP, } [SerializeField] #pragma warning disable CS0414 // The field is assigned but its value is never used (used in RecorderEditor) private EditorIosAudioSessionPreset editorAudioSessionPreset = EditorIosAudioSessionPreset.Game; #pragma warning restore CS0414 #endif [SerializeField] private AndroidAudioInParameters androidMicrophoneSettings = AndroidAudioInParameters.Default; private bool isPausedOrInBackground; [SerializeField] private bool stopRecordingWhenPaused; [SerializeField] private bool useOnAudioFilterRead; [SerializeField] private bool useMicrophoneTypeFallback = true; [SerializeField] private bool recordWhenJoined = true; private DeviceInfo microphoneDevice = DeviceInfo.Default; // int instead of bool to use Interlocked.Exchange() private int microphoneDeviceChangePending; #endregion #region Properties internal void MicrophoneDeviceChangeDetected() { this.microphoneDeviceChangePending = 1; } /// 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. /// Initialized with serialized field and can be updated by Editor at runtime. public bool Encrypt { get => this.voice.Encrypt; set { this.voice.Encrypt = value; this.encrypt = value; } } /// If true, outgoing stream routed back to client via server same way as for remote client's streams. /// Initialized with serialized field and can be updated by Editor at runtime. public bool DebugEchoMode { get => this.voice.DebugEchoMode; set { this.voice.DebugEchoMode = value; this.debugEchoMode = value; } } /// If true, stream data sent in reliable mode. /// Initialized with serialized field and can be updated by Editor at runtime. public bool ReliableMode { get => this.voice.Reliable; set { this.voice.Reliable = value; this.reliableMode = value; } } /// If true, voice detection enabled. public bool VoiceDetection { get { 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 { return this.voiceDetectionThreshold; } set { if (this.voiceDetectionThreshold.Equals(value)) { return; } if (value < 0f || value > 1f) { this.Logger.Log(LogLevel.Error, "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 { 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; this.Logger.Log(LogLevel.Info, "Recorder.UserData changed"); this.RestartRecording(); } } } /// 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; this.Logger.Log(LogLevel.Info, "Recorder.InputFactory changed"); if (this.SourceType == InputSourceType.Factory) { this.RestartRecording(); } } } } /// Returns voice activity detector for recorder's audio stream. public AudioUtil.IVoiceDetector VoiceDetector { get { return this.voiceAudio != null ? this.voiceAudio.VoiceDetector : null; } } /// Target interest group that will receive transmitted audio. /// /// If InterestGroup != 0, recorder's audio data is sent only to clients listening to this group. /// Initialized with serialized field and can be updated by Editor at runtime. /// public byte InterestGroup { get => this.voice.InterestGroup; set { this.voice.InterestGroup = value; this.interestGroup = value; } } /// Target players which will receive transmitted audio. /// Initialized with serialized field and can be updated by Editor at runtime public int[] TargetPlayers { get => this.voice.TargetPlayers; set { var x = value; this.voice.TargetPlayers = x; this.targetPlayers = x; this.useTargetPlayers = x != null; } } /// Returns true if audio stream broadcasts. public bool IsCurrentlyTransmitting { get { return this.RecordingEnabled && 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; this.Logger.Log(LogLevel.Info, "Recorder.Source changed"); this.RestartRecording(); } } } /// Which microphone API to use when the Source is set to Microphone. public MicType MicrophoneType { get { if (Application.platform == RuntimePlatform.WebGLPlayer) { return MicType.Photon; // force Photon type on WebGL } else { return this.microphoneType; } } set { if (this.microphoneType != value) { this.microphoneType = value; this.Logger.Log(LogLevel.Info, "Recorder.MicrophoneType changed"); if (this.SourceType == InputSourceType.Microphone) { this.RestartRecording(); } } } } /// Source audio clip. public AudioClip AudioClip { get { return this.audioClip; } set { if (this.audioClip != value) { this.audioClip = value; this.Logger.Log(LogLevel.Info, "Recorder.AudioClip change"); if (this.SourceType == InputSourceType.AudioClip) { this.RestartRecording(); } } } } /// Loop playback for audio clip sources. public bool LoopAudioClip { get { return this.loopAudioClip; } set { if (this.loopAudioClip != value) { this.loopAudioClip = value; if (this.RecordingEnabled && this.SourceType == InputSourceType.AudioClip) { AudioClipWrapper wrapper = this.inputSource as AudioClipWrapper; if (wrapper != null) { wrapper.Loop = value; } else { this.Logger.Log(LogLevel.Error, "Unexpected: Recorder inputSource is not of AudioClipWrapper type or is null."); } } } } } /// Outgoing audio stream sampling rate. public SamplingRate SamplingRate { get { return this.samplingRate; } set { if (this.samplingRate != value) { this.samplingRate = value; this.Logger.Log(LogLevel.Info, "Recorder.SamplingRate changed"); this.RestartRecording(); } } } /// Outgoing audio stream encoder delay. public OpusCodec.FrameDuration FrameDuration { get { return this.frameDuration; } set { if (this.frameDuration != value) { this.frameDuration = value; this.Logger.Log(LogLevel.Info, "Recorder.FrameDuration changed"); this.RestartRecording(); } } } /// 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) { this.Logger.Log(LogLevel.Error, "Unsupported bitrate value {0}, valid range: {1}-{2}", value, MIN_OPUS_BITRATE, MAX_OPUS_BITRATE); } else { this.bitrate = value; this.Logger.Log(LogLevel.Info, "Recorder.Bitrate changed"); this.RestartRecording(); } } } } /// Gets or sets whether this Recorder is recording audio to be transmitted. public bool RecordingEnabled { get { return this.recordingEnabled; } set { if (this.recordingEnabled != value) { this.recordingEnabled = value; if (this.recordingEnabled) { this.RestartRecording(); } else { this.StopRecording(); } } } } /// 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; this.Logger.Log(LogLevel.Info, "Recorder.UseOnAudioFilterRead changed"); if (this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Unity) { this.RestartRecording(); } } } } /// 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 starts when joining the room and stops when leaving the room. public bool RecordWhenJoined { get { return this.recordWhenJoined; } set { this.recordWhenJoined = value; } } public DeviceInfo MicrophoneDevice { get { return this.microphoneDevice; } set { if (this.microphoneDevice != value) { this.microphoneDevice = value; this.Logger.Log(LogLevel.Info, "Recorder.MicrophoneDevice changed"); if (this.SourceType == InputSourceType.Microphone) { this.RestartRecording(); } } } } public bool AndroidMicrophoneAGC { get { return this.androidMicrophoneSettings.EnableAGC; } } public bool AndroidMicrophoneAEC { get { return this.androidMicrophoneSettings.EnableAEC; } } public bool AndroidMicrophoneNS { get { return this.androidMicrophoneSettings.EnableNS; } } #endregion #region Public Methods /// /// Initializes the Recorder component to be able to transmit audio. /// Called by the VoiceConnection this Recorder belongs to. /// /// The VoiceConnection to be used with this Recorder. internal bool Init(VoiceConnection connection) { if (!this.isActiveAndEnabled) { this.Logger.Log(LogLevel.Warning, "Recorder is disabled."); return false; } if (this.voiceConnection != null) { this.Logger.Log(LogLevel.Warning, "Recorder already initialized."); return false; } this.voiceConnection = connection; this.RestartRecording(); // in case RecordingEnabled is true return true; } internal bool Deinit(VoiceConnection connection) { this.StopRecording(); this.voiceConnection = null; return true; } // prevents multiple restarts per Update() // int instead of bool to use Interlocked.Exchange() int restartRecordingPending = 0; /// /// Restarts recording if is true /// public bool RestartRecording() { if (this.RecordingEnabled) { restartRecordingPending = 1; } return this.RecordingEnabled; } /// 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) { this.Logger.Log(LogLevel.Warning, "Cannot start voice detection calibration when transmission is not enabled"); return; } this.voiceAudio.VoiceDetectorCalibrate(durationMs, newThreshold => { this.voiceDetectionThreshold = this.VoiceDetector.Threshold; if (detectionEndedCallback != null) { detectionEndedCallback(this.voiceDetectionThreshold); } }); } } private void StartRecording() { this.StopRecording(); if (this.voiceConnection == null) { this.Logger.Log(LogLevel.Info, "Recording can't be started if Recorder is not initialized."); return; } this.Logger.Log(LogLevel.Info, "Starting recording"); if (this.inputSource != null) { this.inputSource.Dispose(); this.inputSource = null; } this.voice.RemoveSelf(); this.voice = this.CreateLocalVoiceAudioAndSource(); if (this.voice == LocalVoiceAudioDummy.Dummy) { this.Logger.Log(LogLevel.Error, "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; } return; } if (this.VoiceDetector != null) { this.VoiceDetector.Threshold = this.voiceDetectionThreshold; this.VoiceDetector.ActivityDelayMs = this.voiceDetectionDelayMs; this.VoiceDetector.On = this.voiceDetection; } this.SendPhotonVoiceCreatedMessage(); this.voice.TransmitEnabled = this.TransmitEnabled; } private void StopRecording() { this.Logger.Log(LogLevel.Info, "Stopping recording"); if (this.voice != LocalVoiceAudioDummy.Dummy) { this.voice.RemoveSelf(); this.voice = LocalVoiceAudioDummy.Dummy; this.gameObject.SendMessage("PhotonVoiceRemoved", SendMessageOptions.DontRequireReceiver); } if (this.inputSource != null) { this.inputSource.Dispose(); this.inputSource = null; } } //#if UNITY_EDITOR || (UNITY_IOS || UNITY_VISIONOS) /// /// 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; this.Logger.Log(LogLevel.Info, "Recorder.iOSAudioSessionParameters changed to {0}", this.audioSessionParameters); if (this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon) { this.RestartRecording(); } return true; } return false; } //#endif //#if UNITY_EDITOR || UNITY_ANDROID /// /// 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.androidMicrophoneSettings.EnableAEC != aec || this.androidMicrophoneSettings.EnableAGC != agc || this.androidMicrophoneSettings.EnableNS != ns) { this.androidMicrophoneSettings.EnableAEC = aec; this.androidMicrophoneSettings.EnableAGC = agc; this.androidMicrophoneSettings.EnableNS = ns; this.Logger.Log(LogLevel.Info, "Recorder.nativeAndroidMicrophoneSettings changed to aec = {0}, agc = {1}, ns = {2}", aec, agc, ns); if (this.SourceType == InputSourceType.Microphone && this.MicrophoneType == MicType.Photon) { this.RestartRecording(); } return true; } else { 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) { this.Logger.Log(LogLevel.Info, "Resetting local audio."); (this.inputSource as IResettable).Reset(); return true; } this.Logger.Log(LogLevel.Debug, "InputSource is null or not resettable."); return false; } #endregion #region Private Methods private LocalVoice CreateLocalVoiceAudioAndSource() { if (Application.platform == RuntimePlatform.WebGLPlayer) { #if !UNITY_2021_2_OR_NEWER // opus lib requires Emscripten 2.0.19 this.Logger.Log(LogLevel.Error, "Recorder Opus encoder requies Unity 2021.2 or newer for WebGL"); return new LocalVoiceAudioDummy(); #endif } int samplingRateInt = (int)this.samplingRate; switch (this.SourceType) { case InputSourceType.Microphone: { bool fallbackMicrophone = false; switch (this.MicrophoneType) { case MicType.Unity: { // if fallback, switch to default device from set by other type DeviceInfo micDev = fallbackMicrophone ? DeviceInfo.Default : this.MicrophoneDevice; this.Logger.Log(LogLevel.Info, "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(gameObject, micDev.IDString, samplingRateInt, this.Logger); } else { this.inputSource = new MicWrapper(micDev.IDString, samplingRateInt, this.Logger); } if (this.inputSource != null) { if (this.inputSource.Error != null) { this.Logger.Log(LogLevel.Error, "Unity microphone input source creation failure: {0}", this.inputSource.Error); } else { break; } } if (this.UseMicrophoneTypeFallback && !fallbackMicrophone) { fallbackMicrophone = true; this.Logger.Log(LogLevel.Error, "Unity microphone failed. Falling back to Photon microphone"); goto case MicType.Photon; } } break; case MicType.Photon: { object otherParams = null; // if fallback, switch to default device from set by other type DeviceInfo micDev = fallbackMicrophone ? DeviceInfo.Default : this.MicrophoneDevice; this.Logger.Log(LogLevel.Info, "Setting recorder's source to Photon microphone device={0}", micDev); // TODO: only iOS and Android need specific processing // Per platform Logging left to save something from previous file version switch (Application.platform) { case RuntimePlatform.WindowsPlayer: case RuntimePlatform.WindowsEditor: this.Logger.Log(LogLevel.Info, "Setting recorder's source to WindowsAudioInPusher"); break; case RuntimePlatform.WSAPlayerARM: case RuntimePlatform.WSAPlayerX64: case RuntimePlatform.WSAPlayerX86: this.Logger.Log(LogLevel.Info, "Setting recorder's source to UWP.AudioInPusher"); break; case RuntimePlatform.IPhonePlayer: #if UNITY_VISIONOS case RuntimePlatform.VisionOS: #endif otherParams = audioSessionParameters; this.Logger.Log(LogLevel.Info, "Setting recorder's source to IOS.AudioInPusher with session {0}", audioSessionParameters); break; case RuntimePlatform.OSXPlayer: case RuntimePlatform.OSXEditor: this.Logger.Log(LogLevel.Info, "Setting recorder's source to MacOS.AudioInPusher"); break; case RuntimePlatform.Switch: this.Logger.Log(LogLevel.Info, "Setting recorder's source to Switch.AudioInPusher"); break; case RuntimePlatform.Android: otherParams = androidMicrophoneSettings; this.Logger.Log(LogLevel.Info, "Setting recorder's source to UnityAndroidAudioInAEC"); break; case RuntimePlatform.WebGLPlayer: #if UNITY_2021_2_OR_NEWER // requires ES6 this.Logger.Log(LogLevel.Info, "Setting recorder's source to Unity.WebAudioMicIn"); break; #else this.Logger.Log(LogLevel.Error, "Microphone cature requies Unity 2021.2 or newer for WebGL"); goto default; #endif default: this.Logger.Log(LogLevel.Error, "Photon microphone type is not supported for the current platform {0}", Application.platform); break; } this.inputSource = Platform.CreateDefaultAudioSource(this.Logger, micDev, samplingRateInt, 1, otherParams); if (this.inputSource != null) { if (this.inputSource.Error != null) { this.Logger.Log(LogLevel.Error, "Photon microphone input source creation failure: {0}", this.inputSource.Error); } else { break; } } if (this.UseMicrophoneTypeFallback && !fallbackMicrophone) { fallbackMicrophone = true; this.Logger.Log(LogLevel.Error, "Photon microphone failed. Falling back to Unity microphone"); goto case MicType.Unity; } break; } default: this.Logger.Log(LogLevel.Error, "unknown MicrophoneType value {0}", this.MicrophoneType); return LocalVoiceAudioDummy.Dummy; } } break; case InputSourceType.AudioClip: { if (ReferenceEquals(null, this.AudioClip)) { this.Logger.Log(LogLevel.Error, "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) { // this.Logger.Log(LogLevel.Error, "Recorder.InputFactory must be specified if Recorder.Source set to Factory"); // return LocalVoiceAudioDummy.Dummy; this.Logger.Log(LogLevel.Warning, "Recorder.Source is Factory but Recorder.InputFactory is not set. Setting it to ToneAudioReader."); this.InputFactory = () => new AudioUtil.ToneAudioPusher(); } this.inputSource = this.InputFactory(); if (this.inputSource.Error != null) { this.Logger.Log(LogLevel.Error, "InputFactory creation failure: {0}.", this.inputSource.Error); } } break; default: this.Logger.Log(LogLevel.Error, "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) { this.Logger.Log(LogLevel.Error, "inputSource.Channels is zero"); return LocalVoiceAudioDummy.Dummy; } VoiceInfo voiceInfo = VoiceInfo.CreateAudioOpus(this.samplingRate, this.inputSource.Channels, this.frameDuration, this.Bitrate, this.UserData); AudioSampleType audioSampleType = AudioSampleType.Source; WebRtcAudioDsp dsp = this.GetComponent(); if (null != dsp) { dsp.AdjustVoiceInfo(ref voiceInfo, ref audioSampleType); } VoiceCreateOptions opt = new VoiceCreateOptions() { InterestGroup = this.interestGroup, TargetPlayers = this.useTargetPlayers ? this.targetPlayers : null, DebugEchoMode = this.debugEchoMode, Encrypt = this.encrypt, Reliable = this.reliableMode, }; return this.voiceConnection.VoiceClient.CreateLocalVoiceAudioFromSource(voiceInfo, this.inputSource, audioSampleType, VoiceConnection.ChannelAudio, opt); } protected virtual void SendPhotonVoiceCreatedMessage() { this.gameObject.SendMessage("PhotonVoiceCreated", new Unity.PhotonVoiceCreatedParams { Voice = this.voice, AudioDesc = this.inputSource }, SendMessageOptions.DontRequireReceiver); } protected void Update() { if (this.voiceConnection == null) { return; } if (System.Threading.Interlocked.Exchange(ref this.microphoneDeviceChangePending, 0) != 0) { // can trigger restart handled below this.HandleDeviceChange(); } // restarting recording if (System.Threading.Interlocked.Exchange(ref this.restartRecordingPending, 0) != 0) { if (this.RecordingEnabled) { this.Logger.Log(LogLevel.Info, "Restarting recording"); this.StartRecording(); } } } private void OnDestroy() { if (this.voiceConnection == null) { return; } this.Logger.Log(LogLevel.Info, "Recorder is about to be destroyed, removing local voice."); this.StopRecording(); this.voiceConnection.RemoveRecorder(this); } private void HandleDeviceChange() { if (this.RecordingEnabled) { if (this.SourceType == InputSourceType.Microphone) { if (this.ResetLocalAudio()) { this.Logger.Log(LogLevel.Info, "Local audio reset as a result of audio config/device change."); } else { this.Logger.Log(LogLevel.Info, "Restarting Recording as a result of audio config/device change."); this.RestartRecording(); } } } } private void OnApplicationPause(bool paused) { if (this.voiceConnection == null) { return; } this.Logger.Log(LogLevel.Debug, "OnApplicationPause({0})", paused); this.HandleApplicationPause(paused); } private void OnApplicationFocus(bool focused) { if (this.voiceConnection == null) { return; } this.Logger.Log(LogLevel.Debug, "OnApplicationFocus({0})", focused); this.HandleApplicationPause(!focused); } private void HandleApplicationPause(bool paused) { this.Logger.Log(LogLevel.Info, "App paused?= {0}, isPausedOrInBackground = {1}, StopRecordingWhenPaused = {2}, RecordingEnabled = {3}", paused, this.isPausedOrInBackground, this.StopRecordingWhenPaused, this.RecordingEnabled); if (this.isPausedOrInBackground == paused) // OnApplicationFocus and OnApplicationPause both called { return; } if (paused) { this.isPausedOrInBackground = true; if (this.StopRecordingWhenPaused && this.RecordingEnabled) { this.Logger.Log(LogLevel.Info, "Stopping recording as application went to background or paused"); this.StopRecording(); } } else { if (!this.StopRecordingWhenPaused) { if (this.ResetLocalAudio()) { this.Logger.Log(LogLevel.Info, "Local audio reset as application is back from background or unpaused"); } } else if (this.RecordingEnabled) { this.Logger.Log(LogLevel.Info, "Starting recording as application is back from background or unpaused"); this.RestartRecording(); } this.isPausedOrInBackground = false; } } #endregion public enum InputSourceType { Microphone, AudioClip, Factory } public enum MicType { Unity, Photon } } }