This bug prevented zero length or unknown length files, such as FLAC files with no sample count in the header, or audio streams, from playing properly, and clipped their output to the 0 samples indicated by the field. Now it will simply allow wrapped files to decode until they stop producing output. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
278 lines
7.3 KiB
Objective-C
278 lines
7.3 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 "CueSheetTrack.h"
|
|
#import "CueSheetContainer.h"
|
|
|
|
#import "Logging.h"
|
|
|
|
@implementation CueSheetDecoder
|
|
|
|
+ (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
|
|
[properties setObject:[NSNumber numberWithLong:(trackEnd - trackStart)] forKey:@"totalFrames"];
|
|
|
|
return properties;
|
|
}
|
|
|
|
- (BOOL)open:(id<CogSource>)s
|
|
{
|
|
if (![[s url] isFileURL]) {
|
|
return NO;
|
|
}
|
|
|
|
NSURL *url = [s url];
|
|
|
|
embedded = NO;
|
|
cuesheet = nil;
|
|
NSDictionary * fileMetadata;
|
|
|
|
noFragment = NO;
|
|
|
|
NSString *ext = [url pathExtension];
|
|
if ([ext caseInsensitiveCompare:@"cue"] != NSOrderedSame)
|
|
{
|
|
// Embedded cuesheet check
|
|
fileMetadata = [NSClassFromString(@"AudioMetadataReader") metadataForURL:url skipCue:YES];
|
|
NSString * sheet = [fileMetadata objectForKey:@"cuesheet"];
|
|
if ([sheet length])
|
|
{
|
|
cuesheet = [CueSheet cueSheetWithString:sheet 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];
|
|
|
|
NSURL *trackUrl = (embedded) ? baseURL : [track url];
|
|
|
|
//Kind of a hackish way of accessing outside classes.
|
|
source = [NSClassFromString(@"AudioSource") audioSourceForURL:trackUrl];
|
|
|
|
if (![source open:trackUrl]) {
|
|
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];
|
|
}
|
|
|
|
[self seek: 0];
|
|
|
|
//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
|
|
source = [NSClassFromString(@"AudioSource") audioSourceForURL:url];
|
|
|
|
if (![source open: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;
|
|
}
|
|
|
|
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];
|
|
|
|
[self seek: 0];
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)close {
|
|
if (decoder) {
|
|
[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;
|
|
|
|
//Same file, just next track...this may be unnecessary since frame-based decoding is done now...
|
|
if (embedded || ([[[track url] path] isEqualToString:[url path]] && [[[track url] host] isEqualToString:[url host]] && [[url fragment] intValue] == [[track track] intValue] + 1)) {
|
|
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];
|
|
|
|
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];
|
|
}
|
|
|
|
if (embedded)
|
|
[self seek:0];
|
|
|
|
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;
|
|
}
|
|
|
|
frame += trackStart;
|
|
|
|
framePosition = [decoder seek:frame];
|
|
|
|
return framePosition - trackStart;
|
|
}
|
|
|
|
- (int)readAudio:(void *)buf frames:(UInt32)frames
|
|
{
|
|
if (!noFragment && framePosition + frames > trackEnd) {
|
|
frames = (UInt32)(trackEnd - framePosition);
|
|
}
|
|
|
|
if (!frames)
|
|
{
|
|
DLog(@"Returning 0");
|
|
return 0;
|
|
}
|
|
|
|
int n = [decoder readAudio:buf frames:frames];
|
|
|
|
framePosition += n;
|
|
|
|
return n;
|
|
}
|
|
|
|
|
|
@end
|