// Event called when the Id3v2 tag has read a frame. // This event can be used to modify a read frame, as we do here, to further parse known frames. private static void Id3v2FrameParsed(object sender, Id3v2FrameParsedEventArgs e) { // See if the frame is a comment frame. if (!(e.Frame is Id3v2CommentFrame)) return; // Safe-cast the frame to a comment frame. Id3v2CommentFrame frame = e.Frame as Id3v2CommentFrame; // See if the frame is an iTunes normalization frame. if (!String.Equals(frame.ShortContentDescription, "iTunNORM", StringComparison.OrdinalIgnoreCase)) return; byte[] frameData = e.Frame.ToByteArray(); // Parse the frame as an iTunes normalization frame. Id3v2iTunesNormalizationFrame normalizationFrame = Id3v2Frame.ReadFromStream<Id3v2iTunesNormalizationFrame>( e.Frame.Version, new MemoryStream(frameData), frameData.Length); // Set the new normalization frame as the new frame; this will replace the comment frame. e.Frame = normalizationFrame; }
/// <summary> /// Raises the <see cref="FrameParsed"/> event. /// </summary> /// <param name="e">The <see cref="AudioVideoLib.Tags.Id3v2FrameParsedEventArgs"/> instance containing the event data.</param> private void OnFrameParsed(Id3v2FrameParsedEventArgs e) { EventHandler<Id3v2FrameParsedEventArgs> eventHandlers = FrameParsed; if (eventHandlers != null) eventHandlers(this, e); }
////------------------------------------------------------------------------------------------------------------------------------ /// <inheritdoc/> public IAudioTagOffset ReadFromStream(Stream stream, TagOrigin tagOrigin) { if (stream == null) throw new ArgumentNullException("stream"); if (!stream.CanRead) throw new InvalidOperationException("stream can not be read"); if (!stream.CanSeek) throw new InvalidOperationException("stream can not be seeked"); StreamBuffer sb = stream as StreamBuffer ?? new StreamBuffer(stream); // Try to read the header. Id3v2Header headerOrFooter = ReadHeader(sb, tagOrigin); if (headerOrFooter == null) return null; // Don't throw an exception; just return here. The read header could be a false match. if (headerOrFooter.Size > Id3v2Tag.MaxAllowedSize) return null; // Copy header values. Id3v2Tag tag = new Id3v2Tag(headerOrFooter.Version, headerOrFooter.Flags); bool isHeader = String.Equals(Id3v2Tag.HeaderIdentifier, headerOrFooter.Identifier, StringComparison.OrdinalIgnoreCase); // startOffset: offset the header starts // endOffset: offset the footer ends if available, or when the tag ends long startOffset, endOffset; Id3v2Header header, footer = null; if (isHeader) { header = headerOrFooter; startOffset = header.Position; endOffset = Math.Min(startOffset + Id3v2Tag.HeaderSize + header.Size + (tag.UseFooter ? Id3v2Tag.FooterSize : 0), sb.Length); if (endOffset > sb.Length) { #if DEBUG throw new EndOfStreamException("Tag at start could not be read: stream is truncated."); #else return null; #endif } } else { // We've read the footer. footer = headerOrFooter; endOffset = footer.Position + Id3v2Tag.FooterSize; startOffset = Math.Max(endOffset - Id3v2Tag.FooterSize - footer.Size - Id3v2Tag.HeaderSize, 0); // Seek to the start of the tag. sb.Seek(startOffset, SeekOrigin.Begin); // Read the header; it's location could be off. header = ReadHeader(sb, TagOrigin.Start); if (header == null) { // Seek back a bit more and try again. startOffset = Math.Max(startOffset - Id3v2Tag.HeaderSize, 0); sb.Seek(startOffset, SeekOrigin.Begin); header = ReadHeader(sb, TagOrigin.End); // If we still didn't find a header, seek to the start offset; the header could be missing. if (header == null) sb.Seek(startOffset, SeekOrigin.Begin); } // If we've found a header. if (header != null) { startOffset = header.Position; // Calculate the tag size (i.e. all data, excluding the header and footer) headerOrFooter.Size = (int)Math.Max(endOffset - (startOffset + Id3v2Tag.HeaderSize + Id3v2Tag.FooterSize), 0); } else { // No header found. tag.UseHeader = false; } // Return if this is the case. if (headerOrFooter.Size > Id3v2Tag.MaxAllowedSize) { #if DEBUG throw new InvalidDataException(String.Format("Size ({0}) is larger than the max allowed size ({1})", footer.Size, Id3v2Tag.MaxAllowedSize)); #else return null; #endif } // A footer is read at this point. tag.UseFooter = true; } // At this point, the stream position is located right at the start of the data after the header. // The size is the sum of the byte length of the extended header, the padding and the frames after unsynchronization. // This does not include the footer size nor the header size. int totalSizeItems = Math.Min(headerOrFooter.Size, (int)(sb.Length - sb.Position)); // If the tag has been unsynchronized, synchronize it again. // For version Id3v2.4.0 and later we don't do the unsynch here: // unsynchronization [S:6.1] is done on frame level, instead of on tag level, making it easier to skip frames, increasing the stream ability of the tag. // The unsynchronization flag in the header [S:3.1] indicates if all frames has been unsynchronized, // while the new unsynchronization flag in the frame header [S:4.1.2] indicates unsynchronization. if (tag.UseUnsynchronization && (tag.Version < Id3v2Version.Id3v240)) { int unsynchronizedDataSize = totalSizeItems; byte[] unsynchronizedData = new byte[unsynchronizedDataSize]; unsynchronizedDataSize = sb.Read(unsynchronizedData, 0, unsynchronizedDataSize); byte[] data = Id3v2Tag.GetSynchronizedData(unsynchronizedData, 0, unsynchronizedDataSize); sb = new StreamBuffer(data); // Update the total size of the items with the length of the synchronized data totalSizeItems = data.Length; } // Calculate the padding size. int paddingSize = 0; // Check for an extended header. if (tag.UseExtendedHeader) { int crc; tag.ExtendedHeader = ReadExtendedHeader(sb, tag, out crc); if (tag.ExtendedHeader != null) { if (tag.UseFooter && (tag.ExtendedHeader.PaddingSize != 0)) { // Id3v2 tag can not have padding when an Id3v2 footer has been added. tag.ExtendedHeader.PaddingSize = 0; } paddingSize += tag.ExtendedHeader.PaddingSize; // Calculate CRC if needed. if (tag.ExtendedHeader.CrcDataPresent) { long currentPosition = sb.Position; // Id3v2.3.0: // The CRC should be calculated before unsynchronisation on the data between the extended header and the padding, i.e. the frames and only the frames. // Id3v2.4.0: // The CRC is calculated on all the data between the header and footer as indicated by the header's tag length field, minus the extended header. // Note that this includes the padding (if there is any), but excludes the footer. int dataLength = ((tag.Version >= Id3v2Version.Id3v240) ? totalSizeItems : totalSizeItems - (tag.PaddingSize + tag.ExtendedHeader.PaddingSize) - 4) - tag.ExtendedHeader.GetHeaderSize(tag.Version); byte[] crcData = sb.ToByteArray().Skip((int)currentPosition).Take(dataLength).ToArray(); int calculatedCrc = Cryptography.Crc32.Calculate(crcData); if (calculatedCrc != crc) throw new InvalidDataException(String.Format("CRC {0:X} in tag does not match calculated CRC {1:X}", crc, calculatedCrc)); sb.Position = currentPosition; } } } // Bytes we won't read or have already read, excluding the header/footer. totalSizeItems -= (tag.ExtendedHeader != null ? tag.ExtendedHeader.GetHeaderSize(tag.Version) + paddingSize : 0); // Now let's get to the good part, and start parsing the frames! List<Id3v2Frame> frames = new List<Id3v2Frame>(); long bytesRead = 0; while (bytesRead < totalSizeItems) { long startPosition = sb.Position; // See if the next byte is a padding byte. int identifierByte = sb.ReadByte(false); if (identifierByte == 0x00) { // Rest is padding. // We add up to the padding rather than assigning it, because the ExtendedHeader might include padding as well. tag.PaddingSize += (int)(totalSizeItems - bytesRead); bytesRead += tag.PaddingSize; break; } Id3v2Frame frame = Id3v2Frame.ReadFromStream(tag.Version, sb, totalSizeItems - bytesRead); if (frame != null) { Id3v2FrameParseEventArgs frameParseEventArgs = new Id3v2FrameParseEventArgs(frame); OnFrameParse(frameParseEventArgs); if (!frameParseEventArgs.Cancel) { // If the event has 'replaced' the frame, assign the 'new' frame here and continue. if (frameParseEventArgs.Frame != null) frame = frameParseEventArgs.Frame; // Assign the cryptor instance of the tag to the frame, and decrypt if necessary. bool isEncrypted = frame.UseEncryption; if (isEncrypted) isEncrypted = frame.Decrypt(); // Do the same with decompressing, if necessary. if (!isEncrypted && frame.UseCompression) frame.Decompress(); // Call after read event. Id3v2FrameParsedEventArgs frameParsedEventArgs = new Id3v2FrameParsedEventArgs(frame); OnFrameParsed(frameParsedEventArgs); // Add the 'final' frame from the event. frames.Add(frameParsedEventArgs.Frame); } } else { // Skip next byte. sb.Position = (startPosition + 1); } //Reading is always done forward; not backwards. bytesRead += (sb.Position - startPosition); } paddingSize += tag.PaddingSize; #if DEBUG if (frames.Count() > Id3v2Tag.MaxAllowedFrames) { throw new InvalidDataException( String.Format("Tag has more frames ('{0}') than the allowed max frames count ('{1}').", frames.Count(), Id3v2Tag.MaxAllowedFrames)); } if (bytesRead != totalSizeItems) { throw new InvalidDataException( String.Format("Amount of bytes read ({0}) does not match expected size ({1}).", bytesRead, totalSizeItems)); } // Id3v2.4.0 and later do not have a field for padding size. if (!isHeader && (paddingSize > 0)) throw new InvalidDataException("Id3v2 tag can not have padding when footer is set."); #endif // If the tag is at the start of the stream, is the tag allowed to have a footer? if (isHeader && tag.UseFooter) { sb.Position += Id3v2Tag.FooterSize; footer = ReadHeader(sb, TagOrigin.End); } // Validate the padding. if (paddingSize > 0) { byte[] padding = new byte[paddingSize]; sb.Read(padding, paddingSize); #if DEBUG if (!padding.Any(b => b == 0x00)) throw new InvalidDataException("Padding contains one or more invalid padding bytes."); #endif } ValidateHeader(tag, header, footer); // Sort frames and add them to the tag. AddRequiredFrames(tag.Version, frames); tag.SetFrames(frames); return new AudioTagOffset(tagOrigin, startOffset, endOffset, tag); }