Implement dynamic metadata reading for streams

Supported by FFmpeg, FLAC, Ogg Vorbis, and Opus.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
CQTexperiment
Christopher Snowhill 2022-02-09 13:44:50 -08:00
parent cdf27e4786
commit 0012d1b17e
14 changed files with 470 additions and 57 deletions

View File

@ -163,7 +163,10 @@
if(amountInBuffer < CHUNK_SIZE) {
int framesToRead = CHUNK_SIZE - amountInBuffer;
int framesRead = [decoder readAudio:((char *)inputBuffer) + bytesInBuffer frames:framesToRead];
int framesRead;
@autoreleasepool {
framesRead = [decoder readAudio:((char *)inputBuffer) + bytesInBuffer frames:framesToRead];
}
if(framesRead > 0 && !seekError) {
amountInBuffer += framesRead;

View File

@ -66,8 +66,37 @@
17B5E1DE0CC074D3004E2AF4 /* window.c in Sources */ = {isa = PBXBuildFile; fileRef = 17B5E1970CC074D3004E2AF4 /* window.c */; };
17B5E2C00CC07904004E2AF4 /* stream_encoder.c in Sources */ = {isa = PBXBuildFile; fileRef = 17B5E1950CC074D3004E2AF4 /* stream_encoder.c */; };
17B5E2C10CC07905004E2AF4 /* stream_encoder_framing.c in Sources */ = {isa = PBXBuildFile; fileRef = 17B5E1960CC074D3004E2AF4 /* stream_encoder_framing.c */; };
8356BD0527B3A4C20074E50C /* ogg_mapping.c in Sources */ = {isa = PBXBuildFile; fileRef = 8356BD0127B3A4C20074E50C /* ogg_mapping.c */; };
8356BD0627B3A4C20074E50C /* ogg_encoder_aspect.c in Sources */ = {isa = PBXBuildFile; fileRef = 8356BD0227B3A4C20074E50C /* ogg_encoder_aspect.c */; };
8356BD0727B3A4C20074E50C /* ogg_decoder_aspect.c in Sources */ = {isa = PBXBuildFile; fileRef = 8356BD0327B3A4C20074E50C /* ogg_decoder_aspect.c */; };
8356BD0827B3A4C20074E50C /* ogg_helper.c in Sources */ = {isa = PBXBuildFile; fileRef = 8356BD0427B3A4C20074E50C /* ogg_helper.c */; };
8356BD1327B3A4E20074E50C /* Ogg.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8356BD0F27B3A4D90074E50C /* Ogg.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
8356BD0E27B3A4D90074E50C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8356BD0927B3A4D90074E50C /* Ogg.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 8D07F2C80486CC7A007CD1D0;
remoteInfo = Ogg;
};
8356BD1027B3A4D90074E50C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8356BD0927B3A4D90074E50C /* Ogg.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 734FB2E50B18B33E00D561D7;
remoteInfo = "libogg (static)";
};
8356BD1427B3A4E70074E50C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8356BD0927B3A4D90074E50C /* Ogg.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = 8D07F2BC0486CC7A007CD1D0;
remoteInfo = Ogg;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
0867D69BFE84028FC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
0867D6A5FE840307C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
@ -131,6 +160,11 @@
17B5E1950CC074D3004E2AF4 /* stream_encoder.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; path = stream_encoder.c; sourceTree = "<group>"; };
17B5E1960CC074D3004E2AF4 /* stream_encoder_framing.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; path = stream_encoder_framing.c; sourceTree = "<group>"; };
17B5E1970CC074D3004E2AF4 /* window.c */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.c; path = window.c; sourceTree = "<group>"; };
8356BD0127B3A4C20074E50C /* ogg_mapping.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ogg_mapping.c; sourceTree = "<group>"; };
8356BD0227B3A4C20074E50C /* ogg_encoder_aspect.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ogg_encoder_aspect.c; sourceTree = "<group>"; };
8356BD0327B3A4C20074E50C /* ogg_decoder_aspect.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ogg_decoder_aspect.c; sourceTree = "<group>"; };
8356BD0427B3A4C20074E50C /* ogg_helper.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ogg_helper.c; sourceTree = "<group>"; };
8356BD0927B3A4D90074E50C /* Ogg.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Ogg.xcodeproj; path = ../Ogg/macosx/Ogg.xcodeproj; sourceTree = "<group>"; };
8DC2EF5A0486A6940098B216 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
8DC2EF5B0486A6940098B216 /* FLAC.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FLAC.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D2F7E79907B2D74100F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
@ -141,6 +175,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8356BD1327B3A4E20074E50C /* Ogg.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -163,6 +198,7 @@
089C1665FE841158C02AAC07 /* Resources */,
0867D69AFE84028FC02AAC07 /* External Frameworks and Libraries */,
034768DFFF38A50411DB9C8B /* Products */,
8356BD1227B3A4E20074E50C /* Frameworks */,
);
name = flac;
sourceTree = "<group>";
@ -195,6 +231,7 @@
1058C7B2FEA5585E11CA2CBB /* Other Frameworks */ = {
isa = PBXGroup;
children = (
8356BD0927B3A4D90074E50C /* Ogg.xcodeproj */,
0867D6A5FE840307C02AAC07 /* AppKit.framework */,
D2F7E79907B2D74100F64583 /* CoreData.framework */,
0867D69BFE84028FC02AAC07 /* Foundation.framework */,
@ -280,9 +317,13 @@
17B5E1880CC074D3004E2AF4 /* memory.c */,
17B5E1890CC074D3004E2AF4 /* metadata_iterators.c */,
17B5E18A0CC074D3004E2AF4 /* metadata_object.c */,
8356BD0327B3A4C20074E50C /* ogg_decoder_aspect.c */,
8356BD0227B3A4C20074E50C /* ogg_encoder_aspect.c */,
8356BD0427B3A4C20074E50C /* ogg_helper.c */,
8356BD0127B3A4C20074E50C /* ogg_mapping.c */,
17B5E1940CC074D3004E2AF4 /* stream_decoder.c */,
17B5E1950CC074D3004E2AF4 /* stream_encoder.c */,
17B5E1960CC074D3004E2AF4 /* stream_encoder_framing.c */,
17B5E1950CC074D3004E2AF4 /* stream_encoder.c */,
17B5E1970CC074D3004E2AF4 /* window.c */,
);
path = libFLAC;
@ -333,6 +374,22 @@
path = protected;
sourceTree = "<group>";
};
8356BD0A27B3A4D90074E50C /* Products */ = {
isa = PBXGroup;
children = (
8356BD0F27B3A4D90074E50C /* Ogg.framework */,
8356BD1127B3A4D90074E50C /* libogg.a */,
);
name = Products;
sourceTree = "<group>";
};
8356BD1227B3A4E20074E50C /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -400,6 +457,7 @@
buildRules = (
);
dependencies = (
8356BD1527B3A4E70074E50C /* PBXTargetDependency */,
);
name = "FLAC Framework";
productInstallPath = "$(HOME)/Library/Frameworks";
@ -432,6 +490,12 @@
mainGroup = 0867D691FE84028FC02AAC07 /* flac */;
productRefGroup = 034768DFFF38A50411DB9C8B /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 8356BD0A27B3A4D90074E50C /* Products */;
ProjectRef = 8356BD0927B3A4D90074E50C /* Ogg.xcodeproj */;
},
);
projectRoot = "";
targets = (
8DC2EF4F0486A6940098B216 /* FLAC Framework */,
@ -439,6 +503,23 @@
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
8356BD0F27B3A4D90074E50C /* Ogg.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = Ogg.framework;
remoteRef = 8356BD0E27B3A4D90074E50C /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
8356BD1127B3A4D90074E50C /* libogg.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libogg.a;
remoteRef = 8356BD1027B3A4D90074E50C /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
8DC2EF520486A6940098B216 /* Resources */ = {
isa = PBXResourcesBuildPhase;
@ -455,6 +536,7 @@
buildActionMask = 2147483647;
files = (
17B5E1AC0CC074D3004E2AF4 /* bitmath.c in Sources */,
8356BD0827B3A4C20074E50C /* ogg_helper.c in Sources */,
17B5E1AD0CC074D3004E2AF4 /* bitreader.c in Sources */,
17B5E1AE0CC074D3004E2AF4 /* bitwriter.c in Sources */,
17B5E1AF0CC074D3004E2AF4 /* cpu.c in Sources */,
@ -464,9 +546,12 @@
17B5E1B30CC074D3004E2AF4 /* format.c in Sources */,
17B5E1D00CC074D3004E2AF4 /* lpc.c in Sources */,
17B5E1D10CC074D3004E2AF4 /* md5.c in Sources */,
8356BD0727B3A4C20074E50C /* ogg_decoder_aspect.c in Sources */,
17B5E1D20CC074D3004E2AF4 /* memory.c in Sources */,
17B5E1D30CC074D3004E2AF4 /* metadata_iterators.c in Sources */,
8356BD0527B3A4C20074E50C /* ogg_mapping.c in Sources */,
17B5E1D40CC074D3004E2AF4 /* metadata_object.c in Sources */,
8356BD0627B3A4C20074E50C /* ogg_encoder_aspect.c in Sources */,
17B5E1DB0CC074D3004E2AF4 /* stream_decoder.c in Sources */,
17B5E1DE0CC074D3004E2AF4 /* window.c in Sources */,
17B5E2C00CC07904004E2AF4 /* stream_encoder.c in Sources */,
@ -476,6 +561,14 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
8356BD1527B3A4E70074E50C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = Ogg;
targetProxy = 8356BD1427B3A4E70074E50C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
1DEB91AE08733DA50010E9CD /* Debug */ = {
isa = XCBuildConfiguration;
@ -488,6 +581,7 @@
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_SYMBOL_SEPARATION = YES;
@ -508,7 +602,7 @@
"$(OTHER_CFLAGS_QUOTED_1)",
"-D__MACOSX__",
"-DHAVE_LROUND",
"-DFLAC__HAS_OGG=0",
"-DFLAC__HAS_OGG=1",
);
OTHER_CFLAGS_QUOTED_1 = "-DVERSION=\\\"1.3.3\\\" -DPACKAGE_VERSION=\\\"1.3.3\\\"";
PRECOMPS_INCLUDE_HEADERS_FROM_BUILT_PRODUCTS_DIR = NO;
@ -535,6 +629,7 @@
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
GCC_ENABLE_SYMBOL_SEPARATION = YES;
GCC_MODEL_TUNING = G5;
@ -553,7 +648,7 @@
"$(OTHER_CFLAGS_QUOTED_1)",
"-D__MACOSX__",
"-DHAVE_LROUND",
"-DFLAC__HAS_OGG=0",
"-DFLAC__HAS_OGG=1",
);
OTHER_CFLAGS_QUOTED_1 = "-DVERSION=\\\"1.3.3\\\" -DPACKAGE_VERSION=\\\"1.3.3\\\"";
PRECOMPS_INCLUDE_HEADERS_FROM_BUILT_PRODUCTS_DIR = NO;

View File

@ -56,6 +56,7 @@
8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = "<group>"; };
8356BCE827B37C6F0074E50C /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = "<group>"; };
8356BCEA27B37DA40074E50C /* TagLibID3v2Reader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TagLibID3v2Reader.h; path = ../TagLib/TagLibID3v2Reader.h; sourceTree = "<group>"; };
8356BD1A27B3D06F0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
8384913818081F6C00E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
83AA7D08279EBCC600087AA4 /* libavcodec.59.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavcodec.59.dylib; path = ../../ThirdParty/ffmpeg/lib/libavcodec.59.dylib; sourceTree = "<group>"; };
83AA7D09279EBCC600087AA4 /* libavutil.57.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libavutil.57.dylib; path = ../../ThirdParty/ffmpeg/lib/libavutil.57.dylib; sourceTree = "<group>"; };
@ -139,6 +140,7 @@
08FB77AFFE84173DC02AAC07 /* Classes */ = {
isa = PBXGroup;
children = (
8356BD1A27B3D06F0074E50C /* HTTPSource.h */,
8356BCEA27B37DA40074E50C /* TagLibID3v2Reader.h */,
8356BCE827B37C6F0074E50C /* NSDictionary+Merge.h */,
8356BCE727B37C6F0074E50C /* NSDictionary+Merge.m */,

View File

@ -42,6 +42,7 @@
BOOL endOfAudio;
int metadataIndex;
NSString *genre;
NSString *artist;
NSString *title;
NSString *album;

View File

@ -16,6 +16,8 @@
#import "Logging.h"
#import "HTTPSource.h"
#define ST_BUFF 2048
int ffmpeg_read(void *opaque, uint8_t *buf, int buf_size) {
@ -82,8 +84,9 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
// register all available codecs
NSURL *url = [s url];
if([[url scheme] isEqualToString:@"http"] ||
[[url scheme] isEqualToString:@"https"]) {
if(([[url scheme] isEqualToString:@"http"] ||
[[url scheme] isEqualToString:@"https"]) &&
[[url pathExtension] isEqualToString:@"m3u8"]) {
source = nil;
[s close];
@ -95,20 +98,13 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
return NO;
}
AVDictionary *dict = NULL;
av_dict_set_int(&dict, "icy", 1, 0); // Enable Icy interval metadata, if supported
NSString *urlString = [url absoluteString];
if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, &dict)) < 0) {
av_dict_free(&dict);
if((errcode = avformat_open_input(&formatCtx, [urlString UTF8String], NULL, NULL)) < 0) {
char errDescr[4096];
av_strerror(errcode, errDescr, 4096);
ALog(@"Error opening file, errcode = %d, error = %s", errcode, errDescr);
return NO;
}
av_dict_free(&dict);
} else {
buffer = av_malloc(32 * 1024);
if(!buffer) {
@ -415,6 +411,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
seekable = [s seekable];
genre = @"";
album = @"";
artist = @"";
title = @"";
@ -465,6 +462,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
- (void)updateMetadata {
const AVDictionaryEntry *tag = NULL;
NSString *_genre = genre;
NSString *_album = album;
NSString *_artist = artist;
NSString *_title = title;
@ -481,6 +479,8 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
}
} else if(!strcasecmp(tag->key, "icy-url")) {
_album = [NSString stringWithUTF8String:tag->value];
} else if(!strcasecmp(tag->key, "icy-genre")) {
_genre = [NSString stringWithUTF8String:tag->value];
} else if(!strcasecmp(tag->key, "artist")) {
_artist = [NSString stringWithUTF8String:tag->value];
} else if(!strcasecmp(tag->key, "title")) {
@ -489,9 +489,23 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
}
}
if(![_album isEqual:album] ||
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"];
}
}
if(![_genre isEqual:genre] ||
![_album isEqual:album] ||
![_artist isEqual:artist] ||
![_title isEqual:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
@ -725,7 +739,7 @@ int64_t ffmpeg_seek(void *opaque, int64_t offset, int whence) {
}
- (NSDictionary *)metadata {
return [NSDictionary dictionaryByMerging:@{ @"album": album, @"artist": artist, @"title": title } with:id3Metadata];
return [NSDictionary dictionaryByMerging:@{ @"genre": genre, @"album": album, @"artist": artist, @"title": title } with:id3Metadata];
}
+ (NSArray *)fileTypes {

View File

@ -52,6 +52,7 @@
17C93F040B8FF67A008627D6 /* FlacDecoder.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = FlacDecoder.m; sourceTree = "<group>"; };
17F5641A0C3BDC460019975C /* flac.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = flac.xcodeproj; path = ../../Frameworks/FLAC/flac.xcodeproj; sourceTree = SOURCE_ROOT; };
32DBCF630370AF2F00C91783 /* Flac_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Flac_Prefix.pch; sourceTree = "<group>"; };
8356BD1927B3CCBB0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
8384912D180816C900E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
8D5B49B6048680CD000E48DA /* Flac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Flac.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -103,6 +104,7 @@
08FB77AFFE84173DC02AAC07 /* Classes */ = {
isa = PBXGroup;
children = (
8356BD1927B3CCBB0074E50C /* HTTPSource.h */,
8384912D180816C900E7332D /* Logging.h */,
177FCFC10B90C9960011C3B5 /* Plugin.h */,
17C93F030B8FF67A008627D6 /* FlacDecoder.h */,
@ -274,6 +276,7 @@
GCC_PREFIX_HEADER = Flac_Prefix.pch;
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(HOME)/Library/Bundles";
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.cogx.flac;
PRODUCT_NAME = Flac;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -301,6 +304,7 @@
GCC_PREFIX_HEADER = Flac_Prefix.pch;
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(HOME)/Library/Bundles";
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.cogx.flac;
PRODUCT_NAME = Flac;
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -34,6 +34,12 @@
BOOL hasStreamInfo;
BOOL streamOpened;
BOOL abortFlag;
NSString *genre;
NSString *album;
NSString *artist;
NSString *title;
}
- (void)setSource:(id<CogSource>)s;

View File

@ -10,30 +10,10 @@
#import "Logging.h"
#import "HTTPSource.h"
@implementation FlacDecoder
static const char *CHANNEL_MASK_TAG = "WAVEFORMATEXTENSIBLE_CHANNEL_MASK";
static FLAC__bool flac__utils_get_channel_mask_tag(const FLAC__StreamMetadata *object, FLAC__uint32 *channel_mask) {
int offset;
uint32_t val;
char *p;
FLAC__ASSERT(object);
FLAC__ASSERT(object->type == FLAC__METADATA_TYPE_VORBIS_COMMENT);
if(0 > (offset = FLAC__metadata_object_vorbiscomment_find_entry_from(object, /*offset=*/0, CHANNEL_MASK_TAG)))
return false;
if(object->data.vorbis_comment.comments[offset].length < strlen(CHANNEL_MASK_TAG) + 4)
return false;
if(0 == (p = strchr((const char *)object->data.vorbis_comment.comments[offset].entry, '='))) /* should never happen, but just in case */
return false;
if(strncasecmp(p, "=0x", 3))
return false;
if(sscanf(p + 3, "%x", &val) != 1)
return false;
*channel_mask = val;
return true;
}
FLAC__StreamDecoderReadStatus ReadCallback(const FLAC__StreamDecoder *decoder, FLAC__byte blockBuffer[], size_t *bytes, void *client_data) {
FlacDecoder *flacDecoder = (__bridge FlacDecoder *)client_data;
long bytesRead = [[flacDecoder source] read:blockBuffer amount:*bytes];
@ -99,6 +79,9 @@ FLAC__StreamDecoderLengthStatus LengthCallback(const FLAC__StreamDecoder *decode
FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const sampleblockBuffer[], void *client_data) {
FlacDecoder *flacDecoder = (__bridge FlacDecoder *)client_data;
if(flacDecoder->abortFlag)
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
uint32_t channels = frame->header.channels;
uint32_t bitsPerSample = frame->header.bits_per_sample;
uint32_t frequency = frame->header.sample_rate;
@ -200,7 +183,7 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta
// to determine stream format (this seems to be consistent with flac spec: http://flac.sourceforge.net/format.html)
FlacDecoder *flacDecoder = (__bridge FlacDecoder *)client_data;
if(!flacDecoder->hasStreamInfo) {
if(!flacDecoder->hasStreamInfo && metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
flacDecoder->channels = metadata->data.stream_info.channels;
flacDecoder->channelConfig = 0;
flacDecoder->frequency = metadata->data.stream_info.sample_rate;
@ -212,12 +195,59 @@ void MetadataCallback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMeta
}
if(metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) {
(void)flac__utils_get_channel_mask_tag(metadata, &flacDecoder->channelConfig);
NSString *_genre = flacDecoder->genre;
NSString *_album = flacDecoder->album;
NSString *_artist = flacDecoder->artist;
NSString *_title = flacDecoder->title;
uint8_t nullByte = '\0';
const FLAC__StreamMetadata_VorbisComment *vorbis_comment = &metadata->data.vorbis_comment;
for(int i = 0; i < vorbis_comment->num_comments; ++i) {
NSMutableData *commentField = [NSMutableData dataWithBytes:vorbis_comment->comments[i].entry length:vorbis_comment->comments[i].length];
[commentField appendBytes:&nullByte length:1];
NSString *commentString = [NSString stringWithUTF8String:[commentField bytes]];
NSArray *splitFields = [commentString componentsSeparatedByString:@"="];
if([splitFields count] == 2) {
NSString *name = [splitFields objectAtIndex:0];
NSString *value = [splitFields objectAtIndex:1];
name = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
name = [name lowercaseString];
if([name isEqualToString:@"genre"]) {
_genre = value;
} else if([name isEqualToString:@"album"]) {
_album = value;
} else if([name isEqualToString:@"artist"]) {
_artist = value;
} else if([name isEqualToString:@"title"]) {
_title = value;
} else if([name isEqualToString:@"waveformatextensible_channel_mask"]) {
if([value hasPrefix:@"0x"]) {
char *end;
const char *_value = [value UTF8String] + 2;
flacDecoder->channelConfig = (uint32_t)strtoul(_value, &end, 16);
}
}
}
}
if(![_genre isEqual:flacDecoder->genre] ||
![_album isEqual:flacDecoder->album] ||
![_artist isEqual:flacDecoder->artist] ||
![_title isEqual:flacDecoder->title]) {
flacDecoder->genre = _genre;
flacDecoder->album = _album;
flacDecoder->artist = _artist;
flacDecoder->title = _title;
[flacDecoder willChangeValueForKey:@"metadata"];
[flacDecoder didChangeValueForKey:@"metadata"];
}
}
}
void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) {
// Do nothing?
FlacDecoder *flacDecoder = (__bridge FlacDecoder *)client_data;
if(status != FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC)
flacDecoder->abortFlag = YES;
}
- (BOOL)open:(id<CogSource>)s {
@ -230,20 +260,61 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
[s seek:0 whence:SEEK_SET];
}
// Must peek at stream! HTTP reader supports seeking within its buffer
BOOL isOggFlac = NO;
uint8_t buffer[4];
[s read:buffer amount:4];
[s seek:0 whence:SEEK_SET];
if(memcmp(buffer, "OggS", 4) == 0) {
isOggFlac = YES;
}
genre = @"";
album = @"";
artist = @"";
title = @"";
decoder = FLAC__stream_decoder_new();
if(decoder == NULL)
return NO;
if(FLAC__stream_decoder_init_stream(decoder,
ReadCallback,
([source seekable] ? SeekCallback : NULL),
([source seekable] ? TellCallback : NULL),
([source seekable] ? LengthCallback : NULL),
([source seekable] ? EOFCallback : NULL),
WriteCallback,
MetadataCallback,
ErrorCallback,
(__bridge void *)(self)) != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
if(![source seekable]) {
FLAC__stream_decoder_set_md5_checking(decoder, false);
}
FLAC__stream_decoder_set_metadata_ignore_all(decoder);
FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_VORBIS_COMMENT);
abortFlag = NO;
FLAC__StreamDecoderInitStatus ret;
if(isOggFlac) {
ret = FLAC__stream_decoder_init_ogg_stream(decoder,
ReadCallback,
([source seekable] ? SeekCallback : NULL),
([source seekable] ? TellCallback : NULL),
([source seekable] ? LengthCallback : NULL),
([source seekable] ? EOFCallback : NULL),
WriteCallback,
MetadataCallback,
ErrorCallback,
(__bridge void *)(self));
} else {
ret = FLAC__stream_decoder_init_stream(decoder,
ReadCallback,
([source seekable] ? SeekCallback : NULL),
([source seekable] ? TellCallback : NULL),
([source seekable] ? LengthCallback : NULL),
([source seekable] ? EOFCallback : NULL),
WriteCallback,
MetadataCallback,
ErrorCallback,
(__bridge void *)(self));
}
if(ret != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
return NO;
}
@ -271,7 +342,9 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
break;
}
FLAC__stream_decoder_process_single(decoder);
if(!FLAC__stream_decoder_process_single(decoder)) {
break;
}
}
int bytesPerFrame = ((bitsPerSample + 7) / 8) * channels;
@ -292,6 +365,29 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
}
}
Class sourceClass = [source class];
if([sourceClass isEqual:NSClassFromString(@"HTTPSource")]) {
HTTPSource *httpSource = (HTTPSource *)source;
if([httpSource hasMetadata]) {
NSDictionary *metadata = [httpSource metadata];
NSString *_genre = [metadata valueForKey:@"genre"];
NSString *_album = [metadata valueForKey:@"album"];
NSString *_artist = [metadata valueForKey:@"artist"];
NSString *_title = [metadata valueForKey:@"title"];
if(![_genre isEqualToString:genre] ||
![_album isEqualToString:album] ||
![_artist isEqualToString:artist] ||
![_title isEqualToString:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
}
return framesRead;
}
@ -367,7 +463,7 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
}
- (NSDictionary *)metadata {
return @{};
return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title };
}
+ (NSArray *)fileTypes {
@ -375,7 +471,7 @@ void ErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorS
}
+ (NSArray *)mimeTypes {
return @[@"audio/x-flac"];
return @[@"audio/x-flac", @"application/ogg", @"audio/ogg"];
}
+ (float)priority {

View File

@ -29,6 +29,11 @@
int bitrate;
int channels;
long totalFrames;
NSString *genre;
NSString *album;
NSString *artist;
NSString *title;
}
@end

View File

@ -12,6 +12,8 @@
#import "Logging.h"
#import "HTTPSource.h"
@implementation OpusFile
static const int MAXCHANNELS = 8;
@ -115,9 +117,92 @@ opus_int64 sourceTell(void *_stream) {
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
genre = @"";
album = @"";
artist = @"";
title = @"";
[self updateMetadata];
return YES;
}
- (void)updateMetadata {
const OpusTags *comment = op_tags(opusRef, -1);
if(comment) {
uint8_t nullByte = '\0';
NSString *_genre = genre;
NSString *_album = album;
NSString *_artist = artist;
NSString *_title = title;
for(int i = 0; i < comment->comments; ++i) {
NSMutableData *commentField = [NSMutableData dataWithBytes:comment->user_comments[i] length:comment->comment_lengths[i]];
[commentField appendBytes:&nullByte length:1];
NSString *commentString = [NSString stringWithUTF8String:[commentField bytes]];
NSArray *splitFields = [commentString componentsSeparatedByString:@"="];
if([splitFields count] == 2) {
NSString *name = [splitFields objectAtIndex:0];
NSString *value = [splitFields objectAtIndex:1];
name = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
name = [name lowercaseString];
if([name isEqualToString:@"genre"]) {
_genre = value;
} else if([name isEqualToString:@"album"]) {
_album = value;
} else if([name isEqualToString:@"artist"]) {
_artist = value;
} else if([name isEqualToString:@"title"]) {
_title = value;
}
}
}
if(![_genre isEqual:genre] ||
![_album isEqual:album] ||
![_artist isEqual:artist] ||
![_title isEqual:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
}
- (void)updateIcyMetadata {
NSString *_genre = genre;
NSString *_album = album;
NSString *_artist = artist;
NSString *_title = title;
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"];
}
}
if(![_genre isEqual:genre] ||
![_album isEqual:album] ||
![_artist isEqual:artist] ||
![_title isEqual:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
- (int)readAudio:(void *)buf frames:(UInt32)frames {
int numread;
int total = 0;
@ -128,6 +213,8 @@ opus_int64 sourceTell(void *_stream) {
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
[self updateMetadata];
}
int size = frames * channels;
@ -157,6 +244,8 @@ opus_int64 sourceTell(void *_stream) {
} while(total != size && numread != 0);
[self updateIcyMetadata];
return total / channels;
}
@ -189,7 +278,7 @@ opus_int64 sourceTell(void *_stream) {
}
- (NSDictionary *)metadata {
return @{};
return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title };
}
+ (NSArray *)fileTypes {

View File

@ -46,6 +46,7 @@
/* Begin PBXFileReference section */
833F68411CDBCABC00AFB9F0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
8356BD1B27B469B80074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
8375B03C17FFEA400092A79F /* OpusPlugin.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpusPlugin.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
8375B03F17FFEA400092A79F /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
8375B04217FFEA400092A79F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
@ -122,6 +123,7 @@
8375B04517FFEA400092A79F /* Opus */ = {
isa = PBXGroup;
children = (
8356BD1B27B469B80074E50C /* HTTPSource.h */,
8384913718081F2700E7332D /* Logging.h */,
8375B36D17FFF1FE0092A79F /* Plugin.h */,
8375B36A17FFF1CB0092A79F /* OpusDecoder.h */,

View File

@ -31,6 +31,11 @@
int channels;
float frequency;
long totalFrames;
NSString *genre;
NSString *album;
NSString *artist;
NSString *title;
}
@end

View File

@ -10,6 +10,8 @@
#import "Logging.h"
#import "HTTPSource.h"
@implementation VorbisDecoder
static const int MAXCHANNELS = 8;
@ -95,9 +97,92 @@ long sourceTell(void *datasource) {
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
genre = @"";
album = @"";
artist = @"";
title = @"";
[self updateMetadata];
return YES;
}
- (void)updateMetadata {
const vorbis_comment *comment = ov_comment(&vorbisRef, -1);
if(comment) {
uint8_t nullByte = '\0';
NSString *_genre = genre;
NSString *_album = album;
NSString *_artist = artist;
NSString *_title = title;
for(int i = 0; i < comment->comments; ++i) {
NSMutableData *commentField = [NSMutableData dataWithBytes:comment->user_comments[i] length:comment->comment_lengths[i]];
[commentField appendBytes:&nullByte length:1];
NSString *commentString = [NSString stringWithUTF8String:[commentField bytes]];
NSArray *splitFields = [commentString componentsSeparatedByString:@"="];
if([splitFields count] == 2) {
NSString *name = [splitFields objectAtIndex:0];
NSString *value = [splitFields objectAtIndex:1];
name = [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
name = [name lowercaseString];
if([name isEqualToString:@"genre"]) {
_genre = value;
} else if([name isEqualToString:@"album"]) {
_album = value;
} else if([name isEqualToString:@"artist"]) {
_artist = value;
} else if([name isEqualToString:@"title"]) {
_title = value;
}
}
}
if(![_genre isEqual:genre] ||
![_album isEqual:album] ||
![_artist isEqual:artist] ||
![_title isEqual:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
}
- (void)updateIcyMetadata {
NSString *_genre = genre;
NSString *_album = album;
NSString *_artist = artist;
NSString *_title = title;
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"];
}
}
if(![_genre isEqual:genre] ||
![_album isEqual:album] ||
![_artist isEqual:artist] ||
![_title isEqual:title]) {
genre = _genre;
album = _album;
artist = _artist;
title = _title;
[self willChangeValueForKey:@"metadata"];
[self didChangeValueForKey:@"metadata"];
}
}
- (int)readAudio:(void *)buf frames:(UInt32)frames {
int numread;
int total = 0;
@ -112,6 +197,8 @@ long sourceTell(void *datasource) {
[self willChangeValueForKey:@"properties"];
[self didChangeValueForKey:@"properties"];
[self updateMetadata];
}
do {
@ -141,6 +228,8 @@ long sourceTell(void *datasource) {
} while(total != frames && numread != 0);
[self updateIcyMetadata];
return total;
}
@ -172,7 +261,7 @@ long sourceTell(void *datasource) {
}
- (NSDictionary *)metadata {
return @{};
return @{ @"genre": genre, @"album": album, @"artist": artist, @"title": title };
}
+ (NSArray *)fileTypes {
@ -180,7 +269,7 @@ long sourceTell(void *datasource) {
}
+ (NSArray *)mimeTypes {
return @[@"application/ogg", @"application/x-ogg", @"audio/x-vorbis+ogg"];
return @[@"application/ogg", @"application/x-ogg", @"audio/ogg", @"audio/x-vorbis+ogg"];
}
+ (float)priority {

View File

@ -59,6 +59,7 @@
17C93D340B8FDA66008627D6 /* VorbisDecoder.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = VorbisDecoder.m; sourceTree = "<group>"; };
17F562EF0C3BDAAC0019975C /* Vorbis.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Vorbis.xcodeproj; path = ../../Frameworks/Vorbis/macosx/Vorbis.xcodeproj; sourceTree = SOURCE_ROOT; };
32DBCF630370AF2F00C91783 /* VorbisPlugin_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VorbisPlugin_Prefix.pch; sourceTree = "<group>"; };
8356BD1C27B46A2D0074E50C /* HTTPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPSource.h; path = ../HTTPSource/HTTPSource.h; sourceTree = "<group>"; };
8384913418081A3900E7332D /* Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Logging.h; path = ../../Utils/Logging.h; sourceTree = "<group>"; };
8D5B49B6048680CD000E48DA /* VorbisPlugin.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VorbisPlugin.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
8D5B49B7048680CD000E48DA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -111,6 +112,7 @@
08FB77AFFE84173DC02AAC07 /* Classes */ = {
isa = PBXGroup;
children = (
8356BD1C27B46A2D0074E50C /* HTTPSource.h */,
8384913418081A3900E7332D /* Logging.h */,
177FCF9D0B90C9530011C3B5 /* Plugin.h */,
17C93D330B8FDA66008627D6 /* VorbisDecoder.h */,