370 lines
8.8 KiB
Objective-C
370 lines
8.8 KiB
Objective-C
//
|
|
// CueSheetDecoder.m
|
|
// CueSheet
|
|
//
|
|
// Created by Zaphod Beeblebrox on 10/8/07.
|
|
// Copyright 2007 __MyCompanyName__. All rights reserved.
|
|
//
|
|
|
|
#import "CueSheetDecoder.h"
|
|
|
|
#import "CueSheet.h"
|
|
#import "CueSheetContainer.h"
|
|
#import "CueSheetMetadataReader.h"
|
|
|
|
#import "NSDictionary+Merge.h"
|
|
|
|
#import "Logging.h"
|
|
|
|
@implementation CueSheetDecoder
|
|
|
|
static void *kCueSheetDecoderContext = &kCueSheetDecoderContext;
|
|
|
|
+ (NSArray *)fileTypes {
|
|
return [CueSheetContainer fileTypes];
|
|
}
|
|
|
|
+ (NSArray *)mimeTypes {
|
|
return [CueSheetContainer mimeTypes];
|
|
}
|
|
|
|
+ (float)priority {
|
|
return 16.0f;
|
|
}
|
|
|
|
+ (NSArray *)fileTypeAssociations {
|
|
return @[
|
|
@[@"CUE Sheet File", @"cue.icns", @"cue"]
|
|
];
|
|
}
|
|
|
|
- (NSDictionary *)properties {
|
|
NSMutableDictionary *properties = [[decoder properties] mutableCopy];
|
|
|
|
// Need to alter length
|
|
if(!noFragment)
|
|
[properties setObject:@(trackEnd - trackStart) forKey:@"totalFrames"];
|
|
|
|
return [NSDictionary dictionaryWithDictionary:properties];
|
|
}
|
|
|
|
- (NSDictionary *)metadata {
|
|
NSDictionary *metadata = @{};
|
|
if(track != nil)
|
|
metadata = [CueSheetMetadataReader processDataForTrack:track];
|
|
if(decoder != nil)
|
|
return [metadata dictionaryByMergingWith:[decoder metadata]];
|
|
else
|
|
return metadata;
|
|
}
|
|
|
|
- (BOOL)open:(id<CogSource>)s {
|
|
if(![[s url] isFileURL]) {
|
|
return NO;
|
|
}
|
|
|
|
NSURL *url = [s url];
|
|
|
|
sourceURL = url;
|
|
|
|
embedded = NO;
|
|
cuesheet = nil;
|
|
NSDictionary *fileMetadata;
|
|
|
|
noFragment = NO;
|
|
observersAdded = NO;
|
|
|
|
NSString *ext = [url pathExtension];
|
|
if([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame) {
|
|
// Embedded cuesheet check
|
|
fileMetadata = [NSClassFromString(@"AudioMetadataReader") metadataForURL:url skipCue:YES];
|
|
|
|
source = s;
|
|
|
|
decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES];
|
|
|
|
[self registerObservers];
|
|
|
|
if(![decoder open:source]) {
|
|
ALog(@"Could not open cuesheet decoder");
|
|
return NO;
|
|
}
|
|
|
|
NSDictionary *alsoMetadata = [decoder metadata];
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(sheetString && [sheetString length]) {
|
|
cuesheet = [CueSheet cueSheetWithString:sheetString withFilename:[url path]];
|
|
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.
|
|
if(!embedded) {
|
|
source = [NSClassFromString(@"AudioSource") audioSourceForURL:[track url]];
|
|
|
|
if(![source open:[track url]]) {
|
|
ALog(@"Could not open cuesheet source");
|
|
return NO;
|
|
}
|
|
|
|
decoder = [NSClassFromString(@"AudioDecoder") audioDecoderForSource:source skipCue:YES];
|
|
|
|
if(![decoder open:source]) {
|
|
ALog(@"Could not open cuesheet decoder");
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
seekedToStart = NO;
|
|
|
|
// 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];
|
|
|
|
seekedToStart = NO;
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)registerObservers {
|
|
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;
|
|
}
|
|
}
|
|
|
|
- (void)removeObservers {
|
|
if(observersAdded) {
|
|
[decoder removeObserver:self forKeyPath:@"properties" context:kCueSheetDecoderContext];
|
|
[decoder removeObserver:self forKeyPath:@"metadata" context:kCueSheetDecoderContext];
|
|
observersAdded = NO;
|
|
}
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
ofObject:(id)object
|
|
change:(NSDictionary *)change
|
|
context:(void *)context {
|
|
if(context == kCueSheetDecoderContext) {
|
|
[self willChangeValueForKey:keyPath];
|
|
[self didChangeValueForKey:keyPath];
|
|
} else {
|
|
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
}
|
|
}
|
|
|
|
- (void)close {
|
|
if(decoder) {
|
|
[self removeObservers];
|
|
[decoder close];
|
|
decoder = nil;
|
|
}
|
|
|
|
source = nil;
|
|
cuesheet = nil;
|
|
track = nil;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self close];
|
|
}
|
|
|
|
- (BOOL)setTrack:(NSURL *)url {
|
|
// handling the file directly
|
|
if(noFragment)
|
|
return NO;
|
|
|
|
BOOL pathsAreFiles = NO;
|
|
|
|
if([url isFileURL] && [sourceURL isFileURL]) {
|
|
pathsAreFiles = YES;
|
|
}
|
|
|
|
// Same file, just next track...this may be unnecessary since frame-based decoding is done now...
|
|
if(embedded || ([[sourceURL path] isEqualToString:[url path]] && (pathsAreFiles || [[sourceURL host] isEqualToString:[url host]]))) {
|
|
NSArray *tracks = [cuesheet tracks];
|
|
|
|
int i;
|
|
for(i = 0; i < [tracks count]; i++) {
|
|
if([[[tracks objectAtIndex:i] track] isEqualToString:[url fragment]]) {
|
|
CueSheetTrack *_track = [tracks objectAtIndex:i];
|
|
|
|
if(![[_track url] isEqualTo:[track url]])
|
|
return NO;
|
|
|
|
track = _track;
|
|
|
|
float sampleRate = [[[decoder properties] objectForKey:@"sampleRate"] floatValue];
|
|
|
|
double _trackStart = [track time];
|
|
if(![track timeInSamples]) _trackStart *= sampleRate;
|
|
trackStart = _trackStart;
|
|
|
|
CueSheetTrack *nextTrack = nil;
|
|
if(i + 1 < [tracks count]) {
|
|
nextTrack = [tracks objectAtIndex:i + 1];
|
|
}
|
|
|
|
if(nextTrack && (embedded || [[[nextTrack url] absoluteString] isEqualToString:[[track url] absoluteString]])) {
|
|
double _trackEnd = [nextTrack time];
|
|
if(![nextTrack timeInSamples]) _trackEnd *= sampleRate;
|
|
trackEnd = _trackEnd;
|
|
} else {
|
|
trackEnd = [[[decoder properties] objectForKey:@"totalFrames"] longValue];
|
|
}
|
|
|
|
seekedToStart = NO;
|
|
|
|
DLog(@"CHANGING TRACK!");
|
|
return YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (long)seek:(long)frame {
|
|
if(!noFragment && frame > trackEnd - trackStart) {
|
|
// need a better way of returning fail.
|
|
return -1;
|
|
}
|
|
|
|
seekedToStart = YES;
|
|
|
|
frame += trackStart;
|
|
|
|
framePosition = [decoder seek:frame];
|
|
|
|
return framePosition - trackStart;
|
|
}
|
|
|
|
- (AudioChunk *)readAudio {
|
|
if(!seekedToStart) {
|
|
[self seek:0];
|
|
}
|
|
|
|
int frames = INT_MAX;
|
|
|
|
if(!noFragment && framePosition + frames > trackEnd) {
|
|
frames = (UInt32)(trackEnd - framePosition);
|
|
}
|
|
|
|
if(!frames) {
|
|
DLog(@"Returning 0");
|
|
return nil;
|
|
}
|
|
|
|
AudioChunk *chunk = [decoder readAudio];
|
|
|
|
size_t n = chunk.frameCount;
|
|
if(n > frames) {
|
|
[chunk setFrameCount:frames];
|
|
}
|
|
|
|
framePosition += chunk.frameCount;
|
|
|
|
return chunk;
|
|
}
|
|
|
|
- (BOOL)isSilence {
|
|
if([decoder respondsToSelector:@selector(isSilence)])
|
|
return [decoder isSilence];
|
|
return NO;
|
|
}
|
|
|
|
@end
|