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
parent
1df166b060
commit
a05d4537c1
|
@ -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]];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:@""];
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue