diff --git a/IdSharp.Tagging/APEv2/APEv2Tag.cs b/IdSharp.Tagging/APEv2/APEv2Tag.cs index b2f23e4..313e189 100644 --- a/IdSharp.Tagging/APEv2/APEv2Tag.cs +++ b/IdSharp.Tagging/APEv2/APEv2Tag.cs @@ -28,6 +28,8 @@ public partial class APEv2Tag : IAPEv2Tag private int _tagSize; private int _version; private readonly Dictionary _items = new Dictionary(); + private readonly ReplayGainTagItems _replayGainItems = new ReplayGainTagItems(); + private readonly MP3GainTagItems _mp3GainItems = new MP3GainTagItems(); private string _title; private string _artist; @@ -335,6 +337,12 @@ private void ReadField(Stream stream) Language = itemValue; break; } + + if (itemKey.StartsWith(ReplayGainTagItems.TAG_PREFIX)) + _replayGainItems.SetField(itemKey, itemValue); + else if (itemKey.StartsWith(MP3GainTagItems.TAG_PREFIX)) + _mp3GainItems.SetField(itemKey, itemValue); + } /// @@ -457,6 +465,22 @@ public string Language set { _language = value; RaisePropertyChanged("Language"); } } + /// + /// Gets the ReplayGain items found in the tag + /// + /// ReplayGain items + public ReplayGainTagItems ReplayGainItems { + get { return _replayGainItems; } + } + + /// + /// Gets the MP3Gain items found in the tag + /// + /// MP3Gain items + public MP3GainTagItems MP3GainItems { + get { return _mp3GainItems; } + } + private void RaisePropertyChanged(string propertyName) { PropertyChangedEventHandler propertyChanged = PropertyChanged; diff --git a/IdSharp.Tagging/APEv2/MP3GainTagItems.cs b/IdSharp.Tagging/APEv2/MP3GainTagItems.cs new file mode 100644 index 0000000..36c7204 --- /dev/null +++ b/IdSharp.Tagging/APEv2/MP3GainTagItems.cs @@ -0,0 +1,267 @@ +using System; +using System.ComponentModel; + + +namespace IdSharp.Tagging.APEv2 { + + /// + /// A collection of tag items related to MP3Gain + /// + public class MP3GainTagItems { + + #region Exposed constants + + /// + /// The prefix used to indicate that the tag item is related to MP3Gain + /// + public const string TAG_PREFIX = "MP3GAIN_"; + + #endregion Exposed constants + + + #region Private variables + + private short? _trackMin; + private short? _trackMax; + private short? _albumMin; + private short? _albumMax; + private short? _undoLeftChannel; + private short? _undoRightChannel; + private bool? _undoWrap; + + #endregion Private variables + + + #region Exposed events + + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion Exposed events + + + #region Exposed properties + + /// + /// Gets or sets the track's minimum gain + /// + /// Track's minimum gain + public short? TrackMinimumGain { + get { return _trackMin; } + set { _trackMin = value; RaisePropertyChanged("TrackMinimumGain"); } + } + + /// + /// Gets or sets the track's maximum gain + /// + /// Track's maximum gain + public short? TrackMaximumGain { + get { return _trackMax; } + set { _trackMax = value; RaisePropertyChanged("TrackMaximumGain"); } + } + + /// + /// Gets or sets the track's minimum gain in the context of the album + /// + /// Album's minimum gain + public short? AlbumMinimumGain { + get { return _albumMin; } + set { _albumMin = value; RaisePropertyChanged("AlbumMinimumGain"); } + } + + /// + /// Gets or sets the track's maximum gain in the context of the album + /// + /// Album's maximum gain + public short? AlbumMaximumGain { + get { return _albumMax; } + set { _albumMax = value; RaisePropertyChanged("AlbumMaximumGain"); } + } + + /// + /// Gets or sets the number of steps necessary to undo the gain adjustment of the track's left channel + /// + /// Left channel's undo amount + public short? UndoLeftChannelAdjustment { + get { return _undoLeftChannel; } + set { _undoLeftChannel = value; RaisePropertyChanged("UndoLeftChannelAdjustment"); } + } + + /// + /// Gets or sets the number of steps necessary to undo the gain adjustment of the track's right channel + /// + /// Right channel's undo amount + public short? UndoRightChannelAdjustment { + get { return _undoRightChannel; } + set { _undoRightChannel = value; RaisePropertyChanged("UndoRightChannelAdjustment"); } + } + + /// + /// Gets or sets the flag indicating if wrapping occurred during the gain adjustment on the track + /// + /// Undo wrap flag + public bool? UndoWrapFlag { + get { return _undoWrap; } + set { _undoWrap = value; RaisePropertyChanged("UndoWrapFlag"); } + } + + /// + /// Gets the decibels of gain adjustment performed on the track's left channel + /// + /// Left channel's undo amount + public decimal? UndoLeftChannelAdjustmentInDecibels { + get { return ConvertToDecibels(UndoLeftChannelAdjustment); } + } + + /// + /// Gets the decibels of gain adjustment performed on the track's right channel + /// + /// Right channel's undo amount + public decimal? UndoRightChannelAdjustmentInDecibels { + get { return ConvertToDecibels(UndoRightChannelAdjustment); } + } + + + internal string TrackMinMaxText { + get { + if (_trackMin.HasValue && _trackMax.HasValue) + return ConvertValue(_trackMin.Value) + "," + ConvertValue(_trackMax.Value); + else + return null; + } + set { + TrackMinimumGain = ConvertValue(value, 0); + TrackMaximumGain = ConvertValue(value, 1); + } + } + + internal string AlbumMinMaxText { + get { + if (_albumMin.HasValue && _albumMax.HasValue) + return ConvertValue(_albumMin.Value) + "," + ConvertValue(_albumMax.Value); + else + return null; + } + set { + AlbumMinimumGain = ConvertValue(value, 0); + AlbumMaximumGain = ConvertValue(value, 1); + } + } + + internal string UndoText { + get { + if (_undoLeftChannel.HasValue && _undoRightChannel.HasValue && _undoWrap.HasValue) + return ConvertValue(_undoLeftChannel.Value, true) + "," + + ConvertValue(_undoRightChannel.Value, true) + "," + (_undoWrap.Value ? "W" : "N"); + else + return null; + } + set { + UndoLeftChannelAdjustment = ConvertValue(value, 0); + UndoRightChannelAdjustment = ConvertValue(value, 1); + + string val = ConvertValueToString(value, 2); + + if (string.IsNullOrWhiteSpace(val)) + UndoWrapFlag = null; + else if (val.ToUpper() == "N") + UndoWrapFlag = false; + else if (val.ToUpper() == "W") + UndoWrapFlag = true; + else + UndoWrapFlag = null; + } + } + + #endregion Exposed properties + + + #region Exposed methods + + internal void SetField(string key, string value) { + + if (!key.StartsWith(TAG_PREFIX)) + return; + + if (key == "MP3GAIN_MINMAX") + TrackMinMaxText = value; + else if (key == "MP3GAIN_ALBUM_MINMAX") + AlbumMinMaxText = value; + else if (key == "MP3GAIN_UNDO") + UndoText = value; + } + + #endregion Exposed methods + + + #region Private methods + + private string ConvertValueToString(string value, int position) { + + if (value == null) + return null; + + string[] parts = value.Split(new char[] { ',' }); + + if (parts.Length <= position) + return null; + + return parts[position]; + } + + private short? ConvertValue(string value, int position) { + + string part = ConvertValueToString(value, position); + + if (string.IsNullOrWhiteSpace(part)) + return null; + + short result; + + if (short.TryParse(part, out result)) + return result; + + return null; + } + + private string ConvertValue(short? value, bool includeSign) { + + if (!value.HasValue) + return null; + + return value.Value.ToString((includeSign ? "+" : "") + "000;-000;"); + } + + private string ConvertValue(short? value) { + + return ConvertValue(value, false); + } + + private decimal? ConvertToDecibels(short? value) { + + if (!value.HasValue) + return null; + else + return value.Value * (decimal) 1.5; + } + + private short? ConvertFromDecibels(decimal? value) { + + if (!value.HasValue) + return null; + else + return (short) (value.Value / (decimal) 1.5); + } + + + private void RaisePropertyChanged(string propertyName) { + + if (PropertyChanged != null) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion Private methods + } +} \ No newline at end of file diff --git a/IdSharp.Tagging/APEv2/ReplayGainTagItems.cs b/IdSharp.Tagging/APEv2/ReplayGainTagItems.cs new file mode 100644 index 0000000..cc8c1b1 --- /dev/null +++ b/IdSharp.Tagging/APEv2/ReplayGainTagItems.cs @@ -0,0 +1,201 @@ +using System; +using System.ComponentModel; + + +namespace IdSharp.Tagging.APEv2 { + + /// + /// A collection of tag items related to ReplayGain + /// + public class ReplayGainTagItems { + + #region Exposed constants + + /// + /// The prefix used to indicate that the tag item is related to ReplayGain + /// + public const string TAG_PREFIX = "REPLAYGAIN_"; + + #endregion Exposed constants + + + #region Private variables + + private decimal? _albumGain; + private decimal? _albumPeak; + private decimal? _trackGain; + private decimal? _trackPeak; + + #endregion Private variables + + + #region Exposed events + + /// + /// Occurs when a property value changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + #endregion Exposed events + + + #region Exposed properties + + /// + /// Gets or sets the gain to be applied to the track for normalization in the context of the album + /// + /// Album's gain + public decimal? AlbumGain { + get { return _albumGain; } + set { _albumGain = value; RaisePropertyChanged("AlbumGain"); } + } + + /// + /// Gets or sets the track's "linear" peak value in the context of the album + /// + /// Album's peak value + public decimal? AlbumPeak { + get { return _albumPeak; } + set { _albumPeak = value; RaisePropertyChanged("AlbumPeak"); } + } + + /// + /// Gets or sets the gain to be applied to the track for normalization + /// + /// Track's gain + public decimal? TrackGain { + get { return _trackGain; } + set { _trackGain = value; RaisePropertyChanged("TrackGain"); } + } + + /// + /// Gets or sets the track's "linear" peak value + /// + /// Track's peak value + public decimal? TrackPeak { + get { return _trackPeak; } + set { _trackPeak = value; RaisePropertyChanged("TrackPeak"); } + } + + /// + /// Gets the track's "decibel" peak value in the context of the album + /// + /// Album's peak value + public decimal? AlbumPeakInDecibels { + get { return ConvertToDecibels(AlbumPeak); } + } + + /// + /// Gets the track's "decibel" peak value + /// + /// Track's peak value + public decimal? TrackPeakInDecibels { + get { return ConvertToDecibels(TrackPeak); } + } + + + internal string AlbumGainText { + get { return ConvertValue(_albumGain, " dB"); } + set { AlbumGain = ConvertValue(value, " dB"); } + } + + internal string AlbumPeakText { + get { return ConvertValue(_albumPeak); } + set { AlbumPeak = ConvertValue(value); } + } + + internal string TrackGainText { + get { return ConvertValue(_trackGain, " dB"); } + set { TrackGain = ConvertValue(value, " dB"); } + } + + internal string TrackPeakText { + get { return ConvertValue(_trackPeak); } + set { TrackPeak = ConvertValue(value); } + } + + #endregion Exposed properties + + + #region Exposed methods + + internal void SetField(string key, string value) { + + if (!key.StartsWith("REPLAYGAIN_")) + return; + + if (key == "REPLAYGAIN_ALBUM_GAIN") + AlbumGainText = value; + else if (key == "REPLAYGAIN_ALBUM_PEAK") + AlbumPeakText = value; + else if (key == "REPLAYGAIN_TRACK_GAIN") + TrackGainText = value; + else if (key == "REPLAYGAIN_TRACK_PEAK") + TrackPeakText = value; + } + + #endregion Exposed methods + + + #region Private methods + + private decimal? ConvertValue(string value, string textToRemove) { + + if (value == null) + return null; + + if (!string.IsNullOrEmpty(textToRemove)) + value = value.Replace(textToRemove, ""); + + decimal result; + + if (decimal.TryParse(value, out result)) + return result; + + return null; + } + + private decimal? ConvertValue(string value) { + + return ConvertValue(value, null); + } + + private string ConvertValue(decimal? value, string textToAdd) { + + if (!value.HasValue) + return null; + + return value.Value.ToString("#0.000000") + (textToAdd != null ? textToAdd : ""); + } + + private string ConvertValue(decimal? value) { + + return ConvertValue(value, null); + } + + private decimal? ConvertToDecibels(decimal? value) { + + if (!value.HasValue) + return null; + else + return 20 * (decimal) Math.Log10((double) value); + } + + private decimal? ConvertFromDecibels(decimal? value) { + + if (!value.HasValue) + return null; + else + return (decimal) Math.Pow(10, (double) (value / 20)); + } + + + private void RaisePropertyChanged(string propertyName) { + + if (PropertyChanged != null) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion Private methods + } +} \ No newline at end of file diff --git a/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameCreation.cs b/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameCreation.cs index 971ef97..d783a54 100644 --- a/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameCreation.cs +++ b/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameCreation.cs @@ -31,6 +31,7 @@ public abstract partial class FrameContainer : IFrameContainer private readonly UrlBindingList m_ArtistUrlList; private readonly UrlBindingList m_CommercialInfoUrlList; private readonly UserDefinedTextBindingList m_UserDefinedTextList; + private readonly UserDefinedTextBindingList m_ReplayGainList; private readonly RelativeVolumeAdjustmentBindingList m_RelativeVolumeAdjustmentList; // TODO: this is a single occurrence in 2.3 and 2.2 private readonly UnsynchronizedLyricsBindingList m_UnsynchronizedLyricsList; private readonly GeneralEncapsulatedObjectBindingList m_GeneralEncapsulatedObjectList; @@ -155,6 +156,7 @@ internal FrameContainer() m_CommercialInfoUrlList = new UrlBindingList("WCOM", "WCOM", "WCM"); m_ArtistUrlList = new UrlBindingList("WOAR", "WOAR", "WAR"); m_UserDefinedTextList = new UserDefinedTextBindingList(); + m_ReplayGainList = new UserDefinedTextBindingList(); m_RelativeVolumeAdjustmentList = new RelativeVolumeAdjustmentBindingList(); m_UnsynchronizedLyricsList = new UnsynchronizedLyricsBindingList(); m_GeneralEncapsulatedObjectList = new GeneralEncapsulatedObjectBindingList(); @@ -182,6 +184,7 @@ internal FrameContainer() //"CommercialInfoUrl", new MethodInvoker(ValidateCommercialInfoUrl)); // TODO AddMultipleOccurrenceFrame("WOAR", "WOAR", "WAR", m_ArtistUrlList); AddMultipleOccurrenceFrame("TXXX", "TXXX", "TXX", m_UserDefinedTextList); + AddMultipleOccurrenceFrame(null, null, null, m_ReplayGainList); AddMultipleOccurrenceFrame("RVA2", "RVAD", "RVA", m_RelativeVolumeAdjustmentList); AddMultipleOccurrenceFrame("USLT", "USLT", "ULT", m_UnsynchronizedLyricsList); AddMultipleOccurrenceFrame("GEOB", "GEOB", "GEO", m_GeneralEncapsulatedObjectList); diff --git a/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameProperties.cs b/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameProperties.cs index 60cb553..7a9768d 100644 --- a/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameProperties.cs +++ b/IdSharp.Tagging/ID3v2/Classes/FrameContainer.FrameProperties.cs @@ -526,6 +526,16 @@ public BindingList UserDefinedText get { return m_UserDefinedTextList; } } + /// + /// Gets the list of ReplayGain frames (stored in TXXX/TXX). + /// For example: REPLAYGAIN_TRACK_GAIN, REPLAYGAIN_TRACK_PEAK, REPLAYGAIN_ALBUM_GAIN, REPLAYGAIN_ALBUM_PEAK. + /// + /// The BindingList of ReplayGain frames. For example: REPLAYGAIN_TRACK_GAIN, REPLAYGAIN_TRACK_PEAK, REPLAYGAIN_ALBUM_GAIN, REPLAYGAIN_ALBUM_PEAK. + public BindingList ReplayGainList + { + get { return m_ReplayGainList; } + } + /// /// Gets the list of commercial info URLs. WCOM/WCM. /// diff --git a/IdSharp.Tagging/ID3v2/Classes/FrameContainer.cs b/IdSharp.Tagging/ID3v2/Classes/FrameContainer.cs index 4929b8c..38d90f7 100644 --- a/IdSharp.Tagging/ID3v2/Classes/FrameContainer.cs +++ b/IdSharp.Tagging/ID3v2/Classes/FrameContainer.cs @@ -176,6 +176,16 @@ internal void Read(Stream stream, ID3v2TagVersion tagVersion, TagReadingInfo tag } } + // Process ReplayGain frames + foreach (var frame in new List(m_UserDefinedTextList)) + { + if (frame.Description != null && frame.Description.ToUpper().StartsWith("REPLAYGAIN_")) + { + m_UserDefinedTextList.Remove(frame); + m_ReplayGainList.Add(frame); + } + } + // Process genre // TODO: may need cleanup if (!string.IsNullOrEmpty(m_Genre.Value)) { @@ -232,6 +242,13 @@ internal List