summaryrefslogtreecommitdiff
path: root/src/lib/ecore_audio/ecore_audio_obj_out_core_audio.c
blob: aa0d76b5a01a176ea03989a7852dff8128aa619a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <Eo.h>
#include "ecore_audio_private.h"

#include <CoreAudio/CoreAudio.h>

/* Notes:
 *
 * A lot of source code on the internet dealing with CoreAudio is deprecated.
 * sndfile-play (bundled with libsndfile) is no exception and uses an almost
 * 10 years old API. Nethertheless, sndfile-play has been heavily used to
 * create the CoreAudio module.
 *
 * Documentation is almost non-existant, but here is the technical note from
 * Apple explaining how CoreAudio objects should be manipulated:
 * https://developer.apple.com/library/mac/technotes/tn2223/_index.html
 */

#include "ecore_audio_obj_out_core_audio.h"

typedef struct
{
   Eo                                  *input;
   Eo                                  *output;
   AudioDeviceIOProcID                  proc_id;
   AudioStreamBasicDescription          format;
   AudioObjectID                        obj_id;
   UInt32                               buf_size;

   Eina_Bool                            is_playing;
   Eina_Bool                            fake_stereo;
} Core_Audio_Helper;


/* Apple's error codes are tricky: they are stored as 32 bits integers.
 * However, they are supposed to be represented as 4-bytes strings.
 * There is no equivalent of strerror() (of what I know).
 *
 * Ref: http://vgable.com/blog/2008/04/23/printing-a-fourcharcode/
 *
 * In case of error, take a look at CoreAudio/AudioHardwareBase.h where
 * the error codes are explained.
 */
#define APPLE_ERROR(err_) \
   (char[5]){((err_) >> 24) & 0xff, ((err_) >> 16) & 0xff, ((err_) >> 8) & 0xff, (err_) & 0xff, 0}

#define MY_CLASS ECORE_AUDIO_OUT_CORE_AUDIO_CLASS
#define MY_CLASS_NAME "Ecore_Audio_Out_Core_Audio"

/*
 * Unused structure. Only here because of Eolian.
 * XXX Maybe it is possible to get rid of it.
 */
typedef struct
{
   void *this_data_is_here_to_silent_warnings;
} Ecore_Audio_Out_Core_Audio_Data;


/*============================================================================*
 *                                 Helper API                                 *
 *============================================================================*/

static Core_Audio_Helper *
_core_audio_helper_new(void)
{
   return calloc(1, sizeof(Core_Audio_Helper));
}

static void
_core_audio_helper_stop(Core_Audio_Helper *helper)
{
   EINA_SAFETY_ON_NULL_RETURN(helper);

   OSStatus err;

   if (!helper->is_playing) return;

   /* Stop audio device */
   err = AudioDeviceStop(helper->obj_id, helper->proc_id);
   if (EINA_UNLIKELY(err != noErr))
     ERR("Failed to stop audio device %i for proc id %p: '%s'",
         helper->obj_id, helper->proc_id, APPLE_ERROR(err));

   /* Remove proc ID */
   err = AudioDeviceDestroyIOProcID(helper->obj_id, helper->proc_id);
   if (EINA_UNLIKELY(err != noErr))
     ERR("Failed to stop audio device %i for proc id %p: '%s'",
         helper->obj_id, helper->proc_id, APPLE_ERROR(err));

   helper->is_playing = EINA_FALSE;
}

static void
_core_audio_helper_free(Core_Audio_Helper *helper)
{
   EINA_SAFETY_ON_NULL_RETURN(helper);

   if (helper->is_playing)
     _core_audio_helper_stop(helper);
   free(helper);
}


/*============================================================================*
 *                           Audio Object Properties                          *
 *============================================================================*/

static OSStatus
_audio_object_id_get(AudioObjectID *obj_id)
{
   OSStatus err;
   UInt32 size;
   AudioObjectPropertyAddress prop = {
      kAudioHardwarePropertyDefaultOutputDevice,
      kAudioObjectPropertyScopePlayThrough,
      kAudioObjectPropertyElementMaster
   };

   /* Default output device */
   size = sizeof(AudioObjectID);
   err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, NULL,
                                    &size, obj_id);
   return err;
}

static OSStatus
_audio_device_stream_format_get(AudioObjectID                obj_id,
                                AudioStreamBasicDescription *format)
{
   OSStatus err;
   UInt32 size;
   AudioObjectPropertyAddress prop = {
      kAudioDevicePropertyStreamFormat,
      kAudioObjectPropertyScopePlayThrough,
      kAudioObjectPropertyElementMaster /* Channel number */
   };

   size = sizeof(AudioStreamBasicDescription);
   err = AudioObjectGetPropertyData(obj_id, &prop, 0, NULL, &size, format);
   return err;
}

static OSStatus
_audio_device_stream_format_set(AudioObjectID                obj_id,
                                AudioStreamBasicDescription *format)
{
   OSStatus err;
   UInt32 size;
   AudioObjectPropertyAddress prop = {
      kAudioDevicePropertyStreamFormat,
      kAudioObjectPropertyScopePlayThrough,
      kAudioObjectPropertyElementMaster /* Channel number */
   };

   size = sizeof(AudioStreamBasicDescription);
   err = AudioObjectSetPropertyData(obj_id, &prop, 0, NULL, size, format);
   return err;
}


/*============================================================================*
 *                               Audio Callback                               *
 *============================================================================*/

