OSXOutputPlugin.cxx 10.4 KB
Newer Older
1
/*
2
 * Copyright (C) 2003-2013 The Music Player Daemon Project
3
 * http://www.musicpd.org
4 5 6 7 8 9 10 11 12 13
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
14 15 16 17
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 19
 */

20
#include "config.h"
21
#include "OSXOutputPlugin.hxx"
22
#include "OutputAPI.hxx"
23
#include "util/fifo_buffer.h"
24 25
#include "util/Error.hxx"
#include "util/Domain.hxx"
26 27
#include "thread/Mutex.hxx"
#include "thread/Cond.hxx"
28
#include "system/ByteOrder.hxx"
29
#include "Log.hxx"
30

Rich Healey's avatar
Rich Healey committed
31
#include <CoreAudio/AudioHardware.h>
32
#include <AudioUnit/AudioUnit.h>
33
#include <CoreServices/CoreServices.h>
34

35
struct OSXOutput {
36 37
	struct audio_output base;

38 39 40 41 42
	/* configuration settings */
	OSType component_subtype;
	/* only applicable with kAudioUnitSubType_HALOutput */
	const char *device_name;

43
	AudioUnit au;
44 45
	Mutex mutex;
	Cond condition;
Max Kellermann's avatar
Max Kellermann committed
46 47

	struct fifo_buffer *buffer;
Max Kellermann's avatar
Max Kellermann committed
48
};
49

50
static constexpr Domain osx_output_domain("osx_output");
51

Max Kellermann's avatar
Max Kellermann committed
52 53
static bool
osx_output_test_default_device(void)
Avuton Olrich's avatar
Avuton Olrich committed
54
{
55 56
	/* on a Mac, this is always the default plugin, if nothing
	   else is configured */
57
	return true;
Warren Dukes's avatar
Warren Dukes committed
58 59
}

60
static void
61
osx_output_configure(OSXOutput *oo, const config_param &param)
62
{
63
	const char *device = param.GetBlockValue("device");
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

	if (device == NULL || 0 == strcmp(device, "default")) {
		oo->component_subtype = kAudioUnitSubType_DefaultOutput;
		oo->device_name = NULL;
	}
	else if (0 == strcmp(device, "system")) {
		oo->component_subtype = kAudioUnitSubType_SystemOutput;
		oo->device_name = NULL;
	}
	else {
		oo->component_subtype = kAudioUnitSubType_HALOutput;
		/* XXX am I supposed to g_strdup() this? */
		oo->device_name = device;
	}
}

80
static struct audio_output *
81
osx_output_init(const config_param &param, Error &error)
Avuton Olrich's avatar
Avuton Olrich committed
82
{
83
	OSXOutput *oo = new OSXOutput();
84
	if (!ao_base_init(&oo->base, &osx_output_plugin, param, error)) {
85
		delete oo;
86 87
		return NULL;
	}
Max Kellermann's avatar
Max Kellermann committed
88

89
	osx_output_configure(oo, param);
Max Kellermann's avatar
Max Kellermann committed
90

91
	return &oo->base;
92 93
}

94 95
static void
osx_output_finish(struct audio_output *ao)
Avuton Olrich's avatar
Avuton Olrich committed
96
{
97
	OSXOutput *oo = (OSXOutput *)ao;
Max Kellermann's avatar
Max Kellermann committed
98

99
	delete oo;
100 101
}

