2014-02-26 07:50:54 +00:00
|
|
|
//
|
|
|
|
// VGMDecoder.m
|
|
|
|
// vgmstream
|
|
|
|
//
|
|
|
|
// Created by Christopher Snowhill on 02/25/14.
|
|
|
|
// Copyright 2014 __NoWork, Inc__. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#import "VGMDecoder.h"
|
2017-09-17 04:24:57 +00:00
|
|
|
#import "VGMInterface.h"
|
2014-02-26 07:50:54 +00:00
|
|
|
|
|
|
|
#import "PlaylistController.h"
|
|
|
|
|
2021-10-02 03:21:12 +00:00
|
|
|
#include <stdlib.h>
|
|
|
|
|
2021-10-03 01:38:23 +00:00
|
|
|
#define MAX_BUFFER_SAMPLES ((int)2048)
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
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;
|
2021-10-02 02:18:42 +00:00
|
|
|
}
|
|
|
|
|
2019-10-19 03:21:27 +00:00
|
|
|
@implementation VGMInfoCache
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (id)sharedCache {
|
|
|
|
static VGMInfoCache *sharedMyCache = nil;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
sharedMyCache = [[self alloc] init];
|
|
|
|
});
|
|
|
|
return sharedMyCache;
|
2019-10-19 03:21:27 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (id)init {
|
|
|
|
if(self = [super init]) {
|
|
|
|
storage = [[NSMutableDictionary alloc] init];
|
|
|
|
}
|
|
|
|
return self;
|
2019-10-19 03:21:27 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (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;
|
2022-03-21 23:32:52 +00:00
|
|
|
|
|
|
|
vgmstream_mixing_autodownmix(stream, 6);
|
|
|
|
vgmstream_mixing_enable(stream, MAX_BUFFER_SAMPLES, NULL, &output_channels);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
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 = @"";
|
2022-06-17 13:39:02 +00:00
|
|
|
NSNumber *year = @(0);
|
|
|
|
NSNumber *track = @(0);
|
|
|
|
NSNumber *disc = @(0);
|
2022-02-07 05:49:27 +00:00
|
|
|
NSString *title = @"";
|
|
|
|
|
|
|
|
NSString *codec;
|
|
|
|
|
2022-06-17 13:39:02 +00:00
|
|
|
NSNumber *rgTrackGain = @(0);
|
|
|
|
NSNumber *rgTrackPeak = @(0);
|
|
|
|
NSNumber *rgAlbumGain = @(0);
|
|
|
|
NSNumber *rgAlbumPeak = @(0);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
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)) {
|
2022-05-24 08:07:55 +00:00
|
|
|
NSString *value = guess_encoding_of_string(tag_val);
|
2022-02-07 05:49:27 +00:00
|
|
|
if(!strncasecmp(tag_key, "REPLAYGAIN_", strlen("REPLAYGAIN_"))) {
|
|
|
|
if(!strncasecmp(tag_key + strlen("REPLAYGAIN_"), "TRACK_", strlen("TRACK_"))) {
|
|
|
|
if(!strcasecmp(tag_key + strlen("REPLAYGAIN_TRACK_"), "GAIN")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
rgTrackGain = @([value floatValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
} else if(!strcasecmp(tag_key + strlen("REPLAYGAIN_TRACK_"), "PEAK")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
rgTrackPeak = @([value floatValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
}
|
|
|
|
} else if(!strncasecmp(tag_key + strlen("REPLAYGAIN_"), "ALBUM_", strlen("ALBUM_"))) {
|
|
|
|
if(!strcasecmp(tag_key + strlen("REPLAYGAIN_ALBUM_"), "GAIN")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
rgAlbumGain = @([value floatValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
} else if(!strcasecmp(tag_key + strlen("REPLAYGAIN_ALBUM_"), "PEAK")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
rgAlbumPeak = @([value floatValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if(!strcasecmp(tag_key, "ALBUM")) {
|
|
|
|
album = value;
|
|
|
|
} else if(!strcasecmp(tag_key, "ARTIST")) {
|
|
|
|
artist = value;
|
|
|
|
} else if(!strcasecmp(tag_key, "DATE")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
year = @([value intValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
} else if(!strcasecmp(tag_key, "TRACK") ||
|
|
|
|
!strcasecmp(tag_key, "TRACKNUMBER")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
track = @([value intValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
} else if(!strcasecmp(tag_key, "DISC") ||
|
|
|
|
!strcasecmp(tag_key, "DISCNUMBER")) {
|
2022-06-17 13:39:02 +00:00
|
|
|
disc = @([value intValue]);
|
2022-02-07 05:49:27 +00:00
|
|
|
} else if(!strcasecmp(tag_key, "TITLE")) {
|
|
|
|
title = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
vgmstream_tags_close(tags);
|
|
|
|
close_streamfile(tagFile);
|
|
|
|
}
|
|
|
|
|
2022-06-17 13:39:02 +00:00
|
|
|
NSDictionary *properties = @{ @"bitrate": @(bitrate / 1000),
|
|
|
|
@"sampleRate": @(sampleRate),
|
|
|
|
@"totalFrames": @(totalFrames),
|
|
|
|
@"bitsPerSample": @(16),
|
|
|
|
@"floatingPoint": @(NO),
|
|
|
|
@"channels": @(channels),
|
|
|
|
@"seekable": @(YES),
|
2022-07-08 13:26:28 +00:00
|
|
|
@"replaygain_album_gain": rgAlbumGain,
|
|
|
|
@"replaygain_album_peak": rgAlbumPeak,
|
|
|
|
@"replaygain_track_gain": rgTrackGain,
|
|
|
|
@"replaygain_track_peak": rgTrackPeak,
|
2022-06-17 13:39:02 +00:00
|
|
|
@"codec": codec,
|
|
|
|
@"endian": @"host",
|
|
|
|
@"encoding": @"lossy/lossless" };
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
if([title isEqualToString:@""]) {
|
|
|
|
if(stream->num_streams > 1) {
|
2022-05-24 08:07:55 +00:00
|
|
|
title = [NSString stringWithFormat:@"%@ - %@", [[urlTrimmed URLByDeletingPathExtension] lastPathComponent], guess_encoding_of_string(stream->stream_name)];
|
2022-02-07 05:49:27 +00:00
|
|
|
} else {
|
|
|
|
title = [[urlTrimmed URLByDeletingPathExtension] lastPathComponent];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-17 13:39:02 +00:00
|
|
|
if([track isEqualToNumber:@(0)])
|
|
|
|
track = @(track_num);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-03-06 11:57:03 +00:00
|
|
|
NSMutableDictionary *mutableMetadata = [@{ @"title": title,
|
|
|
|
@"track": track,
|
|
|
|
@"disc": disc } mutableCopy];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
if(![album isEqualToString:@""])
|
|
|
|
[mutableMetadata setValue:album forKey:@"album"];
|
|
|
|
if(![artist isEqualToString:@""])
|
|
|
|
[mutableMetadata setValue:artist forKey:@"artist"];
|
2022-06-17 13:39:02 +00:00
|
|
|
if(![year isEqualToNumber:@(0)])
|
2022-02-07 05:49:27 +00:00
|
|
|
[mutableMetadata setValue:year forKey:@"year"];
|
|
|
|
|
2022-05-24 08:09:39 +00:00
|
|
|
NSDictionary *metadata = [NSDictionary dictionaryWithDictionary:mutableMetadata];
|
2022-02-07 05:49:27 +00:00
|
|
|
|
2022-02-09 03:42:03 +00:00
|
|
|
NSDictionary *package = @{@"properties": properties,
|
|
|
|
@"metadata": metadata};
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
@synchronized(self) {
|
|
|
|
[storage setValue:package forKey:[url absoluteString]];
|
|
|
|
}
|
2019-10-19 03:21:27 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (NSDictionary *)getPropertiesForURL:(NSURL *)url {
|
|
|
|
NSDictionary *properties = nil;
|
|
|
|
|
|
|
|
@synchronized(self) {
|
|
|
|
NSDictionary *package = [storage objectForKey:[url absoluteString]];
|
|
|
|
if(package) {
|
|
|
|
properties = [package objectForKey:@"properties"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return properties;
|
2019-10-19 03:21:27 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (NSDictionary *)getMetadataForURL:(NSURL *)url {
|
|
|
|
NSDictionary *metadata = nil;
|
|
|
|
|
|
|
|
@synchronized(self) {
|
|
|
|
NSDictionary *package = [storage objectForKey:[url absoluteString]];
|
|
|
|
if(package) {
|
|
|
|
metadata = [package objectForKey:@"metadata"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return metadata;
|
2019-10-19 03:21:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
2014-02-26 07:50:54 +00:00
|
|
|
@implementation VGMDecoder
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (void)initialize {
|
|
|
|
register_log_callback();
|
2021-09-06 16:28:26 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (BOOL)open:(id<CogSource>)s {
|
|
|
|
int track_num = [[[s url] fragment] intValue];
|
|
|
|
|
2022-07-02 05:06:08 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
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;
|
2022-03-21 23:32:52 +00:00
|
|
|
|
|
|
|
vgmstream_mixing_autodownmix(stream, 6);
|
|
|
|
vgmstream_mixing_enable(stream, MAX_BUFFER_SAMPLES, NULL, &output_channels);
|
2022-02-07 05:49:27 +00:00
|
|
|
|
|
|
|
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;
|
2022-07-02 05:06:08 +00:00
|
|
|
vcfg.loop_count = loopCount;
|
|
|
|
vcfg.fade_time = fadeTime;
|
2022-02-07 05:49:27 +00:00
|
|
|
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"];
|
|
|
|
|
2014-02-26 07:50:54 +00:00
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (NSDictionary *)properties {
|
2022-06-17 13:39:02 +00:00
|
|
|
return @{ @"bitrate": @(bitrate / 1000),
|
|
|
|
@"sampleRate": @(sampleRate),
|
|
|
|
@"totalFrames": @(totalFrames),
|
|
|
|
@"bitsPerSample": @(16),
|
|
|
|
@"floatingPoint": @(NO),
|
|
|
|
@"channels": @(channels),
|
|
|
|
@"seekable": @(YES),
|
|
|
|
@"endian": @"host",
|
|
|
|
@"encoding": @"lossy/lossless" };
|
2014-02-26 07:50:54 +00:00
|
|
|
}
|
|
|
|
|
2022-02-09 03:56:39 +00:00
|
|
|
- (NSDictionary *)metadata {
|
|
|
|
return @{};
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (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;
|
2014-02-26 07:50:54 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (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;
|
2014-02-26 07:50:54 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (void)close {
|
|
|
|
close_vgmstream(stream);
|
|
|
|
stream = NULL;
|
2016-06-19 19:57:18 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
- (void)dealloc {
|
|
|
|
[self close];
|
2014-02-26 07:50:54 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (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];
|
2014-02-26 07:50:54 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)mimeTypes {
|
2014-02-26 07:50:54 +00:00
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (float)priority {
|
|
|
|
return 0.0;
|
2014-02-26 07:50:54 +00:00
|
|
|
}
|
|
|
|
|
2022-02-07 05:49:27 +00:00
|
|
|
+ (NSArray *)fileTypeAssociations {
|
|
|
|
NSMutableArray *ret = [[NSMutableArray alloc] init];
|
|
|
|
[ret addObject:@"VGMStream Files"];
|
|
|
|
[ret addObject:@"vg.icns"];
|
|
|
|
[ret addObjectsFromArray:[self fileTypes]];
|
|
|
|
|
|
|
|
return @[[NSArray arrayWithArray:ret]];
|
2022-01-18 11:06:03 +00:00
|
|
|
}
|
|
|
|
|
2014-02-26 07:50:54 +00:00
|
|
|
@end
|