Files
UnityExplorer_Fix/src/UI/Widgets/UnityObjects/AudioClipWidget.cs
2022-06-23 06:03:58 +10:00

410 lines
14 KiB
C#

using System.Collections;
using System.Text;
using UnityExplorer.Config;
using UnityExplorer.Inspectors;
using UniverseLib.UI;
using UniverseLib.UI.Models;
using UniverseLib.UI.ObjectPool;
#if CPP
#if INTEROP
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
#else
using UnhollowerRuntimeLib;
using UnhollowerBaseLib;
#endif
#endif
namespace UnityExplorer.UI.Widgets
{
public class AudioClipWidget : UnityObjectWidget
{
static GameObject AudioPlayerObject;
static AudioSource Source;
static AudioClipWidget CurrentlyPlaying;
static Coroutine CurrentlyPlayingCoroutine;
static readonly string zeroLengthString = GetLengthString(0f);
public AudioClip audioClip;
string fullLengthText;
ButtonRef toggleButton;
bool audioPlayerWanted;
GameObject audioPlayerRoot;
ButtonRef playStopButton;
Text progressLabel;
GameObject saveObjectRow;
InputFieldRef savePathInput;
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);
audioClip = target.TryCast<AudioClip>();
this.fullLengthText = GetLengthString(audioClip.length);
if (audioClip.loadType == AudioClipLoadType.DecompressOnLoad)
{
cantSaveRow.SetActive(false);
saveObjectRow.SetActive(true);
SetDefaultSavePath();
}
else
{
cantSaveRow.SetActive(true);
saveObjectRow.SetActive(false);
}
ResetProgressLabel();
}
public override void OnReturnToPool()
{
audioClip = null;
if (audioPlayerWanted)
ToggleAudioWidget();
if (CurrentlyPlaying == this)
StopClip();
this.audioPlayerRoot.transform.SetParent(Pool<AudioClipWidget>.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 = audioClip.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 = $"{zeroLengthString} / {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); // move it as far away as possible
#if CPP
Source = AudioPlayerObject.AddComponent(Il2CppType.Of<AudioSource>()).TryCast<AudioSource>();
#else
Source = AudioPlayerObject.AddComponent<AudioSource>();
#endif
AudioPlayerObject.AddComponent<AudioListener>();
}
private IEnumerator PlayClipCoroutine()
{
playStopButton.ButtonText.text = "Stop Clip";
CurrentlyPlaying = this;
Source.clip = this.audioClip;
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 (!audioClip)
{
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(audioClip, 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 const float RESCALE_FACTOR = 32767; // to convert float to Int16
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
Il2CppStructArray<float> 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
int len = samples.Length;
// converting in 2 float[] steps to Int16[], then Int16[] to Byte[]
short[] intData = new short[len];
// bytesData array is twice the size of dataSource array because a float converted in Int16 is 2 bytes.
byte[] bytesData = new byte[len * 2];
for (int i = 0; i < len; i++)
{
intData[i] = (short)(samples[i] * RESCALE_FACTOR);
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
}
}