| | |
| | | /* |
| | | Simple DirectMedia Layer |
| | | Copyright (C) 1997-2016 Sam Lantinga <slouken@libsdl.org> |
| | | Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org> |
| | | |
| | | This software is provided 'as-is', without any express or implied |
| | | warranty. In no event will the authors be held liable for any damages |
| | |
| | | |
| | | #if SDL_AUDIO_DRIVER_ALSA |
| | | |
| | | #ifndef SDL_ALSA_NON_BLOCKING |
| | | #define SDL_ALSA_NON_BLOCKING 0 |
| | | #endif |
| | | |
| | | /* Allow access to a raw mixing buffer */ |
| | | |
| | | #include <sys/types.h> |
| | | #include <signal.h> /* For kill() */ |
| | | #include <errno.h> |
| | | #include <string.h> |
| | | |
| | | #include "SDL_assert.h" |
| | |
| | | static int (*ALSA_snd_device_name_hint) (int, const char *, void ***); |
| | | static char* (*ALSA_snd_device_name_get_hint) (const void *, const char *); |
| | | static int (*ALSA_snd_device_name_free_hint) (void **); |
| | | static snd_pcm_sframes_t (*ALSA_snd_pcm_avail)(snd_pcm_t *); |
| | | #ifdef SND_CHMAP_API_VERSION |
| | | static snd_pcm_chmap_t* (*ALSA_snd_pcm_get_chmap) (snd_pcm_t *); |
| | | static int (*ALSA_snd_pcm_chmap_print) (const snd_pcm_chmap_t *map, size_t maxlen, char *buf); |
| | | #endif |
| | | |
| | | #ifdef SDL_AUDIO_DRIVER_ALSA_DYNAMIC |
| | | #define snd_pcm_hw_params_sizeof ALSA_snd_pcm_hw_params_sizeof |
| | |
| | | SDL_ALSA_SYM(snd_device_name_hint); |
| | | SDL_ALSA_SYM(snd_device_name_get_hint); |
| | | SDL_ALSA_SYM(snd_device_name_free_hint); |
| | | SDL_ALSA_SYM(snd_pcm_avail); |
| | | #ifdef SND_CHMAP_API_VERSION |
| | | SDL_ALSA_SYM(snd_pcm_get_chmap); |
| | | SDL_ALSA_SYM(snd_pcm_chmap_print); |
| | | #endif |
| | | |
| | | return 0; |
| | | } |
| | |
| | | static void |
| | | ALSA_WaitDevice(_THIS) |
| | | { |
| | | /* We're in blocking mode, so there's nothing to do here */ |
| | | #if SDL_ALSA_NON_BLOCKING |
| | | const snd_pcm_sframes_t needed = (snd_pcm_sframes_t) this->spec.samples; |
| | | while (SDL_AtomicGet(&this->enabled)) { |
| | | const snd_pcm_sframes_t rc = ALSA_snd_pcm_avail(this->hidden->pcm_handle); |
| | | if ((rc < 0) && (rc != -EAGAIN)) { |
| | | /* Hmm, not much we can do - abort */ |
| | | fprintf(stderr, "ALSA snd_pcm_avail failed (unrecoverable): %s\n", |
| | | ALSA_snd_strerror(rc)); |
| | | SDL_OpenedAudioDeviceDisconnected(this); |
| | | return; |
| | | } else if (rc < needed) { |
| | | const Uint32 delay = ((needed - (SDL_max(rc, 0))) * 1000) / this->spec.freq; |
| | | SDL_Delay(SDL_max(delay, 10)); |
| | | } else { |
| | | break; /* ready to go! */ |
| | | } |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | |
| | |
| | | tmp = ptr[3]; ptr[3] = ptr[5]; ptr[5] = tmp; \ |
| | | } |
| | | |
| | | static SDL_INLINE void |
| | | static void |
| | | swizzle_alsa_channels_6_64bit(void *buffer, Uint32 bufferlen) |
| | | { |
| | | SWIZ6(Uint64, buffer, bufferlen); |
| | | } |
| | | |
| | | static SDL_INLINE void |
| | | static void |
| | | swizzle_alsa_channels_6_32bit(void *buffer, Uint32 bufferlen) |
| | | { |
| | | SWIZ6(Uint32, buffer, bufferlen); |
| | | } |
| | | |
| | | static SDL_INLINE void |
| | | static void |
| | | swizzle_alsa_channels_6_16bit(void *buffer, Uint32 bufferlen) |
| | | { |
| | | SWIZ6(Uint16, buffer, bufferlen); |
| | | } |
| | | |
| | | static SDL_INLINE void |
| | | static void |
| | | swizzle_alsa_channels_6_8bit(void *buffer, Uint32 bufferlen) |
| | | { |
| | | SWIZ6(Uint8, buffer, bufferlen); |
| | |
| | | * Called right before feeding this->hidden->mixbuf to the hardware. Swizzle |
| | | * channels from Windows/Mac order to the format alsalib will want. |
| | | */ |
| | | static SDL_INLINE void |
| | | static void |
| | | swizzle_alsa_channels(_THIS, void *buffer, Uint32 bufferlen) |
| | | { |
| | | if (this->spec.channels == 6) { |
| | |
| | | /* !!! FIXME: update this for 7.1 if needed, later. */ |
| | | } |
| | | |
| | | #ifdef SND_CHMAP_API_VERSION |
| | | /* Some devices have the right channel map, no swizzling necessary */ |
| | | static void |
| | | no_swizzle(_THIS, void *buffer, Uint32 bufferlen) |
| | | { |
| | | return; |
| | | } |
| | | #endif /* SND_CHMAP_API_VERSION */ |
| | | |
| | | |
| | | static void |
| | | ALSA_PlayDevice(_THIS) |
| | |
| | | this->spec.channels; |
| | | snd_pcm_uframes_t frames_left = ((snd_pcm_uframes_t) this->spec.samples); |
| | | |
| | | swizzle_alsa_channels(this, this->hidden->mixbuf, frames_left); |
| | | this->hidden->swizzle_func(this, this->hidden->mixbuf, frames_left); |
| | | |
| | | while ( frames_left > 0 && SDL_AtomicGet(&this->enabled) ) { |
| | | int status; |
| | | |
| | | /* This wait is a work-around for a hang when USB devices are |
| | | unplugged. Normally it should not result in any waiting, |
| | | but in the case of a USB unplug, it serves as a way to |
| | | join the playback thread after the timeout occurs */ |
| | | status = ALSA_snd_pcm_wait(this->hidden->pcm_handle, 1000); |
| | | if (status == 0) { |
| | | /*fprintf(stderr, "ALSA timeout waiting for available buffer space\n");*/ |
| | | SDL_OpenedAudioDeviceDisconnected(this); |
| | | return; |
| | | } |
| | | |
| | | status = ALSA_snd_pcm_writei(this->hidden->pcm_handle, |
| | | int status = ALSA_snd_pcm_writei(this->hidden->pcm_handle, |
| | | sample_buf, frames_left); |
| | | |
| | | if (status < 0) { |
| | |
| | | } |
| | | continue; |
| | | } |
| | | else if (status == 0) { |
| | | /* No frames were written (no available space in pcm device). |
| | | Allow other threads to catch up. */ |
| | | Uint32 delay = (frames_left / 2 * 1000) / this->spec.freq; |
| | | SDL_Delay(delay); |
| | | } |
| | | |
| | | sample_buf += status * frame_size; |
| | | frames_left -= status; |
| | | } |
| | |
| | | this->spec.channels; |
| | | const int total_frames = buflen / frame_size; |
| | | snd_pcm_uframes_t frames_left = total_frames; |
| | | snd_pcm_uframes_t wait_time = frame_size / 2; |
| | | |
| | | SDL_assert((buflen % frame_size) == 0); |
| | | |
| | | while ( frames_left > 0 && SDL_AtomicGet(&this->enabled) ) { |
| | | /* !!! FIXME: This works, but needs more testing before going live */ |
| | | /* ALSA_snd_pcm_wait(this->hidden->pcm_handle, -1); */ |
| | | int status = ALSA_snd_pcm_readi(this->hidden->pcm_handle, |
| | | int status; |
| | | |
| | | status = ALSA_snd_pcm_readi(this->hidden->pcm_handle, |
| | | sample_buf, frames_left); |
| | | |
| | | if (status < 0) { |
| | | if (status == -EAGAIN) { |
| | | ALSA_snd_pcm_wait(this->hidden->pcm_handle, wait_time); |
| | | status = 0; |
| | | } |
| | | else if (status < 0) { |
| | | /*printf("ALSA: capture error %d\n", status);*/ |
| | | if (status == -EAGAIN) { |
| | | /* Apparently snd_pcm_recover() doesn't handle this case - |
| | | does it assume snd_pcm_wait() above? */ |
| | | SDL_Delay(1); |
| | | continue; |
| | | } |
| | | status = ALSA_snd_pcm_recover(this->hidden->pcm_handle, status, 0); |
| | | if (status < 0) { |
| | | /* Hmm, not much we can do - abort */ |
| | |
| | | frames_left -= status; |
| | | } |
| | | |
| | | swizzle_alsa_channels(this, buffer, total_frames - frames_left); |
| | | this->hidden->swizzle_func(this, buffer, total_frames - frames_left); |
| | | |
| | | return (total_frames - frames_left) * frame_size; |
| | | } |
| | |
| | | ALSA_CloseDevice(_THIS) |
| | | { |
| | | if (this->hidden->pcm_handle) { |
| | | /* Wait for the submitted audio to drain |
| | | /* Wait for the submitted audio to drain |
| | | ALSA_snd_pcm_drop() can hang, so don't use that. |
| | | */ |
| | | Uint32 delay = ((this->spec.samples * 1000) / this->spec.freq) * 2; |
| | |
| | | } |
| | | |
| | | static int |
| | | ALSA_finalize_hardware(_THIS, snd_pcm_hw_params_t *hwparams, int override) |
| | | ALSA_set_buffer_size(_THIS, snd_pcm_hw_params_t *params) |
| | | { |
| | | int status; |
| | | snd_pcm_hw_params_t *hwparams; |
| | | snd_pcm_uframes_t bufsize; |
| | | snd_pcm_uframes_t persize; |
| | | |
| | | /* Copy the hardware parameters for this setup */ |
| | | snd_pcm_hw_params_alloca(&hwparams); |
| | | ALSA_snd_pcm_hw_params_copy(hwparams, params); |
| | | |
| | | /* Prioritize matching the period size to the requested buffer size */ |
| | | persize = this->spec.samples; |
| | | status = ALSA_snd_pcm_hw_params_set_period_size_near( |
| | | this->hidden->pcm_handle, hwparams, &persize, NULL); |
| | | if ( status < 0 ) { |
| | | return(-1); |
| | | } |
| | | |
| | | /* Next try to restrict the parameters to having only two periods */ |
| | | bufsize = this->spec.samples * 2; |
| | | status = ALSA_snd_pcm_hw_params_set_buffer_size_near( |
| | | this->hidden->pcm_handle, hwparams, &bufsize); |
| | | if ( status < 0 ) { |
| | | return(-1); |
| | | } |
| | | |
| | | /* "set" the hardware with the desired parameters */ |
| | | status = ALSA_snd_pcm_hw_params(this->hidden->pcm_handle, hwparams); |
| | |
| | | return(-1); |
| | | } |
| | | |
| | | /* Get samples for the actual buffer size */ |
| | | status = ALSA_snd_pcm_hw_params_get_buffer_size(hwparams, &bufsize); |
| | | if ( status < 0 ) { |
| | | return(-1); |
| | | } |
| | | if ( !override && bufsize != this->spec.samples * 2 ) { |
| | | return(-1); |
| | | } |
| | | |
| | | /* !!! FIXME: Is this safe to do? */ |
| | | this->spec.samples = bufsize / 2; |
| | | this->spec.samples = persize; |
| | | |
| | | /* This is useful for debugging */ |
| | | if ( SDL_getenv("SDL_AUDIO_ALSA_DEBUG") ) { |
| | | snd_pcm_uframes_t persize = 0; |
| | | unsigned int periods = 0; |
| | | |
| | | ALSA_snd_pcm_hw_params_get_period_size(hwparams, &persize, NULL); |
| | | ALSA_snd_pcm_hw_params_get_periods(hwparams, &periods, NULL); |
| | | |
| | | fprintf(stderr, |
| | |
| | | } |
| | | |
| | | return(0); |
| | | } |
| | | |
| | | static int |
| | | ALSA_set_period_size(_THIS, snd_pcm_hw_params_t *params, int override) |
| | | { |
| | | const char *env; |
| | | int status; |
| | | snd_pcm_hw_params_t *hwparams; |
| | | snd_pcm_uframes_t frames; |
| | | unsigned int periods; |
| | | |
| | | /* Copy the hardware parameters for this setup */ |
| | | snd_pcm_hw_params_alloca(&hwparams); |
| | | ALSA_snd_pcm_hw_params_copy(hwparams, params); |
| | | |
| | | if ( !override ) { |
| | | env = SDL_getenv("SDL_AUDIO_ALSA_SET_PERIOD_SIZE"); |
| | | if ( env ) { |
| | | override = SDL_atoi(env); |
| | | if ( override == 0 ) { |
| | | return(-1); |
| | | } |
| | | } |
| | | } |
| | | |
| | | frames = this->spec.samples; |
| | | status = ALSA_snd_pcm_hw_params_set_period_size_near( |
| | | this->hidden->pcm_handle, hwparams, &frames, NULL); |
| | | if ( status < 0 ) { |
| | | return(-1); |
| | | } |
| | | |
| | | periods = 2; |
| | | status = ALSA_snd_pcm_hw_params_set_periods_near( |
| | | this->hidden->pcm_handle, hwparams, &periods, NULL); |
| | | if ( status < 0 ) { |
| | | return(-1); |
| | | } |
| | | |
| | | return ALSA_finalize_hardware(this, hwparams, override); |
| | | } |
| | | |
| | | static int |
| | | ALSA_set_buffer_size(_THIS, snd_pcm_hw_params_t *params, int override) |
| | | { |
| | | const char *env; |
| | | int status; |
| | | snd_pcm_hw_params_t *hwparams; |
| | | snd_pcm_uframes_t frames; |
| | | |
| | | /* Copy the hardware parameters for this setup */ |
| | | snd_pcm_hw_params_alloca(&hwparams); |
| | | ALSA_snd_pcm_hw_params_copy(hwparams, params); |
| | | |
| | | if ( !override ) { |
| | | env = SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"); |
| | | if ( env ) { |
| | | override = SDL_atoi(env); |
| | | if ( override == 0 ) { |
| | | return(-1); |
| | | } |
| | | } |
| | | } |
| | | |
| | | frames = this->spec.samples * 2; |
| | | status = ALSA_snd_pcm_hw_params_set_buffer_size_near( |
| | | this->hidden->pcm_handle, hwparams, &frames); |
| | | if ( status < 0 ) { |
| | | return(-1); |
| | | } |
| | | |
| | | return ALSA_finalize_hardware(this, hwparams, override); |
| | | } |
| | | |
| | | static int |
| | |
| | | SDL_AudioFormat test_format = 0; |
| | | unsigned int rate = 0; |
| | | unsigned int channels = 0; |
| | | #ifdef SND_CHMAP_API_VERSION |
| | | snd_pcm_chmap_t *chmap; |
| | | char chmap_str[64]; |
| | | #endif |
| | | |
| | | /* Initialize all variables that we clean on shutdown */ |
| | | this->hidden = (struct SDL_PrivateAudioData *) |
| | |
| | | } |
| | | this->spec.format = test_format; |
| | | |
| | | /* Validate number of channels and determine if swizzling is necessary |
| | | * Assume original swizzling, until proven otherwise. |
| | | */ |
| | | this->hidden->swizzle_func = swizzle_alsa_channels; |
| | | #ifdef SND_CHMAP_API_VERSION |
| | | chmap = ALSA_snd_pcm_get_chmap(pcm_handle); |
| | | if (chmap) { |
| | | ALSA_snd_pcm_chmap_print(chmap, sizeof(chmap_str), chmap_str); |
| | | if (SDL_strcmp("FL FR FC LFE RL RR", chmap_str) == 0 || |
| | | SDL_strcmp("FL FR FC LFE SL SR", chmap_str) == 0) { |
| | | this->hidden->swizzle_func = no_swizzle; |
| | | } |
| | | free(chmap); |
| | | } |
| | | #endif /* SND_CHMAP_API_VERSION */ |
| | | |
| | | /* Set the number of channels */ |
| | | status = ALSA_snd_pcm_hw_params_set_channels(pcm_handle, hwparams, |
| | | this->spec.channels); |
| | |
| | | this->spec.freq = rate; |
| | | |
| | | /* Set the buffer size, in samples */ |
| | | if ( ALSA_set_period_size(this, hwparams, 0) < 0 && |
| | | ALSA_set_buffer_size(this, hwparams, 0) < 0 ) { |
| | | /* Failed to set desired buffer size, do the best you can... */ |
| | | status = ALSA_set_period_size(this, hwparams, 1); |
| | | if (status < 0) { |
| | | return SDL_SetError("Couldn't set hardware audio parameters: %s", ALSA_snd_strerror(status)); |
| | | } |
| | | status = ALSA_set_buffer_size(this, hwparams); |
| | | if (status < 0) { |
| | | return SDL_SetError("Couldn't set hardware audio parameters: %s", ALSA_snd_strerror(status)); |
| | | } |
| | | |
| | | /* Set the software parameters */ |
| | | snd_pcm_sw_params_alloca(&swparams); |
| | | status = ALSA_snd_pcm_sw_params_current(pcm_handle, swparams); |
| | |
| | | SDL_memset(this->hidden->mixbuf, this->spec.silence, this->hidden->mixlen); |
| | | } |
| | | |
| | | /* Switch to blocking mode for playback */ |
| | | ALSA_snd_pcm_nonblock(pcm_handle, 0); |
| | | #if !SDL_ALSA_NON_BLOCKING |
| | | if (!iscapture) { |
| | | ALSA_snd_pcm_nonblock(pcm_handle, 0); |
| | | } |
| | | #endif |
| | | |
| | | /* We're ready to rock and roll. :-) */ |
| | | return 0; |
| | |
| | | add_device(const int iscapture, const char *name, void *hint, ALSA_Device **pSeen) |
| | | { |
| | | ALSA_Device *dev = SDL_malloc(sizeof (ALSA_Device)); |
| | | char *desc = ALSA_snd_device_name_get_hint(hint, "DESC"); |
| | | char *desc; |
| | | char *handle = NULL; |
| | | char *ptr; |
| | | |
| | | if (!desc) { |
| | | SDL_free(dev); |
| | | if (!dev) { |
| | | return; |
| | | } else if (!dev) { |
| | | free(desc); |
| | | return; |
| | | } |
| | | |
| | | /* Not all alsa devices are enumerable via snd_device_name_get_hint |
| | | (i.e. bluetooth devices). Therefore if hint is passed in to this |
| | | function as NULL, assume name contains desc. |
| | | Make sure not to free the storage associated with desc in this case */ |
| | | if (hint) { |
| | | desc = ALSA_snd_device_name_get_hint(hint, "DESC"); |
| | | if (!desc) { |
| | | SDL_free(dev); |
| | | return; |
| | | } |
| | | } else { |
| | | desc = (char *) name; |
| | | } |
| | | |
| | | SDL_assert(name != NULL); |
| | |
| | | |
| | | handle = SDL_strdup(name); |
| | | if (!handle) { |
| | | free(desc); |
| | | if (hint) { |
| | | free(desc); |
| | | } |
| | | SDL_free(dev); |
| | | return; |
| | | } |
| | | |
| | | SDL_AddAudioDevice(iscapture, desc, handle); |
| | | free(desc); |
| | | |
| | | if (hint) |
| | | free(desc); |
| | | dev->name = handle; |
| | | dev->iscapture = iscapture; |
| | | dev->next = *pSeen; |
| | |
| | | ALSA_Device *dev; |
| | | Uint32 ticks; |
| | | |
| | | SDL_SetThreadPriority(SDL_THREAD_PRIORITY_LOW); |
| | | |
| | | while (!SDL_AtomicGet(&ALSA_hotplug_shutdown)) { |
| | | void **hints = NULL; |
| | | ALSA_Device *unseen; |
| | | ALSA_Device *seen; |
| | | ALSA_Device *prev; |
| | | |
| | | if (ALSA_snd_device_name_hint(-1, "pcm", &hints) != -1) { |
| | | ALSA_Device *unseen = devices; |
| | | ALSA_Device *seen = NULL; |
| | | ALSA_Device *prev; |
| | | int i, j; |
| | | const char *match = NULL; |
| | | int bestmatch = 0xFFFF; |
| | |
| | | "hw:", "sysdefault:", "default:", NULL |
| | | }; |
| | | |
| | | unseen = devices; |
| | | seen = NULL; |
| | | /* Apparently there are several different ways that ALSA lists |
| | | actual hardware. It could be prefixed with "hw:" or "default:" |
| | | or "sysdefault:" and maybe others. Go through the list and see |
| | |
| | | |
| | | /* report anything still in unseen as removed. */ |
| | | for (dev = unseen; dev; dev = next) { |
| | | /*printf("ALSA: removing %s device '%s'\n", dev->iscapture ? "capture" : "output", dev->name);*/ |
| | | /*printf("ALSA: removing usb %s device '%s'\n", dev->iscapture ? "capture" : "output", dev->name);*/ |
| | | next = dev->next; |
| | | SDL_RemoveAudioDevice(dev->iscapture, dev->name); |
| | | SDL_free(dev->name); |