pulseaudio.c 10.5 KB
Newer Older
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
/*
mediastreamer2 library - modular sound and video processing and streaming
Copyright (C) 2010  Simon MORLAT (simon.morlat@linphone.org)

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.

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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

#include "mediastreamer2/msfilter.h"
#include "mediastreamer2/mssndcard.h"

#include <pulse/pulseaudio.h>

static void init_pulse_context();
static void pulse_card_detect(MSSndCardManager *m);
static MSFilter *pulse_card_create_reader(MSSndCard *card);
static MSFilter *pulse_card_create_writer(MSSndCard *card);


static void pulse_card_init(MSSndCard *obj){
32

33 34 35
}

static void pulse_card_uninit(MSSndCard *obj){
36

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
}


MSSndCardDesc pulse_card_desc={
	.driver_type="PulseAudio",
	.detect=pulse_card_detect,
	.init=pulse_card_init,
	.create_reader=pulse_card_create_reader,
	.create_writer=pulse_card_create_writer,
	.uninit=pulse_card_uninit
};


static void pulse_card_detect(MSSndCardManager *m){
	MSSndCard *card=ms_snd_card_new(&pulse_card_desc);
	if (card!=NULL){
		card->name=ms_strdup("default");
François Grisez's avatar
François Grisez committed
54
		card->capabilities = MS_SND_CARD_CAP_CAPTURE | MS_SND_CARD_CAP_PLAYBACK;
55 56 57 58 59 60
		ms_snd_card_manager_add_card(m,card);
		init_pulse_context();
	}
}

static pa_context *context=NULL;
François Grisez's avatar
François Grisez committed
61
static pa_context_state_t contextState = PA_CONTEXT_UNCONNECTED;
62
static pa_threaded_mainloop *pa_loop=NULL;
François Grisez's avatar
François Grisez committed
63
static const int fragtime = 20;/*ms*/
64 65 66 67 68

static void uninit_pulse_context(){
	pa_context_disconnect(context);
	pa_context_unref(context);
	pa_threaded_mainloop_stop(pa_loop);
69 70 71
	pa_threaded_mainloop_free(pa_loop);
	context = NULL;
	pa_loop = NULL;
72 73
}

François Grisez's avatar
François Grisez committed
74
static void context_state_notify_cb(pa_context *ctx, void *userdata){
75
	const char *sname="";
76
	contextState=pa_context_get_state(ctx);
François Grisez's avatar
François Grisez committed
77
	switch (contextState){
78 79
	case PA_CONTEXT_UNCONNECTED:
		sname="PA_CONTEXT_UNCONNECTED";
80
		break;
81 82
	case PA_CONTEXT_CONNECTING:
		sname="PA_CONTEXT_CONNECTING";
83
		break;
84 85
	case PA_CONTEXT_AUTHORIZING:
		sname="PA_CONTEXT_AUTHORIZING";
86
		break;
87 88
	case PA_CONTEXT_SETTING_NAME:
		sname="PA_CONTEXT_SETTING_NAME";
89
		break;
90 91
	case PA_CONTEXT_READY:
		sname="PA_CONTEXT_READY";
92
		break;
93 94
	case PA_CONTEXT_FAILED:
		sname="PA_CONTEXT_FAILED";
95
		break;
96 97
	case PA_CONTEXT_TERMINATED:
		sname="PA_CONTEXT_TERMINATED";
98 99 100
		break;
	}
	ms_message("New PulseAudio context state: %s",sname);
François Grisez's avatar
François Grisez committed
101
	pa_threaded_mainloop_signal(pa_loop, FALSE);
102 103 104 105 106
}

static void init_pulse_context(){
	if (context==NULL){
		pa_loop=pa_threaded_mainloop_new();
Simon Morlat's avatar
Simon Morlat committed
107
		context=pa_context_new(pa_threaded_mainloop_get_api(pa_loop),NULL);
François Grisez's avatar
François Grisez committed
108 109
		pa_context_set_state_callback(context,context_state_notify_cb,NULL);
		pa_context_connect(context, NULL, 0, NULL);
110 111 112 113 114
		pa_threaded_mainloop_start(pa_loop);
		atexit(uninit_pulse_context);
	}
}

