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 <kode54@gmail.com>
CQTexperiment
Christopher Snowhill 2022-02-12 07:16:59 -08:00
parent 1df166b060
commit a05d4537c1
16 changed files with 1503 additions and 125 deletions

View File

@ -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]];
}

View File

@ -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;

View File

@ -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;

View File

@ -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 = "<group>"; };
8356BD1927B3CCBB0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
8384912D180816C900E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
83AA660A27B7DAE40098D4B8 /* cuesheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = cuesheet.m; sourceTree = "<group>"; };
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 = "<group>"; };
D2F7E65807B2D6F200F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
@ -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;
};

View File

@ -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<CogSource>)s;

View File

@ -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 {

645
Plugins/Flac/cuesheet.m Normal file
View File

@ -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 <Foundation/Foundation.h>
#include <stdlib.h>
#include <string.h>
#include <flac/FLAC_assert.h>
#include <flac/all.h>
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:@""];
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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 = "<group>"; };
8356BD1C27B46A2D0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
8384913418081A3900E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
83AA660E27B7FAFC0098D4B8 /* picture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = picture.h; sourceTree = "<group>"; };
83AA660F27B7FAFC0098D4B8 /* base64.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = base64.h; sourceTree = "<group>"; };
83AA661127B7FAFC0098D4B8 /* picture.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = picture.c; sourceTree = "<group>"; };
83AA661227B7FAFC0098D4B8 /* base64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = base64.c; sourceTree = "<group>"; };
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 = "<group>"; };
D2F7E65807B2D6F200F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
@ -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 = "<group>";
};
83AA660C27B7FAFC0098D4B8 /* vorbis-tools */ = {
isa = PBXGroup;
children = (
83AA660D27B7FAFC0098D4B8 /* include */,
83AA661027B7FAFC0098D4B8 /* share */,
);
path = "vorbis-tools";
sourceTree = "<group>";
};
83AA660D27B7FAFC0098D4B8 /* include */ = {
isa = PBXGroup;
children = (
83AA660E27B7FAFC0098D4B8 /* picture.h */,
83AA660F27B7FAFC0098D4B8 /* base64.h */,
);
path = include;
sourceTree = "<group>";
};
83AA661027B7FAFC0098D4B8 /* share */ = {
isa = PBXGroup;
children = (
83AA661127B7FAFC0098D4B8 /* picture.c */,
83AA661227B7FAFC0098D4B8 /* base64.c */,
);
path = share;
sourceTree = "<group>";
};
/* 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;

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2021 Philipp Schafft <lion@lion.leolix.org>
*
* 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 <stddef.h>
// returns 0 on success.
int base64_decode(const char *in, void **out, size_t *len);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 Philipp Schafft <lion@lion.leolix.org>
*
* 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 <stddef.h>
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

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2002 Michael Smith <msmith@xiph.org>
* Copyright (C) 2015-2021 Philipp Schafft <lion@lion.leolix.org>
*
* 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 <stdlib.h>
#include <string.h>
#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;
}

View File

@ -0,0 +1,236 @@
/*
* Copyright (C) 2021 Philipp Schafft <lion@lion.leolix.org>
*
* 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 <stdint.h>
#include <stdlib.h>
#include <string.h>
#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 "<unknown>";
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);
}