mirror of
				https://github.com/RetroDECK/Duckstation.git
				synced 2025-04-10 19:15:14 +00:00 
			
		
		
		
	
		
			
	
	
		
			1453 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
		
		
			
		
	
	
			1453 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
|   | /*
 | ||
|  |  * Copyright © 2011 Mozilla Foundation | ||
|  |  * | ||
|  |  * This program is made available under an ISC-style license.  See the | ||
|  |  * accompanying file LICENSE for details. | ||
|  |  */ | ||
|  | #undef NDEBUG
 | ||
|  | #define _DEFAULT_SOURCE
 | ||
|  | #define _BSD_SOURCE
 | ||
|  | #define _XOPEN_SOURCE 500
 | ||
|  | #include <pthread.h>
 | ||
|  | #include <sys/time.h>
 | ||
|  | #include <assert.h>
 | ||
|  | #include <limits.h>
 | ||
|  | #include <poll.h>
 | ||
|  | #include <unistd.h>
 | ||
|  | #include <dlfcn.h>
 | ||
|  | #include <alsa/asoundlib.h>
 | ||
|  | #include "cubeb/cubeb.h"
 | ||
|  | #include "cubeb-internal.h"
 | ||
|  | 
 | ||
|  | #ifdef DISABLE_LIBASOUND_DLOPEN
 | ||
|  | #define WRAP(x) x
 | ||
|  | #else
 | ||
|  | #define WRAP(x) cubeb_##x
 | ||
|  | #define LIBASOUND_API_VISIT(X)                   \
 | ||
|  |   X(snd_config)                                  \ | ||
|  |   X(snd_config_add)                              \ | ||
|  |   X(snd_config_copy)                             \ | ||
|  |   X(snd_config_delete)                           \ | ||
|  |   X(snd_config_get_id)                           \ | ||
|  |   X(snd_config_get_string)                       \ | ||
|  |   X(snd_config_imake_integer)                    \ | ||
|  |   X(snd_config_search)                           \ | ||
|  |   X(snd_config_search_definition)                \ | ||
|  |   X(snd_lib_error_set_handler)                   \ | ||
|  |   X(snd_pcm_avail_update)                        \ | ||
|  |   X(snd_pcm_close)                               \ | ||
|  |   X(snd_pcm_delay)                               \ | ||
|  |   X(snd_pcm_drain)                               \ | ||
|  |   X(snd_pcm_frames_to_bytes)                     \ | ||
|  |   X(snd_pcm_get_params)                          \ | ||
|  |   X(snd_pcm_hw_params_any)                       \ | ||
|  |   X(snd_pcm_hw_params_get_channels_max)          \ | ||
|  |   X(snd_pcm_hw_params_get_rate)                  \ | ||
|  |   X(snd_pcm_hw_params_set_rate_near)             \ | ||
|  |   X(snd_pcm_hw_params_sizeof)                    \ | ||
|  |   X(snd_pcm_nonblock)                            \ | ||
|  |   X(snd_pcm_open)                                \ | ||
|  |   X(snd_pcm_open_lconf)                          \ | ||
|  |   X(snd_pcm_pause)                               \ | ||
|  |   X(snd_pcm_poll_descriptors)                    \ | ||
|  |   X(snd_pcm_poll_descriptors_count)              \ | ||
|  |   X(snd_pcm_poll_descriptors_revents)            \ | ||
|  |   X(snd_pcm_readi)                               \ | ||
|  |   X(snd_pcm_recover)                             \ | ||
|  |   X(snd_pcm_set_params)                          \ | ||
|  |   X(snd_pcm_start)                               \ | ||
|  |   X(snd_pcm_state)                               \ | ||
|  |   X(snd_pcm_writei)                              \ | ||
|  | 
 | ||
|  | #define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x;
 | ||
|  | LIBASOUND_API_VISIT(MAKE_TYPEDEF); | ||
|  | #undef MAKE_TYPEDEF
 | ||
|  | /* snd_pcm_hw_params_alloca is actually a macro */ | ||
|  | #define snd_pcm_hw_params_sizeof cubeb_snd_pcm_hw_params_sizeof
 | ||
|  | #endif
 | ||
|  | 
 | ||
|  | #define CUBEB_STREAM_MAX 16
 | ||
|  | #define CUBEB_WATCHDOG_MS 10000
 | ||
|  | 
 | ||
|  | #define CUBEB_ALSA_PCM_NAME "default"
 | ||
|  | 
 | ||
|  | #define ALSA_PA_PLUGIN "ALSA <-> PulseAudio PCM I/O Plugin"
 | ||
|  | 
 | ||
|  | /* ALSA is not thread-safe.  snd_pcm_t instances are individually protected
 | ||
|  |    by the owning cubeb_stream's mutex.  snd_pcm_t creation and destruction | ||
|  |    is not thread-safe until ALSA 1.0.24 (see alsa-lib.git commit 91c9c8f1), | ||
|  |    so those calls must be wrapped in the following mutex. */ | ||
|  | static pthread_mutex_t cubeb_alsa_mutex = PTHREAD_MUTEX_INITIALIZER; | ||
|  | static int cubeb_alsa_error_handler_set = 0; | ||
|  | 
 | ||
|  | static struct cubeb_ops const alsa_ops; | ||
|  | 
 | ||
|  | struct cubeb { | ||
|  |   struct cubeb_ops const * ops; | ||
|  |   void * libasound; | ||
|  | 
 | ||
|  |   pthread_t thread; | ||
|  | 
 | ||
|  |   /* Mutex for streams array, must not be held while blocked in poll(2). */ | ||
|  |   pthread_mutex_t mutex; | ||
|  | 
 | ||
|  |   /* Sparse array of streams managed by this context. */ | ||
|  |   cubeb_stream * streams[CUBEB_STREAM_MAX]; | ||
|  | 
 | ||
|  |   /* fds and nfds are only updated by alsa_run when rebuild is set. */ | ||
|  |   struct pollfd * fds; | ||
|  |   nfds_t nfds; | ||
|  |   int rebuild; | ||
|  | 
 | ||
|  |   int shutdown; | ||
|  | 
 | ||
|  |   /* Control pipe for forcing poll to wake and rebuild fds or recalculate the timeout. */ | ||
|  |   int control_fd_read; | ||
|  |   int control_fd_write; | ||
|  | 
 | ||
|  |   /* Track number of active streams.  This is limited to CUBEB_STREAM_MAX
 | ||
|  |      due to resource contraints. */ | ||
|  |   unsigned int active_streams; | ||
|  | 
 | ||
|  |   /* Local configuration with handle_underrun workaround set for PulseAudio
 | ||
|  |      ALSA plugin.  Will be NULL if the PA ALSA plugin is not in use or the | ||
|  |      workaround is not required. */ | ||
|  |   snd_config_t * local_config; | ||
|  |   int is_pa; | ||
|  | }; | ||
|  | 
 | ||
|  | enum stream_state { | ||
|  |   INACTIVE, | ||
|  |   RUNNING, | ||
|  |   DRAINING, | ||
|  |   PROCESSING, | ||
|  |   ERROR | ||
|  | }; | ||
|  | 
 | ||
