feat: AudioMetadataReader

This commit is contained in:
2026-05-12 19:19:46 +08:00
parent dc11d65148
commit c4634bd981
25 changed files with 626 additions and 682 deletions
Regular → Executable
+12 -7
View File
@@ -47,13 +47,16 @@ public class AudioFileMeta
public required DateTime DateAdded { get; set; }
/// <summary>修改日期,音频创建或录制的日期</summary>
public required DateTime OriginalModificationDate { get; set; }
public required DateTime LastWriteTime { get; set; }
/// <summary>创建时间,音频文件的原始创建时间</summary>
public required DateTime OriginationTime { get; set; }
public required DateTime CreationTime { get; set; }
#endregion
/// <summary>音效名称,不含文件前缀的简短效果名(如 "Air Pressure Release Wet Short 01"</summary>
public string? FxName { get; set; }
///<summary>声道配置</summary>
public string? ChannelLayout { get; set; }
@@ -122,9 +125,10 @@ public class AudioFileMeta
/// <summary>来源引用,原始来源的参考编号</summary>
public string? BwfOriginatorRef { get; set; }
public DateTime? BwfDate { get; set; }
/// <summary>编码历史,BWF格式的编码历史记录</summary>
public string? BwfCodingHistory { get; set; }
public string? BwfDescription { get; set; }
public byte[]? BwfUmid { get; set; }
/// <summary>项目名称,所属制作项目</summary>
public string? ProjectName { get; set; }
@@ -188,13 +192,14 @@ public class AudioFileMeta
/// <summary>版权,版权声明信息</summary>
public string? Copyright { get; set; }
/// <summary>编码历史,BWF格式的编码历史记录</summary>
public string? CodingHistory { get; set; }
/// <summary>麦克风,录音使用的麦克风型号或设置</summary>
public string? Microphone { get; set; }
public string? MicPerspective { get; set; }
public byte[]? Artwork { get; set; }
public byte[]? Waveform { get; set; }
// 用户自定义字段(共8个)
+2 -2
View File
@@ -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))
Regular → Executable
View File
Regular → Executable
+64 -36
View File
@@ -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;
}
/// <summary>
/// 获取 BWF 字段值
/// 从 AdditionalFields 中按优先级获取字段值
/// 返回第一个存在且非空的 value,全部未命中返回 null。
/// </summary>
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;
}
/// <summary>
/// 将 ATL AdditionalFields 中的 Rating 字符串解析为 uint?
/// 支持整数("3")和浮点("0.000000")两种格式
/// 输入为空或无法解析时返回 null
/// </summary>
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);
}
/// <summary>
/// 生成 UUID
/// </summary>
/// <returns>UUID 字符串</returns>
static string GenerateUuid()
{
return Guid.NewGuid().ToString();
}
}
Regular → Executable
+1 -1
View File
@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="System.Data.SQLite" Version="2.0.3" />
<PackageReference Include="z440.atl.core" Version="7.12.0" />
<PackageReference Include="z440.atl.core" Version="7.13.0" />
</ItemGroup>
</Project>
Regular → Executable
+5 -5
View File
@@ -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;";
/// <summary>
/// 获取数据库连接
@@ -15,7 +15,7 @@ public static class Database
/// <returns>数据库连接</returns>
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
);
";