From a05d4537c1d74ac7eda2ce305b9daaa59a543087 Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Sat, 12 Feb 2022 07:16:59 -0800 Subject: [PATCH] Various tagging fixes - Fix Vorbis, Opus, and FLAC tag reading - Fix Vorbis getting a 0 length if passing through the CUE Sheet reader - Implement support for FLAC binary CUE Sheets Signed-off-by: Christopher Snowhill --- Plugins/CueSheet/CueSheetContainer.m | 5 + Plugins/CueSheet/CueSheetDecoder.m | 39 +- Plugins/CueSheet/CueSheetMetadataReader.m | 5 + Plugins/Flac/Flac.xcodeproj/project.pbxproj | 4 + Plugins/Flac/FlacDecoder.h | 16 +- Plugins/Flac/FlacDecoder.m | 143 +++- Plugins/Flac/cuesheet.m | 645 ++++++++++++++++++ Plugins/Opus/Opus/OpusDecoder.h | 15 +- Plugins/Opus/Opus/OpusDecoder.m | 137 ++-- Plugins/Vorbis/VorbisDecoder.h | 14 +- Plugins/Vorbis/VorbisDecoder.m | 124 +++- .../VorbisPlugin.xcodeproj/project.pbxproj | 36 + Plugins/Vorbis/vorbis-tools/include/base64.h | 35 + Plugins/Vorbis/vorbis-tools/include/picture.h | 79 +++ Plugins/Vorbis/vorbis-tools/share/base64.c | 95 +++ Plugins/Vorbis/vorbis-tools/share/picture.c | 236 +++++++ 16 files changed, 1503 insertions(+), 125 deletions(-) create mode 100644 Plugins/Flac/cuesheet.m create mode 100644 Plugins/Vorbis/vorbis-tools/include/base64.h create mode 100644 Plugins/Vorbis/vorbis-tools/include/picture.h create mode 100644 Plugins/Vorbis/vorbis-tools/share/base64.c create mode 100644 Plugins/Vorbis/vorbis-tools/share/picture.c diff --git a/Plugins/CueSheet/CueSheetContainer.m b/Plugins/CueSheet/CueSheetContainer.m index 6c31b0aab..7a4f33f8a 100644 --- a/Plugins/CueSheet/CueSheetContainer.m +++ b/Plugins/CueSheet/CueSheetContainer.m @@ -47,7 +47,12 @@ if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) { // Embedded cuesheet check fileMetadata = [NSClassFromString(@"AudioMetadataReader") metadataForURL:url skipCue:YES]; + + NSDictionary *alsoMetadata = [NSClassFromString(@"AudioPropertiesReader") propertiesForURL:url]; + NSString *sheet = [fileMetadata objectForKey:@"cuesheet"]; + if(!sheet || ![sheet length]) sheet = [alsoMetadata objectForKey:@"cuesheet"]; + if([sheet length]) { cuesheet = [CueSheet cueSheetWithString:sheet withFilename:[url path]]; } diff --git a/Plugins/CueSheet/CueSheetDecoder.m b/Plugins/CueSheet/CueSheetDecoder.m index a05d6c376..a9359b3a1 100644 --- a/Plugins/CueSheet/CueSheetDecoder.m +++ b/Plugins/CueSheet/CueSheetDecoder.m @@ -38,7 +38,8 @@ NSMutableDictionary *properties = [[decoder properties] mutableCopy]; // Need to alter length - [properties setObject:[NSNumber numberWithLong:(trackEnd - trackStart)] forKey:@"totalFrames"]; + if(!noFragment) + [properties setObject:[NSNumber numberWithLong:(trackEnd - trackStart)] forKey:@"totalFrames"]; return [NSDictionary dictionaryWithDictionary:properties]; } @@ -70,7 +71,21 @@ if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) { // Embedded cuesheet check fileMetadata = [NSClassFromString(@"AudioMetadataReader") metadataForURL:url skipCue:YES]; + + source = s; + + decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES]; + + if(![decoder open:source]) { + ALog(@"Could not open cuesheet decoder"); + return NO; + } + + NSDictionary *alsoMetadata = [decoder metadata]; + NSString *sheet = [fileMetadata objectForKey:@"cuesheet"]; + if(!sheet || ![sheet length]) sheet = [alsoMetadata objectForKey:@"cuesheet"]; + if([sheet length]) { cuesheet = [CueSheet cueSheetWithString:sheet withFilename:[url path]]; embedded = YES; @@ -91,21 +106,21 @@ if([[[tracks objectAtIndex:i] track] isEqualToString:[url fragment]]) { track = [tracks objectAtIndex:i]; - NSURL *trackUrl = (embedded) ? baseURL : [track url]; - // Kind of a hackish way of accessing outside classes. - source = [NSClassFromString(@"AudioSource") audioSourceForURL:trackUrl]; + if(!embedded) { + source = [NSClassFromString(@"AudioSource") audioSourceForURL:[track url]]; - if(![source open:trackUrl]) { - ALog(@"Could not open cuesheet source"); - return NO; - } + if(![source open:[track url]]) { + ALog(@"Could not open cuesheet source"); + return NO; + } - decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES]; + decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES]; - if(![decoder open:source]) { - ALog(@"Could not open cuesheet decoder"); - return NO; + if(![decoder open:source]) { + ALog(@"Could not open cuesheet decoder"); + return NO; + } } CueSheetTrack *nextTrack = nil; diff --git a/Plugins/CueSheet/CueSheetMetadataReader.m b/Plugins/CueSheet/CueSheetMetadataReader.m index 7dba95f0a..35126c2b1 100644 --- a/Plugins/CueSheet/CueSheetMetadataReader.m +++ b/Plugins/CueSheet/CueSheetMetadataReader.m @@ -44,7 +44,12 @@ if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) { // Embedded cuesheet check fileMetadata = [audioMetadataReader metadataForURL:url skipCue:YES]; + + NSDictionary *alsoMetadata = [NSClassFromString(@"AudioPropertiesReader") propertiesForURL:url]; + NSString *sheet = [fileMetadata objectForKey:@"cuesheet"]; + if(!sheet || ![sheet length]) sheet = [alsoMetadata objectForKey:@"cuesheet"]; + if([sheet length]) { cuesheet = [CueSheet cueSheetWithString:sheet withFilename:[url path]]; embedded = YES; diff --git a/Plugins/Flac/Flac.xcodeproj/project.pbxproj b/Plugins/Flac/Flac.xcodeproj/project.pbxproj index 10f54ea2e..9e82e36df 100644 --- a/Plugins/Flac/Flac.xcodeproj/project.pbxproj +++ b/Plugins/Flac/Flac.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 17C93F080B8FF67A008627D6 /* FlacDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93F040B8FF67A008627D6 /* FlacDecoder.m */; }; 17F5643C0C3BDC820019975C /* FLAC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17F564220C3BDC460019975C /* FLAC.framework */; }; 17F5643F0C3BDC840019975C /* FLAC.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 17F564220C3BDC460019975C /* FLAC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 83AA660B27B7DAE40098D4B8 /* cuesheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 83AA660A27B7DAE40098D4B8 /* cuesheet.m */; }; 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; /* End PBXBuildFile section */ @@ -54,6 +55,7 @@ 32DBCF630370AF2F00C91783 /* Flac_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Flac_Prefix.pch; sourceTree = ""; }; 8356BD1927B3CCBB0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = ""; }; 8384912D180816C900E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = ""; }; + 83AA660A27B7DAE40098D4B8 /* cuesheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = cuesheet.m; sourceTree = ""; }; 8D5B49B6048680CD000E48DA /* Flac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Flac.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D2F7E65807B2D6F200F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; @@ -104,6 +106,7 @@ 08FB77AFFE84173DC02AAC07 /* Classes */ = { isa = PBXGroup; children = ( + 83AA660A27B7DAE40098D4B8 /* cuesheet.m */, 8356BD1927B3CCBB0074E50C /* HTTPSource.h */, 8384912D180816C900E7332D /* Logging.h */, 177FCFC10B90C9960011C3B5 /* Plugin.h */, @@ -242,6 +245,7 @@ buildActionMask = 2147483647; files = ( 17C93F080B8FF67A008627D6 /* FlacDecoder.m in Sources */, + 83AA660B27B7DAE40098D4B8 /* cuesheet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Plugins/Flac/FlacDecoder.h b/Plugins/Flac/FlacDecoder.h index 1e660b0df..be1f2ca26 100644 --- a/Plugins/Flac/FlacDecoder.h +++ b/Plugins/Flac/FlacDecoder.h @@ -36,10 +36,22 @@ BOOL streamOpened; BOOL abortFlag; - NSString *genre; - NSString *album; NSString *artist; + NSString *albumartist; + NSString *album; NSString *title; + NSString *genre; + NSNumber *year; + NSNumber *track; + NSNumber *disc; + float replayGainAlbumGain; + float replayGainAlbumPeak; + float replayGainTrackGain; + float replayGainTrackPeak; + + NSData *albumArt; + + NSString *cuesheet; } - (void)setSource:(id)s; diff --git a/Plugins/Flac/FlacDecoder.m b/Plugins/Flac/FlacDecoder.m index 8a53238ba..755c32928 100644 --- a/Plugins/Flac/FlacDecoder.m +++ b/Plugins/Flac/FlacDecoder.m @@ -12,6 +12,8 @@ #import "HTTPSource.h" +extern void grabbag__cuesheet_emit(NSString **out, const FLAC__StreamMetadata *cuesheet, const char *file_reference); + @implementation FlacDecoder FLAC__StreamDecoderReadStatus ReadCallback(const FLAC__StreamDecoder *decoder, FLAC__byte blockBuffer[], size_t *bytes, void *client_data) { @@ -194,32 +196,85 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta flacDecoder->hasStreamInfo = YES; } + if(metadata->type == FLAC__METADATA_TYPE_CUESHEET) { + NSString *_cuesheet; + grabbag__cuesheet_emit(&_cuesheet, metadata, [[NSString stringWithFormat:@"\"%@\"", [[[flacDecoder->source url] path] lastPathComponent]] UTF8String]); + + if(![_cuesheet isEqual:flacDecoder->cuesheet]) { + flacDecoder->cuesheet = _cuesheet; + if(![flacDecoder->source seekable]) { + [flacDecoder willChangeValueForKey:@"metadata"]; + [flacDecoder didChangeValueForKey:@"metadata"]; + } + } + } + + if(metadata->type == FLAC__METADATA_TYPE_PICTURE) { + NSData *_albumArt = [NSData dataWithBytes:metadata->data.picture.data length:metadata->data.picture.data_length]; + if(![_albumArt isEqual:flacDecoder->albumArt]) { + flacDecoder->albumArt = _albumArt; + if(![flacDecoder->source seekable]) { + [flacDecoder willChangeValueForKey:@"metadata"]; + [flacDecoder didChangeValueForKey:@"metadata"]; + } + } + } + if(metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) { - NSString *_genre = flacDecoder->genre; - NSString *_album = flacDecoder->album; NSString *_artist = flacDecoder->artist; + NSString *_albumartist = flacDecoder->albumartist; + NSString *_album = flacDecoder->album; NSString *_title = flacDecoder->title; - uint8_t nullByte = '\0'; + NSString *_genre = flacDecoder->genre; + NSNumber *_year = flacDecoder->year; + NSNumber *_track = flacDecoder->track; + NSNumber *_disc = flacDecoder->disc; + float _replayGainAlbumGain = flacDecoder->replayGainAlbumGain; + float _replayGainAlbumPeak = flacDecoder->replayGainAlbumPeak; + float _replayGainTrackGain = flacDecoder->replayGainTrackGain; + float _replayGainTrackPeak = flacDecoder->replayGainTrackPeak; + NSString *_cuesheet = flacDecoder->cuesheet; const FLAC__StreamMetadata_VorbisComment *vorbis_comment = &metadata->data.vorbis_comment; for(int i = 0; i < vorbis_comment->num_comments; ++i) { - NSMutableData *commentField = [NSMutableData dataWithBytes:vorbis_comment->comments[i].entry length:vorbis_comment->comments[i].length]; - [commentField appendBytes:&nullByte length:1]; - NSString *commentString = [NSString stringWithUTF8String:[commentField bytes]]; - NSArray *splitFields = [commentString componentsSeparatedByString:@"="]; - if([splitFields count] == 2) { - NSString *name = [splitFields objectAtIndex:0]; - NSString *value = [splitFields objectAtIndex:1]; - name = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + char *_name; + char *_value; + if(FLAC__metadata_object_vorbiscomment_entry_to_name_value_pair(vorbis_comment->comments[i], &_name, &_value)) { + NSString *name = [NSString stringWithUTF8String:_name]; + NSString *value = [NSString stringWithUTF8String:_value]; + free(_name); + free(_value); name = [name lowercaseString]; - if([name isEqualToString:@"genre"]) { - _genre = value; + if([name isEqualToString:@"artist"]) { + _artist = value; + } else if([name isEqualToString:@"albumartist"]) { + _albumartist = value; } else if([name isEqualToString:@"album"]) { _album = value; - } else if([name isEqualToString:@"artist"]) { - _artist = value; } else if([name isEqualToString:@"title"]) { _title = value; + } else if([name isEqualToString:@"genre"]) { + _genre = value; + } else if([name isEqualToString:@"cuesheet"]) { + _cuesheet = value; + } else if([name isEqualToString:@"date"] || + [name isEqualToString:@"year"]) { + _year = [NSNumber numberWithInt:[value intValue]]; + } else if([name isEqualToString:@"tracknumber"] || + [name isEqualToString:@"tracknum"] || + [name isEqualToString:@"track"]) { + _track = [NSNumber numberWithInt:[value intValue]]; + } else if([name isEqualToString:@"discnumber"] || + [name isEqualToString:@"discnum"] || + [name isEqualToString:@"disc"]) { + _disc = [NSNumber numberWithInt:[value intValue]]; + } else if([name isEqualToString:@"replaygain_album_gain"]) { + _replayGainAlbumGain = [value floatValue]; + } else if([name isEqualToString:@"replaygain_album_peak"]) { + _replayGainAlbumPeak = [value floatValue]; + } else if([name isEqualToString:@"replaygain_track_gain"]) { + _replayGainTrackGain = [value floatValue]; + } else if([name isEqualToString:@"replaygain_track_peak"]) { + _replayGainTrackPeak = [value floatValue]; } else if([name isEqualToString:@"waveformatextensible_channel_mask"]) { if([value hasPrefix:@"0x"]) { char *end; @@ -230,17 +285,37 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta } } - if(![flacDecoder->source seekable] && - (![_genre isEqual:flacDecoder->genre] || - ![_album isEqual:flacDecoder->album] || - ![_artist isEqual:flacDecoder->artist] || - ![_title isEqual:flacDecoder->title])) { - flacDecoder->genre = _genre; - flacDecoder->album = _album; + if(![_artist isEqual:flacDecoder->artist] || + ![_albumartist isEqual:flacDecoder->albumartist] || + ![_album isEqual:flacDecoder->album] || + ![_title isEqual:flacDecoder->title] || + ![_genre isEqual:flacDecoder->genre] || + ![_cuesheet isEqual:flacDecoder->cuesheet] || + ![_year isEqual:flacDecoder->year] || + ![_track isEqual:flacDecoder->track] || + ![_disc isEqual:flacDecoder->disc] || + _replayGainAlbumGain != flacDecoder->replayGainAlbumGain || + _replayGainAlbumPeak != flacDecoder->replayGainAlbumPeak || + _replayGainTrackGain != flacDecoder->replayGainTrackGain || + _replayGainTrackPeak != flacDecoder->replayGainTrackPeak) { flacDecoder->artist = _artist; + flacDecoder->albumartist = _albumartist; + flacDecoder->album = _album; flacDecoder->title = _title; - [flacDecoder willChangeValueForKey:@"metadata"]; - [flacDecoder didChangeValueForKey:@"metadata"]; + flacDecoder->genre = _genre; + flacDecoder->cuesheet = _cuesheet; + flacDecoder->year = _year; + flacDecoder->track = _track; + flacDecoder->disc = _disc; + flacDecoder->replayGainAlbumGain = _replayGainAlbumGain; + flacDecoder->replayGainAlbumPeak = _replayGainAlbumPeak; + flacDecoder->replayGainTrackGain = _replayGainTrackGain; + flacDecoder->replayGainTrackPeak = _replayGainTrackPeak; + + if(![flacDecoder->source seekable]) { + [flacDecoder willChangeValueForKey:@"metadata"]; + [flacDecoder didChangeValueForKey:@"metadata"]; + } } } } @@ -270,10 +345,20 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS isOggFlac = YES; } - genre = @""; - album = @""; artist = @""; + albumartist = @""; + album = @""; title = @""; + genre = @""; + year = @(0); + track = @(0); + disc = @(0); + replayGainAlbumGain = 0.0; + replayGainAlbumPeak = 0.0; + replayGainTrackGain = 0.0; + replayGainTrackPeak = 0.0; + albumArt = [NSData data]; + cuesheet = @""; decoder = FLAC__stream_decoder_new(); if(decoder == NULL) @@ -286,6 +371,8 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS FLAC__stream_decoder_set_metadata_ignore_all(decoder); FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_STREAMINFO); FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); + FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_PICTURE); + FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_CUESHEET); abortFlag = NO; @@ -466,7 +553,7 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS } - (NSDictionary *)metadata { - return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title }; + return @{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"replayGainAlbumGain": @(replayGainAlbumGain), @"replayGainAlbumPeak": @(replayGainAlbumPeak), @"replayGainTrackGain": @(replayGainTrackGain), @"replayGainTrackPeak": @(replayGainTrackPeak), @"cuesheet": cuesheet, @"albumArt": albumArt }; } + (NSArray *)fileTypes { diff --git a/Plugins/Flac/cuesheet.m b/Plugins/Flac/cuesheet.m new file mode 100644 index 000000000..59db6adfa --- /dev/null +++ b/Plugins/Flac/cuesheet.m @@ -0,0 +1,645 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2002-2009 Josh Coalson + * Copyright (C) 2011-2016 Xiph.Org Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#import + +#include +#include + +#include +#include + +uint32_t grabbag__cuesheet_msf_to_frame(uint32_t minutes, uint32_t seconds, uint32_t frames) { + return ((minutes * 60) + seconds) * 75 + frames; +} + +void grabbag__cuesheet_frame_to_msf(uint32_t frame, uint32_t *minutes, uint32_t *seconds, uint32_t *frames) { + *frames = frame % 75; + frame /= 75; + *seconds = frame % 60; + frame /= 60; + *minutes = frame; +} + +/* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */ +static int local__parse_int_(const char *s) { + int ret = 0; + char c; + + if(*s == '\0') + return -1; + + while('\0' != (c = *s++)) + if(c >= '0' && c <= '9') + ret = ret * 10 + (c - '0'); + else + return -1; + + return ret; +} + +/* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */ +static FLAC__int64 local__parse_int64_(const char *s) { + FLAC__int64 ret = 0; + char c; + + if(*s == '\0') + return -1; + + while('\0' != (c = *s++)) + if(c >= '0' && c <= '9') + ret = ret * 10 + (c - '0'); + else + return -1; + + return ret; +} + +/* accept minute:second:frame syntax of '[0-9]+:[0-9][0-9]?:[0-9][0-9]?', but max second of 59 and max frame of 74, e.g. 0:0:0, 123:45:67 + * return sample number or <0 for error + * WATCHOUT: if sample rate is not evenly divisible by 75, the resulting sample number will be approximate + */ +static FLAC__int64 local__parse_msf_(const char *s, uint32_t sample_rate) { + FLAC__int64 ret, field; + char c; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + while(':' != (c = *s++)) { + if(c >= '0' && c <= '9') + field = field * 10 + (c - '0'); + else + return -1; + } + + ret = field * 60 * sample_rate; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + if(':' != (c = *s++)) { + if(c >= '0' && c <= '9') { + field = field * 10 + (c - '0'); + c = *s++; + if(c != ':') + return -1; + } else + return -1; + } + + if(field >= 60) + return -1; + + ret += field * sample_rate; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + if('\0' != (c = *s++)) { + if(c >= '0' && c <= '9') { + field = field * 10 + (c - '0'); + c = *s++; + } else + return -1; + } + + if(c != '\0') + return -1; + + if(field >= 75) + return -1; + + ret += field * (sample_rate / 75); + + return ret; +} + +/* accept minute:second syntax of '[0-9]+:[0-9][0-9]?{,.[0-9]+}', but second < 60, e.g. 0:0.0, 3:5, 15:31.731 + * return sample number or <0 for error + * WATCHOUT: depending on the sample rate, the resulting sample number may be approximate with fractional seconds + */ +static FLAC__int64 local__parse_ms_(const char *s, uint32_t sample_rate) { + FLAC__int64 ret, field; + double x; + char c, *end; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + while(':' != (c = *s++)) { + if(c >= '0' && c <= '9') + field = field * 10 + (c - '0'); + else + return -1; + } + + ret = field * 60 * sample_rate; + + s++; /* skip the ':' */ + if(strspn(s, "0123456789.") != strlen(s)) + return -1; + x = strtod(s, &end); + if(*end || end == s) + return -1; + if(x < 0.0 || x >= 60.0) + return -1; + + ret += (FLAC__int64)(x * sample_rate); + + return ret; +} + +static char *local__get_field_(char **s, FLAC__bool allow_quotes) { + FLAC__bool has_quote = false; + char *p; + + FLAC__ASSERT(0 != s); + + if(0 == *s) + return 0; + + /* skip leading whitespace */ + while(**s && 0 != strchr(" \t\r\n", **s)) + (*s)++; + + if(**s == 0) { + *s = 0; + return 0; + } + + if(allow_quotes && (**s == '"')) { + has_quote = true; + (*s)++; + if(**s == 0) { + *s = 0; + return 0; + } + } + + p = *s; + + if(has_quote) { + *s = strchr(*s, '\"'); + /* if there is no matching end quote, it's an error */ + if(0 == *s) + p = *s = 0; + else { + **s = '\0'; + (*s)++; + } + } else { + while(**s && 0 == strchr(" \t\r\n", **s)) + (*s)++; + if(**s) { + **s = '\0'; + (*s)++; + } else + *s = 0; + } + + return p; +} + +#if 0 +static FLAC__bool local__cuesheet_parse_(FILE *file, const char **error_message, uint32_t *last_line_read, FLAC__StreamMetadata *cuesheet, uint32_t sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset) +{ + char buffer[4096], *line, *field; + uint32_t forced_leadout_track_num = 0; + FLAC__uint64 forced_leadout_track_offset = 0; + int in_track_num = -1, in_index_num = -1; + FLAC__bool disc_has_catalog = false, track_has_flags = false, track_has_isrc = false, has_forced_leadout = false; + FLAC__StreamMetadata_CueSheet *cs = &cuesheet->data.cue_sheet; + + FLAC__ASSERT(!is_cdda || sample_rate == 44100); + /* double protection */ + if(is_cdda && sample_rate != 44100) { + *error_message = "CD-DA cuesheet only allowed with 44.1kHz sample rate"; + return false; + } + + cs->lead_in = is_cdda? 2 * 44100 /* The default lead-in size for CD-DA */ : 0; + cs->is_cd = is_cdda; + + while(0 != fgets(buffer, sizeof(buffer), file)) { + (*last_line_read)++; + line = buffer; + + { + size_t linelen = strlen(line); + if((linelen == sizeof(buffer)-1) && line[linelen-1] != '\n') { + *error_message = "line too long"; + return false; + } + } + + if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) { + if(0 == FLAC__STRCASECMP(field, "CATALOG")) { + if(disc_has_catalog) { + *error_message = "found multiple CATALOG commands"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) { + *error_message = "CATALOG is missing catalog number"; + return false; + } + if(strlen(field) >= sizeof(cs->media_catalog_number)) { + *error_message = "CATALOG number is too long"; + return false; + } + if(is_cdda && (strlen(field) != 13 || strspn(field, "0123456789") != 13)) { + *error_message = "CD-DA CATALOG number must be 13 decimal digits"; + return false; + } + safe_strncpy(cs->media_catalog_number, field, sizeof(cs->media_catalog_number)); + disc_has_catalog = true; + } + else if(0 == FLAC__STRCASECMP(field, "FLAGS")) { + if(track_has_flags) { + *error_message = "found multiple FLAGS commands"; + return false; + } + if(in_track_num < 0 || in_index_num >= 0) { + *error_message = "FLAGS command must come after TRACK but before INDEX"; + return false; + } + while(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) { + if(0 == FLAC__STRCASECMP(field, "PRE")) + cs->tracks[cs->num_tracks-1].pre_emphasis = 1; + } + track_has_flags = true; + } + else if(0 == FLAC__STRCASECMP(field, "INDEX")) { + FLAC__int64 xx; + FLAC__StreamMetadata_CueSheet_Track *track = &cs->tracks[cs->num_tracks-1]; + if(in_track_num < 0) { + *error_message = "found INDEX before any TRACK"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "INDEX is missing index number"; + return false; + } + in_index_num = local__parse_int_(field); + if(in_index_num < 0) { + *error_message = "INDEX has invalid index number"; + return false; + } + FLAC__ASSERT(cs->num_tracks > 0); + if(track->num_indices == 0) { + /* it's the first index point of the track */ + if(in_index_num > 1) { + *error_message = "first INDEX number of a TRACK must be 0 or 1"; + return false; + } + } + else { + if(in_index_num != track->indices[track->num_indices-1].number + 1) { + *error_message = "INDEX numbers must be sequential"; + return false; + } + } + if(is_cdda && in_index_num > 99) { + *error_message = "CD-DA INDEX number must be between 0 and 99, inclusive"; + return false; + } + /*@@@ search for duplicate track number? */ + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "INDEX is missing an offset after the index number"; + return false; + } + /* first parse as minute:second:frame format */ + xx = local__parse_msf_(field, sample_rate); + if(xx < 0) { + /* CD-DA must use only MM:SS:FF format */ + if(is_cdda) { + *error_message = "illegal INDEX offset (not of the form MM:SS:FF)"; + return false; + } + /* as an extension for non-CD-DA we allow MM:SS.SS or raw sample number */ + xx = local__parse_ms_(field, sample_rate); + if(xx < 0) { + xx = local__parse_int64_(field); + if(xx < 0) { + *error_message = "illegal INDEX offset"; + return false; + } + } + } + else if(sample_rate % 75 && xx) { + /* only sample zero is exact */ + *error_message = "illegal INDEX offset (MM:SS:FF form not allowed if sample rate is not a multiple of 75)"; + return false; + } + if(is_cdda && cs->num_tracks == 1 && cs->tracks[0].num_indices == 0 && xx != 0) { + *error_message = "first INDEX of first TRACK must have an offset of 00:00:00"; + return false; + } + if(is_cdda && track->num_indices > 0 && (FLAC__uint64)xx <= track->indices[track->num_indices-1].offset) { + *error_message = "CD-DA INDEX offsets must increase in time"; + return false; + } + /* fill in track offset if it's the first index of the track */ + if(track->num_indices == 0) + track->offset = (FLAC__uint64)xx; + if(is_cdda && cs->num_tracks > 1) { + const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-2]; + if((FLAC__uint64)xx <= prev->offset + prev->indices[prev->num_indices-1].offset) { + *error_message = "CD-DA INDEX offsets must increase in time"; + return false; + } + } + if(!FLAC__metadata_object_cuesheet_track_insert_blank_index(cuesheet, cs->num_tracks-1, track->num_indices)) { + *error_message = "memory allocation error"; + return false; + } + track->indices[track->num_indices-1].offset = (FLAC__uint64)xx - track->offset; + track->indices[track->num_indices-1].number = in_index_num; + } + else if(0 == FLAC__STRCASECMP(field, "ISRC")) { + char *l, *r; + if(track_has_isrc) { + *error_message = "found multiple ISRC commands"; + return false; + } + if(in_track_num < 0 || in_index_num >= 0) { + *error_message = "ISRC command must come after TRACK but before INDEX"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) { + *error_message = "ISRC is missing ISRC number"; + return false; + } + /* strip out dashes */ + for(l = r = field; *r; r++) { + if(*r != '-') + *l++ = *r; + } + *l = '\0'; + if(strlen(field) != 12 || strspn(field, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") < 5 || strspn(field+5, "1234567890") != 7) { + *error_message = "invalid ISRC number"; + return false; + } + safe_strncpy(cs->tracks[cs->num_tracks-1].isrc, field, sizeof(cs->tracks[cs->num_tracks-1].isrc)); + track_has_isrc = true; + } + else if(0 == FLAC__STRCASECMP(field, "TRACK")) { + if(cs->num_tracks > 0) { + const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1]; + if( + prev->num_indices == 0 || + ( + is_cdda && + ( + (prev->num_indices == 1 && prev->indices[0].number != 1) || + (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1) + ) + ) + ) { + *error_message = is_cdda? + "previous TRACK must specify at least one INDEX 01" : + "previous TRACK must specify at least one INDEX"; + return false; + } + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "TRACK is missing track number"; + return false; + } + in_track_num = local__parse_int_(field); + if(in_track_num < 0) { + *error_message = "TRACK has invalid track number"; + return false; + } + if(in_track_num == 0) { + *error_message = "TRACK number must be greater than 0"; + return false; + } + if(is_cdda) { + if(in_track_num > 99) { + *error_message = "CD-DA TRACK number must be between 1 and 99, inclusive"; + return false; + } + } + else { + if(in_track_num == 255) { + *error_message = "TRACK number 255 is reserved for the lead-out"; + return false; + } + else if(in_track_num > 255) { + *error_message = "TRACK number must be between 1 and 254, inclusive"; + return false; + } + } + if(is_cdda && cs->num_tracks > 0 && in_track_num != cs->tracks[cs->num_tracks-1].number + 1) { + *error_message = "CD-DA TRACK numbers must be sequential"; + return false; + } + /*@@@ search for duplicate track number? */ + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "TRACK is missing a track type after the track number"; + return false; + } + if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) { + *error_message = "memory allocation error"; + return false; + } + cs->tracks[cs->num_tracks-1].number = in_track_num; + cs->tracks[cs->num_tracks-1].type = (0 == FLAC__STRCASECMP(field, "AUDIO"))? 0 : 1; /*@@@ should we be more strict with the value here? */ + in_index_num = -1; + track_has_flags = false; + track_has_isrc = false; + } + else if(0 == FLAC__STRCASECMP(field, "REM")) { + if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) { + if(0 == strcmp(field, "FLAC__lead-in")) { + FLAC__int64 xx; + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "FLAC__lead-in is missing offset"; + return false; + } + xx = local__parse_int64_(field); + if(xx < 0) { + *error_message = "illegal FLAC__lead-in offset"; + return false; + } + if(is_cdda && xx % 588 != 0) { + *error_message = "illegal CD-DA FLAC__lead-in offset, must be even multiple of 588 samples"; + return false; + } + cs->lead_in = (FLAC__uint64)xx; + } + else if(0 == strcmp(field, "FLAC__lead-out")) { + int track_num; + FLAC__int64 offset; + if(has_forced_leadout) { + *error_message = "multiple FLAC__lead-out commands"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "FLAC__lead-out is missing track number"; + return false; + } + track_num = local__parse_int_(field); + if(track_num < 0) { + *error_message = "illegal FLAC__lead-out track number"; + return false; + } + forced_leadout_track_num = (uint32_t)track_num; + /*@@@ search for duplicate track number? */ + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "FLAC__lead-out is missing offset"; + return false; + } + offset = local__parse_int64_(field); + if(offset < 0) { + *error_message = "illegal FLAC__lead-out offset"; + return false; + } + forced_leadout_track_offset = (FLAC__uint64)offset; + if(forced_leadout_track_offset != lead_out_offset) { + *error_message = "FLAC__lead-out offset does not match end-of-stream offset"; + return false; + } + has_forced_leadout = true; + } + } + } + } + } + + if(cs->num_tracks == 0) { + *error_message = "there must be at least one TRACK command"; + return false; + } + else { + const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1]; + if( + prev->num_indices == 0 || + ( + is_cdda && + ( + (prev->num_indices == 1 && prev->indices[0].number != 1) || + (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1) + ) + ) + ) { + *error_message = is_cdda? + "previous TRACK must specify at least one INDEX 01" : + "previous TRACK must specify at least one INDEX"; + return false; + } + } + + if(!has_forced_leadout) { + forced_leadout_track_num = is_cdda? 170 : 255; + forced_leadout_track_offset = lead_out_offset; + } + if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) { + *error_message = "memory allocation error"; + return false; + } + cs->tracks[cs->num_tracks-1].number = forced_leadout_track_num; + cs->tracks[cs->num_tracks-1].offset = forced_leadout_track_offset; + + if(!feof(file)) { + *error_message = "read error"; + return false; + } + return true; +} + +FLAC__StreamMetadata *grabbag__cuesheet_parse(FILE *file, const char **error_message, uint32_t *last_line_read, uint32_t sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset) +{ + FLAC__StreamMetadata *cuesheet; + + FLAC__ASSERT(0 != file); + FLAC__ASSERT(0 != error_message); + FLAC__ASSERT(0 != last_line_read); + + *last_line_read = 0; + cuesheet = FLAC__metadata_object_new(FLAC__METADATA_TYPE_CUESHEET); + + if(0 == cuesheet) { + *error_message = "memory allocation error"; + return 0; + } + + if(!local__cuesheet_parse_(file, error_message, last_line_read, cuesheet, sample_rate, is_cdda, lead_out_offset)) { + FLAC__metadata_object_delete(cuesheet); + return 0; + } + + return cuesheet; +} +#endif + +void grabbag__cuesheet_emit(NSString **out, const FLAC__StreamMetadata *cuesheet, const char *file_reference) { + const FLAC__StreamMetadata_CueSheet *cs; + uint32_t track_num, index_num; + + FLAC__ASSERT(0 != out); + FLAC__ASSERT(0 != cuesheet); + FLAC__ASSERT(cuesheet->type == FLAC__METADATA_TYPE_CUESHEET); + + NSMutableArray *stringList = [[NSMutableArray alloc] init]; + + cs = &cuesheet->data.cue_sheet; + + if(*(cs->media_catalog_number)) + [stringList addObject:[NSString stringWithFormat:@"CATALOG %s\n", cs->media_catalog_number]]; + [stringList addObject:[NSString stringWithFormat:@"FILE %s\n", file_reference]]; + + for(track_num = 0; track_num < cs->num_tracks - 1; track_num++) { + const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num; + + [stringList addObject:[NSString stringWithFormat:@" TRACK %02u %s\n", (uint32_t)track->number, track->type == 0 ? "AUDIO" : "DATA"]]; + + if(track->pre_emphasis) + [stringList addObject:@" FLAGS PRE\n"]; + if(*(track->isrc)) + [stringList addObject:[NSString stringWithFormat:@" ISRC %s\n", track->isrc]]; + + for(index_num = 0; index_num < track->num_indices; index_num++) { + const FLAC__StreamMetadata_CueSheet_Index *indx = track->indices + index_num; + + [stringList addObject:[NSString stringWithFormat:@" INDEX %02u ", (uint32_t)indx->number]]; + if(cs->is_cd) { + const uint32_t logical_frame = (uint32_t)((track->offset + indx->offset) / (44100 / 75)); + uint32_t m, s, f; + grabbag__cuesheet_frame_to_msf(logical_frame, &m, &s, &f); + [stringList addObject:[NSString stringWithFormat:@"%02u:%02u:%02u\n", m, s, f]]; + } else + [stringList addObject:[NSString stringWithFormat:@"%" PRIu64 "\n", (track->offset + indx->offset)]]; + } + } + + [stringList addObject:[NSString stringWithFormat:@"REM FLAC__lead-in %" PRIu64 "\n", cs->lead_in]]; + [stringList addObject:[NSString stringWithFormat:@"REM FLAC__lead-out %u %" PRIu64 "\n", (uint32_t)cs->tracks[track_num].number, cs->tracks[track_num].offset]]; + + *out = [stringList componentsJoinedByString:@""]; +} diff --git a/Plugins/Opus/Opus/OpusDecoder.h b/Plugins/Opus/Opus/OpusDecoder.h index 60a8a5cff..46510ae03 100644 --- a/Plugins/Opus/Opus/OpusDecoder.h +++ b/Plugins/Opus/Opus/OpusDecoder.h @@ -30,13 +30,18 @@ int channels; long totalFrames; - double track_gain; - double album_gain; - - NSString *genre; - NSString *album; NSString *artist; + NSString *albumartist; + NSString *album; NSString *title; + NSString *genre; + NSNumber *year; + NSNumber *track; + NSNumber *disc; + float replayGainAlbumGain; + float replayGainTrackGain; + + NSData *albumArt; } @end diff --git a/Plugins/Opus/Opus/OpusDecoder.m b/Plugins/Opus/Opus/OpusDecoder.m index 5f9e62c59..44fbcdcfc 100644 --- a/Plugins/Opus/Opus/OpusDecoder.m +++ b/Plugins/Opus/Opus/OpusDecoder.m @@ -111,64 +111,119 @@ opus_int64 sourceTell(void *_stream) { opus_tags_get_track_gain(tags, &_track_gain); - album_gain = ((double)head->output_gain / 256.0) + 5.0; - track_gain = ((double)_track_gain / 256.0) + album_gain; + replayGainAlbumGain = ((double)head->output_gain / 256.0) + 5.0; + replayGainTrackGain = ((double)_track_gain / 256.0) + replayGainAlbumGain; op_set_gain_offset(opusRef, OP_ABSOLUTE_GAIN, 0); [self willChangeValueForKey:@"properties"]; [self didChangeValueForKey:@"properties"]; - genre = @""; - album = @""; artist = @""; + albumartist = @""; + album = @""; title = @""; + genre = @""; + year = @(0); + track = @(0); + disc = @(0); + albumArt = [NSData data]; [self updateMetadata]; return YES; } -- (void)updateMetadata { - const OpusTags *comment = op_tags(opusRef, -1); +- (NSString *)parseTag:(NSString *)tag fromTags:(const OpusTags *)tags { + NSMutableArray *tagStrings = [[NSMutableArray alloc] init]; - if(comment) { - uint8_t nullByte = '\0'; - NSString *_genre = genre; - NSString *_album = album; - NSString *_artist = artist; - NSString *_title = title; - for(int i = 0; i < comment->comments; ++i) { - NSMutableData *commentField = [NSMutableData dataWithBytes:comment->user_comments[i] length:comment->comment_lengths[i]]; - [commentField appendBytes:&nullByte length:1]; - NSString *commentString = [NSString stringWithUTF8String:[commentField bytes]]; - NSArray *splitFields = [commentString componentsSeparatedByString:@"="]; - if([splitFields count] == 2) { - NSString *name = [splitFields objectAtIndex:0]; - NSString *value = [splitFields objectAtIndex:1]; - name = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - name = [name lowercaseString]; - if([name isEqualToString:@"genre"]) { - _genre = value; - } else if([name isEqualToString:@"album"]) { - _album = value; - } else if([name isEqualToString:@"artist"]) { - _artist = value; - } else if([name isEqualToString:@"title"]) { - _title = value; + int tagCount = opus_tags_query_count(tags, [tag UTF8String]); + + for(int i = 0; i < tagCount; ++i) { + const char *value = opus_tags_query(tags, [tag UTF8String], i); + [tagStrings addObject:[NSString stringWithUTF8String:value]]; + } + + return [tagStrings componentsJoinedByString:@", "]; +} + +- (void)updateMetadata { + const OpusTags *tags = op_tags(opusRef, -1); + + if(tags) { + NSString *_artist = [self parseTag:@"artist" fromTags:tags]; + NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags]; + NSString *_album = [self parseTag:@"album" fromTags:tags]; + NSString *_title = [self parseTag:@"title" fromTags:tags]; + NSString *_genre = [self parseTag:@"genre" fromTags:tags]; + + NSString *_yearDate = [self parseTag:@"date" fromTags:tags]; + NSString *_yearYear = [self parseTag:@"year" fromTags:tags]; + + NSNumber *_year = @(0); + if([_yearDate length]) + _year = @([_yearDate intValue]); + else if([_yearYear length]) + _year = @([_yearYear intValue]); + + NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags]; + NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags]; + NSString *_trackTrack = [self parseTag:@"track" fromTags:tags]; + + NSNumber *_track = @(0); + if([_trackNumber length]) + _track = @([_trackNumber intValue]); + else if([_trackNum length]) + _track = @([_trackNum intValue]); + else if([_trackTrack length]) + _track = @([_trackTrack intValue]); + + NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags]; + NSString *_discNum = [self parseTag:@"discnum" fromTags:tags]; + NSString *_discDisc = [self parseTag:@"disc" fromTags:tags]; + + NSNumber *_disc = @(0); + if([_discNumber length]) + _disc = @([_discNumber intValue]); + else if([_discNum length]) + _disc = @([_discNum intValue]); + else if([_discDisc length]) + _disc = @([_discDisc intValue]); + + NSData *_albumArt = [NSData data]; + + size_t count = opus_tags_query_count(tags, "METADATA_BLOCK_PICTURE"); + if(count) { + const char *pictureTag = opus_tags_query(tags, "METADATA_BLOCK_PICTURE", 0); + OpusPictureTag _pic = { 0 }; + if(opus_picture_tag_parse(&_pic, pictureTag) >= 0) { + if(_pic.format == OP_PIC_FORMAT_PNG || + _pic.format == OP_PIC_FORMAT_JPEG || + _pic.format == OP_PIC_FORMAT_GIF) { + _albumArt = [NSData dataWithBytes:_pic.data length:_pic.data_length]; } + opus_picture_tag_clear(&_pic); } } - if(![source seekable] && - (![_genre isEqual:genre] || - ![_album isEqual:album] || - ![_artist isEqual:artist] || - ![_title isEqual:title])) { - genre = _genre; - album = _album; + if(![_artist isEqual:artist] || + ![_albumartist isEqual:albumartist] || + ![_album isEqual:album] || + ![_title isEqual:title] || + ![_genre isEqual:genre] || + ![_year isEqual:year] || + ![_track isEqual:year] || + ![_disc isEqual:disc] || + ![_albumArt isEqual:albumArt]) { artist = _artist; + albumartist = _albumartist; + album = _album; title = _title; + genre = _genre; + year = _year; + track = _track; + disc = _disc; + albumArt = _albumArt; + [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; } @@ -277,15 +332,15 @@ opus_int64 sourceTell(void *_stream) { @"totalFrames": [NSNumber numberWithDouble:totalFrames], @"bitrate": [NSNumber numberWithInt:bitrate], @"seekable": [NSNumber numberWithBool:([source seekable] && seekable)], - @"replayGainTrackGain": [NSNumber numberWithFloat:track_gain], - @"replayGainAlbumGain": [NSNumber numberWithFloat:album_gain], + @"replayGainAlbumGain": @(replayGainAlbumGain), + @"replayGainTrackGain": @(replayGainTrackGain), @"codec": @"Opus", @"endian": @"host", @"encoding": @"lossy" }; } - (NSDictionary *)metadata { - return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title }; + return @{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt }; } + (NSArray *)fileTypes { diff --git a/Plugins/Vorbis/VorbisDecoder.h b/Plugins/Vorbis/VorbisDecoder.h index 704f14ba9..a915d4be9 100644 --- a/Plugins/Vorbis/VorbisDecoder.h +++ b/Plugins/Vorbis/VorbisDecoder.h @@ -32,10 +32,20 @@ float frequency; long totalFrames; - NSString *genre; - NSString *album; NSString *artist; + NSString *albumartist; + NSString *album; NSString *title; + NSString *genre; + NSNumber *year; + NSNumber *track; + NSNumber *disc; + float replayGainAlbumGain; + float replayGainAlbumPeak; + float replayGainTrackGain; + float replayGainTrackPeak; + + NSData *albumArt; } @end diff --git a/Plugins/Vorbis/VorbisDecoder.m b/Plugins/Vorbis/VorbisDecoder.m index 133c87d10..0d911d8b3 100644 --- a/Plugins/Vorbis/VorbisDecoder.m +++ b/Plugins/Vorbis/VorbisDecoder.m @@ -12,6 +12,8 @@ #import "HTTPSource.h" +#import "picture.h" + @implementation VorbisDecoder static const int MAXCHANNELS = 8; @@ -97,57 +99,109 @@ long sourceTell(void *datasource) { [self willChangeValueForKey:@"properties"]; [self didChangeValueForKey:@"properties"]; - genre = @""; - album = @""; artist = @""; + albumartist = @""; + album = @""; title = @""; + genre = @""; + year = @(0); + track = @(0); + disc = @(0); + albumArt = [NSData data]; [self updateMetadata]; return YES; } +- (NSString *)parseTag:(NSString *)tag fromTags:(vorbis_comment *)tags { + NSMutableArray *tagStrings = [[NSMutableArray alloc] init]; + + int tagCount = vorbis_comment_query_count(tags, [tag UTF8String]); + + for(int i = 0; i < tagCount; ++i) { + const char *value = vorbis_comment_query(tags, [tag UTF8String], i); + [tagStrings addObject:[NSString stringWithUTF8String:value]]; + } + + return [tagStrings componentsJoinedByString:@", "]; +} + - (void)updateMetadata { - if([source seekable]) return; + vorbis_comment *tags = ov_comment(&vorbisRef, -1); - const vorbis_comment *comment = ov_comment(&vorbisRef, -1); + if(tags) { + NSString *_artist = [self parseTag:@"artist" fromTags:tags]; + NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags]; + NSString *_album = [self parseTag:@"album" fromTags:tags]; + NSString *_title = [self parseTag:@"title" fromTags:tags]; + NSString *_genre = [self parseTag:@"genre" fromTags:tags]; - if(comment) { - uint8_t nullByte = '\0'; - NSString *_genre = genre; - NSString *_album = album; - NSString *_artist = artist; - NSString *_title = title; - for(int i = 0; i < comment->comments; ++i) { - NSMutableData *commentField = [NSMutableData dataWithBytes:comment->user_comments[i] length:comment->comment_lengths[i]]; - [commentField appendBytes:&nullByte length:1]; - NSString *commentString = [NSString stringWithUTF8String:[commentField bytes]]; - NSArray *splitFields = [commentString componentsSeparatedByString:@"="]; - if([splitFields count] == 2) { - NSString *name = [splitFields objectAtIndex:0]; - NSString *value = [splitFields objectAtIndex:1]; - name = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - name = [name lowercaseString]; - if([name isEqualToString:@"genre"]) { - _genre = value; - } else if([name isEqualToString:@"album"]) { - _album = value; - } else if([name isEqualToString:@"artist"]) { - _artist = value; - } else if([name isEqualToString:@"title"]) { - _title = value; + NSString *_yearDate = [self parseTag:@"date" fromTags:tags]; + NSString *_yearYear = [self parseTag:@"year" fromTags:tags]; + + NSNumber *_year = @(0); + if([_yearDate length]) + _year = @([_yearDate intValue]); + else if([_yearYear length]) + _year = @([_yearYear intValue]); + + NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags]; + NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags]; + NSString *_trackTrack = [self parseTag:@"track" fromTags:tags]; + + NSNumber *_track = @(0); + if([_trackNumber length]) + _track = @([_trackNumber intValue]); + else if([_trackNum length]) + _track = @([_trackNum intValue]); + else if([_trackTrack length]) + _track = @([_trackTrack intValue]); + + NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags]; + NSString *_discNum = [self parseTag:@"discnum" fromTags:tags]; + NSString *_discDisc = [self parseTag:@"disc" fromTags:tags]; + + NSNumber *_disc = @(0); + if([_discNumber length]) + _disc = @([_discNumber intValue]); + else if([_discNum length]) + _disc = @([_discNum intValue]); + else if([_discDisc length]) + _disc = @([_discDisc intValue]); + + NSData *_albumArt = [NSData data]; + + size_t count = vorbis_comment_query_count(tags, "METADATA_BLOCK_PICTURE"); + if(count) { + const char *pictureTag = vorbis_comment_query(tags, "METADATA_BLOCK_PICTURE", 0); + flac_picture_t *picture = flac_picture_parse_from_base64(pictureTag); + if(picture) { + if(picture->binary && picture->binary_length) { + _albumArt = [NSData dataWithBytes:picture->binary length:picture->binary_length]; } + flac_picture_free(picture); } } - if(![_genre isEqual:genre] || + if(![_artist isEqual:artist] || + ![_albumartist isEqual:albumartist] || ![_album isEqual:album] || - ![_artist isEqual:artist] || - ![_title isEqual:title]) { - genre = _genre; - album = _album; + ![_title isEqual:title] || + ![_genre isEqual:genre] || + ![_year isEqual:year] || + ![_track isEqual:year] || + ![_disc isEqual:disc] || + ![_albumArt isEqual:albumArt]) { artist = _artist; + albumartist = _albumartist; + album = _album; title = _title; + genre = _genre; + year = _year; + track = _track; + disc = _disc; + albumArt = _albumArt; + [self willChangeValueForKey:@"metadata"]; [self didChangeValueForKey:@"metadata"]; } @@ -265,7 +319,7 @@ long sourceTell(void *datasource) { } - (NSDictionary *)metadata { - return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title }; + return @{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt }; } + (NSArray *)fileTypes { diff --git a/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj b/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj index c51a167d3..5cfedb747 100644 --- a/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj +++ b/Plugins/Vorbis/VorbisPlugin.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 17C93D360B8FDA66008627D6 /* VorbisDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 17C93D340B8FDA66008627D6 /* VorbisDecoder.m */; }; 833765A320E4EF1F007287F6 /* Vorbis.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83172ABB20E4EF0100751437 /* Vorbis.framework */; }; + 83AA661627B7FAFC0098D4B8 /* picture.c in Sources */ = {isa = PBXBuildFile; fileRef = 83AA661127B7FAFC0098D4B8 /* picture.c */; }; + 83AA661727B7FAFD0098D4B8 /* base64.c in Sources */ = {isa = PBXBuildFile; fileRef = 83AA661227B7FAFC0098D4B8 /* base64.c */; }; 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; /* End PBXBuildFile section */ @@ -61,6 +63,10 @@ 32DBCF630370AF2F00C91783 /* VorbisPlugin_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VorbisPlugin_Prefix.pch; sourceTree = ""; }; 8356BD1C27B46A2D0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = ""; }; 8384913418081A3900E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = ""; }; + 83AA660E27B7FAFC0098D4B8 /* picture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = picture.h; sourceTree = ""; }; + 83AA660F27B7FAFC0098D4B8 /* base64.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = base64.h; sourceTree = ""; }; + 83AA661127B7FAFC0098D4B8 /* picture.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = picture.c; sourceTree = ""; }; + 83AA661227B7FAFC0098D4B8 /* base64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = base64.c; sourceTree = ""; }; 8D5B49B6048680CD000E48DA /* VorbisPlugin.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VorbisPlugin.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D2F7E65807B2D6F200F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; @@ -82,6 +88,7 @@ 089C166AFE841209C02AAC07 /* Vorbis */ = { isa = PBXGroup; children = ( + 83AA660C27B7FAFC0098D4B8 /* vorbis-tools */, 08FB77AFFE84173DC02AAC07 /* Classes */, 32C88E010371C26100C91783 /* Other Sources */, 089C167CFE841241C02AAC07 /* Resources */, @@ -174,6 +181,33 @@ name = Frameworks; sourceTree = ""; }; + 83AA660C27B7FAFC0098D4B8 /* vorbis-tools */ = { + isa = PBXGroup; + children = ( + 83AA660D27B7FAFC0098D4B8 /* include */, + 83AA661027B7FAFC0098D4B8 /* share */, + ); + path = "vorbis-tools"; + sourceTree = ""; + }; + 83AA660D27B7FAFC0098D4B8 /* include */ = { + isa = PBXGroup; + children = ( + 83AA660E27B7FAFC0098D4B8 /* picture.h */, + 83AA660F27B7FAFC0098D4B8 /* base64.h */, + ); + path = include; + sourceTree = ""; + }; + 83AA661027B7FAFC0098D4B8 /* share */ = { + isa = PBXGroup; + children = ( + 83AA661127B7FAFC0098D4B8 /* picture.c */, + 83AA661227B7FAFC0098D4B8 /* base64.c */, + ); + path = share; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -298,6 +332,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 83AA661627B7FAFC0098D4B8 /* picture.c in Sources */, + 83AA661727B7FAFD0098D4B8 /* base64.c in Sources */, 17C93D360B8FDA66008627D6 /* VorbisDecoder.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Plugins/Vorbis/vorbis-tools/include/base64.h b/Plugins/Vorbis/vorbis-tools/include/base64.h new file mode 100644 index 000000000..9d2b9326a --- /dev/null +++ b/Plugins/Vorbis/vorbis-tools/include/base64.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 Philipp Schafft + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef __BASE64_H__ +#define __BASE64_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// returns 0 on success. +int base64_decode(const char *in, void **out, size_t *len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Plugins/Vorbis/vorbis-tools/include/picture.h b/Plugins/Vorbis/vorbis-tools/include/picture.h new file mode 100644 index 000000000..0f552f81f --- /dev/null +++ b/Plugins/Vorbis/vorbis-tools/include/picture.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Philipp Schafft + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef __PICTURE_H__ +#define __PICTURE_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef enum { + FLAC_PICTURE_INVALID = -1, + FLAC_PICTURE_OTHER = 0, + FLAC_PICTURE_FILE_ICON = 1, + FLAC_PICTURE_OTHER_FILE_ICON = 2, + FLAC_PICTURE_COVER_FRONT = 3, + FLAC_PICTURE_COVER_BACK = 4, + FLAC_PICTURE_LEAFLET_PAGE = 5, + FLAC_PICTURE_MEDIA = 6, + FLAC_PICTURE_LEAD = 7, + FLAC_PICTURE_ARTIST = 8, + FLAC_PICTURE_CONDUCTOR = 9, + FLAC_PICTURE_BAND = 10, + FLAC_PICTURE_COMPOSER = 11, + FLAC_PICTURE_LYRICIST = 12, + FLAC_PICTURE_RECORDING_LOCATION = 13, + FLAC_PICTURE_DURING_RECORDING = 14, + FLAC_PICTURE_DURING_PREFORMANCE = 15, + FLAC_PICTURE_SCREEN_CAPTURE = 16, + FLAC_PICTURE_A_BRIGHT_COLOURED_FISH = 17, + FLAC_PICTURE_ILLUSTRATION = 18, + FLAC_PICTURE_BAND_LOGOTYPE = 19, + FLAC_PICTURE_PUBLISHER_LOGOTYPE = 20 +} flac_picture_type; + +typedef struct { + flac_picture_type type; + const char *media_type; + const char *description; + unsigned int width; + unsigned int height; + unsigned int depth; + unsigned int colors; + const void *binary; + size_t binary_length; + const char *uri; + void *private_data; + size_t private_data_length; +} flac_picture_t; + +const char *flac_picture_type_string(flac_picture_type type); + +flac_picture_t *flac_picture_parse_from_base64(const char *str); +flac_picture_t *flac_picture_parse_from_blob(const void *in, size_t len); + +void flac_picture_free(flac_picture_t *picture); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Plugins/Vorbis/vorbis-tools/share/base64.c b/Plugins/Vorbis/vorbis-tools/share/base64.c new file mode 100644 index 000000000..057b543b8 --- /dev/null +++ b/Plugins/Vorbis/vorbis-tools/share/base64.c @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2002 Michael Smith + * Copyright (C) 2015-2021 Philipp Schafft + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include + +#include "base64.h" + +static const signed char base64decode[256] = { + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -1, -2, -2, + -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2, + -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, + -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 +}; + +int base64_decode(const char *in, void **out, size_t *len) { + const unsigned char *input = (const unsigned char *)in; + size_t todo = strlen(in); + char *output; + size_t opp = 0; // output pointer + + if(todo < 4 || (todo % 4) != 0) + return -1; + + output = calloc(1, todo * 3 / 4 + 5); + if(!output) + return -1; + + while(todo) { + signed char vals[4]; + size_t i; + + for(i = 0; i < (sizeof(vals) / sizeof(*vals)); i++) + vals[i] = base64decode[*input++]; + + if(vals[0] < 0 || vals[1] < 0 || vals[2] < -1 || vals[3] < -1) { + todo -= 4; + continue; + } + + output[opp++] = vals[0] << 2 | vals[1] >> 4; + + /* vals[3] and (if that is) vals[2] can be '=' as padding, which is + * looked up in the base64decode table as '-1'. Check for this case, + * and output zero-terminators instead of characters if we've got + * padding. */ + if(vals[2] >= 0) { + output[opp++] = ((vals[1] & 0x0F) << 4) | (vals[2] >> 2); + } else { + break; + } + + if(vals[3] >= 0) { + output[opp++] = ((vals[2] & 0x03) << 6) | (vals[3]); + } else { + break; + } + + todo -= 4; + } + output[opp++] = 0; + + *out = output; + *len = opp - 1; + + return 0; +} diff --git a/Plugins/Vorbis/vorbis-tools/share/picture.c b/Plugins/Vorbis/vorbis-tools/share/picture.c new file mode 100644 index 000000000..4a5f18ba2 --- /dev/null +++ b/Plugins/Vorbis/vorbis-tools/share/picture.c @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2021 Philipp Schafft + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include +#include + +#include "base64.h" +#include "picture.h" + +const char *flac_picture_type_string(flac_picture_type type) { + switch(type) { + case FLAC_PICTURE_OTHER: + return "Other"; + break; + case FLAC_PICTURE_FILE_ICON: + return "32x32 pixels file icon (PNG)"; + break; + case FLAC_PICTURE_OTHER_FILE_ICON: + return "Other file icon"; + break; + case FLAC_PICTURE_COVER_FRONT: + return "Cover (front)"; + break; + case FLAC_PICTURE_COVER_BACK: + return "Cover (back)"; + break; + case FLAC_PICTURE_LEAFLET_PAGE: + return "Leaflet page"; + break; + case FLAC_PICTURE_MEDIA: + return "Media"; + break; + case FLAC_PICTURE_LEAD: + return "Lead artist/lead performer/soloist"; + break; + case FLAC_PICTURE_ARTIST: + return "Artist/performer"; + break; + case FLAC_PICTURE_CONDUCTOR: + return "Conductor"; + break; + case FLAC_PICTURE_BAND: + return "Band/Orchestra"; + break; + case FLAC_PICTURE_COMPOSER: + return "Composer"; + break; + case FLAC_PICTURE_LYRICIST: + return "Lyricist/text writer"; + break; + case FLAC_PICTURE_RECORDING_LOCATION: + return "Recording Location"; + break; + case FLAC_PICTURE_DURING_RECORDING: + return "During recording"; + break; + case FLAC_PICTURE_DURING_PREFORMANCE: + return "During performance"; + break; + case FLAC_PICTURE_SCREEN_CAPTURE: + return "Movie/video screen capture"; + break; + case FLAC_PICTURE_A_BRIGHT_COLOURED_FISH: + return "A bright coloured fish"; + break; + case FLAC_PICTURE_ILLUSTRATION: + return "Illustration"; + break; + case FLAC_PICTURE_BAND_LOGOTYPE: + return "Band/artist logotype"; + break; + case FLAC_PICTURE_PUBLISHER_LOGOTYPE: + return "Publisher/Studio logotype"; + break; + default: + return ""; + break; + } +} + +static uint32_t read32be(unsigned char *buf) { + uint32_t ret = 0; + + ret = (ret << 8) | *buf++; + ret = (ret << 8) | *buf++; + ret = (ret << 8) | *buf++; + ret = (ret << 8) | *buf++; + + return ret; +} + +static flac_picture_t *flac_picture_parse_eat(void *data, size_t len) { + size_t expected_length = 32; // 8*32 bit + size_t offset = 0; + flac_picture_t *ret; + uint32_t tmp; + + if(len < expected_length) + return NULL; + + ret = calloc(1, sizeof(*ret)); + if(!ret) + return NULL; + + ret->private_data = data; + ret->private_data_length = len; + + ret->type = read32be(data); + + /* + const char *media_type; + const char *description; + unsigned int width; + unsigned int height; + unsigned int depth; + unsigned int colors; + const void *binary; + size_t binary_length; + const char *uri; + */ + tmp = read32be(data + 4); + expected_length += tmp; + if(len < expected_length) { + free(ret); + return NULL; + } + + ret->media_type = data + 8; + offset = 8 + tmp; + tmp = read32be(data + offset); + expected_length += tmp; + if(len < expected_length) { + free(ret); + return NULL; + } + + *(char *)(data + offset) = 0; + offset += 4; + ret->description = data + offset; + offset += tmp; + ret->width = read32be(data + offset); + *(char *)(data + offset) = 0; + offset += 4; + ret->height = read32be(data + offset); + offset += 4; + ret->depth = read32be(data + offset); + offset += 4; + ret->colors = read32be(data + offset); + offset += 4; + ret->binary_length = read32be(data + offset); + expected_length += ret->binary_length; + if(len < expected_length) { + free(ret); + return NULL; + } + offset += 4; + ret->binary = data + offset; + + if(strcmp(ret->media_type, "-->") == 0) { + // Note: it is ensured ret->binary[ret->binary_length] == 0. + ret->media_type = NULL; + ret->uri = ret->binary; + ret->binary = NULL; + ret->binary_length = 0; + } + + return ret; +} + +flac_picture_t *flac_picture_parse_from_base64(const char *str) { + flac_picture_t *ret; + void *data; + size_t len; + + if(!str || !*str) + return NULL; + + if(base64_decode(str, &data, &len) != 0) + return NULL; + + ret = flac_picture_parse_eat(data, len); + + if(!ret) { + free(data); + return NULL; + } + + return ret; +} + +flac_picture_t *flac_picture_parse_from_blob(const void *in, size_t len) { + flac_picture_t *ret; + void *data; + + if(!in || !len) + return NULL; + + data = calloc(1, len + 1); + if(!data) + return NULL; + + memcpy(data, in, len); + + ret = flac_picture_parse_eat(data, len); + + if(!ret) { + free(data); + return NULL; + } + + return ret; +} + +void flac_picture_free(flac_picture_t *picture) { + if(!picture) + return; + + free(picture->private_data); + free(picture); +}