static OSStatus
_audio_io_proc_cb(AudioObjectID          obj_id         EINA_UNUSED,
                  const AudioTimeStamp  *in_now         EINA_UNUSED,
                  const AudioBufferList *input_data     EINA_UNUSED,
                  const AudioTimeStamp  *input_time     EINA_UNUSED,
                  AudioBufferList       *output_data,
                  const AudioTimeStamp  *in_output_time EINA_UNUSED,
                  void                  *data)
{
   Core_Audio_Helper *helper = data;
   float *buf;
   int size, bread, sample_count, k;

   size = output_data->mBuffers[0].mDataByteSize;
   buf = output_data->mBuffers[0].mData;
   sample_count = size / sizeof(float);

   if (helper->fake_stereo)
     {
        bread = ecore_audio_obj_in_read(helper->input, buf, size * 2);

        for (k = bread - 1; k >= 0; --k)
          {
             buf[2 * k + 0] = buf[k];
             buf[2 * k + 1] = buf[k];
          }
        bread /= 2;
     }
   else
     {
        bread = ecore_audio_obj_in_read(helper->input, buf, size * 4);
        bread /= 4;
     }

   /* Done playing */
   if (bread < sample_count)
     {
        INF("Done playing: %i < %i", bread, sample_count);
        /* Auto-detached. Don't need to do more. */
     }

   return noErr;
}


/*============================================================================*
 *                                   Eo API                                   *
 *============================================================================*/

EOLIAN static void
_ecore_audio_out_core_audio_ecore_audio_volume_set(Eo *obj, Ecore_Audio_Out_Core_Audio_Data *sd EINA_UNUSED, double volume)
{
   // TODO Change volume of playing inputs
   ecore_audio_obj_volume_set(eo_super(obj, MY_CLASS), volume);
}

EOLIAN static Eina_Bool
_ecore_audio_out_core_audio_ecore_audio_out_input_attach(Eo *obj, Ecore_Audio_Out_Core_Audio_Data *sd EINA_UNUSED, Eo *input)
{
   Core_Audio_Helper *helper;
   UInt32 channels;
   OSStatus err;
   Eina_Bool chk;

   chk = ecore_audio_obj_out_input_attach(eo_super(obj, MY_CLASS), input);
   if (EINA_UNLIKELY(!chk))
     {
        ERR("Failed to attach input (eo_do_super)");
        goto return_failure;
     }

   helper = _core_audio_helper_new();
   if (EINA_UNLIKELY(helper == NULL))
     {
        CRI("Failed to allocate memory");
        goto detach;
     }

   /* Keep track of input source and output object */
   helper->input = input;
   helper->output = obj;

   /* Default output device */
   err = _audio_object_id_get(&(helper->obj_id));
   if (EINA_UNLIKELY(err != noErr))
     {
        ERR("Failed to get object property: default output device: '%s'",
            APPLE_ERROR(err));
        goto free_helper;
     }

   /* Get data format description */
   err = _audio_device_stream_format_get(helper->obj_id, &(helper->format));
   if (EINA_UNLIKELY(err != noErr))
     {
        ERR("Failed to get property: stream format: '%s'", APPLE_ERROR(err));
        goto free_helper;
     }

   /* Forward samplerate to CoreAudio */
   helper->format.mSampleRate = ecore_audio_obj_in_samplerate_get(input);

   /* Set channels. If only 1 channel, emulate stereo */
   channels = ecore_audio_obj_in_channels_get(input);
   if (channels == 1)
     {
        DBG("Fake stereo enabled for input %p", input);
        helper->fake_stereo = EINA_TRUE;
        channels = 2;
     }
   helper->format.mChannelsPerFrame = channels;

   /* Set new format description */
   err = _audio_device_stream_format_set(helper->obj_id, &(helper->format));
   if (EINA_UNLIKELY(err != noErr))
     {
        ERR("Failed to set property: stream format: '%s'", APPLE_ERROR(err));
        goto free_helper;
     }

   /* We want linear PCM */
   if (helper->format.mFormatID != kAudioFormatLinearPCM)
     {
        ERR("Invalid format ID. Expected linear PCM: '%s'", APPLE_ERROR(err));
        goto free_helper;
     }

   /* Create IO proc ID */
   err = AudioDeviceCreateIOProcID(helper->obj_id, _audio_io_proc_cb,
                                   helper, &(helper->proc_id));
   if (err != noErr)
     {
        ERR("Failed to create IO proc ID. Error: '%s'", APPLE_ERROR(err));
        goto free_helper;
     }

   /* Keep track of data for deallocation */
   eo_key_data_set(input, "coreaudio_data", helper);

   /* Start playing */
   helper->is_playing = EINA_TRUE;
   err = AudioDeviceStart(helper->obj_id, helper->proc_id);
   if (err != noErr)
     {
        ERR("Failed to start proc ID %p for device id %i: '%s'",
            helper->proc_id, helper->obj_id, APPLE_ERROR(err));
        goto free_proc_id;
     }

   return EINA_TRUE;

free_proc_id:
   AudioDeviceDestroyIOProcID(helper->obj_id, helper->proc_id);
free_helper:
   free(helper);
detach:
   ecore_audio_obj_out_input_detach(eo_super(obj, MY_CLASS), input);
return_failure:
   return EINA_FALSE;
}

EOLIAN static Eina_Bool
_ecore_audio_out_core_audio_ecore_audio_out_input_detach(Eo *obj, Ecore_Audio_Out_Core_Audio_Data *sd EINA_UNUSED, Eo *input)
{
   Core_Audio_Helper *data;
   Eina_Bool ret;

   DBG("Detach");
   /* Free helper */
   data = eo_key_data_get(input, "coreaudio_data");
   _core_audio_helper_free(data);

   ret = ecore_audio_obj_out_input_detach(eo_super(obj, MY_CLASS), input);

   return ret;
}

#include "ecore_audio_out_core_audio.eo.c"