This commit is contained in:
2020-08-10 22:23:49 +08:00
parent 6bb6777bb9
commit 19e730574b
265 changed files with 86326 additions and 952 deletions

View File

@@ -0,0 +1,400 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
namespace E7.Native
{
/// <summary>
/// Several properties about the device asked from the native side that might help you.
/// Returned from <see cref="NativeAudio.GetDeviceAudioInformation"/>
///
/// The content of this `struct` changes completely depending on active build platform.
/// You will want to use a preprocessor directive wrapping it.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DeviceAudioInformation
{
#if UNITY_IOS
/// <summary>
/// It is from [AVAudioSessionPortDescription](https://developer.apple.com/documentation/avfoundation/avaudiosessionportdescription).
/// </summary>
public enum IosAudioPortType
{
//---Output---
/// <summary>
/// Line-level output to the dock connector.
/// </summary>
LineOut = 0,
/// <summary>
/// Output to a wired headset.
/// </summary>
Headphones = 1,
/// <summary>
/// Output to a speaker intended to be held near the ear.
/// </summary>
BuiltInReceiver = 2,
/// <summary>
/// Output to the device’s built-in speaker.
/// </summary>
BuiltInSpeaker = 3,
/// <summary>
/// Output to a device via the High-Definition Multimedia Interface (HDMI) specification.
/// </summary>
HDMI = 4,
/// <summary>
/// Output to a remote device over AirPlay.
/// </summary>
AirPlay = 5,
/// <summary>
/// Output to a Bluetooth Low Energy (LE) peripheral.
/// </summary>
BluetoothLE = 6,
/// <summary>
/// Output to a Bluetooth A2DP device.
/// </summary>
BluetoothA2DP = 7,
//---Input---
/// <summary>
/// Line-level input from the dock connector.
/// </summary>
LineIn = 8,
/// <summary>
/// The built-in microphone on a device.
/// </summary>
BuiltInMic = 9,
/// <summary>
/// A microphone that is built-in to a wired headset.
/// </summary>
HeadsetMic = 10,
//---Input-Output---
/// <summary>
/// Input or output on a Bluetooth Hands-Free Profile device.
/// </summary>
BluetoothHFP = 11,
/// <summary>
/// Input or output on a Universal Serial Bus device.
/// </summary>
UsbAudio = 12,
/// <summary>
/// Input or output via Car Audio.
/// </summary>
CarAudio = 13,
}
/// <summary>
/// [iOS] All OUTPUT audio devices currently active.
/// </summary>
/// <remarks>
/// The returned `enum` is native, so on an other platform the available choice completely changes.
/// Use `#if` directive to make this property compile on multiple platforms.
///
/// This is the return value from `[[AVAudioSession sharedInstance] currentRoute]` -> `outputs` -> each item.
/// </remarks>
public IosAudioPortType[] audioDevices { get; private set; }
/// <summary>
/// [iOS] The latency for audio output, measured in seconds.
/// </summary>
/// <remarks>
/// This is shared with Unity, not just for Native Audio, because `AVAudioSession` is a singleton and is shared.
///
/// A result from [this AVAudioSession instance property](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616500-outputlatency).
///
/// For reference on my iPhone SE with device speaker, it is 0.0128750000149012.
/// (Regardless of Project Settings > Audio option selected)
///
/// Value is always specified in seconds; it yields sub-millisecond precision over a range of 10,000 years.
///
/// I don't know how could iOS know its own latency based on current connected device,
/// but I got curious and went to audio shop and debug this number on all the things they let me.
///
/// Hardware Name String Output Latency Brand
/// ---------------------------------------------------------
/// JBL T110BT 0.0825396850705147 JBL
/// JBL Reflect Mini2 0.0825396850705147 JBL
/// NW WS-623 0.0149886617437005 Sony Walkman
/// JBL E45BT 0.111564628779888 JBL
/// MAJOR III BLUETOOTH 0.149297058582306 Marshall
/// JBL Flip 4 0.111564628779888 JBL
/// JBL GO 2 0.0825396850705147 JBL
/// JBL GO 0.0825396850705147 JBL
/// JBL JR POP 0.111564628779888 JBL
/// JBL Charge 4 0.111564628779888 JBL
///
/// Looks like the measurement got several suspicious number, like exact same number across devices,
/// or even some that looks like 10x of the other.
/// So I think it must be calculated from something rather than measured live with fancy technique.
///
/// How is this useful? You could for example, display a warning in the game that your player's device
/// is not suitable to play the game due to high latency.
///
/// However when I test them, most sounds **almost** equal in latency (but all are bad for music games anyways),
/// but that Marshall MAJOR III has obviously much higher latency (almost 3x) than others.
/// Feels much higher than what you see in the data.
///
/// So it shows this number is somewhat reliable, but may not be 100% accurate of the real latency.
/// </remarks>
public double outputLatency { get; private set; }
/// <summary>
/// [iOS] The current audio sample rate, in hertz.
/// </summary>
/// <remarks>
/// This is shared with Unity, not just for Native Audio, because `AVAudioSession`
/// is a singleton and is shared.
///
/// A result from [this AVAudioSession instance property](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616499-samplerate).
///
/// The available range for hardware sample rate is device dependent. It typically ranges from 8000 through 48000 hertz.
/// </remarks>
public double sampleRate { get; private set; }
/// <summary>
/// [iOS] The preferred sample rate, in hertz.
/// </summary>
/// <remarks>
/// This is shared with Unity, not just for Native Audio, because `AVAudioSession` is a singleton and is shared.
///
/// At native side this is freely specifiable, but iOS might give you something else that becomes <see cref="sampleRate"/>.
///
/// A result from [this AVAudioSession instance property](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616543-preferredsamplerate).
/// </remarks>
public double preferredSampleRate { get; private set; }
/// <summary>
/// [iOS] The current I/O buffer duration, in seconds.
/// </summary>
/// <remarks>
/// A result from [this AVAudioSession instance property](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616498-iobufferduration).
///
/// This could be viewed as an another representation of "buffer size" on Android.
/// This is instead in time unit rather than size. And it depends on the current sample rate in order
/// to calculate the resulting buffer size.
///
/// Value is always specified in seconds; it yields sub-millisecond precision over a range of 10,000 years.
///
/// The audio I/O buffer duration is the number of seconds for a single audio input/output cycle.
/// For example, with an I/O buffer duration of 0.005 s, on each audio I/O cycle:
///
/// You receive 0.005 s of audio if obtaining input.
/// You must provide 0.005 s of audio if providing output.
///
/// The typical maximum I/O buffer duration is 0.93 s
/// (corresponding to 4096 sample frames at a sample rate of 44.1 kHz).
/// The minimum I/O buffer duration is at least 0.005 s(256 frames)
/// but might be lower depending on the hardware in use.
///
/// For example if this is 0.01s, at sample rate 24000Hz (What Unity use)
/// it would have to get 0.01s of audio. But compared to 44000Hz rate, that
/// 0.01s of audio is of much less data. By using higher fidelity 44000Hz,
/// the same 0.01s could cause buffer underrun if the device is not fast enough.
///
/// This is shared with Unity, not just for Native Audio, because `AVAudioSession` is a singleton and is shared.
///
/// Here's some behaviour of this number based on my research.
///
/// For reference, with varying Project Settings > Audio options :
/// Best Latency : 0.0106666665524244
/// Good Latency : 0.0213333331048489
/// Best Performance : 0.0426666662096977
///
/// When connected to external audio device like a bluetooth headphone, the number on Best Latency
/// drops to 0.005 but sampling rate moved from 24000 to 44100.
/// This can be interpret as Unity try to make sampling rate compatible with external device,
/// but now must write half less audio seconds because it now have twice as many data.
///
/// By setting <see cref="preferredIOBufferDuration"/> at native side to 0.005
/// (the limit mentioned in the documentation) this became 0.005333.
///
/// When set <see cref="preferredIOBufferDuration"/> back to 0, this became 0.021333. (The same as Good Latency, even though Unity is currently in Best Latency.)
/// However by benchmarking sometimes 0.005 duration do produce worse latency than 0.01, I wonder why..
///
/// </remarks>
public double ioBufferDuration {get; private set;}
/// <summary>
/// [iOS] The preferred I/O buffer duration, in seconds.
/// </summary>
/// <remarks>
/// At native side this is freely specifiable, but iOS might give you something else that becomes <see cref="ioBufferDuration"/>.
///
/// This is shared with Unity, not just for Native Audio, because `AVAudioSession` is a singleton and is shared.
/// This seems to be always 0 in Unity games by default.
///
/// A result from [this AVAudioSession instance property](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616464-preferrediobufferduration).
///
/// Value is always specified in seconds; it yields sub-millisecond precision over a range of 10,000 years.
/// </remarks>
public double preferredIOBufferDuration { get; private set; }
internal const int interopArrayLength = 5;
public DeviceAudioInformation(double[] interopDoubleArray, IosAudioPortType[] portArray)
{
if (interopDoubleArray.Length != interopArrayLength)
{
throw new ArgumentException("The array that fetched data from iOS should be of length " + interopArrayLength);
}
this.outputLatency = interopDoubleArray[0];
this.sampleRate = interopDoubleArray[1];
this.preferredSampleRate = interopDoubleArray[2];
this.ioBufferDuration = interopDoubleArray[3];
this.preferredIOBufferDuration = interopDoubleArray[4];
this.audioDevices = portArray;
}
public override string ToString()
{
return string.Format(
"Audio devices : {0} Output Latency : {1} Sample Rate : {2} Preferred Sample Rate : {3} IO Buffer Duration : {4} Preferred IO Buffer Duration : {5}",
string.Join(", ", this.audioDevices.Select(x => x.ToString()).ToArray()),
outputLatency,
sampleRate,
preferredSampleRate,
ioBufferDuration,
preferredIOBufferDuration
);
}
#endif
#if UNITY_ANDROID
/// <summary>
/// [Android] Only audio matching this sampling rate on a native AudioTrack created with this
/// sampling rate is eligible for fast track playing.
/// </summary>
/// <remarks>
/// This is NOT the sample rate that Native Audio might be currently working with,
/// nor what Unity is using for their own audio source.
/// Just a per-device attribute.
/// </remarks>
public int nativeSamplingRate { get; private set; }
/// <summary>
/// [Android] How large of a buffer that your phone wants to work with.
/// </summary>
/// <remarks>
/// This is NOT the buffer size that Native Audio might be currently working with,
/// nor what Unity is using for their own audio source.
/// Just a per-device attribute.
/// </remarks>
public int optimalBufferSize { get; private set; }
/// <summary>
/// [Android] Indicates a continuous output latency of 45 ms or less.
/// Only valid if the device API >= 23 (6.0, Marshmallow), or else it is always `false`.
/// </summary>
public bool lowLatencyFeature { get; private set; }
/// <summary>
/// [Android] Indicates a continuous round-trip latency of 20 ms or less.
/// Only valid if the device API >= 23 (6.0, Marshmallow), or else it is always `false`.
/// </summary>
public bool proAudioFeature { get; private set; }
/// <summary>
/// [Android] All OUTPUT devices currently active.
/// </summary>
/// <remarks>
/// The returned `enum` is native, so on an other platform the available choice completely changes.
/// Use `#if` directive to make this property compile on multiple platforms.
///
/// These are returnd from `audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);` API.
/// Only valid if the device API >= 23 (6.0, Marshmallow), or else it is always `null`.
/// </remarks>
public AndroidAudioDeviceType[] audioDevices { get; private set; }
/// <summary>
/// I just copied everything from [AudioDeviceInfo](https://developer.android.com/reference/android/media/AudioDeviceInfo.html#constants_2).
/// </summary>
public enum AndroidAudioDeviceType
{
TYPE_AUX_LINE = 19,
TYPE_BLUETOOTH_A2DP = 8,
TYPE_BLUETOOTH_SCO = 7,
TYPE_BUILTIN_EARPIECE = 1,
TYPE_BUILTIN_MIC = 15,
TYPE_BUILTIN_SPEAKER = 2,
TYPE_BUS = 21,
TYPE_DOCK = 13,
TYPE_FM = 14,
TYPE_FM_TUNER = 16,
TYPE_HDMI = 9,
TYPE_HDMI_ARC = 10,
TYPE_HEARING_AID = 23,
TYPE_IP = 20,
TYPE_LINE_ANALOG = 5,
TYPE_LINE_DIGITAL = 6,
TYPE_TELEPHONY = 18,
TYPE_TV_TUNER = 17,
TYPE_UNKNOWN = 0,
TYPE_USB_ACCESSORY = 12,
TYPE_USB_DEVICE = 11,
TYPE_USB_HEADSET = 22,
TYPE_WIRED_HEADPHONES = 4,
TYPE_WIRED_HEADSET = 3,
}
public DeviceAudioInformation(AndroidJavaObject jo)
{
AndroidJavaClass versionClass = new AndroidJavaClass("android/os/Build$VERSION");
int sdkLevel = versionClass.GetStatic<int>("SDK_INT");
this.nativeSamplingRate = jo.Get<int>("nativeSamplingRate");
this.optimalBufferSize = jo.Get<int>("optimalBufferSize");
this.lowLatencyFeature = jo.Get<bool>("lowLatencyFeature");
this.proAudioFeature = jo.Get<bool>("proAudioFeature");
if (sdkLevel >= 23)
{
//This one is a Java array, we need to do JNI manually to each elements
AndroidJavaObject outputDevicesJo = jo.Get<AndroidJavaObject>("outputDevices");
IntPtr outputDevicesRaw = outputDevicesJo.GetRawObject();
int outputDeviceAmount = AndroidJNI.GetArrayLength(outputDevicesRaw);
this.audioDevices = new AndroidAudioDeviceType[outputDeviceAmount];
for (int i = 0; i < outputDeviceAmount; i++)
{
IntPtr outputDevice = AndroidJNI.GetObjectArrayElement(outputDevicesRaw, i);
IntPtr audioDeviceInfoClass = AndroidJNI.GetObjectClass(outputDevice);
IntPtr getTypeMethod = AndroidJNIHelper.GetMethodID(audioDeviceInfoClass, "getType");
int type = AndroidJNI.CallIntMethod(outputDevice, getTypeMethod, new jvalue[] { });
this.audioDevices[i] = (AndroidAudioDeviceType)type;
}
}
else
{
this.audioDevices = new AndroidAudioDeviceType[0];
}
//Debug.Log(this.ToString());
}
public override string ToString()
{
return string.Format("Native Sampling Rate: {0} | Optimal Buffer Size: {1} | Low Latency Feature: {2} | Pro Audio Feature: {3} | Output devices : {4}",
nativeSamplingRate, optimalBufferSize, lowLatencyFeature, proAudioFeature,
string.Join(", ", this.audioDevices.Select(x => x.ToString()).ToArray()));
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c45738ed5eb948f78259c0b922e14dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
{
"name": "E7.NativeAudio",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6e1ceb5b6422b434084667bb46a254f6
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
namespace E7.Native
{
/// <summary>
/// An `interface` to use with <see cref="NativeAudio.GetNativeSourceAuto(INativeSourceSelector)"/>
/// You can implement your own logic that derives an index depending on some internal state.
/// </summary>
/// <remarks>
/// You can for example create `class MyKickDrumSelector : INativeSourceSelector`
/// and `class MySnareSelector : INativeSourceSelector`.
///
/// The target is that the kick is short, but often used. You want it to use native source index 0 exclusively.
/// The snares keep using index 1 and 2 to not have to trouble the kick drum.
///
/// Code the logic such that :
/// - The kick drum one keeps returning `0` in its <see cref="NextNativeSourceIndex"/> implementation.
/// - The snare one return `1` and `2` alternately on each <see cref="NextNativeSourceIndex"/> call.
/// </remarks>
public interface INativeSourceSelector
{
/// <summary>
/// Each call could return a different native source index by your own logic.
/// Native Audio will call this once on each <see cref="NativeAudio.GetNativeSourceAuto(INativeSourceSelector)"/>
///
/// If the returned `int` turns out to be an invalid index at native side, it has a fallback to round-robin
/// native source selection.
/// </summary>
int NextNativeSourceIndex();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3913b6a46eaf044a19486197059432cc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,99 @@
namespace E7.Native
{
public static partial class NativeAudio
{
/// <summary>
/// An option for <see cref="NativeAudio.Initialize(InitializationOptions)"/>.
/// Because it is a `struct`, start making it from <see cref="defaultOptions"/> to get a good default values.
///
/// This class is currently only contains options for Android. iOS options are fixed.
/// </summary>
public struct InitializationOptions
{
/// <summary>
/// A good starting values to create custom options. A `struct` cannot have default value on `new`.
/// </summary>
public static readonly InitializationOptions defaultOptions = new InitializationOptions
{
androidAudioTrackCount = 3,
androidMinimumBufferSize = -1,
preserveOnMinimize = false
};
/// <summary>
/// How many native sources to request for Android. Default to 3 on <see cref="defaultOptions"/>.
/// It directly translates to maximum concurrency you can have while staying unmixed.
///
/// Please read [Problems on number of native sources](https://exceed7.com/native-audio/theories/ways-around-latency.html#problems-on-number-of-native-sources)
/// if you would like to increase this and learn what risks you are getting into.
/// </summary>
public int androidAudioTrackCount;
/// <summary>
/// - If `-1`, it uses buffer size exactly equal to device's native buffer size.
/// - Any number lower than device's native buffer size that is not `-1` will be
/// clamped to device's native buffer size as the lowest possible.
/// - Any number larger than device's native buffer size, you will **not** get exactly that specified buffer size.
/// Instead, we increase from device buffer size by multiple of itself until over the specified size,
/// then you get that size. (Hence the name "minimum")
/// </summary>
/// <remarks>
/// [See the reason of the need to increase by multiple](https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size).
///
/// Smaller buffer size means better latency.
/// Therefore -1 means it is the best latency-wise. (Will not modify the buffer size asked from the device)
///
/// But if you experiences audio glitches, it might be that the device could not write in time
/// when the first buffer runs out of data, the "buffer underrun". (Native Audio uses double buffering)
/// This might be because of device reports a buffer size too low for itself to handle.
/// This is in some Chinese phones apparently.
///
/// Example : Specified `256`
///
/// - Xperia Z5 : Native buffer size : 192 -> what you get : 384
/// - Lenovo A..something : Native buffer size : 620 -> what you get : 620
/// </remarks>
public int androidMinimumBufferSize;
/// <summary>
/// [Android]
/// - If `false` (default on <see cref="defaultOptions"/>), on <see cref="Initialize"/> the native side
/// will remember your request's spec. On minimize it will dispose all the sources
/// (and in turn stopping them). On coming back it will reinitialize with the same spec.
///
/// - If `true` the allocated native sources will not be freed when minimize the app.
/// (The Unity ones do freed and request a new one on coming back)
///
/// [iOS] No effect, iOS's native sources is already minimize-compatible
/// but its playing-when-minimized is prevented by the app's build option.
/// </summary>
/// <remarks>
/// [Android]
///
/// This make it possible for audio played with Native Audio to play while minimizing the app,
/// and also to not spend time disposing and allocating sources again.
///
/// However this is not good since it adds "wake lock" to your game.
/// With `adb shell dumpsys power` while your game is minimized after using Native Audio
/// you will see something like ` PARTIAL_WAKE_LOCK 'AudioMix' ACQ=-27s586ms(uid= 1041 ws= WorkSource{ 10331})`.
/// Meaning that the OS have to keep the audio mix alive all the time.
/// Not to mention most games do not really want this behaviour.
///
/// Most gamers I saw also minimized the game and sometimes forgot to close them off.
/// This cause not only battery drain when there is a wake lock active,
/// but also when the lock turns into `LONG` state it will show up as a warning in Google Play Store,
/// as it could detect that an app has a
/// [Stuck partial wake lock](https://developer.android.com/topic/performance/vitals/wakelock) or not.
///
/// [iOS]
///
/// If you want the audio to continue to be heard in minimize,
/// use "Behaviour in background" set as Custom - Audio in Unity Player Settings then
/// [follow this thread](https://forum.unity.com/threads/how-do-i-get-the-audio-running-in-background-ios.319602/)
/// to setup the `AVAudioSession` to correct settings.
/// </remarks>
public bool preserveOnMinimize;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 585f53fdc3f274294a51e49d54075eef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
namespace E7.Native
{
public static partial class NativeAudio
{
/// <summary>
/// An option for <see cref="NativeAudio.Load(UnityEngine.AudioClip, LoadOptions)"/>.
/// Because it is a `struct`, start making it from <see cref="defaultOptions"/> to get a good default values.
/// </summary>
public struct LoadOptions
{
/// <summary>
/// A good starting values to create custom options. A `struct` cannot have default value on `new`.
/// </summary>
public static readonly LoadOptions defaultOptions = new LoadOptions
{
resamplingQuality = ResamplingQuality.SINC_FASTEST,
};
/// <summary>
/// Determines what resampling quality for [Secret Rabbit Code](http://www.mega-nerd.com/SRC/) to use.
/// </summary>
public enum ResamplingQuality
{
//SINC_BEST_QUALITY = 0,
//SINC_MEDIUM_QUALITY = 1,
/// <summary>
/// Use a coefficients from sinc wave for reconstruction. Takes a bit of time to complete.
/// </summary>
/// <remarks>
/// Some benchmark : Resampling a WAV file of about 2MB (this is quite big) from 44.1kHz
/// to 48kHz freezes the screen for 0.4 seconds on an Xperia Z5.
/// (See https://ccrma.stanford.edu/~jos/resample/)
/// </remarks>
SINC_FASTEST = 2,
/// <summary>
/// Just use the previous value for any missing data.
/// It is the fastest resampling method but might sounds poor.
/// </summary>
ZERO_ORDER_HOLD = 3,
/// <summary>
/// The missing value will be linearly interpolated.
/// Faster than sinc resampling.
/// </summary>
LINEAR = 4,
};
/// <summary>
/// The quality which `libsamplerate` will use to resample your audio to match the device's native rate.
/// Default to <see cref="ResamplingQuality.SINC_FASTEST"/> on <see cref="defaultOptions"/>
/// </summary>
/// <remarks>
/// Two top quality setting has been removed from the source code since the sinc wave constant coefficients
/// are needed and could potentially make Native Audio sized at 0.79MB (For medium quality) or 9.2MB (For best quality)
///
/// If you really need it, you can uncomment and then go modify back the source and recompile with the missing coefficients.
/// </remarks>
public ResamplingQuality resamplingQuality;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3dd4b72c4b617429b9e71fba783d9f9c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,561 @@
using UnityEngine;
using System;
using System.Runtime.InteropServices;
using System.Linq;
namespace E7.Native
{
/// <summary>
/// The most important class, contains `static` methods that are used to command the native side.
/// </summary>
public static partial class NativeAudio
{
/// <summary>
/// Returns `true` after calling <see cref="NativeAudio.Initialize"/> successfully, meaning that
/// we have a certain amount of native sources ready for use at native side.
///
/// It is able to turn back to `false` if you call <see cref="NativeAudio.Dispose"/> to return native sources back
/// to the OS.
/// </summary>
public static bool Initialized { get; private set; }
private static void AssertInitialized()
{
if (!Initialized)
{
throw new InvalidOperationException("You cannot use Native Audio while in uninitialized state.");
}
}
/// <summary>
/// - If in Editor, it is instantly unsupported no matter what build platform selected.
/// - If not in Editor, it is `true` only on Android and iOS.
/// </summary>
public static bool OnSupportedPlatform
{
get
{
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS)
return true;
#else
return false;
#endif
}
}
/// <summary>
/// [iOS] Initializes OpenAL. 15 OpenAL native sources will be allocated all at once.
/// It is not possible to initialize again on iOS. (Nothing will happen)
///
/// [Android] Initializes OpenSL ES. 1 OpenSL ES "Engine" and a number of native sources `AudioPlayer` object
/// (and in turn native `AudioTrack`) will be allocated all at once.
///
/// See <see cref="NativeAudio.Initialize(InitializationOptions)"/> overload how to customize your intialization.
/// </summary>
/// <remarks>
/// - More about this limit : https://developer.android.com/ndk/guides/audio/opensl/opensl-for-android
/// - And my own research here : https://gametorrahod.com/androids-native-audio-primer-for-unity-developers
/// </remarks>
public static void Initialize()
{
Initialize(InitializationOptions.defaultOptions);
}
private static NotSupportedException NotSupportedThrow()
{
return new NotSupportedException("You cannot use Native Audio on unsupported platform, including in editor which counts as Windows or macOS.");
}
/// <summary>
/// [iOS] Initializes OpenAL. 15 OpenAL native sources will be allocated all at once.
/// It is not possible to initialize again on iOS. (Nothing will happen)
///
/// [Android] Initializes OpenSL ES. 1 OpenSL ES "Engine" and a number of native sources `AudioPlayer` object
/// (and in turn native `AudioTrack`) will be allocated all at once.
///
/// See <see cref="NativeAudio.Initialize(InitializationOptions)"/> overload how to customize your intialization.
/// </summary>
/// <remarks>
/// - More about this limit : https://developer.android.com/ndk/guides/audio/opensl/opensl-for-android
/// - And my own research here : https://gametorrahod.com/androids-native-audio-primer-for-unity-developers
/// </remarks>
/// <param name="initializationOptions">
/// Customize your initialization.
/// Start making it from <see cref="InitializationOptions.defaultOptions"/>
/// </param>
/// <exception cref="NotSupportedException">Thrown when you initialize in Editor or something other than
/// iOS or Android at runtime.</exception>
public static void Initialize(InitializationOptions initializationOptions)
{
if (!OnSupportedPlatform)
{
throw NotSupportedThrow();
}
else
{
//Now it is possible to initialize again with different option on Android. It would dispose and reallocate native sources.
#if UNITY_IOS
if (Initialized) return;
#endif
#if UNITY_IOS
int errorCode = _Initialize();
if (errorCode == -1)
{
throw new System.Exception("There is an error initializing Native Audio occured at native side.");
}
//There is also a check at native side but just to be safe here.
Initialized = true;
#elif UNITY_ANDROID
int errorCode = AndroidNativeAudio.CallStatic<int>(AndroidInitialize, initializationOptions.androidAudioTrackCount, initializationOptions.androidMinimumBufferSize, initializationOptions.preserveOnMinimize);
if(errorCode == -1)
{
throw new System.Exception("There is an error initializing Native Audio occured at native side.");
}
Initialized = true;
#endif
}
}
/// <summary>
/// [Android] Undo the <see cref="Initialize"/>.
/// It doesn't affect any loaded audio, just dispose all the native sources returning them to OS and make them
/// available for other applications. You still have to unload each audio.
/// Disposing twice is safe, it does nothing.
///
/// [iOS] Disposing doesn't work.
///
/// [Editor] This is a no-op. It is safe to call and nothing will happen.
/// </summary>
public static void Dispose()
{
#if UNITY_ANDROID
if (Initialized)
{
AndroidNativeAudio.CallStatic(AndroidDispose);
Initialized = false;
}
#elif UNITY_IOS
#else
throw NotSupportedThrow();
#endif
}
/// <summary>
/// Loads by copying Unity-imported <see cref="AudioClip"/>'s raw audio memory to native side.
/// You are free to unload the <see cref="AudioClip"/>'s audio data without affecting what's loaded at the native side after this.
///
/// [Editor] This method is a stub and returns `null`.
/// </summary>
/// <remarks>
/// If you did not <see cref="NativeAudio.Initialize"/> yet, it will initialize with no <see cref="InitializationOptions"/>.
/// You cannot load audio while uninitialized.
///
/// Hard requirements :
///
/// - Load type MUST be Decompress On Load so Native Audio could read raw PCM byte array from your compressed audio.
/// - If you use Load In Background, you must call <see cref="AudioClip.LoadAudioData"/> beforehand and ensure that <see cref="AudioClip.loadState"/> is <see cref="AudioDataLoadState.Loaded"/> before calling <see cref="NativeAudio.Load"/>. Otherwise it would throw an exception. If you are not using <see cref="AudioClip.loadInBackground"/> but also not using <see cref="AudioClip.preloadAudioData"/>, Native Audio can load for you if not yet loaded.
/// - Must not be <see cref="AudioClip.ambisonic"/>.
///
/// It supports all compression format, force to mono, overriding to any sample rate, and quality slider.
///
/// [iOS] Loads an audio into OpenAL's output audio buffer. (Max 256)
/// This buffer will be paired to one of 15 OpenAL source when you play it.
///
/// [Android] Loads an audio into a `short*` array at unmanaged native side.
/// This array will be pushed into one of available `SLAndroidSimpleBufferQueue` when you play it.
///
/// The resampling of audio will occur at this moment to match your player's device native rate.
///
/// The SLES audio player must be created to match the device rate
/// to enable the special "fast path" audio.
/// What's left is to make our audio compatible with that fast path player,
/// which the resampler will take care of.
///
/// You can change the sampling quality of SRC (`libsamplerate`) library on a
/// per-audio basis with the <see cref="NativeAudio.Load(AudioClip, LoadOptions)"/> overload.
/// </remarks>
/// <param name="audioClip">
/// Hard requirements :
/// - Load type MUST be Decompress On Load so Native Audio could read raw PCM byte array from your compressed audio.
/// - If you use Load In Background, you must call <see cref="AudioClip.LoadAudioData"/> beforehand and ensure that <see cref="AudioClip.loadState"/> is <see cref="AudioDataLoadState.Loaded"/> before calling <see cref="NativeAudio.Load"/>. Otherwise it would throw an exception. If you are not using <see cref="AudioClip.loadInBackground"/> but also not using <see cref="AudioClip.preloadAudioData"/>, Native Audio can load for you if not yet loaded.
/// - Must not be <see cref="AudioClip.ambisonic"/>.
/// </param>
/// <returns>
/// An audio buffer pointer for use with <see cref="NativeSource.Play(NativeAudioPointer)"/>.
/// Get the source from <see cref="NativeAudio.GetNativeSource(int)"/>
/// </returns>
/// <exception cref="Exception">Thrown when some unexpected exception at native side loading occurs.</exception>
/// <exception cref="NotSupportedException">Thrown when you have prohibited settings on your <see cref="AudioClip"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when you didn't manually load your <see cref="AudioClip"/> when it is not set to load in background.</exception>
public static NativeAudioPointer Load(AudioClip audioClip)
{
return Load(audioClip, LoadOptions.defaultOptions);
}
/// <summary>
/// Loads by copying Unity-imported <see cref="AudioClip"/>'s raw audio memory to native side.
/// You are free to unload the <see cref="AudioClip"/>'s audio data without affecting what's loaded at the native side after this.
///
/// [Editor] This method is a stub and returns `null`.
/// </summary>
/// <remarks>
/// If you did not <see cref="NativeAudio.Initialize"/> yet, it will initialize with no <see cref="InitializationOptions"/>.
/// You cannot load audio while uninitialized.
///
/// Hard requirements :
///
/// - Load type MUST be Decompress On Load so Native Audio could read raw PCM byte array from your compressed audio.
/// - If you use Load In Background, you must call <see cref="AudioClip.LoadAudioData"/> beforehand and ensure that <see cref="AudioClip.loadState"/> is <see cref="AudioDataLoadState.Loaded"/> before calling <see cref="NativeAudio.Load"/>. Otherwise it would throw an exception. If you are not using <see cref="AudioClip.loadInBackground"/> but also not using <see cref="AudioClip.preloadAudioData"/>, Native Audio can load for you if not yet loaded.
/// - Must not be <see cref="AudioClip.ambisonic"/>.
///
/// It supports all compression format, force to mono, overriding to any sample rate, and quality slider.
///
/// [iOS] Loads an audio into OpenAL's output audio buffer. (Max 256)
/// This buffer will be paired to one of 15 OpenAL source when you play it.
///
/// [Android] Loads an audio into a `short*` array at unmanaged native side.
/// This array will be pushed into one of available `SLAndroidSimpleBufferQueue` when you play it.
///
/// The resampling of audio will occur at this moment to match your player's device native rate.
///
/// The SLES audio player must be created to match the device rate
/// to enable the special "fast path" audio.
/// What's left is to make our audio compatible with that fast path player,
/// which the resampler will take care of.
///
/// You can change the sampling quality of SRC (`libsamplerate`) library on a
/// per-audio basis with the <see cref="NativeAudio.Load(AudioClip, LoadOptions)"/> overload.
/// </remarks>
/// <param name="audioClip">
/// Hard requirements :
/// - Load type MUST be Decompress On Load so Native Audio could read raw PCM byte array from your compressed audio.
/// - If you use Load In Background, you must call <see cref="AudioClip.LoadAudioData"/> beforehand and ensure that <see cref="AudioClip.loadState"/> is <see cref="AudioDataLoadState.Loaded"/> before calling <see cref="NativeAudio.Load"/>. Otherwise it would throw an exception. If you are not using <see cref="AudioClip.loadInBackground"/> but also not using <see cref="AudioClip.preloadAudioData"/>, Native Audio can load for you if not yet loaded.
/// - Must not be <see cref="AudioClip.ambisonic"/>.
/// </param>
/// <param name="loadOptions">Customize your load. Start creating your option from <see cref="LoadOptions.defaultOptions"/>.</param>
/// <returns>
/// An audio buffer pointer for use with <see cref="NativeSource.Play(NativeAudioPointer)"/>.
/// Get the source from <see cref="NativeAudio.GetNativeSource(int)"/>
/// </returns>
/// <exception cref="Exception">Thrown when some unexpected exception at native side loading occurs.</exception>
/// <exception cref="NotSupportedException">Thrown when you have prohibited settings on your <see cref="AudioClip"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when you didn't manually load your <see cref="AudioClip"/> when it is not set to load in background.</exception>
public static NativeAudioPointer Load(AudioClip audioClip, LoadOptions loadOptions)
{
AssertAudioClip(audioClip);
AssertInitialized();
#if UNITY_IOS || UNITY_ANDROID
//We have to wait for GC to collect this big array, or you could do `GC.Collect()` immediately after.
short[] shortArray = AudioClipToShortArray(audioClip);
GCHandle shortArrayPinned = GCHandle.Alloc(shortArray, GCHandleType.Pinned);
#endif
#if UNITY_IOS
int startingIndex = _SendByteArray(shortArrayPinned.AddrOfPinnedObject(), shortArray.Length * 2, audioClip.channels, audioClip.frequency, loadOptions.resamplingQuality);
shortArrayPinned.Free();
if (startingIndex == -1)
{
throw new Exception("Error loading NativeAudio with AudioClip named : " + audioClip.name);
}
else
{
float length = _LengthByAudioBuffer(startingIndex);
return new NativeAudioPointer(audioClip.name, startingIndex, length);
}
#elif UNITY_ANDROID
//The native side will interpret short array as byte array, thus we double the length.
int startingIndex = sendByteArray(shortArrayPinned.AddrOfPinnedObject(), shortArray.Length * 2, audioClip.channels, audioClip.frequency, loadOptions.resamplingQuality);
shortArrayPinned.Free();
if(startingIndex == -1)
{
throw new Exception("Error loading NativeAudio with AudioClip named : " + audioClip.name);
}
else
{
float length = lengthByAudioBuffer(startingIndex);
return new NativeAudioPointer(audioClip.name, startingIndex, length);
}
#else
throw NotSupportedThrow();
#endif
}
/// <summary>
/// (**ADVANCED**) Loads an audio from `StreamingAssets` folder's desination at runtime.
/// Most of the case you should use the <see cref="NativeAudio.Load(AudioClip)"/> overload instead.
///
/// It only supports `.wav` PCM 16-bit format, stereo or mono,
/// in any sampling rate since it will be resampled to fit the device.
/// </summary>
/// <param name="streamingAssetsRelativePath">If the file is `SteamingAssets/Hit.wav` use "Hit.wav" (WITH the extension).</param>
/// <exception cref="System.IO.FileLoadException">Thrown when some unexpected exception at native side loading occurs.</exception>
/// <returns>
/// An audio buffer pointer for use with <see cref="NativeSource.Play(NativeAudioPointer)"/>.
/// Get the source from <see cref="NativeAudio.GetNativeSource(int)"/>
/// </returns>
public static NativeAudioPointer Load(string streamingAssetsRelativePath)
{
return Load(streamingAssetsRelativePath, LoadOptions.defaultOptions);
}
/// <summary>
/// (**ADVANCED**) Loads an audio from `StreamingAssets` folder's desination at runtime.
/// Most of the case you should use the <see cref="NativeAudio.Load(AudioClip)"/> overload instead.
///
/// It only supports `.wav` PCM 16-bit format, stereo or mono,
/// in any sampling rate since it will be resampled to fit the device.
/// </summary>
/// <param name="streamingAssetsRelativePath">If the file is `SteamingAssets/Hit.wav` use "Hit.wav" (WITH the extension).</param>
/// <param name="loadOptions">Customize your load. Start creating your option from <see cref="LoadOptions.defaultOptions"/>.</param>
/// <exception cref="System.IO.FileLoadException">Thrown when some unexpected exception at native side loading occurs.</exception>
/// <returns>
/// An audio buffer pointer for use with <see cref="NativeSource.Play(NativeAudioPointer)"/>.
/// Get the source from <see cref="NativeAudio.GetNativeSource(int)"/>
/// </returns>
public static NativeAudioPointer Load(string streamingAssetsRelativePath, LoadOptions loadOptions)
{
AssertInitialized();
if (System.IO.Path.GetExtension(streamingAssetsRelativePath).ToLower() == ".ogg")
{
throw new NotSupportedException("Loading via StreamingAssets does not support OGG. Please use the AudioClip overload and set the import settings to Vorbis.");
}
#if UNITY_IOS
int startingIndex = _LoadAudio(streamingAssetsRelativePath, (int)loadOptions.resamplingQuality);
if (startingIndex == -1)
{
throw new System.IO.FileLoadException("Error loading audio at path : " + streamingAssetsRelativePath + " Please check if that audio file really exist relative to StreamingAssets folder or not. Remember that you must include the file's extension as well.", streamingAssetsRelativePath);
}
else
{
float length = _LengthByAudioBuffer(startingIndex);
return new NativeAudioPointer(streamingAssetsRelativePath, startingIndex, length);
}
#elif UNITY_ANDROID
int startingIndex = AndroidNativeAudio.CallStatic<int>(AndroidLoadAudio, streamingAssetsRelativePath, (int)loadOptions.resamplingQuality);
if(startingIndex == -1)
{
throw new System.IO.FileLoadException("Error loading audio at path : " + streamingAssetsRelativePath + " Please check if that audio file really exist relative to StreamingAssets folder or not. Remember that you must include the file's extension as well.", streamingAssetsRelativePath);
}
else
{
float length = lengthByAudioBuffer(startingIndex);
return new NativeAudioPointer(streamingAssetsRelativePath, startingIndex, length);
}
#else
throw NotSupportedThrow();
#endif
}
private static void AssertAudioClip(AudioClip audioClip)
{
if(audioClip.loadType != AudioClipLoadType.DecompressOnLoad)
{
throw new NotSupportedException(string.Format("Your audio clip {0} load type is not Decompress On Load but {1}. Native Audio needs to read the raw PCM data by that import mode.", audioClip.name, audioClip.loadType));
}
if(audioClip.channels != 1 && audioClip.channels != 2)
{
throw new NotSupportedException(string.Format("Native Audio only supports mono or stereo. Your audio {0} has {1} channels", audioClip.name, audioClip.channels));
}
if(audioClip.ambisonic)
{
throw new NotSupportedException("Native Audio does not support ambisonic audio!");
}
if(audioClip.loadState != AudioDataLoadState.Loaded && audioClip.loadInBackground)
{
throw new InvalidOperationException("Your audio is not loaded yet while having the import settings Load In Background. Native Audio cannot wait for loading asynchronously for you and it would results in an empty audio. To keep Load In Background import settings, call `audioClip.LoadAudioData()` beforehand and ensure that `audioClip.loadState` is `AudioDataLoadState.Loaded` before calling `NativeAudio.Load`, or remove Load In Background then Native Audio could load it for you.");
}
}
private static short[] AudioClipToShortArray(AudioClip audioClip)
{
if (audioClip.loadState != AudioDataLoadState.Loaded)
{
if (!audioClip.LoadAudioData())
{
throw new Exception(string.Format("Loading audio {0} failed!", audioClip.name));
}
}
float[] data = new float[audioClip.samples * audioClip.channels];
audioClip.GetData(data, 0);
//Convert to 16-bit PCM
short[] shortArray = new short[audioClip.samples * audioClip.channels];
for(int i = 0; i < shortArray.Length; i++)
{
shortArray[i] = (short)(data[i] * short.MaxValue);
}
return shortArray;
}
/// <summary>
/// Get a native source in order to play an audio or control an audio currently played on it.
/// You can keep and cache the returned native source reference and keep using it.
///
/// This method is for when you want a specific index of native source you would like to play on.
/// </summary>
/// <remarks>
/// It checks with the native side if
/// a specified <paramref name="nativeSourceIndex"/> is valid or not before returning a native source
/// interfacing object to you. If not, it has a fallback to round-robin native source selection.
///
/// Refer to [Selecting native sources](https://exceed7.com/native-audio/how-to-use/selecting-native-sources.html)
/// on how to strategize your native source index usage depending on your audio.
/// </remarks>
/// <param name="nativeSourceIndex">
/// Specify a zero-indexed native source that you want. If at <see cref="NativeAudio.Initialize"/> you
/// requested 3, then valid numbers here are : 0, 1, and 2.
///
/// If this index turns out to be an invalid index at native side, it has a fallback to round-robin
/// native source selection.
/// </param>
/// <returns>
/// Native source representation you can use it to play audio.
///
/// If <paramref name="nativeSourceIndex"/> used was invalid,
/// then this is a result of fallback round-robin native source selection.
/// </returns>
public static NativeSource GetNativeSource(int nativeSourceIndex)
{
#if UNITY_ANDROID
return new NativeSource(NativeAudio.getNativeSource(nativeSourceIndex));
#elif UNITY_IOS
return new NativeSource(NativeAudio._GetNativeSource(nativeSourceIndex));
#else
throw NotSupportedThrow();
#endif
}
/// <summary>
/// Get a native source in order to play an audio or control an audio currently played on it.
/// You can keep and cache the returned native source reference and keep using it.
///
/// Unlike <see cref="GetNativeSource(int)"/>,
/// this method is for when you just want to play an audio without much care about stopping
/// a previously played audio on any available native source.
///
/// It selects a native source by round-robin algorithm, just select the next index
/// from the previous play.
/// </summary>
/// <remarks>
/// Refer to [Selecting native sources](https://exceed7.com/native-audio/how-to-use/selecting-native-sources.html)
/// on how to strategize your native source index usage depending on your audio.
/// </remarks>
/// <returns>
/// Native source representation you can use it to play audio resulting from round-robin selection.
/// </returns>
public static NativeSource GetNativeSourceAuto()
{
#if UNITY_ANDROID
return new NativeSource(NativeAudio.getNativeSource(-1));
#elif UNITY_IOS
return new NativeSource(NativeAudio._GetNativeSource(-1));
#else
throw NotSupportedThrow();
#endif
}
/// <summary>
/// Get a native source in order to play an audio or control an audio currently played on it.
/// You can keep and cache the returned native source reference and keep using it.
///
/// Like <see cref="GetNativeSource(int)"/>, this method is for when you want a specific index
/// of native source to play. But unlike that, you can create your own "index returning object"
/// that implements <see cref="INativeSourceSelector"/>. Making it more systematic for you.
/// </summary>
/// <remarks>
/// You can have internal state inside it if it is a `class`, you can emulate the default
/// round-robin native source selection, for example.
///
/// Refer to [Selecting native sources](https://exceed7.com/native-audio/how-to-use/selecting-native-sources.html)
/// on how to strategize your native source index usage depending on your audio.
/// </remarks>
/// <returns>
/// Native source representation you can use it to play audio, resulting from an index that
/// Native Audio got from calling <see cref="INativeSourceSelector.NextNativeSourceIndex"/> on
/// <paramref name="nativeSourceSelector"/>.
/// </returns>
public static NativeSource GetNativeSourceAuto(INativeSourceSelector nativeSourceSelector)
{
#if UNITY_ANDROID
var index = nativeSourceSelector.NextNativeSourceIndex();
return new NativeSource(NativeAudio.getNativeSource(index));
#elif UNITY_IOS
var index = nativeSourceSelector.NextNativeSourceIndex();
return new NativeSource(NativeAudio._GetNativeSource(index));
#else
throw NotSupportedThrow();
#endif
}
/// <summary>
/// Ask the phone about its audio capabilities.
///
/// The returned `struct` has different properties depending on platform.
/// You should put preprocessor directive (`#if UNITY_ANDROID` and so on) over the returned object
/// if you are going to access any of its fields. Or else it would be an error if you switch your build platform.
///
/// [Editor] Does not work, returns default value of <see cref="DeviceAudioInformation"/>.
/// </summary>
public static DeviceAudioInformation GetDeviceAudioInformation()
{
#if UNITY_ANDROID
var jo = AndroidNativeAudio.CallStatic<AndroidJavaObject>(AndroidGetDeviceAudioInformation);
return new DeviceAudioInformation(jo);
#elif UNITY_IOS
double[] interopArray = new double[DeviceAudioInformation.interopArrayLength];
int[] portArray = Enumerable.Repeat(-1, 20).ToArray();
var interopArrayHandle = GCHandle.Alloc(interopArray, GCHandleType.Pinned);
var portArrayHandle = GCHandle.Alloc(portArray, GCHandleType.Pinned);
_GetDeviceAudioInformation(interopArrayHandle.AddrOfPinnedObject(), portArrayHandle.AddrOfPinnedObject());
portArrayHandle.Free();
return new DeviceAudioInformation(
interopDoubleArray: interopArray,
portArray: portArray.Where(x => x != -1).Cast<DeviceAudioInformation.IosAudioPortType>().ToArray()
);
#else
return default(DeviceAudioInformation);
#endif
}
/// <summary>
/// (**EXPERIMENTAL**) Native Audio will load a small silent wav and perform various stress test for about 1 second.
/// Your player won't be able to hear anything, but recommended to do it when there's no other workload running because it will also measure FPS.
///
/// The test will be asynchronous because it has to wait for frame to play the next audio. Yield wait for the result with the returned <see cref="NativeAudioAnalyzer"/>.
/// This is a component of a new game object created to run a test coroutine on your scene.
///
/// If your game is in a yieldable routine, use `yield return new WaitUntil( () => analyzer.Analyzed );' it will wait a frame until that is `true`.
/// If not, you can do a blocking wait with a `while` loop on `analyzer.Analyzed == false`.
///
/// You must have initialized Native Audio before doing the analysis or else Native Audio will initialize with default options.
/// (Remember you cannot initialize twice to fix initialization options)
///
/// By the analysis result you can see if the frame rate drop while using Native Audio or not. I have fixed most of the frame rate drop problem I found.
/// But if there are more obscure devices that drop frame rate, this method can check it at runtime and by the returned result you can stop using Native Audio
/// and return to Unity <see cref="AudioSource"/>.
/// </summary>
public static NativeAudioAnalyzer SilentAnalyze()
{
AssertInitialized();
#if UNITY_ANDROID
var go = new GameObject("NativeAudioAnalyzer");
NativeAudioAnalyzer sa = go.AddComponent<NativeAudioAnalyzer>();
sa.Analyze();
return sa;
#else
throw NotSupportedThrow();
#endif
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2e85b7e6d8b444cff907a1e4e2d3811f
timeCreated: 1509542882
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,141 @@
// Native Audio
// 5argon - Exceed7 Experiments
// Problems/suggestions : 5argon@exceed7.com
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Diagnostics;
using System.Linq;
namespace E7.Native
{
/// <summary>
/// Result from running <see cref="NativeAudioAnalyzer.Analyze"/>.
/// </summary>
public class NativeAudioAnalyzerResult
{
public float averageFps;
}
/// <summary>
/// The game object with this component is able to test native audio over several frames.
/// </summary>
public class NativeAudioAnalyzer : MonoBehaviour
{
/// <summary>
/// You can wait for the result on this. Then after it is done, `AnalysisResult` contains the result. If not, that variable is `null`.
///
/// If your game is in a yieldable routine, use `yield return new WaitUntil( () => analyzer.Analyzed );'
///
/// If not, you can do a blocking wait with a `while` loop on `analyzer.Analyzed == false`.
/// </summary>
public bool Analyzed { get { return analyzeRoutine == null ; } }
private NativeAudioAnalyzerResult analysisResult;
/// <summary>
/// Access this property after `Analyzed` property became true.
/// </summary>
public NativeAudioAnalyzerResult AnalysisResult { get { return analysisResult; } }
/// <summary>
/// If the analysis was too long for your liking you can reduce it here,
/// but the average value return might not be so accurate.
/// </summary>
private const float secondsOfPlay = 1f;
/// <summary>
/// Assuming your game runs at 60 FPS, it will test 60 * seconds times.
/// </summary>
private const int framesOfPlay = (int)(60 * secondsOfPlay);
private float TicksToMs(long ticks) { return ticks / 10000f; }
private float TicksToMs(double ticks) { return (float)(ticks / 10000); }
public List<long> allTicks = new List<long>();
private static float StdDev(IEnumerable<long> values)
{
float ret = 0;
int count = values.Count();
if (count > 1)
{
float avg = (float)values.Average();
float sum = values.Sum(d => (d - avg) * (d - avg));
ret = Mathf.Sqrt(sum / count);
}
return ret;
}
private static NativeAudioPointer silence;
private IEnumerator analyzeRoutine;
private Stopwatch sw;
/// <summary>
/// This is already called from <see cref="NativeAudio.SilentAnalyze"/>
/// But you can do it again if you want, it might return a new result who knows...
///
/// You can wait on the public property `Analyzed`
///
/// If your game is in a yieldable routine, use `yield return new WaitUntil( () => analyzer.Analyzed );'
///
/// If not, you can do a blocking wait with a `while` loop on `analyzer.Analyzed == false`.
/// </summary>
public void Analyze()
{
if(analyzeRoutine != null)
{
StopCoroutine(analyzeRoutine);
}
analyzeRoutine = AnalyzeRoutine();
StartCoroutine(analyzeRoutine);
}
/// <summary>
/// There is a test game object for running the coroutine on your scene.
/// It does not take anything significant but you can call this to destroy it.
/// </summary>
public void Finish()
{
GameObject.Destroy(this);
}
private IEnumerator AnalyzeRoutine()
{
UnityEngine.Debug.Log("Built in analyze start");
sw = new Stopwatch();
allTicks = new List<long>();
if(silence != null)
{
silence.Unload();
}
//This "" is a special path to load a silence.
silence = NativeAudio.Load("");
//To warm up the audio circuit we will discard half of the test.
for (int i = 0; i < framesOfPlay/2; i++)
{
NativeAudio.GetNativeSourceAuto().Play(silence);
yield return null;
}
//Ok this is the real thing.
for (int i = 0; i < framesOfPlay/2; i++)
{
sw.Start();
NativeAudio.GetNativeSourceAuto().Play(silence);
yield return null;
sw.Stop();
allTicks.Add(sw.ElapsedTicks);
sw.Reset();
}
analysisResult = new NativeAudioAnalyzerResult(){
averageFps = 1000 / TicksToMs(allTicks.Average())
};
analyzeRoutine = null;
UnityEngine.Debug.Log("Built in analyze end");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8133be2fe6f4f4f7182c77db3d019b8a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,82 @@
// Native Audio
// 5argon - Exceed7 Experiments
// Problems/suggestions : 5argon@exceed7.com
using UnityEngine;
using System.Runtime.InteropServices;
using System;
namespace E7.Native
{
public static partial class NativeAudio
{
#if UNITY_ANDROID
private static AndroidJavaClass androidNativeAudio;
internal static AndroidJavaClass AndroidNativeAudio
{
get
{
if (androidNativeAudio == null)
{
androidNativeAudio = new AndroidJavaClass("com.Exceed7.NativeAudio.NativeAudio");
}
return androidNativeAudio;
}
}
/// <summary>
/// [Android] Initialize needs to contact Java as it need the device's native sampling rate and native buffer size to get the "fast path" audio.
/// </summary>
internal const string AndroidInitialize = "Initialize";
/// <summary>
/// [Android] Load needs to contact Java as it needs to read the audio file sent from `StreamingAssets`,
/// which could end up in either app persistent space or an another OBB package which we will unpack it and get the content.
/// </summary>
internal const string AndroidLoadAudio = "LoadAudio";
internal const string AndroidGetDeviceAudioInformation = "GetDeviceAudioInformation";
internal const string AndroidDispose = "Dispose";
// -- Operates on an audio file ("source" of data) --
//The lib name is libnativeaudioe7
// -- the play chain --
[DllImport("nativeaudioe7")]
internal static extern int getNativeSource(int nativeSourceIndex);
[DllImport("nativeaudioe7")]
internal static extern void prepareAudio(int audioBufferIndex, int nativeSourceIndex);
[DllImport("nativeaudioe7")]
internal static extern void playAudioWithNativeSourceIndex(int nativeSourceIndex, NativeSource.PlayOptions playOptions);
// -- operation on native sources --
[DllImport("nativeaudioe7")]
internal static extern int stopAudio(int nativeSourceIndex);
[DllImport("nativeaudioe7")]
internal static extern void setVolume(int nativeSourceIndex, float volume);
[DllImport("nativeaudioe7")]
internal static extern void setPan(int nativeSourceIndex, float pan);
[DllImport("nativeaudioe7")]
internal static extern float getPlaybackTime(int nativeSourceIndex);
[DllImport("nativeaudioe7")]
internal static extern void setPlaybackTime(int nativeSourceIndex, float offsetSeconds);
[DllImport("nativeaudioe7")]
internal static extern void pause(int nativeSourceIndex);
[DllImport("nativeaudioe7")]
internal static extern void resume(int nativeSourceIndex);
// -- others --
[DllImport("nativeaudioe7")]
internal static extern int sendByteArray(IntPtr byteArrayInput, int byteSize, int channels, int samplingRate, LoadOptions.ResamplingQuality resamplingQuality);
[DllImport("nativeaudioe7")]
internal static extern void unloadAudio(int audioBufferIndex);
[DllImport("nativeaudioe7")]
internal static extern float lengthByAudioBuffer(int audioBufferIndex);
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea91577c2d6f345288c0dd78df7764d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
// Native Audio
// 5argon - Exceed7 Experiments
// Problems/suggestions : 5argon@exceed7.com
using System;
using System.Runtime.InteropServices;
namespace E7.Native
{
public static partial class NativeAudio
{
#if UNITY_IOS
[DllImport("__Internal")]
internal static extern int _Initialize();
[DllImport("__Internal")]
internal static extern int _SendByteArray(IntPtr byteArrayInput, int byteSize, int channels, int samplingRate, LoadOptions.ResamplingQuality resamplingQuality);
[DllImport("__Internal")]
internal static extern int _LoadAudio(string soundUrl, int resamplingQuality);
[DllImport("__Internal")]
internal static extern void _PrepareAudio(int audioBufferIndex, int nativeSourceIndex);
[DllImport("__Internal")]
internal static extern void _PlayAudioWithNativeSourceIndex(int nativeSourceIndex, NativeSource.PlayOptions playOptions);
[DllImport("__Internal")]
internal static extern void _UnloadAudio(int audioBufferIndex);
[DllImport("__Internal")]
internal static extern float _LengthByAudioBuffer(int audioBufferIndex);
[DllImport("__Internal")]
internal static extern void _GetDeviceAudioInformation(IntPtr interopArray, IntPtr outputDeviceEnumArray);
[DllImport("__Internal")]
internal static extern int _GetNativeSource(int nativeSourceIndex);
// -- Operates on sound "source" chosen for a particular audio --
// ("source" terms of OpenAL is like a speaker, not the "source of data" which is a loaded byte array.)
[DllImport("__Internal")]
internal static extern void _StopAudio(int nativeSourceIndex);
[DllImport("__Internal")]
internal static extern void _SetVolume(int nativeSourceIndex, float volume);
[DllImport("__Internal")]
internal static extern void _SetPan(int nativeSourceIndex, float pan);
[DllImport("__Internal")]
internal static extern float _GetPlaybackTime(int nativeSourceIndex);
[DllImport("__Internal")]
internal static extern void _SetPlaybackTime(int nativeSourceIndex, float offsetSeconds);
[DllImport("__Internal")]
internal static extern void _Pause(int nativeSourceIndex);
[DllImport("__Internal")]
internal static extern void _Resume(int nativeSourceIndex);
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6761d0d4a735549829cae783b8dcde08
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,180 @@
// Native Audio
// 5argon - Exceed7 Experiments
// Problems/suggestions : 5argon@exceed7.com
using System;
using UnityEngine;
namespace E7.Native
{
/// <summary>
/// A representation of loaded audio memory at the native side.
///
/// When you <see cref="NativeAudio.Load(AudioClip)"/> it is copying audio memory to native side. Each memory area
/// of loaded audio is given an ID. This "pointer" is not really a "memory address pointer" like in C++, but just
/// the mentioned ID. Just a simple integer.
///
/// Please do not create an instance of this class on your own. You can only get and keep from calling
/// <see cref="NativeAudio.Load(AudioClip)"/>
/// </summary>
public class NativeAudioPointer
{
private string soundPath;
private int startingIndex;
/// <summary>
/// Some implementation in the future may need you to specify concurrent amount for each audio upfront so I prepared this field.
/// But it is always 1 for now. It has no effect because both iOS and Android implementation automatically rotate players on play.
/// and so you get the concurrent amount equal to amount of players shared for all sounds, not just this one sound.
/// </summary>
private int amount;
private bool isUnloaded;
/// <summary>
/// **Cached** length in **seconds** of a loaded audio calculated from PCM byte size and specifications.
/// </summary>
public float Length { get; private set; }
private int currentIndex;
/// <summary>
/// This will automatically cycles for you if the amount is not 1.
/// </summary>
internal int NextIndex
{
get
{
int toReturn = currentIndex;
currentIndex = currentIndex + 1;
if (currentIndex > startingIndex + amount - 1)
{
currentIndex = startingIndex;
}
return toReturn;
}
}
/// <param name="amount">Right now amount is not used anywhere yet.</param>
internal NativeAudioPointer(string soundPath, int index, float length, int amount = 1)
{
this.soundPath = soundPath;
this.startingIndex = index;
this.amount = amount;
this.Length = length;
this.currentIndex = index;
}
internal void AssertLoadedAndInitialized()
{
if (isUnloaded)
{
throw new InvalidOperationException("You cannot use an unloaded NativeAudioPointer.");
}
if (NativeAudio.Initialized == false)
{
throw new InvalidOperationException("You cannot use NativeAudioPointer while Native Audio itself is not in initialized state.");
}
}
public override string ToString()
{
return soundPath;
}
/// <summary>
/// Free up loaded audio memory.
/// You cannot call <see cref="NativeSource.Play(NativeAudioPointer)"/> using this pointer anymore after unloading.
/// It will throw an exception.
///
/// **THIS METHOD IS UNSAFE ON ANDROID.** Read remarks and use with care!
/// </summary>
/// <remarks>
/// [iOS] Unloads OpenAL audio buffer. If some native sources are currently playing
/// audio memory that you just unload, those tracks will be stopped automatically.
///
/// [Android] `free` the unmanaged audio data array at native side instantly.
///
/// This memory freeing could cause segmentation fault (SIGSEGV) if there are audio tracks currently playing the memory.
/// You have to make sure by yourself there is no native source playing this audio before unloading.
///
/// On some higher-end phones, it will not crash but instead you will hear loud glitched audio.
/// This is the sound of playhead running over freed memory and it interprets those as
/// sound instead of crashing.
///
/// Below is the details why this method was not made less dangerous.
///
/// So the correct approach should be like this : we have to find out who is using the audio.
/// There could be multiple users playing a single audio memory. And then stop them all before freeing memory, ideally.
///
/// However my native implementation for the best latency is to never stop any source, because
/// starting one again cause problems on some phones.
/// The callbacks are always running. We can't stop, won't stop.
///
/// The next idea is to let the callback know that it is not good to continue, by setting some kind
/// of "unloaded" flag for each audio on unloading. Then we free the memory immediately as before.
///
/// However, those callbacks are on a separated thread. It might be in the middle of copying
/// audio, and already pass the check we want to do to prevent the copy. Communication by flagging is possible
/// but it may be too late. This is what results in `memcpy` crash in your SIGSEGV crash report.
///
/// How about unloading NOT unload the audio instantly when you call unload,
/// but allowing all the playing sources that are using that audio to play this audio memory til the end.
/// at the same time prevents any new user. Then when all current tracks finished, unload at that moment.
/// (By keeping a play count of sorts similar to garbage collection, when reduced to zero while unload flag
/// is true, release the resource.)
///
/// Unfortunately again, this means we have to add `if` conditional to the callback function. So it could
/// decide should it free memory or not.
/// This callback function runs every little audio buffer that will be sent out your speaker, so it is a very
/// hot code path. Performance is very important especially considering the point of Native Audio.
///
/// For better assembly code, I have optimized very hard to elimiate all `if`, to reduce the need of branch prediction for CPU.
/// And I couldn't bring myself to add back an `if` that the whole point is just to protect
/// from SIGSEGV potential from unloading, an operation that
/// is 1% rare when compared to how many times we play the audio, which triggers the callback over and over.
///
/// And what's more, you could prevent the crash manually 100% by just stopping the source you know are
/// playing that audio before unloading. Waiting a moment for all audio to finish before unloading is an option too.
/// I choose this manual work over automatic protection for rare case by adding something to a hot code path.
///
/// Finally, what I settled with is that unloading **could cause SIGSEGV by design**, and it is an unsafe method. I won't fix it.
/// Sorry that it doesn't look polished but it's all for better latency, the whole point of Native Audio.
/// I will do whatever it takes to get to the enqueue buffer call faster in that callback method.
///
/// What you have to do is just to be careful not to unload while someone is playing that audio by yourself.
/// The code can't help you since the check would be expensive.
///
/// One last warning, if you <see cref="NativeSource.Stop"/>
/// then immediately <see cref="Unload"/> on the next line of code,
/// it is actually not 100% safe.
///
/// The thing that keeps pumping audio to the speaker runs on thread, by callbacks that runs on themselves over and over.
/// But stopping is issued on the main thread. It is basically just setting some flags so that the next time that audio thread came,
/// it stops putting out any more audio. However by the nature of thread it will be concurrent with your main thread.
///
/// So for example this situation : if the stop runs, the thread had already pass the check for stop and is putting out audio,
/// then you call unload, then you get SIGSEGV because it is putting out freed memory.
///
/// So stop and give it a few frames before unloading to be safe.
/// </remarks>
public void Unload()
{
if (!isUnloaded)
{
#if UNITY_IOS
NativeAudio._UnloadAudio(startingIndex);
isUnloaded = true;
#elif UNITY_ANDROID
for (int i = startingIndex; i < startingIndex + amount; i++)
{
NativeAudio.unloadAudio(i);
}
#endif
isUnloaded = true;
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 313a968b52ac54af7b855c6c556a8127
timeCreated: 1509544680
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,308 @@
// Native Audio
// 5argon - Exceed7 Experiments
// Problems/suggestions : 5argon@exceed7.com
using System;
using UnityEngine;
namespace E7.Native
{
/// <summary>
/// This is a referece to one of all native sources you obtained at <see cref="NativeAudio.Initialize"/>.
/// Parallels <see cref="AudioSource"/> of Unity except they are at native side, you play an audio using it.
///
/// Main way to get this is by <see cref="NativeAudio.GetNativeSource(int)"/>,
/// <see cref="NativeAudio.GetNativeSourceAuto()"/>, or <see cref="NativeAudio.GetNativeSourceAuto(INativeSourceSelector)"/>
/// </summary>
public partial struct NativeSource
{
/// <summary>
/// This is used to separate a `struct` returned from Native Audio's <see cref="NativeAudio.GetNativeSource(int)"/> method
/// from a default `struct`. (A trick to make `struct` kinda nullable.)
/// </summary>
public bool IsValid { get; private set; }
/// <summary>
/// It's like an ID of this native source. This is zero-indexed of how many native sources you get at <see cref="NativeAudio.Initialize"/>
/// If you initialize 3 native sources, then this could be 0, 1, or 2.
/// </summary>
public int Index { get; private set; }
private void AssertInitialized()
{
if (NativeAudio.Initialized == false)
{
throw new InvalidOperationException("You cannot use NativeSource while Native Audio itself is not yet in initialized state.");
}
}
//Constructing this is reserved for the lib since there are only some certain moment we can assure a valid index.
internal NativeSource(int index)
{
this.Index = index;
this.IsValid = true;
}
/// <summary>
/// Immediately stop this native source. If it was playing an audio then effectively it stops the audio.
/// </summary>
/// <remarks>
/// [iOS] One of all OpenAL sources that was used to play this sound will stop.
///
/// [Android] One of all SLAndroidSimpleBufferQueue that was used to play this sound will stop.
/// </remarks>
public void Stop()
{
AssertInitialized();
#if UNITY_IOS
NativeAudio._StopAudio(Index);
#elif UNITY_ANDROID
NativeAudio.stopAudio(Index);
#endif
}
/// <summary>
/// Change the volume of native source while it is playing.
/// </summary>
/// <remarks>
/// [iOS] Maps to `AL_GAIN`. It is a scalar amplitude multiplier, so the value can go over 1.0 for increasing volume but can be clipped.
/// If you put 0.5f, it is attenuated by 6 dB.
///
/// [Android] Maps to `SLVolumeItf` interface -> `SetVolumeLevel`.
/// The floating volume parameter will be converted to millibel (20xlog10x100) so that putting 0.5f here results in 6dB attenuation.
/// </remarks>
public void SetVolume(float volume)
{
AssertInitialized();
#if UNITY_IOS
NativeAudio._SetVolume(Index, volume);
#elif UNITY_ANDROID
NativeAudio.setVolume(Index, volume);
#endif
}
/// <summary>
/// This pan is based on "balance effect" and not a "constant energy pan". That is
/// at the center you hear each side fully. (Constant energy pan has 3dB attenuation to both on center.)
/// </summary>
/// <remarks>
/// [iOS] 2D panning in iOS will be emulated in OpenAL's 3D audio engine by splitting your stereo sound into a separated mono sounds,
/// then position each one on left and right ear of the listener. When panning, instead of adjusting gain we will just move the source
/// further from the listener and the distance attenuation will do the work. (Gain is reserved to the setting volume command,
/// so we have 2 stage of gain adjustment this way.
///
/// [Android] Maps to SLVolumeItf interface -> SetStereoPosition
/// </remarks>
/// <param name="pan">
/// -1 for full left, 0 for center, 1 for full right.
/// </param>
public void SetPan(float pan)
{
AssertInitialized();
#if UNITY_IOS
NativeAudio._SetPan(Index, pan);
#elif UNITY_ANDROID
NativeAudio.setPan(Index, pan);
#endif
}
/// <summary>
/// Return the current playback time of this native source.
/// It is relative to the start of audio data currently playing on the source in **seconds**.
/// </summary>
/// <remarks>
/// The API is very time sensitive and may or may not change the value in the same frame.
/// (depending on where you call it in the script)
///
/// This behaviour is similar to when calling <see cref="AudioSettings.dspTime"/>
/// or <see cref="AudioSource.time"/> property, those two are in the same update step.
///
/// Note that <see cref="Time.realtimeSinceStartup"/> is not in an update step unlike audio time,
/// and will change every time you call even in 2 consecutive lines of code.
///
/// A looping audio played by <see cref="PlayOptions.sourceLoop"/> has a playback time resets to 0 everytime a new loop arrives.
///
/// [iOS] Get `AL_SEC_OFFSET` attribute. It update in a certain discrete step, and if that step happen in the middle of
/// the frame this method will return different value depending on where in the script you call it. The update step timing is THE SAME as
/// <see cref="AudioSettings.dspTime"/> and <see cref="AudioSource.time"/>.
///
/// I observed (in iPad 3, iOS 9) that this function sometimes lags on first few calls.
/// It might help to pre-warm by calling this several times in loading screen or something.
///
/// [Android] Use `GetPosition` of `SLPlayItf` interface. It update in a certain discrete step, and if that step happen in the middle of
/// the frame this method will return different value depending on where in the script you call it. The update step timing is INDEPENDENT from
/// <see cref="AudioSettings.dspTime"/> and <see cref="AudioSource.time"/>.
///
/// Because of how "stop hack" was implemented, any stopped audio will have a playback time equals to audio's length (not 0)
/// </remarks>
public float GetPlaybackTime()
{
AssertInitialized();
#if UNITY_IOS
return NativeAudio._GetPlaybackTime(Index);
#elif UNITY_ANDROID
return NativeAudio.getPlaybackTime(Index);
#else
return 0;
#endif
}
/// <summary>
/// Set a playback time of this native source. If the source is in a paused state it is immediately resumed.
/// You can set it even while the native source is playing.
/// </summary>
/// <param name="offsetSeconds"></param>
public void SetPlaybackTime(float offsetSeconds)
{
AssertInitialized();
#if UNITY_IOS
NativeAudio._SetPlaybackTime(Index, offsetSeconds);
#elif UNITY_ANDROID
NativeAudio.setPlaybackTime(Index, offsetSeconds);
#endif
}
/// <summary>
/// Pause this native source.
///
/// The source is not protected against being chosen for other audio while pausing,
/// and if that happens the pause status will be cleared out.
/// </summary>
public void Pause()
{
AssertInitialized();
#if UNITY_IOS
NativeAudio._Pause(Index);
#elif UNITY_ANDROID
NativeAudio.pause(Index);
#endif
}
/// <summary>
/// Resume this native source.
///
/// If by the time you call resume the source has already been used to play other audio,
/// the resume will have no effect since the pause status had already been clreared out.
/// </summary>
public void Resume()
{
AssertInitialized();
#if UNITY_IOS
NativeAudio._Resume(Index);
#elif UNITY_ANDROID
NativeAudio.resume(Index);
#endif
}
/// <summary>
/// A native source will play an audio using loaded audio memory at native side, specified by <paramref name="nativeAudioPointer"/>.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when you attempt to play an unloaded audio.</exception>
public void Play(NativeAudioPointer nativeAudioPointer)
{
Play(nativeAudioPointer, PlayOptions.defaultOptions);
}
/// <summary>
/// A native source will play an audio using loaded audio memory at native side, specified by <paramref name="nativeAudioPointer"/>.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when you attempt to play an unloaded audio.</exception>
/// <param name="playOptions">Customize your play. Begin creating the option from <see cref="PlayOptions.defaultOptions"/></param>
public void Play(NativeAudioPointer nativeAudioPointer, PlayOptions playOptions)
{
nativeAudioPointer.AssertLoadedAndInitialized();
#if UNITY_IOS
NativeAudio._PrepareAudio(nativeAudioPointer.NextIndex, Index);
NativeAudio._PlayAudioWithNativeSourceIndex(Index, playOptions);
#elif UNITY_ANDROID
NativeAudio.prepareAudio(nativeAudioPointer.NextIndex, Index);
NativeAudio.playAudioWithNativeSourceIndex(Index, playOptions);
#endif
}
/// <summary>
/// (**EXPERIMENTAL**) Try to make the next <see cref="Play(NativeAudioPointer)"/> faster by pre-associating
/// the pointer to this native source. Whether if this is possible or not depends on platform.
///
/// To "fire" the prepared audio, use the parameterless play <see cref="PlayPrepared"/> method.
///
/// Not recommended to care about this generally, because the gain could be next to nothing for hassle you get.
/// But it is a method stub for the future where there maybe a significant optimization in doing so.
///
/// [iOS] Implemented, but likely negligible..
/// (didn't profile extensively yet, but theoretically there is something to prepare here.)
///
/// [Android] Not implemented, no effect.
/// </summary>
/// <remarks>
/// [iOS] Normally on <see cref="Play(NativeAudioPointer)"/> OpenAL will
///
/// 1. Choose a source at native side, depending on your <see cref="PlayOptions"/>
/// when using <see cref="Play(NativeAudioPointer, PlayOptions)"/> if manually.
/// Or automatically round-robin without options.
/// 2. Stop that source, and then assign a new audio buffer to it.
/// 3. Play that source.
///
/// Preparing make it do 1. and 2. preemptively. Then <see cref="PlayPrepared"/> performs 3. "blindly"
/// without caring about the current audio. If you didn't wait too long, the preparation should be usable.
///
/// [Android] No effect as OpenSL ES play audio by pushing data into `SLAndroidSimpleBufferQueueItf`.
/// All the prepare is already at the <see cref="NativeAudio.Load(AudioClip)"/>. I cannot find any other way
/// to pre-speeding this up.
/// </remarks>
/// <param name="nativeAudioPointer">An audio to prepare into this native source.</param>
public void Prepare(NativeAudioPointer nativeAudioPointer)
{
nativeAudioPointer.AssertLoadedAndInitialized();
#if UNITY_IOS
NativeAudio._PrepareAudio(nativeAudioPointer.NextIndex, Index);
#elif UNITY_ANDROID
//There is no possible preparation for OpenSL ES at the moment..
#endif
}
/// <summary>
/// (**EXPERIMENTAL**)
/// Play the audio "blindly" without <see cref="NativeAudioPointer"/>,
/// but **believing** that the prepared audio at <see cref="Prepare(NativeAudioPointer)"/> is still
/// associated with this native source.
/// If successful, the play could be potentially faster depending on platforms.
///
/// If you waited too long and the native source has already been used with other audio, this may produce unexpected
/// result such as repeating an audio you were not expecting when you prepared. With careful native source
/// planning, you can know that this will or will not happen.
///
/// [iOS] Use this after <see cref="Prepare(NativeAudioPointer)"/>.
///
/// [Android] No effect, Android has no prepare implemented yet.
/// </summary>
public void PlayPrepared()
{
PlayPrepared(PlayOptions.defaultOptions);
}
/// <summary>
/// (**EXPERIMENTAL**)
/// Play the audio "blindly" without <see cref="NativeAudioPointer"/>,
/// but **believing** that the prepared audio at <see cref="Prepare(NativeAudioPointer)"/> is still
/// associated with this native source.
/// If successful, the play could be potentially faster depending on platforms.
///
/// If you waited too long and the native source has already been used with other audio, this may produce unexpected
/// result such as repeating an audio you were not expecting when you prepared. With careful native source
/// planning, you can know that this will or will not happen.
///
/// [iOS] Use this after <see cref="Prepare(NativeAudioPointer)"/>.
///
/// [Android] No effect, Android has no prepare implemented yet.
/// </summary>
public void PlayPrepared(PlayOptions playOptions)
{
#if UNITY_IOS
NativeAudio._PlayAudioWithNativeSourceIndex(Index, playOptions);
#elif UNITY_ANDROID
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be829a9a3f7434660a2c9e85a5333fb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
// Native Audio
// 5argon - Exceed7 Experiments
// Problems/suggestions : 5argon@exceed7.com
using System.Runtime.InteropServices;
namespace E7.Native
{
public partial struct NativeSource
{
/// <summary>
/// Used with <see cref="Play(NativeAudioPointer, PlayOptions)"/> to customize your play.
/// Start creating it from <see cref="PlayOptions.defaultOptions"/>.
/// </summary>
/// <remarks>
/// On some platforms like iOS, adjusting them after the play with <see cref="NativeSource"/>
/// is already too late because you will already hear the audio. (Even in consecutive lines of code)
///
/// It has to be a `struct` since this will be sent to the native side,
/// interop to a matching code in other language.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
public struct PlayOptions
{
/// <summary>
/// Structs couldn't have custom default values and something like volume is better defaulted to 1 instead of 0.
/// This prellocated `static` variable contains sensible default values that you can copy from as a starting point.
/// </summary>
/// <remarks>
/// Consists of :
///
/// - Volume 1 (no attenuation)
/// - Pan 0 (center)
/// - Offset seconds 0 (starts from the beginning)
/// - Source loop `false`
/// </remarks>
public static readonly PlayOptions defaultOptions = new PlayOptions
{
volume = 1,
pan = 0,
offsetSeconds = 0,
sourceLoop = false,
};
// /// <summary>
// /// Choose a target native source for this play. Choosing a source index that is already playing an audio will
// /// cut the previous audio off. Valid index is zero-indexed based on how many you get at <see cref="NativeAudio.Initialize"/>.
// /// If you initialize 3 native sources, you can use 0, 1, or 2.
// ///
// /// - If -1 (<see cref="PlayOptions.defaultOptions"/>) The native source target will be round-robin selected for you.
// /// - If any 0+ number, you specify which native source you like to use for this play.
// /// - If the number is over how many sources the native side actually gave you at initialization,
// /// it is converted to be like -1 automatically.
// ///
// /// All other options in this play options affects the target native source resulting from this.
// /// </summary>
// /// <remarks>
// /// </remarks>
// public int nativeSourceIndex;
/// <summary>
/// Set the volume of target native source before play.
/// </summary>
/// <remarks>
/// [iOS] Maps to `AL_GAIN`. It is a scalar amplitude multiplier, so the value can go over 1.0 for increasing volume but can be clipped.
/// If you put 0.5f, it is attenuated by 6 dB.
///
/// [Android] Maps to `SLVolumeItf` interface -> `SetVolumeLevel`.
/// The floating volume parameter will be converted to millibel (20xlog10x100) so that putting 0.5f here results in 6dB attenuation.
/// </remarks>
public float volume;
/// <summary>
/// Set the pan of target native source before play.
/// -1 for full left, 0 for center, 1 for full right.
///
/// This pan is based on "balance effect" and not a "constant energy pan".
/// That is at the center you hear each side fully. (Constant energy pan has 3dB attenuation to both on center.)
/// </summary>
/// <remarks>
/// [iOS] 2D panning in iOS will be emulated in OpenAL's 3D audio engine by splitting your stereo sound into a separated mono sounds,
/// then position each one on left and right ear of the listener. When panning, instead of adjusting gain we will just move the source
/// further from the listener and the distance attenuation will do the work. (Gain is reserved to the setting volume command,
/// so we have 2 stage of gain adjustment this way.
///
/// [Android] Maps to `SLVolumeItf` interface -> `SetStereoPosition`
/// </remarks>
public float pan;
/// <summary>
/// Start playing from other point in the audio by offsetting
/// the target native source's playhead time SECONDS unit.
///
/// Will do nothing if the offset is over the length of audio.
/// </summary>
public float offsetSeconds;
/// <summary>
/// Apply a looping state on the native source.
/// </summary>
/// <remarks>
/// The reason why it is "sourceLoop" instead of "loop" is to emphasize that if some newer sound
/// decided to use that native source to play, that looping sound is immediately stopped since we do not mix
/// and one native source can only handle one audio.
///
/// To "protect" the looping sound, you likely have to plan your native source index carefully when
/// choosing which source to play via <see cref="NativeAudio.GetNativeSource(int)"/>
///
/// Using the default round-robin <see cref="NativeAudio.GetNativeSourceAuto"/> sooner or later will stop your looping sound when it wraps back.
/// </remarks>
public bool sourceLoop;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 699aa965027f94db9a7062714c477358
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: