mirror of
https://github.com/libretro/RetroArch
synced 2025-02-01 00:32:46 +00:00
483 lines
14 KiB
C
483 lines
14 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2011-2017 Daniel De Matteis
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include <lists/string_list.h>
|
|
#include <string/stdstring.h>
|
|
|
|
#include <alsa/asoundlib.h>
|
|
#include <asm-generic/errno.h>
|
|
|
|
#include "../audio_driver.h"
|
|
#include "../common/alsa.h"
|
|
#include "../../verbosity.h"
|
|
|
|
int alsa_init_pcm(snd_pcm_t **pcm,
|
|
const char* device,
|
|
snd_pcm_stream_t stream,
|
|
unsigned rate,
|
|
unsigned latency,
|
|
unsigned channels,
|
|
alsa_stream_info_t *stream_info,
|
|
unsigned *new_rate,
|
|
int mode)
|
|
{
|
|
snd_pcm_format_t format;
|
|
snd_pcm_uframes_t buffer_size;
|
|
snd_pcm_hw_params_t *params = NULL;
|
|
snd_pcm_sw_params_t *sw_params = NULL;
|
|
unsigned latency_usec = latency * 1000;
|
|
unsigned periods = 4;
|
|
unsigned orig_rate = rate;
|
|
const char *alsa_dev = device ? device : "default";
|
|
int errnum = 0;
|
|
|
|
RARCH_DBG("[ALSA]: Requesting device \"%s\" for %s stream\n", alsa_dev, snd_pcm_stream_name(stream));
|
|
|
|
if ((errnum = snd_pcm_open(pcm, alsa_dev, stream, mode)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to open %s stream on device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
alsa_dev,
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_hw_params_malloc(¶ms)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to allocate hardware parameters: %s\n",
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_hw_params_any(*pcm, params)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to query hardware parameters from %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
format = (snd_pcm_hw_params_test_format(*pcm, params, SND_PCM_FORMAT_FLOAT) == 0)
|
|
? SND_PCM_FORMAT_FLOAT : SND_PCM_FORMAT_S16;
|
|
stream_info->has_float = (format == SND_PCM_FORMAT_FLOAT);
|
|
|
|
RARCH_LOG("[ALSA]: Using %s sample format for %s device \"%s\"\n",
|
|
snd_pcm_format_name(format),
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm)
|
|
);
|
|
|
|
if ((errnum = snd_pcm_hw_params_set_access(*pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to set %s access for %s device \"%s\": %s\n",
|
|
snd_pcm_access_name(SND_PCM_ACCESS_RW_INTERLEAVED),
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
stream_info->frame_bits = snd_pcm_format_physical_width(format) * channels;
|
|
|
|
if ((errnum = snd_pcm_hw_params_set_format(*pcm, params, format)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to set %s format for %s device \"%s\": %s\n",
|
|
snd_pcm_format_name(format),
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_hw_params_set_channels(*pcm, params, channels)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to set %u-channel audio for %s device \"%s\": %s\n",
|
|
channels,
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
/* Don't allow rate resampling when probing for the default rate (but ignore if this call fails) */
|
|
if ((errnum = snd_pcm_hw_params_set_rate_resample(*pcm, params, false)) < 0)
|
|
{
|
|
RARCH_WARN("[ALSA]: Failed to request a default unsampled rate for %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
}
|
|
|
|
if ((errnum = snd_pcm_hw_params_set_rate_near(*pcm, params, &rate, 0)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to request a rate near %uHz for %s device \"%s\": %s\n",
|
|
rate,
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if (new_rate)
|
|
*new_rate = rate;
|
|
|
|
if ((snd_pcm_hw_params_set_buffer_time_near(*pcm, params, &latency_usec, NULL)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to request a buffer time near %uus for %s device \"%s\": %s\n",
|
|
latency_usec,
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
if ((errnum = snd_pcm_hw_params_set_periods_near(*pcm, params, &periods, NULL)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to request %u periods per buffer for %s device \"%s\": %s\n",
|
|
periods,
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_hw_params(*pcm, params)) < 0)
|
|
{ /* This calls snd_pcm_prepare() under the hood */
|
|
RARCH_ERR("[ALSA]: Failed to install hardware parameters for %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
/* Shouldn't have to bother with this,
|
|
* but some drivers are apparently broken. */
|
|
if ((errnum = snd_pcm_hw_params_get_period_size(params, &stream_info->period_frames, NULL)) < 0)
|
|
{
|
|
RARCH_WARN("[ALSA]: Failed to get an exact period size from %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
RARCH_WARN("[ALSA]: Trying the minimum period size instead\n");
|
|
|
|
if ((errnum = snd_pcm_hw_params_get_period_size_min(params, &stream_info->period_frames, NULL)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to get min period size from %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
stream_info->period_size = snd_pcm_frames_to_bytes(*pcm, stream_info->period_frames);
|
|
if (stream_info->period_size < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to convert a period size of %lu frames to bytes: %s\n",
|
|
stream_info->period_frames,
|
|
snd_strerror(stream_info->period_frames));
|
|
goto error;
|
|
}
|
|
|
|
RARCH_LOG("[ALSA]: Period: %u periods per buffer (%lu frames, %lu bytes)\n",
|
|
periods,
|
|
stream_info->period_frames,
|
|
stream_info->period_size);
|
|
|
|
if ((errnum = snd_pcm_hw_params_get_buffer_size(params, &buffer_size)) < 0)
|
|
{
|
|
RARCH_WARN("[ALSA]: Failed to get exact buffer size from %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
RARCH_WARN("[ALSA]: Trying the maximum buffer size instead\n");
|
|
|
|
if ((errnum = snd_pcm_hw_params_get_buffer_size_max(params, &buffer_size)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to get max buffer size from %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
goto error;
|
|
}
|
|
}
|
|
|
|
|
|
stream_info->buffer_size = snd_pcm_frames_to_bytes(*pcm, buffer_size);
|
|
if (stream_info->buffer_size < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to convert a buffer size of %lu frames to bytes: %s\n",
|
|
buffer_size,
|
|
snd_strerror(buffer_size));
|
|
goto error;
|
|
}
|
|
RARCH_LOG("[ALSA]: Buffer size: %lu frames (%lu bytes)\n", buffer_size, stream_info->buffer_size);
|
|
|
|
stream_info->can_pause = snd_pcm_hw_params_can_pause(params);
|
|
|
|
RARCH_LOG("[ALSA]: Can pause: %s.\n", stream_info->can_pause ? "yes" : "no");
|
|
|
|
if ((errnum = snd_pcm_sw_params_malloc(&sw_params)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to allocate software parameters: %s\n",
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_sw_params_current(*pcm, sw_params)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to query current software parameters for %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_sw_params_set_start_threshold(*pcm, sw_params, buffer_size / 2)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to set start %lu-frame threshold for %s device \"%s\": %s\n",
|
|
buffer_size / 2,
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
if ((errnum = snd_pcm_sw_params(*pcm, sw_params)) < 0)
|
|
{
|
|
RARCH_ERR("[ALSA]: Failed to install software parameters for %s device \"%s\": %s\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm),
|
|
snd_strerror(errnum));
|
|
|
|
goto error;
|
|
}
|
|
|
|
snd_pcm_hw_params_free(params);
|
|
snd_pcm_sw_params_free(sw_params);
|
|
|
|
RARCH_LOG("[ALSA]: Initialized %s device \"%s\"\n",
|
|
snd_pcm_stream_name(stream),
|
|
snd_pcm_name(*pcm));
|
|
|
|
return 0;
|
|
error:
|
|
if (params)
|
|
snd_pcm_hw_params_free(params);
|
|
|
|
if (sw_params)
|
|
snd_pcm_sw_params_free(sw_params);
|
|
|
|
if (*pcm)
|
|
{
|
|
alsa_free_pcm(*pcm);
|
|
*pcm = NULL;
|
|
}
|
|
|
|
return errnum;
|
|
}
|
|
|
|
void alsa_free_pcm(snd_pcm_t *pcm)
|
|
{
|
|
if (pcm)
|
|
{
|
|
int errnum = 0;
|
|
|
|
if ((errnum = snd_pcm_drop(pcm)) < 0)
|
|
{
|
|
RARCH_WARN("[ALSA]: Failed to drop remaining samples in %s stream \"%s\": %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_strerror(errnum));
|
|
}
|
|
|
|
if ((errnum = snd_pcm_close(pcm)) < 0)
|
|
{
|
|
RARCH_WARN("[ALSA]: Failed to close %s stream \"%s\": %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_strerror(errnum));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool alsa_start_pcm(snd_pcm_t *pcm)
|
|
{
|
|
int errnum = 0;
|
|
snd_pcm_state_t pcm_state;
|
|
|
|
if (!pcm)
|
|
return false;
|
|
|
|
pcm_state = snd_pcm_state(pcm);
|
|
switch (pcm_state)
|
|
{
|
|
case SND_PCM_STATE_PAUSED: /* If we're unpausing a valid (but paused) stream... */
|
|
if ((errnum = snd_pcm_pause(pcm, false)) < 0) /* ...but we failed... */
|
|
goto error;
|
|
|
|
break;
|
|
case SND_PCM_STATE_PREPARED:
|
|
/* If we're starting this stream for the first time... */
|
|
if ((errnum = snd_pcm_start(pcm)) < 0) /* ..but we failed... */
|
|
goto error;
|
|
|
|
break;
|
|
case SND_PCM_STATE_RUNNING:
|
|
RARCH_DBG("[ALSA]: %s stream \"%s\" is already running, no action needed.\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm));
|
|
return true;
|
|
default:
|
|
RARCH_ERR("[ALSA]: Failed to start %s stream \"%s\" in unexpected state %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_state_name(pcm_state));
|
|
return false;
|
|
}
|
|
|
|
RARCH_DBG("[ALSA]: Started %s stream \"%s\", transitioning from %s to %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_state_name(pcm_state),
|
|
snd_pcm_state_name(snd_pcm_state(pcm)));
|
|
|
|
return true;
|
|
|
|
error:
|
|
RARCH_ERR("[ALSA]: Failed to start %s stream \"%s\" in state %s: %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_state_name(pcm_state),
|
|
snd_strerror(errnum));
|
|
|
|
return false;
|
|
}
|
|
|
|
bool alsa_stop_pcm(snd_pcm_t *pcm)
|
|
{
|
|
int errnum = 0;
|
|
snd_pcm_state_t pcm_state;
|
|
|
|
if (!pcm)
|
|
return false;
|
|
|
|
pcm_state = snd_pcm_state(pcm);
|
|
switch (pcm_state)
|
|
{
|
|
case SND_PCM_STATE_PAUSED:
|
|
RARCH_DBG("[ALSA]: %s stream \"%s\" is already paused, no action needed.\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm));
|
|
return true;
|
|
case SND_PCM_STATE_PREPARED:
|
|
RARCH_DBG("[ALSA]: %s stream \"%s\" is prepared but not running, no action needed.\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm));
|
|
return true;
|
|
case SND_PCM_STATE_RUNNING:
|
|
/* If we're pausing an active stream... */
|
|
if ((errnum = snd_pcm_pause(pcm, true)) < 0) /* ...but we failed... */
|
|
goto error;
|
|
|
|
break;
|
|
default:
|
|
RARCH_ERR("[ALSA]: Failed to stop %s stream \"%s\" in unexpected state %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_state_name(pcm_state));
|
|
|
|
return false;
|
|
}
|
|
|
|
RARCH_DBG("[ALSA]: Stopped %s stream \"%s\", transitioning from %s to %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_state_name(pcm_state),
|
|
snd_pcm_state_name(snd_pcm_state(pcm)));
|
|
|
|
return true;
|
|
|
|
error:
|
|
RARCH_ERR("[ALSA]: Failed to stop %s stream \"%s\" in state %s: %s\n",
|
|
snd_pcm_stream_name(snd_pcm_stream(pcm)),
|
|
snd_pcm_name(pcm),
|
|
snd_pcm_state_name(pcm_state),
|
|
snd_strerror(errnum));
|
|
|
|
return false;
|
|
}
|
|
|
|
struct string_list *alsa_device_list_type_new(const char* type)
|
|
{
|
|
void **hints, **n;
|
|
union string_list_elem_attr attr;
|
|
struct string_list *s = string_list_new();
|
|
|
|
if (!s)
|
|
return NULL;
|
|
|
|
attr.i = 0;
|
|
|
|
if (snd_device_name_hint(-1, "pcm", &hints) != 0)
|
|
{
|
|
string_list_free(s);
|
|
return NULL;
|
|
}
|
|
|
|
n = hints;
|
|
|
|
while (*n)
|
|
{
|
|
char *name = snd_device_name_get_hint(*n, "NAME");
|
|
char *io = snd_device_name_get_hint(*n, "IOID");
|
|
char *desc = snd_device_name_get_hint(*n, "DESC");
|
|
|
|
/* description of device IOID - input / output identifcation
|
|
* ("Input" or "Output"), NULL means both) */
|
|
|
|
if (!io || (string_is_equal(io, type)))
|
|
string_list_append(s, name, attr);
|
|
|
|
if (name)
|
|
free(name);
|
|
if (io)
|
|
free(io);
|
|
if (desc)
|
|
free(desc);
|
|
|
|
n++;
|
|
}
|
|
|
|
/* free hint buffer too */
|
|
snd_device_name_free_hint(hints);
|
|
|
|
return s;
|
|
}
|