102
static bool
103
osx_output_set_device(OSXOutput *oo, Error &error)
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
{
	bool ret = true;
	OSStatus status;
	UInt32 size, numdevices;
	AudioDeviceID *deviceids = NULL;
	char name[256];
	unsigned int i;

	if (oo->component_subtype != kAudioUnitSubType_HALOutput)
		goto done;

	/* how many audio devices are there? */
	status = AudioHardwareGetPropertyInfo(kAudioHardwarePropertyDevices,
					      &size,
					      NULL);
	if (status != noErr) {
120 121 122
		error.Format(osx_output_domain, status,
			     "Unable to determine number of OS X audio devices: %s",
			     GetMacOSStatusCommentString(status));
123 124 125 126 127 128
		ret = false;
		goto done;
	}

	/* what are the available audio device IDs? */
	numdevices = size / sizeof(AudioDeviceID);
129
	deviceids = new AudioDeviceID[numdevices];
130 131 132 133
	status = AudioHardwareGetProperty(kAudioHardwarePropertyDevices,
					  &size,
					  deviceids);
	if (status != noErr) {
134 135 136
		error.Format(osx_output_domain, status,
			     "Unable to determine OS X audio device IDs: %s",
			     GetMacOSStatusCommentString(status));
137 138 139 140 141 142 143 144 145 146 147
		ret = false;
		goto done;
	}

	/* which audio device matches oo->device_name? */
	for (i = 0; i < numdevices; i++) {
		size = sizeof(name);
		status = AudioDeviceGetProperty(deviceids[i], 0, false,
						kAudioDevicePropertyDeviceName,
						&size, name);
		if (status != noErr) {
148 149 150 151 152
			error.Format(osx_output_domain, status,
				     "Unable to determine OS X device name "
				     "(device %u): %s",
				     (unsigned int) deviceids[i],
				     GetMacOSStatusCommentString(status));
153 154 155 156
			ret = false;
			goto done;
		}
		if (strcmp(oo->device_name, name) == 0) {
157 158 159
			FormatDebug(osx_output_domain,
				    "found matching device: ID=%u, name=%s",
				    (unsigned)deviceids[i], name);
160 161 162 163
			break;
		}
	}
	if (i == numdevices) {
164 165 166 167
		FormatWarning(osx_output_domain,
			      "Found no audio device with name '%s' "
			      "(will use default audio device)",
			      oo->device_name);
168 169 170 171 172 173 174 175 176 177
		goto done;
	}

	status = AudioUnitSetProperty(oo->au,
				      kAudioOutputUnitProperty_CurrentDevice,
				      kAudioUnitScope_Global,
				      0,
				      &(deviceids[i]),
				      sizeof(AudioDeviceID));
	if (status != noErr) {
178 179 180
		error.Format(osx_output_domain, status,
			     "Unable to set OS X audio output device: %s",
			     GetMacOSStatusCommentString(status));
181 182 183
		ret = false;
		goto done;
	}
184 185 186 187

	FormatDebug(osx_output_domain,
		    "set OS X audio output device ID=%u, name=%s",
		    (unsigned)deviceids[i], name);
188 189

done:
190
	delete[] deviceids;
191 192 193
	return ret;
}

194 195
static OSStatus
osx_render(void *vdata,
196 197 198 199
	   gcc_unused AudioUnitRenderActionFlags *io_action_flags,
	   gcc_unused const AudioTimeStamp *in_timestamp,
	   gcc_unused UInt32 in_bus_number,
	   gcc_unused UInt32 in_number_frames,
200 201
	   AudioBufferList *buffer_list)
{
202
	OSXOutput *od = (OSXOutput *) vdata;
203 204 205
	AudioBuffer *buffer = &buffer_list->mBuffers[0];
	size_t buffer_size = buffer->mDataByteSize;

Max Kellermann's avatar
Max Kellermann committed
206
	assert(od->buffer != NULL);
207

208
	od->mutex.lock();
209

Max Kellermann's avatar
Max Kellermann committed
210 211
	size_t nbytes;
	const void *src = fifo_buffer_read(od->buffer, &nbytes);
212

Max Kellermann's avatar
Max Kellermann committed
213 214 215
	if (src != NULL) {
		if (nbytes > buffer_size)
			nbytes = buffer_size;
216

Max Kellermann's avatar
Max Kellermann committed
217 218 219 220
		memcpy(buffer->mData, src, nbytes);
		fifo_buffer_consume(od->buffer, nbytes);
	} else
		nbytes = 0;
221

222 223
	od->condition.signal();
	od->mutex.unlock();
224

225 226 227 228 229 230 231
	buffer->mDataByteSize = nbytes;

	unsigned i;
	for (i = 1; i < buffer_list->mNumberBuffers; ++i) {
		buffer = &buffer_list->mBuffers[i];
		buffer->mDataByteSize = 0;
	}
232 233 234 235

	return 0;
}

