Skip to content

Commit 45a2901

Browse files
committed
Add AcoustID and R128 loudness tag support
- Add AcoustIdId and AcoustIdFingerprint properties for audio fingerprinting - Add R128TrackGain and R128AlbumGain for EBU R 128 loudness normalization - Implement in Tag base class, Id3v2Tag, and VorbisComment - Update CopyTo method to include new fields - Add comprehensive tests for all new properties
1 parent 98bbf66 commit 45a2901

4 files changed

Lines changed: 230 additions & 1 deletion

File tree

src/TagLibSharp2/Core/Tag.cs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,48 @@ public virtual string[] Composers {
526526
/// </remarks>
527527
public virtual string? ReplayGainAlbumPeak { get => null; set { } }
528528

529+
// R128 Loudness (EBU R 128)
530+
531+
/// <summary>
532+
/// Gets or sets the R128 track gain value.
533+
/// </summary>
534+
/// <remarks>
535+
/// <para>
536+
/// EBU R 128 is a loudness normalization recommendation from the European Broadcasting Union.
537+
/// The track gain is stored as an integer representing gain in 1/256th of a dB (Q7.8 format).
538+
/// </para>
539+
/// <para>
540+
/// For example, a value of "256" represents +1 dB, "-512" represents -2 dB.
541+
/// Some implementations store this as a plain dB string like "-14.00 dB".
542+
/// </para>
543+
/// <para>
544+
/// In ID3v2, this is stored in a TXXX frame with description "R128_TRACK_GAIN".
545+
/// In Vorbis Comments, this is stored as R128_TRACK_GAIN.
546+
/// Not all tag formats support this field. Default implementation returns null.
547+
/// </para>
548+
/// </remarks>
549+
public virtual string? R128TrackGain { get => null; set { } }
550+
551+
/// <summary>
552+
/// Gets or sets the R128 album gain value.
553+
/// </summary>
554+
/// <remarks>
555+
/// <para>
556+
/// EBU R 128 is a loudness normalization recommendation from the European Broadcasting Union.
557+
/// The album gain is stored as an integer representing gain in 1/256th of a dB (Q7.8 format).
558+
/// </para>
559+
/// <para>
560+
/// For example, a value of "256" represents +1 dB, "-512" represents -2 dB.
561+
/// Some implementations store this as a plain dB string like "-14.00 dB".
562+
/// </para>
563+
/// <para>
564+
/// In ID3v2, this is stored in a TXXX frame with description "R128_ALBUM_GAIN".
565+
/// In Vorbis Comments, this is stored as R128_ALBUM_GAIN.
566+
/// Not all tag formats support this field. Default implementation returns null.
567+
/// </para>
568+
/// </remarks>
569+
public virtual string? R128AlbumGain { get => null; set { } }
570+
529571
// MusicBrainz IDs
530572

531573
/// <summary>
@@ -652,6 +694,39 @@ public virtual string? MusicBrainzReleaseArtistId {
652694
/// </remarks>
653695
public virtual string? MusicBrainzReleaseCountry { get => null; set { } }
654696

697+
/// <summary>
698+
/// Gets or sets the AcoustID identifier.
699+
/// </summary>
700+
/// <remarks>
701+
/// <para>
702+
/// A UUID identifying this audio track in the AcoustID database.
703+
/// AcoustID uses audio fingerprints to identify tracks regardless of metadata.
704+
/// </para>
705+
/// <para>
706+
/// In ID3v2, this is stored in a TXXX frame with description "ACOUSTID_ID".
707+
/// In Vorbis Comments, this is stored as ACOUSTID_ID.
708+
/// Not all tag formats support this field. Default implementation returns null.
709+
/// </para>
710+
/// </remarks>
711+
public virtual string? AcoustIdId { get => null; set { } }
712+
713+
/// <summary>
714+
/// Gets or sets the AcoustID audio fingerprint.
715+
/// </summary>
716+
/// <remarks>
717+
/// <para>
718+
/// The chromaprint audio fingerprint used by AcoustID for audio identification.
719+
/// This is a compressed representation of the audio's acoustic characteristics.
720+
/// </para>
721+
/// <para>
722+
/// In ID3v2, this is stored in a TXXX frame with description "ACOUSTID_FINGERPRINT".
723+
/// In Vorbis Comments, this is stored as ACOUSTID_FINGERPRINT.
724+
/// Fingerprints are typically generated by the chromaprint/fpcalc utility.
725+
/// Not all tag formats support this field. Default implementation returns null.
726+
/// </para>
727+
/// </remarks>
728+
public virtual string? AcoustIdFingerprint { get => null; set { } }
729+
655730
/// <summary>
656731
/// Gets or sets the sort order for the composer name.
657732
/// </summary>
@@ -950,7 +1025,7 @@ public void CopyTo (Tag target, TagCopyOptions options = TagCopyOptions.All)
9501025
target.ComposersSort = ComposersSort;
9511026
}
9521027

953-
// ReplayGain
1028+
// ReplayGain and R128
9541029
if ((options & TagCopyOptions.ReplayGain) != 0) {
9551030
if (ReplayGainTrackGain is not null)
9561031
target.ReplayGainTrackGain = ReplayGainTrackGain;
@@ -960,6 +1035,10 @@ public void CopyTo (Tag target, TagCopyOptions options = TagCopyOptions.All)
9601035
target.ReplayGainAlbumGain = ReplayGainAlbumGain;
9611036
if (ReplayGainAlbumPeak is not null)
9621037
target.ReplayGainAlbumPeak = ReplayGainAlbumPeak;
1038+
if (R128TrackGain is not null)
1039+
target.R128TrackGain = R128TrackGain;
1040+
if (R128AlbumGain is not null)
1041+
target.R128AlbumGain = R128AlbumGain;
9631042
}
9641043

9651044
// MusicBrainz IDs
@@ -986,6 +1065,10 @@ public void CopyTo (Tag target, TagCopyOptions options = TagCopyOptions.All)
9861065
target.MusicBrainzReleaseType = MusicBrainzReleaseType;
9871066
if (MusicBrainzReleaseCountry is not null)
9881067
target.MusicBrainzReleaseCountry = MusicBrainzReleaseCountry;
1068+
if (AcoustIdId is not null)
1069+
target.AcoustIdId = AcoustIdId;
1070+
if (AcoustIdFingerprint is not null)
1071+
target.AcoustIdFingerprint = AcoustIdFingerprint;
9891072
}
9901073

