cog/Plugins/CueSheet/CueSheetDecoder.m

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