|  | struct cubeb_stream { | ||
|  |   /* Note: Must match cubeb_stream layout in cubeb.c. */ | ||
|  |   cubeb * context; | ||
|  |   void * user_ptr; | ||
|  |   /**/ | ||
|  |   pthread_mutex_t mutex; | ||
|  |   snd_pcm_t * pcm; | ||
|  |   cubeb_data_callback data_callback; | ||
|  |   cubeb_state_callback state_callback; | ||
|  |   snd_pcm_uframes_t stream_position; | ||
|  |   snd_pcm_uframes_t last_position; | ||
|  |   snd_pcm_uframes_t buffer_size; | ||
|  |   cubeb_stream_params params; | ||
|  | 
 | ||
|  |   /* Every member after this comment is protected by the owning context's
 | ||
|  |      mutex rather than the stream's mutex, or is only used on the context's | ||
|  |      run thread. */ | ||
|  |   pthread_cond_t cond; /* Signaled when the stream's state is changed. */ | ||
|  | 
 | ||
|  |   enum stream_state state; | ||
|  | 
 | ||
|  |   struct pollfd * saved_fds; /* A copy of the pollfds passed in at init time. */ | ||
|  |   struct pollfd * fds; /* Pointer to this waitable's pollfds within struct cubeb's fds. */ | ||
|  |   nfds_t nfds; | ||
|  | 
 | ||
|  |   struct timeval drain_timeout; | ||
|  | 
 | ||
|  |   /* XXX: Horrible hack -- if an active stream has been idle for
 | ||
|  |      CUBEB_WATCHDOG_MS it will be disabled and the error callback will be | ||
|  |      called.  This works around a bug seen with older versions of ALSA and | ||
|  |      PulseAudio where streams would stop requesting new data despite still | ||
|  |      being logically active and playing. */ | ||
|  |   struct timeval last_activity; | ||
|  |   float volume; | ||
|  | 
 | ||
|  |   char * buffer; | ||
|  |   snd_pcm_uframes_t bufframes; | ||
|  |   snd_pcm_stream_t stream_type; | ||
|  | 
 | ||
|  |   struct cubeb_stream * other_stream; | ||
|  | }; | ||
|  | 
 | ||
