2007-10-11 23:11:58 +00:00
|
|
|
//
|
|
|
|
// GameFile.m
|
|
|
|
// Cog
|
|
|
|
//
|
|
|
|
// Created by Vincent Spader on 5/29/06.
|
|
|
|
// Copyright 2006 Vincent Spader. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#import "GameDecoder.h"
|
|
|
|
|
2013-10-11 12:03:55 +00:00
|
|
|
#import "Logging.h"
|
|
|
|
|
2014-02-14 05:16:18 +00:00
|
|
|
#import "PlaylistController.h"
|
|
|
|
|
2007-10-11 23:11:58 +00:00
|
|
|
@implementation GameDecoder
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
gme_err_t readCallback(void *data, void *out, int count) {
|
|
|
|
id source = (__bridge id)data;
|
2022-02-02 03:04:06 +00:00
|
|
|
DLog(@"Amount: %i", count);
|
2022-02-07 05:49:27 +00:00
|
|
|
int n = (int)[source read:out amount:count];
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Read: %i", n);
|
2022-02-07 05:49:27 +00:00
|
|
|
if(n <= 0) {
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"ERROR!");
|
2022-02-07 05:49:27 +00:00
|
|
|
return (gme_err_t)1; // Return non-zero for error
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
return 0; // Return 0 for no error
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (id)init {
|
|
|
|
self = [super init];
|
|
|
|
if(self) {
|
|
|
|
emu = NULL;
|
|
|
|
}
|
|
|
|
return self;
|
2016-06-19 19:57:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (BOOL)open:(id<CogSource>)s {
|
2007-10-11 23:11:58 +00:00
|
|
|
[self setSource:s];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
// We need file-size to use GME
|
|
|
|
if(![source seekable]) {
|
2007-10-11 23:11:58 +00:00
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2007-10-11 23:11:58 +00:00
|
|
|
gme_err_t error;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2016-06-19 19:57:18 +00:00
|
|
|
NSString *ext = [[[source url] pathExtension] lowercaseString];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2007-10-11 23:11:58 +00:00
|
|
|
gme_type_t type = gme_identify_extension([ext UTF8String]);
|
2022-02-07 05:49:27 +00:00
|
|
|
if(!type) {
|
2013-10-11 12:03:55 +00:00
|
|
|
ALog(@"GME: No type!");
|
2007-10-11 23:11:58 +00:00
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-07-02 05:06:08 +00:00
|
|
|
sampleRate = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthSampleRate"] doubleValue];
|
|
|
|
if(sampleRate < 8000.0) {
|
|
|
|
sampleRate = 44100.0;
|
|
|
|
} else if(sampleRate > 192000.0) {
|
|
|
|
sampleRate = 192000.0;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
if(type == gme_spc_type || type == gme_sfm_type)
|
2022-07-02 05:06:08 +00:00
|
|
|
sampleRate = 32000.0;
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-05-24 08:11:07 +00:00
|
|
|
emu = gme_new_emu(type, (int)sampleRate);
|
2022-02-07 05:49:27 +00:00
|
|
|
if(!emu) {
|
2013-10-11 12:03:55 +00:00
|
|
|
ALog(@"GME: No new emu!");
|
2007-10-11 23:11:58 +00:00
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2007-10-11 23:11:58 +00:00
|
|
|
[source seek:0 whence:SEEK_END];
|
|
|
|
long size = [source tell];
|
|
|
|
[source seek:0 whence:SEEK_SET];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Size: %li", size);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2016-05-05 20:05:39 +00:00
|
|
|
error = gme_load_custom(emu, readCallback, size, (__bridge void *)(s));
|
2022-02-07 05:49:27 +00:00
|
|
|
if(error) {
|
2013-10-11 12:03:55 +00:00
|
|
|
ALog(@"GME: ERROR Loding custom!");
|
2007-10-11 23:11:58 +00:00
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
NSURL *m3uurl = [[source url] URLByDeletingPathExtension];
|
|
|
|
m3uurl = [m3uurl URLByAppendingPathExtension:@"m3u"];
|
|
|
|
id audioSourceClass = NSClassFromString(@"AudioSource");
|
|
|
|
id<CogSource> m3usrc = [audioSourceClass audioSourceForURL:m3uurl];
|
|
|
|
if([m3usrc open:m3uurl]) {
|
|
|
|
if([m3usrc seekable]) {
|
|
|
|
[m3usrc seek:0 whence:SEEK_END];
|
|
|
|
long size = [m3usrc tell];
|
|
|
|
[m3usrc seek:0 whence:SEEK_SET];
|
|
|
|
|
|
|
|
void *data = malloc(size);
|
|
|
|
[m3usrc read:data amount:size];
|
|
|
|
|
|
|
|
gme_load_m3u_data(emu, data, size);
|
|
|
|
free(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int track_num = [[[source url] fragment] intValue]; // What if theres no fragment? Assuming we get 0.
|
|
|
|
|
|
|
|
gme_info_t *info;
|
|
|
|
error = gme_track_info(emu, &info, track_num);
|
|
|
|
if(error) {
|
2013-10-11 12:03:55 +00:00
|
|
|
ALog(@"Unable to get track info");
|
2022-02-07 05:49:27 +00:00
|
|
|
return NO;
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
// As recommended
|
|
|
|
if(info->length > 0) {
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Using length: %i", info->length);
|
2013-09-28 03:24:23 +00:00
|
|
|
length = info->length;
|
2022-02-07 05:49:27 +00:00
|
|
|
} else if(info->loop_length > 0) {
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Using loop length: %i", info->loop_length);
|
2022-07-02 05:06:08 +00:00
|
|
|
int loopCount = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultLoopCount"] intValue];
|
|
|
|
if(loopCount < 0) {
|
|
|
|
loopCount = 1;
|
|
|
|
} else if(loopCount > 10) {
|
|
|
|
loopCount = 10;
|
|
|
|
}
|
|
|
|
length = info->intro_length + loopCount * info->loop_length;
|
2022-02-07 05:49:27 +00:00
|
|
|
} else {
|
2022-07-02 05:06:08 +00:00
|
|
|
double defaultLength = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultSeconds"] doubleValue];
|
|
|
|
if(defaultLength < 0) {
|
|
|
|
defaultLength = 150.0;
|
|
|
|
}
|
|
|
|
length = (int)ceil(defaultLength * 1000.0);
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Setting default: %li", length);
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
2017-03-13 04:10:35 +00:00
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
if(info->fade_length >= 0) {
|
2017-03-13 04:10:35 +00:00
|
|
|
fade = info->fade_length;
|
2022-02-07 05:49:27 +00:00
|
|
|
} else {
|
2022-07-02 05:06:08 +00:00
|
|
|
double defaultFade = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultFadeSeconds"] doubleValue];
|
|
|
|
if(defaultFade < 0) {
|
|
|
|
defaultFade = 0;
|
|
|
|
}
|
|
|
|
fade = (int)ceil(defaultFade * 1000.0);
|
2017-03-13 04:10:35 +00:00
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
gme_free_info(info);
|
2007-10-11 23:11:58 +00:00
|
|
|
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Length: %li", length);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2013-10-11 12:03:55 +00:00
|
|
|
DLog(@"Track num: %i", track_num);
|
2007-10-11 23:11:58 +00:00
|
|
|
error = gme_start_track(emu, track_num);
|
2022-02-07 05:49:27 +00:00
|
|
|
if(error) {
|
2013-10-11 12:03:55 +00:00
|
|
|
ALog(@"GME: Error starting track");
|
2007-10-11 23:11:58 +00:00
|
|
|
return NO;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
length += fade;
|
2007-10-12 02:55:59 +00:00
|
|
|
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
|
|
[self didChangeValueForKey:@"properties"];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2007-10-11 23:11:58 +00:00
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (NSDictionary *)properties {
|
2022-06-17 13:39:02 +00:00
|
|
|
return @{ @"bitrate": @(0),
|
|
|
|
@"sampleRate": @(sampleRate),
|
2022-07-02 05:06:08 +00:00
|
|
|
@"totalFrames": @((long)(length * (sampleRate * 0.001))),
|
2022-06-17 13:39:02 +00:00
|
|
|
@"bitsPerSample": @(sizeof(short) * 8), // Samples are short
|
|
|
|
@"channels": @(2), // output from gme_play is in stereo
|
|
|
|
@"seekable": @(YES),
|
|
|
|
@"endian": @"host",
|
|
|
|
@"encoding": @"synthesized" };
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-09 03:56:39 +00:00
|
|
|
- (NSDictionary *)metadata {
|
|
|
|
return @{};
|
|
|
|
}
|
|
|
|
|
2022-07-10 22:14:47 +00:00
|
|
|
- (AudioChunk *)readAudio {
|
|
|
|
int frames = 1024;
|
|
|
|
int16_t buffer[frames * 2];
|
|
|
|
void *buf = (void *)buffer;
|
|
|
|
|
|
|
|
id audioChunkClass = NSClassFromString(@"AudioChunk");
|
|
|
|
AudioChunk *chunk = [[audioChunkClass alloc] initWithProperties:[self properties]];
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
int numSamples = frames * 2; // channels = 2
|
|
|
|
|
|
|
|
if(gme_track_ended(emu)) {
|
2022-07-10 22:14:47 +00:00
|
|
|
return nil;
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
if(IsRepeatOneSet())
|
|
|
|
gme_set_fade(emu, -1, 0);
|
|
|
|
else
|
|
|
|
gme_set_fade(emu, (int)(length - fade), (int)fade);
|
|
|
|
|
2007-10-11 23:11:58 +00:00
|
|
|
gme_play(emu, numSamples, (short int *)buf);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
// Some formats support length, but we'll add that in the future.
|
2007-10-11 23:11:58 +00:00
|
|
|
//(From gme.txt) If track length, then use it. If loop length, play for intro + loop * 2. Otherwise, default to 2.5 minutes
|
2022-07-10 22:14:47 +00:00
|
|
|
// GME will always generate samples. There's no real EOS.
|
|
|
|
|
|
|
|
[chunk assignSamples:buffer frameCount:numSamples];
|
|
|
|
|
|
|
|
return chunk;
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (long)seek:(long)frame {
|
2007-10-11 23:11:58 +00:00
|
|
|
gme_err_t error;
|
2022-07-02 05:06:08 +00:00
|
|
|
error = gme_seek(emu, frame * sampleRate * 0.001);
|
2022-02-07 05:49:27 +00:00
|
|
|
if(error) {
|
2007-10-11 23:11:58 +00:00
|
|
|
return -1;
|
|
|
|
}
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2008-02-10 16:04:28 +00:00
|
|
|
return frame;
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (void)close {
|
|
|
|
if(emu) {
|
2007-10-11 23:11:58 +00:00
|
|
|
gme_delete(emu);
|
|
|
|
emu = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (void)dealloc {
|
|
|
|
[self close];
|
2016-06-19 19:57:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)fileTypes {
|
2022-01-19 02:12:57 +00:00
|
|
|
return @[@"ay", @"gbs", @"hes", @"kss", @"nsf", @"nsfe", @"sap", @"sfm", @"sgc", @"spc"];
|
2007-10-11 23:11:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)mimeTypes {
|
2007-10-20 03:17:43 +00:00
|
|
|
return nil;
|
2007-10-14 18:39:58 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (float)priority {
|
|
|
|
return 1.0;
|
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 {
|
|
|
|
NSMutableArray *ret = [[NSMutableArray alloc] init];
|
|
|
|
[ret addObject:@"Game Music Emu Files"];
|
|
|
|
[ret addObject:@"vg.icns"];
|
|
|
|
[ret addObjectsFromArray:[self fileTypes]];
|
2022-01-18 11:06:03 +00:00
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
return @[ret];
|
|
|
|
}
|
2022-01-18 11:06:03 +00:00
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (void)setSource:(id<CogSource>)s {
|
2007-10-11 23:11:58 +00:00
|
|
|
source = s;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (id<CogSource>)source {
|
2007-10-11 23:11:58 +00:00
|
|
|
return source;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|