타이틀 타이밍

This commit is contained in:
2026-07-13 11:25:44 +09:00
parent 33f2c13223
commit c30a74c9c3
19 changed files with 1557 additions and 4 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 16134748a5d8b544bbcab26197b912b6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 653843badaadbba42a66bae2168ac836

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db9aafe58478a4b4ab17774501b5ef9d

View 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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e521fddf8dfa5f447b4fce13ad1d02e1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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.

View 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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 19973b10f1a39f542a8487cea75ca34d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3a0f3ad0dd3aa6b45ac67d3171120164
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
*/
}
}
}

View File

@@ -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

View 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();
}
}
}
}

View File

@@ -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