diff --git a/lib/unhollowed/UnityEngine.AudioModule.dll b/lib/unhollowed/UnityEngine.AudioModule.dll new file mode 100644 index 0000000..89cfbda Binary files /dev/null and b/lib/unhollowed/UnityEngine.AudioModule.dll differ diff --git a/src/UI/Widgets/UnityObjects/AudioClipWidget.cs b/src/UI/Widgets/UnityObjects/AudioClipWidget.cs new file mode 100644 index 0000000..0ab4f49 --- /dev/null +++ b/src/UI/Widgets/UnityObjects/AudioClipWidget.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityExplorer.Config; +using UnityExplorer.Inspectors; +using UniverseLib; +using UniverseLib.UI; +using UniverseLib.UI.Models; +using UniverseLib.UI.ObjectPool; +using UniverseLib.Utility; + +namespace UnityExplorer.UI.Widgets +{ + public class AudioClipWidget : UnityObjectWidget + { + static GameObject AudioPlayerObject; + static AudioSource Source; + static AudioClipWidget CurrentlyPlaying; + static Coroutine CurrentlyPlayingCoroutine; + + public AudioClip RefAudioClip; + private string fullLengthText; + + private ButtonRef toggleButton; + private bool audioPlayerWanted; + + private GameObject audioPlayerRoot; + private ButtonRef playStopButton; + private Text progressLabel; + private GameObject saveObjectRow; + private InputFieldRef savePathInput; + private GameObject cantSaveRow; + + public override void OnBorrowed(object target, Type targetType, ReflectionInspector inspector) + { + base.OnBorrowed(target, targetType, inspector); + + this.audioPlayerRoot.transform.SetParent(inspector.UIRoot.transform); + this.audioPlayerRoot.transform.SetSiblingIndex(inspector.UIRoot.transform.childCount - 2); + + RefAudioClip = target.TryCast(); + this.fullLengthText = GetLengthString(RefAudioClip.length); + + if (RefAudioClip.loadType == AudioClipLoadType.DecompressOnLoad) + { + cantSaveRow.SetActive(false); + saveObjectRow.SetActive(true); + SetDefaultSavePath(); + } + else + { + cantSaveRow.SetActive(true); + saveObjectRow.SetActive(false); + } + + ResetProgressLabel(); + } + + public override void OnReturnToPool() + { + RefAudioClip = null; + + if (audioPlayerWanted) + ToggleAudioWidget(); + + if (CurrentlyPlaying == this) + StopClip(); + + this.audioPlayerRoot.transform.SetParent(Pool.Instance.InactiveHolder.transform); + + base.OnReturnToPool(); + } + + private void ToggleAudioWidget() + { + if (audioPlayerWanted) + { + audioPlayerWanted = false; + + toggleButton.ButtonText.text = "Show Player"; + audioPlayerRoot.SetActive(false); + } + else + { + audioPlayerWanted = true; + + toggleButton.ButtonText.text = "Hide Player"; + audioPlayerRoot.SetActive(true); + } + } + + void SetDefaultSavePath() + { + string name = RefAudioClip.name; + if (string.IsNullOrEmpty(name)) + name = "untitled"; + savePathInput.Text = Path.Combine(ConfigManager.Default_Output_Path.Value, $"{name}.wav"); + } + + static string GetLengthString(float seconds) + { + TimeSpan ts = TimeSpan.FromSeconds(seconds); + + StringBuilder sb = new(); + + if (ts.Hours > 0) + sb.Append($"{ts.Hours}:"); + + sb.Append($"{ts.Minutes:00}:"); + + sb.Append($"{ts.Seconds:00}:"); + sb.Append($"{ts.Milliseconds:000}"); + + return sb.ToString(); + } + + private void ResetProgressLabel() + { + this.progressLabel.text = $"{GetLengthString(0f)} / {fullLengthText}"; + } + + private void OnPlayStopClicked() + { + SetupAudioPlayer(); + + if (CurrentlyPlaying == this) + { + // we are playing a clip. stop it. + StopClip(); + } + else + { + // If something else is playing a clip, stop that. + if (CurrentlyPlaying != null) + CurrentlyPlaying.StopClip(); + + // we want to start playing a clip. + CurrentlyPlayingCoroutine = RuntimeHelper.StartCoroutine(PlayClipCoroutine()); + } + } + + static void SetupAudioPlayer() + { + if (AudioPlayerObject) + return; + + AudioPlayerObject = new GameObject("UnityExplorer.AudioPlayer"); + UnityEngine.Object.DontDestroyOnLoad(AudioPlayerObject); + AudioPlayerObject.hideFlags = HideFlags.HideAndDontSave; + AudioPlayerObject.transform.position = new(int.MinValue, int.MinValue); + +#if CPP + Source = AudioPlayerObject.AddComponent(UnhollowerRuntimeLib.Il2CppType.Of()).TryCast(); +#else + Source = AudioPlayerObject.AddComponent(); +#endif + AudioPlayerObject.AddComponent(); + } + + private IEnumerator PlayClipCoroutine() + { + playStopButton.ButtonText.text = "Stop Clip"; + CurrentlyPlaying = this; + Source.clip = this.RefAudioClip; + Source.Play(); + + while (Source.isPlaying) + { + progressLabel.text = $"{GetLengthString(Source.time)} / {fullLengthText}"; + yield return null; + } + + CurrentlyPlayingCoroutine = null; + StopClip(); + } + + private void StopClip() + { + if (CurrentlyPlayingCoroutine != null) + RuntimeHelper.StopCoroutine(CurrentlyPlayingCoroutine); + + Source.Stop(); + CurrentlyPlaying = null; + CurrentlyPlayingCoroutine = null; + playStopButton.ButtonText.text = "Play Clip"; + + ResetProgressLabel(); + } + + public void OnSaveClipClicked() + { + if (!RefAudioClip) + { + ExplorerCore.LogWarning("AudioClip is null, maybe it was destroyed?"); + return; + } + + if (string.IsNullOrEmpty(savePathInput.Text)) + { + ExplorerCore.LogWarning("Save path cannot be empty!"); + return; + } + + string path = savePathInput.Text; + if (!path.EndsWith(".wav", StringComparison.InvariantCultureIgnoreCase)) + path += ".wav"; + + path = IOUtility.EnsureValidFilePath(path); + + if (File.Exists(path)) + File.Delete(path); + + SavWav.Save(RefAudioClip, path); + } + + public override GameObject CreateContent(GameObject uiRoot) + { + GameObject ret = base.CreateContent(uiRoot); + + // Toggle Button + + toggleButton = UIFactory.CreateButton(UIRoot, "AudioWidgetToggleButton", "Show Player", new Color(0.2f, 0.3f, 0.2f)); + toggleButton.Transform.SetSiblingIndex(0); + UIFactory.SetLayoutElement(toggleButton.Component.gameObject, minHeight: 25, minWidth: 170); + toggleButton.OnClick += ToggleAudioWidget; + + // Actual widget + + audioPlayerRoot = UIFactory.CreateVerticalGroup(uiRoot, "AudioWidget", false, false, true, true, spacing: 5); + UIFactory.SetLayoutElement(audioPlayerRoot, flexibleWidth: 9999, flexibleHeight: 50); + audioPlayerRoot.SetActive(false); + + // Player + + GameObject playerRow = UIFactory.CreateHorizontalGroup(audioPlayerRoot, "PlayerWidget", false, false, true, true, + spacing: 5, padding: new() { x = 3f, w = 3f, y = 3f, z = 3f }); + + playStopButton = UIFactory.CreateButton(playerRow, "PlayerButton", "Play", normalColor: new(0.2f, 0.4f, 0.2f)); + playStopButton.OnClick += OnPlayStopClicked; + UIFactory.SetLayoutElement(playStopButton.GameObject, minWidth: 60, minHeight: 25); + + progressLabel = UIFactory.CreateLabel(playerRow, "ProgressLabel", "0 / 0"); + UIFactory.SetLayoutElement(progressLabel.gameObject, flexibleWidth: 9999, minHeight: 25); + + ResetProgressLabel(); + + // Save helper + + saveObjectRow = UIFactory.CreateHorizontalGroup(audioPlayerRoot, "SaveRow", false, false, true, true, 2, new Vector4(2, 2, 2, 2), + new Color(0.1f, 0.1f, 0.1f)); + + ButtonRef saveBtn = UIFactory.CreateButton(saveObjectRow, "SaveButton", "Save .WAV", new Color(0.2f, 0.25f, 0.2f)); + UIFactory.SetLayoutElement(saveBtn.Component.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); + saveBtn.OnClick += OnSaveClipClicked; + + savePathInput = UIFactory.CreateInputField(saveObjectRow, "SaveInput", "..."); + UIFactory.SetLayoutElement(savePathInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 9999); + + // cant save label + cantSaveRow = UIFactory.CreateHorizontalGroup(audioPlayerRoot, "CantSaveRow", true, true, true, true); + UIFactory.SetLayoutElement(cantSaveRow, minHeight: 25, flexibleWidth: 9999); + UIFactory.CreateLabel( + cantSaveRow, + "CantSaveLabel", + "Cannot save this AudioClip as the data is compressed or streamed. Try a tool such as AssetRipper to unpack it.", + color: Color.grey); + + return ret; + } + } + +#region SavWav + + // Copyright (c) 2012 Calvin Rien + // http://the.darktable.com + // + // This software is provided 'as-is', without any express or implied warranty. In + // no event will the authors be held liable for any damages arising from the use + // of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it freely, + // subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not claim + // that you wrote the original software. If you use this software in a product, + // an acknowledgment in the product documentation would be appreciated but is not + // required. + // + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // + // 3. This notice may not be removed or altered from any source distribution. + // + // ============================================================================= + // + // derived from Gregorio Zanon's script + // http://forum.unity3d.com/threads/119295-Writing-AudioListener.GetOutputData-to-wav-problem?p=806734&viewfull=1#post806734 + + public static class SavWav + { + public const int HEADER_SIZE = 44; + + public static void Save(AudioClip clip, string filepath) + { + using FileStream fileStream = CreateEmpty(filepath); + + ConvertAndWrite(fileStream, clip); + WriteHeader(fileStream, clip); + } + + static FileStream CreateEmpty(string filepath) + { + FileStream fileStream = new(filepath, FileMode.Create); + byte emptyByte = default; + + for (int i = 0; i < HEADER_SIZE; i++) //preparing the header + fileStream.WriteByte(emptyByte); + + return fileStream; + } + + static void ConvertAndWrite(FileStream fileStream, AudioClip clip) + { +#if CPP + UnhollowerBaseLib.Il2CppStructArray samples = new float[clip.samples * clip.channels]; + AudioClip.GetData(clip, samples, clip.samples, 0); +#else + float[] samples = new float[clip.samples * clip.channels]; + clip.GetData(samples, 0); +#endif + + // converting in 2 float[] steps to Int16[], then Int16[] to Byte[] + short[] intData = new short[samples.Length]; + + // bytesData array is twice the size of dataSource array because a float converted in Int16 is 2 bytes. + byte[] bytesData = new byte[samples.Length * 2]; + + float rescaleFactor = 32767; // to convert float to Int16 + + for (int i = 0; i < samples.Length; i++) + { + intData[i] = (short)(samples[i] * rescaleFactor); + byte[] byteArr = BitConverter.GetBytes(intData[i]); + byteArr.CopyTo(bytesData, i * 2); + } + + fileStream.Write(bytesData, 0, bytesData.Length); + } + + static void WriteHeader(FileStream stream, AudioClip clip) + { + int hz = clip.frequency; + int channels = clip.channels; + int samples = clip.samples; + + stream.Seek(0, SeekOrigin.Begin); + + byte[] riff = Encoding.UTF8.GetBytes("RIFF"); + stream.Write(riff, 0, 4); + + byte[] chunkSize = BitConverter.GetBytes(stream.Length - 8); + stream.Write(chunkSize, 0, 4); + + byte[] wave = Encoding.ASCII.GetBytes("WAVE"); + stream.Write(wave, 0, 4); + + byte[] fmt = Encoding.ASCII.GetBytes("fmt "); + stream.Write(fmt, 0, 4); + + byte[] subChunk1 = BitConverter.GetBytes(16); + stream.Write(subChunk1, 0, 4); + + byte[] audioFormat = BitConverter.GetBytes(1); + stream.Write(audioFormat, 0, 2); + + byte[] numChannels = BitConverter.GetBytes(channels); + stream.Write(numChannels, 0, 2); + + byte[] sampleRate = BitConverter.GetBytes(hz); + stream.Write(sampleRate, 0, 4); + + byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); // sampleRate * bytesPerSample*number of channels, here 44100*2*2 + stream.Write(byteRate, 0, 4); + + ushort blockAlign = (ushort)(channels * 2); + stream.Write(BitConverter.GetBytes(blockAlign), 0, 2); + + ushort bps = 16; + byte[] bitsPerSample = BitConverter.GetBytes(bps); + stream.Write(bitsPerSample, 0, 2); + + byte[] datastring = Encoding.UTF8.GetBytes("data"); + stream.Write(datastring, 0, 4); + + byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2); + stream.Write(subChunk2, 0, 4); + + stream.Seek(0, SeekOrigin.Begin); + } + +#endregion + } +} \ No newline at end of file diff --git a/src/UI/Widgets/UnityObjects/UnityObjectWidget.cs b/src/UI/Widgets/UnityObjects/UnityObjectWidget.cs index fe84aa1..36b004e 100644 --- a/src/UI/Widgets/UnityObjects/UnityObjectWidget.cs +++ b/src/UI/Widgets/UnityObjects/UnityObjectWidget.cs @@ -15,7 +15,6 @@ namespace UnityExplorer.UI.Widgets public Component ComponentRef; public ReflectionInspector ParentInspector; - protected GameObject unityObjectRow; protected ButtonRef gameObjectButton; protected InputFieldRef nameInput; protected InputFieldRef instanceIdInput; @@ -33,6 +32,8 @@ namespace UnityExplorer.UI.Widgets if (targetType == typeof(Texture2D)) ret = Pool.Borrow(); + else if (targetType == typeof(AudioClip)) + ret = Pool.Borrow(); else ret = Pool.Borrow(); @@ -42,7 +43,7 @@ namespace UnityExplorer.UI.Widgets public virtual void OnBorrowed(object target, Type targetType, ReflectionInspector inspector) { - this.ParentInspector = inspector; + this.ParentInspector = inspector ?? throw new ArgumentNullException(nameof(inspector)); if (!this.UIRoot) CreateContent(inspector.UIRoot); @@ -52,7 +53,7 @@ namespace UnityExplorer.UI.Widgets this.UIRoot.transform.SetSiblingIndex(inspector.UIRoot.transform.childCount - 2); UnityObjectRef = (UnityEngine.Object)target.TryCast(typeof(UnityEngine.Object)); - unityObjectRow.SetActive(true); + UIRoot.SetActive(true); nameInput.Text = UnityObjectRef.name; instanceIdInput.Text = UnityObjectRef.GetInstanceID().ToString(); @@ -101,31 +102,29 @@ namespace UnityExplorer.UI.Widgets public virtual GameObject CreateContent(GameObject uiRoot) { - unityObjectRow = UIFactory.CreateUIObject("UnityObjectRow", uiRoot); - UIFactory.SetLayoutGroup(unityObjectRow, false, false, true, true, 5); - UIFactory.SetLayoutElement(unityObjectRow, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); + UIRoot = UIFactory.CreateUIObject("UnityObjectRow", uiRoot); + UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, 5); + UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleHeight: 0, flexibleWidth: 9999); - UIRoot = unityObjectRow; - - var nameLabel = UIFactory.CreateLabel(unityObjectRow, "NameLabel", "Name:", TextAnchor.MiddleLeft, Color.grey); + var nameLabel = UIFactory.CreateLabel(UIRoot, "NameLabel", "Name:", TextAnchor.MiddleLeft, Color.grey); UIFactory.SetLayoutElement(nameLabel.gameObject, minHeight: 25, minWidth: 45, flexibleWidth: 0); - nameInput = UIFactory.CreateInputField(unityObjectRow, "NameInput", "untitled"); + nameInput = UIFactory.CreateInputField(UIRoot, "NameInput", "untitled"); UIFactory.SetLayoutElement(nameInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 1000); nameInput.Component.readOnly = true; - gameObjectButton = UIFactory.CreateButton(unityObjectRow, "GameObjectButton", "Inspect GameObject", new Color(0.2f, 0.2f, 0.2f)); + gameObjectButton = UIFactory.CreateButton(UIRoot, "GameObjectButton", "Inspect GameObject", new Color(0.2f, 0.2f, 0.2f)); UIFactory.SetLayoutElement(gameObjectButton.Component.gameObject, minHeight: 25, minWidth: 160); gameObjectButton.OnClick += OnGameObjectButtonClicked; - var instanceLabel = UIFactory.CreateLabel(unityObjectRow, "InstanceLabel", "Instance ID:", TextAnchor.MiddleRight, Color.grey); + var instanceLabel = UIFactory.CreateLabel(UIRoot, "InstanceLabel", "Instance ID:", TextAnchor.MiddleRight, Color.grey); UIFactory.SetLayoutElement(instanceLabel.gameObject, minHeight: 25, minWidth: 100, flexibleWidth: 0); - instanceIdInput = UIFactory.CreateInputField(unityObjectRow, "InstanceIDInput", "ERROR"); + instanceIdInput = UIFactory.CreateInputField(UIRoot, "InstanceIDInput", "ERROR"); UIFactory.SetLayoutElement(instanceIdInput.UIRoot, minHeight: 25, minWidth: 100, flexibleWidth: 0); instanceIdInput.Component.readOnly = true; - unityObjectRow.SetActive(false); + UIRoot.SetActive(false); return UIRoot; } diff --git a/src/UnityExplorer.csproj b/src/UnityExplorer.csproj index ba8b56d..4c73b3b 100644 --- a/src/UnityExplorer.csproj +++ b/src/UnityExplorer.csproj @@ -199,6 +199,10 @@ ..\lib\unhollowed\UnityEngine.dll False + + ..\lib\unhollowed\UnityEngine.AudioModule.dll + False + ..\lib\unhollowed\UnityEngine.CoreModule.dll False @@ -324,6 +328,7 @@ +