François Grisez's avatar
François Grisez committed
115 116 117 118
static bool_t wait_for_context_state(pa_context_state_t successState, pa_context_state_t failureState){
	pa_threaded_mainloop_lock(pa_loop);
	while(contextState != successState && contextState != failureState) {
		pa_threaded_mainloop_wait(pa_loop);
119
	}
François Grisez's avatar
François Grisez committed
120 121
	pa_threaded_mainloop_unlock(pa_loop);
	return contextState == successState;
122 123
}

François Grisez's avatar
François Grisez committed
124 125 126 127 128 129
typedef enum _StreamType {
	STREAM_TYPE_PLAYBACK,
	STREAM_TYPE_RECORD
} StreamType;

typedef struct _Stream{
130
	pa_sample_spec sampleSpec;
131
	pa_stream *stream;
François Grisez's avatar
François Grisez committed
132 133
	pa_stream_state_t state;
}Stream;
134

135
static void stream_disconnect(Stream *s);
François Grisez's avatar
François Grisez committed
136 137

static void stream_state_notify_cb(pa_stream *p, void *userData) {
138
	Stream *ctx = (Stream *)userData;
François Grisez's avatar
François Grisez committed
139 140 141 142 143 144 145 146 147 148 149 150 151
	ctx->state = pa_stream_get_state(p);
	pa_threaded_mainloop_signal(pa_loop, 0);
}

static bool_t stream_wait_for_state(Stream *ctx, pa_stream_state_t successState, pa_stream_state_t failureState) {
	pa_threaded_mainloop_lock(pa_loop);
	while(ctx->state != successState && ctx->state != failureState) {
		pa_threaded_mainloop_wait(pa_loop);
	}
	pa_threaded_mainloop_unlock(pa_loop);
	return ctx->state == successState;
}

152 153
static Stream *stream_new(void) {
	Stream *s = ms_new0(Stream, 1);
154 155 156
	s->sampleSpec.format = PA_SAMPLE_S16LE;
	s->sampleSpec.channels=1;
	s->sampleSpec.rate=8000;
François Grisez's avatar
François Grisez committed
157 158
	s->stream = NULL;
	s->state = PA_STREAM_UNCONNECTED;
159 160 161 162 163 164 165
	return s;
}

static void stream_free(Stream *s) {
	if(s->stream) {
		stream_disconnect(s);
	}
François Grisez's avatar
François Grisez committed
166
	ms_free(s);
167 168
}

169
static bool_t stream_connect(Stream *s, StreamType type, pa_buffer_attr *attr) {
170
	int err;
171
	if (context==NULL) {
François Grisez's avatar
François Grisez committed
172
		ms_error("No PulseAudio context");
173
		return FALSE;
François Grisez's avatar
François Grisez committed
174
	}
175 176 177 178 179 180 181 182
	s->stream=pa_stream_new(context,"phone",&s->sampleSpec,NULL);
	if (s->stream==NULL){
		ms_error("fails to create PulseAudio stream");
		return FALSE;
	}
	pa_stream_set_state_callback(s->stream, stream_state_notify_cb, s);
	pa_threaded_mainloop_lock(pa_loop);
	if(type == STREAM_TYPE_PLAYBACK) {
François Grisez's avatar
François Grisez committed
183
		err=pa_stream_connect_playback(s->stream,NULL,attr, PA_STREAM_ADJUST_LATENCY, NULL, NULL);
184 185 186 187 188 189 190 191 192 193 194
	} else {
		err=pa_stream_connect_record(s->stream,NULL,attr, PA_STREAM_ADJUST_LATENCY);
	}
	pa_threaded_mainloop_unlock(pa_loop);
	if(err < 0 || !stream_wait_for_state(s, PA_STREAM_READY, PA_STREAM_FAILED)) {
		ms_error("Fails to connect pulseaudio stream. err=%d", err);
		pa_stream_unref(s->stream);
		s->stream = NULL;
		return FALSE;
	}
	return TRUE;
François Grisez's avatar
François Grisez committed
195
}
196

François Grisez's avatar
François Grisez committed
197 198 199 200 201 202
static void stream_disconnect(Stream *s) {
	int err;
	if (s->stream) {
		pa_threaded_mainloop_lock(pa_loop);
		err = pa_stream_disconnect(s->stream);
		pa_threaded_mainloop_unlock(pa_loop);
François Grisez's avatar
François Grisez committed
203
		if(err!=0 || !stream_wait_for_state(s, PA_STREAM_TERMINATED, PA_STREAM_FAILED)) {
204
			ms_error("pa_stream_disconnect() failed. err=%d", err);
François Grisez's avatar
François Grisez committed
205 206 207 208 209
		}
		pa_stream_unref(s->stream);
		s->stream = NULL;
	}
}
210

