diff --git a/LICENSE.txt b/LICENSE.txt old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/data/SoundMiner/Boom.sqlite b/data/SoundMiner/Boom.sqlite deleted file mode 100644 index 11c15ce..0000000 Binary files a/data/SoundMiner/Boom.sqlite and /dev/null differ diff --git a/data/SoundMiner/Dialog.sqlite b/data/SoundMiner/Dialog.sqlite deleted file mode 100644 index 0b410cf..0000000 Binary files a/data/SoundMiner/Dialog.sqlite and /dev/null differ diff --git a/data/SoundMiner/Music.sqlite b/data/SoundMiner/Music.sqlite deleted file mode 100644 index 6a8b9e8..0000000 Binary files a/data/SoundMiner/Music.sqlite and /dev/null differ diff --git a/data/basehead.xlsx b/data/basehead.xlsx deleted file mode 100644 index fb27ec6..0000000 Binary files a/data/basehead.xlsx and /dev/null differ diff --git a/data/表结构分析.md b/data/表结构分析.md deleted file mode 100644 index 8fd0ddf..0000000 --- a/data/表结构分析.md +++ /dev/null @@ -1,191 +0,0 @@ -让我整理一下这些字段的含义: - -1. id - 主键ID -2. path - 原始导入路径 -3. filename - 带扩展名的文件名 -4. description - 文件自带描述 -5. length - 音频长度(时长) -6. bit_depth - 位深度(如16bit, 24bit) -7. channels - 声道数(单声道/立体声/环绕声) -8. date_added - 导入日期 -9. sample_rate - 采样率(如44.1kHz, 48kHz) -10. type - 文件类型(如WAV, MP3, AIFF等) -11. cd_title - CD标题 -12. artist - 艺术家 -13. coding_history - 编码历史 -14. original_date - 原始日期 -15. originator_ref - 来源引用 -16. originator - 来源/创作者 -17. timecode - 时间码 -18. subcategory - 子分类 -19. category - 分类 -20. comments - 注释 -21. episode - 剧集/集数 -22. frame_rate - 帧率 -23. group - 分组 -24. library - 音频库 -25. location - 位置/地点 -26. project_name - 项目名称 -27. rating - 评分 -28. tape - 磁带 -29. scene - 场景 -30. sync_point - 同步点 -31. take - 录音条次 -32. track_title - 曲目标题 -33. composer - 作曲家 -34. designer - 设计师 -35. publisher - 出版商 -36. folder - 文件夹 -37. genre - 流派/风格 -38. keywords - 关键词 -39. copyright - 版权 -40. recordist - 录音师 -41. manufacturer - 制造商 -42. notes - 备注 -43. microphone - 麦克风 -44. directory - 目录 -45. origination_time - 创建时间 -46. unique_id - 唯一标识符 -47. bpm - 每分钟节拍数 -48. mood - 情绪/氛围 -49. short_id - 短ID -50. cue_number - 提示号 -51. style - 风格 -52. markers - 标记点 -53. is_edited - 是否已编辑 -54. is_split - 是否已分割 - 55-62. user1-user8 - 用户自定义字段 - -我会为用户提供一个详细的分析报告。 - -我来为您分析这个音频管理软件的表结构。根据字段名称和数据类型,这是一个专业的**音频资产管理系统**的数据库结构,类似于 SoundMiner、BaseHead 等专业音频库管理软件。 - -## 字段含义分析 - -### 📌 基础标识字段 - - - -| 字段名 | 类型 | 含义说明 | -| ----------- | ------- | ---------------------------------------------- | -| `id` | integer | **主键ID**,音频文件的唯一数字标识符 | -| `unique_id` | text | **全局唯一标识符**,通常为UUID,用于跨系统识别 | -| `short_id` | text | **短ID**,便于人工识别或显示的简短标识 | - -### 📁 文件存储字段 - - - -| 字段名 | 类型 | 含义说明 | -| ----------- | ---- | ---------------------------------------------------- | -| `path` | text | **原始导入路径**,音频文件被导入时的完整文件路径 | -| `filename` | text | **文件名**,包含扩展名的音频文件名称(如 sound.wav) | -| `folder` | text | **文件夹**,文件所在的文件夹名称 | -| `directory` | text | **目录**,文件的存储目录路径 | - -### 🎵 音频技术参数 - - - -| 字段名 | 类型 | 含义说明 | -| ------------- | ------- | -------------------------------------------------------- | -| `length` | real | **时长**,音频文件的播放长度(通常以秒为单位) | -| `bit_depth` | integer | **位深度**,音频采样位深(如 16、24、32 bit) | -| `channels` | integer | **声道数**,如 1=单声道、2=立体声、6=5.1环绕声 | -| `sample_rate` | integer | **采样率**,如 44100、48000、96000 Hz | -| `type` | text | **文件类型**,音频格式(WAV、MP3、AIFF、FLAC等) | -| `bpm` | real | **节拍速度**,每分钟节拍数,用于音乐类音频 | -| `frame_rate` | text | **帧率**,视频同步用的时间基准(如 24fps、25fps、30fps) | -| `timecode` | integer | **时间码**,用于音视频同步的时间戳信息 | - -### 📝 元数据描述字段 - - - -| 字段名 | 类型 | 含义说明 | -| ------------- | ------------ | ---------------------------------------------------- | -| `description` | text | **描述**,音频内容的文字说明 | -| `category` | text | **分类**,音频的大类(如:音效、音乐、对白、环境音) | -| `subcategory` | text | **子分类**,更细分的类别(如:交通→汽车→引擎) | -| `genre` | text | **流派**,音乐风格分类(如:摇滚、古典、电子) | -| `style` | text | **风格**,更具体的风格描述 | -| `mood` | text | **情绪**,音频传达的情感氛围(如:紧张、欢快、悲伤) | -| `keywords` | text | **关键词**,用于搜索的标签,多个关键词通常用逗号分隔 | -| `rating` | decimal(5.3) | **评分**,音频质量或推荐度评分(如 0-5星) | - -### 👤 创作者信息字段 - - - -| 字段名 | 类型 | 含义说明 | -| ---------------- | ---- | -------------------------------- | -| `artist` | text | **艺术家**,表演者或演奏者名称 | -| `composer` | text | **作曲家**,音乐创作者 | -| `designer` | text | **设计师**,音效设计师名称 | -| `recordist` | text | **录音师**,现场录音人员 | -| `publisher` | text | **出版商**,发行方或版权持有方 | -| `manufacturer` | text | **制造商**,音频库或设备的制作方 | -| `originator` | text | **来源方**,原始创建者或来源机构 | -| `originator_ref` | text | **来源引用**,原始来源的参考编号 | - -### 🎬 项目制作字段 - - - -| 字段名 | 类型 | 含义说明 | -| -------------- | ------- | -------------------------------------- | -| `project_name` | text | **项目名称**,所属制作项目 | -| `library` | text | **音频库**,所属音效库或音乐库的名称 | -| `cd_title` | text | **CD标题**,原始CD专辑名称 | -| `track_title` | text | **曲目标题**,音乐或音轨的标题 | -| `episode` | text | **剧集**,所属剧集或系列编号 | -| `scene` | text | **场景**,对应的影视场景标识 | -| `take` | text | **条次**,录音的版本号或拍摄条次 | -| `tape` | text | **磁带**,原始磁带编号(历史遗留字段) | -| `cue_number` | integer | **提示号**,播放列表或乐谱中的编号 | -| `sync_point` | integer | **同步点**,用于音视频同步的参考点 | - -### 📋 管理与状态字段 - - - -| 字段名 | 类型 | 含义说明 | -| ------------------ | ------- | -------------------------------------------------- | -| `date_added` | date | **导入日期**,音频入库的时间 | -| `original_date` | text | **原始日期**,音频创建或录制的日期 | -| `origination_time` | text | **创建时间**,音频文件的原始创建时间 | -| `is_edited` | integer | **是否已编辑**,布尔值(0=否,1=是) | -| `is_split` | integer | **是否已分割**,布尔值,标记是否从长文件中分割出来 | -| `location` | text | **位置**,录音地点或存储位置 | -| `group` | text | **分组**,用于组织管理的分组标识 | -| `markers` | text | **标记点**,音频内的关键时间点标记 | - -### 📄 备注与版权字段 - - - -| 字段名 | 类型 | 含义说明 | -| ---------------- | ---- | -------------------------------------- | -| `comments` | text | **注释**,用户或系统添加的备注 | -| `notes` | text | **备注**,额外的说明信息 | -| `copyright` | text | **版权**,版权声明信息 | -| `coding_history` | text | **编码历史**,BWF格式的编码历史记录 | -| `microphone` | text | **麦克风**,录音使用的麦克风型号或设置 | - -### 🔧 用户自定义字段 - - - -| 字段名 | 类型 | 含义说明 | -| ----------------- | ---- | ------------------------------------------------------------ | -| `user1` ~ `user8` | text | **用户自定义字段**,共8个预留字段,供用户根据实际需求自定义用途 | - -## 总结 - -这是一个**专业级音频资产管理系统**的完整表结构,主要特点: - -1. **符合广播级标准**:包含 BWF(Broadcast Wave Format)标准字段如 `coding_history`、`originator`、`timecode` 等 -2. **支持影视后期制作**:有 `scene`、`take`、`episode`、`sync_point` 等影视制作相关字段 -3. **音乐管理功能完善**:包含 `bpm`、`mood`、`genre`、`composer` 等音乐元数据 -4. **灵活扩展性**:预留了8个用户自定义字段 -5. **版权管理支持**:包含 `copyright`、`publisher`、`manufacturer` 等版权相关字段 \ No newline at end of file diff --git a/src/AtlFieldExtractor/AtlFieldExtractor.csproj b/src/AtlFieldExtractor/AtlFieldExtractor.csproj new file mode 100755 index 0000000..b93a434 --- /dev/null +++ b/src/AtlFieldExtractor/AtlFieldExtractor.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/src/AtlFieldExtractor/DumpCodingHistory.cs b/src/AtlFieldExtractor/DumpCodingHistory.cs new file mode 100755 index 0000000..415ed10 --- /dev/null +++ b/src/AtlFieldExtractor/DumpCodingHistory.cs @@ -0,0 +1,86 @@ +using ATL; + +namespace OCES.Resonance.AtlFieldExtractor; + +class DumpCodingHistory +{ + // static void Main(string[] args) + // { + // string sourceDir = args.Length > 0 + // ? args[0] + // : Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../data/source")); + // + // Console.WriteLine($"扫描目录: {sourceDir}"); + // + // if (!Directory.Exists(sourceDir)) + // { + // Console.WriteLine($"目录不存在: {sourceDir}"); + // return; + // } + // + // var wavFiles = Directory.GetFiles(sourceDir, "*.wav", SearchOption.AllDirectories); + // Console.WriteLine($"找到 {wavFiles.Length} 个 WAV 文件"); + // + // foreach (var wavFile in wavFiles) + // { + // try + // { + // DumpFile(wavFile); + // } + // catch (Exception ex) + // { + // Console.WriteLine($"处理文件失败: {wavFile}"); + // Console.WriteLine($"错误: {ex.Message}"); + // } + // } + // + // Console.WriteLine("处理完成!"); + // } + + static void DumpFile(string wavFilePath) + { + Console.WriteLine($"处理: {wavFilePath}"); + + var track = new Track(wavFilePath); + + if (track.AdditionalFields == null || + !track.AdditionalFields.TryGetValue("bext.codingHistory", out var codingHistory)) + { + Console.WriteLine(" -> 无 bext.codingHistory 字段"); + return; + } + + Console.WriteLine($" -> 字符串长度: {codingHistory.Length}"); + + // 用 Latin-1 编码转回字节(Latin-1 把 Unicode 0x00-0xFF 直接映射为 byte 0x00-0xFF) + var bytes = System.Text.Encoding.Latin1.GetBytes(codingHistory); + var outputPath = Path.Combine( + Path.GetDirectoryName(wavFilePath)!, + Path.GetFileNameWithoutExtension(wavFilePath) + ".codingHistory.bin" + ); + + File.WriteAllBytes(outputPath, bytes); + Console.WriteLine($" -> 已保存: {outputPath}"); + Console.WriteLine($" -> 字节数: {bytes.Length}"); + + // 打印前 64 个字节的 hex + var hexPreview = Math.Min(64, bytes.Length); + Console.Write(" -> Hex 前 64 字节: "); + for (int i = 0; i < hexPreview; i++) + { + Console.Write($"{bytes[i]:X2} "); + if ((i + 1) % 16 == 0) Console.Write(" "); + } + Console.WriteLine(); + + // 打印前 64 个字节的 ASCII(不可打印字符用 . 替代) + Console.Write(" -> ASCII 前 64 字节: "); + for (int i = 0; i < hexPreview; i++) + { + var c = (char)bytes[i]; + Console.Write(c >= 32 && c < 127 ? c : '.'); + if ((i + 1) % 16 == 0) Console.Write(" "); + } + Console.WriteLine(); + } +} diff --git a/src/AtlFieldExtractor/Program.cs b/src/AtlFieldExtractor/Program.cs new file mode 100755 index 0000000..4d5bcf2 --- /dev/null +++ b/src/AtlFieldExtractor/Program.cs @@ -0,0 +1,148 @@ +using ATL; + +namespace OCES.Resonance.AtlFieldExtractor; + +class Program +{ + static void Main(string[] args) + { + // 默认扫描路径:项目的 data/source 目录 + string sourceDir = args.Length > 0 + ? args[0] + : Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../../data/source")); + + Console.WriteLine($"扫描目录: {sourceDir}"); + + if (!Directory.Exists(sourceDir)) + { + Console.WriteLine($"目录不存在: {sourceDir}"); + return; + } + + // 查找所有 WAV 文件 + var wavFiles = Directory.GetFiles(sourceDir, "*.wav", SearchOption.AllDirectories); + + Console.WriteLine($"找到 {wavFiles.Length} 个 WAV 文件"); + + foreach (var wavFile in wavFiles) + { + try + { + ProcessFile(wavFile); + } + catch (Exception ex) + { + Console.WriteLine($"处理文件失败: {wavFile}"); + Console.WriteLine($"错误: {ex.Message}"); + } + } + + Console.WriteLine("处理完成!"); + } + + static void ProcessFile(string wavFilePath) + { + Console.WriteLine($"处理: {wavFilePath}"); + + var track = new Track(wavFilePath); + var outputPath = Path.ChangeExtension(wavFilePath, ".atl.txt"); + + using var writer = new StreamWriter(outputPath); + writer.WriteLine("=== ATL Track 基础属性 ==="); + writer.WriteLine(); + + // 基础技术信息 + writer.WriteLine($"DurationMs: {track.DurationMs}"); + writer.WriteLine($"Duration: {track.Duration}"); + writer.WriteLine($"SampleRate: {track.SampleRate}"); + writer.WriteLine($"BitDepth: {track.BitDepth}"); + writer.WriteLine($"Channels: {track.ChannelsArrangement.NbChannels}"); + writer.WriteLine($"ChannelsArrangement: {track.ChannelsArrangement}"); + + writer.WriteLine(); + writer.WriteLine("=== ATL 元数据字段 ==="); + writer.WriteLine(); + + // 通用元数据 + writer.WriteLine($"Title: {track.Title}"); + writer.WriteLine($"Artist: {track.Artist}"); + writer.WriteLine($"Album: {track.Album}"); + writer.WriteLine($"AlbumArtist: {track.AlbumArtist}"); + writer.WriteLine($"Composer: {track.Composer}"); + writer.WriteLine($"Comment: {track.Comment}"); + writer.WriteLine($"Genre: {track.Genre}"); + writer.WriteLine($"Year: {track.Year}"); + writer.WriteLine($"TrackNumber: {track.TrackNumber}"); + writer.WriteLine($"DiscNumber: {track.DiscNumber}"); + writer.WriteLine($"Publisher: {track.Publisher}"); + writer.WriteLine($"Copyright: {track.Copyright}"); + writer.WriteLine($"Description: {track.Description}"); + writer.WriteLine($"OriginalReleaseDate: {track.OriginalReleaseDate}"); + writer.WriteLine($"Date: {track.Date}"); + writer.WriteLine($"BPM: {track.BPM}"); + writer.WriteLine($"Group: {track.Group}"); + writer.WriteLine($"SeriesTitle: {track.SeriesTitle}"); + writer.WriteLine($"SeriesPart: {track.SeriesPart}"); + + writer.WriteLine(); + writer.WriteLine("=== ATL AdditionalFields (额外字段) ==="); + writer.WriteLine(); + + // 打印所有附加字段 + if (track.AdditionalFields != null && track.AdditionalFields.Count > 0) + { + foreach (var field in track.AdditionalFields.OrderBy(f => f.Key)) + { + writer.WriteLine($"[{field.Key}] = {field.Value}"); + } + } + else + { + writer.WriteLine("(无附加字段)"); + } + + writer.WriteLine(); + writer.WriteLine("=== ATL AdditionalImages (封面/图片) ==="); + writer.WriteLine(); + + // 打印图片信息 + if (track.EmbeddedPictures != null && track.EmbeddedPictures.Count > 0) + { + foreach (var pic in track.EmbeddedPictures) + { + writer.WriteLine($"PictureType: {pic.PicType}"); + writer.WriteLine($"Description: {pic.Description}"); + writer.WriteLine($"MimeType: {pic.MimeType}"); + writer.WriteLine($"Data Length: {pic.PictureData?.Length ?? 0} bytes"); + writer.WriteLine(); + } + } + else + { + writer.WriteLine("(无嵌入图片)"); + } + + writer.WriteLine(); + writer.WriteLine("=== ATL Chapters (章节信息) ==="); + writer.WriteLine(); + + // 打印章节信息 + if (track.Chapters != null && track.Chapters.Count > 0) + { + foreach (var chapter in track.Chapters) + { + writer.WriteLine($"Title: {chapter.Title}"); + writer.WriteLine($"StartTime: {chapter.StartTime}"); + writer.WriteLine($"EndTime: {chapter.EndTime}"); + writer.WriteLine($"Sub-Title: {chapter.Subtitle}"); + writer.WriteLine(); + } + } + else + { + writer.WriteLine("(无章节信息)"); + } + + Console.WriteLine($" -> 已保存: {outputPath}"); + } +} diff --git a/src/Core.Tests/AudioFileScannerTests.cs b/src/Core.Tests/AudioFileScannerTests.cs old mode 100644 new mode 100755 diff --git a/src/Core.Tests/AudioLibraryServiceTests.cs b/src/Core.Tests/AudioLibraryServiceTests.cs deleted file mode 100644 index 39cf91b..0000000 --- a/src/Core.Tests/AudioLibraryServiceTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -using FluentAssertions; - -namespace OCES.Resonance.Core.Tests; - -/// -/// AudioLibraryService 集成测试 -/// -public class AudioLibraryServiceTests : IDisposable -{ - readonly AudioLibraryService _service; - readonly string _testDirectory; - - public AudioLibraryServiceTests() - { - _service = new AudioLibraryService(); - _testDirectory = Path.Combine(Path.GetTempPath(), $"LibraryServiceTest_{Guid.NewGuid()}"); - Directory.CreateDirectory(_testDirectory); - } - - public void Dispose() - { - if (Directory.Exists(_testDirectory)) - { - Directory.Delete(_testDirectory, true); - } - // 清理测试数据库 - var dbPath = "default.db"; - if (File.Exists(dbPath)) - { - File.Delete(dbPath); - } - } - - [Fact] - public async Task ScanAndImportToLibrary_空目录_返回零总数() - { - // Act - var result = await _service.ScanAndImportToLibrary(_testDirectory); - - // Assert - result.TotalFiles.Should().Be(0); - result.SuccessCount.Should().Be(0); - result.ErrorCount.Should().Be(0); - } - - [Fact] - public async Task ScanAndImportToLibrary_包含音频文件_成功导入() - { - // Arrange - var wavFile = Path.Combine(_testDirectory, "test.wav"); - File.WriteAllText(wavFile, "fake wav content"); - - // Act - var result = await _service.ScanAndImportToLibrary(_testDirectory); - - // Assert - result.TotalFiles.Should().Be(1); - result.SuccessCount.Should().Be(1); - result.ErrorCount.Should().Be(0); - } - - [Fact] - public async Task ScanAndImportToLibrary_多个文件_正确统计() - { - // Arrange - File.WriteAllText(Path.Combine(_testDirectory, "audio1.wav"), "fake"); - File.WriteAllText(Path.Combine(_testDirectory, "audio2.mp3"), "fake"); - File.WriteAllText(Path.Combine(_testDirectory, "audio3.flac"), "fake"); - File.WriteAllText(Path.Combine(_testDirectory, "document.txt"), "not audio"); - - // Act - var result = await _service.ScanAndImportToLibrary(_testDirectory); - - // Assert - result.TotalFiles.Should().Be(3); - result.SuccessCount.Should().Be(3); - } - - [Fact] - public async Task ScanAndImportToLibrary_递归扫描子目录() - { - // Arrange - var subDir = Path.Combine(_testDirectory, "subdir"); - Directory.CreateDirectory(subDir); - File.WriteAllText(Path.Combine(_testDirectory, "root.wav"), "fake"); - File.WriteAllText(Path.Combine(subDir, "sub.wav"), "fake"); - - // Act - var result = await _service.ScanAndImportToLibrary(_testDirectory); - - // Assert - result.TotalFiles.Should().Be(2); - result.SuccessCount.Should().Be(2); - } - - [Fact] - public void ScanResult_GetSummary_返回正确格式() - { - // Arrange - var result = new ScanResult - { - TotalFiles = 10, - SuccessCount = 8, - SkipCount = 1, - ErrorCount = 1, - }; - - // Act - var summary = result.GetSummary(); - - // Assert - summary.Should().Contain("10"); - summary.Should().Contain("8"); - summary.Should().Contain("1"); - } - - [Fact] - public async Task ScanAndImportToLibrary_包含不支持格式_只导入音频() - { - // Arrange - File.WriteAllText(Path.Combine(_testDirectory, "audio.wav"), "fake"); - File.WriteAllText(Path.Combine(_testDirectory, "image.jpg"), "fake"); - File.WriteAllText(Path.Combine(_testDirectory, "doc.pdf"), "fake"); - File.WriteAllText(Path.Combine(_testDirectory, "text.txt"), "fake"); - - // Act - var result = await _service.ScanAndImportToLibrary(_testDirectory); - - // Assert - result.TotalFiles.Should().Be(1); - result.SuccessCount.Should().Be(1); - } -} diff --git a/src/Core.Tests/AudioMetadataReaderTest.cs b/src/Core.Tests/AudioMetadataReaderTest.cs new file mode 100644 index 0000000..90ee5e0 --- /dev/null +++ b/src/Core.Tests/AudioMetadataReaderTest.cs @@ -0,0 +1,273 @@ +using System.Globalization; +using OCES.Resonance.Core; + +namespace Core.Tests; + +public class AudioMetadataReaderTest +{ + private static readonly string _dataSourceRoot = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "../../../../../../data/source")); + + private static string[] GetWavFiles(string vendor) + { + string dir = Path.Combine(_dataSourceRoot, vendor); + return Directory.Exists(dir) + ? Directory.GetFiles(dir, "*.wav", SearchOption.AllDirectories) + : []; + } + + private static string? GetAtlTxtPath(string wavPath) + { + string atlPath = Path.ChangeExtension(wavPath, ".atl.txt"); + return File.Exists(atlPath) ? atlPath : null; + } + + /// + /// 解析 .atl.txt 文件,返回基础属性、元数据字段和额外字段的字典 + /// + private static (Dictionary basic, Dictionary meta, Dictionary extra) + ParseAtlTxt(string atlPath) + { + var basic = new Dictionary(); + var meta = new Dictionary(); + var extra = new Dictionary(); + + string[] lines = File.ReadAllLines(atlPath); + string section = ""; + + foreach (string line in lines) + { + if (line.StartsWith("=== ATL Track 基础属性 ===")) + { + section = "basic"; + continue; + } + if (line.StartsWith("=== ATL 元数据字段 ===")) + { + section = "meta"; + continue; + } + if (line.StartsWith("=== ATL AdditionalFields")) + { + section = "extra"; + continue; + } + if (line.StartsWith("=== ATL AdditionalImages") || line.StartsWith("=== ATL Chapters")) + { + section = ""; + continue; + } + + if (section == "extra" && line.StartsWith("[")) + { + int eqIdx = line.IndexOf("] = "); + if (eqIdx > 0) + { + string key = line[1..eqIdx]; + string value = line[(eqIdx + 4)..]; + extra[key] = value; + } + } + else if ((section == "basic" || section == "meta") && line.Contains(": ")) + { + int colonIdx = line.IndexOf(": "); + if (colonIdx > 0) + { + string key = line[..colonIdx].Trim(); + string value = line[(colonIdx + 2)..].Trim(); + if (section == "basic") + basic[key] = value; + else + meta[key] = value; + } + } + } + + return (basic, meta, extra); + } + + [Fact] + public void ReadMetadata_ShouldThrow_FileNotFoundException() + { + AudioMetadataReader reader = new(); + FileInfo nonExistentFile = new(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + + Assert.Throws(() => reader.ReadMetadata(nonExistentFile)); + } + + /// + /// Boom 目录的 WAV 文件应包含完整的基础技术参数和富元数据 + /// + [Fact] + public void ReadMetadata_ShouldPopulateAllFields_ForBoomFiles() + { + string[] wavFiles = GetWavFiles("Boom"); + if (wavFiles.Length == 0) return; + + AudioMetadataReader reader = new(); + + foreach (string wavPath in wavFiles) + { + FileInfo file = new(wavPath); + AudioFileMeta meta = reader.ReadMetadata(file); + string? atlPath = GetAtlTxtPath(wavPath); + + // 必备条目:基础技术参数 + Assert.NotEqual(0.0, meta.Duration); + Assert.True(meta.BitDepth > 0); + Assert.True(meta.Channels > 0); + Assert.True(meta.SampleRate > 0); + Assert.Equal("WAV", meta.Type); + Assert.Equal(wavPath, meta.Path); + Assert.Equal(file.Name, meta.Filename); + Assert.Equal(file.Directory?.Name, meta.Folder); + Assert.Equal(file.DirectoryName, meta.Directory); + Assert.NotEmpty(meta.UniqueId); + Assert.NotEmpty(meta.Md5); + Assert.Equal(36, meta.UniqueId.Length); // UUID 格式 + Assert.Equal(32, meta.Md5.Length); // MD5 十六进制格式 + Assert.NotNull(meta.ChannelLayout); + + // 文件系统时间 + Assert.Equal(file.LastWriteTime, meta.LastWriteTime); + Assert.Equal(file.CreationTime, meta.CreationTime); + + if (atlPath == null) + continue; + + var (basic, metaFields, extra) = ParseAtlTxt(atlPath); + + // 基础技术参数与 ATL 一致 + Assert.Equal(int.Parse(basic["DurationMs"]) / 1000.0, meta.Duration); + Assert.Equal(int.Parse(basic["BitDepth"]), meta.BitDepth); + Assert.Equal(int.Parse(basic["Channels"]), meta.Channels); + Assert.Equal(int.Parse(basic["SampleRate"]), (int)meta.SampleRate); + Assert.Equal(basic["ChannelsArrangement"], meta.ChannelLayout); + + // 标准元数据字段 + Assert.Equal(metaFields["Title"], meta.TrackTitle); + Assert.Equal(metaFields["Artist"], meta.Artist); + Assert.Equal(metaFields["Album"], meta.CdTitle); + Assert.Equal(metaFields["Composer"], meta.Composer); + Assert.Equal(metaFields["Comment"], meta.Description); + Assert.Equal(metaFields["Publisher"], meta.Publisher); + Assert.Equal(metaFields["Copyright"], meta.Copyright); + Assert.Equal(metaFields["Group"], meta.Group); + + // Genre: ATL 规范字段可能与 iXML 不一致,仅做非空校验 + if (!string.IsNullOrEmpty(metaFields["Genre"])) + Assert.Equal(metaFields["Genre"], meta.Genre); + + // TrackNumber + if (int.TryParse(metaFields["TrackNumber"], out int trackNum) && trackNum > 0) + Assert.Equal(trackNum, meta.TrackNumber); + + // DiscNumber + if (int.TryParse(metaFields["DiscNumber"], out int discNum) && discNum > 0) + Assert.Equal(discNum, meta.DiscNumber); + + // Year + if (int.TryParse(metaFields["Year"], out int year) && year > 0) + Assert.Equal(year, meta.TrackYear); + + // BPM + if (int.TryParse(metaFields["BPM"], out int bpm) && bpm > 0) + Assert.Equal((double)bpm, meta.Bpm); + + // iXML / USER 自定义字段(仅 Boom 有) + if (extra.TryGetValue("ixml.ASWG.category", out string? cat) || extra.TryGetValue("ixml.USER.CATEGORY", out cat)) + Assert.Equal(cat, meta.Category); + if (extra.TryGetValue("ixml.ASWG.catId", out string? catId) || extra.TryGetValue("ixml.USER.CATID", out catId)) + Assert.Equal(catId, meta.CatId); + if (extra.TryGetValue("ixml.ASWG.library", out string? lib) || extra.TryGetValue("ixml.USER.LIBRARY", out lib)) + Assert.Equal(lib, meta.Library); + if (extra.TryGetValue("ixml.ASWG.notes", out string? notes) || extra.TryGetValue("ixml.USER.NOTES", out notes)) + Assert.Equal(notes, meta.Notes); + if (extra.TryGetValue("ixml.USER.FXNAME", out string? fxName)) + Assert.Equal(fxName, meta.FxName); + if (extra.TryGetValue("ixml.USER.KEYWORDS", out string? keywords)) + Assert.Equal(keywords, meta.Keywords); + if (extra.TryGetValue("ixml.USER.MANUFACTURER", out string? manufacturer)) + Assert.Equal(manufacturer, meta.Manufacturer); + if (extra.TryGetValue("USER.MICPERSPECTIVE", out string? micPersp)) + Assert.Equal(micPersp, meta.MicPerspective); + if (extra.TryGetValue("USER.SUBCATEGORY", out string? subcat) || extra.TryGetValue("ASWG.subCategory", out subcat)) + Assert.Equal(subcat, meta.Subcategory); + if (extra.TryGetValue("USER.RATING", out string? ratingRaw)) + { + if (double.TryParse(ratingRaw, NumberStyles.Float, CultureInfo.InvariantCulture, out double ratingVal)) + Assert.Equal((uint)Math.Round(ratingVal), meta.Rating); + } + + // BWF bext 字段 + if (extra.TryGetValue("bext.coding_history", out string? codingHistory)) + Assert.Equal(codingHistory, meta.BwfCodingHistory); + if (extra.TryGetValue("bext.originator", out string? originator)) + Assert.Equal(originator, meta.BwfOriginator); + if (extra.TryGetValue("bext.originatorRef", out string? originatorRef)) + Assert.Equal(originatorRef, meta.BwfOriginatorRef); + } + } + + /// + /// Boom 目录的 WAV 文件应包含嵌入的封面图片 + /// + [Fact] + public void ReadMetadata_ShouldContainArtwork_ForBoomFiles() + { + string[] wavFiles = GetWavFiles("Boom"); + if (wavFiles.Length == 0) return; + + AudioMetadataReader reader = new(); + + foreach (string wavPath in wavFiles) + { + FileInfo file = new(wavPath); + AudioFileMeta meta = reader.ReadMetadata(file); + + Assert.NotNull(meta.Artwork); + Assert.NotEmpty(meta.Artwork); + } + } + + /// + /// Sound Idea 目录的 WAV 文件元数据最少:仅基础技术参数, + /// 无 iXML、BWF 或嵌入图片 + /// + [Fact] + public void ReadMetadata_ShouldHaveNoOptionalMetadata_ForSoundIdeaFiles() + { + string[] wavFiles = GetWavFiles("Sound Idea"); + if (wavFiles.Length == 0) return; + + AudioMetadataReader reader = new(); + + foreach (string wavPath in wavFiles) + { + FileInfo file = new(wavPath); + AudioFileMeta meta = reader.ReadMetadata(file); + + // 必备条目正确 + Assert.NotEqual(0.0, meta.Duration); + Assert.True(meta.BitDepth > 0); + Assert.True(meta.Channels > 0); + Assert.True(meta.SampleRate > 0); + Assert.Equal("WAV", meta.Type); + Assert.Equal(wavPath, meta.Path); + Assert.Equal(file.Name, meta.Filename); + Assert.NotEmpty(meta.Md5); + + // 可选字段为空或默认值(Sound Idea 无附加字段) + Assert.Null(meta.Category); + Assert.Null(meta.Subcategory); + Assert.Null(meta.CatId); + Assert.Null(meta.Library); + Assert.Null(meta.FxName); + Assert.Null(meta.Keywords); + Assert.Null(meta.BwfCodingHistory); + Assert.Null(meta.BwfOriginator); + Assert.Null(meta.BwfOriginatorRef); + Assert.Null(meta.Artwork); + } + } +} diff --git a/src/Core.Tests/AudioMetadataReaderTests.cs b/src/Core.Tests/AudioMetadataReaderTests.cs deleted file mode 100644 index c1b90f8..0000000 --- a/src/Core.Tests/AudioMetadataReaderTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using FluentAssertions; - -namespace OCES.Resonance.Core.Tests; - -/// -/// AudioMetadataReader 单元测试 -/// -public class AudioMetadataReaderTests -{ - readonly AudioMetadataReader _reader; - readonly string _testDirectory; - - public AudioMetadataReaderTests() - { - this._reader = new AudioMetadataReader(); - this._testDirectory = Path.Combine(Path.GetTempPath(), $"MetadataReaderTest_{Guid.NewGuid()}"); - Directory.CreateDirectory(this._testDirectory); - } - - [Fact] - public void ReadMetadata_文件不存在_抛出FileNotFoundException() - { - // Arrange - string nonExistentFile = Path.Combine(this._testDirectory, "non_existent.wav"); - - // Act & Assert - Func act = () => this._reader.ReadMetadata(nonExistentFile); - act.Should().Throw(); - } - - [Fact] - public void ReadMetadata_空文件_仍然返回元数据() - { - // Arrange - string emptyFile = Path.Combine(this._testDirectory, "empty.wav"); - File.WriteAllText(emptyFile, ""); - - // Act - AudioFileMeta result = this._reader.ReadMetadata(emptyFile); - - // Assert - result.Should().NotBeNull(); - result.Filename.Should().Be("empty.wav"); - result.Path.Should().Be(Path.GetFullPath(emptyFile)); - result.Md5.Should().NotBeNullOrEmpty(); - result.UniqueId.Should().NotBeNullOrEmpty(); - } - - [Fact] - public void ReadMetadata_生成唯一ID_每次调用都不同() - { - // Arrange - string testFile = Path.Combine(this._testDirectory, "test.wav"); - File.WriteAllText(testFile, "fake audio content"); - - // Act - AudioFileMeta result1 = this._reader.ReadMetadata(testFile); - AudioFileMeta result2 = this._reader.ReadMetadata(testFile); - - // Assert - result1.UniqueId.Should().NotBe(result2.UniqueId); - } - - [Fact] - public void ReadMetadata_相同文件_MD5相同() - { - // Arrange - var testFile = Path.Combine(this._testDirectory, "test.wav"); - File.WriteAllText(testFile, "fake audio content"); - - // Act - var result1 = this._reader.ReadMetadata(testFile); - var result2 = this._reader.ReadMetadata(testFile); - - // Assert - result1.Md5.Should().Be(result2.Md5); - } - - [Theory] - [InlineData(".wav", "WAV")] - [InlineData(".mp3", "MP3")] - [InlineData(".flac", "FLAC")] - [InlineData(".aiff", "AIFF")] - public void ReadMetadata_不同格式_正确设置Type字段(string extension, string expectedType) - { - // Arrange - string testFile = Path.Combine(this._testDirectory, $"test{extension}"); - File.WriteAllText(testFile, "fake audio content"); - - // Act - AudioFileMeta result = this._reader.ReadMetadata(testFile); - - // Assert - result.Type.Should().Be(expectedType); - } - - [Fact] - public void ReadMetadata_提取文件夹信息() - { - // Arrange - string subDir = Path.Combine(this._testDirectory, "SubFolder"); - Directory.CreateDirectory(subDir); - string testFile = Path.Combine(subDir, "test.wav"); - File.WriteAllText(testFile, "fake audio content"); - - // Act - AudioFileMeta result = this._reader.ReadMetadata(testFile); - - // Assert - result.Folder.Should().Be("SubFolder"); - result.Directory.Should().Be(subDir); - } - - [Fact] - public void ReadMetadata_设置DateAdded为当前时间() - { - // Arrange - string testFile = Path.Combine(this._testDirectory, "test.wav"); - File.WriteAllText(testFile, "fake audio content"); - DateTime beforeTime = DateTime.Now; - - // Act - AudioFileMeta result = this._reader.ReadMetadata(testFile); - DateTime afterTime = DateTime.Now; - - // Assert - result.DateAdded.Should().BeOnOrAfter(beforeTime); - result.DateAdded.Should().BeOnOrBefore(afterTime); - } -} diff --git a/src/Core.Tests/Core.Tests.csproj b/src/Core.Tests/Core.Tests.csproj old mode 100644 new mode 100755 index d409452..9daf4aa --- a/src/Core.Tests/Core.Tests.csproj +++ b/src/Core.Tests/Core.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Core.Tests/DatabaseTests.cs b/src/Core.Tests/DatabaseTests.cs deleted file mode 100644 index 7ecce99..0000000 --- a/src/Core.Tests/DatabaseTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Data.SQLite; -using FluentAssertions; - -namespace OCES.Resonance.Core.Tests; - -/// -/// Database 单元测试 -/// -public class DatabaseTests : IDisposable -{ - readonly string _testDbName; - readonly string _testDbPath; - - public DatabaseTests() - { - _testDbName = $"test_{Guid.NewGuid():N}"; - _testDbPath = $"{_testDbName}.db"; - } - - public void Dispose() - { - if (File.Exists(_testDbPath)) - { - File.Delete(_testDbPath); - } - } - - [Fact] - public void GetConnection_返回有效连接() - { - // Act - using var connection = Database.GetConnection(_testDbName); - - // Assert - connection.Should().NotBeNull(); - connection.Should().BeOfType(); - } - - [Fact] - public void InitializeDatabase_创建表结构() - { - // Act - Database.InitializeDatabase(); - - // Assert - 验证表已创建(通过尝试查询) - using var connection = Database.GetConnection(); - connection.Open(); - var command = connection.CreateCommand(); - command.CommandText = "SELECT COUNT(*) FROM audio_files;"; - var count = command.ExecuteScalar(); - count.Should().NotBeNull(); - } - - [Fact] - public void AddEntry_添加有效记录_返回True() - { - // Arrange - Database.InitializeDatabase(); - var meta = CreateTestMeta(); - - // Act - var result = Database.AddEntry(meta); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void AddEntry_添加记录后_可以查询到() - { - // Arrange - Database.InitializeDatabase(); - var meta = CreateTestMeta(); - - // Act - Database.AddEntry(meta); - - // Assert - using var connection = Database.GetConnection(); - connection.Open(); - var command = connection.CreateCommand(); - command.CommandText = "SELECT filename FROM audio_files WHERE unique_id = @id;"; - var param = command.CreateParameter(); - param.ParameterName = "@id"; - param.Value = meta.UniqueId; - command.Parameters.Add(param); - var filename = command.ExecuteScalar() as string; - filename.Should().Be(meta.Filename); - } - - [Fact] - public void AddEntries_批量添加_返回正确数量() - { - // Arrange - Database.InitializeDatabase(); - var entries = new List - { - CreateTestMeta("file1.wav"), - CreateTestMeta("file2.wav"), - CreateTestMeta("file3.wav"), - }; - - // Act - var count = Database.AddEntries(entries); - - // Assert - count.Should().Be(3); - } - - [Fact] - public void EntryExists_记录存在_返回True() - { - // Arrange - Database.InitializeDatabase(); - var meta = CreateTestMeta(); - Database.AddEntry(meta); - - // Act - var exists = Database.EntryExists(meta.Md5, meta.Path); - - // Assert - exists.Should().BeTrue(); - } - - [Fact] - public void EntryExists_记录不存在_返回False() - { - // Arrange - Database.InitializeDatabase(); - - // Act - var exists = Database.EntryExists("non_existent_md5", "/non/existent/path.wav"); - - // Assert - exists.Should().BeFalse(); - } - - [Fact] - public void AddEntry_重复UniqueId_第二次添加失败() - { - // Arrange - Database.InitializeDatabase(); - var meta1 = CreateTestMeta(); - var meta2 = CreateTestMeta(); - meta2.UniqueId = meta1.UniqueId; // 使用相同的 UniqueId - Database.AddEntry(meta1); - - // Act - var result = Database.AddEntry(meta2); - - // Assert - result.Should().BeFalse(); - } - - static AudioFileMeta CreateTestMeta(string filename = "test.wav") - { - return new AudioFileMeta - { - Id = 0, // 自增 ID,由数据库生成 - UniqueId = Guid.NewGuid().ToString("N"), - Md5 = Guid.NewGuid().ToString("N"), - Path = $"/test/path/{filename}", - Filename = filename, - Folder = "test", - Directory = "/test/path", - Duration = 10.5, - TotalSamples = 441000, - BitDepth = 24, - Channels = 2, - SampleRate = 44100, - Type = "WAV", - DateAdded = DateTime.Now, - OriginalModificationDate = DateTime.Now.AddDays(-1), - OriginationTime = DateTime.Now.AddDays(-1), - }; - } -} diff --git a/src/Core/AudioFileMeta.cs b/src/Core/AudioFileMeta.cs old mode 100644 new mode 100755 index 4ffaf2a..f8e688a --- a/src/Core/AudioFileMeta.cs +++ b/src/Core/AudioFileMeta.cs @@ -47,13 +47,16 @@ public class AudioFileMeta public required DateTime DateAdded { get; set; } /// 修改日期,音频创建或录制的日期 - public required DateTime OriginalModificationDate { get; set; } + public required DateTime LastWriteTime { get; set; } /// 创建时间,音频文件的原始创建时间 - public required DateTime OriginationTime { get; set; } + public required DateTime CreationTime { get; set; } #endregion + /// 音效名称,不含文件前缀的简短效果名(如 "Air Pressure Release Wet Short 01") + public string? FxName { get; set; } + ///声道配置 public string? ChannelLayout { get; set; } @@ -122,9 +125,10 @@ public class AudioFileMeta /// 来源引用,原始来源的参考编号 public string? BwfOriginatorRef { get; set; } - public DateTime? BwfDate { get; set; } + /// 编码历史,BWF格式的编码历史记录 + public string? BwfCodingHistory { get; set; } - public string? BwfDescription { get; set; } + public byte[]? BwfUmid { get; set; } /// 项目名称,所属制作项目 public string? ProjectName { get; set; } @@ -188,13 +192,14 @@ public class AudioFileMeta /// 版权,版权声明信息 public string? Copyright { get; set; } - /// 编码历史,BWF格式的编码历史记录 - public string? CodingHistory { get; set; } - /// 麦克风,录音使用的麦克风型号或设置 public string? Microphone { get; set; } public string? MicPerspective { get; set; } + + public byte[]? Artwork { get; set; } + + public byte[]? Waveform { get; set; } // 用户自定义字段(共8个) diff --git a/src/Core/AudioFileMetaValidator.cs b/src/Core/AudioFileMetaValidator.cs old mode 100644 new mode 100755 index 6b556ae..0080406 --- a/src/Core/AudioFileMetaValidator.cs +++ b/src/Core/AudioFileMetaValidator.cs @@ -38,8 +38,8 @@ public class AudioFileMetaValidator // 验证日期字段 ValidateRequiredField(meta.DateAdded, "DateAdded", errors); - ValidateRequiredField(meta.OriginalModificationDate, "OriginalModificationDate", errors); - ValidateRequiredField(meta.OriginationTime, "OriginationTime", errors); + ValidateRequiredField(meta.LastWriteTime, "LastWriteTime", errors); + ValidateRequiredField(meta.CreationTime, "CreationTime", errors); // 验证文件路径是否存在 if (!File.Exists(meta.Path)) diff --git a/src/Core/AudioFileScanner.cs b/src/Core/AudioFileScanner.cs old mode 100644 new mode 100755 diff --git a/src/Core/AudioMetadataReader.cs b/src/Core/AudioMetadataReader.cs old mode 100644 new mode 100755 index e3ba01f..66a7352 --- a/src/Core/AudioMetadataReader.cs +++ b/src/Core/AudioMetadataReader.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Security.Cryptography; using ATL; @@ -27,7 +28,7 @@ public class AudioMetadataReader { // 必备条目 Id = 0, // 自增 ID,由数据库生成 - UniqueId = GenerateUuid(), + UniqueId = Guid.NewGuid().ToString(), Md5 = CalculateMd5(audioFile.FullName), Path = audioFile.FullName, Filename = audioFile.Name, @@ -39,52 +40,88 @@ public class AudioMetadataReader SampleRate = track.SampleRate, Type = audioFile.Extension.TrimStart('.').ToUpperInvariant(), DateAdded = DateTime.Now, - OriginalModificationDate = audioFile.LastWriteTime, - OriginationTime = audioFile.CreationTime, + LastWriteTime = audioFile.LastWriteTime, + CreationTime = audioFile.CreationTime, // 可选条目 - Bpm = track.BPM > 0 ? track.BPM : null, - Description = track.Description, - Genre = track.Genre, - Artist = track.Artist, - Composer = track.Composer, - Comments = track.Comment, - Publisher = track.Publisher, - Copyright = track.Copyright, - CdTitle = track.Album, - DiscNumber = track.DiscNumber, - TrackTitle = track.Title, - ReleaseDate = track.OriginalReleaseDate, - TrackYear = track.Year, - TrackNumber = track.TrackNumber, - Group = track.Group, + Artist = track.Artist, + Artwork = track.EmbeddedPictures?.FirstOrDefault()?.PictureData, + Bpm = track.BPM > 0 ? track.BPM : null, + Category = GetField(track, "ixml.ASWG.category", "ixml.USER.CATEGORY"), + CatId = GetField(track, "ixml.ASWG.catId", "ixml.USER.CATID"), + CdTitle = track.Album, + ChannelLayout = track.ChannelsArrangement?.ToString() ?? string.Empty, + Composer = track.Composer, + Copyright = track.Copyright, + Description = track.Comment, + DiscNumber = track.DiscNumber, + FxName = GetField(track, "ixml.USER.FXNAME"), + Genre = track.Genre, + Group = track.Group, + Keywords = GetField(track, "ixml.USER.KEYWORDS"), + Library = GetField(track, "ixml.ASWG.library", "ixml.USER.LIBRARY"), + Manufacturer = GetField(track, "ixml.USER.MANUFACTURER"), + MicPerspective = GetField(track, "USER.MICPERSPECTIVE"), + Notes = GetField(track, "ixml.ASWG.notes", "ixml.USER.NOTES"), + Publisher = track.Publisher, + Rating = ParseRating(GetField(track, "USER.RATING")), + ReleaseDate = track.OriginalReleaseDate, + Subcategory = GetField(track, "USER.SUBCATEGORY", "ASWG.subCategory"), + TrackNumber = track.TrackNumber, + TrackTitle = track.Title, + TrackYear = track.Year, // BWF 特定字段 (通过附加数据获取) - CodingHistory = GetBwfField(track, "CodingHistory"), - BwfOriginator = GetBwfField(track, "BwfOriginator"), - BwfOriginatorRef = GetBwfField(track, "BwfOriginatorRef"), + BwfCodingHistory = GetField(track, "bext.coding_history"), + BwfOriginator = GetField(track, "bext.originator"), + BwfOriginatorRef = GetField(track, "bext.originatorRef"), }; return meta; } /// - /// 获取 BWF 字段值 + /// 从 AdditionalFields 中按优先级获取字段值。 + /// 返回第一个存在且非空的 value,全部未命中返回 null。 /// - private static string? GetBwfField(Track track, string fieldName) + static string? GetField(Track track, params string[] fieldNames) { - try + if (track.AdditionalFields is null) return null; + + foreach (string name in fieldNames) { - if (track.AdditionalFields != null && track.AdditionalFields.TryGetValue(fieldName, out string? field)) + if (track.AdditionalFields.TryGetValue(name, out string? field) + && !string.IsNullOrEmpty(field)) { return field; } } - catch + + return null; + } + + /// + /// 将 ATL AdditionalFields 中的 Rating 字符串解析为 uint? + /// 支持整数("3")和浮点("0.000000")两种格式 + /// 输入为空或无法解析时返回 null + /// + static uint? ParseRating(string? rawValue) + { + if (string.IsNullOrWhiteSpace(rawValue)) + return null; + + if (double.TryParse(rawValue, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out double value)) { - // 忽略 BWF 字段读取错误 + if (value is < 0 or > uint.MaxValue) + return null; + + return (uint)Math.Round(value); } + return null; } @@ -101,13 +138,4 @@ public class AudioMetadataReader byte[] hash = md5.ComputeHash(stream); return Convert.ToHexStringLower(hash); } - - /// - /// 生成 UUID - /// - /// UUID 字符串 - static string GenerateUuid() - { - return Guid.NewGuid().ToString(); - } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj old mode 100644 new mode 100755 index fabe4d1..147373c --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Core/Database.cs b/src/Core/Database.cs old mode 100644 new mode 100755 index fff52a5..6c417e8 --- a/src/Core/Database.cs +++ b/src/Core/Database.cs @@ -6,7 +6,7 @@ namespace OCES.Resonance.Core; public static class Database { - static readonly string DefaultConnectionString = "Data Source=default.db;Version=3;"; + static readonly string DefaultConnectionString = "Data Source=default.rdb;Version=3;"; /// /// 获取数据库连接 @@ -15,7 +15,7 @@ public static class Database /// 数据库连接 public static IDbConnection GetConnection(string dbName = "default") { - string connectionString = $"Data Source={dbName}.db;Version=3;"; + string connectionString = $"Data Source={dbName}.rdb;Version=3;"; return new SQLiteConnection(connectionString); } @@ -28,7 +28,7 @@ public static class Database connection.Open(); const string sql = @" - CREATE TABLE IF NOT EXISTS sounds ( + CREATE TABLE IF NOT EXISTS audio_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, unique_id TEXT NOT NULL UNIQUE, short_id TEXT, @@ -144,7 +144,7 @@ public static class Database @BwfOriginator, @BwfOriginatorRef, @ProjectName, @Library, @CdTitle, @TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint, @ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group, - @Markers, @Comments, @Notes, @Copyright, @CodingHistory, @Microphone, + @Markers, @Comments, @Notes, @Copyright, @BwfCodingHistory, @Microphone, @MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8 ); "; @@ -195,7 +195,7 @@ public static class Database @BwfOriginator, @BwfOriginatorRef, @ProjectName, @Library, @CdTitle, @TrackTitle, @Episode, @Scene, @Take, @Tape, @CueNumber, @SyncPoint, @ReleaseDate, @TrackYear, @IsEdited, @IsSplit, @Location, @Group, - @Markers, @Comments, @Notes, @Copyright, @CodingHistory, @Microphone, + @Markers, @Comments, @Notes, @Copyright, @BwfCodingHistory, @Microphone, @MicPerspective, @User1, @User2, @User3, @User4, @User5, @User6, @User7, @User8 ); "; diff --git a/src/Resonance.sln b/src/Resonance.sln old mode 100644 new mode 100755 index b91825e..1bbef9a --- a/src/Resonance.sln +++ b/src/Resonance.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Tests", "Core.Tests\Core.Tests.csproj", "{24F92458-FB39-44BE-A32F-41275183AF1B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AtlFieldExtractor", "AtlFieldExtractor\AtlFieldExtractor.csproj", "{17699013-3F4A-408F-84CE-FD4178B1699D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,18 @@ Global {24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x64.Build.0 = Release|Any CPU {24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x86.ActiveCfg = Release|Any CPU {24F92458-FB39-44BE-A32F-41275183AF1B}.Release|x86.Build.0 = Release|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x64.ActiveCfg = Debug|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x64.Build.0 = Debug|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x86.ActiveCfg = Debug|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Debug|x86.Build.0 = Debug|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Release|Any CPU.Build.0 = Release|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x64.ActiveCfg = Release|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x64.Build.0 = Release|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x86.ActiveCfg = Release|Any CPU + {17699013-3F4A-408F-84CE-FD4178B1699D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Resonance.sln.DotSettings b/src/Resonance.sln.DotSettings old mode 100644 new mode 100755 diff --git a/src/Resonance.sln.DotSettings.user b/src/Resonance.sln.DotSettings.user old mode 100644 new mode 100755 index de367d6..140ef8e --- a/src/Resonance.sln.DotSettings.user +++ b/src/Resonance.sln.DotSettings.user @@ -1,11 +1,17 @@  ForceIncluded + ForceIncluded <AssemblyExplorer> <Assembly Path="/Users/oliver/.nuget/packages/fluentassertions/8.9.0/lib/net6.0/FluentAssertions.dll" /> </AssemblyExplorer> + 24F92458-FB39-44BE-A32F-41275183AF1B + 33ff99d8-b1ad-4ed5-a4fd-376b97c2c841 <SessionState ContinuousTestingMode="0" IsActive="True" Name="IsSupportedAudioFile_ShouldWork" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <TestAncestor> <TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::OCES.Resonance.Core.Tests.AudioFileScannerTests</TestId> <TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::OCES.Resonance.Core.Tests.AudioMetadataReaderTests.ReadMetadata_文件不存在_抛出FileNotFoundException</TestId> + <TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.AudioMetadataReaderTest.ReadMetadata_ShoudThrow_FileNotFoundException</TestId> + <TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.AudioMetadataReaderTest.ReadMetadata_ShouldThrow_FileNotFoundException</TestId> + <TestId>xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.AudioMetadataReaderTest</TestId> </TestAncestor> </SessionState> \ No newline at end of file