Cog/Plugins/GME/GameDecoder.m
Christopher Snowhill 5ac279e289 [GME Input] Fix decoder output sample count
The output has been assigning twice as many samples as it was supposed
to ever since commit 8d851e5bda, which
ended up generating the correct 1024 samples (2048 per GME parameter),
but assigned 2048 to the AudioChunk, which resulted in over-reading the
audio buffer, and thankfully not crashing, but instead causing an awful
sound distortion effect as random memory contents were played as PCM
audio.

Fixes #320

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-08-04 18:03:00 -07:00

256 lines
5.7 KiB
Objective-C

//
// GameFile.m
// Cog
//
// Created by Vincent Spader on 5/29/06.
// Copyright 2006 Vincent Spader. All rights reserved.
//
#import "GameDecoder.h"
#import "Logging.h"
#import "PlaylistController.h"
@implementation GameDecoder
gme_err_t readCallback(void *data, void *out, int count) {
id source = (__bridge id)data;
DLog(@"Amount: %i", count);
int n = (int)[source read:out amount:count];
DLog(@"Read: %i", n);
if(n <= 0) {
DLog(@"ERROR!");
return (gme_err_t)1; // Return non-zero for error
}
return 0; // Return 0 for no error
}
- (id)init {
self = [super init];
if(self) {
emu = NULL;
}
return self;
}
- (BOOL)open:(id<CogSource>)s {
[self setSource:s];
// We need file-size to use GME
if(![source seekable]) {
return NO;
}
gme_err_t error;
NSString *ext = [[[source url] pathExtension] lowercaseString];
gme_type_t type = gme_identify_extension([ext UTF8String]);
if(!type) {
ALog(@"GME: No type!");
return NO;
}
sampleRate = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthSampleRate"] doubleValue];
if(sampleRate < 8000.0) {
sampleRate = 44100.0;
} else if(sampleRate > 192000.0) {
sampleRate = 192000.0;
}
if(type == gme_spc_type || type == gme_sfm_type)
sampleRate = 32000.0;
emu = gme_new_emu(type, (int)sampleRate);
if(!emu) {
ALog(@"GME: No new emu!");
return NO;
}
[source seek:0 whence:SEEK_END];
long size = [source tell];
[source seek:0 whence:SEEK_SET];
DLog(@"Size: %li", size);
error = gme_load_custom(emu, readCallback, size, (__bridge void *)(s));
if(error) {
ALog(@"GME: ERROR Loding custom!");
return NO;
}
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) {
ALog(@"Unable to get track info");
return NO;
}
// As recommended
if(info->length > 0) {
DLog(@"Using length: %i", info->length);
length = info->length;
} else if(info->loop_length > 0) {
DLog(@"Using loop length: %i", info->loop_length);
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;
} else {
double defaultLength = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultSeconds"] doubleValue];
if(defaultLength < 0) {
defaultLength = 150.0;
}
length = (int)ceil(defaultLength * 1000.0);
DLog(@"Setting default: %li", length);
}
if(info->fade_length >= 0) {
fade = info->fade_length;
} else {
double defaultFade = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultFadeSeconds"] doubleValue];
if(defaultFade < 0) {
defaultFade = 0;
}
fade = (int)ceil(defaultFade * 1000.0);
}
gme_free_info(info);
DLog(@"Length: %li", length);
DLog(@"Track num: %i", track_num);
error = gme_start_track(emu, track_num);
if(error) {
ALog(@"GME: Error starting track");
return NO;
}
length += fade;
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
return YES;
}
- (NSDictionary *)properties {
return @{ @"bitrate": @(0),
@"sampleRate": @(sampleRate),
@"totalFrames": @((long)(length * (sampleRate * 0.001))),
@"bitsPerSample": @(sizeof(short) * 8), // Samples are short
@"channels": @(2), // output from gme_play is in stereo
@"seekable": @(YES),
@"endian": @"host",
@"encoding": @"synthesized" };
}
- (NSDictionary *)metadata {
return @{};
}
- (AudioChunk *)readAudio {
int frames = 1024;
void *buf = (void *)sampleBuffer;
id audioChunkClass = NSClassFromString(@"AudioChunk");
AudioChunk *chunk = [[audioChunkClass alloc] initWithProperties:[self properties]];
int numSamples = frames * 2; // channels = 2
if(gme_track_ended(emu)) {
return nil;
}
if(IsRepeatOneSet())
gme_set_fade(emu, -1, 0);
else
gme_set_fade(emu, (int)(length - fade), (int)fade);
gme_play(emu, numSamples, (short int *)buf);
// Some formats support length, but we'll add that in the future.
//(From gme.txt) If track length, then use it. If loop length, play for intro + loop * 2. Otherwise, default to 2.5 minutes
// GME will always generate samples. There's no real EOS.
[chunk assignSamples:sampleBuffer frameCount:frames];
return chunk;
}
- (long)seek:(long)frame {
gme_err_t error;
error = gme_seek(emu, frame * sampleRate * 0.001);
if(error) {
return -1;
}
return frame;
}
- (void)close {
if(emu) {
gme_delete(emu);
emu = NULL;
}
}
- (void)dealloc {
[self close];
}
+ (NSArray *)fileTypes {
return @[@"ay", @"gbs", @"hes", @"kss", @"nsf", @"nsfe", @"sap", @"sfm", @"sgc", @"spc"];
}
+ (NSArray *)mimeTypes {
return nil;
}
+ (float)priority {
return 1.0;
}
+ (NSArray *)fileTypeAssociations {
NSMutableArray *ret = [[NSMutableArray alloc] init];
[ret addObject:@"Game Music Emu Files"];
[ret addObject:@"vg.icns"];
[ret addObjectsFromArray:[self fileTypes]];
return @[ret];
}
- (void)setSource:(id<CogSource>)s {
source = s;
}
- (id<CogSource>)source {
return source;
}
@end