타이틀 타이밍
This commit is contained in:
Binary file not shown.
BIN
Assets/01_Scenes/Intro_Test2_timing.unity
LFS
Normal file
BIN
Assets/01_Scenes/Intro_Test2_timing.unity
LFS
Normal file
Binary file not shown.
7
Assets/01_Scenes/Intro_Test2_timing.unity.meta
Normal file
7
Assets/01_Scenes/Intro_Test2_timing.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16134748a5d8b544bbcab26197b912b6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
133
Assets/02_Scripts/Intro/IntroBGMController.cs
Normal file
133
Assets/02_Scripts/Intro/IntroBGMController.cs
Normal file
@@ -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>();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Intro/IntroBGMController.cs.meta
Normal file
2
Assets/02_Scripts/Intro/IntroBGMController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 653843badaadbba42a66bae2168ac836
|
||||
222
Assets/02_Scripts/Intro/IntroTitleSequence.cs
Normal file
222
Assets/02_Scripts/Intro/IntroTitleSequence.cs
Normal file
@@ -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<IntroSequenceItem> sequenceItems = new List<IntroSequenceItem>();
|
||||
|
||||
[Header("Optional Particle")]
|
||||
public ParticleSystem petalParticle;
|
||||
public float petalStartDelay = 0.2f;
|
||||
|
||||
private readonly Dictionary<GameObject, Vector3> originalPositions = new Dictionary<GameObject, Vector3>();
|
||||
private readonly Dictionary<GameObject, Vector3> originalScales = new Dictionary<GameObject, Vector3>();
|
||||
|
||||
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<SpriteRenderer>(true);
|
||||
foreach (SpriteRenderer sr in spriteRenderers)
|
||||
{
|
||||
Color color = sr.color;
|
||||
color.a = alpha;
|
||||
sr.color = color;
|
||||
}
|
||||
|
||||
Graphic[] graphics = target.GetComponentsInChildren<Graphic>(true);
|
||||
foreach (Graphic graphic in graphics)
|
||||
{
|
||||
Color color = graphic.color;
|
||||
color.a = alpha;
|
||||
graphic.color = color;
|
||||
}
|
||||
|
||||
CanvasGroup[] canvasGroups = target.GetComponentsInChildren<CanvasGroup>(true);
|
||||
foreach (CanvasGroup canvasGroup in canvasGroups)
|
||||
{
|
||||
canvasGroup.alpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOptionalMotionScripts(GameObject target, bool floating, bool parallax, bool buttonPulse)
|
||||
{
|
||||
IntroFloatingLayer[] floatingLayers = target.GetComponentsInChildren<IntroFloatingLayer>(true);
|
||||
foreach (IntroFloatingLayer layer in floatingLayers)
|
||||
{
|
||||
layer.enabled = floating;
|
||||
}
|
||||
|
||||
IntroParallaxLayer[] parallaxLayers = target.GetComponentsInChildren<IntroParallaxLayer>(true);
|
||||
foreach (IntroParallaxLayer layer in parallaxLayers)
|
||||
{
|
||||
layer.enabled = parallax;
|
||||
}
|
||||
|
||||
IntroButtonPulse[] buttonPulses = target.GetComponentsInChildren<IntroButtonPulse>(true);
|
||||
foreach (IntroButtonPulse pulse in buttonPulses)
|
||||
{
|
||||
pulse.enabled = buttonPulse;
|
||||
}
|
||||
}
|
||||
|
||||
private float EaseOutCubic(float t)
|
||||
{
|
||||
return 1f - Mathf.Pow(1f - t, 3f);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Intro/IntroTitleSequence.cs.meta
Normal file
2
Assets/02_Scripts/Intro/IntroTitleSequence.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db9aafe58478a4b4ab17774501b5ef9d
|
||||
BIN
Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav
LFS
Normal file
BIN
Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav
LFS
Normal file
Binary file not shown.
23
Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav.meta
Normal file
23
Assets/11_Audio/BGM/Intro/@@Paradise_꽃보다 남자 MR 1.wav.meta
Normal file
@@ -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:
|
||||
8
Assets/Easy Audio Cutter.meta
Normal file
8
Assets/Easy Audio Cutter.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e521fddf8dfa5f447b4fce13ad1d02e1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
58
Assets/Easy Audio Cutter/Documentation.txt
Normal file
58
Assets/Easy Audio Cutter/Documentation.txt
Normal file
@@ -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.
|
||||
14
Assets/Easy Audio Cutter/Documentation.txt.meta
Normal file
14
Assets/Easy Audio Cutter/Documentation.txt.meta
Normal file
@@ -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
|
||||
8
Assets/Easy Audio Cutter/Scripts.meta
Normal file
8
Assets/Easy Audio Cutter/Scripts.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19973b10f1a39f542a8487cea75ca34d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Easy Audio Cutter/Scripts/Editor.meta
Normal file
8
Assets/Easy Audio Cutter/Scripts/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a0f3ad0dd3aa6b45ac67d3171120164
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<AudioClip>(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<EasyAudioCutter>("Easy Audio Cutter");
|
||||
window.Show();
|
||||
window.Initialize(clip);
|
||||
Event.current.Use();
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
967
Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs
Normal file
967
Assets/Easy Audio Cutter/Scripts/Editor/EasyAudioCutter.cs
Normal file
@@ -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<AudioClip> mergeClips = new List<AudioClip>();
|
||||
|
||||
[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<EasyAudioCutter>("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<EasyAudioCutter>("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<AudioSource>();
|
||||
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<float> mergedSamples = new List<float>();
|
||||
|
||||
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<float> mergedSamples = new List<float>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
Reference in New Issue
Block a user