// ---------------------------------------------------------------------------- // // Photon Voice for Unity - Copyright (C) 2018 Exit Games GmbH // // // Component representing remote audio stream in local scene. // // developer@photonengine.com // ---------------------------------------------------------------------------- //#define USE_ONAUDIOFILTERREAD using System; using UnityEngine; namespace Photon.Voice.Unity { /// Component representing remote audio stream in local scene. [RequireComponent(typeof(AudioSource))] [AddComponentMenu("Photon Voice/Speaker")] [DisallowMultipleComponent] public class Speaker : VoiceComponent { #region Private Fields private IAudioOut audioOutput; private RemoteVoiceLink remoteVoiceLink; [SerializeField] private bool playbackOnlyWhenEnabled; #if USE_ONAUDIOFILTERREAD private AudioSyncBuffer outBuffer; private int outputSampleRate; #endif #pragma warning disable 414 [SerializeField] [HideInInspector] private int playDelayMs = 200; #pragma warning restore 414 [SerializeField] private PlaybackDelaySettings playbackDelaySettings = new PlaybackDelaySettings { MinDelaySoft = PlaybackDelaySettings.DEFAULT_LOW, MaxDelaySoft = PlaybackDelaySettings.DEFAULT_HIGH, MaxDelayHard = PlaybackDelaySettings.DEFAULT_MAX }; private bool playbackExplicitlyStopped; #endregion #region Public Fields ///Remote audio stream playback delay to compensate packets latency variations. Try 100 - 200 if sound is choppy. [Obsolete("Use SetPlaybackDelaySettings methods instead")] public int PlayDelayMs { get { return this.playbackDelaySettings.MinDelaySoft; } set { if (value >= 0 && value < this.playbackDelaySettings.MaxDelaySoft) { this.playbackDelaySettings.MinDelaySoft = value; } } } #if UNITY_PS4 || UNITY_SHARLIN /// Set the PlayStation User ID to determine on which users headphones to play audio. /// /// Note: at the moment, only the first Speaker can successfully set the User ID. /// Subsequently initialized Speakers will play their audio on the headphones that have been set with the first Speaker initialized. public int PlayStationUserID = 0; #endif /// /// A custom factory method to return implementation used for the playback. /// public Func> CustomAudioOutFactory; #endregion #region Properties /// Is the speaker playing right now. public bool IsPlaying { get { return this.IsInitialized && this.audioOutput.IsPlaying; } } /// Smoothed difference between (jittering) stream and (clock-driven) audioOutput. public int Lag { get { return this.IsPlaying ? this.audioOutput.Lag : -1; } } /// /// Register a method to be called when remote voice removed. /// public Action OnRemoteVoiceRemoveAction { get; set; } /// Per room, the connected users/players are represented with a Realtime.Player, also known as Actor. /// Photon Voice calls this Actor, to avoid a name-clash with the Player class in Voice. public Realtime.Player Actor { get; protected internal set; } /// /// Whether or not this Speaker has been linked to a remote voice stream. /// public bool IsLinked { get { return this.remoteVoiceLink != null; } } #if UNITY_EDITOR /// /// USE IN EDITOR ONLY /// public RemoteVoiceLink RemoteVoiceLink { get { return this.remoteVoiceLink; } } #else internal RemoteVoiceLink RemoteVoiceLink { get { return this.remoteVoiceLink; } } #endif /// If true, component will work only when enabled and active in hierarchy. public bool PlaybackOnlyWhenEnabled { get { return this.playbackOnlyWhenEnabled; } set { if (this.playbackOnlyWhenEnabled != value) { this.playbackOnlyWhenEnabled = value; if (this.IsLinked) { if (this.playbackOnlyWhenEnabled) { if (this.isActiveAndEnabled != this.PlaybackStarted) { if (this.isActiveAndEnabled) { if (!this.playbackExplicitlyStopped) { this.StartPlaying(); } } else { this.StopPlaying(); } } } else if (!this.PlaybackStarted && !this.playbackExplicitlyStopped) { this.StartPlaying(); } } } } } /// Returns if the playback is on. public bool PlaybackStarted { get; private set; } /// Gets the value in ms above which the audio player tries to keep the delay. public int PlaybackDelayMinSoft { get { return this.playbackDelaySettings.MinDelaySoft; } } /// Gets the value in ms below which the audio player tries to keep the delay. public int PlaybackDelayMaxSoft { get { return this.playbackDelaySettings.MaxDelaySoft; } } /// Gets the value in ms that audio play delay will not exceed. public int PlaybackDelayMaxHard { get { return this.playbackDelaySettings.MaxDelayHard; } } internal bool IsInitialized { get { return this.audioOutput != null; } } #endregion #region Private Methods private void OnEnable() { if (this.IsLinked && !this.PlaybackStarted && !this.playbackExplicitlyStopped) { this.StartPlaying(); } } private void OnDisable() { if (this.PlaybackOnlyWhenEnabled && this.PlaybackStarted) { this.StopPlaying(); } } private void Initialize() { if (this.IsInitialized) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Already initialized."); } return; } if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("Initializing."); } Func> factory; if (this.CustomAudioOutFactory != null) { factory = this.CustomAudioOutFactory; } else { factory = this.GetDefaultAudioOutFactory(); } #if !UNITY_EDITOR && (UNITY_PS4 || UNITY_SHARLIN) this.audioOutput = new Photon.Voice.PlayStation.PlayStationAudioOut(this.PlayStationUserID, factory); #else this.audioOutput = factory(); #endif if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("Initialized."); } } internal Func> GetDefaultAudioOutFactory() { #if USE_ONAUDIOFILTERREAD this.outBuffer = new AudioSyncBuffer(this.playbackDelaySettings.MinDelaySoft, this.Logger, string.Empty, this.Logger.IsDebugEnabled); this.outputSampleRate = AudioSettings.outputSampleRate; Func> factory = () => this.outBuffer; #else var pdc = new AudioOutDelayControl.PlayDelayConfig { Low = this.playbackDelaySettings.MinDelaySoft, High = this.playbackDelaySettings.MaxDelaySoft, Max = this.playbackDelaySettings.MaxDelayHard }; Func> factory = () => new UnityAudioOut(this.GetComponent(), pdc, this.Logger, string.Empty, this.Logger.IsDebugEnabled); #endif return factory; } internal bool OnRemoteVoiceInfo(RemoteVoiceLink stream) { if (stream == null) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("RemoteVoiceLink is null, cancelled linking"); } return false; } if (!this.IsInitialized) { this.Initialize(); } if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("OnRemoteVoiceInfo {0}", stream); } if (this.IsLinked) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Speaker already linked to {0}, cancelled linking to {1}", this.remoteVoiceLink, stream); } return false; } if (stream.Info.Channels <= 0) // early avoid possible crash due to ArgumentException in AudioClip.Create inside UnityAudioOut.Start { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Received voice info channels is not expected (<= 0), cancelled linking to {0}", stream); } return false; } this.remoteVoiceLink = stream; this.remoteVoiceLink.RemoteVoiceRemoved += this.OnRemoteVoiceRemove; if (this.IsInitialized) { if (!this.PlaybackOnlyWhenEnabled || this.isActiveAndEnabled) { return this.StartPlayback(); } return true; } return false; } internal void OnRemoteVoiceRemove() { if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("OnRemoteVoiceRemove {0}", this.remoteVoiceLink); } this.StopPlaying(); if (this.OnRemoteVoiceRemoveAction != null) { this.OnRemoteVoiceRemoveAction(this); } this.CleanUp(); } internal void OnAudioFrame(FrameOut frame) { this.audioOutput.Push(frame.Buf); if (frame.EndOfStream) { this.audioOutput.Flush(); } } private bool StartPlaying() { if (!this.IsLinked) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot start playback because speaker is not linked"); } return false; } if (this.PlaybackStarted) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Playback is already started"); } return false; } if (!this.IsInitialized) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot start playback because not initialized yet"); } return false; } if (!this.isActiveAndEnabled && this.PlaybackOnlyWhenEnabled) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot start playback because PlaybackOnlyWhenEnabled is true and Speaker is not enabled or its GameObject is not active in the hierarchy."); } return false; } VoiceInfo voiceInfo = this.remoteVoiceLink.Info; if (voiceInfo.Channels == 0) { if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Cannot start playback because Channels == 0, stream {0}", this.remoteVoiceLink); } return false; } if (this.Logger.IsInfoEnabled) { this.Logger.LogInfo("Speaker about to start playback stream {0}, delay {1}", this.remoteVoiceLink, this.playbackDelaySettings); } this.audioOutput.Start(voiceInfo.SamplingRate, voiceInfo.Channels, voiceInfo.FrameDurationSamples); this.remoteVoiceLink.FloatFrameDecoded += this.OnAudioFrame; this.PlaybackStarted = true; this.playbackExplicitlyStopped = false; return true; } private void OnDestroy() { if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("OnDestroy"); } this.StopPlaying(true); this.CleanUp(); } private bool StopPlaying(bool force = false) { if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("StopPlaying"); } if (!force && !this.PlaybackStarted) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot stop playback because it's not started"); } return false; } if (this.IsLinked) { this.remoteVoiceLink.FloatFrameDecoded -= this.OnAudioFrame; } else if (!force && this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Speaker not linked while stopping playback"); } if (this.IsInitialized) { this.audioOutput.Stop(); } else if (!force && this.Logger.IsWarningEnabled) { this.Logger.LogWarning("audioOutput is null while stopping playback"); } this.PlaybackStarted = false; return true; } private void CleanUp() { if (this.Logger.IsDebugEnabled) { this.Logger.LogDebug("CleanUp"); } if (this.remoteVoiceLink != null) { this.remoteVoiceLink.RemoteVoiceRemoved -= this.OnRemoteVoiceRemove; this.remoteVoiceLink = null; } this.Actor = null; } #if USE_ONAUDIOFILTERREAD private void OnAudioFilterRead(float[] data, int channels) { this.outBuffer.Read(data, channels, this.outputSampleRate); } #endif #if UNITY_EDITOR private void OnValidate() { if (this.playDelayMs > 0) { if (this.playbackDelaySettings.MinDelaySoft != this.playDelayMs) { this.playbackDelaySettings.MinDelaySoft = this.playDelayMs; if (this.playbackDelaySettings.MaxDelaySoft <= this.playbackDelaySettings.MinDelaySoft) { this.playbackDelaySettings.MaxDelaySoft = 2 * this.playbackDelaySettings.MinDelaySoft; if (this.playbackDelaySettings.MaxDelayHard < this.playbackDelaySettings.MaxDelaySoft) { this.playbackDelaySettings.MaxDelayHard = this.playbackDelaySettings.MaxDelaySoft + 1000; } } } this.playDelayMs = -1; } } #endif internal void Service() { if (this.PlaybackStarted) { this.audioOutput.Service(); } } #endregion #region Public Methods /// /// Starts the audio playback of the linked incoming remote audio stream via AudioSource component. /// /// True if playback is successfully started. public bool StartPlayback() { return this.StartPlaying(); } /// /// Stops the audio playback of the linked incoming remote audio stream via AudioSource component. /// /// True if playback is successfully stopped. public bool StopPlayback() { if (this.playbackExplicitlyStopped) { if (this.Logger.IsWarningEnabled) { this.Logger.LogWarning("Cannot stop playback because it was already been explicitly stopped."); } return false; } this.playbackExplicitlyStopped = this.StopPlaying(); return this.playbackExplicitlyStopped; } /// /// Restarts the audio playback of the linked incoming remote audio stream via AudioSource component. /// /// If true, player will be reinitialized. /// True if playback is successfully restarted. public bool RestartPlayback(bool reinit = false) { if (!this.StopPlayback()) { return false; } if (reinit) { this.audioOutput = null; this.Initialize(); } return this.StartPlayback(); } /// /// Sets the settings for the playback behaviour in case of delays. /// /// Playback delay configuration struct. /// If a change has been made. public bool SetPlaybackDelaySettings(PlaybackDelaySettings pdc) { return this.SetPlaybackDelaySettings(pdc.MinDelaySoft, pdc.MaxDelaySoft, pdc.MaxDelayHard); } /// /// Sets the settings for the playback behaviour in case of delays. /// /// In milliseconds, audio player tries to keep the playback delay above this value. /// In milliseconds, audio player tries to keep the playback below above this value. /// In milliseconds, audio player guarantees that the playback delay never exceeds this value. /// If a change has been made. public bool SetPlaybackDelaySettings(int low, int high, int max) { if (low >= 0 && low < high) { if (this.playbackDelaySettings.MaxDelaySoft != high || this.playbackDelaySettings.MinDelaySoft != low || this.playbackDelaySettings.MaxDelayHard != max) { if (max < high) { max = high; } this.playbackDelaySettings.MaxDelaySoft = high; this.playbackDelaySettings.MinDelaySoft = low; this.playbackDelaySettings.MaxDelayHard = max; if (this.IsPlaying) { this.RestartPlayback(true); } else if (this.IsInitialized) { this.audioOutput = null; this.Initialize(); } return true; } } else if (this.Logger.IsErrorEnabled) { this.Logger.LogError("Wrong playback delay config values, make sure 0 <= Low < High, low={0}, high={1}, max={2}", low, high, max); } return false; } #endregion } }