]> git.sesse.net Git - vlc/blob - modules/gui/macosx/asystm.m
* Mac OS X audio device discovery and selection, patch courtesy of
[vlc] / modules / gui / macosx / asystm.m
1 //
2 //  asystm.m
3 //  
4 //
5 //  Created by Heiko Panther on Tue Sep 10 2002.
6 //  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
7 //
8
9 #import "asystm.h"
10 #define MAXINT 0x7fffffff
11
12 #define DEBUG_ASYSTM 1
13
14 // this is a basic set of rules
15 #define gNumClassificationRules 4
16
17 const struct classificationRule gClassificationRules[gNumClassificationRules]=
18 {
19 {       // The old AC3 format type
20     'IAC3',
21     0,
22     audiodevice_class_ac3,
23     "Digital AC3"
24 },
25 {       // The new AC3 format type
26     kAudioFormat60958AC3,
27     0,
28     audiodevice_class_ac3,
29     "Digital AC3"
30 },
31 {
32     kAudioFormatLinearPCM,
33     2,
34     audiodevice_class_pcm2,
35     "Stereo PCM"
36 },
37 {
38     kAudioFormatLinearPCM,
39     6,
40     audiodevice_class_pcm6,
41     "6 Channel PCM"
42 }
43 };
44
45 MacOSXAudioSystem *gTheMacOSXAudioSystem; // Remove this global, access audio system froma aout some other way
46
47 @implementation MacOSXSoundOption
48
49 - (id)initWithName:(NSString*)_name deviceID:(AudioDeviceID)devID streamIndex:(UInt32)strInd formatID:(UInt32)formID chans:(UInt32)chans;
50 {
51     id me=0;
52     if((me=[super init]))
53     {
54         name = _name;
55         [name retain];
56         deviceID=devID;
57         streamIndex=strInd;
58         mFormatID=formID;
59         mChannels=chans;
60     }
61     return(me);
62 }
63
64 - (NSString*)name {return name;};
65
66 - (AudioDeviceID)deviceID {return deviceID;};
67
68 - (UInt32)streamIndex {return streamIndex;};
69
70 - (UInt32)mFormatID {return mFormatID;};
71
72 - (UInt32)mChannels {return mChannels;};
73
74 - (void)dealloc {[name release];};
75
76 @end
77
78
79 @implementation MacOSXAudioSystem
80
81 OSStatus listenerProc (AudioDeviceID            inDevice,
82                        UInt32                   inChannel,
83                        Boolean                  isInput,
84                        AudioDevicePropertyID    inPropertyID,
85                        void*                    inClientData)
86 {
87     intf_thread_t * p_intf = [NSApp getIntf];
88     msg_Dbg(p_intf, "********** Property Listener called! device %d, channel %d, isInput %d, propertyID %4.4s",
89             inDevice, inChannel, isInput, &inPropertyID);
90     return 0;
91 };
92
93 OSStatus streamListenerProc (AudioStreamID              inStream,
94                        UInt32                   inChannel,
95                        AudioDevicePropertyID    inPropertyID,
96                        void*                    inClientData)
97 {
98     intf_thread_t * p_intf = [NSApp getIntf];
99     msg_Dbg(p_intf, "********** Property Listener called! stream %d, channel %d, propertyID %4.4s",
100             inStream, inChannel, &inPropertyID);
101     return 0;
102 };
103
104 - (id)initWithGUI:(VLCMain*)_main
105 {
106     id me=0;
107     if((me=[super init]))
108     {
109         gTheMacOSXAudioSystem=self;
110         main=_main;
111         [main retain];
112         intf_thread_t * p_intf = [NSApp getIntf];
113         selectedOutput=0;
114
115         // find audio devices
116         // find out how many audio devices there are, if any
117         OSStatus        status = noErr;
118         UInt32          theSize;
119         Boolean         outWritable;
120         AudioDeviceID   *deviceList = NULL;
121         UInt32 i;
122
123         status = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices, &theSize, &outWritable);
124         if(status != noErr)
125         {
126             msg_Err(p_intf, "AudioHardwareGetPropertyInfo failed");
127         };
128
129         // calculate the number of device available
130         UInt32 devicesAvailable = theSize / sizeof(AudioDeviceID);
131         // Bail if there aren't any devices
132         if(devicesAvailable < 1)
133         {
134             msg_Err(p_intf, "no devices found");
135         }
136         if(DEBUG_ASYSTM) msg_Dbg(p_intf, "Have %i devices!", devicesAvailable);
137
138         // make space for the devices we are about to get
139         deviceList = (AudioDeviceID*)malloc(theSize);
140         // get an array of AudioDeviceIDs
141         status = AudioHardwareGetProperty(kAudioHardwarePropertyDevices, &theSize, (void *) deviceList);
142         if(status != noErr)
143         {
144             msg_Err(p_intf, "could not get Device list");
145         };
146
147         // Build a menu
148         NSMenuItem *newItem;
149         newItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@"Sound output" action:NULL keyEquivalent:@""];
150         newMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"Sound output"];
151         [newItem setSubmenu:newMenu];
152         [[NSApp mainMenu] addItem:newItem];
153         [newItem release];
154
155         // check which devices can do what class of audio
156         //    struct mosx_AudioDeviceData deviceData;
157         for(i=0; i<devicesAvailable; i++)
158             [self CheckDevice:deviceList[i] isInput:false];     // only check the output part
159
160         [newMenu release];
161         free(deviceList);
162     };
163     return me;
164 };
165
166 - (AudioStreamID) getStreamIDForIndex:(UInt32)streamIndex device:(AudioDeviceID)deviceID
167 {
168     // Does not currently use the stream index, but just returns the stream ID of the first stream.
169     // Get the stream ID
170     Boolean isInput=false, outWritable;
171     UInt32 theSize;
172     OSStatus err =  AudioDeviceGetPropertyInfo(deviceID, 0, isInput, kAudioDevicePropertyStreams,  &theSize, &outWritable);
173     AudioStreamID *streamList = (AudioStreamID*)malloc(theSize);
174     err = AudioDeviceGetProperty(deviceID, 0, isInput, kAudioDevicePropertyStreams, &theSize, streamList);
175     AudioStreamID streamID = streamList[streamIndex - 1];
176     free(streamList);
177     return streamID;
178 }
179
180 - (void)CheckDevice:(AudioDeviceID)deviceID isInput:(bool)isInput
181 {
182     OSStatus err;
183     UInt32              theSize;
184     Boolean             outWritable;
185     AudioBufferList     *bufferList = 0;
186     UInt32 i, j;
187     intf_thread_t * p_intf = [NSApp getIntf];
188     char deviceName[32];        // Make this a CFString!
189
190     // Add property listener
191     err=AudioDeviceAddPropertyListener(deviceID, 1, isInput, kAudioDevicePropertyStreams, listenerProc, 0);
192     if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
193     err=AudioDeviceAddPropertyListener(deviceID, 1, isInput, kAudioDevicePropertyStreamConfiguration, listenerProc, 0);
194     if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
195     err=AudioDeviceAddPropertyListener(deviceID, 1, isInput, kAudioDevicePropertyStreamFormat, listenerProc, 0);
196     if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
197
198     // Get the device name
199     err = AudioDeviceGetPropertyInfo(deviceID, 0, isInput, kAudioDevicePropertyDeviceName,  &theSize, &outWritable);
200     theSize=sizeof(deviceName);
201     err = AudioDeviceGetProperty(deviceID, 0, isInput, kAudioDevicePropertyDeviceName, &theSize, deviceName);
202
203     // Get the stream configuration
204     err =  AudioDeviceGetPropertyInfo(deviceID, 0, isInput, kAudioDevicePropertyStreamConfiguration,  &theSize, &outWritable);
205     bufferList = (AudioBufferList*)malloc(theSize);
206     err = AudioDeviceGetProperty(deviceID, 0, isInput, kAudioDevicePropertyStreamConfiguration, &theSize, bufferList);
207
208     if(DEBUG_ASYSTM) msg_Dbg(p_intf, "\nFound a %s, examing its %s, it has %i streams.", deviceName, (isInput?"Input":"Output"), bufferList->mNumberBuffers);
209
210     // find details of each stream
211     for (i=0; i < bufferList->mNumberBuffers; i++)
212     {
213         short streamIndex=i+1;
214         UInt32 nActFormats;
215         AudioStreamBasicDescription *formatsAvailable;
216
217         AudioStreamID streamID=[self getStreamIDForIndex:streamIndex device:deviceID];
218
219         // Add property listener
220         err=AudioStreamAddPropertyListener(streamID, 0, kAudioDevicePropertyStreams, streamListenerProc, 0);
221         if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
222         err=AudioStreamAddPropertyListener(streamID, 0, kAudioDevicePropertyStreamConfiguration, streamListenerProc, 0);
223         if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
224         err=AudioStreamAddPropertyListener(streamID, 0, kAudioDevicePropertyStreamFormat, streamListenerProc, 0);
225         if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
226         err=AudioStreamAddPropertyListener(streamID, 0, kAudioStreamPropertyPhysicalFormat, streamListenerProc, 0);
227         if(err) msg_Err(p_intf, "Add Property Listener failed, err=%4.4s", &err);
228
229         // Get the # of actual formats in the current stream
230         err =  AudioStreamGetPropertyInfo(streamID, 0, kAudioStreamPropertyPhysicalFormats,  &theSize, &outWritable);
231         nActFormats = theSize / sizeof(AudioStreamBasicDescription);
232         if(DEBUG_ASYSTM) msg_Dbg(p_intf, "stream index %i, streamID %i, nActFormats %d", streamIndex, streamID, nActFormats);
233
234         // Get the format specifications
235         formatsAvailable=(AudioStreamBasicDescription*) malloc(theSize);
236         err = AudioStreamGetProperty(streamID, 0, kAudioStreamPropertyPhysicalFormats, &theSize, formatsAvailable);
237         if(err) msg_Err(p_intf, "AudioDeviceGetProperty err %d", err);
238         
239         // now classify the device and add a menu entry for each device class it matches
240         for(j=0; j<gNumClassificationRules; j++)
241         {
242             UInt32 numChans=MAXINT, format=0;
243             for(i=0; i<theSize/sizeof(AudioStreamBasicDescription); i++)
244             {
245                 if(DEBUG_ASYSTM) msg_Dbg(p_intf, "Finding formats: %4.4s - %d chans, %d Hz, %d bits/sample, %d bytes/frame",
246           &formatsAvailable[i].mFormatID, formatsAvailable[i].mChannelsPerFrame,
247           (UInt32)formatsAvailable[i].mSampleRate,
248           formatsAvailable[i].mBitsPerChannel, formatsAvailable[i].mBytesPerFrame);
249
250                 if(formatsAvailable[i].mFormatID != gClassificationRules[j].mFormatID && gClassificationRules[j].mFormatID!=0) continue;
251                 if(formatsAvailable[i].mChannelsPerFrame < gClassificationRules[j].mChannelsPerFrame && gClassificationRules[j].mChannelsPerFrame!=0) continue;
252                 // we want to choose the format with the smallest allowable channel number for this class
253                 if(formatsAvailable[i].mChannelsPerFrame < numChans)
254                 {
255                     numChans=formatsAvailable[i].mChannelsPerFrame;
256                     format=i;
257                 };
258             };
259             if(numChans!=MAXINT) // we found a good setting
260             {
261                 if(DEBUG_ASYSTM) msg_Dbg(p_intf, "classified into %d", gClassificationRules[j].characteristic);
262                 // make a sound option object
263                 char menuentry[48];
264                 snprintf(menuentry, 48, "%.32s: %.16s", deviceName, gClassificationRules[j].qualifierString);
265                 MacOSXSoundOption *device=[[MacOSXSoundOption alloc] initWithName:[NSString stringWithCString:menuentry] deviceID:deviceID streamIndex:streamIndex formatID:formatsAvailable[format].mFormatID chans:formatsAvailable[format].mChannelsPerFrame];
266                 [self registerSoundOption:device];
267             };
268         };
269         free(formatsAvailable);
270     }
271
272     free(bufferList);
273 };
274
275 - (void)registerSoundOption:(MacOSXSoundOption*)option {
276     NSMenuItem *newItem;
277     newItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[option name] action:NULL keyEquivalent:@""];
278     [newItem setImage:[NSImage imageNamed:@"eomt_browsedata"]];
279     [newItem setTarget:self];
280     [newItem setAction:@selector(selectAction:)];
281     [newItem setRepresentedObject:option];
282     [newMenu addItem:newItem];
283     if(selectedOutput==0) [self selectAction:newItem];
284     [newItem release];
285 };
286     
287 - (void)selectAction:(id)sender {
288     [selectedOutput setState:NSOffState];
289     selectedOutput=sender;
290     [sender setState:NSOnState];
291 };
292
293 static void printStreamDescription(char *description, AudioStreamBasicDescription *format)
294 {
295     intf_thread_t * p_intf = [NSApp getIntf];
296     if(DEBUG_ASYSTM) msg_Dbg(p_intf, "%s: mSampleRate %ld, mFormatID %4.4s, mFormatFlags %ld, mBytesPerPacket %ld, mFramesPerPacket %ld, mBytesPerFrame %ld, mChannelsPerFrame %ld, mBitsPerChannel %ld",
297             description,
298             (UInt32)format->mSampleRate, &format->mFormatID,
299             format->mFormatFlags, format->mBytesPerPacket,
300             format->mFramesPerPacket, format->mBytesPerFrame,
301             format->mChannelsPerFrame, format->mBitsPerChannel);
302 };
303
304
305 - (AudioDeviceID)getSelectedDeviceSetToRate:(int)preferredSampleRate{
306     // I know the selected device, stream, and the required format ID. Now find a format
307     // that comes closest to the preferred rate
308     // For sample size, it is assumed that 16 bits will always be enough.
309     // Note that the caller is not guranteed to get the rate she preferred.
310     AudioStreamBasicDescription *formatsAvailable;
311     MacOSXSoundOption *selectedOption=[selectedOutput representedObject];
312     bool foundFormat=false;
313     UInt32 theSize;
314     Boolean outWritable;
315     OSStatus err;
316     UInt32 i;
317     intf_thread_t * p_intf = [NSApp getIntf];
318     AudioDeviceID deviceID=[selectedOption deviceID];
319
320     // get the streamID (it might have changed)
321     AudioStreamID streamID=[self getStreamIDForIndex:[selectedOption streamIndex] device:deviceID];
322         
323     // Find the actual formats
324     err =  AudioStreamGetPropertyInfo(streamID, 0, kAudioStreamPropertyPhysicalFormats,  &theSize, &outWritable);
325     formatsAvailable=(AudioStreamBasicDescription*) malloc(theSize);
326     err = AudioStreamGetProperty(streamID, 0, kAudioStreamPropertyPhysicalFormats, &theSize, formatsAvailable);
327     if(err)
328     {
329         msg_Err(p_intf, "Error %4.4s getting the stream formats", &err);
330         return 0;
331     };
332     
333     UInt32 formtmp=[selectedOption mFormatID];
334     if(DEBUG_ASYSTM) msg_Dbg(p_intf, "looking for:   %4.4s - %d chans, %d Hz", &formtmp,
335             [selectedOption mChannels], preferredSampleRate);
336
337     // Check if there's a "best match" which has our required rate
338     for(i=0; i<theSize/sizeof(AudioStreamBasicDescription); i++)
339     {
340         if(DEBUG_ASYSTM) msg_Dbg(p_intf, "actual:   %4.4s - %d chans, %d Hz, %d bits/sample, %d bytes/frame",
341          &formatsAvailable[i].mFormatID, formatsAvailable[i].mChannelsPerFrame,
342          (int)formatsAvailable[i].mSampleRate,
343          formatsAvailable[i].mBitsPerChannel, formatsAvailable[i].mBytesPerFrame);
344
345         if(formatsAvailable[i].mChannelsPerFrame<0 || formatsAvailable[i].mChannelsPerFrame>100) {
346             msg_Err(p_intf, "bogus format! index %i", i);
347             return 0;
348         };
349         
350         if( formatsAvailable[i].mFormatID == [selectedOption mFormatID]
351      && formatsAvailable[i].mChannelsPerFrame == [selectedOption mChannels]
352      && (int)formatsAvailable[i].mSampleRate == preferredSampleRate)
353         {
354             if(DEBUG_ASYSTM) msg_Dbg(p_intf, "Found the perfect format!!");
355             foundFormat=true;
356             break;
357         };
358     };
359
360     int rate=MAXINT, format=0;
361     if(!foundFormat)
362     {
363         for(i=0; i<theSize/sizeof(AudioStreamBasicDescription); i++)
364         {
365             // We don't have one... check if there's one with a higher sample rate.
366             // Upsampling should be better than downsampling.
367             // Select the smallest of the higher sample rates, to save resources.
368             int actrate=(int)formatsAvailable[i].mSampleRate;
369             if( formatsAvailable[i].mFormatID == [selectedOption mFormatID]
370          && formatsAvailable[i].mChannelsPerFrame == [selectedOption mChannels]
371          &&  actrate > preferredSampleRate)
372             {
373                 if(actrate < rate)
374                 {
375                     rate=actrate;
376                     format=i;
377                 }
378             };
379         };
380         if(rate!=MAXINT)        // This means we have found a rate!! Yes!
381         {
382             if(DEBUG_ASYSTM) msg_Dbg(p_intf, "Only got a format with higher sample rate");
383             foundFormat=true;
384             i=format;
385         };
386     };
387     
388     if(!foundFormat)
389     {
390         rate=0;
391         for(i=0; i<theSize/sizeof(AudioStreamBasicDescription); i++)
392         {
393             // Our last chance: select the highest lower sample rate.
394             int actrate=(int)formatsAvailable[i].mSampleRate;
395             if( actrate >= preferredSampleRate) // We must have done something wrong
396             {
397                 if(DEBUG_ASYSTM) msg_Err(p_intf, "Found a rate that should have been selected previously.");
398                 free(formatsAvailable);
399                 return 0;
400             };
401
402             if(actrate > rate)
403             {
404                 rate=actrate;
405                 format=i;
406             }
407         };
408
409         if(rate!=0)     // This means we have found a rate!! Yes!
410         {
411             if(DEBUG_ASYSTM) msg_Dbg(p_intf, "Only got a format with lower sample rate");
412             foundFormat=true;
413             i=format;
414         }
415         else // We must have done something wrong
416         {
417             msg_Err(p_intf, "Found no rate which is equal, higher or lower to requested rate. That means our device either: a) didn't exist in the first place or b) has been removed since.");
418             free(formatsAvailable);
419             return 0;
420         };
421     };
422     AudioStreamBasicDescription desc=formatsAvailable[i];
423     free(formatsAvailable);
424
425     // Set the device stream format
426     Boolean isWriteable;
427
428     err = AudioStreamGetPropertyInfo(streamID, 0, kAudioStreamPropertyPhysicalFormat, &theSize, &isWriteable);
429     if(err) msg_Err(p_intf, "GetPropertyInfo (stream format) error %4.4s - theSize %d", &err, theSize);
430     if(DEBUG_ASYSTM) msg_Dbg(p_intf, "size %d, writable %d", theSize, isWriteable);
431
432     if(DEBUG_ASYSTM) printStreamDescription("want to set", &desc);
433
434     err = AudioStreamSetProperty(streamID, 0, 0, kAudioStreamPropertyPhysicalFormat, theSize, &desc);
435     if(err) msg_Err(p_intf, "SetProperty (stream format) error %4.4s - theSize %d", &err, theSize);
436
437     // Because of the format change, the streamID has changed!
438     // That's why we return the deviceID.
439     return deviceID;
440 };
441
442     
443 - (void)dealloc
444 {
445     [main release];
446 };
447
448
449 @end