/* RetroArch - A frontend for libretro. * Copyright (C) 2023 Jesse Talavera-Greenberg * * RetroArch 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 Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch 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 RetroArch. * If not, see . */ #include #include #include #include "audio/common/alsa.h" #include "audio/common/alsathread.h" #include "audio/microphone_driver.h" #include "verbosity.h" #include "retro_assert.h" typedef struct alsa_thread_microphone_handle { alsa_thread_info_t info; } alsa_thread_microphone_handle_t; typedef struct alsa_thread { bool nonblock; } alsa_thread_microphone_t; static void *alsa_thread_microphone_init(void) { alsa_thread_microphone_t *alsa = (alsa_thread_microphone_t*)calloc(1, sizeof(alsa_thread_microphone_t)); if (!alsa) { RARCH_ERR("[ALSA] Failed to allocate driver context\n"); return NULL; } RARCH_LOG("[ALSA] Using ALSA version %s\n", snd_asoundlib_version()); return alsa; } static void alsa_thread_microphone_close_mic(void *driver_context, void *microphone_context); static void alsa_thread_microphone_free(void *driver_context) { alsa_thread_microphone_t *alsa = (alsa_thread_microphone_t*)driver_context; if (alsa) { free(alsa); } } /** @see alsa_thread_read_microphone() */ static void alsa_microphone_worker_thread(void *microphone_context) { alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t*)microphone_context; uint8_t *buf = NULL; uintptr_t thread_id = sthread_get_current_thread_id(); retro_assert(microphone != NULL); buf = (uint8_t *)calloc(1, microphone->info.stream_info.period_size); if (!buf) { RARCH_ERR("[ALSA] [capture thread %p]: Failed to allocate audio buffer\n", thread_id); goto end; } RARCH_DBG("[ALSA] [capture thread %p]: Beginning microphone worker thread\n", thread_id); RARCH_DBG("[ALSA] [capture thread %p]: Microphone \"%s\" is in state %s\n", thread_id, snd_pcm_name(microphone->info.pcm), snd_pcm_state_name(snd_pcm_state(microphone->info.pcm))); while (!microphone->info.thread_dead) { /* Until we're told to stop... */ size_t avail; size_t fifo_size; snd_pcm_sframes_t frames; int errnum = 0; /* Lock the incoming sample queue (the main thread may block) */ slock_lock(microphone->info.fifo_lock); /* Fill the incoming sample queue with whatever we recently read */ avail = FIFO_WRITE_AVAIL(microphone->info.buffer); fifo_size = MIN(microphone->info.stream_info.period_size, avail); fifo_write(microphone->info.buffer, buf, fifo_size); /* Tell the main thread that it's okay to query the mic again */ scond_signal(microphone->info.cond); /* Unlock the incoming sample queue (the main thread may resume) */ slock_unlock(microphone->info.fifo_lock); /* If underrun, fill rest with silence. */ memset(buf + fifo_size, 0, microphone->info.stream_info.period_size - fifo_size); errnum = snd_pcm_wait(microphone->info.pcm, 33); if (errnum == 0) { RARCH_DBG("[ALSA] [capture thread %p]: Timeout after 33ms waiting for input\n", thread_id); continue; } else if (errnum == -EPIPE || errnum == -ESTRPIPE || errnum == -EINTR) { RARCH_WARN("[ALSA] [capture thread %p]: Wait error: %s\n", thread_id, snd_strerror(errnum)); if ((errnum = snd_pcm_recover(microphone->info.pcm, errnum, false)) < 0) { RARCH_ERR("[ALSA] [capture thread %p]: Failed to recover from prior wait error: %s\n", thread_id, snd_strerror(errnum)); break; } continue; } frames = snd_pcm_readi(microphone->info.pcm, buf, microphone->info.stream_info.period_frames); if (frames == -EPIPE || frames == -EINTR || frames == -ESTRPIPE) { RARCH_WARN("[ALSA] [capture thread %p]: Read error: %s\n", thread_id, snd_strerror(frames)); if ((errnum = snd_pcm_recover(microphone->info.pcm, frames, false)) < 0) { RARCH_ERR("[ALSA] [capture thread %p]: Failed to recover from prior read error: %s\n", thread_id, snd_strerror(errnum)); break; } continue; } else if (frames < 0) { RARCH_ERR("[ALSA] [capture thread %p]: Read error: %s.\n", thread_id, snd_strerror(frames)); break; } } end: slock_lock(microphone->info.cond_lock); microphone->info.thread_dead = true; scond_signal(microphone->info.cond); slock_unlock(microphone->info.cond_lock); free(buf); RARCH_DBG("[ALSA] [capture thread %p]: Ending microphone worker thread\n", thread_id); } static int alsa_thread_microphone_read(void *driver_context, void *microphone_context, void *buf, size_t size) { alsa_thread_microphone_t *alsa = (alsa_thread_microphone_t*)driver_context; alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t*)microphone_context; snd_pcm_state_t state; if (!alsa || !microphone || !buf) /* If any of the parameters were invalid... */ return -1; if (microphone->info.thread_dead) /* If the mic thread is shutting down... */ return -1; state = snd_pcm_state(microphone->info.pcm); if (state != SND_PCM_STATE_RUNNING) { int errnum; RARCH_WARN("[ALSA]: Expected microphone \"%s\" to be in state RUNNING, was in state %s\n", snd_pcm_name(microphone->info.pcm), snd_pcm_state_name(state)); errnum = snd_pcm_start(microphone->info.pcm); if (errnum < 0) { RARCH_ERR("[ALSA]: Failed to start microphone \"%s\": %s\n", snd_pcm_name(microphone->info.pcm), snd_strerror(errnum)); return -1; } } if (alsa->nonblock) { /* If driver interactions shouldn't block... */ size_t avail; size_t write_amt; /* "Hey, I'm gonna borrow the queue." */ slock_lock(microphone->info.fifo_lock); avail = FIFO_READ_AVAIL(microphone->info.buffer); write_amt = MIN(avail, size); /* "It's okay if you don't have any new samples, I'll just check in on you later." */ fifo_read(microphone->info.buffer, buf, write_amt); /* "Here, take this queue back." */ slock_unlock(microphone->info.fifo_lock); return (int)write_amt; } else { size_t read = 0; while (read < size && !microphone->info.thread_dead) { /* Until we've read all requested samples (or we're told to stop)... */ size_t avail; /* "Hey, I'm gonna borrow the queue." */ slock_lock(microphone->info.fifo_lock); avail = FIFO_READ_AVAIL(microphone->info.buffer); if (avail == 0) { /* "Oh, wait, it's empty." */ /* "Here, take it back..." */ slock_unlock(microphone->info.fifo_lock); /* "...I'll just wait right here." */ slock_lock(microphone->info.cond_lock); /* "Unless we're closing up shop..." */ if (!microphone->info.thread_dead) /* "...let me know when you've produced some samples." */ scond_wait(microphone->info.cond, microphone->info.cond_lock); /* "Oh, you're ready? Okay, I'm gonna continue." */ slock_unlock(microphone->info.cond_lock); } else { size_t read_amt = MIN(size - read, avail); /* "I'll just go ahead and consume all these samples..." * (As many as will fit in buf, or as many as are available.) */ fifo_read(microphone->info.buffer,buf + read, read_amt); /* "I'm done, you can take the queue back now." */ slock_unlock(microphone->info.fifo_lock); read += read_amt; } /* "I'll be right back..." */ } return (int)read; } } static bool alsa_thread_microphone_mic_alive(const void *driver_context, const void *microphone_context); static void *alsa_thread_microphone_open_mic(void *driver_context, const char *device, unsigned rate, unsigned latency, unsigned *new_rate) { alsa_thread_microphone_t *alsa = (alsa_thread_microphone_t*)driver_context; alsa_thread_microphone_handle_t *microphone = NULL; if (!alsa) /* If we weren't given a valid ALSA context... */ return NULL; microphone = calloc(1, sizeof(alsa_thread_microphone_handle_t)); if (!microphone) { /* If the microphone context couldn't be allocated... */ RARCH_ERR("[ALSA] Failed to allocate microphone context\n"); return NULL; } if (alsa_init_pcm(µphone->info.pcm, device, SND_PCM_STREAM_CAPTURE, rate, latency, 1, µphone->info.stream_info, new_rate, 0) < 0) { goto error; } microphone->info.fifo_lock = slock_new(); microphone->info.cond_lock = slock_new(); microphone->info.cond = scond_new(); microphone->info.buffer = fifo_new(microphone->info.stream_info.buffer_size); if (!microphone->info.fifo_lock || !microphone->info.cond_lock || !microphone->info.cond || !microphone->info.buffer || !microphone->info.pcm) goto error; microphone->info.worker_thread = sthread_create(alsa_microphone_worker_thread, microphone); if (!microphone->info.worker_thread) { RARCH_ERR("[ALSA]: Failed to initialize microphone worker thread\n"); goto error; } RARCH_DBG("[ALSA]: Initialized microphone worker thread\n"); return microphone; error: RARCH_ERR("[ALSA]: Failed to initialize microphone...\n"); if (microphone) { if (microphone->info.pcm) { snd_pcm_close(microphone->info.pcm); } alsa_thread_microphone_close_mic(alsa, microphone); } return NULL; } static void alsa_thread_microphone_close_mic(void *driver_context, void *microphone_context) { alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t*)microphone_context; (void)driver_context; if (microphone) { alsa_thread_free_info_members(µphone->info); free(microphone); } } static bool alsa_thread_microphone_mic_alive(const void *driver_context, const void *microphone_context) { alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t *)microphone_context; (void)driver_context; if (!microphone) return false; return snd_pcm_state(microphone->info.pcm) == SND_PCM_STATE_RUNNING; } static void alsa_thread_microphone_set_nonblock_state(void *driver_context, bool state) { alsa_thread_microphone_t *alsa = (alsa_thread_microphone_t*)driver_context; alsa->nonblock = state; } static struct string_list *alsa_thread_microphone_device_list_new(const void *data) { (void)data; return alsa_device_list_type_new("Input"); } static void alsa_thread_microphone_device_list_free(const void *driver_context, struct string_list *devices) { (void)driver_context; string_list_free(devices); /* Does nothing if devices is NULL */ } static bool alsa_thread_microphone_start_mic(void *driver_context, void *microphone_context) { alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t*)microphone_context; (void)driver_context; if (!microphone) return false; return alsa_start_pcm(microphone->info.pcm); } static bool alsa_thread_microphone_stop_mic(void *driver_context, void *microphone_context) { alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t*)microphone_context; (void)driver_context; if (!microphone) return false; return alsa_stop_pcm(microphone->info.pcm); } static bool alsa_thread_microphone_mic_use_float(const void *driver_context, const void *microphone_context) { alsa_thread_microphone_handle_t *microphone = (alsa_thread_microphone_handle_t*)microphone_context; return microphone->info.stream_info.has_float; } microphone_driver_t microphone_alsathread = { alsa_thread_microphone_init, alsa_thread_microphone_free, alsa_thread_microphone_read, alsa_thread_microphone_set_nonblock_state, "alsathread", alsa_thread_microphone_device_list_new, alsa_thread_microphone_device_list_free, alsa_thread_microphone_open_mic, alsa_thread_microphone_close_mic, alsa_thread_microphone_mic_alive, alsa_thread_microphone_start_mic, alsa_thread_microphone_stop_mic, alsa_thread_microphone_mic_use_float };