|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | + |
| 4 | +namespace TagLib.Id3v2 |
| 5 | +{ |
| 6 | + /// <summary> |
| 7 | + /// This class extends <see cref="Frame" /> to provide support for |
| 8 | + /// Chapter Frames, i.e. "<c>CHAP</c>", (ID3v2 Chapter Frame Addendum 1.0, |
| 9 | + /// https://id3.org/id3v2-chapters-1.0). |
| 10 | + /// </summary> |
| 11 | + /// <remarks> |
| 12 | + /// The Chapter Frame is special in that it can hold an arbitrary amount |
| 13 | + /// of sub-frames, which are made available here in the SubFrames list. |
| 14 | + /// |
| 15 | + /// Each Chapter Frame must have an identifying string that is unique across |
| 16 | + /// all <see cref="ChapterFrame"/>s and <see cref="TableOfContentsFrame"/>s |
| 17 | + /// in the tag. This is the property <see cref="Id"/>. It is not intended |
| 18 | + /// for humans consumption and players will not display it. A chapter can |
| 19 | + /// be titled by adding a "<c>TIT2</c>" <see cref="TextInformationFrame"/>. |
| 20 | + /// |
| 21 | + /// There are two ways the Chapter Frame can state a chapter’s beginning |
| 22 | + /// and end: by milliseconds or by byte offset, accessible here as |
| 23 | + /// StartMilliseconds/EndMilliseconds and StartByteOffset/EndByteOffset |
| 24 | + /// respectively. The byte offsets are the zero-based byte positions of |
| 25 | + /// the first audio frame in the chapter or the first audio frame folliwing |
| 26 | + /// the chapter, counted from the beginning of the file. The byte offsets |
| 27 | + /// are to be ignored according to the spec if they are FF FF FF FF. This |
| 28 | + /// class does not synchronize the two ways in any way, so make sure to set |
| 29 | + /// both appropriately. The byte offsets are however initialized to be |
| 30 | + /// ignored, so with blank frames, you can focus on the milliseconds. |
| 31 | + /// |
| 32 | + /// According to the spec, chapters may overlap and have gaps. |
| 33 | + /// </remarks> |
| 34 | + public class ChapterFrame : Frame |
| 35 | + { |
| 36 | + #region Constructors |
| 37 | + |
| 38 | + /// <summary> |
| 39 | + /// Constructs and initializes a new empty instance of <see |
| 40 | + /// cref="ChapterFrame" />. |
| 41 | + /// </summary> |
| 42 | + public ChapterFrame () |
| 43 | + : base(FrameType.CHAP, 4) |
| 44 | + { |
| 45 | + } |
| 46 | + |
| 47 | + /// <summary> |
| 48 | + /// Constructs and initializes a new empty instance of <see |
| 49 | + /// cref="ChapterFrame" /> with the given chapter ID. |
| 50 | + /// </summary> |
| 51 | + public ChapterFrame (string id) |
| 52 | + : this() |
| 53 | + { |
| 54 | + Id = id; |
| 55 | + } |
| 56 | + |
| 57 | + /// <summary> |
| 58 | + /// Constructs and initializes a new instance of <see cref="ChapterFrame" /> |
| 59 | + /// with the given chapter ID and adds a <see cref="TextInformationFrame"/> |
| 60 | + /// "<c>TIT2</c>" with the given title. |
| 61 | + /// </summary> |
| 62 | + public ChapterFrame (string id, string title) |
| 63 | + : this(id) |
| 64 | + { |
| 65 | + SubFrames.Add(new TextInformationFrame("TIT2") { Text = new[] { title } }); |
| 66 | + } |
| 67 | + |
| 68 | + /// <summary> |
| 69 | + /// Constructs and initializes a new instance of <see |
| 70 | + /// cref="ChapterFrame" /> by reading its raw data in a |
| 71 | + /// specified ID3v2 version. |
| 72 | + /// </summary> |
| 73 | + /// <param name="data"> |
| 74 | + /// A <see cref="ByteVector" /> object starting with the raw |
| 75 | + /// representation of the new frame. |
| 76 | + /// </param> |
| 77 | + /// <param name="version"> |
| 78 | + /// A <see cref="byte" /> indicating the ID3v2 version the |
| 79 | + /// raw frame is encoded in. |
| 80 | + /// </param> |
| 81 | + public ChapterFrame (ByteVector data, byte version) |
| 82 | + : base (data, version) |
| 83 | + { |
| 84 | + SetData (data, 0, version, true); |
| 85 | + } |
| 86 | + |
| 87 | + /// <summary> |
| 88 | + /// Constructs and initializes a new instance of <see |
| 89 | + /// cref="ChapterFrame" /> by reading its raw data in a |
| 90 | + /// specified ID3v2 version. |
| 91 | + /// </summary> |
| 92 | + /// <param name="data"> |
| 93 | + /// A <see cref="ByteVector" /> object containing the raw |
| 94 | + /// representation of the new frame. |
| 95 | + /// </param> |
| 96 | + /// <param name="offset"> |
| 97 | + /// A <see cref="int" /> indicating at what offset in |
| 98 | + /// <paramref name="data" /> the frame actually begins. |
| 99 | + /// </param> |
| 100 | + /// <param name="header"> |
| 101 | + /// A <see cref="FrameHeader" /> containing the header of the |
| 102 | + /// frame found at <paramref name="offset" /> in the data. |
| 103 | + /// </param> |
| 104 | + /// <param name="version"> |
| 105 | + /// A <see cref="byte" /> indicating the ID3v2 version the |
| 106 | + /// raw frame is encoded in. |
| 107 | + /// </param> |
| 108 | + protected internal ChapterFrame (ByteVector data, int offset, FrameHeader header, byte version) |
| 109 | + : base (header) |
| 110 | + { |
| 111 | + SetData (data, offset, version, false); |
| 112 | + } |
| 113 | + |
| 114 | + #endregion |
| 115 | + |
| 116 | + |
| 117 | + #region Public Properties |
| 118 | + |
| 119 | + /// <summary> |
| 120 | + /// Gets and sets the internal chapter id. This should be |
| 121 | + /// <see cref="StringType.Latin1" /> . |
| 122 | + /// </summary> |
| 123 | + public string Id { get; set; } |
| 124 | + |
| 125 | + /// <summary> |
| 126 | + /// Gets and sets the start time of the chapter in milliseconds. |
| 127 | + /// </summary> |
| 128 | + public uint StartMilliseconds { get; set; } |
| 129 | + |
| 130 | + /// <summary> |
| 131 | + /// Gets and sets the end time of the chapter in milliseconds. |
| 132 | + /// </summary> |
| 133 | + public uint EndMilliseconds { get; set; } |
| 134 | + |
| 135 | + /// <summary> |
| 136 | + /// Gets and sets the chapter’s first audio frame’s byte position |
| 137 | + /// from the beginning of the file. |
| 138 | + /// The spec makes this ignorable if it is FF FF FF FF, which is |
| 139 | + /// the initial value. |
| 140 | + /// </summary> |
| 141 | + public uint StartByteOffset { get; set; } = 0xFFFFFFFF; |
| 142 | + |
| 143 | + /// <summary> |
| 144 | + /// Gets and sets the byte position of the first audio frame following |
| 145 | + /// the chapter from the beginning of the file. |
| 146 | + /// The spec makes this ignorable if it is FF FF FF FF, which is |
| 147 | + /// the initial value. |
| 148 | + /// </summary> |
| 149 | + public uint EndByteOffset { get; set; } = 0xFFFFFFFF; |
| 150 | + |
| 151 | + /// <summary> |
| 152 | + /// Gets and sets the descriptive sub-fields for this chapter. It |
| 153 | + /// is recommended by the spec to have at least a "<c>TIT2</c>" |
| 154 | + /// <see cref="TextInformationFrame"/> with the chapter title, but |
| 155 | + /// it can contain anything. Particularly, players like to display |
| 156 | + /// per-chapter "<c>APIC</c>" <see cref="AttachmentFrame"/>s and |
| 157 | + /// <see cref="UrlLinkFrame"/>s. |
| 158 | + /// </summary> |
| 159 | + /// <value> |
| 160 | + /// A List of arbitrary <see cref="Frame" />s. |
| 161 | + /// </value> |
| 162 | + public List<Frame> SubFrames { get; set; } = new List<Frame>(); |
| 163 | + |
| 164 | + #endregion |
| 165 | + |
| 166 | + |
| 167 | + #region Protected Methods |
| 168 | + |
| 169 | + /// <summary> |
| 170 | + /// Populates the values in the current instance by parsing |
| 171 | + /// its field data in a specified version. |
| 172 | + /// </summary> |
| 173 | + /// <param name="data"> |
| 174 | + /// A <see cref="ByteVector" /> object containing the |
| 175 | + /// extracted field data. |
| 176 | + /// </param> |
| 177 | + /// <param name="version"> |
| 178 | + /// A <see cref="byte" /> indicating the ID3v2 version the |
| 179 | + /// field data is encoded in. |
| 180 | + /// </param> |
| 181 | + protected override void ParseFields (ByteVector data, byte version) |
| 182 | + { |
| 183 | + // https://id3.org/id3v2-chapters-1.0 |
| 184 | + |
| 185 | + int idLength = data.IndexOf((byte)0) + 1; |
| 186 | + |
| 187 | + Id = data.ToString(StringType.Latin1, 0, idLength - 1); //Always Latin1, at least there is no mention of encoding in the spec |
| 188 | + StartMilliseconds = data.Mid(idLength, 4).ToUInt(); |
| 189 | + EndMilliseconds = data.Mid(idLength + 4, 4).ToUInt(); |
| 190 | + StartByteOffset = data.Mid(idLength + 8, 4).ToUInt(); //I don’t really know why one would use the offsets. |
| 191 | + EndByteOffset = data.Mid(idLength + 12, 4).ToUInt(); //They are to be ignored if all 4 Bytes are FF, i.e. 4,294,967,295. |
| 192 | + |
| 193 | + SubFrames = new List<Frame>(); |
| 194 | + int frame_data_position = idLength + 16; |
| 195 | + int frame_data_endposition = data.Count; |
| 196 | + while (frame_data_position < frame_data_endposition) |
| 197 | + { |
| 198 | + Frame frame; |
| 199 | + try |
| 200 | + { |
| 201 | + frame = FrameFactory.CreateFrame(data, null, ref frame_data_position, version, true /* ? */); |
| 202 | + } |
| 203 | + catch (NotImplementedException) |
| 204 | + { |
| 205 | + continue; |
| 206 | + } |
| 207 | + catch (CorruptFileException) |
| 208 | + { |
| 209 | + throw; |
| 210 | + } |
| 211 | + |
| 212 | + if (frame == null) |
| 213 | + break; |
| 214 | + |
| 215 | + // Only add frames that contain data. |
| 216 | + if (frame.Size == 0) |
| 217 | + continue; |
| 218 | + |
| 219 | + SubFrames.Add(frame); |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + /// <summary> |
| 224 | + /// Renders the values in the current instance into field |
| 225 | + /// data for a specified version. |
| 226 | + /// </summary> |
| 227 | + /// <param name="version"> |
| 228 | + /// A <see cref="byte" /> indicating the ID3v2 version the |
| 229 | + /// field data is to be encoded in. |
| 230 | + /// </param> |
| 231 | + /// <returns> |
| 232 | + /// A <see cref="ByteVector" /> object containing the |
| 233 | + /// rendered field data. |
| 234 | + /// </returns> |
| 235 | + protected override ByteVector RenderFields (byte version) |
| 236 | + { |
| 237 | + var data = ByteVector.FromString(Id, StringType.Latin1); |
| 238 | + data.Add((byte)0); //it would be neat if Add were chainable… |
| 239 | + data.Add(ByteVector.FromUInt(StartMilliseconds)); |
| 240 | + data.Add(ByteVector.FromUInt(EndMilliseconds)); |
| 241 | + data.Add(ByteVector.FromUInt(StartByteOffset)); |
| 242 | + data.Add(ByteVector.FromUInt(EndByteOffset)); |
| 243 | + |
| 244 | + foreach (var f in SubFrames) |
| 245 | + data.Add(f.Render(version)); |
| 246 | + |
| 247 | + return data; |
| 248 | + } |
| 249 | + |
| 250 | + #endregion |
| 251 | + |
| 252 | + |
| 253 | + #region ICloneable |
| 254 | + |
| 255 | + /// <summary> |
| 256 | + /// Creates a deep copy of the current instance. |
| 257 | + /// </summary> |
| 258 | + /// <returns> |
| 259 | + /// A new <see cref="Frame" /> object identical to the |
| 260 | + /// current instance. |
| 261 | + /// </returns> |
| 262 | + public override Frame Clone() |
| 263 | + { |
| 264 | + var frame = new ChapterFrame(Id); |
| 265 | + frame.StartMilliseconds = StartMilliseconds; |
| 266 | + frame.EndMilliseconds = EndMilliseconds; |
| 267 | + frame.StartByteOffset = StartByteOffset; |
| 268 | + frame.EndByteOffset = EndByteOffset; |
| 269 | + |
| 270 | + foreach(var f in SubFrames) |
| 271 | + frame.SubFrames.Add(f.Clone()); |
| 272 | + |
| 273 | + return frame; |
| 274 | + } |
| 275 | + |
| 276 | + #endregion |
| 277 | + } |
| 278 | +} |
0 commit comments