|  | static int | ||
|  | any_revents(struct pollfd * fds, nfds_t nfds) | ||
|  | { | ||
|  |   nfds_t i; | ||
|  | 
 | ||
|  |   for (i = 0; i < nfds; ++i) { | ||
|  |     if (fds[i].revents) { | ||
|  |       return 1; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return 0; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | cmp_timeval(struct timeval * a, struct timeval * b) | ||
|  | { | ||
|  |   if (a->tv_sec == b->tv_sec) { | ||
|  |     if (a->tv_usec == b->tv_usec) { | ||
|  |       return 0; | ||
|  |     } | ||
|  |     return a->tv_usec > b->tv_usec ? 1 : -1; | ||
|  |   } | ||
|  |   return a->tv_sec > b->tv_sec ? 1 : -1; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | timeval_to_relative_ms(struct timeval * tv) | ||
|  | { | ||
|  |   struct timeval now; | ||
|  |   struct timeval dt; | ||
|  |   long long t; | ||
|  |   int r; | ||
|  | 
 | ||
|  |   gettimeofday(&now, NULL); | ||
|  |   r = cmp_timeval(tv, &now); | ||
|  |   if (r >= 0) { | ||
|  |     timersub(tv, &now, &dt); | ||
|  |   } else { | ||
|  |     timersub(&now, tv, &dt); | ||
|  |   } | ||
|  |   t = dt.tv_sec; | ||
|  |   t *= 1000; | ||
|  |   t += (dt.tv_usec + 500) / 1000; | ||
|  | 
 | ||
|  |   if (t > INT_MAX) { | ||
|  |     t = INT_MAX; | ||
|  |   } else if (t < INT_MIN) { | ||
|  |     t = INT_MIN; | ||
|  |   } | ||
|  | 
 | ||
|  |   return r >= 0 ? t : -t; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | ms_until(struct timeval * tv) | ||
|  | { | ||
|  |   return timeval_to_relative_ms(tv); | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | ms_since(struct timeval * tv) | ||
|  | { | ||
|  |   return -timeval_to_relative_ms(tv); | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | rebuild(cubeb * ctx) | ||
|  | { | ||
|  |   nfds_t nfds; | ||
|  |   int i; | ||
|  |   nfds_t j; | ||
|  |   cubeb_stream * stm; | ||
|  | 
 | ||
|  |   assert(ctx->rebuild); | ||
|  | 
 | ||
|  |   /* Always count context's control pipe fd. */ | ||
|  |   nfds = 1; | ||
|  |   for (i = 0; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |     stm = ctx->streams[i]; | ||
|  |     if (stm) { | ||
|  |       stm->fds = NULL; | ||
|  |       if (stm->state == RUNNING) { | ||
|  |         nfds += stm->nfds; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   free(ctx->fds); | ||
|  |   ctx->fds = calloc(nfds, sizeof(struct pollfd)); | ||
|  |   assert(ctx->fds); | ||
|  |   ctx->nfds = nfds; | ||
|  | 
 | ||
|  |   /* Include context's control pipe fd. */ | ||
|  |   ctx->fds[0].fd = ctx->control_fd_read; | ||
|  |   ctx->fds[0].events = POLLIN | POLLERR; | ||
|  | 
 | ||
|  |   for (i = 0, j = 1; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |     stm = ctx->streams[i]; | ||
|  |     if (stm && stm->state == RUNNING) { | ||
|  |       memcpy(&ctx->fds[j], stm->saved_fds, stm->nfds * sizeof(struct pollfd)); | ||
|  |       stm->fds = &ctx->fds[j]; | ||
|  |       j += stm->nfds; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   ctx->rebuild = 0; | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | poll_wake(cubeb * ctx) | ||
|  | { | ||
|  |   if (write(ctx->control_fd_write, "x", 1) < 0) { | ||
|  |     /* ignore write error */ | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | set_timeout(struct timeval * timeout, unsigned int ms) | ||
|  | { | ||
|  |   gettimeofday(timeout, NULL); | ||
|  |   timeout->tv_sec += ms / 1000; | ||
|  |   timeout->tv_usec += (ms % 1000) * 1000; | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | stream_buffer_decrement(cubeb_stream * stm, long count) | ||
|  | { | ||
|  |   char * bufremains = stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, count); | ||
|  |   memmove(stm->buffer, bufremains, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes - count)); | ||
|  |   stm->bufframes -= count; | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | alsa_set_stream_state(cubeb_stream * stm, enum stream_state state) | ||
|  | { | ||
|  |   cubeb * ctx; | ||
|  |   int r; | ||
|  | 
 | ||
|  |   ctx = stm->context; | ||
|  |   stm->state = state; | ||
|  |   r = pthread_cond_broadcast(&stm->cond); | ||
|  |   assert(r == 0); | ||
|  |   ctx->rebuild = 1; | ||
|  |   poll_wake(ctx); | ||
|  | } | ||
|  | 
 | ||
|  | static enum stream_state | ||
|  | alsa_process_stream(cubeb_stream * stm) | ||
|  | { | ||
|  |   unsigned short revents; | ||
|  |   snd_pcm_sframes_t avail; | ||
|  |   int draining; | ||
|  | 
 | ||
|  |   draining = 0; | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&stm->mutex); | ||
|  | 
 | ||
|  |   /* Call _poll_descriptors_revents() even if we don't use it
 | ||
|  |      to let underlying plugins clear null events.  Otherwise poll() | ||
|  |      may wake up again and again, producing unnecessary CPU usage. */ | ||
|  |   WRAP(snd_pcm_poll_descriptors_revents)(stm->pcm, stm->fds, stm->nfds, &revents); | ||
|  | 
 | ||
|  |   avail = WRAP(snd_pcm_avail_update)(stm->pcm); | ||
|  | 
 | ||
|  |   /* Got null event? Bail and wait for another wakeup. */ | ||
|  |   if (avail == 0) { | ||
|  |     pthread_mutex_unlock(&stm->mutex); | ||
|  |     return RUNNING; | ||
|  |   } | ||
|  | 
 | ||
|  |   /* This could happen if we were suspended with SIGSTOP/Ctrl+Z for a long time. */ | ||
|  |   if ((unsigned int) avail > stm->buffer_size) { | ||
|  |     avail = stm->buffer_size; | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Capture: Read available frames */ | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_CAPTURE && avail > 0) { | ||
|  |     snd_pcm_sframes_t got; | ||
|  | 
 | ||
|  |     if (avail + stm->bufframes > stm->buffer_size) { | ||
|  |       /* Buffer overflow. Skip and overwrite with new data. */ | ||
|  |       stm->bufframes = 0; | ||
|  |       // TODO: should it be marked as DRAINING?
 | ||
|  |     } | ||
|  | 
 | ||
|  |     got = WRAP(snd_pcm_readi)(stm->pcm, stm->buffer+stm->bufframes, avail); | ||
|  | 
 | ||
|  |     if (got < 0) { | ||
|  |       avail = got; // the error handler below will recover us
 | ||
|  |     } else { | ||
|  |       stm->bufframes += got; | ||
|  |       stm->stream_position += got; | ||
|  | 
 | ||
|  |       gettimeofday(&stm->last_activity, NULL); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Capture: Pass read frames to callback function */ | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_CAPTURE && stm->bufframes > 0 && | ||
|  |       (!stm->other_stream || stm->other_stream->bufframes < stm->other_stream->buffer_size)) { | ||
|  |     snd_pcm_sframes_t wrote = stm->bufframes; | ||
|  |     struct cubeb_stream * mainstm = stm->other_stream ? stm->other_stream : stm; | ||
|  |     void * other_buffer = stm->other_stream ? stm->other_stream->buffer + stm->other_stream->bufframes : NULL; | ||
|  | 
 | ||
|  |     /* Correct write size to the other stream available space */ | ||
|  |     if (stm->other_stream && wrote > (snd_pcm_sframes_t) (stm->other_stream->buffer_size - stm->other_stream->bufframes)) { | ||
|  |       wrote = stm->other_stream->buffer_size - stm->other_stream->bufframes; | ||
|  |     } | ||
|  | 
 | ||
|  |     pthread_mutex_unlock(&stm->mutex); | ||
|  |     wrote = stm->data_callback(mainstm, stm->user_ptr, stm->buffer, other_buffer, wrote); | ||
|  |     pthread_mutex_lock(&stm->mutex); | ||
|  | 
 | ||
|  |     if (wrote < 0) { | ||
|  |       avail = wrote; // the error handler below will recover us
 | ||
|  |     } else { | ||
|  |       stream_buffer_decrement(stm, wrote); | ||
|  | 
 | ||
|  |       if (stm->other_stream) { | ||
|  |         stm->other_stream->bufframes += wrote; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Playback: Don't have enough data? Let's ask for more. */ | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > (snd_pcm_sframes_t) stm->bufframes && | ||
|  |       (!stm->other_stream || stm->other_stream->bufframes > 0)) { | ||
|  |     long got = avail - stm->bufframes; | ||
|  |     void * other_buffer = stm->other_stream ? stm->other_stream->buffer : NULL; | ||
|  |     char * buftail = stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes); | ||
|  | 
 | ||
|  |     /* Correct read size to the other stream available frames */ | ||
|  |     if (stm->other_stream && got > (snd_pcm_sframes_t) stm->other_stream->bufframes) { | ||
|  |       got = stm->other_stream->bufframes; | ||
|  |     } | ||
|  | 
 | ||
|  |     pthread_mutex_unlock(&stm->mutex); | ||
|  |     got = stm->data_callback(stm, stm->user_ptr, other_buffer, buftail, got); | ||
|  |     pthread_mutex_lock(&stm->mutex); | ||
|  | 
 | ||
|  |     if (got < 0) { | ||
|  |       avail = got; // the error handler below will recover us
 | ||
|  |     } else { | ||
|  |       stm->bufframes += got; | ||
|  | 
 | ||
|  |       if (stm->other_stream) { | ||
|  |         stream_buffer_decrement(stm->other_stream, got); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Playback: Still don't have enough data? Add some silence. */ | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > (snd_pcm_sframes_t) stm->bufframes) { | ||
|  |     long drain_frames = avail - stm->bufframes; | ||
|  |     double drain_time = (double) drain_frames / stm->params.rate; | ||
|  | 
 | ||
|  |     char * buftail = stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes); | ||
|  |     memset(buftail, 0, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, drain_frames)); | ||
|  |     stm->bufframes = avail; | ||
|  | 
 | ||
|  |     /* Mark as draining, unless we're waiting for capture */ | ||
|  |     if (!stm->other_stream || stm->other_stream->bufframes > 0) { | ||
|  |       set_timeout(&stm->drain_timeout, drain_time * 1000); | ||
|  | 
 | ||
|  |       draining = 1; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Playback: Have enough data and no errors. Let's write it out. */ | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > 0) { | ||
|  |     snd_pcm_sframes_t wrote; | ||
|  | 
 | ||
|  |     if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) { | ||
|  |       float * b = (float *) stm->buffer; | ||
|  |       for (uint32_t i = 0; i < avail * stm->params.channels; i++) { | ||
|  |         b[i] *= stm->volume; | ||
|  |       } | ||
|  |     } else { | ||
|  |       short * b = (short *) stm->buffer; | ||
|  |       for (uint32_t i = 0; i < avail * stm->params.channels; i++) { | ||
|  |         b[i] *= stm->volume; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     wrote = WRAP(snd_pcm_writei)(stm->pcm, stm->buffer, avail); | ||
|  |     if (wrote < 0) { | ||
|  |       avail = wrote; // the error handler below will recover us
 | ||
|  |     } else { | ||
|  |       stream_buffer_decrement(stm, wrote); | ||
|  | 
 | ||
|  |       stm->stream_position += wrote; | ||
|  |       gettimeofday(&stm->last_activity, NULL); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Got some error? Let's try to recover the stream. */ | ||
|  |   if (avail < 0) { | ||
|  |     avail = WRAP(snd_pcm_recover)(stm->pcm, avail, 0); | ||
|  | 
 | ||
|  |     /* Capture pcm must be started after initial setup/recover */ | ||
|  |     if (avail >= 0 && | ||
|  |         stm->stream_type == SND_PCM_STREAM_CAPTURE && | ||
|  |         WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) { | ||
|  |       avail = WRAP(snd_pcm_start)(stm->pcm); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Failed to recover, this stream must be broken. */ | ||
|  |   if (avail < 0) { | ||
|  |     pthread_mutex_unlock(&stm->mutex); | ||
|  |     stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); | ||
|  |     return ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_unlock(&stm->mutex); | ||
|  |   return draining ? DRAINING : RUNNING; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_run(cubeb * ctx) | ||
|  | { | ||
|  |   int r; | ||
|  |   int timeout; | ||
|  |   int i; | ||
|  |   char dummy; | ||
|  |   cubeb_stream * stm; | ||
|  |   enum stream_state state; | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  | 
 | ||
|  |   if (ctx->rebuild) { | ||
|  |     rebuild(ctx); | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Wake up at least once per second for the watchdog. */ | ||
|  |   timeout = 1000; | ||
|  |   for (i = 0; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |     stm = ctx->streams[i]; | ||
|  |     if (stm && stm->state == DRAINING) { | ||
|  |       r = ms_until(&stm->drain_timeout); | ||
|  |       if (r >= 0 && timeout > r) { | ||
|  |         timeout = r; | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  |   r = poll(ctx->fds, ctx->nfds, timeout); | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  | 
 | ||
|  |   if (r > 0) { | ||
|  |     if (ctx->fds[0].revents & POLLIN) { | ||
|  |       if (read(ctx->control_fd_read, &dummy, 1) < 0) { | ||
|  |         /* ignore read error */ | ||
|  |       } | ||
|  | 
 | ||
|  |       if (ctx->shutdown) { | ||
|  |         pthread_mutex_unlock(&ctx->mutex); | ||
|  |         return -1; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     for (i = 0; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |       stm = ctx->streams[i]; | ||
|  |       /* We can't use snd_pcm_poll_descriptors_revents here because of
 | ||
|  |          https://github.com/kinetiknz/cubeb/issues/135. */
 | ||
|  |       if (stm && stm->state == RUNNING && stm->fds && any_revents(stm->fds, stm->nfds)) { | ||
|  |         alsa_set_stream_state(stm, PROCESSING); | ||
|  |         pthread_mutex_unlock(&ctx->mutex); | ||
|  |         state = alsa_process_stream(stm); | ||
|  |         pthread_mutex_lock(&ctx->mutex); | ||
|  |         alsa_set_stream_state(stm, state); | ||
|  |       } | ||
|  |     } | ||
|  |   } else if (r == 0) { | ||
|  |     for (i = 0; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |       stm = ctx->streams[i]; | ||
|  |       if (stm) { | ||
|  |         if (stm->state == DRAINING && ms_since(&stm->drain_timeout) >= 0) { | ||
|  |           alsa_set_stream_state(stm, INACTIVE); | ||
|  |           stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); | ||
|  |         } else if (stm->state == RUNNING && ms_since(&stm->last_activity) > CUBEB_WATCHDOG_MS) { | ||
|  |           alsa_set_stream_state(stm, ERROR); | ||
|  |           stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   return 0; | ||
|  | } | ||
|  | 
 | ||
|  | static void * | ||
|  | alsa_run_thread(void * context) | ||
|  | { | ||
|  |   cubeb * ctx = context; | ||
|  |   int r; | ||
|  | 
 | ||
|  |   do { | ||
|  |     r = alsa_run(ctx); | ||
|  |   } while (r >= 0); | ||
|  | 
 | ||
|  |   return NULL; | ||
|  | } | ||
|  | 
 | ||
|  | static snd_config_t * | ||
|  | get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm) | ||
|  | { | ||
|  |   int r; | ||
|  |   snd_config_t * slave_pcm; | ||
|  |   snd_config_t * slave_def; | ||
|  |   snd_config_t * pcm; | ||
|  |   char const * string; | ||
|  |   char node_name[64]; | ||
|  | 
 | ||
|  |   slave_def = NULL; | ||
|  | 
 | ||
|  |   r = WRAP(snd_config_search)(root_pcm, "slave", &slave_pcm); | ||
|  |   if (r < 0) { | ||
|  |     return NULL; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_config_get_string)(slave_pcm, &string); | ||
|  |   if (r >= 0) { | ||
|  |     r = WRAP(snd_config_search_definition)(lconf, "pcm_slave", string, &slave_def); | ||
|  |     if (r < 0) { | ||
|  |       return NULL; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   do { | ||
|  |     r = WRAP(snd_config_search)(slave_def ? slave_def : slave_pcm, "pcm", &pcm); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     r = WRAP(snd_config_get_string)(slave_def ? slave_def : slave_pcm, &string); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     r = snprintf(node_name, sizeof(node_name), "pcm.%s", string); | ||
|  |     if (r < 0 || r > (int) sizeof(node_name)) { | ||
|  |       break; | ||
|  |     } | ||
|  |     r = WRAP(snd_config_search)(lconf, node_name, &pcm); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     return pcm; | ||
|  |   } while (0); | ||
|  | 
 | ||
|  |   if (slave_def) { | ||
|  |     WRAP(snd_config_delete)(slave_def); | ||
|  |   } | ||
|  | 
 | ||
|  |   return NULL; | ||
|  | } | ||
|  | 
 | ||
|  | /* Work around PulseAudio ALSA plugin bug where the PA server forces a
 | ||
|  |    higher than requested latency, but the plugin does not update its (and | ||
|  |    ALSA's) internal state to reflect that, leading to an immediate underrun | ||
|  |    situation.  Inspired by WINE's make_handle_underrun_config. | ||
|  |    Reference: http://mailman.alsa-project.org/pipermail/alsa-devel/2012-July/05 */
 | ||
|  | static snd_config_t * | ||
|  | init_local_config_with_workaround(char const * pcm_name) | ||
|  | { | ||
|  |   int r; | ||
|  |   snd_config_t * lconf; | ||
|  |   snd_config_t * pcm_node; | ||
|  |   snd_config_t * node; | ||
|  |   char const * string; | ||
|  |   char node_name[64]; | ||
|  | 
 | ||
|  |   lconf = NULL; | ||
|  | 
 | ||
|  |   if (*WRAP(snd_config) == NULL) { | ||
|  |     return NULL; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_config_copy)(&lconf, *WRAP(snd_config)); | ||
|  |   if (r < 0) { | ||
|  |     return NULL; | ||
|  |   } | ||
|  | 
 | ||
|  |   do { | ||
|  |     r = WRAP(snd_config_search_definition)(lconf, "pcm", pcm_name, &pcm_node); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     r = WRAP(snd_config_get_id)(pcm_node, &string); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     r = snprintf(node_name, sizeof(node_name), "pcm.%s", string); | ||
|  |     if (r < 0 || r > (int) sizeof(node_name)) { | ||
|  |       break; | ||
|  |     } | ||
|  |     r = WRAP(snd_config_search)(lconf, node_name, &pcm_node); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     /* If this PCM has a slave, walk the slave configurations until we reach the bottom. */ | ||
|  |     while ((node = get_slave_pcm_node(lconf, pcm_node)) != NULL) { | ||
|  |       pcm_node = node; | ||
|  |     } | ||
|  | 
 | ||
|  |     /* Fetch the PCM node's type, and bail out if it's not the PulseAudio plugin. */ | ||
|  |     r = WRAP(snd_config_search)(pcm_node, "type", &node); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     r = WRAP(snd_config_get_string)(node, &string); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (strcmp(string, "pulse") != 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     /* Don't clobber an explicit existing handle_underrun value, set it only
 | ||
|  |        if it doesn't already exist. */ | ||
|  |     r = WRAP(snd_config_search)(pcm_node, "handle_underrun", &node); | ||
|  |     if (r != -ENOENT) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     /* Disable pcm_pulse's asynchronous underrun handling. */ | ||
|  |     r = WRAP(snd_config_imake_integer)(&node, "handle_underrun", 0); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     r = WRAP(snd_config_add)(pcm_node, node); | ||
|  |     if (r < 0) { | ||
|  |       break; | ||
|  |     } | ||
|  | 
 | ||
|  |     return lconf; | ||
|  |   } while (0); | ||
|  | 
 | ||
|  |   WRAP(snd_config_delete)(lconf); | ||
|  | 
 | ||
|  |   return NULL; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_locked_pcm_open(snd_pcm_t ** pcm, char const * pcm_name, snd_pcm_stream_t stream, snd_config_t * local_config) | ||
|  | { | ||
|  |   int r; | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&cubeb_alsa_mutex); | ||
|  |   if (local_config) { | ||
|  |     r = WRAP(snd_pcm_open_lconf)(pcm, pcm_name, stream, SND_PCM_NONBLOCK, local_config); | ||
|  |   } else { | ||
|  |     r = WRAP(snd_pcm_open)(pcm, pcm_name, stream, SND_PCM_NONBLOCK); | ||
|  |   } | ||
|  |   pthread_mutex_unlock(&cubeb_alsa_mutex); | ||
|  | 
 | ||
|  |   return r; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_locked_pcm_close(snd_pcm_t * pcm) | ||
|  | { | ||
|  |   int r; | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&cubeb_alsa_mutex); | ||
|  |   r = WRAP(snd_pcm_close)(pcm); | ||
|  |   pthread_mutex_unlock(&cubeb_alsa_mutex); | ||
|  | 
 | ||
|  |   return r; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_register_stream(cubeb * ctx, cubeb_stream * stm) | ||
|  | { | ||
|  |   int i; | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   for (i = 0; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |     if (!ctx->streams[i]) { | ||
|  |       ctx->streams[i] = stm; | ||
|  |       break; | ||
|  |     } | ||
|  |   } | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   return i == CUBEB_STREAM_MAX; | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | alsa_unregister_stream(cubeb_stream * stm) | ||
|  | { | ||
|  |   cubeb * ctx; | ||
|  |   int i; | ||
|  | 
 | ||
|  |   ctx = stm->context; | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   for (i = 0; i < CUBEB_STREAM_MAX; ++i) { | ||
|  |     if (ctx->streams[i] == stm) { | ||
|  |       ctx->streams[i] = NULL; | ||
|  |       break; | ||
|  |     } | ||
|  |   } | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | silent_error_handler(char const * file, int line, char const * function, | ||
|  |                      int err, char const * fmt, ...) | ||
|  | { | ||
|  |   (void)file; | ||
|  |   (void)line; | ||
|  |   (void)function; | ||
|  |   (void)err; | ||
|  |   (void)fmt; | ||
|  | } | ||
|  | 
 | ||
|  | /*static*/ int | ||
|  | alsa_init(cubeb ** context, char const * context_name) | ||
|  | { | ||
|  |   (void)context_name; | ||
|  |   void * libasound = NULL; | ||
|  |   cubeb * ctx; | ||
|  |   int r; | ||
|  |   int i; | ||
|  |   int fd[2]; | ||
|  |   pthread_attr_t attr; | ||
|  |   snd_pcm_t * dummy; | ||
|  | 
 | ||
|  |   assert(context); | ||
|  |   *context = NULL; | ||
|  | 
 | ||
|  | #ifndef DISABLE_LIBASOUND_DLOPEN
 | ||
|  |   libasound = dlopen("libasound.so.2", RTLD_LAZY); | ||
|  |   if (!libasound) { | ||
|  |     libasound = dlopen("libasound.so", RTLD_LAZY); | ||
|  |     if (!libasound) { | ||
|  |       return CUBEB_ERROR; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | #define LOAD(x) {                               \
 | ||
|  |     cubeb_##x = dlsym(libasound, #x);            \ | ||
|  |     if (!cubeb_##x) {                           \ | ||
|  |       dlclose(libasound);                        \ | ||
|  |       return CUBEB_ERROR;                       \ | ||
|  |     }                                           \ | ||
|  |   } | ||
|  | 
 | ||
|  |   LIBASOUND_API_VISIT(LOAD); | ||
|  | #undef LOAD
 | ||
|  | #endif
 | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&cubeb_alsa_mutex); | ||
|  |   if (!cubeb_alsa_error_handler_set) { | ||
|  |     WRAP(snd_lib_error_set_handler)(silent_error_handler); | ||
|  |     cubeb_alsa_error_handler_set = 1; | ||
|  |   } | ||
|  |   pthread_mutex_unlock(&cubeb_alsa_mutex); | ||
|  | 
 | ||
|  |   ctx = calloc(1, sizeof(*ctx)); | ||
|  |   assert(ctx); | ||
|  | 
 | ||
|  |   ctx->ops = &alsa_ops; | ||
|  |   ctx->libasound = libasound; | ||
|  | 
 | ||
|  |   r = pthread_mutex_init(&ctx->mutex, NULL); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   r = pipe(fd); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   for (i = 0; i < 2; ++i) { | ||
|  |     fcntl(fd[i], F_SETFD, fcntl(fd[i], F_GETFD) | FD_CLOEXEC); | ||
|  |     fcntl(fd[i], F_SETFL, fcntl(fd[i], F_GETFL) | O_NONBLOCK); | ||
|  |   } | ||
|  | 
 | ||
|  |   ctx->control_fd_read = fd[0]; | ||
|  |   ctx->control_fd_write = fd[1]; | ||
|  | 
 | ||
|  |   /* Force an early rebuild when alsa_run is first called to ensure fds and
 | ||
|  |      nfds have been initialized. */ | ||
|  |   ctx->rebuild = 1; | ||
|  | 
 | ||
|  |   r = pthread_attr_init(&attr); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   r = pthread_attr_setstacksize(&attr, 256 * 1024); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   r = pthread_create(&ctx->thread, &attr, alsa_run_thread, ctx); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   r = pthread_attr_destroy(&attr); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   /* Open a dummy PCM to force the configuration space to be evaluated so that
 | ||
|  |      init_local_config_with_workaround can find and modify the default node. */ | ||
|  |   r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, NULL); | ||
|  |   if (r >= 0) { | ||
|  |     alsa_locked_pcm_close(dummy); | ||
|  |   } | ||
|  |   ctx->is_pa = 0; | ||
|  |   pthread_mutex_lock(&cubeb_alsa_mutex); | ||
|  |   ctx->local_config = init_local_config_with_workaround(CUBEB_ALSA_PCM_NAME); | ||
|  |   pthread_mutex_unlock(&cubeb_alsa_mutex); | ||
|  |   if (ctx->local_config) { | ||
|  |     ctx->is_pa = 1; | ||
|  |     r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, ctx->local_config); | ||
|  |     /* If we got a local_config, we found a PA PCM.  If opening a PCM with that
 | ||
|  |        config fails with EINVAL, the PA PCM is too old for this workaround. */ | ||
|  |     if (r == -EINVAL) { | ||
|  |       pthread_mutex_lock(&cubeb_alsa_mutex); | ||
|  |       WRAP(snd_config_delete)(ctx->local_config); | ||
|  |       pthread_mutex_unlock(&cubeb_alsa_mutex); | ||
|  |       ctx->local_config = NULL; | ||
|  |     } else if (r >= 0) { | ||
|  |       alsa_locked_pcm_close(dummy); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   *context = ctx; | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static char const * | ||
|  | alsa_get_backend_id(cubeb * ctx) | ||
|  | { | ||
|  |   (void)ctx; | ||
|  |   return "alsa"; | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | alsa_destroy(cubeb * ctx) | ||
|  | { | ||
|  |   int r; | ||
|  | 
 | ||
|  |   assert(ctx); | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   ctx->shutdown = 1; | ||
|  |   poll_wake(ctx); | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   r = pthread_join(ctx->thread, NULL); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   close(ctx->control_fd_read); | ||
|  |   close(ctx->control_fd_write); | ||
|  |   pthread_mutex_destroy(&ctx->mutex); | ||
|  |   free(ctx->fds); | ||
|  | 
 | ||
|  |   if (ctx->local_config) { | ||
|  |     pthread_mutex_lock(&cubeb_alsa_mutex); | ||
|  |     WRAP(snd_config_delete)(ctx->local_config); | ||
|  |     pthread_mutex_unlock(&cubeb_alsa_mutex); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (ctx->libasound) { | ||
|  |     dlclose(ctx->libasound); | ||
|  |   } | ||
|  | 
 | ||
|  |   free(ctx); | ||
|  | } | ||
|  | 
 | ||
|  | static void alsa_stream_destroy(cubeb_stream * stm); | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, | ||
|  |                         snd_pcm_stream_t stream_type, | ||
|  |                         cubeb_devid deviceid, | ||
|  |                         cubeb_stream_params * stream_params, | ||
|  |                         unsigned int latency_frames, | ||
|  |                         cubeb_data_callback data_callback, | ||
|  |                         cubeb_state_callback state_callback, | ||
|  |                         void * user_ptr) | ||
|  | { | ||
|  |   (void)stream_name; | ||
|  |   cubeb_stream * stm; | ||
|  |   int r; | ||
|  |   snd_pcm_format_t format; | ||
|  |   snd_pcm_uframes_t period_size; | ||
|  |   int latency_us = 0; | ||
|  |   char const * pcm_name = deviceid ? (char const *) deviceid : CUBEB_ALSA_PCM_NAME; | ||
|  | 
 | ||
|  |   assert(ctx && stream); | ||
|  | 
 | ||
|  |   *stream = NULL; | ||
|  | 
 | ||
|  |   if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) { | ||
|  |     return CUBEB_ERROR_NOT_SUPPORTED; | ||
|  |   } | ||
|  | 
 | ||
|  |   switch (stream_params->format) { | ||
|  |   case CUBEB_SAMPLE_S16LE: | ||
|  |     format = SND_PCM_FORMAT_S16_LE; | ||
|  |     break; | ||
|  |   case CUBEB_SAMPLE_S16BE: | ||
|  |     format = SND_PCM_FORMAT_S16_BE; | ||
|  |     break; | ||
|  |   case CUBEB_SAMPLE_FLOAT32LE: | ||
|  |     format = SND_PCM_FORMAT_FLOAT_LE; | ||
|  |     break; | ||
|  |   case CUBEB_SAMPLE_FLOAT32BE: | ||
|  |     format = SND_PCM_FORMAT_FLOAT_BE; | ||
|  |     break; | ||
|  |   default: | ||
|  |     return CUBEB_ERROR_INVALID_FORMAT; | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   if (ctx->active_streams >= CUBEB_STREAM_MAX) { | ||
|  |     pthread_mutex_unlock(&ctx->mutex); | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  |   ctx->active_streams += 1; | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   stm = calloc(1, sizeof(*stm)); | ||
|  |   assert(stm); | ||
|  | 
 | ||
|  |   stm->context = ctx; | ||
|  |   stm->data_callback = data_callback; | ||
|  |   stm->state_callback = state_callback; | ||
|  |   stm->user_ptr = user_ptr; | ||
|  |   stm->params = *stream_params; | ||
|  |   stm->state = INACTIVE; | ||
|  |   stm->volume = 1.0; | ||
|  |   stm->buffer = NULL; | ||
|  |   stm->bufframes = 0; | ||
|  |   stm->stream_type = stream_type; | ||
|  |   stm->other_stream = NULL; | ||
|  | 
 | ||
|  |   r = pthread_mutex_init(&stm->mutex, NULL); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   r = pthread_cond_init(&stm->cond, NULL); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   r = alsa_locked_pcm_open(&stm->pcm, pcm_name, stm->stream_type, ctx->local_config); | ||
|  |   if (r < 0) { | ||
|  |     alsa_stream_destroy(stm); | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_nonblock)(stm->pcm, 1); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   latency_us = latency_frames * 1e6 / stm->params.rate; | ||
|  | 
 | ||
|  |   /* Ugly hack: the PA ALSA plugin allows buffer configurations that can't
 | ||
|  |      possibly work.  See https://bugzilla.mozilla.org/show_bug.cgi?id=761274.
 | ||
|  |      Only resort to this hack if the handle_underrun workaround failed. */ | ||
|  |   if (!ctx->local_config && ctx->is_pa) { | ||
|  |     const int min_latency = 5e5; | ||
|  |     latency_us = latency_us < min_latency ? min_latency: latency_us; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_set_params)(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED, | ||
|  |                          stm->params.channels, stm->params.rate, 1, | ||
|  |                          latency_us); | ||
|  |   if (r < 0) { | ||
|  |     alsa_stream_destroy(stm); | ||
|  |     return CUBEB_ERROR_INVALID_FORMAT; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_get_params)(stm->pcm, &stm->buffer_size, &period_size); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   /* Double internal buffer size to have enough space when waiting for the other side of duplex connection */ | ||
|  |   stm->buffer_size *= 2; | ||
|  |   stm->buffer = calloc(1, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->buffer_size)); | ||
|  |   assert(stm->buffer); | ||
|  | 
 | ||
|  |   stm->nfds = WRAP(snd_pcm_poll_descriptors_count)(stm->pcm); | ||
|  |   assert(stm->nfds > 0); | ||
|  | 
 | ||
|  |   stm->saved_fds = calloc(stm->nfds, sizeof(struct pollfd)); | ||
|  |   assert(stm->saved_fds); | ||
|  |   r = WRAP(snd_pcm_poll_descriptors)(stm->pcm, stm->saved_fds, stm->nfds); | ||
|  |   assert((nfds_t) r == stm->nfds); | ||
|  | 
 | ||
|  |   if (alsa_register_stream(ctx, stm) != 0) { | ||
|  |     alsa_stream_destroy(stm); | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   *stream = stm; | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name, | ||
|  |                  cubeb_devid input_device, | ||
|  |                  cubeb_stream_params * input_stream_params, | ||
|  |                  cubeb_devid output_device, | ||
|  |                  cubeb_stream_params * output_stream_params, | ||
|  |                  unsigned int latency_frames, | ||
|  |                  cubeb_data_callback data_callback, cubeb_state_callback state_callback, | ||
|  |                  void * user_ptr) | ||
|  | { | ||
|  |   int result = CUBEB_OK; | ||
|  |   cubeb_stream * instm = NULL, * outstm = NULL; | ||
|  | 
 | ||
|  |   if (result == CUBEB_OK && input_stream_params) { | ||
|  |     result = alsa_stream_init_single(ctx, &instm, stream_name, SND_PCM_STREAM_CAPTURE, | ||
|  |                                      input_device, input_stream_params, latency_frames, | ||
|  |                                      data_callback, state_callback, user_ptr); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result == CUBEB_OK && output_stream_params) { | ||
|  |     result = alsa_stream_init_single(ctx, &outstm, stream_name, SND_PCM_STREAM_PLAYBACK, | ||
|  |                                      output_device, output_stream_params, latency_frames, | ||
|  |                                      data_callback, state_callback, user_ptr); | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result == CUBEB_OK && input_stream_params && output_stream_params) { | ||
|  |     instm->other_stream = outstm; | ||
|  |     outstm->other_stream = instm; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (result != CUBEB_OK && instm) { | ||
|  |     alsa_stream_destroy(instm); | ||
|  |   } | ||
|  | 
 | ||
|  |   *stream = outstm ? outstm : instm; | ||
|  | 
 | ||
|  |   return result; | ||
|  | } | ||
|  | 
 | ||
|  | static void | ||
|  | alsa_stream_destroy(cubeb_stream * stm) | ||
|  | { | ||
|  |   int r; | ||
|  |   cubeb * ctx; | ||
|  | 
 | ||
|  |   assert(stm && (stm->state == INACTIVE || | ||
|  |                  stm->state == ERROR || | ||
|  |                  stm->state == DRAINING)); | ||
|  | 
 | ||
|  |   ctx = stm->context; | ||
|  | 
 | ||
|  |   if (stm->other_stream) { | ||
|  |     stm->other_stream->other_stream = NULL; // to stop infinite recursion
 | ||
|  |     alsa_stream_destroy(stm->other_stream); | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&stm->mutex); | ||
|  |   if (stm->pcm) { | ||
|  |     if (stm->state == DRAINING) { | ||
|  |       WRAP(snd_pcm_drain)(stm->pcm); | ||
|  |     } | ||
|  |     alsa_locked_pcm_close(stm->pcm); | ||
|  |     stm->pcm = NULL; | ||
|  |   } | ||
|  |   free(stm->saved_fds); | ||
|  |   pthread_mutex_unlock(&stm->mutex); | ||
|  |   pthread_mutex_destroy(&stm->mutex); | ||
|  | 
 | ||
|  |   r = pthread_cond_destroy(&stm->cond); | ||
|  |   assert(r == 0); | ||
|  | 
 | ||
|  |   alsa_unregister_stream(stm); | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   assert(ctx->active_streams >= 1); | ||
|  |   ctx->active_streams -= 1; | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   free(stm->buffer); | ||
|  | 
 | ||
|  |   free(stm); | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) | ||
|  | { | ||
|  |   int r; | ||
|  |   cubeb_stream * stm; | ||
|  |   snd_pcm_hw_params_t* hw_params; | ||
|  |   cubeb_stream_params params; | ||
|  |   params.rate = 44100; | ||
|  |   params.format = CUBEB_SAMPLE_FLOAT32NE; | ||
|  |   params.channels = 2; | ||
|  | 
 | ||
|  |   snd_pcm_hw_params_alloca(&hw_params); | ||
|  | 
 | ||
|  |   assert(ctx); | ||
|  | 
 | ||
|  |   r = alsa_stream_init(ctx, &stm, "", NULL, NULL, NULL, ¶ms, 100, NULL, NULL, NULL); | ||
|  |   if (r != CUBEB_OK) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   assert(stm); | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_hw_params_any)(stm->pcm, hw_params); | ||
|  |   if (r < 0) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, max_channels); | ||
|  |   if (r < 0) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   alsa_stream_destroy(stm); | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) { | ||
|  |   (void)ctx; | ||
|  |   int r, dir; | ||
|  |   snd_pcm_t * pcm; | ||
|  |   snd_pcm_hw_params_t * hw_params; | ||
|  | 
 | ||
|  |   snd_pcm_hw_params_alloca(&hw_params); | ||
|  | 
 | ||
|  |   /* get a pcm, disabling resampling, so we get a rate the
 | ||
|  |    * hardware/dmix/pulse/etc. supports. */ | ||
|  |   r = WRAP(snd_pcm_open)(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK, SND_PCM_NO_AUTO_RESAMPLE); | ||
|  |   if (r < 0) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_hw_params_any)(pcm, hw_params); | ||
|  |   if (r < 0) { | ||
|  |     WRAP(snd_pcm_close)(pcm); | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, rate, &dir); | ||
|  |   if (r >= 0) { | ||
|  |     /* There is a default rate: use it. */ | ||
|  |     WRAP(snd_pcm_close)(pcm); | ||
|  |     return CUBEB_OK; | ||
|  |   } | ||
|  | 
 | ||
|  |   /* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */ | ||
|  |   *rate = 44100; | ||
|  | 
 | ||
|  |   r = WRAP(snd_pcm_hw_params_set_rate_near)(pcm, hw_params, rate, NULL); | ||
|  |   if (r < 0) { | ||
|  |     WRAP(snd_pcm_close)(pcm); | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   WRAP(snd_pcm_close)(pcm); | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_frames) | ||
|  | { | ||
|  |   (void)ctx; | ||
|  |   /* 40ms is found to be an acceptable minimum, even on a super low-end
 | ||
|  |    * machine. */ | ||
|  |   *latency_frames = 40 * params.rate / 1000; | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_start(cubeb_stream * stm) | ||
|  | { | ||
|  |   cubeb * ctx; | ||
|  | 
 | ||
|  |   assert(stm); | ||
|  |   ctx = stm->context; | ||
|  | 
 | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) { | ||
|  |     int r = alsa_stream_start(stm->other_stream); | ||
|  |     if (r != CUBEB_OK) | ||
|  |       return r; | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&stm->mutex); | ||
|  |   /* Capture pcm must be started after initial setup/recover */ | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_CAPTURE && | ||
|  |       WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) { | ||
|  |     WRAP(snd_pcm_start)(stm->pcm); | ||
|  |   } | ||
|  |   WRAP(snd_pcm_pause)(stm->pcm, 0); | ||
|  |   gettimeofday(&stm->last_activity, NULL); | ||
|  |   pthread_mutex_unlock(&stm->mutex); | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   if (stm->state != INACTIVE) { | ||
|  |     pthread_mutex_unlock(&ctx->mutex); | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  |   alsa_set_stream_state(stm, RUNNING); | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_stop(cubeb_stream * stm) | ||
|  | { | ||
|  |   cubeb * ctx; | ||
|  |   int r; | ||
|  | 
 | ||
|  |   assert(stm); | ||
|  |   ctx = stm->context; | ||
|  | 
 | ||
|  |   if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) { | ||
|  |     int r = alsa_stream_stop(stm->other_stream); | ||
|  |     if (r != CUBEB_OK) | ||
|  |       return r; | ||
|  |   } | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&ctx->mutex); | ||
|  |   while (stm->state == PROCESSING) { | ||
|  |     r = pthread_cond_wait(&stm->cond, &ctx->mutex); | ||
|  |     assert(r == 0); | ||
|  |   } | ||
|  | 
 | ||
|  |   alsa_set_stream_state(stm, INACTIVE); | ||
|  |   pthread_mutex_unlock(&ctx->mutex); | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&stm->mutex); | ||
|  |   WRAP(snd_pcm_pause)(stm->pcm, 1); | ||
|  |   pthread_mutex_unlock(&stm->mutex); | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_get_position(cubeb_stream * stm, uint64_t * position) | ||
|  | { | ||
|  |   snd_pcm_sframes_t delay; | ||
|  | 
 | ||
|  |   assert(stm && position); | ||
|  | 
 | ||
|  |   pthread_mutex_lock(&stm->mutex); | ||
|  | 
 | ||
|  |   delay = -1; | ||
|  |   if (WRAP(snd_pcm_state)(stm->pcm) != SND_PCM_STATE_RUNNING || | ||
|  |       WRAP(snd_pcm_delay)(stm->pcm, &delay) != 0) { | ||
|  |     *position = stm->last_position; | ||
|  |     pthread_mutex_unlock(&stm->mutex); | ||
|  |     return CUBEB_OK; | ||
|  |   } | ||
|  | 
 | ||
|  |   assert(delay >= 0); | ||
|  | 
 | ||
|  |   *position = 0; | ||
|  |   if (stm->stream_position >= (snd_pcm_uframes_t) delay) { | ||
|  |     *position = stm->stream_position - delay; | ||
|  |   } | ||
|  | 
 | ||
|  |   stm->last_position = *position; | ||
|  | 
 | ||
|  |   pthread_mutex_unlock(&stm->mutex); | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_get_latency(cubeb_stream * stm, uint32_t * latency) | ||
|  | { | ||
|  |   snd_pcm_sframes_t delay; | ||
|  |   /* This function returns the delay in frames until a frame written using
 | ||
|  |      snd_pcm_writei is sent to the DAC. The DAC delay should be < 1ms anyways. */ | ||
|  |   if (WRAP(snd_pcm_delay)(stm->pcm, &delay)) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   *latency = delay; | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_stream_set_volume(cubeb_stream * stm, float volume) | ||
|  | { | ||
|  |   /* setting the volume using an API call does not seem very stable/supported */ | ||
|  |   pthread_mutex_lock(&stm->mutex); | ||
|  |   stm->volume = volume; | ||
|  |   pthread_mutex_unlock(&stm->mutex); | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_enumerate_devices(cubeb * context, cubeb_device_type type, | ||
|  |                        cubeb_device_collection * collection) | ||
|  | { | ||
|  |   cubeb_device_info* device = NULL; | ||
|  | 
 | ||
|  |   if (!context) | ||
|  |     return CUBEB_ERROR; | ||
|  | 
 | ||
|  |   uint32_t rate, max_channels; | ||
|  |   int r; | ||
|  | 
 | ||
|  |   r = alsa_get_preferred_sample_rate(context, &rate); | ||
|  |   if (r != CUBEB_OK) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   r = alsa_get_max_channel_count(context, &max_channels); | ||
|  |   if (r != CUBEB_OK) { | ||
|  |     return CUBEB_ERROR; | ||
|  |   } | ||
|  | 
 | ||
|  |   char const * a_name = "default"; | ||
|  |   device = (cubeb_device_info *) calloc(1, sizeof(cubeb_device_info)); | ||
|  |   assert(device); | ||
|  |   if (!device) | ||
|  |     return CUBEB_ERROR; | ||
|  | 
 | ||
|  |   device->device_id = a_name; | ||
|  |   device->devid = (cubeb_devid) device->device_id; | ||
|  |   device->friendly_name = a_name; | ||
|  |   device->group_id = a_name; | ||
|  |   device->vendor_name = a_name; | ||
|  |   device->type = type; | ||
|  |   device->state = CUBEB_DEVICE_STATE_ENABLED; | ||
|  |   device->preferred = CUBEB_DEVICE_PREF_ALL; | ||
|  |   device->format = CUBEB_DEVICE_FMT_S16NE; | ||
|  |   device->default_format = CUBEB_DEVICE_FMT_S16NE; | ||
|  |   device->max_channels = max_channels; | ||
|  |   device->min_rate = rate; | ||
|  |   device->max_rate = rate; | ||
|  |   device->default_rate = rate; | ||
|  |   device->latency_lo = 0; | ||
|  |   device->latency_hi = 0; | ||
|  | 
 | ||
|  |   collection->device = device; | ||
|  |   collection->count = 1; | ||
|  | 
 | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static int | ||
|  | alsa_device_collection_destroy(cubeb * context, | ||
|  |                                cubeb_device_collection * collection) | ||
|  | { | ||
|  |   assert(collection->count == 1); | ||
|  |   (void) context; | ||
|  |   free(collection->device); | ||
|  |   return CUBEB_OK; | ||
|  | } | ||
|  | 
 | ||
|  | static struct cubeb_ops const alsa_ops = { | ||
|  |   .init = alsa_init, | ||
|  |   .get_backend_id = alsa_get_backend_id, | ||
|  |   .get_max_channel_count = alsa_get_max_channel_count, | ||
|  |   .get_min_latency = alsa_get_min_latency, | ||
|  |   .get_preferred_sample_rate = alsa_get_preferred_sample_rate, | ||
|  |   .enumerate_devices = alsa_enumerate_devices, | ||
|  |   .device_collection_destroy = alsa_device_collection_destroy, | ||
|  |   .destroy = alsa_destroy, | ||
|  |   .stream_init = alsa_stream_init, | ||
|  |   .stream_destroy = alsa_stream_destroy, | ||
|  |   .stream_start = alsa_stream_start, | ||
|  |   .stream_stop = alsa_stream_stop, | ||
|  |   .stream_reset_default_device = NULL, | ||
|  |   .stream_get_position = alsa_stream_get_position, | ||
|  |   .stream_get_latency = alsa_stream_get_latency, | ||
|  |   .stream_set_volume = alsa_stream_set_volume, | ||
|  |   .stream_get_current_device = NULL, | ||
|  |   .stream_device_destroy = NULL, | ||
|  |   .stream_register_device_changed_callback = NULL, | ||
|  |   .register_device_collection_changed = NULL | ||
|  | }; |