// Native Audio // 5argon - Exceed7 Experiments // Problems/suggestions : 5argon@exceed7.com using System; using UnityEngine; namespace E7.Native { /// /// A representation of loaded audio memory at the native side. /// /// When you 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 /// /// public class NativeAudioPointer { private string soundPath; private int startingIndex; /// /// 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. /// private int amount; private bool isUnloaded; /// /// **Cached** length in **seconds** of a loaded audio calculated from PCM byte size and specifications. /// public float Length { get; private set; } private int currentIndex; /// /// This will automatically cycles for you if the amount is not 1. /// internal int NextIndex { get { int toReturn = currentIndex; currentIndex = currentIndex + 1; if (currentIndex > startingIndex + amount - 1) { currentIndex = startingIndex; } return toReturn; } } /// Right now amount is not used anywhere yet. 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; } /// /// Free up loaded audio memory. /// You cannot call using this pointer anymore after unloading. /// It will throw an exception. /// /// **THIS METHOD IS UNSAFE ON ANDROID.** Read remarks and use with care! /// /// /// [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 /// then immediately 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. /// 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; } } } }