bool WavPackMetadata::ReadMetadata(CFErrorRef *error) { // Start from scratch CFDictionaryRemoveAllValues(mMetadata); CFDictionaryRemoveAllValues(mChangedMetadata); UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, FALSE, buf, PATH_MAX)) return false; auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true); TagLib::WavPack::File file(stream); if(!file.isValid()) { if(nullptr != error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid WavPack file."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a WavPack file"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("WavPack")); if(file.audioProperties()) { auto properties = file.audioProperties(); AddAudioPropertiesToDictionary(mMetadata, properties); if(properties->bitsPerSample()) AddIntToDictionary(mMetadata, kPropertiesBitsPerChannelKey, properties->bitsPerSample()); if(properties->sampleFrames()) AddIntToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->sampleFrames()); } if(file.ID3v1Tag()) AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag()); if(file.APETag()) { std::vector<AttachedPicture *> pictures; AddAPETagToDictionary(mMetadata, pictures, file.APETag()); for(auto picture : pictures) AddSavedPicture(picture); } return true; }
bool SFB::Audio::TrueAudioMetadata::_ReadMetadata(CFErrorRef *error) { UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, FALSE, buf, PATH_MAX)) return false; std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true)); if(!stream->isOpen()) { if(error) { SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), ""); SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), ""); SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), ""); *error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion); } return false; } TagLib::TrueAudio::File file(stream.get()); if(!file.isValid()) { if(nullptr != error) { SFB::CFString description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), ""); SFB::CFString failureReason = CFCopyLocalizedString(CFSTR("Not a True Audio file"), ""); SFB::CFString recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion); } return false; } CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("True Audio")); if(file.audioProperties()) { auto properties = file.audioProperties(); AddAudioPropertiesToDictionary(mMetadata, properties); if(properties->bitsPerSample()) AddIntToDictionary(mMetadata, kBitsPerChannelKey, properties->bitsPerSample()); if(properties->sampleFrames()) AddIntToDictionary(mMetadata, kTotalFramesKey, (int)properties->sampleFrames()); } // Add all tags that are present if(file.ID3v1Tag()) AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag()); if(file.ID3v2Tag()) AddID3v2TagToDictionary(mMetadata, mPictures, file.ID3v2Tag()); return true; }
bool SFB::Audio::AddAudioPropertiesToDictionary(CFMutableDictionaryRef dictionary, const TagLib::AudioProperties *properties) { if(nullptr == dictionary || nullptr == properties) return false; if(properties->length()) AddIntToDictionary(dictionary, Metadata::kDurationKey, properties->length()); if(properties->channels()) AddIntToDictionary(dictionary, Metadata::kChannelsPerFrameKey, properties->channels()); if(properties->sampleRate()) AddIntToDictionary(dictionary, Metadata::kSampleRateKey, properties->sampleRate()); if(properties->bitrate()) AddIntToDictionary(dictionary, Metadata::kBitrateKey, properties->bitrate()); return true; }
SFB::Audio::AttachedPicture::AttachedPicture(CFDataRef data, AttachedPicture::Type type, CFStringRef description) : mMetadata(0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks), mChangedMetadata(0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks), mState(ChangeState::Saved) { if(data) CFDictionarySetValue(mMetadata, kDataKey, data); AddIntToDictionary(mMetadata, kTypeKey, (int)type); if(description) CFDictionarySetValue(mMetadata, kDescriptionKey, description); }
SFB::Audio::AttachedPicture::AttachedPicture(CFDataRef data, AttachedPicture::Type type, CFStringRef description) : mState(ChangeState::Saved) { mMetadata = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); mChangedMetadata = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if(data) CFDictionarySetValue(mMetadata, kDataKey, data); AddIntToDictionary(mMetadata, kTypeKey, (int)type); if(description) CFDictionarySetValue(mMetadata, kDescriptionKey, description); }
bool SFB::Audio::AddTagToDictionary(CFMutableDictionaryRef dictionary, const TagLib::Tag *tag) { if(nullptr == dictionary || nullptr == tag) return false; TagLib::AddStringToCFDictionary(dictionary, Metadata::kTitleKey, tag->title()); TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumTitleKey, tag->album()); TagLib::AddStringToCFDictionary(dictionary, Metadata::kArtistKey, tag->artist()); TagLib::AddStringToCFDictionary(dictionary, Metadata::kGenreKey, tag->genre()); if(tag->year()) AddIntToDictionaryAsString(dictionary, Metadata::kReleaseDateKey, (int)tag->year()); if(tag->track()) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, (int)tag->track()); TagLib::AddStringToCFDictionary(dictionary, Metadata::kCommentKey, tag->comment()); return true; }
bool TrueAudioMetadata::ReadMetadata(CFErrorRef *error) { // Start from scratch CFDictionaryRemoveAllValues(mMetadata); CFDictionaryRemoveAllValues(mChangedMetadata); UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, FALSE, buf, PATH_MAX)) return false; // TODO: Use unique_ptr once the switch to C++11 STL is made std::auto_ptr<TagLib::FileStream> stream(new TagLib::FileStream(reinterpret_cast<const char *>(buf), true)); if(!stream->isOpen()) { if(error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Input/output error"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } TagLib::TrueAudio::File file(stream.get()); if(!file.isValid()) { if(nullptr != error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid True Audio file."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a True Audio file"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("True Audio")); if(file.audioProperties()) { auto properties = file.audioProperties(); AddAudioPropertiesToDictionary(mMetadata, properties); if(properties->bitsPerSample()) AddIntToDictionary(mMetadata, kPropertiesBitsPerChannelKey, properties->bitsPerSample()); if(properties->sampleFrames()) AddIntToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->sampleFrames()); } // Add all tags that are present if(file.ID3v1Tag()) AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag()); if(file.ID3v2Tag()) { std::vector<AttachedPicture *> pictures; AddID3v2TagToDictionary(mMetadata, pictures, file.ID3v2Tag()); for(auto picture : pictures) AddSavedPicture(picture); } return true; }
bool SFB::Audio::MP3Metadata::_ReadMetadata(CFErrorRef *error) { UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX)) return false; std::unique_ptr<TagLib::FileStream> stream(new TagLib::FileStream((const char *)buf, true)); if(!stream->isOpen()) { if(error) { SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” could not be opened for reading."), "")); SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Input/output error"), "")); SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file may have been renamed, moved, deleted, or you may not have appropriate permissions."), "")); *error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion); } return false; } TagLib::MPEG::File file(stream.get(), TagLib::ID3v2::FrameFactory::instance()); if(!file.isValid()) { if(nullptr != error) { SFB::CFString description(CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), "")); SFB::CFString failureReason(CFCopyLocalizedString(CFSTR("Not an MPEG file"), "")); SFB::CFString recoverySuggestion(CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), "")); *error = CreateErrorForURL(Metadata::ErrorDomain, Metadata::InputOutputError, description, mURL, failureReason, recoverySuggestion); } return false; } CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MP3")); if(file.audioProperties()) { auto properties = file.audioProperties(); AddAudioPropertiesToDictionary(mMetadata, properties); // TODO: Is this too much information? #if 0 switch(properties->version()) { case TagLib::MPEG::Header::Version1: switch(properties->layer()) { case 1: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-1 Layer I")); break; case 2: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-1 Layer II")); break; case 3: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-1 Layer III")); break; } break; case TagLib::MPEG::Header::Version2: switch(properties->layer()) { case 1: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2 Layer I")); break; case 2: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2 Layer II")); break; case 3: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2 Layer III")); break; } break; case TagLib::MPEG::Header::Version2_5: switch(properties->layer()) { case 1: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2.5 Layer I")); break; case 2: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2.5 Layer II")); break; case 3: CFDictionarySetValue(mMetadata, kFormatNameKey, CFSTR("MPEG-2.5 Layer III")); break; } break; } #endif if(properties->xingHeader() && properties->xingHeader()->totalFrames()) AddIntToDictionary(mMetadata, kTotalFramesKey, (int)properties->xingHeader()->totalFrames()); } if(file.APETag()) AddAPETagToDictionary(mMetadata, mPictures, file.APETag()); if(file.ID3v1Tag()) AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag()); if(file.ID3v2Tag()) AddID3v2TagToDictionary(mMetadata, mPictures, file.ID3v2Tag()); return true; }
bool FLACMetadata::ReadMetadata(CFErrorRef *error) { // Start from scratch CFDictionaryRemoveAllValues(mMetadata); CFDictionaryRemoveAllValues(mChangedMetadata); UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX)) return false; auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true); TagLib::FLAC::File file(stream, TagLib::ID3v2::FrameFactory::instance()); if(!file.isValid()) { if(nullptr != error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid FLAC file."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a FLAC file"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("FLAC")); if(file.audioProperties()) { auto properties = file.audioProperties(); AddAudioPropertiesToDictionary(mMetadata, properties); if(properties->sampleWidth()) AddIntToDictionary(mMetadata, kPropertiesBitsPerChannelKey, properties->sampleWidth()); if(properties->sampleFrames()) AddLongLongToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->sampleFrames()); } // Add all tags that are present if(file.ID3v1Tag()) AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag()); if(file.ID3v2Tag()) { std::vector<AttachedPicture *> pictures; AddID3v2TagToDictionary(mMetadata, pictures, file.ID3v2Tag()); for(auto picture : pictures) AddSavedPicture(picture); } if(file.xiphComment()) { std::vector<AttachedPicture *> pictures; AddXiphCommentToDictionary(mMetadata, pictures, file.xiphComment()); for(auto picture : pictures) AddSavedPicture(picture); } // Add album art for(auto iter : file.pictureList()) { CFDataRef data = CFDataCreate(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(iter->data().data()), iter->data().size()); CFStringRef description = nullptr; if(!iter->description().isNull()) description = CFStringCreateWithCString(kCFAllocatorDefault, iter->description().toCString(true), kCFStringEncodingUTF8); AttachedPicture *picture = new AttachedPicture(data, static_cast<AttachedPicture::Type>(iter->type()), description); AddSavedPicture(picture); if(data) CFRelease(data), data = nullptr; if(description) CFRelease(description), description = nullptr; } return true; }
bool SFB::Audio::AddID3v2TagToDictionary(CFMutableDictionaryRef dictionary, std::vector<std::shared_ptr<AttachedPicture>>& attachedPictures, const TagLib::ID3v2::Tag *tag) { if(nullptr == dictionary || nullptr == tag) return false; // Add the basic tags not specific to ID3v2 AddTagToDictionary(dictionary, tag); // Release date auto frameList = tag->frameListMap()["TDRC"]; if(!frameList.isEmpty()) { /* The timestamp fields are based on a subset of ISO 8601. When being as precise as possible the format of a time string is yyyy-MM-ddTHH:mm:ss (year, "-", month, "-", day, "T", hour (out of 24), ":", minutes, ":", seconds), but the precision may be reduced by removing as many time indicators as wanted. Hence valid timestamps are yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use the slash character as described in 8601, and for multiple non- contiguous dates, use multiple strings, if allowed by the frame definition. */ TagLib::AddStringToCFDictionary(dictionary, Metadata::kReleaseDateKey, frameList.front()->toString()); } // Extract composer if present frameList = tag->frameListMap()["TCOM"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kComposerKey, frameList.front()->toString()); // Extract album artist frameList = tag->frameListMap()["TPE2"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumArtistKey, frameList.front()->toString()); // BPM frameList = tag->frameListMap()["TBPM"]; if(!frameList.isEmpty()) { bool ok = false; int BPM = frameList.front()->toString().toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kBPMKey, BPM); } // Rating TagLib::ID3v2::PopularimeterFrame *popularimeter = nullptr; frameList = tag->frameListMap()["POPM"]; if(!frameList.isEmpty() && nullptr != (popularimeter = dynamic_cast<TagLib::ID3v2::PopularimeterFrame *>(frameList.front()))) AddIntToDictionary(dictionary, Metadata::kRatingKey, popularimeter->rating()); // Extract total tracks if present frameList = tag->frameListMap()["TRCK"]; if(!frameList.isEmpty()) { // Split the tracks at '/' TagLib::String s = frameList.front()->toString(); bool ok; size_t pos = s.find("/", 0); if(TagLib::String::npos() != pos) { int trackNum = s.substr(0, pos).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, trackNum); int trackTotal = s.substr(pos + 1).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kTrackTotalKey, trackTotal); } else if(s.length()) { int trackNum = s.toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, trackNum); } } // Extract disc number and total discs frameList = tag->frameListMap()["TPOS"]; if(!frameList.isEmpty()) { // Split the tracks at '/' TagLib::String s = frameList.front()->toString(); bool ok; size_t pos = s.find("/", 0); if(TagLib::String::npos() != pos) { int discNum = s.substr(0, pos).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, discNum); int discTotal = s.substr(pos + 1).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kDiscTotalKey, discTotal); } else if(s.length()) { int discNum = s.toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, discNum); } } // Lyrics frameList = tag->frameListMap()["USLT"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kLyricsKey, frameList.front()->toString()); // Extract compilation if present (iTunes TCMP tag) frameList = tag->frameListMap()["TCMP"]; if(!frameList.isEmpty()) // It seems that the presence of this frame indicates a compilation CFDictionarySetValue(dictionary, Metadata::kCompilationKey, kCFBooleanTrue); frameList = tag->frameListMap()["TSRC"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kISRCKey, frameList.front()->toString()); // MusicBrainz auto musicBrainzReleaseIDFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "MusicBrainz Album Id"); if(musicBrainzReleaseIDFrame) TagLib::AddStringToCFDictionary(dictionary, Metadata::kMusicBrainzReleaseIDKey, musicBrainzReleaseIDFrame->fieldList().back()); auto musicBrainzRecordingIDFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "MusicBrainz Track Id"); if(musicBrainzRecordingIDFrame) TagLib::AddStringToCFDictionary(dictionary, Metadata::kMusicBrainzRecordingIDKey, musicBrainzRecordingIDFrame->fieldList().back()); // Sorting and grouping frameList = tag->frameListMap()["TSOT"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kTitleSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSOA"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumTitleSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSOP"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kArtistSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSO2"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumArtistSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSOC"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kComposerSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TIT1"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kGroupingKey, frameList.front()->toString()); // ReplayGain bool foundReplayGain = false; // Preference is TXXX frames, RVA2 frame, then LAME header auto trackGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_TRACK_GAIN"); auto trackPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_TRACK_PEAK"); auto albumGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_ALBUM_GAIN"); auto albumPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_ALBUM_PEAK"); if(!trackGainFrame) trackGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_track_gain"); if(trackGainFrame) { SFB::CFString str(trackGainFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kTrackGainKey, num); AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, 89.0); foundReplayGain = true; } if(!trackPeakFrame) trackPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_track_peak"); if(trackPeakFrame) { SFB::CFString str(trackPeakFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kTrackPeakKey, num); } if(!albumGainFrame) albumGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_album_gain"); if(albumGainFrame) { SFB::CFString str(albumGainFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kAlbumGainKey, num); AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, 89.0); foundReplayGain = true; } if(!albumPeakFrame) albumPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_album_peak"); if(albumPeakFrame) { SFB::CFString str(albumPeakFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kAlbumPeakKey, num); } // If nothing found check for RVA2 frame if(!foundReplayGain) { frameList = tag->frameListMap()["RVA2"]; for(auto frameIterator : tag->frameListMap()["RVA2"]) { TagLib::ID3v2::RelativeVolumeFrame *relativeVolume = dynamic_cast<TagLib::ID3v2::RelativeVolumeFrame *>(frameIterator); if(!relativeVolume) continue; // Attempt to use the master volume if present auto channels = relativeVolume->channels(); auto channelType = TagLib::ID3v2::RelativeVolumeFrame::MasterVolume; // Fall back on whatever else exists in the frame if(!channels.contains(TagLib::ID3v2::RelativeVolumeFrame::MasterVolume)) channelType = channels.front(); float volumeAdjustment = relativeVolume->volumeAdjustment(channelType); if(TagLib::String("track", TagLib::String::Latin1) == relativeVolume->identification()) { if((int)volumeAdjustment) AddFloatToDictionary(dictionary, Metadata::kTrackGainKey, volumeAdjustment); } else if(TagLib::String("album", TagLib::String::Latin1) == relativeVolume->identification()) { if((int)volumeAdjustment) AddFloatToDictionary(dictionary, Metadata::kAlbumGainKey, volumeAdjustment); } // Fall back to track gain if identification is not specified else { if((int)volumeAdjustment) AddFloatToDictionary(dictionary, Metadata::kTrackGainKey, volumeAdjustment); } } } // Extract album art if present for(auto it : tag->frameListMap()["APIC"]) { TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(it); if(frame) { SFB::CFData data((const UInt8 *)frame->picture().data(), (CFIndex)frame->picture().size()); SFB::CFString description; if(!frame->description().isEmpty()) description = CFString(frame->description().toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, (AttachedPicture::Type)frame->type(), description)); } } return true; }
bool SFB::Audio::AddAPETagToDictionary(CFMutableDictionaryRef dictionary, std::vector<std::shared_ptr<AttachedPicture>>& attachedPictures, const TagLib::APE::Tag *tag) { if(nullptr == dictionary || nullptr == tag) return false; if(tag->isEmpty()) return true; SFB::CFMutableDictionary additionalMetadata(0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); for(auto iterator : tag->itemListMap()) { auto item = iterator.second; if(item.isEmpty()) continue; if(TagLib::APE::Item::Text == item.type()) { SFB::CFString key(item.key().toCString(true), kCFStringEncodingUTF8); SFB::CFString value(item.toString().toCString(true), kCFStringEncodingUTF8); if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUM"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSER"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GENRE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGenreKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DATE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kReleaseDateKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DESCRIPTION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCommentKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPILATION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCompilationKey, CFStringGetIntValue(value) ? kCFBooleanTrue : kCFBooleanFalse); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("LYRICS"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kLyricsKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("BPM"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kBPMKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("RATING"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kRatingKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ISRC"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kISRCKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MCN"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMCNKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_ALBUMID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzReleaseIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_TRACKID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzRecordingIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMTITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSERSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GROUPING"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGroupingKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_REFERENCE_LOUDNESS"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackPeakKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumPeakKey, CFStringGetDoubleValue(value)); #if 0 else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("METADATA_BLOCK_PICTURE"), kCFCompareCaseInsensitive)) { // Handle embedded pictures for(auto blockIterator : item.values()) { auto encodedBlock = blockIterator.data(TagLib::String::UTF8); // Decode the Base-64 encoded data auto decodedBlock = TagLib::DecodeBase64(encodedBlock); // Create the picture TagLib::FLAC::Picture picture; picture.parse(decodedBlock); SFB::CFData data((const UInt8 *)picture.data().data(), picture.data().size()); SFB::CFString description = nullptr; if(!picture.description().isNull()) description(picture.description().toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, (AttachedPicture::Type)picture.type(), description)); } } #endif // Put all unknown tags into the additional metadata else CFDictionarySetValue(additionalMetadata, key, value); } else if(TagLib::APE::Item::Binary == item.type()) { SFB::CFString key(item.key().toCString(true), kCFStringEncodingUTF8); // From http://www.hydrogenaudio.org/forums/index.php?showtopic=40603&view=findpost&p=504669 /* <length> 32 bit <flags with binary bit set> 32 bit <field name> "Cover Art (Front)"|"Cover Art (Back)" 0x00 <description> UTF-8 string (needs to be a file name to be recognized by AudioShell - meh) 0x00 <cover data> binary */ if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("Cover Art (Front)"), kCFCompareCaseInsensitive) || kCFCompareEqualTo == CFStringCompare(key, CFSTR("Cover Art (Back)"), kCFCompareCaseInsensitive)) { auto binaryData = item.binaryData(); size_t pos = binaryData.find('\0'); if(TagLib::ByteVector::npos() != pos && 3 < binaryData.size()) { SFB::CFData data((const UInt8 *)binaryData.mid(pos + 1).data(), (CFIndex)(binaryData.size() - pos - 1)); SFB::CFString description(TagLib::String(binaryData.mid(0, pos), TagLib::String::UTF8).toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, kCFCompareEqualTo == CFStringCompare(key, CFSTR("Cover Art (Front)"), kCFCompareCaseInsensitive) ? AttachedPicture::Type::FrontCover : AttachedPicture::Type::BackCover, description)); } } } } if(CFDictionaryGetCount(additionalMetadata)) CFDictionarySetValue(dictionary, Metadata::kAdditionalMetadataKey, additionalMetadata); return true; }
bool MP3Metadata::ReadMetadata(CFErrorRef *error) { // Start from scratch CFDictionaryRemoveAllValues(mMetadata); CFDictionaryRemoveAllValues(mChangedMetadata); UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX)) return false; auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf), true); TagLib::MPEG::File file(stream, TagLib::ID3v2::FrameFactory::instance()); if(!file.isValid()) { if(nullptr != error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid MPEG file."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not an MPEG file"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MP3")); if(file.audioProperties()) { auto properties = file.audioProperties(); AddAudioPropertiesToDictionary(mMetadata, properties); // TODO: Is this too much information? #if 0 switch(properties->version()) { case TagLib::MPEG::Header::Version1: switch(properties->layer()) { case 1: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-1 Layer I")); break; case 2: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-1 Layer II")); break; case 3: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-1 Layer III")); break; } break; case TagLib::MPEG::Header::Version2: switch(properties->layer()) { case 1: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2 Layer I")); break; case 2: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2 Layer II")); break; case 3: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2 Layer III")); break; } break; case TagLib::MPEG::Header::Version2_5: switch(properties->layer()) { case 1: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2.5 Layer I")); break; case 2: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2.5 Layer II")); break; case 3: CFDictionarySetValue(mMetadata, kPropertiesFormatNameKey, CFSTR("MPEG-2.5 Layer III")); break; } break; } #endif if(properties->xingHeader() && properties->xingHeader()->totalFrames()) AddIntToDictionary(mMetadata, kPropertiesTotalFramesKey, properties->xingHeader()->totalFrames()); } if(file.APETag()) { std::vector<AttachedPicture *> pictures; AddAPETagToDictionary(mMetadata, pictures, file.APETag()); for(auto picture : pictures) AddSavedPicture(picture); } if(file.ID3v1Tag()) AddID3v1TagToDictionary(mMetadata, file.ID3v1Tag()); if(file.ID3v2Tag()) { std::vector<AttachedPicture *> pictures; AddID3v2TagToDictionary(mMetadata, pictures, file.ID3v2Tag()); for(auto picture : pictures) AddSavedPicture(picture); } return true; }
bool SFB::Audio::AddXiphCommentToDictionary(CFMutableDictionaryRef dictionary, std::vector<std::shared_ptr<AttachedPicture>>& attachedPictures, const TagLib::Ogg::XiphComment *tag) { if(nullptr == dictionary || nullptr == tag) return false; SFB::CFMutableDictionary additionalMetadata = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); for(auto it : tag->fieldListMap()) { // According to the Xiph comment specification keys should only contain a limited subset of ASCII, but UTF-8 is a safer choice SFB::CFString key = CFStringCreateWithCString(kCFAllocatorDefault, it.first.toCString(true), kCFStringEncodingUTF8); // Vorbis allows multiple comments with the same key, but this isn't supported by AudioMetadata SFB::CFString value = CFStringCreateWithCString(kCFAllocatorDefault, it.second.front().toCString(true), kCFStringEncodingUTF8); if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUM"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSER"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GENRE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGenreKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DATE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kReleaseDateKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DESCRIPTION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCommentKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPILATION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCompilationKey, CFStringGetIntValue(value) ? kCFBooleanTrue : kCFBooleanFalse); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("LYRICS"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kLyricsKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("BPM"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kBPMKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("RATING"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kRatingKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ISRC"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kISRCKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MCN"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMCNKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_ALBUMID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzReleaseIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_TRACKID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzRecordingIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMTITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSERSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GROUPING"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGroupingKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_REFERENCE_LOUDNESS"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackPeakKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumPeakKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("METADATA_BLOCK_PICTURE"), kCFCompareCaseInsensitive)) { // Handle embedded pictures for(auto blockIterator : it.second) { auto encodedBlock = blockIterator.data(TagLib::String::UTF8); // Decode the Base-64 encoded data auto decodedBlock = TagLib::DecodeBase64(encodedBlock); // Create the picture TagLib::FLAC::Picture picture; picture.parse(decodedBlock); SFB::CFData data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)picture.data().data(), (CFIndex)picture.data().size()); SFB::CFString description; if(!picture.description().isEmpty()) description = CFStringCreateWithCString(kCFAllocatorDefault, picture.description().toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, (AttachedPicture::Type)picture.type(), description)); } } // Put all unknown tags into the additional metadata else CFDictionarySetValue(additionalMetadata, key, value); } if(CFDictionaryGetCount(additionalMetadata)) CFDictionarySetValue(dictionary, Metadata::kAdditionalMetadataKey, additionalMetadata); return true; }