211 212
typedef Stream RecordStream;

François Grisez's avatar
François Grisez committed
213
static void pulse_read_init(MSFilter *f){
214
	if(!wait_for_context_state(PA_CONTEXT_READY, PA_CONTEXT_FAILED)) {
François Grisez's avatar
François Grisez committed
215
		ms_error("Could not connect to a pulseaudio server");
216
		return;
217
	}
François Grisez's avatar
François Grisez committed
218 219 220 221 222 223
	f->data = stream_new();
}

static void pulse_read_preprocess(MSFilter *f) {
	RecordStream *s=(RecordStream *)f->data;
	pa_buffer_attr attr;
224 225 226 227 228
	attr.maxlength = -1;
	attr.fragsize = pa_usec_to_bytes(fragtime * 1000, &s->sampleSpec);
	attr.tlength = -1;
	attr.minreq = -1;
	attr.prebuf = -1;
François Grisez's avatar
François Grisez committed
229 230
	if(!stream_connect(s, STREAM_TYPE_RECORD, &attr)) {
		ms_error("Pulseaudio: fail to connect record stream");
231 232 233 234
	}
}

static void pulse_read_process(MSFilter *f){
235 236 237
	RecordStream *s=(RecordStream *)f->data;
	const void *buffer=NULL;
	size_t nbytes;
François Grisez's avatar
François Grisez committed
238

François Grisez's avatar
François Grisez committed
239 240
	if(s->stream == NULL) {
		ms_error("Record stream not connected");
241 242 243 244 245 246 247 248 249 250 251 252 253
		return;
	}
	pa_threaded_mainloop_lock(pa_loop);
	while(pa_stream_readable_size(s->stream) > 0) {
		if(pa_stream_peek(s->stream, &buffer, &nbytes) >= 0) {
			if(buffer != NULL) {
				mblk_t *om = allocb(nbytes, 0);
				memcpy(om->b_wptr, buffer, nbytes);
				om->b_wptr += nbytes;
				ms_queue_put(f->outputs[0], om);
			}
			if(nbytes > 0) {
				pa_stream_drop(s->stream);
François Grisez's avatar
François Grisez committed
254
			}
255 256 257
		} else {
			ms_error("pa_stream_peek() failed");
			break;
258 259
		}
	}
260
	pa_threaded_mainloop_unlock(pa_loop);
261 262
}

François Grisez's avatar
François Grisez committed
263 264 265 266 267
static void pulse_read_postprocess(MSFilter *f) {
	RecordStream *s=(RecordStream *)f->data;
	stream_disconnect(s);
}

268
static void pulse_read_uninit(MSFilter *f) {
François Grisez's avatar
François Grisez committed
269
	stream_free((Stream *)f->data);
270 271
}

272
static int pulse_read_set_sr(MSFilter *f, void *arg){
François Grisez's avatar
François Grisez committed
273 274 275
	RecordStream *s = (RecordStream *)f->data;
	s->sampleSpec.rate = *(int *)arg;
	return 0;
276 277
}

François Grisez's avatar
François Grisez committed
278 279 280 281
static int pulse_read_get_sr(MSFilter *f, void *arg) {
	RecordStream *s = (RecordStream *)f->data;
	*(int *)arg = s->sampleSpec.rate;
	return 0;
282 283 284
}

static int pulse_read_set_nchannels(MSFilter *f, void *arg){
François Grisez's avatar
François Grisez committed
285 286 287
	RecordStream *s = (RecordStream *)f->data;
	s->sampleSpec.channels = *(int *)arg;
	return 0;
288 289
}

Simon Morlat's avatar
Simon Morlat committed
290
static int pulse_read_get_nchannels(MSFilter *f, void *arg){
François Grisez's avatar
François Grisez committed
291 292 293
	RecordStream *s = (RecordStream *)f->data;
	*(int *)arg = s->sampleSpec.channels;
	return 0;
Simon Morlat's avatar
Simon Morlat committed
294 295
}

296 297 298 299
static MSFilterMethod pulse_read_methods[]={
	{	MS_FILTER_SET_SAMPLE_RATE , pulse_read_set_sr },
	{	MS_FILTER_GET_SAMPLE_RATE , pulse_read_get_sr },
	{	MS_FILTER_SET_NCHANNELS	, pulse_read_set_nchannels },
Simon Morlat's avatar
Simon Morlat committed
300
	{	MS_FILTER_GET_NCHANNELS	, pulse_read_get_nchannels },
301 302 303 304 305 306 307 308 309 310 311
	{	0	, 0 }
};

