diff --git a/Frameworks/TagLib/taglib/taglib/fileref.cpp b/Frameworks/TagLib/taglib/taglib/fileref.cpp index 2aab81e94..dace6f0a4 100644 --- a/Frameworks/TagLib/taglib/taglib/fileref.cpp +++ b/Frameworks/TagLib/taglib/taglib/fileref.cpp @@ -100,8 +100,10 @@ namespace // updated. However at some point that list should be created at the same time // that a default file type resolver is created. - if(ext.isEmpty()) - return 0; + if(ext.isEmpty()) { + // HACK + return new MPEG::File(stream, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + } // .oga can be any audio in the Ogg container. So leave it to content-based detection. diff --git a/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj b/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj index 0ce1ae031..d9ab5503d 100644 --- a/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj +++ b/Plugins/FFMPEG/FFMPEG.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 8352D49B1CDDB8B2009D16AA /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8352D49A1CDDB8B2009D16AA /* VideoToolbox.framework */; }; 8352D49D1CDDB8C0009D16AA /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8352D49C1CDDB8C0009D16AA /* CoreMedia.framework */; }; 8352D49F1CDDB8D7009D16AA /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8352D49E1CDDB8D7009D16AA /* CoreVideo.framework */; }; + 8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */; }; 83AA7D0C279EBCC600087AA4 /* libavcodec.59.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D08279EBCC600087AA4 /* libavcodec.59.dylib */; }; 83AA7D0D279EBCC600087AA4 /* libavutil.57.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D09279EBCC600087AA4 /* libavutil.57.dylib */; }; 83AA7D0E279EBCC600087AA4 /* libswresample.4.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 83AA7D0A279EBCC600087AA4 /* libswresample.4.dylib */; }; @@ -52,6 +53,9 @@ 8352D49A1CDDB8B2009D16AA /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; 8352D49C1CDDB8C0009D16AA /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; 8352D49E1CDDB8D7009D16AA /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; + 8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = ""; }; + 8356BCE827B37C6F0074E50C /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = ""; }; + 8356BCEA27B37DA40074E50C /* TagLibID3v2Reader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TagLibID3v2Reader.h; path = ../TagLib/TagLibID3v2Reader.h; sourceTree = ""; }; 8384913818081F6C00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = ""; }; 83AA7D08279EBCC600087AA4 /* libavcodec.59.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavcodec.59.dylib; path = ../../ThirdParty/ffmpeg/lib/libavcodec.59.dylib; sourceTree = ""; }; 83AA7D09279EBCC600087AA4 /* libavutil.57.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavutil.57.dylib; path = ../../ThirdParty/ffmpeg/lib/libavutil.57.dylib; sourceTree = ""; }; @@ -135,6 +139,9 @@ 08FB77AFFE84173DC02AAC07 /* Classes */ = { isa = PBXGroup; children = ( + 8356BCEA27B37DA40074E50C /* TagLibID3v2Reader.h */, + 8356BCE827B37C6F0074E50C /* NSDictionary+Merge.h */, + 8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */, 8384913818081F6C00E7332D /* Logging.h */, B09E94370D747FAD0064F138 /* Plugin.h */, B09E942D0D747F410064F138 /* FFMPEGDecoder.h */, @@ -253,6 +260,7 @@ buildActionMask = 2147483647; files = ( B09E942F0D747F410064F138 /* FFMPEGDecoder.m in Sources */, + 8356BCE927B37C6F0074E50C /* NSDictionary+Merge.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Plugins/FFMPEG/FFMPEGDecoder.h b/Plugins/FFMPEG/FFMPEGDecoder.h index b3681a75e..a604c6a53 100644 --- a/Plugins/FFMPEG/FFMPEGDecoder.h +++ b/Plugins/FFMPEG/FFMPEGDecoder.h @@ -40,6 +40,12 @@ int64_t skipSamples; BOOL endOfStream; BOOL endOfAudio; + + int metadataIndex; + NSString *artist; + NSString *title; + NSString *album; + NSDictionary *id3Metadata; } @end diff --git a/Plugins/FFMPEG/FFMPEGDecoder.m b/Plugins/FFMPEG/FFMPEGDecoder.m index 86c08b398..0e6f79521 100644 --- a/Plugins/FFMPEG/FFMPEGDecoder.m +++ b/Plugins/FFMPEG/FFMPEGDecoder.m @@ -9,6 +9,9 @@ // test #import "FFMPEGDecoder.h" +#import "NSDictionary+Merge.h" +#import "TagLibID3v2Reader.h" + #include #import "Logging.h" @@ -92,13 +95,20 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { return NO; } + AVDictionary *dict = NULL; + + av_dict_set_int(&dict, "icy", 1, 0); // Enable Icy interval metadata, if supported + NSString *urlString = [url absoluteString]; - if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, NULL)) < 0) { + if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, &dict)) < 0) { + av_dict_free(&dict); char errDescr[4096]; av_strerror(errcode, errDescr, 4096); ALog(@"Error opening file, errcode = %d, error = %s", errcode, errDescr); return NO; } + + av_dict_free(&dict); } else { buffer = av_malloc(32 * 1024); if(!buffer) { @@ -136,6 +146,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { } streamIndex = -1; + metadataIndex = -1; AVCodecParameters *codecPar; for(i = 0; i < formatCtx->nb_streams; i++) { @@ -144,6 +155,8 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { if(streamIndex < 0 && codecPar->codec_type == AVMEDIA_TYPE_AUDIO) { DLog(@"audio codec found"); streamIndex = i; + } else if(codecPar->codec_id == AV_CODEC_ID_TIMED_ID3) { + metadataIndex = i; } else { stream->discard = AVDISCARD_ALL; } @@ -402,6 +415,12 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { seekable = [s seekable]; + album = @""; + artist = @""; + title = @""; + id3Metadata = @{}; + [self updateMetadata]; + return YES; } @@ -444,6 +463,56 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { [self close]; } +- (void)updateMetadata { + const AVDictionaryEntry *tag = NULL; + NSString *_album = album; + NSString *_artist = artist; + NSString *_title = title; + if(formatCtx->metadata) { + while((tag = av_dict_get(formatCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { + if(!strcasecmp(tag->key, "streamtitle")) { + NSString *artistTitle = [NSString stringWithUTF8String:tag->value]; + NSArray *splitValues = [artistTitle componentsSeparatedByString:@" - "]; + _artist = @""; + _title = [splitValues objectAtIndex:0]; + if([splitValues count] > 1) { + _artist = _title; + _title = [splitValues objectAtIndex:1]; + } + } else if(!strcasecmp(tag->key, "icy-url")) { + _album = [NSString stringWithUTF8String:tag->value]; + } else if(!strcasecmp(tag->key, "artist")) { + _artist = [NSString stringWithUTF8String:tag->value]; + } else if(!strcasecmp(tag->key, "title")) { + _title = [NSString stringWithUTF8String:tag->value]; + } + } + } + + if(![_album isEqual:album] || + ![_artist isEqual:artist] || + ![_title isEqual:title]) { + album = _album; + artist = _artist; + title = _title; + [self willChangeValueForKey:@"metadata"]; + [self didChangeValueForKey:@"metadata"]; + } +} + +- (void)updateID3Metadata { + NSData *tag = [NSData dataWithBytes:lastReadPacket->data length:lastReadPacket->size]; + Class tagReader = NSClassFromString(@"TagLibID3v2Reader"); + if(tagReader && [tagReader respondsToSelector:@selector(metadataForTag:)]) { + NSDictionary *_id3Metadata = [tagReader metadataForTag:tag]; + if(![_id3Metadata isEqualTo:id3Metadata]) { + id3Metadata = _id3Metadata; + [self willChangeValueForKey:@"metadata"]; + [self didChangeValueForKey:@"metadata"]; + } + } +} + - (int)readAudio:(void *)buf frames:(UInt32)frames { if(totalFrames && framesRead >= totalFrames) return 0; @@ -482,6 +551,12 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { } if(formatCtx->pb && formatCtx->pb->error) break; } + + if(lastReadPacket->stream_index == metadataIndex) { + [self updateID3Metadata]; + continue; + } + if(lastReadPacket->stream_index != streamIndex) continue; } @@ -593,6 +668,8 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { bytesRead += toConsume; } + [self updateMetadata]; + int framesReadNow = bytesRead / frameSize; if(totalFrames && (framesRead + framesReadNow > totalFrames)) framesReadNow = (int)(totalFrames - framesRead); @@ -648,7 +725,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) { } - (NSDictionary *)metadata { - return @{}; + return [NSDictionary dictionaryByMerging:@{ @"album": album, @"artist": artist, @"title": title } with:id3Metadata]; } + (NSArray *)fileTypes { diff --git a/Plugins/TagLib/TagLib.xcodeproj/project.pbxproj b/Plugins/TagLib/TagLib.xcodeproj/project.pbxproj index 19424046b..ce97a4149 100644 --- a/Plugins/TagLib/TagLib.xcodeproj/project.pbxproj +++ b/Plugins/TagLib/TagLib.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 17C93FC30B90056C008627D6 /* TagLibMetadataReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93FC20B90056C008627D6 /* TagLibMetadataReader.m */; }; 17F563B40C3BDBB30019975C /* TagLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17F563A60C3BDB8F0019975C /* TagLib.framework */; }; 17F563B60C3BDBB50019975C /* TagLib.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 17F563A60C3BDB8F0019975C /* TagLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 8356BCE527B377C20074E50C /* TagLibID3v2Reader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8356BCE327B377C20074E50C /* TagLibID3v2Reader.h */; }; + 8356BCE627B377C20074E50C /* TagLibID3v2Reader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8356BCE427B377C20074E50C /* TagLibID3v2Reader.mm */; }; 8384913A18081FFC00E7332D /* Logging.h in Headers */ = {isa = PBXBuildFile; fileRef = 8384913918081FFC00E7332D /* Logging.h */; }; 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; /* End PBXBuildFile section */ @@ -56,6 +58,8 @@ 17C93FC20B90056C008627D6 /* TagLibMetadataReader.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; path = TagLibMetadataReader.m; sourceTree = ""; }; 17F563A00C3BDB8F0019975C /* TagLib.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = TagLib.xcodeproj; path = ../../Frameworks/TagLib/TagLib.xcodeproj; sourceTree = SOURCE_ROOT; }; 32DBCF630370AF2F00C91783 /* TagLib_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TagLib_Prefix.pch; sourceTree = ""; }; + 8356BCE327B377C20074E50C /* TagLibID3v2Reader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TagLibID3v2Reader.h; sourceTree = ""; }; + 8356BCE427B377C20074E50C /* TagLibID3v2Reader.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TagLibID3v2Reader.mm; sourceTree = ""; }; 8384913918081FFC00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = ""; }; 8D5B49B6048680CD000E48DA /* TagLib.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TagLib.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -113,6 +117,8 @@ 177FCFA40B90C9600011C3B5 /* Plugin.h */, 17C93FC10B90056C008627D6 /* TagLibMetadataReader.h */, 17C93FC20B90056C008627D6 /* TagLibMetadataReader.m */, + 8356BCE327B377C20074E50C /* TagLibID3v2Reader.h */, + 8356BCE427B377C20074E50C /* TagLibID3v2Reader.mm */, ); name = Classes; sourceTree = ""; @@ -168,6 +174,7 @@ buildActionMask = 2147483647; files = ( 8384913A18081FFC00E7332D /* Logging.h in Headers */, + 8356BCE527B377C20074E50C /* TagLibID3v2Reader.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -257,6 +264,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8356BCE627B377C20074E50C /* TagLibID3v2Reader.mm in Sources */, 17C93FC30B90056C008627D6 /* TagLibMetadataReader.m in Sources */, 07CACE8B0ED1AD1000C0F1E8 /* TagLibMetadataWriter.m in Sources */, ); diff --git a/Plugins/TagLib/TagLibID3v2Reader.h b/Plugins/TagLib/TagLibID3v2Reader.h new file mode 100644 index 000000000..5dc0ba8b4 --- /dev/null +++ b/Plugins/TagLib/TagLibID3v2Reader.h @@ -0,0 +1,16 @@ +// +// TagLibID3v2Reader.h +// TagLib Plugin +// +// Created by Christopher Snowhill on 2/8/22. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TagLibID3v2Reader : NSObject ++ (NSDictionary *)metadataForTag:(NSData *)tagBlock; +@end + +NS_ASSUME_NONNULL_END diff --git a/Plugins/TagLib/TagLibID3v2Reader.mm b/Plugins/TagLib/TagLibID3v2Reader.mm new file mode 100644 index 000000000..7f707317a --- /dev/null +++ b/Plugins/TagLib/TagLibID3v2Reader.mm @@ -0,0 +1,153 @@ +// +// TagLibID3v2Reader.m +// TagLib Plugin +// +// Created by Christopher Snowhill on 2/8/22. +// + +#import "TagLibID3v2Reader.h" + +#import +#import +#import +#import +#import + +@implementation TagLibID3v2Reader + ++ (NSDictionary *)metadataForTag:(NSData *)tagBlock { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + + // if ( !*TagLib::ascii_encoding ) { + // NSStringEncoding enc = [NSString defaultCStringEncoding]; + // CFStringEncoding cfenc = CFStringConvertNSStringEncodingToEncoding(enc); + // NSString *ref = (NSString *)CFStringConvertEncodingToIANACharSetName(cfenc); + // UInt32 cp = CFStringConvertEncodingToWindowsCodepage(cfenc); + // + // // Most tags are using windows codepage, so remap OS X codepage to Windows one. + // + // static struct { + // UInt32 from, to; + // } codepage_remaps[] = { + // { 10001, 932 }, // Japanese Shift-JIS + // { 10002, 950 }, // Traditional Chinese + // { 10003, 949 }, // Korean + // { 10004, 1256 }, // Arabic + // { 10005, 1255 }, // Hebrew + // { 10006, 1253 }, // Greek + // { 10007, 1251 }, // Cyrillic + // { 10008, 936 }, // Simplified Chinese + // { 10029, 1250 }, // Central European (latin2) + // }; + // + // int i; + // int max = sizeof(codepage_remaps)/sizeof(codepage_remaps[0]); + // for ( i=0; iartist(); + albumartist = tag->albumartist(); + title = tag->title(); + album = tag->album(); + genre = tag->genre(); + comment = tag->comment(); + cuesheet = tag->cuesheet(); + + year = tag->year(); + [dict setObject:[NSNumber numberWithInt:year] forKey:@"year"]; + + track = tag->track(); + [dict setObject:[NSNumber numberWithInt:track] forKey:@"track"]; + + disc = tag->disc(); + [dict setObject:[NSNumber numberWithInt:disc] forKey:@"disc"]; + + rgAlbumGain = tag->rgAlbumGain(); + rgAlbumPeak = tag->rgAlbumPeak(); + rgTrackGain = tag->rgTrackGain(); + rgTrackPeak = tag->rgTrackPeak(); + [dict setObject:[NSNumber numberWithFloat:rgAlbumGain] forKey:@"replayGainAlbumGain"]; + [dict setObject:[NSNumber numberWithFloat:rgAlbumPeak] forKey:@"replayGainAlbumPeak"]; + [dict setObject:[NSNumber numberWithFloat:rgTrackGain] forKey:@"replayGainTrackGain"]; + [dict setObject:[NSNumber numberWithFloat:rgTrackPeak] forKey:@"replayGainTrackPeak"]; + + soundcheck = tag->soundcheck(); + if(!soundcheck.isEmpty()) { + TagLib::StringList tag = soundcheck.split(" "); + TagLib::StringList wantedTag; + for(int i = 0, count = tag.size(); i < count; i++) { + if(tag[i].length() == 8) + wantedTag.append(tag[i]); + } + + if(wantedTag.size() >= 10) { + float volume1 = -log10((double)((uint32_t)wantedTag[0].toInt(16)) / 1000) * 10; + float volume2 = -log10((double)((uint32_t)wantedTag[1].toInt(16)) / 1000) * 10; + float volumeToUse = MIN(volume1, volume2); + float volumeScale = pow(10, volumeToUse / 20); + [dict setObject:[NSNumber numberWithFloat:volumeScale] forKey:@"volume"]; + } + } + + if(!artist.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:artist.toCString(true)] forKey:@"artist"]; + + if(!albumartist.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:albumartist.toCString(true)] forKey:@"albumartist"]; + + if(!album.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:album.toCString(true)] forKey:@"album"]; + + if(!title.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:title.toCString(true)] forKey:@"title"]; + + if(!genre.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:genre.toCString(true)] forKey:@"genre"]; + + if(!cuesheet.isEmpty()) + [dict setObject:[NSString stringWithUTF8String:cuesheet.toCString(true)] forKey:@"cuesheet"]; + + // Try to load the image. + NSData *image = nil; + + TagLib::MPEG::File *mf = dynamic_cast(f.file()); + if(mf) { + TagLib::ID3v2::FrameList pictures = mf->ID3v2Tag()->frameListMap()["APIC"]; + if(!pictures.isEmpty()) { + TagLib::ID3v2::AttachedPictureFrame *pic = static_cast(pictures.front()); + + image = [NSData dataWithBytes:pic->picture().data() length:pic->picture().size()]; + } + } + + if(nil != image) { + [dict setObject:image forKey:@"albumArt"]; + } + } + } + + return [NSDictionary dictionaryWithDictionary:dict]; +} + +@end