// %BANNER_BEGIN%
// ---------------------------------------------------------------------
// %COPYRIGHT_BEGIN%
// Copyright (c) (2018-2022) Magic Leap, Inc. All Rights Reserved.
// Use of this file is governed by the Software License Agreement, located here: https://www.magicleap.com/software-license-agreement-ml2
// Terms and conditions applicable to third-party materials accompanying this distribution may also be found in the top-level NOTICE file appearing herein.
// %COPYRIGHT_END%
// ---------------------------------------------------------------------
// %BANNER_END%
namespace UnityEngine.XR.MagicLeap
{
using System;
using System.Runtime.InteropServices;
using UnityEngine.XR.MagicLeap.Native;
///
/// Manages Audio.
///
public sealed partial class MLAudioOutput : MLAutoAPISingleton
{
///
/// The current audio device.
///
private Device audioDevice = Device.Wearable;
///
/// The last audio device.
///
private Device lastAudioDevice = Device.Wearable;
///
/// The value of the master volume.
///
private float masterVolume;
///
/// The delegate for the master volume changed event.
///
/// The new master volume value.
public delegate void OnMasterVolumeChangedDelegate(float volume);
///
/// The delegate for audio output device changed event.
///
/// The new audio output device.
public delegate void OnAudioOutputDeviceChangedDelegate(Device device);
///
/// The delegate for audio output media event.
///
/// The new media event.
public delegate void MLAudioMediaEventDelegate(MediaEvent mediaEvent);
///
/// Raised whenever the master volume gets changed.
///
public static event OnMasterVolumeChangedDelegate OnMasterVolumeChanged = delegate { };
///
/// Raised whenever the audio output device gets changed.
///
public static event OnAudioOutputDeviceChangedDelegate OnAudioOutputDeviceChanged = delegate { };
///
/// Raised whenever the media event happens.
///
public static event MLAudioMediaEventDelegate OnMediaEvent = delegate { };
///
/// The currently active output device.
///
public enum Device : uint
{
///
/// Built-in speakers in the wearable.
///
Wearable,
///
/// 3.5mm jack on the belt pack.
///
AnalogJack
}
///
/// Possible media control events initiated by the user.
///
public enum MediaEvent
{
///
/// Indicates a user command to play
///
Play,
///
/// Indicates a user command to stop.
///
Stop,
///
/// Indicates a user command to pause.
///
Pause,
///
/// Indicates a user command to go to next track.
///
NextTrack,
///
/// Indicates a user command to go to previous track.
///
PrevTrack,
};
///
/// Gets the audio output device.
///
public static Device AudioOutputDevice
{
get
{
audioOutputDevicePerfMarker.Begin();
Device device = Instance.InternalGetOutputDevice();
audioOutputDevicePerfMarker.End();
return device;
}
}
///
/// Gets the master volume for the device.
///
public static float MasterVolume
{
get { return Instance.masterVolume; }
}
public static MLResult SetSoundBypassesMasterVolume(bool isBypassing) => MLResult.Create(Instance.SetSoundBypassesMasterVolumeInternal(isBypassing));
public static MLResult GetSoundBypassesMasterVolume(out bool isBypassing) => MLResult.Create(Instance.GetSoundBypassesMasterVolumeInternal(out isBypassing));
///
/// Setting this option on a sound output causes its output to bypass master volume, making
/// it effectively "always audible" (assuming it is neither muted nor set to zero volume
/// on a per-sound basis). This option can only be set on medical-enabled devices (60601
/// compliant), and will only work for non-spatial sounds.Non-spatial sound parameters
/// such as volume, mute, pitch and looping are still in effect for sounds that are
/// bypassing master volume.
///
private MLResult.Code SetSoundBypassesMasterVolumeInternal(bool isBypassing)
{
ulong audioHandle = NativeBindings.MLUnityAudioGetHandle();
var resultCode = NativeBindings.MLAudioSetSoundBypassesMasterVolume(audioHandle, isBypassing);
MLResult.DidNativeCallSucceed(resultCode, nameof(MLAudioOutput.NativeBindings.MLAudioSetSoundBypassesMasterVolume));
return resultCode;
}
///
/// Queries whether a sound output is exempt from attenuation due to master volume.
/// This call reports whether the output from a sound output is bypassing master volume,
/// making it effectively "always audible" (assuming it is neither muted nor set to zero volume
/// on a per-sound basis). This option can only be set on medical-enabled devices(60601
/// compliant), and will only work for non-spatial sounds.Non-spatial sound parameters
/// such as volume, mute, pitch and looping are still in effect for sounds that are
/// bypassing master volume.
///
private MLResult.Code GetSoundBypassesMasterVolumeInternal(out bool isBypassing)
{
ulong audioHandle = NativeBindings.MLUnityAudioGetHandle();
var resultCode = NativeBindings.MLAudioGetSoundBypassesMasterVolume(audioHandle, out isBypassing);
MLResult.DidNativeCallSucceed(resultCode, nameof(MLAudioOutput.NativeBindings.MLAudioGetSoundBypassesMasterVolume));
return resultCode;
}
///
/// Gets the result string for a MLResult.Code.
///
/// The MLResult.Code to be requested.
/// The result string.
internal static string GetResultString(MLResult.Code resultCode)
{
try
{
return Marshal.PtrToStringAnsi(NativeBindings.MLAudioGetResultString(resultCode));
}
catch (System.DllNotFoundException)
{
MLPluginLog.Error("MLAudioOutput.GetResultString failed. Reason: MLAudio API is currently available only on device.");
}
catch (System.EntryPointNotFoundException)
{
MLPluginLog.Error("MLAudioOutput.GetResultString failed. Reason: API symbols not found");
}
return string.Empty;
}
#if !DOXYGEN_SHOULD_SKIP_THIS
///
/// Called by MLAutoAPISingleton to start the API
///
///
/// MLResult.Result will be MLResult.Code.Ok if successful.
/// MLResult.Result will be MLResult.Code.UnspecifiedFailure if failed due to internal error.
/// MLResult.Result will be MLResult.Code.InvalidParam if a parameter is invalid.
/// MLResult.Result will be MLResult.Code.PermissionDenied if AudioCaptureMic permission is denied.
/// MLResult.Result will be MLResult.Code.AudioNotImplemented if the function is not implemented.
///
protected override MLResult.Code StartAPI()
{
startAPIPerfMarker.Begin();
// Set the initial audio device.
Instance.lastAudioDevice = Instance.InternalGetOutputDevice();
MLResult.Code resultCode;
// Master Volume Callback
resultCode = this.RegisterCallbacks();
if (resultCode != MLResult.Code.Ok)
{
startAPIPerfMarker.End();
return resultCode;
}
// Get the initial MasterVolume value.
resultCode = Instance.GetMasterVolume(out this.masterVolume);
if (resultCode != MLResult.Code.Ok)
{
startAPIPerfMarker.End();
return resultCode;
}
startAPIPerfMarker.End();
return resultCode;
}
#endif // DOXYGEN_SHOULD_SKIP_THIS
///
/// Called by MLAutoAPISingleton on destruction
///
/// Not Implemented
protected override MLResult.Code StopAPI()
{
try
{
return UnregisterCallbacks();
}
catch (System.DllNotFoundException)
{
MLPluginLog.Error(MLResult.Code.APIDLLNotFound);
throw;
}
}
///
/// Called every device frame
///
protected override void Update()
{
updatePerfMarker.Begin();
if (this.lastAudioDevice != Instance.InternalGetOutputDevice())
{
Instance.lastAudioDevice = Instance.audioDevice;
// Notify event listeners.
// Callback is not needed to be in the queue because it is in Update.
OnAudioOutputDeviceChanged?.Invoke(Instance.lastAudioDevice);
}
updatePerfMarker.End();
}
///
/// Handles the callback for MLAudioSetMasterVolume.
///
/// The volume value.
/// A pointer to the callback.
[AOT.MonoPInvokeCallback(typeof(NativeBindings.MLAudioMasterVolumeChangedCallback))]
private static void HandleOnMLAudioSetMasterVolumeCallback(float volume, IntPtr callback)
{
Instance.masterVolume = volume;
MLThreadDispatch.Call(volume, OnMasterVolumeChanged);
}
///
/// Handles the callback for MLAudioSetMediaEventCallback.
///
/// The media event.
/// A pointer to the callback.
[AOT.MonoPInvokeCallback(typeof(NativeBindings.MLAudioMediaEventCallback))]
private static void HandleOnMediaEventCallback(MediaEvent mediaEvent, IntPtr callback)
{
MLThreadDispatch.Call(mediaEvent, OnMediaEvent);
}
///
/// Returns the master volume for the audio system.
/// The range of the volume is 0-10, with 0 being silent and 10 being full volume.
///
/// The current volume value.
///
/// MLResult.Result will be MLResult.Code.Ok if successful.
/// MLResult.Result will be MLResult.Code.UnspecifiedFailure if failed due to internal error.
/// MLResult.Result will be MLResult.Code.InvalidParam if a parameter is invalid.
/// MLResult.Result will be MLResult.Code.PermissionDenied if AudioCaptureMic permission is denied.
/// MLResult.Result will be MLResult.Code.AudioNotImplemented if the function is not implemented.
///
private MLResult.Code GetMasterVolume(out float volume)
{
MLResult.Code result;
try
{
nativeGetMasterVolumePerfMarker.Begin();
result = NativeBindings.MLAudioGetMasterVolume(out volume);
MLResult.DidNativeCallSucceed(result, nameof(NativeBindings.MLAudioGetMasterVolume));
nativeGetMasterVolumePerfMarker.End();
if (result != MLResult.Code.Ok)
{
MLPluginLog.ErrorFormat("MLAudioOutput.GetMasterVolume failed to get the volume. Reason: {0}", result);
}
}
catch (System.DllNotFoundException)
{
// Exception is caught in the Singleton BaseStart().
throw;
}
return result;
}
///
/// Registers a callback for the device volume change event.
///
///
/// MLResult.Result will be MLResult.Code.Ok if successful.
/// MLResult.Result will be MLResult.Code.UnspecifiedFailure if failed due to internal error.
/// MLResult.Result will be MLResult.Code.InvalidParam if a parameter is invalid.
/// MLResult.Result will be MLResult.Code.PermissionDenied if AudioCaptureMic permission is denied.
/// MLResult.Result will be MLResult.Code.AudioNotImplemented if the function is not implemented.
///
private MLResult.Code RegisterCallbacks()
{
MLResult.Code result;
nativeSetMasterVolumeCallbackPerfMarker.Begin();
// Attempt to register the native callback for the volume change event.
result = NativeBindings.MLAudioSetMasterVolumeCallback(HandleOnMLAudioSetMasterVolumeCallback, IntPtr.Zero);
MLResult.DidNativeCallSucceed(result, nameof(NativeBindings.MLAudioSetMasterVolumeCallback));
nativeSetMasterVolumeCallbackPerfMarker.End();
if (result != MLResult.Code.Ok)
{
MLPluginLog.ErrorFormat("MLAudioOutput.RegisterOnVolumeChangeCallback failed to register callback. Reason: {0}", result);
return result;
}
nativeMediaEventCallbackPerfMarker.Begin();
// Attempt to register the native callback for the media event change event.
result = NativeBindings.MLAudioSetMediaEventCallback(HandleOnMediaEventCallback, IntPtr.Zero);
MLResult.DidNativeCallSucceed(result, nameof(NativeBindings.MLAudioSetMediaEventCallback));
nativeMediaEventCallbackPerfMarker.End();
if (result != MLResult.Code.Ok)
{
MLPluginLog.ErrorFormat("MLAudioOutput.MLAudioSetMediaEventCallback failed to register callback. Reason: {0}", result);
return result;
}
return result;
}
///
/// Unregisters a previously registered callback for the device volume change event.
///
///
/// MLResult.Result will be MLResult.Code.Ok if successful.
/// MLResult.Result will be MLResult.Code.UnspecifiedFailure if failed due to internal error.
/// MLResult.Result will be MLResult.Code.InvalidParam if a parameter is invalid.
/// MLResult.Result will be MLResult.Code.PermissionDenied if AudioCaptureMic permission is denied.
/// MLResult.Result will be MLResult.Code.AudioNotImplemented if the function is not implemented.
///
private MLResult.Code UnregisterCallbacks()
{
MLResult.Code result;
nativeSetMasterVolumeCallbackPerfMarker.Begin();
// Unregister the native callback for the volume change event.
result = NativeBindings.MLAudioSetMasterVolumeCallback(null, IntPtr.Zero);
MLResult.DidNativeCallSucceed(result, nameof(NativeBindings.MLAudioSetMasterVolumeCallback));
nativeSetMasterVolumeCallbackPerfMarker.End();
if (result != MLResult.Code.Ok)
{
MLPluginLog.ErrorFormat("MLAudioOutput.UnregisterOnVolumeChangeCallback failed to register callback. Reason: {0}", result);
}
nativeMediaEventCallbackPerfMarker.Begin();
// Attempt to register the native callback for the volume change event.
result = NativeBindings.MLAudioSetMediaEventCallback(null, IntPtr.Zero);
MLResult.DidNativeCallSucceed(result, nameof(NativeBindings.MLAudioSetMediaEventCallback));
nativeMediaEventCallbackPerfMarker.End();
if (result != MLResult.Code.Ok)
{
MLPluginLog.ErrorFormat("MLAudioOutput.RegisterOnMediaEventCallback failed to register callback. Reason: {0}", result);
}
return result;
}
///
/// Get the current audio output device.
///
/// The audio output device.
private MLAudioOutput.Device InternalGetOutputDevice()
{
try
{
nativeGetOutputDevicePerfMarker.Begin();
MLResult.Code result = NativeBindings.MLAudioGetOutputDevice(out Instance.audioDevice);
MLResult.DidNativeCallSucceed(result, nameof(NativeBindings.MLAudioGetOutputDevice));
nativeGetOutputDevicePerfMarker.End();
if (result != MLResult.Code.Ok)
{
MLPluginLog.ErrorFormat("MLAudioOutpu.InternalGetOutputDevice failed to get the audio output device. Reason: {0}", result);
}
}
catch (System.DllNotFoundException)
{
MLPluginLog.Error(this.DllNotFoundError);
throw;
}
return Instance.audioDevice;
}
}
}