static MSFilterDesc pulse_read_desc={
	.id=MS_PULSE_READ_ID,
	.name="MSPulseRead",
	.text="Sound input plugin based on PulseAudio",
	.ninputs=0,
	.noutputs=1,
	.category=MS_FILTER_OTHER,
	.init=pulse_read_init,
François Grisez's avatar
François Grisez committed
312
	.preprocess=pulse_read_preprocess,
313
	.process=pulse_read_process,
François Grisez's avatar
François Grisez committed
314
	.postprocess=pulse_read_postprocess,
315
	.uninit=pulse_read_uninit,
316 317 318 319
	.methods=pulse_read_methods
};


François Grisez's avatar
François Grisez committed
320
typedef Stream PlaybackStream;
321 322

static void pulse_write_init(MSFilter *f){
323 324 325 326
	if(!wait_for_context_state(PA_CONTEXT_READY, PA_CONTEXT_FAILED)) {
		ms_error("Could not connect to a pulseaudio server");
		return;
	}
François Grisez's avatar
François Grisez committed
327 328 329 330 331 332 333
	f->data = stream_new();
}

static void pulse_write_preprocess(MSFilter *f) {
	PlaybackStream *s=(PlaybackStream*)f->data;
	if(!stream_connect(s, STREAM_TYPE_PLAYBACK, NULL)) {
		ms_error("Pulseaudio: fail to connect playback stream");
334 335 336 337
	}
}

static void pulse_write_process(MSFilter *f){
338
	PlaybackStream *s=(PlaybackStream*)f->data;
François Grisez's avatar
François Grisez committed
339
	if(s->stream) {
François Grisez's avatar
François Grisez committed
340
		mblk_t *im;
341 342
		while((im=ms_queue_get(f->inputs[0]))) {
			int err, writable;
François Grisez's avatar
François Grisez committed
343
			int bsize=msgdsize(im);
344 345 346 347 348 349 350
			pa_threaded_mainloop_lock(pa_loop);
			writable=pa_stream_writable_size(s->stream);
			if (writable>=0 && writable<bsize){
				pa_stream_flush(s->stream,NULL,NULL);
				ms_warning("pa_stream_writable_size(): not enough space, flushing");
			}else if ((err=pa_stream_write(s->stream,im->b_rptr,bsize,NULL,0,PA_SEEK_RELATIVE))!=0){
				ms_error("pa_stream_write(): %s",pa_strerror(err));
351
			}
352
			pa_threaded_mainloop_unlock(pa_loop);
François Grisez's avatar
François Grisez committed
353
			freemsg(im);
354
		}
355 356
	} else {
		ms_queue_flush(f->inputs[0]);
357 358 359
	}
}

François Grisez's avatar
François Grisez committed
360 361 362 363 364
static void pulse_write_postprocess(MSFilter *f) {
	PlaybackStream *s=(PlaybackStream*)f->data;
	stream_disconnect(s);
}

365
static void pulse_write_uninit(MSFilter *f) {
François Grisez's avatar
François Grisez committed
366
	stream_free((Stream *)f->data);
367 368
}

369 370 371 372 373 374 375 376
static MSFilterDesc pulse_write_desc={
	.id=MS_PULSE_WRITE_ID,
	.name="MSPulseWrite",
	.text="Sound output plugin based on PulseAudio",
	.ninputs=1,
	.noutputs=0,
	.category=MS_FILTER_OTHER,
	.init=pulse_write_init,
François Grisez's avatar
François Grisez committed
377
	.preprocess=pulse_write_preprocess,
378
	.process=pulse_write_process,
François Grisez's avatar
François Grisez committed
379
	.postprocess=pulse_write_postprocess,
380
	.uninit=pulse_write_uninit,
381 382 383
	.methods=pulse_read_methods
};

François Grisez's avatar
François Grisez committed
384
static MSFilter *pulse_card_create_reader(MSSndCard *card) {
385 386 387
	return ms_filter_new_from_desc (&pulse_read_desc);
}

François Grisez's avatar
François Grisez committed
388
static MSFilter *pulse_card_create_writer(MSSndCard *card) {
389 390 391
	return ms_filter_new_from_desc (&pulse_write_desc);
}