Default time, fade, loop count, and sample rate may now be overridden. Synchronized preferences strings tables. Spanish translation of new options pending, new releases won't be pushed until they're complete. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
419 lines
11 KiB
Objective-C
419 lines
11 KiB
Objective-C
//
|
|
// VGMDecoder.m
|
|
// vgmstream
|
|
//
|
|
// Created by Christopher Snowhill on 02/25/14.
|
|
// Copyright 2014 __NoWork, Inc__. All rights reserved.
|
|
//
|
|
|
|
#import "VGMDecoder.h"
|
|
#import "VGMInterface.h"
|
|
|
|
#import "PlaylistController.h"
|
|
|
|
#include <stdlib.h>
|
|
|
|
#define MAX_BUFFER_SAMPLES ((int)2048)
|
|
|
|
static NSString *get_description_tag(const char *description, const char *tag, char delimiter) {
|
|
// extract a "tag" from the description string
|
|
if(!delimiter) delimiter = '\n';
|
|
const char *pos = strstr(description, tag);
|
|
const char *eos = NULL;
|
|
if(pos != NULL) {
|
|
pos += strlen(tag);
|
|
eos = strchr(pos, delimiter);
|
|
if(eos == NULL) eos = pos + strlen(pos);
|
|
char temp[eos - pos + 1];
|
|
memcpy(temp, pos, eos - pos);
|
|
temp[eos - pos] = '\0';
|
|
return [NSString stringWithUTF8String:temp];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
@implementation VGMInfoCache
|
|
|
|
+ (id)sharedCache {
|
|
static VGMInfoCache *sharedMyCache = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
sharedMyCache = [[self alloc] init];
|
|
});
|
|
return sharedMyCache;
|
|
}
|
|
|
|
- (id)init {
|
|
if(self = [super init]) {
|
|
storage = [[NSMutableDictionary alloc] init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)stuffURL:(NSURL *)url stream:(VGMSTREAM *)stream {
|
|
vgmstream_cfg_t vcfg = { 0 };
|
|
|
|
vcfg.allow_play_forever = 1;
|
|
vcfg.play_forever = 0;
|
|
vcfg.loop_count = 2;
|
|
vcfg.fade_time = 10;
|
|
vcfg.fade_delay = 0;
|
|
vcfg.ignore_loop = 0;
|
|
|
|
vgmstream_apply_config(stream, &vcfg);
|
|
|
|
int output_channels = stream->channels;
|
|
|
|
vgmstream_mixing_autodownmix(stream, 6);
|
|
vgmstream_mixing_enable(stream, MAX_BUFFER_SAMPLES, NULL, &output_channels);
|
|
|
|
int track_num = [[url fragment] intValue];
|
|
|
|
int sampleRate = stream->sample_rate;
|
|
int channels = output_channels;
|
|
long totalFrames = vgmstream_get_samples(stream);
|
|
|
|
int bitrate = get_vgmstream_average_bitrate(stream);
|
|
|
|
char description[1024];
|
|
describe_vgmstream(stream, description, 1024);
|
|
|
|
NSString *path = [url absoluteString];
|
|
NSRange fragmentRange = [path rangeOfString:@"#" options:NSBackwardsSearch];
|
|
if(fragmentRange.location != NSNotFound) {
|
|
path = [path substringToIndex:fragmentRange.location];
|
|
}
|
|
|
|
NSURL *urlTrimmed = [NSURL fileURLWithPath:[path stringByRemovingPercentEncoding]];
|
|
|
|
NSURL *folder = [urlTrimmed URLByDeletingLastPathComponent];
|
|
NSURL *tagurl = [folder URLByAppendingPathComponent:@"!tags.m3u" isDirectory:NO];
|
|
|
|
NSString *filename = [urlTrimmed lastPathComponent];
|
|
|
|
NSString *album = @"";
|
|
NSString *artist = @"";
|
|
NSNumber *year = @(0);
|
|
NSNumber *track = @(0);
|
|
NSNumber *disc = @(0);
|
|
NSString *title = @"";
|
|
|
|
NSString *codec;
|
|
|
|
NSNumber *rgTrackGain = @(0);
|
|
NSNumber *rgTrackPeak = @(0);
|
|
NSNumber *rgAlbumGain = @(0);
|
|
NSNumber *rgAlbumPeak = @(0);
|
|
|
|
codec = get_description_tag(description, "encoding: ", 0);
|
|
|
|
STREAMFILE *tagFile = open_cog_streamfile_from_url(tagurl);
|
|
if(tagFile) {
|
|
VGMSTREAM_TAGS *tags;
|
|
const char *tag_key, *tag_val;
|
|
|
|
tags = vgmstream_tags_init(&tag_key, &tag_val);
|
|
vgmstream_tags_reset(tags, [filename UTF8String]);
|
|
while(vgmstream_tags_next_tag(tags, tagFile)) {
|
|
NSString *value = guess_encoding_of_string(tag_val);
|
|
if(!strncasecmp(tag_key, "REPLAYGAIN_", strlen("REPLAYGAIN_"))) {
|
|
if(!strncasecmp(tag_key + strlen("REPLAYGAIN_"), "TRACK_", strlen("TRACK_"))) {
|
|
if(!strcasecmp(tag_key + strlen("REPLAYGAIN_TRACK_"), "GAIN")) {
|
|
rgTrackGain = @([value floatValue]);
|
|
} else if(!strcasecmp(tag_key + strlen("REPLAYGAIN_TRACK_"), "PEAK")) {
|
|
rgTrackPeak = @([value floatValue]);
|
|
}
|
|
} else if(!strncasecmp(tag_key + strlen("REPLAYGAIN_"), "ALBUM_", strlen("ALBUM_"))) {
|
|
if(!strcasecmp(tag_key + strlen("REPLAYGAIN_ALBUM_"), "GAIN")) {
|
|
rgAlbumGain = @([value floatValue]);
|
|
} else if(!strcasecmp(tag_key + strlen("REPLAYGAIN_ALBUM_"), "PEAK")) {
|
|
rgAlbumPeak = @([value floatValue]);
|
|
}
|
|
}
|
|
} else if(!strcasecmp(tag_key, "ALBUM")) {
|
|
album = value;
|
|
} else if(!strcasecmp(tag_key, "ARTIST")) {
|
|
artist = value;
|
|
} else if(!strcasecmp(tag_key, "DATE")) {
|
|
year = @([value intValue]);
|
|
} else if(!strcasecmp(tag_key, "TRACK") ||
|
|
!strcasecmp(tag_key, "TRACKNUMBER")) {
|
|
track = @([value intValue]);
|
|
} else if(!strcasecmp(tag_key, "DISC") ||
|
|
!strcasecmp(tag_key, "DISCNUMBER")) {
|
|
disc = @([value intValue]);
|
|
} else if(!strcasecmp(tag_key, "TITLE")) {
|
|
title = value;
|
|
}
|
|
}
|
|
vgmstream_tags_close(tags);
|
|
close_streamfile(tagFile);
|
|
}
|
|
|
|
NSDictionary *properties = @{ @"bitrate": @(bitrate / 1000),
|
|
@"sampleRate": @(sampleRate),
|
|
@"totalFrames": @(totalFrames),
|
|
@"bitsPerSample": @(16),
|
|
@"floatingPoint": @(NO),
|
|
@"channels": @(channels),
|
|
@"seekable": @(YES),
|
|
@"replayGainAlbumGain": rgAlbumGain,
|
|
@"replayGainAlbumPeak": rgAlbumPeak,
|
|
@"replayGainTrackGain": rgTrackGain,
|
|
@"replayGainTrackPeak": rgTrackPeak,
|
|
@"codec": codec,
|
|
@"endian": @"host",
|
|
@"encoding": @"lossy/lossless" };
|
|
|
|
if([title isEqualToString:@""]) {
|
|
if(stream->num_streams > 1) {
|
|
title = [NSString stringWithFormat:@"%@ - %@", [[urlTrimmed URLByDeletingPathExtension] lastPathComponent], guess_encoding_of_string(stream->stream_name)];
|
|
} else {
|
|
title = [[urlTrimmed URLByDeletingPathExtension] lastPathComponent];
|
|
}
|
|
}
|
|
|
|
if([track isEqualToNumber:@(0)])
|
|
track = @(track_num);
|
|
|
|
NSMutableDictionary *mutableMetadata = [@{ @"title": title,
|
|
@"track": track,
|
|
@"disc": disc } mutableCopy];
|
|
|
|
if(![album isEqualToString:@""])
|
|
[mutableMetadata setValue:album forKey:@"album"];
|
|
if(![artist isEqualToString:@""])
|
|
[mutableMetadata setValue:artist forKey:@"artist"];
|
|
if(![year isEqualToNumber:@(0)])
|
|
[mutableMetadata setValue:year forKey:@"year"];
|
|
|
|
NSDictionary *metadata = [NSDictionary dictionaryWithDictionary:mutableMetadata];
|
|
|
|
NSDictionary *package = @{@"properties": properties,
|
|
@"metadata": metadata};
|
|
|
|
@synchronized(self) {
|
|
[storage setValue:package forKey:[url absoluteString]];
|
|
}
|
|
}
|
|
|
|
- (NSDictionary *)getPropertiesForURL:(NSURL *)url {
|
|
NSDictionary *properties = nil;
|
|
|
|
@synchronized(self) {
|
|
NSDictionary *package = [storage objectForKey:[url absoluteString]];
|
|
if(package) {
|
|
properties = [package objectForKey:@"properties"];
|
|
}
|
|
}
|
|
|
|
return properties;
|
|
}
|
|
|
|
- (NSDictionary *)getMetadataForURL:(NSURL *)url {
|
|
NSDictionary *metadata = nil;
|
|
|
|
@synchronized(self) {
|
|
NSDictionary *package = [storage objectForKey:[url absoluteString]];
|
|
if(package) {
|
|
metadata = [package objectForKey:@"metadata"];
|
|
}
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation VGMDecoder
|
|
|
|
+ (void)initialize {
|
|
register_log_callback();
|
|
}
|
|
|
|
- (BOOL)open:(id<CogSource>)s {
|
|
int track_num = [[[s url] fragment] intValue];
|
|
|
|
loopCount = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultLoopCount"] intValue];
|
|
if(loopCount < 1) {
|
|
loopCount = 1;
|
|
} else if(loopCount > 10) {
|
|
loopCount = 10;
|
|
}
|
|
|
|
fadeTime = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] valueForKey:@"synthDefaultFadeTime"] doubleValue];
|
|
if(fadeTime < 0.0) {
|
|
fadeTime = 0.0;
|
|
}
|
|
|
|
NSString *path = [[s url] absoluteString];
|
|
NSRange fragmentRange = [path rangeOfString:@"#" options:NSBackwardsSearch];
|
|
if(fragmentRange.location != NSNotFound) {
|
|
path = [path substringToIndex:fragmentRange.location];
|
|
}
|
|
|
|
NSLog(@"Opening %@ subsong %d", path, track_num);
|
|
|
|
stream = init_vgmstream_from_cogfile([[path stringByRemovingPercentEncoding] UTF8String], track_num);
|
|
if(!stream)
|
|
return NO;
|
|
|
|
int output_channels = stream->channels;
|
|
|
|
vgmstream_mixing_autodownmix(stream, 6);
|
|
vgmstream_mixing_enable(stream, MAX_BUFFER_SAMPLES, NULL, &output_channels);
|
|
|
|
canPlayForever = stream->loop_flag;
|
|
if(canPlayForever) {
|
|
playForever = IsRepeatOneSet();
|
|
} else {
|
|
playForever = NO;
|
|
}
|
|
|
|
vgmstream_cfg_t vcfg = { 0 };
|
|
|
|
vcfg.allow_play_forever = 1;
|
|
vcfg.play_forever = playForever;
|
|
vcfg.loop_count = loopCount;
|
|
vcfg.fade_time = fadeTime;
|
|
vcfg.fade_delay = 0;
|
|
vcfg.ignore_loop = 0;
|
|
|
|
vgmstream_apply_config(stream, &vcfg);
|
|
|
|
sampleRate = stream->sample_rate;
|
|
channels = output_channels;
|
|
totalFrames = vgmstream_get_samples(stream);
|
|
|
|
framesRead = 0;
|
|
|
|
bitrate = get_vgmstream_average_bitrate(stream);
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
[self didChangeValueForKey:@"properties"];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSDictionary *)properties {
|
|
return @{ @"bitrate": @(bitrate / 1000),
|
|
@"sampleRate": @(sampleRate),
|
|
@"totalFrames": @(totalFrames),
|
|
@"bitsPerSample": @(16),
|
|
@"floatingPoint": @(NO),
|
|
@"channels": @(channels),
|
|
@"seekable": @(YES),
|
|
@"endian": @"host",
|
|
@"encoding": @"lossy/lossless" };
|
|
}
|
|
|
|
- (NSDictionary *)metadata {
|
|
return @{};
|
|
}
|
|
|
|
- (int)readAudio:(void *)buf frames:(UInt32)frames {
|
|
UInt32 framesMax = frames;
|
|
UInt32 framesDone = 0;
|
|
|
|
if(canPlayForever) {
|
|
BOOL repeatone = IsRepeatOneSet();
|
|
|
|
if(repeatone != playForever) {
|
|
playForever = repeatone;
|
|
vgmstream_set_play_forever(stream, repeatone);
|
|
}
|
|
}
|
|
|
|
if(framesRead + frames > totalFrames && !playForever)
|
|
frames = totalFrames - framesRead;
|
|
if(frames > framesMax)
|
|
frames = 0; // integer overflow?
|
|
|
|
while(frames) {
|
|
sample sample_buffer[MAX_BUFFER_SAMPLES * VGMSTREAM_MAX_CHANNELS];
|
|
|
|
UInt32 frames_to_do = frames;
|
|
if(frames_to_do > MAX_BUFFER_SAMPLES)
|
|
frames_to_do = MAX_BUFFER_SAMPLES;
|
|
|
|
memset(sample_buffer, 0, frames_to_do * channels * sizeof(sample_buffer[0]));
|
|
|
|
render_vgmstream(sample_buffer, frames_to_do, stream);
|
|
|
|
framesRead += frames_to_do;
|
|
framesDone += frames_to_do;
|
|
|
|
sample *sbuf = (sample *)buf;
|
|
|
|
memcpy(sbuf, sample_buffer, frames_to_do * channels * sizeof(sbuf[0]));
|
|
|
|
sbuf += frames_to_do * channels;
|
|
|
|
buf = (void *)sbuf;
|
|
|
|
frames -= frames_to_do;
|
|
}
|
|
|
|
return framesDone;
|
|
}
|
|
|
|
- (long)seek:(long)frame {
|
|
if(canPlayForever) {
|
|
BOOL repeatone = IsRepeatOneSet();
|
|
|
|
if(repeatone != playForever) {
|
|
playForever = repeatone;
|
|
vgmstream_set_play_forever(stream, repeatone);
|
|
}
|
|
}
|
|
|
|
if(frame > totalFrames)
|
|
frame = totalFrames;
|
|
|
|
seek_vgmstream(stream, frame);
|
|
|
|
framesRead = frame;
|
|
|
|
return frame;
|
|
}
|
|
|
|
- (void)close {
|
|
close_vgmstream(stream);
|
|
stream = NULL;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[self close];
|
|
}
|
|
|
|
+ (NSArray *)fileTypes {
|
|
NSMutableArray *array = [[NSMutableArray alloc] init];
|
|
|
|
size_t count;
|
|
const char **formats = vgmstream_get_formats(&count);
|
|
|
|
for(size_t i = 0; i < count; ++i) {
|
|
[array addObject:[NSString stringWithUTF8String:formats[i]]];
|
|
}
|
|
|
|
return [NSArray arrayWithArray:array];
|
|
}
|
|
|
|
+ (NSArray *)mimeTypes {
|
|
return nil;
|
|
}
|
|
|
|
+ (float)priority {
|
|
return 0.0;
|
|
}
|
|
|
|
+ (NSArray *)fileTypeAssociations {
|
|
NSMutableArray *ret = [[NSMutableArray alloc] init];
|
|
[ret addObject:@"VGMStream Files"];
|
|
[ret addObject:@"vg.icns"];
|
|
[ret addObjectsFromArray:[self fileTypes]];
|
|
|
|
return @[[NSArray arrayWithArray:ret]];
|
|
}
|
|
|
|
@end
|