From c30a74c9c3cf4433473c82f6a30b70cc21302aa2 Mon Sep 17 00:00:00 2001 From: rainylinjing-reboot Date: Mon, 13 Jul 2026 11:25:44 +0900 Subject: [PATCH] =?UTF-8?q?=ED=83=80=EC=9D=B4=ED=8B=80=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/01_Scenes/Intro_Test2.unity | 4 +- Assets/01_Scenes/Intro_Test2_timing.unity | 3 + .../01_Scenes/Intro_Test2_timing.unity.meta | 7 + Assets/02_Scripts/Intro/IntroBGMController.cs | 133 +++ .../Intro/IntroBGMController.cs.meta | 2 + Assets/02_Scripts/Intro/IntroTitleSequence.cs | 222 ++++ .../Intro/IntroTitleSequence.cs.meta | 2 + .../BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav | 3 + .../@@Paradise_꽃보다 남자 MR 1.wav.meta | 23 + Assets/Easy Audio Cutter.meta | 8 + Assets/Easy Audio Cutter/Documentation.txt | 58 ++ .../Easy Audio Cutter/Documentation.txt.meta | 14 + Assets/Easy Audio Cutter/Scripts.meta | 8 + Assets/Easy Audio Cutter/Scripts/Editor.meta | 8 + .../Editor/AudioCutterProjectButton.cs | 59 ++ .../Editor/AudioCutterProjectButton.cs.meta | 18 + .../Scripts/Editor/EasyAudioCutter.cs | 967 ++++++++++++++++++ .../Scripts/Editor/EasyAudioCutter.cs.meta | 18 + .../XR/Settings/OpenXR Package Settings.asset | 4 +- 19 files changed, 1557 insertions(+), 4 deletions(-) create mode 100644 Assets/01_Scenes/Intro_Test2_timing.unity create mode 100644 Assets/01_Scenes/Intro_Test2_timing.unity.meta create mode 100644 Assets/02_Scripts/Intro/IntroBGMController.cs create mode 100644 Assets/02_Scripts/Intro/IntroBGMController.cs.meta create mode 100644 Assets/02_Scripts/Intro/IntroTitleSequence.cs create mode 100644 Assets/02_Scripts/Intro/IntroTitleSequence.cs.meta create mode 100644 Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav create mode 100644 Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav.meta create mode 100644 Assets/Easy Audio Cutter.meta create mode 100644 Assets/Easy Audio Cutter/Documentation.txt create mode 100644 Assets/Easy Audio Cutter/Documentation.txt.meta create mode 100644 Assets/Easy Audio Cutter/Scripts.meta create mode 100644 Assets/Easy Audio Cutter/Scripts/Editor.meta create mode 100644 Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs create mode 100644 Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs.meta create mode 100644 Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs create mode 100644 Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs.meta diff --git a/Assets/01_Scenes/Intro_Test2.unity b/Assets/01_Scenes/Intro_Test2.unity index 4cb80029..20260ad2 100644 --- a/Assets/01_Scenes/Intro_Test2.unity +++ b/Assets/01_Scenes/Intro_Test2.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47350ae568a00e755395be23ade210ba3ed29aad0c1e7161bd4dcf21731c4d65 -size 88029 +oid sha256:a8e458cfb77fa7978a1102ffc28da6cd6fdad6e3d870c7486f242f7d6346114d +size 594172 diff --git a/Assets/01_Scenes/Intro_Test2_timing.unity b/Assets/01_Scenes/Intro_Test2_timing.unity new file mode 100644 index 00000000..cd3ca5f0 --- /dev/null +++ b/Assets/01_Scenes/Intro_Test2_timing.unity @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a62f06fc28b43302af4c428658d85ba8b77a3b4f78b0656768ffe98b55c96e2 +size 594170 diff --git a/Assets/01_Scenes/Intro_Test2_timing.unity.meta b/Assets/01_Scenes/Intro_Test2_timing.unity.meta new file mode 100644 index 00000000..c10f9625 --- /dev/null +++ b/Assets/01_Scenes/Intro_Test2_timing.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 16134748a5d8b544bbcab26197b912b6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/02_Scripts/Intro/IntroBGMController.cs b/Assets/02_Scripts/Intro/IntroBGMController.cs new file mode 100644 index 00000000..75b6fe2c --- /dev/null +++ b/Assets/02_Scripts/Intro/IntroBGMController.cs @@ -0,0 +1,133 @@ +using System.Collections; +using UnityEngine; + +[RequireComponent(typeof(AudioSource))] +public class IntroBGMController : MonoBehaviour +{ + [Header("Audio Source")] + [SerializeField] private AudioSource audioSource; + + [Header("Start Timing")] + [SerializeField] private bool playOnStart = true; + [SerializeField] private float startDelay = 0.5f; + + [Header("Fade In")] + [SerializeField] private bool useFadeIn = true; + [SerializeField] private float fadeInDuration = 2.0f; + [Range(0f, 1f)] + [SerializeField] private float targetVolume = 0.55f; + + [Header("Loop")] + [SerializeField] private bool loop = true; + + private Coroutine bgmRoutine; + + private void Awake() + { + if (audioSource == null) + { + audioSource = GetComponent(); + } + + audioSource.playOnAwake = false; + audioSource.loop = loop; + audioSource.volume = 0f; + audioSource.spatialBlend = 0f; + } + + private void Start() + { + if (playOnStart) + { + PlayBGM(); + } + } + + public void PlayBGM() + { + if (audioSource == null || audioSource.clip == null) + { + Debug.LogWarning("[IntroBGMController] AudioSource 또는 AudioClip이 없습니다."); + return; + } + + if (bgmRoutine != null) + { + StopCoroutine(bgmRoutine); + } + + bgmRoutine = StartCoroutine(PlayBGMRoutine()); + } + + private IEnumerator PlayBGMRoutine() + { + if (startDelay > 0f) + { + yield return new WaitForSeconds(startDelay); + } + + audioSource.volume = useFadeIn ? 0f : targetVolume; + audioSource.loop = loop; + audioSource.Play(); + + if (useFadeIn) + { + float elapsed = 0f; + + while (elapsed < fadeInDuration) + { + elapsed += Time.deltaTime; + float t = Mathf.Clamp01(elapsed / fadeInDuration); + float easedT = EaseOutCubic(t); + + audioSource.volume = Mathf.Lerp(0f, targetVolume, easedT); + + yield return null; + } + + audioSource.volume = targetVolume; + } + + bgmRoutine = null; + } + + public void StopBGM(float fadeOutDuration = 1.0f) + { + if (audioSource == null) + return; + + if (bgmRoutine != null) + { + StopCoroutine(bgmRoutine); + } + + bgmRoutine = StartCoroutine(StopBGMRoutine(fadeOutDuration)); + } + + private IEnumerator StopBGMRoutine(float fadeOutDuration) + { + float startVolume = audioSource.volume; + float elapsed = 0f; + + while (elapsed < fadeOutDuration) + { + elapsed += Time.deltaTime; + float t = Mathf.Clamp01(elapsed / fadeOutDuration); + float easedT = EaseOutCubic(t); + + audioSource.volume = Mathf.Lerp(startVolume, 0f, easedT); + + yield return null; + } + + audioSource.volume = 0f; + audioSource.Stop(); + + bgmRoutine = null; + } + + private float EaseOutCubic(float t) + { + return 1f - Mathf.Pow(1f - t, 3f); + } +} \ No newline at end of file diff --git a/Assets/02_Scripts/Intro/IntroBGMController.cs.meta b/Assets/02_Scripts/Intro/IntroBGMController.cs.meta new file mode 100644 index 00000000..55dfdc27 --- /dev/null +++ b/Assets/02_Scripts/Intro/IntroBGMController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 653843badaadbba42a66bae2168ac836 \ No newline at end of file diff --git a/Assets/02_Scripts/Intro/IntroTitleSequence.cs b/Assets/02_Scripts/Intro/IntroTitleSequence.cs new file mode 100644 index 00000000..7925fc68 --- /dev/null +++ b/Assets/02_Scripts/Intro/IntroTitleSequence.cs @@ -0,0 +1,222 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +public class IntroTitleSequence : MonoBehaviour +{ + [System.Serializable] + public class IntroSequenceItem + { + [Header("Info")] + public string itemName; + + [Header("Target")] + public GameObject target; + + [Header("Timing")] + public float delay = 0f; + public float fadeDuration = 0.6f; + + [Header("Appear Motion")] + [Range(0.1f, 1f)] + public float startScaleRatio = 0.92f; + + public Vector3 startOffset = new Vector3(0f, -0.08f, 0f); + + [Header("After Appear")] + public bool enableFloatingAfterAppear = true; + public bool enableParallaxAfterAppear = true; + public bool enableButtonPulseAfterAppear = false; + } + + [Header("Sequence")] + public bool playOnStart = true; + public float globalDelayMultiplier = 1f; + public float globalFadeMultiplier = 1f; + public List sequenceItems = new List(); + + [Header("Optional Particle")] + public ParticleSystem petalParticle; + public float petalStartDelay = 0.2f; + + private readonly Dictionary originalPositions = new Dictionary(); + private readonly Dictionary originalScales = new Dictionary(); + + private void Start() + { + if (playOnStart) + { + PlaySequence(); + } + } + + public void PlaySequence() + { + StopAllCoroutines(); + + CacheOriginalTransforms(); + PrepareAllItems(); + + StartCoroutine(PlaySequenceRoutine()); + } + + private void CacheOriginalTransforms() + { + originalPositions.Clear(); + originalScales.Clear(); + + foreach (IntroSequenceItem item in sequenceItems) + { + if (item == null || item.target == null) + continue; + + originalPositions[item.target] = item.target.transform.localPosition; + originalScales[item.target] = item.target.transform.localScale; + } + } + + private void PrepareAllItems() + { + foreach (IntroSequenceItem item in sequenceItems) + { + if (item == null || item.target == null) + continue; + + GameObject target = item.target; + + target.SetActive(true); + + Vector3 originalPosition = originalPositions[target]; + Vector3 originalScale = originalScales[target]; + + target.transform.localPosition = originalPosition + item.startOffset; + target.transform.localScale = originalScale * item.startScaleRatio; + + SetTargetAlpha(target, 0f); + + SetOptionalMotionScripts(target, false, false, false); + } + } + + private IEnumerator PlaySequenceRoutine() + { + if (petalParticle != null) + { + petalParticle.Stop(); + + if (petalStartDelay > 0f) + yield return new WaitForSeconds(petalStartDelay); + + petalParticle.Play(); + } + + foreach (IntroSequenceItem item in sequenceItems) + { + if (item == null || item.target == null) + continue; + + StartCoroutine(AppearRoutine(item)); + } + } + + private IEnumerator AppearRoutine(IntroSequenceItem item) + { + float delay = Mathf.Max(0f, item.delay * globalDelayMultiplier); + float duration = Mathf.Max(0.01f, item.fadeDuration * globalFadeMultiplier); + + if (delay > 0f) + yield return new WaitForSeconds(delay); + + GameObject target = item.target; + + Vector3 originalPosition = originalPositions[target]; + Vector3 originalScale = originalScales[target]; + + Vector3 startPosition = originalPosition + item.startOffset; + Vector3 startScale = originalScale * item.startScaleRatio; + + target.transform.localPosition = startPosition; + target.transform.localScale = startScale; + SetTargetAlpha(target, 0f); + + float elapsed = 0f; + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + + float t = Mathf.Clamp01(elapsed / duration); + float easedT = EaseOutCubic(t); + + SetTargetAlpha(target, easedT); + + target.transform.localPosition = Vector3.Lerp(startPosition, originalPosition, easedT); + target.transform.localScale = Vector3.Lerp(startScale, originalScale, easedT); + + yield return null; + } + + SetTargetAlpha(target, 1f); + target.transform.localPosition = originalPosition; + target.transform.localScale = originalScale; + + SetOptionalMotionScripts( + target, + item.enableFloatingAfterAppear, + item.enableParallaxAfterAppear, + item.enableButtonPulseAfterAppear + ); + } + + private void SetTargetAlpha(GameObject target, float alpha) + { + SpriteRenderer[] spriteRenderers = target.GetComponentsInChildren(true); + foreach (SpriteRenderer sr in spriteRenderers) + { + Color color = sr.color; + color.a = alpha; + sr.color = color; + } + + Graphic[] graphics = target.GetComponentsInChildren(true); + foreach (Graphic graphic in graphics) + { + Color color = graphic.color; + color.a = alpha; + graphic.color = color; + } + + CanvasGroup[] canvasGroups = target.GetComponentsInChildren(true); + foreach (CanvasGroup canvasGroup in canvasGroups) + { + canvasGroup.alpha = alpha; + } + } + + private void SetOptionalMotionScripts(GameObject target, bool floating, bool parallax, bool buttonPulse) + { + IntroFloatingLayer[] floatingLayers = target.GetComponentsInChildren(true); + foreach (IntroFloatingLayer layer in floatingLayers) + { + layer.enabled = floating; + } + + IntroParallaxLayer[] parallaxLayers = target.GetComponentsInChildren(true); + foreach (IntroParallaxLayer layer in parallaxLayers) + { + layer.enabled = parallax; + } + + IntroButtonPulse[] buttonPulses = target.GetComponentsInChildren(true); + foreach (IntroButtonPulse pulse in buttonPulses) + { + pulse.enabled = buttonPulse; + } + } + + private float EaseOutCubic(float t) + { + return 1f - Mathf.Pow(1f - t, 3f); + } +} \ No newline at end of file diff --git a/Assets/02_Scripts/Intro/IntroTitleSequence.cs.meta b/Assets/02_Scripts/Intro/IntroTitleSequence.cs.meta new file mode 100644 index 00000000..3dd37ae7 --- /dev/null +++ b/Assets/02_Scripts/Intro/IntroTitleSequence.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: db9aafe58478a4b4ab17774501b5ef9d \ No newline at end of file diff --git a/Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav b/Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav new file mode 100644 index 00000000..8a4569a0 --- /dev/null +++ b/Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dec0989cc94e3d955d659c163b53a90b834d0d5657ddeabe824428f6145de0da +size 43808012 diff --git a/Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav.meta b/Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav.meta new file mode 100644 index 00000000..e1d2c843 --- /dev/null +++ b/Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav.meta @@ -0,0 +1,23 @@ +fileFormatVersion: 2 +guid: 2c7b766e21b92354eb51fc937c77a0dc +AudioImporter: + externalObjects: {} + serializedVersion: 8 + defaultSettings: + serializedVersion: 2 + loadType: 0 + sampleRateSetting: 0 + sampleRateOverride: 44100 + compressionFormat: 1 + quality: 1 + conversionMode: 0 + preloadAudioData: 0 + platformSettingOverrides: {} + forceToMono: 0 + normalize: 1 + loadInBackground: 0 + ambisonic: 0 + 3D: 1 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Easy Audio Cutter.meta b/Assets/Easy Audio Cutter.meta new file mode 100644 index 00000000..0b416fe7 --- /dev/null +++ b/Assets/Easy Audio Cutter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e521fddf8dfa5f447b4fce13ad1d02e1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Easy Audio Cutter/Documentation.txt b/Assets/Easy Audio Cutter/Documentation.txt new file mode 100644 index 00000000..0acbd619 --- /dev/null +++ b/Assets/Easy Audio Cutter/Documentation.txt @@ -0,0 +1,58 @@ +Easy Audio Cutter - User Manual +Trim Tab + +Trimming: Allows trimming a specific section of an audio clip by adjusting Start and End sliders. + +Fades: Fade In and Fade Out durations can be customized to create smooth transitions. You can fine-tune the fade shapes using the Animation Curve fields. + +Reverse Audio: Toggle this option to reverse the selected audio section (great for SFX like rewinds or magic effects). + +Waveform Seeking: Click anywhere on the waveform visualization to instantly jump (seek) to that time during preview. + +Real-time Update: The waveform updates in real-time as you adjust trim, fade, or reverse settings. + +Merge Tab + +Combine Clips: Combines multiple audio files into a single continuous clip. + +Reorder: Use the '↑' and '↓' buttons next to each clip to change their order in the sequence. + +Format Requirement: All audio files added to the list must have the same format (same sample rate and channel count) to ensure compatibility. + +Preview: You can listen to the entire merged sequence before saving. + +Adjust Volume Tab + +Volume Control: Increases or decreases the volume of a single selected audio file. + +Range: Use the slider range from -1 to 1. Negative values decrease volume, positive values increase it. + +Safety: The tool automatically clamps values to prevent audio clipping (distortion). + +Output: Saves the modified clip with an _Adjusted suffix to preserve the original file. + +General Features & Controls + +Shortcuts: + +Inspector: Click the ✂ (Scissor) icon in the AudioClip Inspector's preview bar. + +Project View: Right-click on any Audio Asset -> Easy Audio Cutter. + +Component: Right-click the AudioClip component title -> Easy Audio Cutter. + +Preview Volume: A global slider at the top allows you to adjust the volume of the editor's preview player without affecting the actual file. + +Undo/Redo: Full support (Ctrl+Z / Ctrl+Y) for all slider adjustments, toggles, and list operations. + +Waveform: Includes a red Playhead line to track playback position visually. + +Output Format: All processed audio files are exported in WAV format for maximum compatibility. + +Notes + +Requires an AudioClip to be assigned before performing operations. + +Preview audio is temporary and cleared from memory when the window is closed. + +For best results, use high-quality source files. \ No newline at end of file diff --git a/Assets/Easy Audio Cutter/Documentation.txt.meta b/Assets/Easy Audio Cutter/Documentation.txt.meta new file mode 100644 index 00000000..e249f425 --- /dev/null +++ b/Assets/Easy Audio Cutter/Documentation.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 69d72b5998006c24b8d052f1ff801681 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 316085 + packageName: Easy Audio Cutter + packageVersion: 1.2 + assetPath: Assets/Easy Audio Cutter/Documentation.txt + uploadId: 834314 diff --git a/Assets/Easy Audio Cutter/Scripts.meta b/Assets/Easy Audio Cutter/Scripts.meta new file mode 100644 index 00000000..41f401b8 --- /dev/null +++ b/Assets/Easy Audio Cutter/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 19973b10f1a39f542a8487cea75ca34d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Easy Audio Cutter/Scripts/Editor.meta b/Assets/Easy Audio Cutter/Scripts/Editor.meta new file mode 100644 index 00000000..dc26cf23 --- /dev/null +++ b/Assets/Easy Audio Cutter/Scripts/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3a0f3ad0dd3aa6b45ac67d3171120164 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs b/Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs new file mode 100644 index 00000000..db3c0bd8 --- /dev/null +++ b/Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs @@ -0,0 +1,59 @@ +using UnityEditor; +using UnityEngine; + +namespace EasyAudioCutter +{ + [InitializeOnLoad] + public static class AudioCutterProjectButton + { + private const string ButtonText = "✂ Edit"; + private const float ButtonWidth = 70f; + + static AudioCutterProjectButton() + { + EditorApplication.projectWindowItemOnGUI += DrawButtonOnSelectedAudioClip; + } + + private static void DrawButtonOnSelectedAudioClip(string guid, Rect selectionRect) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + AudioClip clip = AssetDatabase.LoadAssetAtPath(path); + + if (clip == null) + return; + + bool isSelected = Selection.activeObject == clip; + + if (!isSelected) + return; + + + Rect buttonRect = new Rect(selectionRect); + + if (selectionRect.width > 150) + { + buttonRect.x = selectionRect.xMax - ButtonWidth - 2f; + buttonRect.width = ButtonWidth; + buttonRect.height = selectionRect.height; + } + else + { + buttonRect.x = selectionRect.x + (selectionRect.width / 2f) - (ButtonWidth / 2f); + buttonRect.width = ButtonWidth; + buttonRect.y = selectionRect.y + 5f; + buttonRect.height = 25f; + } + + GUIStyle buttonStyle = EditorStyles.miniButton; + /* + if (GUI.Button(buttonRect, ButtonText, buttonStyle)) + { + EasyAudioCutter window = EditorWindow.GetWindow("Easy Audio Cutter"); + window.Show(); + window.Initialize(clip); + Event.current.Use(); + } + */ + } + } +} \ No newline at end of file diff --git a/Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs.meta b/Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs.meta new file mode 100644 index 00000000..4874d680 --- /dev/null +++ b/Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c11014813ef96fb41bbe7e7cc16b0e91 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 316085 + packageName: Easy Audio Cutter + packageVersion: 1.2 + assetPath: Assets/Easy Audio Cutter/Scripts/Editor/AudioCutterProjectButton.cs + uploadId: 834314 diff --git a/Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs b/Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs new file mode 100644 index 00000000..3d8c0d2e --- /dev/null +++ b/Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs @@ -0,0 +1,967 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Reflection; + +namespace EasyAudioCutter +{ + public class EasyAudioCutter : EditorWindow + { + private enum Tab { Trim, Merge, AdjustVolume } + + [SerializeField] + private Tab currentTab = Tab.Trim; + + [SerializeField] + private AudioClip sourceClip; + [SerializeField] + private float trimStart = 0f; + [SerializeField] + private float trimEnd = 1f; + [SerializeField] + private float fadeInDuration = 0f; + [SerializeField] + private float fadeOutDuration = 0f; + [SerializeField] + private AnimationCurve fadeInCurve = AnimationCurve.Linear(0, 0, 1, 1); + [SerializeField] + private AnimationCurve fadeOutCurve = AnimationCurve.Linear(0, 1, 1, 0); + [SerializeField] + private bool reverseAudio = false; + + [SerializeField] + private List mergeClips = new List(); + + [SerializeField] + private AudioClip volumeClip; + + private AudioSource previewAudioSource; + private AudioClip previewClip; + + private float[] waveformSamples; + private const int waveformWidth = 400; + private const float minWaveformHeight = 2f; + + [SerializeField] + private bool loopPreview = false; + private double previewStartTime = -1; + + [SerializeField] + private float volumeIncrease = 0f; + + [SerializeField] + private float previewVolume = 1f; + + private const string PREF_PREVIEW_VOLUME = "EasyAudioCutter_PreviewVolume"; + + [MenuItem("Tools/Easy Audio Cutter")] + public static void ShowWindow() + { + var window = GetWindow("Easy Audio Cutter"); + window.minSize = new Vector2(400, 400); + window.Show(); + } + + [MenuItem("Assets/Easy Audio Cutter", false, 20)] + public static void EditSelectedAudioClip() + { + AudioClip selectedClip = Selection.activeObject as AudioClip; + if (selectedClip != null) + { + OpenWithClip(selectedClip); + } + } + + [MenuItem("Assets/Easy Audio Cutter", true)] + public static bool ValidateEditSelectedAudioClip() + { + return Selection.activeObject is AudioClip; + } + + [MenuItem("CONTEXT/AudioClip/Easy Audio Cutter")] + public static void EditContextAudioClip(MenuCommand command) + { + AudioClip clip = command.context as AudioClip; + if (clip != null) + { + OpenWithClip(clip); + } + } + + public static void OpenWithClip(AudioClip clip) + { + var window = GetWindow("Easy Audio Cutter"); + window.minSize = new Vector2(400, 400); + window.Initialize(clip); + window.Show(); + window.Focus(); + } + + public void Initialize(AudioClip clip) + { + sourceClip = clip; + trimStart = 0f; + trimEnd = sourceClip != null ? sourceClip.length : 1f; + fadeInDuration = 0f; + fadeOutDuration = 0f; + reverseAudio = false; + mergeClips.Clear(); + volumeClip = null; + + if (currentTab == Tab.Merge && mergeClips.Count == 0) mergeClips.Add(clip); + if (currentTab == Tab.AdjustVolume) volumeClip = clip; + + previewVolume = EditorPrefs.GetFloat(PREF_PREVIEW_VOLUME, 1f); + + if (previewAudioSource != null) + { + previewAudioSource.Stop(); + DestroyPreviewClip(); + previewAudioSource.volume = previewVolume; + } + UpdateWaveform(); + Repaint(); + } + + private void OnEnable() + { + previewVolume = EditorPrefs.GetFloat(PREF_PREVIEW_VOLUME, 1f); + + GameObject go = new GameObject("AudioPreviewPlayer"); + go.hideFlags = HideFlags.HideAndDontSave; + previewAudioSource = go.AddComponent(); + previewAudioSource.playOnAwake = false; + previewAudioSource.loop = false; + previewAudioSource.volume = previewVolume; + + Undo.undoRedoPerformed += OnUndoRedo; + } + + private void OnDisable() + { + if (previewAudioSource != null) + DestroyImmediate(previewAudioSource.gameObject); + DestroyPreviewClip(); + + Undo.undoRedoPerformed -= OnUndoRedo; + } + + private void OnUndoRedo() + { + if (currentTab == Tab.Trim) + { + UpdateWaveform(); + } + Repaint(); + } + + private void OnGUI() + { + EasyAudioCutterTheme.EnsureStyles(); + + if (previewAudioSource != null) + { + if (Mathf.Abs(previewAudioSource.volume - previewVolume) > 0.01f) + { + previewAudioSource.volume = previewVolume; + } + + if (previewAudioSource.isPlaying) + { + Repaint(); + } + } + + Color bgColor = EditorGUIUtility.isProSkin ? new Color(0.07f, 0.1f, 0.2f) : new Color(0.8f, 0.8f, 0.8f); + EditorGUI.DrawRect(new Rect(0, 0, position.width, position.height), bgColor); + + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + + if (GUILayout.Button("Help", EasyAudioCutterTheme.ButtonStyle, GUILayout.Width(100))) + { + DrawHelpDialog(); + } + EditorGUILayout.Space(10); + + EditorGUI.BeginChangeCheck(); + Tab newTab = (Tab)GUILayout.Toolbar((int)currentTab, new string[] { "Trim", "Merge", "Adjust Volume" }); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Tab"); + currentTab = newTab; + } + + EditorGUILayout.Space(5); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Preview Volume", EasyAudioCutterTheme.SliderLabelStyle, GUILayout.Width(100)); + EditorGUI.BeginChangeCheck(); + float newVol = EditorGUILayout.Slider(previewVolume, 0f, 1f); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Preview Volume"); + previewVolume = newVol; + EditorPrefs.SetFloat(PREF_PREVIEW_VOLUME, previewVolume); + if (previewAudioSource != null) previewAudioSource.volume = previewVolume; + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + EasyAudioCutterTheme.DrawSeparator(); + EditorGUILayout.Space(); + + switch (currentTab) + { + case Tab.Trim: DrawTrimTab(); break; + case Tab.Merge: DrawMergeTab(); break; + case Tab.AdjustVolume: DrawAdjustVolumeTab(); break; + } + } + + private void DrawHelpDialog() + { + string message = "Easy Audio Cutter - Help\n\n" + + "**Trim Tab**\n" + + "• Allows trimming a specific section with fade in/out effects.\n" + + "• Toggle 'Reverse Audio' to play the section backwards.\n" + + "• Click anywhere on the waveform to seek/jump to that time.\n" + + "• Supports adjustable start/end times and custom fade curves.\n" + + "• Real-time waveform visualization with a playhead tracker.\n" + + "• Tip: Use the curve editor for creative fade transitions.\n\n" + + + "**Merge Tab**\n" + + "• Combines multiple audio files into a single clip.\n" + + "• Use '↑' and '↓' buttons to reorder clips in the list.\n" + + "• All audio files must have the same format (sample rate/channels).\n" + + "• Provides preview functionality for the merged sequence.\n" + + "• Tip: Verify formats beforehand to avoid merge errors.\n\n" + + + "**Adjust Volume Tab**\n" + + "• Adjusts the volume of a single selected audio clip.\n" + + "• Use -1 to 1 range: negative decreases, positive increases volume.\n" + + "• Real-time preview of volume changes before saving.\n" + + "• Saves modified clip with '_Adjusted' suffix.\n" + + "• Tip: Avoid values close to -1 or 1 to prevent clipping.\n\n" + + + "**General Information**\n" + + "• Shortcuts: Right-click any Audio Asset or use the '✂' button in the Inspector.\n" + + "• Supports Undo/Redo (Ctrl+Z) for all slider and toggle actions.\n" + + "• Global 'Preview Volume' slider allows adjusting playback level.\n" + + "• Saves all processed audio in WAV format.\n" + + "• Developed and updated as of July 05, 2025.\n"; + EditorUtility.DisplayDialog("Easy Audio Cutter - Help", message, "OK"); + } + + private void DrawTrimTab() + { + EditorGUILayout.LabelField("Trim AudioClip", EasyAudioCutterTheme.HeaderStyle); + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + AudioClip newSource = (AudioClip)EditorGUILayout.ObjectField("Source Clip", sourceClip, typeof(AudioClip), false); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Source Clip"); + Initialize(newSource); + } + + if (sourceClip == null) + { + EditorGUILayout.HelpBox("Please assign an AudioClip to trim.", MessageType.Info); + return; + } + + EditorGUILayout.LabelField($"Length: {sourceClip.length:F2} seconds", EasyAudioCutterTheme.LabelStyle); + EditorGUILayout.Space(); + + float oldTrimStart = trimStart; + float oldTrimEnd = trimEnd; + float oldFadeIn = fadeInDuration; + float oldFadeOut = fadeOutDuration; + bool oldReverse = reverseAudio; + + EditorGUI.BeginChangeCheck(); + float newTrimStart = EditorGUILayout.Slider("Start Time", trimStart, 0f, sourceClip.length); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Trim Start"); + trimStart = newTrimStart; + } + + EditorGUI.BeginChangeCheck(); + float newTrimEnd = EditorGUILayout.Slider("End Time", trimEnd, 0f, sourceClip.length); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Trim End"); + trimEnd = newTrimEnd; + } + trimEnd = Mathf.Max(trimStart, trimEnd); + + EditorGUI.BeginChangeCheck(); + float newFadeIn = EditorGUILayout.Slider("Fade In Duration", fadeInDuration, 0f, trimEnd - trimStart); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Fade In"); + fadeInDuration = newFadeIn; + } + + EditorGUI.BeginChangeCheck(); + float newFadeOut = EditorGUILayout.Slider("Fade Out Duration", fadeOutDuration, 0f, trimEnd - trimStart); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Fade Out"); + fadeOutDuration = newFadeOut; + } + + EditorGUI.BeginChangeCheck(); + bool newReverse = EditorGUILayout.Toggle("Reverse Audio", reverseAudio); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Toggle Reverse"); + reverseAudio = newReverse; + } + + if (!Mathf.Approximately(oldTrimStart, trimStart) || + !Mathf.Approximately(oldTrimEnd, trimEnd) || + !Mathf.Approximately(oldFadeIn, fadeInDuration) || + !Mathf.Approximately(oldFadeOut, fadeOutDuration) || + oldReverse != reverseAudio) + { + UpdateWaveform(); + } + + EditorGUILayout.Space(); + + Rect waveformRect = GUILayoutUtility.GetRect(position.width - 20, 100); + waveformRect.x += 10; + waveformRect.width -= 20; + DrawWaveform(waveformRect); + + if (Event.current.type == EventType.MouseDown && waveformRect.Contains(Event.current.mousePosition)) + { + if (previewAudioSource != null && previewClip != null) + { + float clickPercent = (Event.current.mousePosition.x - waveformRect.x) / waveformRect.width; + clickPercent = Mathf.Clamp01(clickPercent); + + if (!previewAudioSource.isPlaying) + { + CreatePreviewClipAndPlay(); + } + + previewAudioSource.time = clickPercent * previewClip.length; + Repaint(); + } + } + + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + bool newLoop = EditorGUILayout.Toggle("Loop Preview", loopPreview); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Toggle Loop"); + loopPreview = newLoop; + } + + if (GUILayout.Button(previewAudioSource.isPlaying ? "Stop Preview" : "Play Preview", EasyAudioCutterTheme.ButtonStyle)) + { + if (previewAudioSource.isPlaying) + { + previewAudioSource.Stop(); + DestroyPreviewClip(); + } + else + { + CreatePreviewClipAndPlay(); + } + } + + EditorGUILayout.Space(); + if (GUILayout.Button("Process and Save Trimmed Clip", EasyAudioCutterTheme.ButtonStyle)) + { + ProcessAndSaveTrimmedClip(); + } + } + + private void DrawMergeTab() + { + EditorGUILayout.LabelField("Merge AudioClips", EasyAudioCutterTheme.HeaderStyle); + EditorGUILayout.Space(); + + for (int i = 0; i < mergeClips.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + if (GUILayout.Button("↑", GUILayout.Width(20))) + { + if (i > 0) + { + Undo.RecordObject(this, "Move Clip Up"); + var temp = mergeClips[i]; + mergeClips[i] = mergeClips[i - 1]; + mergeClips[i - 1] = temp; + } + } + if (GUILayout.Button("↓", GUILayout.Width(20))) + { + if (i < mergeClips.Count - 1) + { + Undo.RecordObject(this, "Move Clip Down"); + var temp = mergeClips[i]; + mergeClips[i] = mergeClips[i + 1]; + mergeClips[i + 1] = temp; + } + } + + EditorGUI.BeginChangeCheck(); + AudioClip newClip = (AudioClip)EditorGUILayout.ObjectField(mergeClips[i], typeof(AudioClip), false); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Merge Clip"); + mergeClips[i] = newClip; + } + + if (GUILayout.Button("X", EasyAudioCutterTheme.ButtonStyle, GUILayout.Width(20))) + { + Undo.RecordObject(this, "Remove Merge Clip"); + mergeClips.RemoveAt(i); + i--; + } + EditorGUILayout.EndHorizontal(); + } + + if (GUILayout.Button("+ Add AudioClip", EasyAudioCutterTheme.ButtonStyle)) + { + Undo.RecordObject(this, "Add Merge Clip"); + mergeClips.Add(null); + } + + EditorGUILayout.Space(); + + if (mergeClips.Count >= 2) + { + if (GUILayout.Button(previewAudioSource.isPlaying ? "Stop Preview" : "Play Merged Preview", EasyAudioCutterTheme.ButtonStyle)) + { + if (previewAudioSource.isPlaying) + { + previewAudioSource.Stop(); + DestroyPreviewClip(); + } + else + { + CreateMergedPreviewClipAndPlay(); + } + } + + EditorGUILayout.Space(); + + if (GUILayout.Button("Process and Save Merged Clip", EasyAudioCutterTheme.ButtonStyle)) + { + ProcessAndSaveMergedClip(); + } + } + else + { + EditorGUILayout.HelpBox("Add at least two AudioClips to merge.", MessageType.Info); + } + } + + private void DrawAdjustVolumeTab() + { + EditorGUILayout.LabelField("Adjust Volume", EasyAudioCutterTheme.HeaderStyle); + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + AudioClip newVolumeClip = (AudioClip)EditorGUILayout.ObjectField("Audio Clip", volumeClip, typeof(AudioClip), false); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Volume Clip"); + volumeClip = newVolumeClip; + } + + EditorGUILayout.Space(); + + EditorGUI.BeginChangeCheck(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Volume Adjustment", EasyAudioCutterTheme.SliderLabelStyle); + float newVolume = EditorGUILayout.Slider(volumeIncrease, -1f, 1f); + EditorGUILayout.EndHorizontal(); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Change Volume Amount"); + volumeIncrease = newVolume; + } + + EditorGUILayout.LabelField("Note: -1 to 1 range.", EditorStyles.helpBox); + + if (volumeClip != null) + { + if (GUILayout.Button("Apply Volume Adjustment (Preview)", EasyAudioCutterTheme.ButtonStyle)) + { + ApplyVolumeIncrease(); + } + + EditorGUILayout.Space(); + + if (GUILayout.Button("Process and Save Adjusted Clip", EasyAudioCutterTheme.ButtonStyle)) + { + ProcessAndSaveAdjustedClips(); + } + } + else + { + EditorGUILayout.HelpBox("Add an AudioClip to adjust volume.", MessageType.Info); + } + } + + private void DestroyPreviewClip() + { + if (previewClip != null) + { + DestroyImmediate(previewClip); + previewClip = null; + previewStartTime = -1; + } + } + + private void CreatePreviewClipAndPlay() + { + if (sourceClip == null || trimEnd <= trimStart) return; + + previewStartTime = EditorApplication.timeSinceStartup; + + int freq = sourceClip.frequency; + int channels = sourceClip.channels; + + int startSample = Mathf.FloorToInt(trimStart * freq) * channels; + int sampleLength = Mathf.FloorToInt((trimEnd - trimStart) * freq) * channels; + + float[] allData = new float[sourceClip.samples * channels]; + sourceClip.GetData(allData, 0); + + float[] trimmedData = new float[sampleLength]; + System.Array.Copy(allData, startSample, trimmedData, 0, sampleLength); + + if (reverseAudio) + { + System.Array.Reverse(trimmedData); + } + + ApplyFades(trimmedData, freq, channels); + + previewClip = AudioClip.Create("PreviewTrim", sampleLength / channels, channels, freq, false); + previewClip.SetData(trimmedData, 0); + previewAudioSource.clip = previewClip; + previewAudioSource.volume = previewVolume; + previewAudioSource.loop = loopPreview; + previewAudioSource.Play(); + } + + private void ProcessAndSaveTrimmedClip() + { + if (sourceClip == null) return; + + int freq = sourceClip.frequency; + int channels = sourceClip.channels; + + int startSample = Mathf.FloorToInt(trimStart * freq) * channels; + int sampleLength = Mathf.FloorToInt((trimEnd - trimStart) * freq) * channels; + + float[] srcData = new float[sourceClip.samples * channels]; + sourceClip.GetData(srcData, 0); + + float[] trimmedData = new float[sampleLength]; + System.Array.Copy(srcData, startSample, trimmedData, 0, sampleLength); + + if (reverseAudio) + { + System.Array.Reverse(trimmedData); + } + + ApplyFades(trimmedData, freq, channels); + + SaveWav(trimmedData, freq, channels, "Trimmed_" + sourceClip.name + (reverseAudio ? "_Reverse" : "")); + } + + private void ApplyFades(float[] data, int freq, int channels) + { + int fadeInSamples = Mathf.FloorToInt(fadeInDuration * freq) * channels; + for (int i = 0; i < fadeInSamples && i < data.Length; i++) + { + float t = (float)i / fadeInSamples; + data[i] *= fadeInCurve.Evaluate(t); + } + + int fadeOutSamples = Mathf.FloorToInt(fadeOutDuration * freq) * channels; + int startFadeOut = data.Length - fadeOutSamples; + for (int i = 0; i < fadeOutSamples && (startFadeOut + i) < data.Length; i++) + { + float t = (float)i / fadeOutSamples; + data[startFadeOut + i] *= fadeOutCurve.Evaluate(t); + } + } + + private void CreateMergedPreviewClipAndPlay() + { + if (mergeClips.Count < 2) return; + + int freq = mergeClips[0].frequency; + int channels = mergeClips[0].channels; + + List mergedSamples = new List(); + + foreach (var clip in mergeClips) + { + if (clip == null || clip.frequency != freq || clip.channels != channels) + { + EditorUtility.DisplayDialog("Error", "All clips must have same format.", "OK"); + return; + } + + float[] data = new float[clip.samples * channels]; + clip.GetData(data, 0); + mergedSamples.AddRange(data); + } + + float[] mergedData = mergedSamples.ToArray(); + int samples = mergedData.Length / channels; + + DestroyPreviewClip(); + previewClip = AudioClip.Create("PreviewMerged", samples, channels, freq, false); + previewClip.SetData(mergedData, 0); + previewAudioSource.clip = previewClip; + previewAudioSource.volume = previewVolume; + previewAudioSource.loop = loopPreview; + previewAudioSource.Play(); + } + + private void ProcessAndSaveMergedClip() + { + if (mergeClips.Count < 2) return; + + int freq = mergeClips[0].frequency; + int channels = mergeClips[0].channels; + + List mergedSamples = new List(); + + foreach (var clip in mergeClips) + { + if (clip == null || clip.frequency != freq || clip.channels != channels) + { + EditorUtility.DisplayDialog("Error", "All clips must have same format.", "OK"); + return; + } + + float[] data = new float[clip.samples * channels]; + clip.GetData(data, 0); + mergedSamples.AddRange(data); + } + + SaveWav(mergedSamples.ToArray(), freq, channels, "MergedAudio"); + } + + private void ApplyVolumeIncrease() + { + if (previewAudioSource.isPlaying) + { + previewAudioSource.Stop(); + DestroyPreviewClip(); + } + + if (volumeClip != null) + { + float[] data = new float[volumeClip.samples * volumeClip.channels]; + volumeClip.GetData(data, 0); + + for (int i = 0; i < data.Length; i++) + { + data[i] += data[i] * volumeIncrease; + if (data[i] > 1f) data[i] = 1f; + if (data[i] < -1f) data[i] = -1f; + } + + int samples = data.Length / volumeClip.channels; + AudioClip tempClip = AudioClip.Create(volumeClip.name + "_Temp", samples, volumeClip.channels, volumeClip.frequency, false); + tempClip.SetData(data, 0); + previewAudioSource.clip = tempClip; + previewAudioSource.volume = previewVolume; + previewAudioSource.loop = false; + previewAudioSource.Play(); + } + } + + private void ProcessAndSaveAdjustedClips() + { + if (volumeClip != null) + { + float[] data = new float[volumeClip.samples * volumeClip.channels]; + volumeClip.GetData(data, 0); + + for (int i = 0; i < data.Length; i++) + { + data[i] += data[i] * volumeIncrease; + if (data[i] > 1f) data[i] = 1f; + if (data[i] < -1f) data[i] = -1f; + } + + SaveWav(data, volumeClip.frequency, volumeClip.channels, volumeClip.name + "_Adjusted"); + EditorUtility.DisplayDialog("Done", "Adjusted clip saved!", "OK"); + } + } + + private void SaveWav(float[] data, int frequency, int channels, string defaultName) + { + int samples = data.Length / channels; + AudioClip clip = AudioClip.Create(defaultName, samples, channels, frequency, false); + clip.SetData(data, 0); + + string path = EditorUtility.SaveFilePanelInProject("Save Audio", defaultName + ".wav", "wav", "Choose location"); + + if (!string.IsNullOrEmpty(path)) + { + byte[] bytes = WavUtility.FromAudioClip(clip); + File.WriteAllBytes(path, bytes); + AssetDatabase.ImportAsset(path); + EditorUtility.DisplayDialog("Done", "Audio saved!", "OK"); + } + } + + private void UpdateWaveform() + { + if (sourceClip == null) + { + waveformSamples = null; + return; + } + + int freq = sourceClip.frequency; + int channels = sourceClip.channels; + + int startSample = Mathf.FloorToInt(trimStart * freq) * channels; + int lengthSamples = Mathf.FloorToInt((trimEnd - trimStart) * freq) * channels; + + float[] allData = new float[sourceClip.samples * channels]; + sourceClip.GetData(allData, 0); + + float[] trimmedData = new float[lengthSamples]; + System.Array.Copy(allData, startSample, trimmedData, 0, lengthSamples); + + if (reverseAudio) + { + System.Array.Reverse(trimmedData); + } + + ApplyFades(trimmedData, freq, channels); + + int samplesPerPixel = Mathf.Max(1, trimmedData.Length / waveformWidth); + waveformSamples = new float[waveformWidth]; + for (int i = 0; i < waveformWidth; i++) + { + float max = 0f; + int start = i * samplesPerPixel; + int end = Mathf.Min(start + samplesPerPixel, trimmedData.Length); + for (int j = start; j < end; j += channels) + { + float val = Mathf.Abs(trimmedData[j]); + if (val > max) max = val; + } + waveformSamples[i] = max; + } + + Repaint(); + } + + private void DrawWaveform(Rect rect) + { + if (waveformSamples == null || waveformSamples.Length == 0) return; + + Color waveBgColor = EditorGUIUtility.isProSkin ? new Color(0.05f, 0.08f, 0.15f) : new Color(0.9f, 0.9f, 0.9f); + EditorGUI.DrawRect(rect, waveBgColor); + float midY = rect.y + rect.height / 2f; + + Handles.BeginGUI(); + Handles.color = new Color(1f, 0.6f, 0f); + + float totalWaveformWidth = waveformSamples.Length; + float xOffset = (rect.width - totalWaveformWidth) / 2f; + + for (int i = 0; i < waveformSamples.Length; i++) + { + float x = rect.x + xOffset + i; + float height = Mathf.Max(minWaveformHeight, waveformSamples[i] * rect.height); + Handles.DrawLine( + new Vector3(x, midY - height / 2), + new Vector3(x, midY + height / 2) + ); + } + + float startX = rect.x + xOffset; + float endX = rect.x + xOffset + waveformSamples.Length; + + Handles.color = EditorGUIUtility.isProSkin ? Color.white : Color.black; + Handles.DrawLine(new Vector3(startX, rect.y), new Vector3(startX, rect.y + rect.height)); + Handles.DrawLine(new Vector3(endX, rect.y), new Vector3(endX, rect.y + rect.height)); + + if (previewAudioSource != null && previewAudioSource.isPlaying && previewClip != null) + { + float progress = previewAudioSource.time / previewClip.length; + float playheadX = rect.x + (progress * rect.width); + + Handles.color = Color.red; + Handles.DrawLine(new Vector3(playheadX, rect.y), new Vector3(playheadX, rect.y + rect.height)); + } + + Handles.EndGUI(); + } + } + + [CustomEditor(typeof(AudioClip))] + public class AudioClipInspectorOverride : Editor + { + private Editor _defaultEditor; + + private void OnEnable() + { + var type = typeof(Editor).Assembly.GetType("UnityEditor.AudioClipInspector"); + if (type != null) + _defaultEditor = CreateEditor(target, type); + } + + private void OnDisable() + { + if (_defaultEditor != null) DestroyImmediate(_defaultEditor); + } + + public override void OnInspectorGUI() + { + if (_defaultEditor != null) + { + _defaultEditor.OnInspectorGUI(); + } + else + { + base.OnInspectorGUI(); + } + } + + public override bool HasPreviewGUI() + { + return _defaultEditor != null && _defaultEditor.HasPreviewGUI(); + } + + public override void OnPreviewGUI(Rect r, GUIStyle background) + { + if (_defaultEditor != null) _defaultEditor.OnPreviewGUI(r, background); + } + + public override void OnInteractivePreviewGUI(Rect r, GUIStyle background) + { + if (_defaultEditor != null) _defaultEditor.OnInteractivePreviewGUI(r, background); + } + + public override void OnPreviewSettings() + { + if (_defaultEditor != null) + { + _defaultEditor.OnPreviewSettings(); + } + + if (GUILayout.Button(new GUIContent("✂", "Open in Easy Audio Cutter"), EditorStyles.toolbarButton, GUILayout.Width(30))) + { + EasyAudioCutter.OpenWithClip((AudioClip)target); + } + } + } + + public static class EasyAudioCutterTheme + { + public static GUIStyle HeaderStyle { get; private set; } + public static GUIStyle ButtonStyle { get; private set; } + public static GUIStyle SliderLabelStyle { get; private set; } + public static GUIStyle LabelStyle { get; private set; } + + private static bool isInitialized = false; + private static bool wasProSkin = false; + + public static void EnsureStyles() + { + if (!isInitialized || wasProSkin != EditorGUIUtility.isProSkin) + { + SetupStyles(); + } + } + + public static void DrawSeparator() + { + var rect = GUILayoutUtility.GetRect(1f, 1f); + EditorGUI.DrawRect(rect, new Color(0.5f, 0.5f, 0.5f, 0.5f)); + } + + private static void SetupStyles() + { + wasProSkin = EditorGUIUtility.isProSkin; + bool isDark = wasProSkin; + + HeaderStyle = new GUIStyle(EditorStyles.boldLabel); + HeaderStyle.normal.textColor = isDark ? new Color(0.4f, 0.8f, 1f) : new Color(0.1f, 0.3f, 0.5f); + HeaderStyle.fontSize = 14; + + ButtonStyle = new GUIStyle(GUI.skin.button); + ButtonStyle.normal.textColor = isDark ? Color.white : Color.black; + ButtonStyle.fontSize = 12; + ButtonStyle.padding = new RectOffset(6, 6, 4, 4); + + SliderLabelStyle = new GUIStyle(EditorStyles.label); + SliderLabelStyle.normal.textColor = isDark ? Color.cyan : new Color(0.0f, 0.4f, 0.7f); + + LabelStyle = new GUIStyle(EditorStyles.label); + LabelStyle.normal.textColor = isDark ? Color.white : Color.black; + + isInitialized = true; + } + } + + public static class WavUtility + { + public static byte[] FromAudioClip(AudioClip clip) + { + using (var memoryStream = new MemoryStream()) + { + using (var writer = new BinaryWriter(memoryStream)) + { + var hz = clip.frequency; + var channels = clip.channels; + var samples = clip.samples; + writer.Write(Encoding.ASCII.GetBytes("RIFF")); + writer.Write(36 + samples * channels * 2); + writer.Write(Encoding.ASCII.GetBytes("WAVE")); + writer.Write(Encoding.ASCII.GetBytes("fmt ")); + writer.Write(16); + writer.Write((ushort)1); + writer.Write((ushort)channels); + writer.Write(hz); + writer.Write(hz * channels * 2); + writer.Write((ushort)(channels * 2)); + writer.Write((ushort)16); + writer.Write(Encoding.ASCII.GetBytes("data")); + writer.Write(samples * channels * 2); + + float[] data = new float[samples * channels]; + clip.GetData(data, 0); + + foreach (var sample in data) + { + writer.Write((short)(sample * 32767f)); + } + } + return memoryStream.ToArray(); + } + } + } +} \ No newline at end of file diff --git a/Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs.meta b/Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs.meta new file mode 100644 index 00000000..4aea67ac --- /dev/null +++ b/Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f8a7022201f6834469cfd3979098c29e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 316085 + packageName: Easy Audio Cutter + packageVersion: 1.2 + assetPath: Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs + uploadId: 834314 diff --git a/Assets/XR/Settings/OpenXR Package Settings.asset b/Assets/XR/Settings/OpenXR Package Settings.asset index 1dcd5548..318d128f 100644 --- a/Assets/XR/Settings/OpenXR Package Settings.asset +++ b/Assets/XR/Settings/OpenXR Package Settings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98691639fd584fbe114c35e839cfcabbf0b982c56cdd204a244fcf4b5efb0828 -size 119132 +oid sha256:f4eb593a157b64bdf26aa29da72fd36146705ad99298979e6a224a9e5c398f29 +size 42280