1 /*
2 Copyright (C) 2015. Guillermo A. Amaral B. <g@maral.me>
3
4 Based on Pulse Input plugin by Leonhard Oelke.
5
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include <util/bmem.h>
21 #include <util/platform.h>
22 #include <util/threading.h>
23 #include <util/util_uint64.h>
24 #include <obs-module.h>
25
26 #include <alsa/asoundlib.h>
27 #include <alsa/pcm.h>
28
29 #include <pthread.h>
30
31 #define blog(level, msg, ...) blog(level, "alsa-input: " msg, ##__VA_ARGS__)
32
33 #define NSEC_PER_SEC 1000000000LL
34 #define NSEC_PER_MSEC 1000000L
35 #define STARTUP_TIMEOUT_NS (500 * NSEC_PER_MSEC)
36 #define REOPEN_TIMEOUT 1000UL
37 #define SHUTDOWN_ON_DEACTIVATE false
38
39 struct alsa_data {
40 obs_source_t *source;
41 #if SHUTDOWN_ON_DEACTIVATE
42 bool active;
43 #endif
44
45 /* user settings */
46 char *device;
47
48 /* pthread */
49 pthread_t listen_thread;
50 pthread_t reopen_thread;
51 os_event_t *abort_event;
52 volatile bool listen;
53 volatile bool reopen;
54
55 /* alsa */
56 snd_pcm_t *handle;
57 snd_pcm_format_t format;
58 snd_pcm_uframes_t period_size;
59
60 unsigned int channels;
61 unsigned int rate;
62 unsigned int sample_size;
63 uint8_t *buffer;
64 uint64_t first_ts;
65 };
66
67 static const char *alsa_get_name(void *);
68 static bool alsa_devices_changed(obs_properties_t *props, obs_property_t *p,
69 obs_data_t *settings);
70 static obs_properties_t *alsa_get_properties(void *);
71 static void *alsa_create(obs_data_t *, obs_source_t *);
72 static void alsa_destroy(void *);
73 static void alsa_activate(void *);
74 static void alsa_deactivate(void *);
75 static void alsa_get_defaults(obs_data_t *);
76 static void alsa_update(void *, obs_data_t *);
77
78 struct obs_source_info alsa_input_capture = {
79 .id = "alsa_input_capture",
80 .type = OBS_SOURCE_TYPE_INPUT,
81 .output_flags = OBS_SOURCE_AUDIO,
82 .create = alsa_create,
83 .destroy = alsa_destroy,
84 #if SHUTDOWN_ON_DEACTIVATE
85 .activate = alsa_activate,
86 .deactivate = alsa_deactivate,
87 #endif
88 .update = alsa_update,
89 .get_defaults = alsa_get_defaults,
90 .get_name = alsa_get_name,
91 .get_properties = alsa_get_properties,
92 .icon_type = OBS_ICON_TYPE_AUDIO_INPUT,
93 };
94
95 static bool _alsa_try_open(struct alsa_data *);
96 static bool _alsa_open(struct alsa_data *);
97 static void _alsa_close(struct alsa_data *);
98 static bool _alsa_configure(struct alsa_data *);
99 static void _alsa_start_reopen(struct alsa_data *);
100 static void _alsa_stop_reopen(struct alsa_data *);
101 static void *_alsa_listen(void *);
102 static void *_alsa_reopen(void *);
103
104 static enum audio_format _alsa_to_obs_audio_format(snd_pcm_format_t);
105 static enum speaker_layout _alsa_channels_to_obs_speakers(unsigned int);
106
107 /*****************************************************************************/
108
alsa_create(obs_data_t * settings,obs_source_t * source)109 void *alsa_create(obs_data_t *settings, obs_source_t *source)
110 {
111 struct alsa_data *data = bzalloc(sizeof(struct alsa_data));
112
113 data->source = source;
114 #if SHUTDOWN_ON_DEACTIVATE
115 data->active = false;
116 #endif
117 data->buffer = NULL;
118 data->device = NULL;
119 data->first_ts = 0;
120 data->handle = NULL;
121 data->listen = false;
122 data->reopen = false;
123 data->listen_thread = 0;
124 data->reopen_thread = 0;
125
126 const char *device = obs_data_get_string(settings, "device_id");
127
128 if (strcmp(device, "__custom__") == 0)
129 device = obs_data_get_string(settings, "custom_pcm");
130
131 data->device = bstrdup(device);
132 data->rate = obs_data_get_int(settings, "rate");
133
134 if (os_event_init(&data->abort_event, OS_EVENT_TYPE_MANUAL) != 0) {
135 blog(LOG_ERROR, "Abort event creation failed!");
136 goto cleanup;
137 }
138
139 #if !SHUTDOWN_ON_DEACTIVATE
140 _alsa_try_open(data);
141 #endif
142 return data;
143
144 cleanup:
145 if (data->device)
146 bfree(data->device);
147
148 bfree(data);
149 return NULL;
150 }
151
alsa_destroy(void * vptr)152 void alsa_destroy(void *vptr)
153 {
154 struct alsa_data *data = vptr;
155
156 if (data->handle)
157 _alsa_close(data);
158
159 os_event_destroy(data->abort_event);
160 bfree(data->device);
161 bfree(data);
162 }
163
164 #if SHUTDOWN_ON_DEACTIVATE
alsa_activate(void * vptr)165 void alsa_activate(void *vptr)
166 {
167 struct alsa_data *data = vptr;
168
169 data->active = true;
170 _alsa_try_open(data);
171 }
172
alsa_deactivate(void * vptr)173 void alsa_deactivate(void *vptr)
174 {
175 struct alsa_data *data = vptr;
176
177 _alsa_stop_reopen(data);
178 _alsa_close(data);
179 data->active = false;
180 }
181 #endif
182
alsa_update(void * vptr,obs_data_t * settings)183 void alsa_update(void *vptr, obs_data_t *settings)
184 {
185 struct alsa_data *data = vptr;
186 const char *device;
187 unsigned int rate;
188 bool reset = false;
189
190 device = obs_data_get_string(settings, "device_id");
191
192 if (strcmp(device, "__custom__") == 0)
193 device = obs_data_get_string(settings, "custom_pcm");
194
195 if (strcmp(data->device, device) != 0) {
196 bfree(data->device);
197 data->device = bstrdup(device);
198 reset = true;
199 }
200
201 rate = obs_data_get_int(settings, "rate");
202 if (data->rate != rate) {
203 data->rate = rate;
204 reset = true;
205 }
206
207 #if SHUTDOWN_ON_DEACTIVATE
208 if (reset && data->handle)
209 _alsa_close(data);
210
211 if (data->active && !data->handle)
212 _alsa_try_open(data);
213 #else
214 if (reset) {
215 if (data->handle)
216 _alsa_close(data);
217 _alsa_try_open(data);
218 }
219 #endif
220 }
221
alsa_get_name(void * unused)222 const char *alsa_get_name(void *unused)
223 {
224 UNUSED_PARAMETER(unused);
225 return obs_module_text("AlsaInput");
226 }
227
alsa_get_defaults(obs_data_t * settings)228 void alsa_get_defaults(obs_data_t *settings)
229 {
230 obs_data_set_default_string(settings, "device_id", "default");
231 obs_data_set_default_string(settings, "custom_pcm", "default");
232 obs_data_set_default_int(settings, "rate", 44100);
233 }
234
alsa_devices_changed(obs_properties_t * props,obs_property_t * p,obs_data_t * settings)235 static bool alsa_devices_changed(obs_properties_t *props, obs_property_t *p,
236 obs_data_t *settings)
237 {
238 UNUSED_PARAMETER(p);
239 bool visible = false;
240 const char *device_id = obs_data_get_string(settings, "device_id");
241
242 if (strcmp(device_id, "__custom__") == 0)
243 visible = true;
244
245 obs_property_t *custom_pcm = obs_properties_get(props, "custom_pcm");
246
247 obs_property_set_visible(custom_pcm, visible);
248 obs_property_modified(custom_pcm, settings);
249
250 return true;
251 }
252
alsa_get_properties(void * unused)253 obs_properties_t *alsa_get_properties(void *unused)
254 {
255 void **hints;
256 void **hint;
257 char *name = NULL;
258 char *descr = NULL;
259 char *io = NULL;
260 char *descr_i;
261 obs_properties_t *props;
262 obs_property_t *devices;
263 obs_property_t *rate;
264
265 UNUSED_PARAMETER(unused);
266
267 props = obs_properties_create();
268
269 devices = obs_properties_add_list(props, "device_id",
270 obs_module_text("Device"),
271 OBS_COMBO_TYPE_LIST,
272 OBS_COMBO_FORMAT_STRING);
273
274 obs_property_list_add_string(devices, "Default", "default");
275
276 obs_properties_add_text(props, "custom_pcm", obs_module_text("PCM"),
277 OBS_TEXT_DEFAULT);
278
279 rate = obs_properties_add_list(props, "rate", obs_module_text("Rate"),
280 OBS_COMBO_TYPE_LIST,
281 OBS_COMBO_FORMAT_INT);
282
283 obs_property_set_modified_callback(devices, alsa_devices_changed);
284
285 obs_property_list_add_int(rate, "32000 Hz", 32000);
286 obs_property_list_add_int(rate, "44100 Hz", 44100);
287 obs_property_list_add_int(rate, "48000 Hz", 48000);
288
289 if (snd_device_name_hint(-1, "pcm", &hints) < 0)
290 return props;
291
292 hint = hints;
293 while (*hint != NULL) {
294 /* check if we're dealing with an Input */
295 io = snd_device_name_get_hint(*hint, "IOID");
296 if (io != NULL && strcmp(io, "Input") != 0)
297 goto next;
298
299 name = snd_device_name_get_hint(*hint, "NAME");
300 if (name == NULL || strstr(name, "front:") == NULL)
301 goto next;
302
303 descr = snd_device_name_get_hint(*hint, "DESC");
304 if (!descr)
305 goto next;
306
307 descr_i = descr;
308 while (*descr_i) {
309 if (*descr_i == '\n') {
310 *descr_i = '\0';
311 break;
312 } else
313 ++descr_i;
314 }
315
316 obs_property_list_add_string(devices, descr, name);
317
318 next:
319 if (name != NULL)
320 free(name), name = NULL;
321
322 if (descr != NULL)
323 free(descr), descr = NULL;
324
325 if (io != NULL)
326 free(io), io = NULL;
327
328 ++hint;
329 }
330 obs_property_list_add_string(devices, "Custom", "__custom__");
331
332 snd_device_name_free_hint(hints);
333
334 return props;
335 }
336
337 /*****************************************************************************/
338
_alsa_try_open(struct alsa_data * data)339 bool _alsa_try_open(struct alsa_data *data)
340 {
341 _alsa_stop_reopen(data);
342
343 if (_alsa_open(data))
344 return true;
345
346 _alsa_start_reopen(data);
347
348 return false;
349 }
350
_alsa_open(struct alsa_data * data)351 bool _alsa_open(struct alsa_data *data)
352 {
353 pthread_attr_t attr;
354 int err;
355
356 err = snd_pcm_open(&data->handle, data->device, SND_PCM_STREAM_CAPTURE,
357 0);
358 if (err < 0) {
359 blog(LOG_ERROR, "Failed to open '%s': %s", data->device,
360 snd_strerror(err));
361 return false;
362 }
363
364 if (!_alsa_configure(data))
365 goto cleanup;
366
367 if (snd_pcm_state(data->handle) != SND_PCM_STATE_PREPARED) {
368 blog(LOG_ERROR, "Device not prepared: '%s'", data->device);
369 goto cleanup;
370 }
371
372 /* start listening */
373
374 err = snd_pcm_start(data->handle);
375 if (err < 0) {
376 blog(LOG_ERROR, "Failed to start '%s': %s", data->device,
377 snd_strerror(err));
378 goto cleanup;
379 }
380
381 /* create capture thread */
382
383 pthread_attr_init(&attr);
384 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
385
386 err = pthread_create(&data->listen_thread, &attr, _alsa_listen, data);
387 if (err) {
388 pthread_attr_destroy(&attr);
389 blog(LOG_ERROR,
390 "Failed to create capture thread for device '%s'.",
391 data->device);
392 goto cleanup;
393 }
394
395 pthread_attr_destroy(&attr);
396 return true;
397
398 cleanup:
399 _alsa_close(data);
400 return false;
401 }
402
_alsa_close(struct alsa_data * data)403 void _alsa_close(struct alsa_data *data)
404 {
405 if (data->listen_thread) {
406 os_atomic_set_bool(&data->listen, false);
407 pthread_join(data->listen_thread, NULL);
408 data->listen_thread = 0;
409 }
410
411 if (data->handle) {
412 snd_pcm_drop(data->handle);
413 snd_pcm_close(data->handle), data->handle = NULL;
414 }
415
416 if (data->buffer)
417 bfree(data->buffer), data->buffer = NULL;
418 }
419
_alsa_configure(struct alsa_data * data)420 bool _alsa_configure(struct alsa_data *data)
421 {
422 snd_pcm_hw_params_t *hwparams;
423 int err;
424 int dir;
425
426 snd_pcm_hw_params_alloca(&hwparams);
427
428 err = snd_pcm_hw_params_any(data->handle, hwparams);
429 if (err < 0) {
430 blog(LOG_ERROR, "snd_pcm_hw_params_any failed: %s",
431 snd_strerror(err));
432 return false;
433 }
434
435 err = snd_pcm_hw_params_set_access(data->handle, hwparams,
436 SND_PCM_ACCESS_RW_INTERLEAVED);
437 if (err < 0) {
438 blog(LOG_ERROR, "snd_pcm_hw_params_set_access failed: %s",
439 snd_strerror(err));
440 return false;
441 }
442
443 #define FORMAT_SIZE 4
444 snd_pcm_format_t formats[FORMAT_SIZE] = {SND_PCM_FORMAT_S16_LE,
445 SND_PCM_FORMAT_S32_LE,
446 SND_PCM_FORMAT_FLOAT_LE,
447 SND_PCM_FORMAT_U8};
448 bool format_found = false;
449 for (int i = 0; i < FORMAT_SIZE; ++i) {
450 data->format = formats[i];
451 err = snd_pcm_hw_params_test_format(data->handle, hwparams,
452 data->format);
453 if (err == 0) {
454 format_found = true;
455 break;
456 }
457 }
458 #undef FORMAT_SIZE
459 if (!format_found) {
460 blog(LOG_ERROR, "device doesnt support any OBS formats");
461 return false;
462 }
463 snd_pcm_hw_params_set_format(data->handle, hwparams, data->format);
464 if (err < 0) {
465 blog(LOG_ERROR, "snd_pcm_hw_params_set_format failed: %s",
466 snd_strerror(err));
467 return false;
468 }
469
470 err = snd_pcm_hw_params_set_rate_near(data->handle, hwparams,
471 &data->rate, 0);
472 if (err < 0) {
473 blog(LOG_ERROR, "snd_pcm_hw_params_set_rate_near failed: %s",
474 snd_strerror(err));
475 return false;
476 }
477 blog(LOG_INFO, "PCM '%s' rate set to %d", data->device, data->rate);
478
479 err = snd_pcm_hw_params_get_channels(hwparams, &data->channels);
480 if (err < 0)
481 data->channels = 2;
482
483 err = snd_pcm_hw_params_set_channels_near(data->handle, hwparams,
484 &data->channels);
485 if (err < 0) {
486 blog(LOG_ERROR,
487 "snd_pcm_hw_params_set_channels_near failed: %s",
488 snd_strerror(err));
489 return false;
490 }
491 blog(LOG_INFO, "PCM '%s' channels set to %d", data->device,
492 data->channels);
493
494 err = snd_pcm_hw_params(data->handle, hwparams);
495 if (err < 0) {
496 blog(LOG_ERROR, "snd_pcm_hw_params failed: %s",
497 snd_strerror(err));
498 return false;
499 }
500
501 err = snd_pcm_hw_params_get_period_size(hwparams, &data->period_size,
502 &dir);
503 if (err < 0) {
504 blog(LOG_ERROR, "snd_pcm_hw_params_get_period_size failed: %s",
505 snd_strerror(err));
506 return false;
507 }
508
509 data->sample_size =
510 (data->channels * snd_pcm_format_physical_width(data->format)) /
511 8;
512
513 if (data->buffer)
514 bfree(data->buffer);
515 data->buffer = bzalloc(data->period_size * data->sample_size);
516
517 return true;
518 }
519
_alsa_start_reopen(struct alsa_data * data)520 void _alsa_start_reopen(struct alsa_data *data)
521 {
522 pthread_attr_t attr;
523 int err;
524
525 if (os_atomic_load_bool(&data->reopen))
526 return;
527
528 pthread_attr_init(&attr);
529 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
530
531 err = pthread_create(&data->reopen_thread, &attr, _alsa_reopen, data);
532 if (err) {
533 blog(LOG_ERROR,
534 "Failed to create reopen thread for device '%s'.",
535 data->device);
536 }
537
538 pthread_attr_destroy(&attr);
539 }
540
_alsa_stop_reopen(struct alsa_data * data)541 void _alsa_stop_reopen(struct alsa_data *data)
542 {
543 if (os_atomic_load_bool(&data->reopen))
544 os_event_signal(data->abort_event);
545
546 if (data->reopen_thread) {
547 pthread_join(data->reopen_thread, NULL);
548 data->reopen_thread = 0;
549 }
550
551 os_event_reset(data->abort_event);
552 }
553
_alsa_listen(void * attr)554 void *_alsa_listen(void *attr)
555 {
556 struct alsa_data *data = attr;
557 struct obs_source_audio out;
558
559 blog(LOG_DEBUG, "Capture thread started.");
560
561 out.data[0] = data->buffer;
562 out.format = _alsa_to_obs_audio_format(data->format);
563 out.speakers = _alsa_channels_to_obs_speakers(data->channels);
564 out.samples_per_sec = data->rate;
565
566 os_atomic_set_bool(&data->listen, true);
567
568 do {
569 snd_pcm_sframes_t frames = snd_pcm_readi(
570 data->handle, data->buffer, data->period_size);
571
572 if (!os_atomic_load_bool(&data->listen))
573 break;
574
575 if (frames <= 0) {
576 frames = snd_pcm_recover(data->handle, frames, 0);
577 if (frames <= 0) {
578 snd_pcm_wait(data->handle, 100);
579 continue;
580 }
581 }
582
583 out.frames = frames;
584 out.timestamp =
585 os_gettime_ns() -
586 util_mul_div64(frames, NSEC_PER_SEC, data->rate);
587
588 if (!data->first_ts)
589 data->first_ts = out.timestamp + STARTUP_TIMEOUT_NS;
590
591 if (out.timestamp > data->first_ts)
592 obs_source_output_audio(data->source, &out);
593 } while (os_atomic_load_bool(&data->listen));
594
595 blog(LOG_DEBUG, "Capture thread is about to exit.");
596
597 pthread_exit(NULL);
598 return NULL;
599 }
600
_alsa_reopen(void * attr)601 void *_alsa_reopen(void *attr)
602 {
603 struct alsa_data *data = attr;
604 unsigned long timeout = REOPEN_TIMEOUT;
605
606 blog(LOG_DEBUG, "Reopen thread started.");
607
608 os_atomic_set_bool(&data->reopen, true);
609
610 while (os_event_timedwait(data->abort_event, timeout) == ETIMEDOUT) {
611 if (_alsa_open(data))
612 break;
613
614 if (timeout < (REOPEN_TIMEOUT * 5))
615 timeout += REOPEN_TIMEOUT;
616 }
617
618 os_atomic_set_bool(&data->reopen, false);
619
620 blog(LOG_DEBUG, "Reopen thread is about to exit.");
621
622 pthread_exit(NULL);
623 return NULL;
624 }
625
_alsa_to_obs_audio_format(snd_pcm_format_t format)626 enum audio_format _alsa_to_obs_audio_format(snd_pcm_format_t format)
627 {
628 switch (format) {
629 case SND_PCM_FORMAT_U8:
630 return AUDIO_FORMAT_U8BIT;
631 case SND_PCM_FORMAT_S16_LE:
632 return AUDIO_FORMAT_16BIT;
633 case SND_PCM_FORMAT_S32_LE:
634 return AUDIO_FORMAT_32BIT;
635 case SND_PCM_FORMAT_FLOAT_LE:
636 return AUDIO_FORMAT_FLOAT;
637 default:
638 break;
639 }
640
641 return AUDIO_FORMAT_UNKNOWN;
642 }
643
_alsa_channels_to_obs_speakers(unsigned int channels)644 enum speaker_layout _alsa_channels_to_obs_speakers(unsigned int channels)
645 {
646 switch (channels) {
647 case 1:
648 return SPEAKERS_MONO;
649 case 2:
650 return SPEAKERS_STEREO;
651 case 3:
652 return SPEAKERS_2POINT1;
653 case 4:
654 return SPEAKERS_4POINT0;
655 case 5:
656 return SPEAKERS_4POINT1;
657 case 6:
658 return SPEAKERS_5POINT1;
659 case 8:
660 return SPEAKERS_7POINT1;
661 }
662
663 return SPEAKERS_UNKNOWN;
664 }
665