236
static bool
237
osx_output_enable(struct audio_output *ao, Error &error)
Avuton Olrich's avatar
Avuton Olrich committed
238
{
239
	OSXOutput *oo = (OSXOutput *)ao;
240

241
	ComponentDescription desc;
242
	desc.componentType = kAudioUnitType_Output;
243
	desc.componentSubType = oo->component_subtype;
244 245 246 247
	desc.componentManufacturer = kAudioUnitManufacturer_Apple;
	desc.componentFlags = 0;
	desc.componentFlagsMask = 0;

248
	Component comp = FindNextComponent(NULL, &desc);
Avuton Olrich's avatar
Avuton Olrich committed
249
	if (comp == 0) {
250 251
		error.Set(osx_output_domain,
			  "Error finding OS X component");
252
		return false;
Warren Dukes's avatar
Warren Dukes committed
253 254
	}

255
	OSStatus status = OpenAComponent(comp, &oo->au);
256
	if (status != noErr) {
257 258 259
		error.Format(osx_output_domain, status,
			     "Unable to open OS X component: %s",
			     GetMacOSStatusCommentString(status));
260
		return false;
Warren Dukes's avatar
Warren Dukes committed
261 262
	}

263
	if (!osx_output_set_device(oo, error)) {
264
		CloseComponent(oo->au);
265
		return false;
266 267
	}

268
	AURenderCallbackStruct callback;
269
	callback.inputProc = osx_render;
270
	callback.inputProcRefCon = oo;
271

272 273 274 275 276
	ComponentResult result =
		AudioUnitSetProperty(oo->au,
				     kAudioUnitProperty_SetRenderCallback,
				     kAudioUnitScope_Input, 0,
				     &callback, sizeof(callback));
277
	if (result != noErr) {
278
		CloseComponent(oo->au);
279 280
		error.Set(osx_output_domain, result,
			  "unable to set callback for OS X audio unit");
281
		return false;
282
	}
Warren Dukes's avatar
Warren Dukes committed
283

284 285 286 287 288 289
	return true;
}

static void
osx_output_disable(struct audio_output *ao)
{
290
	OSXOutput *oo = (OSXOutput *)ao;
291 292 293 294 295 296 297

	CloseComponent(oo->au);
}

static void
osx_output_cancel(struct audio_output *ao)
{
298
	OSXOutput *od = (OSXOutput *)ao;
299

300
	const ScopeLock protect(od->mutex);
Max Kellermann's avatar
Max Kellermann committed
301
	fifo_buffer_clear(od->buffer);
302 303 304 305 306
}

static void
osx_output_close(struct audio_output *ao)
{
307
	OSXOutput *od = (OSXOutput *)ao;
308 309 310

	AudioOutputUnitStop(od->au);
	AudioUnitUninitialize(od->au);
Max Kellermann's avatar
Max Kellermann committed
311 312

	fifo_buffer_free(od->buffer);
313 314 315
}

static bool
316 317
osx_output_open(struct audio_output *ao, AudioFormat &audio_format,
		Error &error)
