Skip to content

Commit 6a74634

Browse files
authored
Added support for chapters by implementing the frames CHAP and CTOC (#228)
* Added support for chapters by implementing the frames CHAP and CTOC from the spec addendum at https://id3.org/id3v2-chapters-1.0. * Added the ICloneable overrides for CHAP and CTOC.
1 parent 22c635c commit 6a74634

File tree

5 files changed

+592
-4
lines changed

5 files changed

+592
-4
lines changed

src/TaglibSharp/ByteVector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,7 @@ public string ToString (StringType type)
15321532
/// </returns>
15331533
public override string ToString ()
15341534
{
1535-
return ToString (StringType.UTF8);
1535+
return ToString (StringType.UTF8, 0, Count);
15361536
}
15371537

15381538
/// <summary>

src/TaglibSharp/Id3v2/FrameFactory.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public static Frame CreateFrame (ByteVector data, File file, ref int offset, byt
139139
position = offset;
140140
}
141141

142-
// If the next data is position is 0, assume
142+
// If the next data in position is 0, assume
143143
// that we've hit the padding portion of the
144144
// frame data.
145145

@@ -162,8 +162,8 @@ public static Frame CreateFrame (ByteVector data, File file, ref int offset, byt
162162
}
163163

164164
if (alreadyUnsynched) {
165-
// Mark the frame as not Unsynchronozed because the entire
166-
// tag has already been Unsynchronized
165+
// Mark the frame as not unsynchronized because the entire
166+
// tag has already been unsynchronized
167167
header.Flags &= ~FrameFlags.Unsynchronisation;
168168
}
169169

@@ -270,6 +270,10 @@ public static Frame CreateFrame (ByteVector data, File file, ref int offset, byt
270270
if (header.FrameId == FrameType.PRIV)
271271
return new PrivateFrame (data, position, header, version);
272272

273+
// User Url Link (frames 4.3.2)
274+
if (header.FrameId == FrameType.WXXX)
275+
return new UserUrlLinkFrame (data, position, header, version);
276+
273277
// Url Link (frames 4.3.1)
274278
if (header.FrameId[0] == (byte)'W')
275279
return new UrlLinkFrame (data, position, header, version);
@@ -278,6 +282,14 @@ public static Frame CreateFrame (ByteVector data, File file, ref int offset, byt
278282
if (header.FrameId == FrameType.ETCO)
279283
return new EventTimeCodesFrame (data, position, header, version);
280284

285+
// Chapter (ID3v2 Chapter Frame Addendum)
286+
if (header.FrameId == FrameType.CHAP)
287+
return new ChapterFrame (data, position, header, version);
288+
289+
// Table of Contents (ID3v2 Chapter Frame Addendum)
290+
if (header.FrameId == FrameType.CTOC)
291+
return new TableOfContentsFrame (data, position, header, version);
292+
281293
return new UnknownFrame (data, position, header, version);
282294
}
283295

src/TaglibSharp/Id3v2/FrameTypes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ static class FrameType
4040
{
4141
public static readonly ReadOnlyByteVector APIC = "APIC";
4242
public static readonly ReadOnlyByteVector COMM = "COMM";
43+
public static readonly ReadOnlyByteVector CHAP = "CHAP"; // Chapter Frame
44+
public static readonly ReadOnlyByteVector CTOC = "CTOC"; // Table of Contents Frame
4345
public static readonly ReadOnlyByteVector EQUA = "EQUA";
4446
public static readonly ReadOnlyByteVector GEOB = "GEOB";
4547
public static readonly ReadOnlyByteVector IPLS = "IPLS";
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)