5 // Created on 2010-08-30.
9 #import "HBAudioController.h"
12 NSString *keyAudioCodecName = @"keyAudioCodecName";
13 NSString *keyAudioMP4 = @"keyAudioMP4";
14 NSString *keyAudioMKV = @"keyAudioMKV";
15 NSString *keyAudioSampleRateName = @"keyAudioSampleRateName";
16 NSString *keyAudioBitrateName = @"keyAudioBitrateName";
17 NSString *keyAudioMustMatchTrack = @"keyAudioMustMatchTrack";
18 NSString *keyAudioMixdownName = @"keyAudioMixdownName";
20 NSString *keyAudioCodec = @"codec";
21 NSString *keyAudioMixdown = @"mixdown";
22 NSString *keyAudioSamplerate = @"samplerate";
23 NSString *keyAudioBitrate = @"bitrate";
25 static NSMutableArray *masterCodecArray = nil;
26 static NSMutableArray *masterMixdownArray = nil;
27 static NSMutableArray *masterSampleRateArray = nil;
28 static NSMutableArray *masterBitRateArray = nil;
30 @interface NSArray (HBAudioSupport)
31 - (NSDictionary *) dictionaryWithObject: (id) anObject matchingKey: (NSString *) aKey;
32 - (NSDictionary *) lastDictionaryWithObject: (id) anObject matchingKey: (NSString *) aKey;
34 @implementation NSArray (HBAudioSupport)
35 - (NSDictionary *) dictionaryWithObject: (id) anObject matchingKey: (NSString *) aKey reverse: (BOOL) reverse
38 NSDictionary *retval = nil;
39 NSEnumerator *enumerator = reverse ? [self reverseObjectEnumerator] : [self objectEnumerator];
43 while (nil != (dict = [enumerator nextObject]) && nil == retval) {
44 if (nil != (aValue = [dict objectForKey: aKey]) && YES == [aValue isEqual: anObject]) {
50 - (NSDictionary *) dictionaryWithObject: (id) anObject matchingKey: (NSString *) aKey
51 { return [self dictionaryWithObject: anObject matchingKey: aKey reverse: NO]; }
52 - (NSDictionary *) lastDictionaryWithObject: (id) anObject matchingKey: (NSString *) aKey
53 { return [self dictionaryWithObject: anObject matchingKey: aKey reverse: YES]; }
57 @implementation HBAudio
60 #pragma mark Object Setup
65 if ([HBAudio class] == self) {
67 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
70 masterCodecArray = [[NSMutableArray alloc] init]; // knowingly leaked
71 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
72 NSLocalizedString(@"AAC (CoreAudio)", @"AAC (CoreAudio)"), keyAudioCodecName,
73 [NSNumber numberWithInt: HB_ACODEC_CA_AAC], keyAudioCodec,
74 [NSNumber numberWithBool: YES], keyAudioMP4,
75 [NSNumber numberWithBool: YES], keyAudioMKV,
76 [NSNumber numberWithBool: NO], keyAudioMustMatchTrack,
78 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
79 NSLocalizedString(@"AAC (faac)", @"AAC (faac)"), keyAudioCodecName,
80 [NSNumber numberWithInt: HB_ACODEC_FAAC], keyAudioCodec,
81 [NSNumber numberWithBool: YES], keyAudioMP4,
82 [NSNumber numberWithBool: YES], keyAudioMKV,
83 [NSNumber numberWithBool: NO], keyAudioMustMatchTrack,
85 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
86 NSLocalizedString(@"MP3 (lame)", @"MP3 (lame)"), keyAudioCodecName,
87 [NSNumber numberWithInt: HB_ACODEC_LAME], keyAudioCodec,
88 [NSNumber numberWithBool: YES], keyAudioMP4,
89 [NSNumber numberWithBool: YES], keyAudioMKV,
90 [NSNumber numberWithBool: NO], keyAudioMustMatchTrack,
92 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
93 NSLocalizedString(@"AC3 Passthru", @"AC3 Passthru"), keyAudioCodecName,
94 [NSNumber numberWithInt: HB_ACODEC_AC3_PASS], keyAudioCodec,
95 [NSNumber numberWithBool: YES], keyAudioMP4,
96 [NSNumber numberWithBool: YES], keyAudioMKV,
97 [NSNumber numberWithInt: HB_ACODEC_AC3], keyAudioMustMatchTrack,
99 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
100 NSLocalizedString(@"AC3", @"AC3"), keyAudioCodecName,
101 [NSNumber numberWithInt: HB_ACODEC_AC3], keyAudioCodec,
102 [NSNumber numberWithBool: YES], keyAudioMP4,
103 [NSNumber numberWithBool: YES], keyAudioMKV,
104 [NSNumber numberWithBool: NO], keyAudioMustMatchTrack,
106 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
107 NSLocalizedString(@"DTS Passthru", @"DTS Passthru"), keyAudioCodecName,
108 [NSNumber numberWithInt: HB_ACODEC_DCA_PASS], keyAudioCodec,
109 [NSNumber numberWithBool: NO], keyAudioMP4,
110 [NSNumber numberWithBool: YES], keyAudioMKV,
111 [NSNumber numberWithInt: HB_ACODEC_DCA], keyAudioMustMatchTrack,
113 [masterCodecArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
114 NSLocalizedString(@"Vorbis (vorbis)", @"Vorbis (vorbis)"), keyAudioCodecName,
115 [NSNumber numberWithInt: HB_ACODEC_VORBIS], keyAudioCodec,
116 [NSNumber numberWithBool: NO], keyAudioMP4,
117 [NSNumber numberWithBool: YES], keyAudioMKV,
118 [NSNumber numberWithBool: NO], keyAudioMustMatchTrack,
121 masterMixdownArray = [[NSMutableArray alloc] init]; // knowingly leaked
122 [masterMixdownArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
123 NSLocalizedString(@"AC3 Passthru", @"AC3 Passthru"), keyAudioMixdownName,
124 [NSNumber numberWithInt: HB_ACODEC_AC3_PASS], keyAudioMixdown,
126 [masterMixdownArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
127 NSLocalizedString(@"DTS Passthru", @"DTS Passthru"), keyAudioMixdownName,
128 [NSNumber numberWithInt: HB_ACODEC_DCA_PASS], keyAudioMixdown,
130 for (i = 0; i < hb_audio_mixdowns_count; i++) {
131 [masterMixdownArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
132 [NSString stringWithUTF8String: hb_audio_mixdowns[i].human_readable_name], keyAudioMixdownName,
133 [NSNumber numberWithInt: hb_audio_mixdowns[i].amixdown], keyAudioMixdown,
137 // Note that for the Auto value we use 0 for the sample rate because our controller will give back the track's
138 // input sample rate when it finds this 0 value as the selected sample rate. We do this because the input
139 // sample rate depends on the track, which means it depends on the title, so cannot be nicely set up here.
140 masterSampleRateArray = [[NSMutableArray alloc] init]; // knowingly leaked
141 [masterSampleRateArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
142 NSLocalizedString(@"Auto", @"Auto"), keyAudioSampleRateName,
143 [NSNumber numberWithInt: 0], keyAudioSamplerate,
145 for (i = 0; i < hb_audio_rates_count; i++) {
146 [masterSampleRateArray addObject: [NSDictionary dictionaryWithObjectsAndKeys:
147 [NSString stringWithUTF8String: hb_audio_rates[i].string], keyAudioSampleRateName,
148 [NSNumber numberWithInt: hb_audio_rates[i].rate], keyAudioSamplerate,
152 masterBitRateArray = [[NSMutableArray alloc] init]; // knowingly leaked
153 for (i = 0; i < hb_audio_bitrates_count; i++) {
154 dict = [NSDictionary dictionaryWithObjectsAndKeys:
155 [NSString stringWithUTF8String: hb_audio_bitrates[i].string], keyAudioBitrateName,
156 [NSNumber numberWithInt: hb_audio_bitrates[i].rate], keyAudioBitrate,
158 [masterBitRateArray addObject: dict];
166 // Ensure the list of codecs is accurate
167 // Update the current value of codec based on the revised list
168 - (void) updateCodecs
171 NSMutableArray *permittedCodecs = [NSMutableArray array];
172 unsigned int count = [masterCodecArray count];
174 NSString *keyThatAllows = nil;
176 // Determine which key we use to see which codecs are permitted
177 switch ([videoContainerTag intValue]) {
179 keyThatAllows = keyAudioMP4;
182 keyThatAllows = keyAudioMKV;
185 keyThatAllows = @"error condition";
189 // First get a list of the permitted codecs based on the internal rules
190 if (nil != track && YES == [self enabled]) {
193 for (unsigned int i = 0; i < count; i++) {
194 dict = [masterCodecArray objectAtIndex: i];
196 // First make sure only codecs permitted by the container are here
197 goodToAdd = [[dict objectForKey: keyThatAllows] boolValue];
199 // Now we make sure if DTS or AC3 is not available in the track it is not put in the codec list, but in a general way
200 if (YES == [[dict objectForKey: keyAudioMustMatchTrack] boolValue]) {
201 if ([[dict objectForKey: keyAudioMustMatchTrack] intValue] != [[[self track] objectForKey: keyAudioInputCodec] intValue]) {
206 if (YES == goodToAdd) {
207 [permittedCodecs addObject: dict];
212 // Now make sure the permitted list and the actual ones matches
213 [self setCodecs: permittedCodecs];
215 // Ensure our codec is on the list of permitted codecs
216 if (nil == [self codec] || NO == [permittedCodecs containsObject: [self codec]]) {
217 if (0 < [permittedCodecs count]) {
218 [self setCodec: [permittedCodecs objectAtIndex: 0]]; // This should be defaulting to Core Audio
221 [self setCodec: nil];
228 - (void) updateMixdowns: (BOOL) shouldSetDefault
231 NSMutableArray *permittedMixdowns = [NSMutableArray array];
236 unsigned int count = [masterMixdownArray count];
237 int codecCodec = [[codec objectForKey: keyAudioCodec] intValue];
238 int channelLayout = [[track objectForKey: keyAudioInputChannelLayout] intValue];
239 int theDefaultMixdown = hb_get_default_mixdown(codecCodec, channelLayout);
240 int theBestMixdown = hb_get_best_mixdown(codecCodec, channelLayout, 0);
242 for (unsigned int i = 0; i < count; i++) {
243 dict = [masterMixdownArray objectAtIndex: i];
244 currentMixdown = [[dict objectForKey: keyAudioMixdown] intValue];
246 // Basically with the way the mixdowns are stored, the assumption from the libhb point of view
247 // currently is that all mixdowns from the best down to mono are supported.
248 if (currentMixdown <= theBestMixdown) {
250 } else if (0 == theBestMixdown && codecCodec == currentMixdown) {
251 // 0 means passthrough, add the current mixdown if it matches the passthrough codec
257 if (YES == shouldAdd) {
258 [permittedMixdowns addObject: dict];
262 if (0 == theDefaultMixdown) {
263 // a mixdown of 0 means passthrough
264 theDefaultMixdown = codecCodec;
267 if (NO == [self enabled]) {
268 permittedMixdowns = nil;
271 // Now make sure the permitted list and the actual ones matches
272 [self setMixdowns: permittedMixdowns];
274 // Select the proper one
275 if (YES == shouldSetDefault) {
276 [self setMixdown: [permittedMixdowns dictionaryWithObject: [NSNumber numberWithInt: theDefaultMixdown] matchingKey: keyAudioMixdown]];
279 if (nil == [self mixdown] || NO == [permittedMixdowns containsObject: [self mixdown]]) {
280 [self setMixdown: [permittedMixdowns lastObject]];
286 - (void) updateBitRates: (BOOL) shouldSetDefault
289 NSMutableArray *permittedBitRates = [NSMutableArray array];
296 unsigned int count = [masterBitRateArray count];
297 int trackInputBitRate = [[[self track] objectForKey: keyAudioInputBitrate] intValue];
298 BOOL limitsToTrackInputBitRate = ([[codec objectForKey: keyAudioCodec] intValue] & HB_ACODEC_PASS_FLAG) ? YES : NO;
299 int theSampleRate = [[[self sampleRate] objectForKey: keyAudioSamplerate] intValue];
301 if (0 == theSampleRate) { // this means Auto
302 theSampleRate = [[[self track] objectForKey: keyAudioInputSampleRate] intValue];
305 int ourCodec = [[codec objectForKey: keyAudioCodec] intValue];
306 int ourMixdown = [[[self mixdown] objectForKey: keyAudioMixdown] intValue];
307 hb_get_audio_bitrate_limits(ourCodec, theSampleRate, ourMixdown, &minBitRate, &maxBitRate);
308 int theDefaultBitRate = hb_get_default_audio_bitrate(ourCodec, theSampleRate, ourMixdown);
310 for (unsigned int i = 0; i < count; i++) {
311 dict = [masterBitRateArray objectAtIndex: i];
312 currentBitRate = [[dict objectForKey: keyAudioBitrate] intValue];
314 // First ensure the bitrate falls within range of the codec
315 shouldAdd = (currentBitRate >= minBitRate && currentBitRate <= maxBitRate);
317 // Now make sure the mixdown is not limiting us to the track input bitrate
318 if (YES == shouldAdd && YES == limitsToTrackInputBitRate) {
319 if (currentBitRate != trackInputBitRate) {
324 if (YES == shouldAdd) {
325 [permittedBitRates addObject: dict];
329 // There is a situation where we have a mixdown requirement to match the track input bit rate,
330 // but it does not fall into the range the codec supports. Therefore, we force it here.
331 if (YES == limitsToTrackInputBitRate && 0 == [permittedBitRates count]) {
332 NSDictionary *missingBitRate = [masterBitRateArray dictionaryWithObject: [NSNumber numberWithInt: trackInputBitRate] matchingKey: keyAudioBitrate];
333 if (nil == missingBitRate) {
334 // We are in an even worse situation where the requested bit rate does not even exist in the underlying
335 // library of supported bitrates. Of course since this value is ignored we can freely make a bogus one
336 // for the UI just to make the user a little more aware.
337 missingBitRate = [NSDictionary dictionaryWithObjectsAndKeys:
338 [NSString stringWithFormat: @"%d", trackInputBitRate], keyAudioBitrateName,
339 [NSNumber numberWithInt: trackInputBitRate], keyAudioBitrate,
342 [permittedBitRates addObject: missingBitRate];
345 if (NO == [self enabled]) {
346 permittedBitRates = nil;
349 // Make sure we are updated with the permitted list
350 [self setBitRates: permittedBitRates];
352 // Select the proper one
353 if (YES == shouldSetDefault) {
354 [self setBitRateFromName: [NSString stringWithFormat:@"%d", theDefaultBitRate]];
357 if (nil == [self bitRate] || NO == [permittedBitRates containsObject: [self bitRate]]) {
358 [self setBitRate: [permittedBitRates lastObject]];
367 if (self = [super init]) {
368 [self addObserver: self forKeyPath: @"videoContainerTag" options: 0 context: NULL];
369 [self addObserver: self forKeyPath: @"track" options: NSKeyValueObservingOptionOld context: NULL];
370 [self addObserver: self forKeyPath: @"codec" options: 0 context: NULL];
371 [self addObserver: self forKeyPath: @"mixdown" options: 0 context: NULL];
372 [self addObserver: self forKeyPath: @"sampleRate" options: 0 context: NULL];
378 #pragma mark Accessors
383 @synthesize sampleRate;
386 @synthesize videoContainerTag;
387 @synthesize controller;
390 @synthesize mixdowns;
391 @synthesize bitRates;
393 - (NSArray *) tracks { return [controller masterTrackArray]; }
395 - (NSArray *) sampleRates { return masterSampleRateArray; }
400 [self removeObserver: self forKeyPath: @"videoContainerTag"];
401 [self removeObserver: self forKeyPath: @"track"];
402 [self removeObserver: self forKeyPath: @"codec"];
403 [self removeObserver: self forKeyPath: @"mixdown"];
404 [self removeObserver: self forKeyPath: @"sampleRate"];
405 [self setTrack: nil];
406 [self setCodec: nil];
407 [self setMixdown: nil];
408 [self setSampleRate: nil];
409 [self setBitRate: nil];
411 [self setVideoContainerTag: nil];
412 [self setCodecs: nil];
413 [self setMixdowns: nil];
414 [self setBitRates: nil];
422 - (void) observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object change: (NSDictionary *) change context: (void *) context
425 if (YES == [keyPath isEqualToString: @"videoContainerTag"]) {
428 else if (YES == [keyPath isEqualToString: @"track"]) {
429 if (nil != [self track]) {
431 [self updateMixdowns: YES];
432 if (YES == [self enabled]) {
433 [self setSampleRate: [[self sampleRates] objectAtIndex: 0]]; // default to Auto
435 if (YES == [[controller noneTrack] isEqual: [change objectForKey: NSKeyValueChangeOldKey]]) {
436 [controller switchingTrackFromNone: self];
438 if (YES == [[controller noneTrack] isEqual: [self track]]) {
439 [controller settingTrackToNone: self];
443 else if (YES == [keyPath isEqualToString: @"codec"]) {
444 [self updateMixdowns: YES];
445 [self updateBitRates: YES];
447 else if (YES == [keyPath isEqualToString: @"mixdown"]) {
448 [self updateBitRates: YES];
449 [[NSNotificationCenter defaultCenter] postNotificationName: HBMixdownChangedNotification object: self];
451 else if (YES == [keyPath isEqualToString: @"sampleRate"]) {
452 [self updateBitRates: NO];
458 #pragma mark Special Setters
460 - (void) setTrackFromIndex: (int) aValue
463 [self setTrack: [[self tracks] dictionaryWithObject: [NSNumber numberWithInt: aValue] matchingKey: keyAudioTrackIndex]];
467 // This returns whether it is able to set the actual codec desired.
468 - (BOOL) setCodecFromName: (NSString *) aValue
471 NSDictionary *dict = [[self codecs] dictionaryWithObject: aValue matchingKey: keyAudioCodecName];
474 [self setCodec: dict];
476 return (nil != dict);
479 - (void) setMixdownFromName: (NSString *) aValue
482 NSDictionary *dict = [[self mixdowns] dictionaryWithObject: aValue matchingKey: keyAudioMixdownName];
485 [self setMixdown: dict];
490 - (void) setSampleRateFromName: (NSString *) aValue
493 NSDictionary *dict = [[self sampleRates] dictionaryWithObject: aValue matchingKey: keyAudioSampleRateName];
496 [self setSampleRate: dict];
501 - (void) setBitRateFromName: (NSString *) aValue
504 NSDictionary *dict = [[self bitRates] dictionaryWithObject: aValue matchingKey: keyAudioBitrateName];
507 [self setBitRate: dict];
514 #pragma mark Validation
516 // Because we have indicated that the binding for the drc validates immediately we can implement the
517 // key value binding method to ensure the drc stays in our accepted range.
518 - (BOOL) validateDrc: (id *) ioValue error: (NSError *) outError
523 if (nil != *ioValue) {
524 if (0.0 < [*ioValue floatValue] && 1.0 > [*ioValue floatValue]) {
525 *ioValue = [NSNumber numberWithFloat: 1.0];
533 #pragma mark Bindings Support
538 return (nil != track) ? (NO == [track isEqual: [controller noneTrack]]) : NO;
541 - (BOOL) mixdownEnabled
544 BOOL retval = [self enabled];
547 int myMixdown = [[[self mixdown] objectForKey: keyAudioMixdown] intValue];
548 if (HB_ACODEC_AC3_PASS == myMixdown || HB_ACODEC_DCA_PASS == myMixdown) {
558 BOOL retval = [self enabled];
561 int myTrackCodec = [[[self track] objectForKey: keyAudioInputCodec] intValue];
562 int myCodecCodec = [[[self codec] objectForKey: keyAudioCodec] intValue];
563 if (HB_ACODEC_AC3 != myTrackCodec || HB_ACODEC_AC3_PASS == myCodecCodec) {
570 + (NSSet *) keyPathsForValuesAffectingValueForKey: (NSString *) key
575 if (YES == [key isEqualToString: @"enabled"]) {
576 retval = [NSSet setWithObjects: @"track", nil];
578 else if (YES == [key isEqualToString: @"AC3Enabled"]) {
579 retval = [NSSet setWithObjects: @"track", @"codec", nil];
581 else if (YES == [key isEqualToString: @"mixdownEnabled"]) {
582 retval = [NSSet setWithObjects: @"track", @"mixdown", nil];