internal static ICoverArt FromBase64(ReadOnlySpan <byte> value) { // Use heap allocations for cover art > 256kB var byteCount = Base64.GetMaxDecodedFromUtf8Length(value.Length); var decodedValue = byteCount < 0x40000 ? stackalloc byte[byteCount] : new byte[byteCount]; Base64.DecodeFromUtf8(value, decodedValue, out _, out _); // If the image isn't a "Front Cover" or "Other", return null var imageType = BinaryPrimitives.ReadUInt32BigEndian(decodedValue); if (imageType != 3 && imageType != 0) { return(null); } var offset = 4; // Seek past the mime type and description offset += (int)BinaryPrimitives.ReadUInt32BigEndian(decodedValue.Slice(offset)) + 4; offset += (int)BinaryPrimitives.ReadUInt32BigEndian(decodedValue.Slice(offset)) + 4; // Seek past the width, height, color depth and type offset += 16; return(CoverArtFactory.GetOrCreate( decodedValue.Slice(offset + 4, (int)BinaryPrimitives.ReadUInt32BigEndian(decodedValue.Slice(offset))))); }
public void CreatePathInvalidThrowsException([NotNull] string fileName) { Assert.Throws <ImageInvalidException>(() => CoverArtFactory.GetOrCreate(Path.Combine( new DirectoryInfo(Directory.GetCurrentDirectory()).Parent?.Parent?.Parent?.Parent?.FullName, "TestFiles", "Invalid", fileName))); }
public void GetDataReturnsExpectedValue([NotNull] string fileName, [NotNull] string expectedHash) { Assert.Equal(expectedHash, HashUtility.CalculateHash(CoverArtFactory.GetOrCreate( Path.Combine( new DirectoryInfo(Directory.GetCurrentDirectory()).Parent?.Parent?.Parent?.Parent?.FullName, "TestFiles", "Valid", fileName)) .Data.ToArray())); }
public void HasExpectedMimeType([NotNull] string fileName, [NotNull] string mimeType) { Assert.Equal(mimeType, CoverArtFactory.GetOrCreate( Path.Combine( new DirectoryInfo(Directory.GetCurrentDirectory()).Parent?.Parent?.Parent?.Parent?.FullName, "TestFiles", "Valid", fileName)) .MimeType); }
public void HasExpectedLossless([NotNull] string fileName, bool expectedLossless) { Assert.Equal(expectedLossless, CoverArtFactory.GetOrCreate( Path.Combine( new DirectoryInfo(Directory.GetCurrentDirectory()).Parent?.Parent?.Parent?.Parent?.FullName, "TestFiles", "Valid", fileName)) .Lossless); }
public void HasExpectedColorDepth([NotNull] string fileName, int expectedColorDepth) { Assert.Equal(expectedColorDepth, CoverArtFactory.GetOrCreate( Path.Combine( new DirectoryInfo(Directory.GetCurrentDirectory()).Parent?.Parent?.Parent?.Parent?.FullName, "TestFiles", "Valid", fileName)) .ColorDepth); }
internal MetadataToIlstAtomAdapter([NotNull] AudioMetadata metadata, bool compressCoverArt) { if (!string.IsNullOrEmpty(metadata.Title)) { _atoms.Add(new TextAtom("©nam", metadata.Title)); } if (!string.IsNullOrEmpty(metadata.Artist)) { _atoms.Add(new TextAtom("©ART", metadata.Artist)); } if (!string.IsNullOrEmpty(metadata.Album)) { _atoms.Add(new TextAtom("©alb", metadata.Album)); } if (!string.IsNullOrEmpty(metadata.AlbumArtist)) { _atoms.Add(new TextAtom("aART", metadata.AlbumArtist)); } if (!string.IsNullOrEmpty(metadata.Composer)) { _atoms.Add(new TextAtom("©wrt", metadata.Composer)); } if (!string.IsNullOrEmpty(metadata.Genre)) { _atoms.Add(new TextAtom("©gen", metadata.Genre)); } if (!string.IsNullOrEmpty(metadata.Comment)) { _atoms.Add(new TextAtom("©cmt", metadata.Comment)); } if (!string.IsNullOrEmpty(metadata.Day) && !string.IsNullOrEmpty(metadata.Month) && !string.IsNullOrEmpty(metadata.Year)) { _atoms.Add(new TextAtom("©day", $"{metadata.Year}-{metadata.Month}-{metadata.Day}")); } else if (!string.IsNullOrEmpty(metadata.Year)) { _atoms.Add(new TextAtom("©day", metadata.Year)); } if (!string.IsNullOrEmpty(metadata.TrackNumber)) { _atoms.Add(new TrackNumberAtom(metadata.TrackNumber, metadata.TrackCount)); } if (metadata.CoverArt != null) { _atoms.Add(new CoverAtom(compressCoverArt ? CoverArtFactory.ConvertToLossy(metadata.CoverArt) : metadata.CoverArt)); } }
public void CreatesExpectedOutput( int index, string fileName, TestAudioMetadata metadata, string imageFileName, SettingDictionary settings, string[] validHashes) { var sourceDirectory = Path.Combine(PathUtility.GetTestFileRoot(), "Valid"); var path = Path.Combine("Output", "Save-AudioMetadata", "Valid", $"{index:000} - {fileName}"); Directory.CreateDirectory(Path.GetDirectoryName(path) !); File.Copy(Path.Combine(sourceDirectory, fileName), path, true); var audioFile = new TaggedAudioFile(path); _mapper.Map(metadata, audioFile.Metadata); if (!string.IsNullOrEmpty(imageFileName)) { audioFile.Metadata.CoverArt = CoverArtFactory.GetOrCreate(Path.Combine(sourceDirectory, imageFileName)); } using (var ps = PowerShell.Create()) { ps.Runspace = _moduleFixture.Runspace; ps.AddCommand("Save-AudioMetadata") .AddArgument(audioFile); foreach (var item in settings) { if (item.Value is bool boolValue) { if (boolValue) { ps.AddParameter(item.Key); } } else { ps.AddParameter(item.Key, item.Value); } } ps.Invoke(); } Assert.Contains(HashUtility.CalculateHash(audioFile.Path), validHashes); }
protected override unsafe void MetadataCallback(IntPtr handle, IntPtr metadataBlock, IntPtr userData) { // ReSharper disable once SwitchStatementMissingSomeCases switch ((MetadataType)Marshal.ReadInt32(metadataBlock)) { case MetadataType.VorbisComment: var vorbisComment = Marshal.PtrToStructure <VorbisCommentMetadataBlock>(metadataBlock).VorbisComment; for (var commentIndex = 0; commentIndex < vorbisComment.Count; commentIndex++) { var entry = Marshal.PtrToStructure <VorbisCommentEntry>(IntPtr.Add(vorbisComment.Comments, commentIndex * Marshal.SizeOf <VorbisCommentEntry>())); var commentBytes = new Span <byte>(entry.Entry.ToPointer(), (int)entry.Length); var delimiter = commentBytes.IndexOf((byte)0x3D); // '=' #if NETSTANDARD2_0 var keyBytes = commentBytes.Slice(0, delimiter); var valueBytes = commentBytes.Slice(delimiter + 1); AudioMetadata.Set( Encoding.ASCII.GetString( (byte *)Unsafe.AsPointer(ref MemoryMarshal.GetReference(keyBytes)), keyBytes.Length), Encoding.UTF8.GetString( (byte *)Unsafe.AsPointer(ref MemoryMarshal.GetReference(valueBytes)), valueBytes.Length)); #else AudioMetadata.Set( Encoding.ASCII.GetString(commentBytes.Slice(0, delimiter)), Encoding.UTF8.GetString(commentBytes.Slice(delimiter + 1))); #endif } break; case MetadataType.Picture: var picture = Marshal.PtrToStructure <PictureMetadataBlock>(metadataBlock).Picture; if (picture.Type == PictureType.CoverFront || picture.Type == PictureType.Other) { AudioMetadata.CoverArt = CoverArtFactory.GetOrCreate( new Span <byte>(picture.Data.ToPointer(), (int)picture.DataLength)); } break; } }
internal MetadataToTagModelAdapter([NotNull] AudioMetadata metadata, [NotNull] string encoding) { AddTextFrame("TIT2", metadata.Title, encoding); AddTextFrame("TPE1", metadata.Artist, encoding); AddTextFrame("TALB", metadata.Album, encoding); AddTextFrame("TPE2", metadata.AlbumArtist, encoding); AddTextFrame("TCOM", metadata.Composer, encoding); AddTextFrame("TCON", metadata.Genre, encoding); AddFullTextFrame("COMM", metadata.Comment, "eng", encoding); AddTextFrame("TDAT", GetDateText(metadata), encoding); AddTextFrame("TYER", metadata.Year, encoding); AddTextFrame("TRCK", GetTrackText(metadata), encoding); // ReplayGain fields are always in Latin-1, encoding as per specification AddUserDefinedFrame("REPLAYGAIN_TRACK_PEAK", metadata.TrackPeak, "Latin1", true); AddUserDefinedFrame("REPLAYGAIN_ALBUM_PEAK", metadata.AlbumPeak, "Latin1", true); if (!string.IsNullOrEmpty(metadata.TrackGain)) { AddUserDefinedFrame("REPLAYGAIN_TRACK_GAIN", $"{metadata.TrackGain} dB", "Latin1", true); } if (!string.IsNullOrEmpty(metadata.AlbumGain)) { AddUserDefinedFrame("REPLAYGAIN_ALBUM_GAIN", $"{metadata.AlbumGain} dB", "Latin1", true); } if (metadata.CoverArt == null) { return; } // Always store images in JPEG format since MP3 is also lossy var lossyCoverArt = CoverArtFactory.ConvertToLossy(metadata.CoverArt); Add(new FramePicture("APIC") { PictureType = PictureTypeCode.CoverFront, Mime = lossyCoverArt.MimeType, PictureData = lossyCoverArt.Data.ToArray() }); }
void ProcessPath(string path) { try { ICoverArt result; try { result = CoverArtFactory.GetOrCreate(path); } finally { ProcessLogMessages(); } WriteObject(result); } catch (AudioException e) { WriteError(new(e, e.GetType().Name, ErrorCategory.InvalidData, Path)); } }
internal unsafe MetadataToOpusCommentAdapter([NotNull] AudioMetadata metadata) { Handle = SafeNativeMethods.OpusEncoderCommentsCreate(); if (!string.IsNullOrEmpty(metadata.Title)) { AddTag("TITLE", metadata.Title); } if (!string.IsNullOrEmpty(metadata.Artist)) { AddTag("ARTIST", metadata.Artist); } if (!string.IsNullOrEmpty(metadata.Album)) { AddTag("ALBUM", metadata.Album); } if (!string.IsNullOrEmpty(metadata.AlbumArtist)) { AddTag("ALBUMARTIST", metadata.AlbumArtist); } if (!string.IsNullOrEmpty(metadata.Composer)) { AddTag("COMPOSER", metadata.Composer); } if (!string.IsNullOrEmpty(metadata.Genre)) { AddTag("GENRE", metadata.Genre); } if (!string.IsNullOrEmpty(metadata.Comment)) { AddTag("DESCRIPTION", metadata.Comment); } if (!string.IsNullOrEmpty(metadata.Day) && !string.IsNullOrEmpty(metadata.Month) && !string.IsNullOrEmpty(metadata.Year)) { AddTag("DATE", $"{metadata.Year}-{metadata.Month}-{metadata.Day}"); } else if (!string.IsNullOrEmpty(metadata.Year)) { AddTag("YEAR", metadata.Year); } if (!string.IsNullOrEmpty(metadata.TrackNumber)) { AddTag("TRACKNUMBER", !string.IsNullOrEmpty(metadata.TrackCount) ? $"{metadata.TrackNumber}/{metadata.TrackCount}" : metadata.TrackNumber); } if (!string.IsNullOrEmpty(metadata.TrackGain)) { AddTag("R128_TRACK_GAIN", ConvertGain(metadata.TrackGain)); } if (!string.IsNullOrEmpty(metadata.AlbumGain)) { AddTag("R128_ALBUM_GAIN", ConvertGain(metadata.AlbumGain)); } if (metadata.CoverArt == null) { return; } // Always store images in JPEG format since Vorbis is also lossy var coverArt = CoverArtFactory.ConvertToLossy(metadata.CoverArt); fixed(byte *coverArtAddress = coverArt.Data) { var error = SafeNativeMethods.OpusEncoderCommentsAddPictureFromMemory( Handle, coverArtAddress, new IntPtr(coverArt.Data.Length), -1, IntPtr.Zero); if (error != 0) { throw new AudioEncodingException($"Opus encountered error {error} writing the cover art."); } } }
internal TagModelToMetadataAdapter([NotNull] TagModel tagModel) { foreach (var frame in tagModel) { switch (frame) { case FrameText frameText: // ReSharper disable once SwitchStatementMissingSomeCases switch (frameText.FrameId) { case "TIT2": Title = frameText.Text; break; case "TPE1": Artist = frameText.Text; break; case "TALB": Album = frameText.Text; break; case "TPE2": AlbumArtist = frameText.Text; break; case "TCOM": Composer = frameText.Text; break; case "TCON": Genre = frameText.Text; break; // The TDAT frame contains the day and the month: case "TDAT": Day = frameText.Text.Substring(0, 2); Month = frameText.Text.Substring(2); break; case "TYER": Year = frameText.Text; break; // The TRCK frame contains the track number and (optionally) the track count: case "TRCK": var segments = frameText.Text.Split('/'); TrackNumber = segments[0]; if (segments.Length > 1) { TrackCount = segments[1]; } break; } break; case FrameFullText frameFullText: if (frameFullText.FrameId.Equals("COMM", StringComparison.Ordinal) && string.IsNullOrEmpty(frameFullText.Description)) { Comment = frameFullText.Text; } break; case FrameTextUserDef frameTextUserDef: // ReSharper disable once SwitchStatementMissingSomeCases switch (frameTextUserDef.Description) { case "REPLAYGAIN_TRACK_PEAK": TrackPeak = frameTextUserDef.Text; break; case "REPLAYGAIN_ALBUM_PEAK": AlbumPeak = frameTextUserDef.Text; break; case "REPLAYGAIN_TRACK_GAIN": #if NETSTANDARD2_0 TrackGain = frameTextUserDef.Text.Replace(" dB", string.Empty); #else TrackGain = frameTextUserDef.Text.Replace(" dB", string.Empty, StringComparison.OrdinalIgnoreCase); #endif break; case "REPLAYGAIN_ALBUM_GAIN": #if NETSTANDARD2_0 AlbumGain = frameTextUserDef.Text.Replace(" dB", string.Empty); #else AlbumGain = frameTextUserDef.Text.Replace(" dB", string.Empty, StringComparison.OrdinalIgnoreCase); #endif break; } break; case FramePicture framePicture: CoverArt = CoverArtFactory.GetOrCreate(framePicture.PictureData); break; } } }
internal MetadataToTagModelAdapter(AudioMetadata metadata, int version, string encoding) { Header.Version = (byte)version; var textType = encoding switch { "UTF16" => TextType.Utf16, "UTF8" => TextType.Utf8, _ => TextType.Ascii }; AddTextFrame("TIT2", metadata.Title, textType); AddTextFrame("TPE1", metadata.Artist, textType); AddTextFrame("TALB", metadata.Album, textType); AddTextFrame("TPE2", metadata.AlbumArtist, textType); AddTextFrame("TCOM", metadata.Composer, textType); AddTextFrame("TCON", metadata.Genre, textType); AddTextFrame("COMM", metadata.Comment, textType); if (version == 3) { AddTextFrame("TDAT", GetDateText(metadata), textType); AddTextFrame("TYER", metadata.Year, textType); } else { AddTextFrame("TDRC", GetTimeStamp(metadata), textType); } AddTextFrame("TRCK", GetTrackText(metadata), textType); AddReplayGainFrame(metadata.TrackPeak, "REPLAYGAIN_TRACK_PEAK"); AddReplayGainFrame(metadata.AlbumPeak, "REPLAYGAIN_ALBUM_PEAK"); AddReplayGainFormattedFrame(metadata.TrackGain, "REPLAYGAIN_TRACK_GAIN"); AddReplayGainFormattedFrame(metadata.AlbumGain, "REPLAYGAIN_ALBUM_GAIN"); if (metadata.CoverArt == null) { return; } // Always store images in JPEG format, since MP3 is also lossy var lossyCoverArt = CoverArtFactory.ConvertToLossy(metadata.CoverArt); Frames.Add(new FramePicture { PictureType = PictureType.CoverFront, Mime = lossyCoverArt.MimeType, PictureData = lossyCoverArt.Data.ToArray() }); } void AddReplayGainFormattedFrame(string value, string description) { if (!string.IsNullOrEmpty(value)) { AddReplayGainFrame($"{value} dB", description); } } void AddReplayGainFrame(string value, string description) => AddTextFrame("TXXX", value, TextType.Ascii, description, true); void AddTextFrame(string frameId, string value, TextType textType = TextType.Ascii, string?description = null, bool fileAlter = false) { if (string.IsNullOrEmpty(value)) { return; } var frame = (FrameText)FrameFactory.Build(frameId); frame.Text = value; frame.TextType = textType; frame.FileAlter = fileAlter; if (description != null) { if (frame is IFrameDescription frameDescription) { frameDescription.Description = description; } } Frames.Add(frame); }
public void CreateDataNullThrowsException() { Assert.Throws <ArgumentNullException>(() => CoverArtFactory.GetOrCreate((byte[])null)); }
public void CreatePathNullThrowsException() { Assert.Throws <ArgumentNullException>(() => CoverArtFactory.GetOrCreate((string)null)); }
internal MetadataToVorbisCommentAdapter([NotNull] AudioMetadata metadata) { SafeNativeMethods.VorbisCommentInit(out _comment); if (!string.IsNullOrEmpty(metadata.Title)) { AddTag("TITLE", metadata.Title); } if (!string.IsNullOrEmpty(metadata.Artist)) { AddTag("ARTIST", metadata.Artist); } if (!string.IsNullOrEmpty(metadata.Album)) { AddTag("ALBUM", metadata.Album); } if (!string.IsNullOrEmpty(metadata.AlbumArtist)) { AddTag("ALBUMARTIST", metadata.AlbumArtist); } if (!string.IsNullOrEmpty(metadata.Composer)) { AddTag("COMPOSER", metadata.Composer); } if (!string.IsNullOrEmpty(metadata.Genre)) { AddTag("GENRE", metadata.Genre); } if (!string.IsNullOrEmpty(metadata.Comment)) { AddTag("DESCRIPTION", metadata.Comment); } if (!string.IsNullOrEmpty(metadata.Day) && !string.IsNullOrEmpty(metadata.Month) && !string.IsNullOrEmpty(metadata.Year)) { AddTag("DATE", $"{metadata.Year}-{metadata.Month}-{metadata.Day}"); } else if (!string.IsNullOrEmpty(metadata.Year)) { AddTag("YEAR", metadata.Year); } if (!string.IsNullOrEmpty(metadata.TrackNumber)) { AddTag("TRACKNUMBER", !string.IsNullOrEmpty(metadata.TrackCount) ? $"{metadata.TrackNumber}/{metadata.TrackCount}" : metadata.TrackNumber); } if (!string.IsNullOrEmpty(metadata.TrackPeak)) { AddTag("REPLAYGAIN_TRACK_PEAK", metadata.TrackPeak); } if (!string.IsNullOrEmpty(metadata.AlbumPeak)) { AddTag("REPLAYGAIN_ALBUM_PEAK", metadata.AlbumPeak); } if (!string.IsNullOrEmpty(metadata.TrackGain)) { AddTag("REPLAYGAIN_TRACK_GAIN", $"{metadata.TrackGain} dB"); } if (!string.IsNullOrEmpty(metadata.AlbumGain)) { AddTag("REPLAYGAIN_ALBUM_GAIN", $"{metadata.AlbumGain} dB"); } // Always store images in JPEG format since Vorbis is also lossy if (metadata.CoverArt != null) { AddTag("METADATA_BLOCK_PICTURE", CoverArtAdapter.ToBase64( CoverArtFactory.ConvertToLossy(metadata.CoverArt))); } }
public void CreatePathNotFoundThrowsException() { Assert.Throws <FileNotFoundException>(() => CoverArtFactory.GetOrCreate("Foo")); }
public CoverAtom(ReadOnlySpan <byte> data) { // There could be more than one data atom. Ignore all but the first. Value = CoverArtFactory.GetOrCreate(data.Slice(24, (int)BinaryPrimitives.ReadUInt32BigEndian(data.Slice(8, 4)) - 16)); }