2007-10-09 02:25:40 +00:00
|
|
|
//
|
|
|
|
// CueSheetDecoder.m
|
|
|
|
// CueSheet
|
|
|
|
//
|
|
|
|
// Created by Zaphod Beeblebrox on 10/8/07.
|
|
|
|
// Copyright 2007 __MyCompanyName__. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#import "CueSheetDecoder.h"
|
|
|
|
|
|
|
|
#import "CueSheet.h"
|
2007-10-10 01:59:25 +00:00
|
|
|
#import "CueSheetContainer.h"
|
2022-06-12 07:55:37 +00:00
|
|
|
#import "CueSheetMetadataReader.h"
|
|
|
|
|
|
|
|
#import "NSDictionary+Merge.h"
|
2007-10-09 02:25:40 +00:00
|
|
|
|
2013-10-11 12:03:55 +00:00
|
|
|
#import "Logging.h"
|
|
|
|
|
2007-10-09 02:25:40 +00:00
|
|
|
@implementation CueSheetDecoder
|
|
|
|
|
2022-06-15 23:47:43 +00:00
|
|
|
static void *kCueSheetDecoderContext = &kCueSheetDecoderContext;
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)fileTypes {
|
2007-10-10 01:59:25 +00:00
|
|
|
return [CueSheetContainer fileTypes];
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)mimeTypes {
|
2007-10-14 18:56:23 +00:00
|
|
|
return [CueSheetContainer mimeTypes];
|
2007-10-14 18:39:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (float)priority {
|
|
|
|
return 16.0f;
|
Implemented support for multiple decoders per file name extension, with a floating point priority control per interface. In the event that more than one input is registered to a given extension, and we match that extension, it will be passed off to an instance of the multi-decoder wrapper, which will try opening the file with all of the decoders in order of priority, until either one of them accepts it, or all of them have failed. This paves the way for adding a VGMSTREAM input, so I can give it a very low priority, since it has several formats that are verified by file name extension only. All current inputs have been given a priority of 1.0, except for CoreAudio, which was given a priority of 0.5, because it contains an MP3 and AC3 decoders that I'd rather not use if I don't have to.
2013-10-21 17:54:11 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)fileTypeAssociations {
|
|
|
|
return @[
|
|
|
|
@[@"CUE Sheet File", @"cue.icns", @"cue"]
|
|
|
|
];
|
2022-01-18 11:06:03 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (NSDictionary *)properties {
|
2007-10-10 01:59:25 +00:00
|
|
|
NSMutableDictionary *properties = [[decoder properties] mutableCopy];
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
// Need to alter length
|
2022-02-12 15:16:59 +00:00
|
|
|
if(!noFragment)
|
2022-06-17 13:39:02 +00:00
|
|
|
[properties setObject:@(trackEnd - trackStart) forKey:@"totalFrames"];
|
2007-10-10 01:59:25 +00:00
|
|
|
|
2022-02-09 03:56:39 +00:00
|
|
|
return [NSDictionary dictionaryWithDictionary:properties];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSDictionary *)metadata {
|
2022-06-12 07:55:37 +00:00
|
|
|
NSDictionary *metadata = @{};
|
|
|
|
if(track != nil)
|
|
|
|
metadata = [CueSheetMetadataReader processDataForTrack:track];
|
2022-02-11 14:03:45 +00:00
|
|
|
if(decoder != nil)
|
2022-06-12 07:55:37 +00:00
|
|
|
return [metadata dictionaryByMergingWith:[decoder metadata]];
|
2022-02-11 14:03:45 +00:00
|
|
|
else
|
2022-06-12 07:55:37 +00:00
|
|
|
return metadata;
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (BOOL)open:(id<CogSource>)s {
|
|
|
|
if(![[s url] isFileURL]) {
|
2007-10-10 01:59:25 +00:00
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSURL *url = [s url];
|
|
|
|
|
2022-02-11 14:03:45 +00:00
|
|
|
sourceURL = url;
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
embedded = NO;
|
|
|
|
cuesheet = nil;
|
|
|
|
NSDictionary *fileMetadata;
|
|
|
|
|
|
|
|
noFragment = NO;
|
2022-02-08 03:18:45 +00:00
|
|
|
observersAdded = NO;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
NSString *ext = [url pathExtension];
|
|
|
|
if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) {
|
|
|
|
// Embedded cuesheet check
|
|
|
|
fileMetadata = [NSClassFromString(@"AudioMetadataReader") metadataForURL:url skipCue:YES];
|
2022-02-12 15:16:59 +00:00
|
|
|
|
|
|
|
source = s;
|
|
|
|
|
|
|
|
decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES];
|
|
|
|
|
2022-07-10 21:57:22 +00:00
|
|
|
[self registerObservers];
|
|
|
|
|
2022-02-12 15:16:59 +00:00
|
|
|
if(![decoder open:source]) {
|
|
|
|
ALog(@"Could not open cuesheet decoder");
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSDictionary *alsoMetadata = [decoder metadata];
|
|
|
|
|
2022-10-16 21:59:19 +00:00
|
|
|
id sheet = [fileMetadata objectForKey:@"cuesheet"];
|
|
|
|
NSString *sheetString = nil;
|
|
|
|
if(sheet) {
|
|
|
|
if([sheet isKindOfClass:[NSArray class]]) {
|
|
|
|
NSArray *sheetContainer = sheet;
|
|
|
|
if([sheetContainer count]) {
|
|
|
|
sheetString = sheetContainer[0];
|
|
|
|
}
|
|
|
|
} else if([sheet isKindOfClass:[NSString class]]) {
|
|
|
|
sheetString = sheet;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!sheetString || ![sheetString length]) {
|
|
|
|
sheet = [alsoMetadata objectForKey:@"cuesheet"];
|
|
|
|
if(sheet) {
|
|
|
|
if([sheet isKindOfClass:[NSArray class]]) {
|
|
|
|
NSArray *sheetContainer = sheet;
|
|
|
|
if([sheetContainer count]) {
|
|
|
|
sheetString = sheetContainer[0];
|
|
|
|
}
|
|
|
|
} else if([sheet isKindOfClass:[NSString class]]) {
|
|
|
|
sheetString = sheet;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-12 15:16:59 +00:00
|
|
|
|
2022-10-16 21:59:19 +00:00
|
|
|
if(sheetString && [sheetString length]) {
|
|
|
|
cuesheet = [CueSheet cueSheetWithString:sheetString withFilename:[url path]];
|
2022-02-07 05:49:27 +00:00
|
|
|
embedded = YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
baseURL = url;
|
|
|
|
|
|
|
|
NSString *fragment = [url fragment];
|
|
|
|
if(!fragment || [fragment isEqualToString:@""])
|
|
|
|
noFragment = YES;
|
|
|
|
} else
|
|
|
|
cuesheet = [CueSheet cueSheetWithFile:[url path]];
|
|
|
|
|
|
|
|
if(!noFragment) {
|
|
|
|
NSArray *tracks = [cuesheet tracks];
|
|
|
|
int i;
|
|
|
|
for(i = 0; i < [tracks count]; i++) {
|
|
|
|
if([[[tracks objectAtIndex:i] track] isEqualToString:[url fragment]]) {
|
|
|
|
track = [tracks objectAtIndex:i];
|
|
|
|
|
|
|
|
// Kind of a hackish way of accessing outside classes.
|
2022-02-12 15:16:59 +00:00
|
|
|
if(!embedded) {
|
|
|
|
source = [NSClassFromString(@"AudioSource") audioSourceForURL:[track url]];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-02-12 15:16:59 +00:00
|
|
|
if(![source open:[track url]]) {
|
|
|
|
ALog(@"Could not open cuesheet source");
|
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-02-12 15:16:59 +00:00
|
|
|
decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-02-12 15:16:59 +00:00
|
|
|
if(![decoder open:source]) {
|
|
|
|
ALog(@"Could not open cuesheet decoder");
|
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
CueSheetTrack *nextTrack = nil;
|
|
|
|
if(i + 1 < [tracks count]) {
|
|
|
|
nextTrack = [tracks objectAtIndex:i + 1];
|
|
|
|
}
|
|
|
|
|
|
|
|
NSDictionary *properties = [decoder properties];
|
|
|
|
int bitsPerSample = [[properties objectForKey:@"bitsPerSample"] intValue];
|
|
|
|
int channels = [[properties objectForKey:@"channels"] intValue];
|
|
|
|
float sampleRate = [[properties objectForKey:@"sampleRate"] floatValue];
|
|
|
|
|
|
|
|
bytesPerFrame = (bitsPerSample / 8) * channels;
|
|
|
|
|
|
|
|
double _trackStart = [track time];
|
|
|
|
if(![track timeInSamples]) _trackStart *= sampleRate;
|
|
|
|
trackStart = _trackStart;
|
|
|
|
|
|
|
|
if(nextTrack && (embedded || ([[[nextTrack url] absoluteString] isEqualToString:[[track url] absoluteString]]))) {
|
|
|
|
double _trackEnd = [nextTrack time];
|
|
|
|
if(![nextTrack timeInSamples]) _trackEnd *= sampleRate;
|
|
|
|
trackEnd = _trackEnd;
|
|
|
|
} else {
|
|
|
|
trackEnd = [[properties objectForKey:@"totalFrames"] doubleValue];
|
|
|
|
}
|
|
|
|
|
2022-06-25 09:38:17 +00:00
|
|
|
seekedToStart = NO;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
// Note: Should register for observations of the decoder
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
|
|
[self didChangeValueForKey:@"properties"];
|
|
|
|
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Fix for embedded cuesheet handler parsing non-embedded files,
|
|
|
|
// or files that are already in the playlist without a fragment
|
|
|
|
NSDictionary *properties = [decoder properties];
|
|
|
|
int bitsPerSample = [[properties objectForKey:@"bitsPerSample"] intValue];
|
|
|
|
int channels = [[properties objectForKey:@"channels"] intValue];
|
|
|
|
|
|
|
|
bytesPerFrame = (bitsPerSample / 8) * channels;
|
|
|
|
|
|
|
|
trackStart = 0;
|
|
|
|
|
|
|
|
trackEnd = [[properties objectForKey:@"totalFrames"] doubleValue];
|
|
|
|
|
2022-06-25 09:38:17 +00:00
|
|
|
seekedToStart = NO;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
return YES;
|
|
|
|
}
|
2007-10-10 01:59:25 +00:00
|
|
|
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2022-02-08 03:18:45 +00:00
|
|
|
- (void)registerObservers {
|
2022-06-15 23:47:43 +00:00
|
|
|
if(!observersAdded) {
|
|
|
|
DLog(@"REGISTERING OBSERVERS");
|
|
|
|
[decoder addObserver:self
|
|
|
|
forKeyPath:@"properties"
|
|
|
|
options:(NSKeyValueObservingOptionNew)
|
|
|
|
context:kCueSheetDecoderContext];
|
|
|
|
|
|
|
|
[decoder addObserver:self
|
|
|
|
forKeyPath:@"metadata"
|
|
|
|
options:(NSKeyValueObservingOptionNew)
|
|
|
|
context:kCueSheetDecoderContext];
|
|
|
|
|
|
|
|
observersAdded = YES;
|
|
|
|
}
|
2022-02-08 03:18:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)removeObservers {
|
|
|
|
if(observersAdded) {
|
2022-06-15 23:47:43 +00:00
|
|
|
[decoder removeObserver:self forKeyPath:@"properties" context:kCueSheetDecoderContext];
|
|
|
|
[decoder removeObserver:self forKeyPath:@"metadata" context:kCueSheetDecoderContext];
|
2022-02-08 03:18:45 +00:00
|
|
|
observersAdded = NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
|
|
ofObject:(id)object
|
|
|
|
change:(NSDictionary *)change
|
|
|
|
context:(void *)context {
|
2022-06-15 23:47:43 +00:00
|
|
|
if(context == kCueSheetDecoderContext) {
|
|
|
|
[self willChangeValueForKey:keyPath];
|
|
|
|
[self didChangeValueForKey:keyPath];
|
|
|
|
} else {
|
|
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
|
|
}
|
2022-02-08 03:18:45 +00:00
|
|
|
}
|
|
|
|
|
2007-10-10 01:59:25 +00:00
|
|
|
- (void)close {
|
2022-02-07 05:49:27 +00:00
|
|
|
if(decoder) {
|
2022-02-08 03:18:45 +00:00
|
|
|
[self removeObservers];
|
2007-10-10 01:59:25 +00:00
|
|
|
[decoder close];
|
|
|
|
decoder = nil;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
source = nil;
|
|
|
|
cuesheet = nil;
|
|
|
|
track = nil;
|
2007-10-11 02:08:29 +00:00
|
|
|
}
|
|
|
|
|
2016-06-19 19:57:18 +00:00
|
|
|
- (void)dealloc {
|
2022-02-07 05:49:27 +00:00
|
|
|
[self close];
|
2016-06-19 19:57:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (BOOL)setTrack:(NSURL *)url {
|
|
|
|
// handling the file directly
|
|
|
|
if(noFragment)
|
|
|
|
return NO;
|
|
|
|
|
2022-02-11 14:03:45 +00:00
|
|
|
BOOL pathsAreFiles = NO;
|
|
|
|
|
|
|
|
if([url isFileURL] && [sourceURL isFileURL]) {
|
|
|
|
pathsAreFiles = YES;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
// Same file, just next track...this may be unnecessary since frame-based decoding is done now...
|
2022-02-11 14:03:45 +00:00
|
|
|
if(embedded || ([[sourceURL path] isEqualToString:[url path]] && (pathsAreFiles || [[sourceURL host] isEqualToString:[url host]]))) {
|
2007-10-11 02:08:29 +00:00
|
|
|
NSArray *tracks = [cuesheet tracks];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2007-10-11 02:08:29 +00:00
|
|
|
int i;
|
2022-02-07 05:49:27 +00:00
|
|
|
for(i = 0; i < [tracks count]; i++) {
|
|
|
|
if([[[tracks objectAtIndex:i] track] isEqualToString:[url fragment]]) {
|
2022-02-11 14:03:45 +00:00
|
|
|
CueSheetTrack *_track = [tracks objectAtIndex:i];
|
|
|
|
|
|
|
|
if(![[_track url] isEqualTo:[track url]])
|
|
|
|
return NO;
|
|
|
|
|
|
|
|
track = _track;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2008-03-30 15:43:28 +00:00
|
|
|
float sampleRate = [[[decoder properties] objectForKey:@"sampleRate"] floatValue];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-01-15 06:45:45 +00:00
|
|
|
double _trackStart = [track time];
|
2022-02-07 05:49:27 +00:00
|
|
|
if(![track timeInSamples]) _trackStart *= sampleRate;
|
|
|
|
trackStart = _trackStart;
|
|
|
|
|
2007-10-11 02:08:29 +00:00
|
|
|
CueSheetTrack *nextTrack = nil;
|
2022-02-07 05:49:27 +00:00
|
|
|
if(i + 1 < [tracks count]) {
|
2007-10-11 02:08:29 +00:00
|
|
|
nextTrack = [tracks objectAtIndex:i + 1];
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
if(nextTrack && (embedded || [[[nextTrack url] absoluteString] isEqualToString:[[track url] absoluteString]])) {
|
|
|
|
double _trackEnd = [nextTrack time];
|
|
|
|
if(![nextTrack timeInSamples]) _trackEnd *= sampleRate;
|
|
|
|
trackEnd = _trackEnd;
|
|
|
|
} else {
|
2008-03-30 15:43:28 +00:00
|
|
|
trackEnd = [[[decoder properties] objectForKey:@"totalFrames"] longValue];
|
2007-10-11 02:08:29 +00:00
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-06-25 09:38:17 +00:00
|
|
|
seekedToStart = NO;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"CHANGING TRACK!");
|
2007-10-11 02:08:29 +00:00
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2007-10-11 02:08:29 +00:00
|
|
|
return NO;
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (long)seek:(long)frame {
|
|
|
|
if(!noFragment && frame > trackEnd - trackStart) {
|
|
|
|
// need a better way of returning fail.
|
2008-03-30 15:43:28 +00:00
|
|
|
return -1;
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-06-25 09:38:17 +00:00
|
|
|
seekedToStart = YES;
|
|
|
|
|
2008-03-30 15:43:28 +00:00
|
|
|
frame += trackStart;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2008-03-30 15:43:28 +00:00
|
|
|
framePosition = [decoder seek:frame];
|
2007-10-11 02:08:29 +00:00
|
|
|
|
2022-01-20 06:09:29 +00:00
|
|
|
return framePosition - trackStart;
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
- (AudioChunk *)readAudio {
|
2022-06-25 09:38:17 +00:00
|
|
|
if(!seekedToStart) {
|
|
|
|
[self seek:0];
|
|
|
|
}
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
int frames = INT_MAX;
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
if(!noFragment && framePosition + frames > trackEnd) {
|
2017-09-18 02:21:48 +00:00
|
|
|
frames = (UInt32)(trackEnd - framePosition);
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
if(!frames) {
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Returning 0");
|
2022-07-10 22:14:47 +00:00
|
|
|
return nil;
|
2007-10-10 01:59:25 +00:00
|
|
|
}
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
AudioChunk *chunk = [decoder readAudio];
|
|
|
|
|
|
|
|
size_t n = chunk.frameCount;
|
|
|
|
if(n > frames) {
|
|
|
|
[chunk setFrameCount:frames];
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
framePosition += chunk.frameCount;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
return chunk;
|
2007-10-09 02:25:40 +00:00
|
|
|
}
|
|
|
|
|
2022-02-10 10:15:48 +00:00
|
|
|
- (BOOL)isSilence {
|
|
|
|
if([decoder respondsToSelector:@selector(isSilence)])
|
|
|
|
return [decoder isSilence];
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2007-10-09 02:25:40 +00:00
|
|
|
@end
|