9911074
// Pictures

src/TagLibSharp2/Id3/Id3v2/Id3v2Tag.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,18 @@ public override string? ReplayGainAlbumPeak {
708708
set => SetUserText ("REPLAYGAIN_ALBUM_PEAK", value);
709709
}
710710

711+
/// <inheritdoc/>
712+
public override string? R128TrackGain {
713+
get => GetUserText ("R128_TRACK_GAIN");
714+
set => SetUserText ("R128_TRACK_GAIN", value);
715+
}
716+
717+
/// <inheritdoc/>
718+
public override string? R128AlbumGain {
719+
get => GetUserText ("R128_ALBUM_GAIN");
720+
set => SetUserText ("R128_ALBUM_GAIN", value);
721+
}
722+
711723
/// <inheritdoc/>
712724
public override string? MusicBrainzTrackId {
713725
get => GetUserText ("MUSICBRAINZ_TRACKID");
@@ -802,6 +814,18 @@ public override string? MusicIpId {
802814
set => SetUserText ("MusicIP PUID", value);
803815
}
804816

817+
/// <inheritdoc/>
818+
public override string? AcoustIdId {
819+
get => GetUserText ("ACOUSTID_ID");
820+
set => SetUserText ("ACOUSTID_ID", value);
821+
}
822+
823+
/// <inheritdoc/>
824+
public override string? AcoustIdFingerprint {
825+
get => GetUserText ("ACOUSTID_FINGERPRINT");
826+
set => SetUserText ("ACOUSTID_FINGERPRINT", value);
827+
}
828+
805829
/// <summary>
806830
/// Gets or sets the roles of the performers.
807831
/// </summary>

src/TagLibSharp2/Xiph/VorbisComment.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,18 @@ public override string? ReplayGainAlbumPeak {
487487
set => SetValue ("REPLAYGAIN_ALBUM_PEAK", value);
488488
}
489489

490+
/// <inheritdoc/>
491+
public override string? R128TrackGain {
492+
get => GetValue ("R128_TRACK_GAIN");
493+
set => SetValue ("R128_TRACK_GAIN", value);
494+
}
495+
496+
/// <inheritdoc/>
497+
public override string? R128AlbumGain {
498+
get => GetValue ("R128_ALBUM_GAIN");
499+
set => SetValue ("R128_ALBUM_GAIN", value);
500+
}
501+
490502
/// <inheritdoc/>
491503
public override string? MusicBrainzTrackId {
492504
get => GetValue ("MUSICBRAINZ_TRACKID");
@@ -588,6 +600,18 @@ public override string? MusicIpId {
588600
set => SetValue ("MUSICIP_PUID", value);
589601
}
590602

603+
/// <inheritdoc/>
604+
public override string? AcoustIdId {
605+
get => GetValue ("ACOUSTID_ID");
606+
set => SetValue ("ACOUSTID_ID", value);
607+
}
608+
609+
/// <inheritdoc/>
610+
public override string? AcoustIdFingerprint {
611+
get => GetValue ("ACOUSTID_FINGERPRINT");
612+
set => SetValue ("ACOUSTID_FINGERPRINT", value);
613+
}
614+
591615
/// <inheritdoc/>
592616
#pragma warning disable CA1819 // Properties should not return arrays - TagLib# API compatibility
593617
public override string[]? PerformersRole {

tests/TagLibSharp2.Tests/Id3/Id3v2/Id3v2TagExtendedTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,104 @@ public void MusicBrainz_AllFields_RenderAndReadBack ()
188188

189189
#endregion
190190

191+
#region R128 Loudness Tests
192+
193+
[TestMethod]
194+
public void R128TrackGain_GetSet_RoundTrips ()
195+
{
196+
var tag = new Id3v2Tag ();
197+
var gain = "-512"; // -2 dB in Q7.8 format
198+
199+
tag.R128TrackGain = gain;
200+
201+
Assert.AreEqual (gain, tag.R128TrackGain);
202+
Assert.AreEqual (gain, tag.GetUserText ("R128_TRACK_GAIN"));
203+
}
204+
205+
[TestMethod]
206+
public void R128AlbumGain_GetSet_RoundTrips ()
207+
{
208+
var tag = new Id3v2Tag ();
209+
var gain = "256"; // +1 dB in Q7.8 format
210+
211+
tag.R128AlbumGain = gain;
212+
213+
Assert.AreEqual (gain, tag.R128AlbumGain);
214+
Assert.AreEqual (gain, tag.GetUserText ("R128_ALBUM_GAIN"));
215+
}
216+
217+
[TestMethod]
218+
public void R128_AllFields_RenderAndReadBack ()
219+
{
220+
var tag = new Id3v2Tag ();
221+
tag.R128TrackGain = "-512";
222+
tag.R128AlbumGain = "256";
223+
224+
var rendered = tag.Render ();
225+
var readResult = Id3v2Tag.Read (rendered.Span);
226+
227+
Assert.IsTrue (readResult.IsSuccess);
228+
Assert.AreEqual ("-512", readResult.Tag!.R128TrackGain);
229+
Assert.AreEqual ("256", readResult.Tag.R128AlbumGain);
230+
}
231+
232+
#endregion
233+
234+
#region AcoustID Tests
235+
236+
[TestMethod]
237+
public void AcoustIdId_GetSet_RoundTrips ()
238+
{
239+
var tag = new Id3v2Tag ();
240+
var acoustId = "f4e7c9d8-1234-5678-9abc-def012345678";
241+
242+
tag.AcoustIdId = acoustId;
243+
244+
Assert.AreEqual (acoustId, tag.AcoustIdId);
245+
Assert.AreEqual (acoustId, tag.GetUserText ("ACOUSTID_ID"));
246+
}
247+
248+
[TestMethod]
249+
public void AcoustIdFingerprint_GetSet_RoundTrips ()
250+
{
251+
var tag = new Id3v2Tag ();
252+
var fingerprint = "AQADtNQyRUkSRZEGAAAAAA";
253+
254+
tag.AcoustIdFingerprint = fingerprint;
255+
256+
Assert.AreEqual (fingerprint, tag.AcoustIdFingerprint);
257+
Assert.AreEqual (fingerprint, tag.GetUserText ("ACOUSTID_FINGERPRINT"));
258+
}
259+
260+
[TestMethod]
261+
public void AcoustId_SetNull_RemovesField ()
262+
{
263+
var tag = new Id3v2Tag ();
264+
tag.AcoustIdId = "f4e7c9d8-1234-5678-9abc-def012345678";
265+
266+
tag.AcoustIdId = null;
267+
268+
Assert.IsNull (tag.AcoustIdId);
269+
Assert.IsNull (tag.GetUserText ("ACOUSTID_ID"));
270+
}
271+
272+
[TestMethod]
273+
public void AcoustId_AllFields_RenderAndReadBack ()
274+
{
275+
var tag = new Id3v2Tag ();
276+
tag.AcoustIdId = "f4e7c9d8-1234-5678-9abc-def012345678";
277+
tag.AcoustIdFingerprint = "AQADtNQyRUkSRZEGAAAAAA";
278+
279+
var rendered = tag.Render ();
280+
var readResult = Id3v2Tag.Read (rendered.Span);
281+
282+
Assert.IsTrue (readResult.IsSuccess);
283+
Assert.AreEqual ("f4e7c9d8-1234-5678-9abc-def012345678", readResult.Tag!.AcoustIdId);
284+
Assert.AreEqual ("AQADtNQyRUkSRZEGAAAAAA", readResult.Tag.AcoustIdFingerprint);
285+
}
286+
287+
#endregion
288+
191289
#region Combined Tests
192290

193291
[TestMethod]

0 commit comments

Comments
 (0)