Skip to content

Commit

Permalink
Better handing of UCS2 in ID3 tags. (Replaces PR #64).
Browse files Browse the repository at this point in the history
  • Loading branch information
Corey-M committed May 30, 2023
1 parent ed76eb6 commit e4658ce
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 81 deletions.
58 changes: 46 additions & 12 deletions LameDLLWrap/LibMp3Lame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,16 +343,13 @@ public bool StrictISO
#endregion

#region Filtering control
#pragma warning disable 1591
public int LowPassFreq { get { return NativeMethods.lame_get_lowpassfreq(context); } set { Setter(NativeMethods.lame_set_lowpassfreq, value); } }
public int LowPassWidth { get { return NativeMethods.lame_get_lowpasswidth(context); } set { Setter(NativeMethods.lame_set_lowpasswidth, value); } }
public int HighPassFreq { get { return NativeMethods.lame_get_highpassfreq(context); } set { Setter(NativeMethods.lame_set_highpassfreq, value); } }
public int HighPassWidth { get { return NativeMethods.lame_get_highpasswidth(context); } set { Setter(NativeMethods.lame_set_highpasswidth, value); } }
#pragma warning restore 1591
#endregion

#region Internal state variables, read only
#pragma warning disable 1591
public MPEGVersion Version { get { return NativeMethods.lame_get_version(context); } }
public int EncoderDelay { get { return NativeMethods.lame_get_encoder_delay(context); } }
public int EncoderPadding { get { return NativeMethods.lame_get_encoder_padding(context); } }
Expand All @@ -365,7 +362,6 @@ public bool StrictISO
public float PeakSample { get { return NativeMethods.lame_get_PeakSample(context); } }
public int NoClipGainChange { get { return NativeMethods.lame_get_noclipGainChange(context); } }
public float NoClipScale { get { return NativeMethods.lame_get_noclipScale(context); } }
#pragma warning restore 1591
#endregion

#endregion
Expand Down Expand Up @@ -504,6 +500,28 @@ public void SetMsgFunc(ReportFunction fn)
#region ID3 tag support
private static GenreCallback id3GenreCallback = null;

/// <summary>.NET Standard 2.0 does not include Latin1 in the standing Encoding list, but supports it.</summary>
private static readonly Encoding Latin1 = Encoding.GetEncoding("Latin1");

private static bool IsLatin1(string text)
{
if (text is null)
return true;
byte[] bytes = Latin1.GetBytes(text);
return bytes.Length == text.Length && Latin1.GetString(bytes) == text;
}

/// <summary>Add TextInfo with <paramref name="id"/>, as either Latin1 or UCS2 (with BOM/terminator) depending on content.</summary>
/// <param name="id">TextInfo identifier.</param>
/// <param name="text">Content.</param>
/// <returns>Result of attempting to add TextInfo tag.</returns>
public bool ID3SetTextInfo(string id, string text)
{
if (IsLatin1(text))
return CheckResult(NativeMethods.id3tag_set_textinfo_latin1(context, id, text));
return CheckResult(NativeMethods.id3tag_set_textinfo_utf16(context, id, UCS2.GetBytes(text)));
}

private static void ID3Genre_proxy(int index, string genre, IntPtr cookie)
{
id3GenreCallback?.Invoke(index, genre);
Expand Down Expand Up @@ -577,24 +595,36 @@ public void ID3SetPad(int nBytes)
/// <param name="title">Value to set</param>
public void ID3SetTitle(string title)
{
NativeMethods.id3tag_set_title(context, title);
if (IsLatin1(title))
NativeMethods.id3tag_set_title(context, title);
else
ID3SetTextInfo("TIT2", title);
}

public void ID3SetArtist(string artist)
{
NativeMethods.id3tag_set_artist(context, artist);
if (IsLatin1(artist))
NativeMethods.id3tag_set_artist(context, artist);
else
ID3SetTextInfo("TPE1", artist);
}

public void ID3SetAlbum(string album)
{
NativeMethods.id3tag_set_album(context, album);
if (IsLatin1(album))
NativeMethods.id3tag_set_album(context, album);
else
ID3SetTextInfo("TALB", album);
}

/// <summary>Set year</summary>
/// <param name="year">Year value to set, as string</param>
public void ID3SetYear(string year)
{
NativeMethods.id3tag_set_year(context, year);
if (IsLatin1(year))
NativeMethods.id3tag_set_year(context, year);
else
ID3SetTextInfo("TYER", year);
}

/// <summary>Set year</summary>
Expand All @@ -606,7 +636,7 @@ public void ID3SetYear(int year)

public bool ID3SetComment(string comment)
{
if (Encoding.UTF8.GetByteCount(comment) == comment.Length)
if (IsLatin1(comment))
return CheckResult(NativeMethods.id3tag_set_comment(context, comment));

// Comment is Unicode. Encode as UCS2 with BOM and terminator.
Expand All @@ -616,12 +646,16 @@ public bool ID3SetComment(string comment)

public bool ID3SetTrack(string track)
{
return CheckResult(NativeMethods.id3tag_set_track(context, track));
if (IsLatin1(track))
return CheckResult(NativeMethods.id3tag_set_track(context, track));
return ID3SetTextInfo("TRCK", track);
}

public bool ID3SetGenre(string genre)
{
return CheckResult(NativeMethods.id3tag_set_genre(context, genre));
if (IsLatin1(genre))
return CheckResult(NativeMethods.id3tag_set_genre(context, genre));
return ID3SetTextInfo("TCON", genre);
}

public int ID3SetGenre(int genreIndex)
Expand All @@ -631,7 +665,7 @@ public int ID3SetGenre(int genreIndex)

public bool ID3SetFieldValue(string value)
{
if (Encoding.UTF8.GetByteCount(value) == value.Length)
if (IsLatin1(value))
return CheckResult(NativeMethods.id3tag_set_fieldvalue(context, value));

// Value is Unicode. Encode as UCS2 with BOM and terminator.
Expand Down
9 changes: 9 additions & 0 deletions LameDLLWrap/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,15 @@ internal static string printf(string format, IntPtr va_args)
[DllImport(libname, CallingConvention = CallingConvention.Cdecl)]
internal static extern int id3tag_set_albumart(IntPtr context, [In]byte[] image, int size);

[DllImport(libname, CallingConvention = CallingConvention.Cdecl)]
internal static extern int id3tag_set_textinfo_latin1(IntPtr context, [MarshalAs(UnmanagedType.LPStr), In] string id, [MarshalAs(UnmanagedType.LPStr), In] string text);

[DllImport(libname, CallingConvention = CallingConvention.Cdecl)]
internal static extern int id3tag_set_textinfo_utf16(IntPtr context, [MarshalAs(UnmanagedType.LPStr), In] string id, [MarshalAs(UnmanagedType.LPWStr), In] string text);

[DllImport(libname, CallingConvention = CallingConvention.Cdecl)]
internal static extern int id3tag_set_textinfo_utf16(IntPtr context, [MarshalAs(UnmanagedType.LPStr), In] string id, byte[] bytes);

// lame_get_id3v1_tag copies ID3v1 tag into buffer.
// Function returns number of bytes copied into buffer, or number
// of bytes required if 'size' is too small.
Expand Down
122 changes: 64 additions & 58 deletions NAudio.Lame/ID3TagData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,72 @@

namespace NAudio.Lame
{/// <summary>ID3 tag content</summary>
public class ID3TagData
{
// Standard values:
/// <summary>Track title (TIT2)</summary>
public string Title;
/// <summary>Artist (TPE1)</summary>
public string Artist;
/// <summary>Album (TALB)</summary>
public string Album;
/// <summary>Year (TYER)</summary>
public string Year;
/// <summary>Comment (COMM)</summary>
public string Comment;
/// <summary>Genre (TCON)</summary>
public string Genre;
/// <summary>Track number (TRCK)</summary>
public string Track;
public class ID3TagData
{
// Standard values:
/// <summary>Track title (TIT2)</summary>
public string Title;
/// <summary>Artist (TPE1)</summary>
public string Artist;
/// <summary>Album (TALB)</summary>
public string Album;
/// <summary>Year (TYER)</summary>
public string Year;
/// <summary>Comment (COMM)</summary>
public string Comment;
/// <summary>Genre (TCON)</summary>
public string Genre;
/// <summary>Track number (TRCK)</summary>
public string Track;

// Experimental:
/// <summary>Subtitle (TIT3)</summary>
public string Subtitle;
/// <summary>AlbumArtist (TPE2)</summary>
public string AlbumArtist;
// Experimental:
/// <summary>Subtitle (TIT3)</summary>
public string Subtitle;
/// <summary>AlbumArtist (TPE2)</summary>
public string AlbumArtist;

/// <summary>User defined text frames (TXXX) - Multiples are allowed as long as their description is unique (Format : "description=text")</summary>
/// <remarks>
/// Obsolete. Please use <see cref="UserDefinedText"/> property instead.
///
/// Implemented as accessor to <see cref="UserDefinedText"/> Dictionary.
///
/// If multiple tags with the same description are supplied only the last one is used.
/// </remarks>
[Obsolete("Use the UserDefinedText property instead.", false)]
public string[] UserDefinedTags
{
get => UserDefinedText.Select(kv => $"{kv.Key}={kv.Value}").ToArray();
set => SetUDT(value);
}
/// <summary>User defined text frames (TXXX) - Multiples are allowed as long as their description is unique (Format : "description=text")</summary>
/// <remarks>
/// Obsolete. Please use <see cref="UserDefinedText"/> property instead.
///
/// Implemented as accessor to <see cref="UserDefinedText"/> Dictionary.
///
/// If multiple tags with the same description are supplied only the last one is used.
/// </remarks>
[Obsolete("Use the UserDefinedText property instead.", false)]
public string[] UserDefinedTags
{
get => UserDefinedText.Select(kv => $"{kv.Key}={kv.Value}").ToArray();
set => SetUDT(value);
}

/// <summary>User defined text frames (TXXX)</summary>
/// <remarks>Stored in ID3v2 tag as one TXXX frame per item.</remarks>
public Dictionary<string, string> UserDefinedText { get; } = new Dictionary<string, string>();
/// <summary>Album art - PNG, JPG or GIF file content</summary>
public byte[] AlbumArt;

/// <summary>Album art - PNG, JPG or GIF file content</summary>
public byte[] AlbumArt;
/// <summary>Create ID3v2 tag only when set, else create ID3v1 tag as well.</summary>
public bool V2Only { get; set; } = true;

/// <summary>
/// Clear <see cref="UserDefinedText"/> and insret values from collection of "description=text" strings.
/// </summary>
/// <param name="data">Collection to load.</param>
public void SetUDT(IEnumerable<string> data)
{
UserDefinedText.Clear();
foreach (var item in data)
{
string key = item.Split('=').First();
int valuePos = key.Length + 1;
string val = valuePos > item.Length ? string.Empty : item.Substring(valuePos);
UserDefinedText[key] = val;
}
}
}
}
/// <summary>Custom field values to add to ID3v2 tag during write.</summary>
public Dictionary<string, string> CustomFields { get; } = new Dictionary<string, string>();

/// <summary>User defined text frames (TXXX)</summary>
/// <remarks>Stored in ID3v2 tag as one TXXX frame per item.</remarks>
public Dictionary<string, string> UserDefinedText { get; } = new Dictionary<string, string>();

/// <summary>
/// Clear <see cref="UserDefinedText"/> and insert values from collection of "description=text" strings.
/// </summary>
/// <param name="data">Collection to load.</param>
public void SetUDT(IEnumerable<string> data)
{
UserDefinedText.Clear();
foreach (var item in data)
{
string key = item.Split('=').First();
int valuePos = key.Length + 1;
string val = valuePos > item.Length ? string.Empty : item.Substring(valuePos);
UserDefinedText[key] = val;
}
}
}
}
27 changes: 16 additions & 11 deletions NAudio.Lame/LameMP3FileWriter.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using LameDLLWrap;
using NAudio.Wave;
using LameDLLWrap;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace NAudio.Lame
{
Expand Down Expand Up @@ -192,7 +192,7 @@ protected override void Dispose(bool disposing)

_lame?.Dispose();
_lame = null;

if (_disposeOutput)
{
_outStream?.Dispose();
Expand Down Expand Up @@ -247,13 +247,13 @@ private void Encode()
/// <summary>Non-seekable stream. Always false.</summary>
public override bool CanSeek => false;
/// <summary>True when encoder can accept more data</summary>
public override bool CanWrite => _outStream != null && _lame != null;
public override bool CanWrite => _outStream != null && _lame != null;

/// <summary>Dummy Position. Always 0.</summary>
public override long Position
{
get => 0;
set => throw new NotImplementedException();
set => throw new NotImplementedException();
}

/// <summary>Dummy Length. Always 0.</summary>
Expand Down Expand Up @@ -411,6 +411,13 @@ private void ApplyID3Tag(ID3TagData tag)

_lame.ID3Init();

if (tag.V2Only)
_lame.ID3V2Only();

// Add custom fields
foreach (var kv in tag.CustomFields)
_lame.ID3SetFieldValue($"{kv.Key}={kv.Value}");

// Apply standard ID3 fields
if (!string.IsNullOrEmpty(tag.Title))
_lame.ID3SetTitle(tag.Title);
Expand All @@ -435,19 +442,17 @@ private void ApplyID3Tag(ID3TagData tag)

// Add user-defined tags if present
foreach (var kv in tag.UserDefinedText)
{
_lame.ID3SetFieldValue($"TXXX={kv.Key}={kv.Value}");
}

// Set the album art if supplied
if (tag.AlbumArt?.Length > 0)
_lame.ID3SetAlbumArt(tag.AlbumArt);

// check size of ID3 tag, if too large write it ourselves.
// If ID3 tag is too large LAME fails to write it correctly, so write it ourselves.
byte[] data = _lame.ID3GetID3v2Tag();
if (data?.Length >= 32768)
{
_lame.ID3WriteTagAutomatic = false;

_outStream.Write(data, 0, data.Length);
}
}
Expand Down

0 comments on commit e4658ce

Please sign in to comment.