318
{
319
	OSXOutput *od = (OSXOutput *)ao;
320 321

	AudioStreamBasicDescription stream_description;
322
	stream_description.mSampleRate = audio_format.sample_rate;
Max Kellermann's avatar
Max Kellermann committed
323 324
	stream_description.mFormatID = kAudioFormatLinearPCM;
	stream_description.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
325

326 327
	switch (audio_format.format) {
	case SampleFormat::S8:
328 329 330
		stream_description.mBitsPerChannel = 8;
		break;

331
	case SampleFormat::S16:
332 333 334
		stream_description.mBitsPerChannel = 16;
		break;

335
	case SampleFormat::S32:
336 337 338
		stream_description.mBitsPerChannel = 32;
		break;

339
	default:
340
		audio_format.format = SampleFormat::S32;
341
		stream_description.mBitsPerChannel = 32;
342 343
		break;
	}
344

345 346
	if (IsBigEndian())
		stream_description.mFormatFlags |= kLinearPCMFormatFlagIsBigEndian;
347

348
	stream_description.mBytesPerPacket = audio_format.GetFrameSize();
349 350
	stream_description.mFramesPerPacket = 1;
	stream_description.mBytesPerFrame = stream_description.mBytesPerPacket;
351
	stream_description.mChannelsPerFrame = audio_format.channels;
352

353 354 355 356 357
	ComponentResult result =
		AudioUnitSetProperty(od->au, kAudioUnitProperty_StreamFormat,
				     kAudioUnitScope_Input, 0,
				     &stream_description,
				     sizeof(stream_description));
358
	if (result != noErr) {
359 360
		error.Set(osx_output_domain, result,
			  "Unable to set format on OS X device");
361
		return false;
362 363
	}

364 365
	OSStatus status = AudioUnitInitialize(od->au);
	if (status != noErr) {
366 367 368
		error.Set(osx_output_domain, status,
			  "Unable to initialize OS X audio unit: %s",
			  GetMacOSStatusCommentString(status));
369 370 371
		return false;
	}

372
	/* create a buffer of 1s */
373 374
	od->buffer = fifo_buffer_new(audio_format.sample_rate *
				     audio_format.GetFrameSize());
375

376 377
	status = AudioOutputUnitStart(od->au);
	if (status != 0) {
378
		AudioUnitUninitialize(od->au);
379 380 381
		error.Format(osx_output_domain, status,
			     "unable to start audio output: %s",
			     GetMacOSStatusCommentString(status));
382 383 384
		return false;
	}

385
	return true;
386 387
}

388
static size_t
389
osx_output_play(struct audio_output *ao, const void *chunk, size_t size,
390
		gcc_unused Error &error)
Avuton Olrich's avatar
Avuton Olrich committed
391
{
392
	OSXOutput *od = (OSXOutput *)ao;
Warren Dukes's avatar
Warren Dukes committed
393

394
	const ScopeLock protect(od->mutex);
Warren Dukes's avatar
Warren Dukes committed
395

Max Kellermann's avatar
Max Kellermann committed
396 397
	void *dest;
	size_t max_length;
Warren Dukes's avatar
Warren Dukes committed
398

Max Kellermann's avatar
Max Kellermann committed
399 400 401 402
	while (true) {
		dest = fifo_buffer_write(od->buffer, &max_length);
		if (dest != NULL)
			break;
Warren Dukes's avatar
Warren Dukes committed
403

Max Kellermann's avatar
Max Kellermann committed
404
		/* wait for some free space in the buffer */
405
		od->condition.wait(od->mutex);
Max Kellermann's avatar
Max Kellermann committed
406
	}
407

Max Kellermann's avatar
Max Kellermann committed
408 409
	if (size > max_length)
		size = max_length;
410

Max Kellermann's avatar
Max Kellermann committed
411 412
	memcpy(dest, chunk, size);
	fifo_buffer_append(od->buffer, size);
413

Max Kellermann's avatar
Max Kellermann committed
414
	return size;
415 416
}

417
const struct audio_output_plugin osx_output_plugin = {
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
	"osx",
	osx_output_test_default_device,
	osx_output_init,
	osx_output_finish,
	osx_output_enable,
	osx_output_disable,
	osx_output_open,
	osx_output_close,
	nullptr,
	nullptr,
	osx_output_play,
	nullptr,
	osx_output_cancel,
	nullptr,
	nullptr,
433
};