// ----------------------------------------------------------------------------
//
// 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
}
}
}