using UnityEditor; using UnityEngine; using System.IO; using System.Text.RegularExpressions; using System.Collections.Generic; using System.Linq; /// /// 音频GUID转移工具 - 将MP3/WAV文件的GUID转移到OGG文件 /// /// 此脚本解决了Unity Timeline中音频资源格式转换的问题: /// 当需要将Timeline中的WAV/MP3音频替换为OGG格式时,直接替换文件会导致引用丢失。 /// 本工具通过转移GUID,确保Timeline中的引用保持不变。 /// public static class AudioGUIDTransfer { // 定义我们关心的原始音频扩展名 private static readonly HashSet OriginalAudioExtensions = new HashSet { ".mp3", ".wav" }; // 定义目标音频扩展名 private const string TargetAudioExtension = ".ogg"; // 在Unity菜单栏的 "Assets" 下添加一个菜单项 [MenuItem("Assets/AudioFormatTransfer")] public static void TransferGUIDsInSelectedFolder() { // 获取当前在Project窗口中选中的对象 Object selectedObject = Selection.activeObject; // --- 输入验证 --- if (selectedObject == null) { EditorUtility.DisplayDialog("错误", "请在Project窗口中选择一个包含音频文件的文件夹。", "确定"); return; } string selectedPath = AssetDatabase.GetAssetPath(selectedObject); if (!AssetDatabase.IsValidFolder(selectedPath)) { EditorUtility.DisplayDialog("错误", "请选择一个有效的文件夹。", "确定"); return; } // --- 确认对话框(对于破坏性操作至关重要) --- if (!EditorUtility.DisplayDialog("确认操作", $"您确定要将文件夹 '{selectedPath}' 中所有MP3/WAV文件的GUID转移到对应的同名OGG文件,并删除原始文件吗?\n\n此操作不可逆,请务必提前备份您的项目!", "是", "否")) { return; // 用户取消了操作 } // --- 核心处理逻辑 --- ProcessFolder(selectedPath); // --- 完成消息 --- EditorUtility.DisplayDialog("完成", "MP3/WAV GUID转移到OGG并删除原始文件的操作已完成。请检查Console窗口获取详细日志。", "确定"); } /// /// 处理给定文件夹,将MP3/WAV文件的GUID转移到对应的OGG文件,并删除原始文件。 /// /// 要处理的文件夹的完整路径。 private static void ProcessFolder(string folderPath) { // 获取选中文件夹中所有文件(仅限顶层目录,不包括子文件夹) string[] allFilesInFolder = Directory.GetFiles(folderPath, "*.*", SearchOption.TopDirectoryOnly); // 按基础名称(不含扩展名)对文件进行分组 Dictionary> fileGroups = new Dictionary>(); foreach (string filePath in allFilesInFolder) { // 跳过.meta文件,因为我们只关心实际资源文件 if (filePath.EndsWith(".meta")) continue; string fileName = Path.GetFileName(filePath); string baseName = GetBaseNameWithoutExtensions(fileName); if (!fileGroups.ContainsKey(baseName)) { fileGroups.Add(baseName, new List()); } fileGroups[baseName].Add(filePath); } int processedCount = 0; int skippedCount = 0; // 遍历每个文件组 foreach (var group in fileGroups) { string baseName = group.Key; List filesInGroup = group.Value; // 尝试找到一个原始文件(MP3/WAV)和一个OGG文件 string originalFilePath = null; string oggFilePath = null; foreach (string filePath in filesInGroup) { string ext = Path.GetExtension(filePath).ToLower(); if (ext == TargetAudioExtension) { if (oggFilePath != null) { Debug.LogWarning($"在 '{folderPath}' 文件夹中发现多个同名OGG文件 '{baseName}{TargetAudioExtension}'。跳过此组文件。"); oggFilePath = null; break; } oggFilePath = filePath; } else if (OriginalAudioExtensions.Contains(ext)) { if (originalFilePath != null) { Debug.LogWarning($"在 '{folderPath}' 文件夹中发现多个同名原始音频文件(MP3/WAV)'{baseName}'。跳过此组文件。"); originalFilePath = null; break; } originalFilePath = filePath; } } // 如果找到了一个有效的配对(一个原始文件,一个OGG文件) if (originalFilePath != null && oggFilePath != null) { // 转换为Unity资产路径格式 string assetOriginalPath = originalFilePath.Replace(Application.dataPath, "Assets").Replace('\\', '/'); string assetOggPath = oggFilePath.Replace(Application.dataPath, "Assets").Replace('\\', '/'); // 从原始文件的.meta文件中获取GUID string originalMetaPath = originalFilePath + ".meta"; string originalGuid = GetGuidFromMetaFile(originalMetaPath); if (string.IsNullOrEmpty(originalGuid)) { Debug.LogError($"无法从原始文件 '{originalFilePath}' 的.meta文件中读取GUID。跳过此文件。"); skippedCount++; continue; } Debug.Log($"原始文件 '{assetOriginalPath}' 的GUID为: {originalGuid}"); // 关键步骤1:删除OGG文件的.meta文件(如果存在) string oggMetaPath = oggFilePath + ".meta"; if (File.Exists(oggMetaPath)) { try { File.Delete(oggMetaPath); Debug.Log($"已删除OGG文件的.meta文件: '{oggMetaPath}'"); } catch (System.Exception ex) { Debug.LogError($"删除OGG文件的.meta文件时出错: {ex.Message}"); skippedCount++; continue; } } // 关键步骤2:删除原始文件(MP3/WAV) // 注意:我们不使用AssetDatabase.DeleteAsset,因为它可能不会完全释放GUID try { // 先删除原始文件的.meta文件 if (File.Exists(originalMetaPath)) { File.Delete(originalMetaPath); Debug.Log($"已删除原始文件的.meta文件: '{originalMetaPath}'"); } // 再删除原始文件本身 if (File.Exists(originalFilePath)) { File.Delete(originalFilePath); Debug.Log($"已删除原始文件: '{originalFilePath}'"); } } catch (System.Exception ex) { Debug.LogError($"删除原始文件或其.meta文件时出错: {ex.Message}"); skippedCount++; continue; } // 关键步骤3:刷新资产数据库,确保Unity知道文件已被删除 AssetDatabase.Refresh(); // 关键步骤4:为OGG文件创建新的.meta文件,使用原始文件的GUID string metaContent = GenerateMetaFileContent(originalGuid); try { File.WriteAllText(oggMetaPath, metaContent); Debug.Log($"已为OGG文件创建新的.meta文件,使用原始GUID: '{oggMetaPath}'"); } catch (System.Exception ex) { Debug.LogError($"为OGG文件创建.meta文件时出错: {ex.Message}"); skippedCount++; continue; } // 关键步骤5:再次刷新资产数据库,确保Unity导入新的.meta文件 AssetDatabase.Refresh(); // 关键步骤6:验证OGG文件的GUID是否正确 string newOggGuid = AssetDatabase.AssetPathToGUID(assetOggPath); if (newOggGuid == originalGuid) { Debug.Log($"成功: 文件 '{assetOggPath}' 现在使用原始文件的GUID: {originalGuid}"); processedCount++; } else { Debug.LogError($"GUID转移失败! OGG文件 '{assetOggPath}' 的GUID为 {newOggGuid},而不是预期的 {originalGuid}"); skippedCount++; } } else if (originalFilePath == null && oggFilePath != null) { // 只有OGG文件,没有对应的原始文件,这是正常的,不需要警告 continue; } else if (originalFilePath != null && oggFilePath == null) { Debug.LogWarning($"找到原始文件 '{originalFilePath}',但没有对应的OGG文件。跳过此文件。"); skippedCount++; } } // 最终刷新,确保所有更改都已反映在Unity中 AssetDatabase.Refresh(); Debug.Log($"处理完成:成功处理 {processedCount} 对文件,跳过 {skippedCount} 对文件。"); } /// /// 从文件名中提取基础名称,移除所有已知的音频扩展名。 /// private static string GetBaseNameWithoutExtensions(string fileName) { string baseName = Path.GetFileNameWithoutExtension(fileName); // 检查是否还有其他音频扩展名 foreach (string ext in OriginalAudioExtensions.Concat(new[] { TargetAudioExtension })) { if (baseName.EndsWith(ext, System.StringComparison.OrdinalIgnoreCase)) { baseName = baseName.Substring(0, baseName.Length - ext.Length); } } return baseName; } /// /// 从Unity的.meta文件中提取GUID。 /// private static string GetGuidFromMetaFile(string metaFilePath) { if (!File.Exists(metaFilePath)) { Debug.LogError($"Meta文件不存在: {metaFilePath}"); return null; } string metaContent = File.ReadAllText(metaFilePath); Match match = Regex.Match(metaContent, @"guid: ([0-9a-fA-F]{32})"); if (match.Success) { return match.Groups[1].Value; } return null; } /// /// 生成包含指定GUID的.meta文件内容。 /// private static string GenerateMetaFileContent(string guid) { // 创建一个基本的AudioImporter .meta文件内容 return $@"fileFormatVersion: 2 guid: {guid} AudioImporter: externalObjects: {{}} serializedVersion: 6 defaultSettings: loadType: 0 sampleRateSetting: 0 sampleRateOverride: 44100 compressionFormat: 1 quality: 1 conversionMode: 0 platformSettingOverrides: {{}} forceToMono: 0 normalize: 1 preloadAudioData: 1 loadInBackground: 0 ambisonic: 0 3D: 1 userData: assetBundleName: assetBundleVariant: "; } // 菜单项的验证方法 [MenuItem("Assets/Transfer MP3/WAV GUID to OGG in Selected Folder", true)] public static bool ValidateTransferGUIDsInSelectedFolder() { // 仅当在Project窗口中选中单个文件夹时才启用菜单项 if (Selection.activeObject == null) return false; string selectedPath = AssetDatabase.GetAssetPath(Selection.activeObject); return AssetDatabase.IsValidFolder(selectedPath); } }