2013-10-05 06:01:33 -03:00
|
|
|
//
|
|
|
|
// OpusDecoder.m
|
|
|
|
// Opus
|
|
|
|
//
|
|
|
|
// Created by Christopher Snowhill on 10/4/13.
|
|
|
|
// Copyright 2013 __NoWork, Inc__. All rights reserved.
|
|
|
|
//
|
|
|
|
|
2013-10-05 18:15:09 -03:00
|
|
|
#import "Plugin.h"
|
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
#import "OpusDecoder.h"
|
|
|
|
|
2013-10-11 09:03:55 -03:00
|
|
|
#import "Logging.h"
|
2013-10-05 06:01:33 -03:00
|
|
|
|
2022-02-09 18:44:50 -03:00
|
|
|
#import "HTTPSource.h"
|
|
|
|
|
2022-06-17 13:27:17 -04:00
|
|
|
#import "NSDictionary+Merge.h"
|
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
@implementation OpusFile
|
|
|
|
|
2021-12-28 05:54:28 -03:00
|
|
|
static const int MAXCHANNELS = 8;
|
|
|
|
static const int chmap[MAXCHANNELS][MAXCHANNELS] = {
|
2022-02-07 02:49:27 -03:00
|
|
|
{
|
|
|
|
0,
|
|
|
|
}, // mono
|
|
|
|
{
|
|
|
|
0,
|
|
|
|
1,
|
|
|
|
}, // l, r
|
|
|
|
{
|
|
|
|
0,
|
|
|
|
2,
|
|
|
|
1,
|
|
|
|
}, // l, c, r -> l, r, c
|
|
|
|
{
|
|
|
|
0,
|
|
|
|
1,
|
|
|
|
2,
|
|
|
|
3,
|
|
|
|
}, // l, r, bl, br
|
|
|
|
{
|
|
|
|
0,
|
|
|
|
2,
|
|
|
|
1,
|
|
|
|
3,
|
|
|
|
4,
|
|
|
|
}, // l, c, r, bl, br -> l, r, c, bl, br
|
|
|
|
{ 0, 2, 1, 5, 3, 4 }, // l, c, r, bl, br, lfe -> l, r, c, lfe, bl, br
|
|
|
|
{ 0, 2, 1, 6, 5, 3, 4 }, // l, c, r, sl, sr, bc, lfe -> l, r, c, lfe, bc, sl, sr
|
|
|
|
{ 0, 2, 1, 7, 5, 6, 3, 4 } // l, c, r, sl, sr, bl, br, lfe -> l, r, c, lfe, bl, br, sl, sr
|
2021-12-28 05:54:28 -03:00
|
|
|
};
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
int sourceRead(void *_stream, unsigned char *_ptr, int _nbytes) {
|
2016-06-19 15:57:18 -04:00
|
|
|
id source = (__bridge id)_stream;
|
2013-10-05 06:01:33 -03:00
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
return (int)[source read:_ptr amount:_nbytes];
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
int sourceSeek(void *_stream, opus_int64 _offset, int _whence) {
|
2016-06-19 15:57:18 -04:00
|
|
|
id source = (__bridge id)_stream;
|
2013-10-05 06:01:33 -03:00
|
|
|
return ([source seek:_offset whence:_whence] ? 0 : -1);
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
int sourceClose(void *_stream) {
|
2013-10-05 06:01:33 -03:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
opus_int64 sourceTell(void *_stream) {
|
2016-06-19 15:57:18 -04:00
|
|
|
id source = (__bridge id)_stream;
|
2013-10-05 06:01:33 -03:00
|
|
|
|
|
|
|
return [source tell];
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (id)init {
|
|
|
|
self = [super init];
|
|
|
|
if(self) {
|
|
|
|
opusRef = NULL;
|
|
|
|
}
|
|
|
|
return self;
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (BOOL)open:(id<CogSource>)s {
|
2016-06-19 15:57:18 -04:00
|
|
|
source = s;
|
2022-02-07 02:49:27 -03:00
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
OpusFileCallbacks callbacks = {
|
2022-02-07 02:49:27 -03:00
|
|
|
.read = sourceRead,
|
|
|
|
.seek = sourceSeek,
|
|
|
|
.close = sourceClose,
|
|
|
|
.tell = sourceTell
|
2013-10-05 06:01:33 -03:00
|
|
|
};
|
2022-02-07 02:49:27 -03:00
|
|
|
|
|
|
|
int error;
|
|
|
|
opusRef = op_open_callbacks((__bridge void *)source, &callbacks, NULL, 0, &error);
|
|
|
|
|
|
|
|
if(!opusRef) {
|
2013-10-11 09:03:55 -03:00
|
|
|
DLog(@"FAILED TO OPEN OPUS FILE");
|
2013-10-05 06:01:33 -03:00
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
currentSection = lastSection = op_current_link(opusRef);
|
|
|
|
|
|
|
|
bitrate = (op_bitrate(opusRef, currentSection) / 1000.0);
|
|
|
|
channels = op_channel_count(opusRef, currentSection);
|
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
seekable = op_seekable(opusRef);
|
2022-02-07 02:49:27 -03:00
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
totalFrames = op_pcm_total(opusRef, -1);
|
2022-02-07 02:49:27 -03:00
|
|
|
|
2022-02-10 02:27:33 -03:00
|
|
|
const OpusHead *head = op_head(opusRef, -1);
|
|
|
|
const OpusTags *tags = op_tags(opusRef, -1);
|
|
|
|
|
|
|
|
int _track_gain = 0;
|
|
|
|
|
|
|
|
opus_tags_get_track_gain(tags, &_track_gain);
|
|
|
|
|
2022-02-12 12:16:59 -03:00
|
|
|
replayGainAlbumGain = ((double)head->output_gain / 256.0) + 5.0;
|
|
|
|
replayGainTrackGain = ((double)_track_gain / 256.0) + replayGainAlbumGain;
|
2022-02-10 02:27:33 -03:00
|
|
|
|
|
|
|
op_set_gain_offset(opusRef, OP_ABSOLUTE_GAIN, 0);
|
2013-10-05 06:01:33 -03:00
|
|
|
|
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
|
|
[self didChangeValueForKey:@"properties"];
|
2022-02-07 02:49:27 -03:00
|
|
|
|
2022-02-09 18:44:50 -03:00
|
|
|
artist = @"";
|
2022-02-12 12:16:59 -03:00
|
|
|
albumartist = @"";
|
|
|
|
album = @"";
|
2022-02-09 18:44:50 -03:00
|
|
|
title = @"";
|
2022-02-12 12:16:59 -03:00
|
|
|
genre = @"";
|
2022-06-17 13:27:17 -04:00
|
|
|
icygenre = @"";
|
|
|
|
icyalbum = @"";
|
|
|
|
icyartist = @"";
|
|
|
|
icytitle = @"";
|
2022-02-12 12:16:59 -03:00
|
|
|
year = @(0);
|
|
|
|
track = @(0);
|
|
|
|
disc = @(0);
|
|
|
|
albumArt = [NSData data];
|
2022-02-09 18:44:50 -03:00
|
|
|
[self updateMetadata];
|
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2022-02-12 12:16:59 -03:00
|
|
|
- (NSString *)parseTag:(NSString *)tag fromTags:(const OpusTags *)tags {
|
|
|
|
NSMutableArray *tagStrings = [[NSMutableArray alloc] init];
|
|
|
|
|
|
|
|
int tagCount = opus_tags_query_count(tags, [tag UTF8String]);
|
|
|
|
|
|
|
|
for(int i = 0; i < tagCount; ++i) {
|
|
|
|
const char *value = opus_tags_query(tags, [tag UTF8String], i);
|
|
|
|
[tagStrings addObject:[NSString stringWithUTF8String:value]];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [tagStrings componentsJoinedByString:@", "];
|
|
|
|
}
|
|
|
|
|
2022-02-09 18:44:50 -03:00
|
|
|
- (void)updateMetadata {
|
2022-02-12 12:16:59 -03:00
|
|
|
const OpusTags *tags = op_tags(opusRef, -1);
|
|
|
|
|
|
|
|
if(tags) {
|
|
|
|
NSString *_artist = [self parseTag:@"artist" fromTags:tags];
|
|
|
|
NSString *_albumartist = [self parseTag:@"albumartist" fromTags:tags];
|
|
|
|
NSString *_album = [self parseTag:@"album" fromTags:tags];
|
|
|
|
NSString *_title = [self parseTag:@"title" fromTags:tags];
|
|
|
|
NSString *_genre = [self parseTag:@"genre" fromTags:tags];
|
|
|
|
|
|
|
|
NSString *_yearDate = [self parseTag:@"date" fromTags:tags];
|
|
|
|
NSString *_yearYear = [self parseTag:@"year" fromTags:tags];
|
|
|
|
|
|
|
|
NSNumber *_year = @(0);
|
|
|
|
if([_yearDate length])
|
|
|
|
_year = @([_yearDate intValue]);
|
|
|
|
else if([_yearYear length])
|
|
|
|
_year = @([_yearYear intValue]);
|
|
|
|
|
|
|
|
NSString *_trackNumber = [self parseTag:@"tracknumber" fromTags:tags];
|
|
|
|
NSString *_trackNum = [self parseTag:@"tracknum" fromTags:tags];
|
|
|
|
NSString *_trackTrack = [self parseTag:@"track" fromTags:tags];
|
|
|
|
|
|
|
|
NSNumber *_track = @(0);
|
|
|
|
if([_trackNumber length])
|
|
|
|
_track = @([_trackNumber intValue]);
|
|
|
|
else if([_trackNum length])
|
|
|
|
_track = @([_trackNum intValue]);
|
|
|
|
else if([_trackTrack length])
|
|
|
|
_track = @([_trackTrack intValue]);
|
|
|
|
|
|
|
|
NSString *_discNumber = [self parseTag:@"discnumber" fromTags:tags];
|
|
|
|
NSString *_discNum = [self parseTag:@"discnum" fromTags:tags];
|
|
|
|
NSString *_discDisc = [self parseTag:@"disc" fromTags:tags];
|
|
|
|
|
|
|
|
NSNumber *_disc = @(0);
|
|
|
|
if([_discNumber length])
|
|
|
|
_disc = @([_discNumber intValue]);
|
|
|
|
else if([_discNum length])
|
|
|
|
_disc = @([_discNum intValue]);
|
|
|
|
else if([_discDisc length])
|
|
|
|
_disc = @([_discDisc intValue]);
|
|
|
|
|
|
|
|
NSData *_albumArt = [NSData data];
|
|
|
|
|
|
|
|
size_t count = opus_tags_query_count(tags, "METADATA_BLOCK_PICTURE");
|
|
|
|
if(count) {
|
|
|
|
const char *pictureTag = opus_tags_query(tags, "METADATA_BLOCK_PICTURE", 0);
|
|
|
|
OpusPictureTag _pic = { 0 };
|
|
|
|
if(opus_picture_tag_parse(&_pic, pictureTag) >= 0) {
|
|
|
|
if(_pic.format == OP_PIC_FORMAT_PNG ||
|
|
|
|
_pic.format == OP_PIC_FORMAT_JPEG ||
|
|
|
|
_pic.format == OP_PIC_FORMAT_GIF) {
|
|
|
|
_albumArt = [NSData dataWithBytes:_pic.data length:_pic.data_length];
|
2022-02-09 18:44:50 -03:00
|
|
|
}
|
2022-02-12 12:16:59 -03:00
|
|
|
opus_picture_tag_clear(&_pic);
|
2022-02-09 18:44:50 -03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-12 12:16:59 -03:00
|
|
|
if(![_artist isEqual:artist] ||
|
|
|
|
![_albumartist isEqual:albumartist] ||
|
|
|
|
![_album isEqual:album] ||
|
|
|
|
![_title isEqual:title] ||
|
|
|
|
![_genre isEqual:genre] ||
|
|
|
|
![_year isEqual:year] ||
|
|
|
|
![_track isEqual:year] ||
|
|
|
|
![_disc isEqual:disc] ||
|
|
|
|
![_albumArt isEqual:albumArt]) {
|
2022-02-09 18:44:50 -03:00
|
|
|
artist = _artist;
|
2022-02-12 12:16:59 -03:00
|
|
|
albumartist = _albumartist;
|
|
|
|
album = _album;
|
2022-02-09 18:44:50 -03:00
|
|
|
title = _title;
|
2022-02-12 12:16:59 -03:00
|
|
|
genre = _genre;
|
|
|
|
year = _year;
|
|
|
|
track = _track;
|
|
|
|
disc = _disc;
|
|
|
|
albumArt = _albumArt;
|
|
|
|
|
2022-02-09 18:44:50 -03:00
|
|
|
[self willChangeValueForKey:@"metadata"];
|
|
|
|
[self didChangeValueForKey:@"metadata"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)updateIcyMetadata {
|
2022-02-09 20:04:49 -03:00
|
|
|
if([source seekable]) return;
|
|
|
|
|
2022-06-17 13:27:17 -04:00
|
|
|
NSString *_genre = icygenre;
|
|
|
|
NSString *_album = icyalbum;
|
|
|
|
NSString *_artist = icyartist;
|
|
|
|
NSString *_title = icytitle;
|
2022-02-09 18:44:50 -03:00
|
|
|
|
|
|
|
Class sourceClass = [source class];
|
|
|
|
if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) {
|
|
|
|
HTTPSource *httpSource = (HTTPSource *)source;
|
|
|
|
if([httpSource hasMetadata]) {
|
|
|
|
NSDictionary *metadata = [httpSource metadata];
|
|
|
|
_genre = [metadata valueForKey:@"genre"];
|
|
|
|
_album = [metadata valueForKey:@"album"];
|
|
|
|
_artist = [metadata valueForKey:@"artist"];
|
|
|
|
_title = [metadata valueForKey:@"title"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-17 13:27:17 -04:00
|
|
|
if(![_genre isEqual:icygenre] ||
|
|
|
|
![_album isEqual:icyalbum] ||
|
|
|
|
![_artist isEqual:icyartist] ||
|
|
|
|
![_title isEqual:icytitle]) {
|
|
|
|
icygenre = _genre;
|
|
|
|
icyalbum = _album;
|
|
|
|
icyartist = _artist;
|
|
|
|
icytitle = _title;
|
2022-02-09 18:44:50 -03:00
|
|
|
[self willChangeValueForKey:@"metadata"];
|
|
|
|
[self didChangeValueForKey:@"metadata"];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (int)readAudio:(void *)buf frames:(UInt32)frames {
|
2013-10-05 06:01:33 -03:00
|
|
|
int numread;
|
|
|
|
int total = 0;
|
2022-02-07 02:49:27 -03:00
|
|
|
|
|
|
|
if(currentSection != lastSection) {
|
|
|
|
bitrate = (op_bitrate(opusRef, currentSection) / 1000.0);
|
2013-10-05 06:01:33 -03:00
|
|
|
channels = op_channel_count(opusRef, currentSection);
|
2022-02-07 02:49:27 -03:00
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
[self willChangeValueForKey:@"properties"];
|
|
|
|
[self didChangeValueForKey:@"properties"];
|
2022-02-09 18:44:50 -03:00
|
|
|
|
|
|
|
[self updateMetadata];
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
2022-02-07 02:49:27 -03:00
|
|
|
|
|
|
|
int size = frames * channels;
|
|
|
|
|
|
|
|
do {
|
|
|
|
float *out = ((float *)buf) + total;
|
|
|
|
float tempbuf[512 * channels];
|
2013-10-05 06:01:33 -03:00
|
|
|
lastSection = currentSection;
|
2022-02-07 02:49:27 -03:00
|
|
|
int toread = size - total;
|
|
|
|
if(toread > 512) toread = 512;
|
|
|
|
numread = op_read_float(opusRef, (channels < MAXCHANNELS) ? tempbuf : out, toread, NULL);
|
|
|
|
if(numread > 0 && channels <= MAXCHANNELS) {
|
|
|
|
for(int i = 0; i < numread; ++i) {
|
|
|
|
for(int j = 0; j < channels; ++j) {
|
|
|
|
out[i * channels + j] = tempbuf[i * channels + chmap[channels - 1][j]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
currentSection = op_current_link(opusRef);
|
|
|
|
if(numread > 0) {
|
2013-10-05 18:15:09 -03:00
|
|
|
total += numread * channels;
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
2022-02-07 02:49:27 -03:00
|
|
|
|
|
|
|
if(currentSection != lastSection) {
|
2013-10-05 06:01:33 -03:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
} while(total != size && numread != 0);
|
|
|
|
|
2022-02-09 18:44:50 -03:00
|
|
|
[self updateIcyMetadata];
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
return total / channels;
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (void)close {
|
|
|
|
op_free(opusRef);
|
|
|
|
opusRef = NULL;
|
2016-06-19 15:57:18 -04:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (void)dealloc {
|
|
|
|
[self close];
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (long)seek:(long)frame {
|
|
|
|
op_pcm_seek(opusRef, frame);
|
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
- (NSDictionary *)properties {
|
2022-06-17 09:39:02 -04:00
|
|
|
return @{ @"channels": @(channels),
|
|
|
|
@"bitsPerSample": @(32),
|
|
|
|
@"floatingPoint": @(YES),
|
|
|
|
@"sampleRate": @(48000),
|
|
|
|
@"totalFrames": @(totalFrames),
|
|
|
|
@"bitrate": @(bitrate),
|
|
|
|
@"seekable": @([source seekable] && seekable),
|
2022-02-12 12:16:59 -03:00
|
|
|
@"replayGainAlbumGain": @(replayGainAlbumGain),
|
|
|
|
@"replayGainTrackGain": @(replayGainTrackGain),
|
2022-02-10 02:27:33 -03:00
|
|
|
@"codec": @"Opus",
|
|
|
|
@"endian": @"host",
|
|
|
|
@"encoding": @"lossy" };
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-09 00:56:39 -03:00
|
|
|
- (NSDictionary *)metadata {
|
2022-06-17 13:27:17 -04:00
|
|
|
return [@{ @"artist": artist, @"albumartist": albumartist, @"album": album, @"title": title, @"genre": genre, @"year": year, @"track": track, @"disc": disc, @"albumArt": albumArt } dictionaryByMergingWith:@{ @"genre": icygenre, @"album": icyalbum, @"artist": icyartist, @"title": icytitle }];
|
2022-02-09 00:56:39 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
+ (NSArray *)fileTypes {
|
|
|
|
return @[@"opus", @"ogg"];
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
+ (NSArray *)mimeTypes {
|
2022-01-18 23:12:57 -03:00
|
|
|
return @[@"audio/x-opus+ogg", @"application/ogg"];
|
2013-10-05 06:01:33 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03: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 14:54:11 -03:00
|
|
|
}
|
|
|
|
|
2022-02-07 02:49:27 -03:00
|
|
|
+ (NSArray *)fileTypeAssociations {
|
|
|
|
return @[
|
|
|
|
@[@"Opus Audio File", @"ogg.icns", @"opus"],
|
|
|
|
@[@"Ogg Audio File", @"ogg.icns", @"ogg"]
|
|
|
|
];
|
2022-01-18 08:06:03 -03:00
|
|
|
}
|
|
|
|
|
2013-10-05 06:01:33 -03:00
|
|
|
@end
|