mirror of
https://gitlab.com/qemu-project/qemu
synced 2024-09-06 18:05:37 +00:00
![Martin Schrodt](/assets/img/avatar_default.png)
Several people have reported to have bag microphone lag with the PA backend. While I cannot reproduce the problem here, it seems that their PA somehow decides to buffer the microphone input for way too long, causing this delay. This patch sets an upper limit to the amount of data PA should hold. This fixes the problem reliably on their side, while having no adverse effects on mine. Signed-off-by: Martin Schrodt <martin@schrodt.org> Message-id: 20190615153852.99040-1-martin@schrodt.org Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
977 lines
24 KiB
C
977 lines
24 KiB
C
/* public domain */
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/module.h"
|
|
#include "audio.h"
|
|
#include "qapi/opts-visitor.h"
|
|
|
|
#include <pulse/pulseaudio.h>
|
|
|
|
#define AUDIO_CAP "pulseaudio"
|
|
#include "audio_int.h"
|
|
#include "audio_pt_int.h"
|
|
|
|
typedef struct {
|
|
Audiodev *dev;
|
|
pa_threaded_mainloop *mainloop;
|
|
pa_context *context;
|
|
} paaudio;
|
|
|
|
typedef struct {
|
|
HWVoiceOut hw;
|
|
int done;
|
|
int live;
|
|
int decr;
|
|
int rpos;
|
|
pa_stream *stream;
|
|
void *pcm_buf;
|
|
struct audio_pt pt;
|
|
paaudio *g;
|
|
int samples;
|
|
} PAVoiceOut;
|
|
|
|
typedef struct {
|
|
HWVoiceIn hw;
|
|
int done;
|
|
int dead;
|
|
int incr;
|
|
int wpos;
|
|
pa_stream *stream;
|
|
void *pcm_buf;
|
|
struct audio_pt pt;
|
|
const void *read_data;
|
|
size_t read_index, read_length;
|
|
paaudio *g;
|
|
int samples;
|
|
} PAVoiceIn;
|
|
|
|
static void qpa_audio_fini(void *opaque);
|
|
|
|
static void GCC_FMT_ATTR (2, 3) qpa_logerr (int err, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start (ap, fmt);
|
|
AUD_vlog (AUDIO_CAP, fmt, ap);
|
|
va_end (ap);
|
|
|
|
AUD_log (AUDIO_CAP, "Reason: %s\n", pa_strerror (err));
|
|
}
|
|
|
|
#ifndef PA_CONTEXT_IS_GOOD
|
|
static inline int PA_CONTEXT_IS_GOOD(pa_context_state_t x)
|
|
{
|
|
return
|
|
x == PA_CONTEXT_CONNECTING ||
|
|
x == PA_CONTEXT_AUTHORIZING ||
|
|
x == PA_CONTEXT_SETTING_NAME ||
|
|
x == PA_CONTEXT_READY;
|
|
}
|
|
#endif
|
|
|
|
#ifndef PA_STREAM_IS_GOOD
|
|
static inline int PA_STREAM_IS_GOOD(pa_stream_state_t x)
|
|
{
|
|
return
|
|
x == PA_STREAM_CREATING ||
|
|
x == PA_STREAM_READY;
|
|
}
|
|
#endif
|
|
|
|
#define CHECK_SUCCESS_GOTO(c, rerror, expression, label) \
|
|
do { \
|
|
if (!(expression)) { \
|
|
if (rerror) { \
|
|
*(rerror) = pa_context_errno ((c)->context); \
|
|
} \
|
|
goto label; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define CHECK_DEAD_GOTO(c, stream, rerror, label) \
|
|
do { \
|
|
if (!(c)->context || !PA_CONTEXT_IS_GOOD (pa_context_get_state((c)->context)) || \
|
|
!(stream) || !PA_STREAM_IS_GOOD (pa_stream_get_state ((stream)))) { \
|
|
if (((c)->context && pa_context_get_state ((c)->context) == PA_CONTEXT_FAILED) || \
|
|
((stream) && pa_stream_get_state ((stream)) == PA_STREAM_FAILED)) { \
|
|
if (rerror) { \
|
|
*(rerror) = pa_context_errno ((c)->context); \
|
|
} \
|
|
} else { \
|
|
if (rerror) { \
|
|
*(rerror) = PA_ERR_BADSTATE; \
|
|
} \
|
|
} \
|
|
goto label; \
|
|
} \
|
|
} while (0)
|
|
|
|
static int qpa_simple_read (PAVoiceIn *p, void *data, size_t length, int *rerror)
|
|
{
|
|
paaudio *g = p->g;
|
|
|
|
pa_threaded_mainloop_lock (g->mainloop);
|
|
|
|
CHECK_DEAD_GOTO (g, p->stream, rerror, unlock_and_fail);
|
|
|
|
while (length > 0) {
|
|
size_t l;
|
|
|
|
while (!p->read_data) {
|
|
int r;
|
|
|
|
r = pa_stream_peek (p->stream, &p->read_data, &p->read_length);
|
|
CHECK_SUCCESS_GOTO (g, rerror, r == 0, unlock_and_fail);
|
|
|
|
if (!p->read_data) {
|
|
pa_threaded_mainloop_wait (g->mainloop);
|
|
CHECK_DEAD_GOTO (g, p->stream, rerror, unlock_and_fail);
|
|
} else {
|
|
p->read_index = 0;
|
|
}
|
|
}
|
|
|
|
l = p->read_length < length ? p->read_length : length;
|
|
memcpy (data, (const uint8_t *) p->read_data+p->read_index, l);
|
|
|
|
data = (uint8_t *) data + l;
|
|
length -= l;
|
|
|
|
p->read_index += l;
|
|
p->read_length -= l;
|
|
|
|
if (!p->read_length) {
|
|
int r;
|
|
|
|
r = pa_stream_drop (p->stream);
|
|
p->read_data = NULL;
|
|
p->read_length = 0;
|
|
p->read_index = 0;
|
|
|
|
CHECK_SUCCESS_GOTO (g, rerror, r == 0, unlock_and_fail);
|
|
}
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
return 0;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
return -1;
|
|
}
|
|
|
|
static int qpa_simple_write (PAVoiceOut *p, const void *data, size_t length, int *rerror)
|
|
{
|
|
paaudio *g = p->g;
|
|
|
|
pa_threaded_mainloop_lock (g->mainloop);
|
|
|
|
CHECK_DEAD_GOTO (g, p->stream, rerror, unlock_and_fail);
|
|
|
|
while (length > 0) {
|
|
size_t l;
|
|
int r;
|
|
|
|
while (!(l = pa_stream_writable_size (p->stream))) {
|
|
pa_threaded_mainloop_wait (g->mainloop);
|
|
CHECK_DEAD_GOTO (g, p->stream, rerror, unlock_and_fail);
|
|
}
|
|
|
|
CHECK_SUCCESS_GOTO (g, rerror, l != (size_t) -1, unlock_and_fail);
|
|
|
|
if (l > length) {
|
|
l = length;
|
|
}
|
|
|
|
r = pa_stream_write (p->stream, data, l, NULL, 0LL, PA_SEEK_RELATIVE);
|
|
CHECK_SUCCESS_GOTO (g, rerror, r >= 0, unlock_and_fail);
|
|
|
|
data = (const uint8_t *) data + l;
|
|
length -= l;
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
return 0;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
return -1;
|
|
}
|
|
|
|
static void *qpa_thread_out (void *arg)
|
|
{
|
|
PAVoiceOut *pa = arg;
|
|
HWVoiceOut *hw = &pa->hw;
|
|
|
|
if (audio_pt_lock(&pa->pt, __func__)) {
|
|
return NULL;
|
|
}
|
|
|
|
for (;;) {
|
|
int decr, to_mix, rpos;
|
|
|
|
for (;;) {
|
|
if (pa->done) {
|
|
goto exit;
|
|
}
|
|
|
|
if (pa->live > 0) {
|
|
break;
|
|
}
|
|
|
|
if (audio_pt_wait(&pa->pt, __func__)) {
|
|
goto exit;
|
|
}
|
|
}
|
|
|
|
decr = to_mix = audio_MIN(pa->live, pa->samples >> 5);
|
|
rpos = pa->rpos;
|
|
|
|
if (audio_pt_unlock(&pa->pt, __func__)) {
|
|
return NULL;
|
|
}
|
|
|
|
while (to_mix) {
|
|
int error;
|
|
int chunk = audio_MIN (to_mix, hw->samples - rpos);
|
|
struct st_sample *src = hw->mix_buf + rpos;
|
|
|
|
hw->clip (pa->pcm_buf, src, chunk);
|
|
|
|
if (qpa_simple_write (pa, pa->pcm_buf,
|
|
chunk << hw->info.shift, &error) < 0) {
|
|
qpa_logerr (error, "pa_simple_write failed\n");
|
|
return NULL;
|
|
}
|
|
|
|
rpos = (rpos + chunk) % hw->samples;
|
|
to_mix -= chunk;
|
|
}
|
|
|
|
if (audio_pt_lock(&pa->pt, __func__)) {
|
|
return NULL;
|
|
}
|
|
|
|
pa->rpos = rpos;
|
|
pa->live -= decr;
|
|
pa->decr += decr;
|
|
}
|
|
|
|
exit:
|
|
audio_pt_unlock(&pa->pt, __func__);
|
|
return NULL;
|
|
}
|
|
|
|
static int qpa_run_out (HWVoiceOut *hw, int live)
|
|
{
|
|
int decr;
|
|
PAVoiceOut *pa = (PAVoiceOut *) hw;
|
|
|
|
if (audio_pt_lock(&pa->pt, __func__)) {
|
|
return 0;
|
|
}
|
|
|
|
decr = audio_MIN (live, pa->decr);
|
|
pa->decr -= decr;
|
|
pa->live = live - decr;
|
|
hw->rpos = pa->rpos;
|
|
if (pa->live > 0) {
|
|
audio_pt_unlock_and_signal(&pa->pt, __func__);
|
|
}
|
|
else {
|
|
audio_pt_unlock(&pa->pt, __func__);
|
|
}
|
|
return decr;
|
|
}
|
|
|
|
static int qpa_write (SWVoiceOut *sw, void *buf, int len)
|
|
{
|
|
return audio_pcm_sw_write (sw, buf, len);
|
|
}
|
|
|
|
/* capture */
|
|
static void *qpa_thread_in (void *arg)
|
|
{
|
|
PAVoiceIn *pa = arg;
|
|
HWVoiceIn *hw = &pa->hw;
|
|
|
|
if (audio_pt_lock(&pa->pt, __func__)) {
|
|
return NULL;
|
|
}
|
|
|
|
for (;;) {
|
|
int incr, to_grab, wpos;
|
|
|
|
for (;;) {
|
|
if (pa->done) {
|
|
goto exit;
|
|
}
|
|
|
|
if (pa->dead > 0) {
|
|
break;
|
|
}
|
|
|
|
if (audio_pt_wait(&pa->pt, __func__)) {
|
|
goto exit;
|
|
}
|
|
}
|
|
|
|
incr = to_grab = audio_MIN(pa->dead, pa->samples >> 5);
|
|
wpos = pa->wpos;
|
|
|
|
if (audio_pt_unlock(&pa->pt, __func__)) {
|
|
return NULL;
|
|
}
|
|
|
|
while (to_grab) {
|
|
int error;
|
|
int chunk = audio_MIN (to_grab, hw->samples - wpos);
|
|
void *buf = advance (pa->pcm_buf, wpos);
|
|
|
|
if (qpa_simple_read (pa, buf,
|
|
chunk << hw->info.shift, &error) < 0) {
|
|
qpa_logerr (error, "pa_simple_read failed\n");
|
|
return NULL;
|
|
}
|
|
|
|
hw->conv (hw->conv_buf + wpos, buf, chunk);
|
|
wpos = (wpos + chunk) % hw->samples;
|
|
to_grab -= chunk;
|
|
}
|
|
|
|
if (audio_pt_lock(&pa->pt, __func__)) {
|
|
return NULL;
|
|
}
|
|
|
|
pa->wpos = wpos;
|
|
pa->dead -= incr;
|
|
pa->incr += incr;
|
|
}
|
|
|
|
exit:
|
|
audio_pt_unlock(&pa->pt, __func__);
|
|
return NULL;
|
|
}
|
|
|
|
static int qpa_run_in (HWVoiceIn *hw)
|
|
{
|
|
int live, incr, dead;
|
|
PAVoiceIn *pa = (PAVoiceIn *) hw;
|
|
|
|
if (audio_pt_lock(&pa->pt, __func__)) {
|
|
return 0;
|
|
}
|
|
|
|
live = audio_pcm_hw_get_live_in (hw);
|
|
dead = hw->samples - live;
|
|
incr = audio_MIN (dead, pa->incr);
|
|
pa->incr -= incr;
|
|
pa->dead = dead - incr;
|
|
hw->wpos = pa->wpos;
|
|
if (pa->dead > 0) {
|
|
audio_pt_unlock_and_signal(&pa->pt, __func__);
|
|
}
|
|
else {
|
|
audio_pt_unlock(&pa->pt, __func__);
|
|
}
|
|
return incr;
|
|
}
|
|
|
|
static int qpa_read (SWVoiceIn *sw, void *buf, int len)
|
|
{
|
|
return audio_pcm_sw_read (sw, buf, len);
|
|
}
|
|
|
|
static pa_sample_format_t audfmt_to_pa (AudioFormat afmt, int endianness)
|
|
{
|
|
int format;
|
|
|
|
switch (afmt) {
|
|
case AUDIO_FORMAT_S8:
|
|
case AUDIO_FORMAT_U8:
|
|
format = PA_SAMPLE_U8;
|
|
break;
|
|
case AUDIO_FORMAT_S16:
|
|
case AUDIO_FORMAT_U16:
|
|
format = endianness ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE;
|
|
break;
|
|
case AUDIO_FORMAT_S32:
|
|
case AUDIO_FORMAT_U32:
|
|
format = endianness ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE;
|
|
break;
|
|
default:
|
|
dolog ("Internal logic error: Bad audio format %d\n", afmt);
|
|
format = PA_SAMPLE_U8;
|
|
break;
|
|
}
|
|
return format;
|
|
}
|
|
|
|
static AudioFormat pa_to_audfmt (pa_sample_format_t fmt, int *endianness)
|
|
{
|
|
switch (fmt) {
|
|
case PA_SAMPLE_U8:
|
|
return AUDIO_FORMAT_U8;
|
|
case PA_SAMPLE_S16BE:
|
|
*endianness = 1;
|
|
return AUDIO_FORMAT_S16;
|
|
case PA_SAMPLE_S16LE:
|
|
*endianness = 0;
|
|
return AUDIO_FORMAT_S16;
|
|
case PA_SAMPLE_S32BE:
|
|
*endianness = 1;
|
|
return AUDIO_FORMAT_S32;
|
|
case PA_SAMPLE_S32LE:
|
|
*endianness = 0;
|
|
return AUDIO_FORMAT_S32;
|
|
default:
|
|
dolog ("Internal logic error: Bad pa_sample_format %d\n", fmt);
|
|
return AUDIO_FORMAT_U8;
|
|
}
|
|
}
|
|
|
|
static void context_state_cb (pa_context *c, void *userdata)
|
|
{
|
|
paaudio *g = userdata;
|
|
|
|
switch (pa_context_get_state(c)) {
|
|
case PA_CONTEXT_READY:
|
|
case PA_CONTEXT_TERMINATED:
|
|
case PA_CONTEXT_FAILED:
|
|
pa_threaded_mainloop_signal (g->mainloop, 0);
|
|
break;
|
|
|
|
case PA_CONTEXT_UNCONNECTED:
|
|
case PA_CONTEXT_CONNECTING:
|
|
case PA_CONTEXT_AUTHORIZING:
|
|
case PA_CONTEXT_SETTING_NAME:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void stream_state_cb (pa_stream *s, void * userdata)
|
|
{
|
|
paaudio *g = userdata;
|
|
|
|
switch (pa_stream_get_state (s)) {
|
|
|
|
case PA_STREAM_READY:
|
|
case PA_STREAM_FAILED:
|
|
case PA_STREAM_TERMINATED:
|
|
pa_threaded_mainloop_signal (g->mainloop, 0);
|
|
break;
|
|
|
|
case PA_STREAM_UNCONNECTED:
|
|
case PA_STREAM_CREATING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void stream_request_cb (pa_stream *s, size_t length, void *userdata)
|
|
{
|
|
paaudio *g = userdata;
|
|
|
|
pa_threaded_mainloop_signal (g->mainloop, 0);
|
|
}
|
|
|
|
static pa_stream *qpa_simple_new (
|
|
paaudio *g,
|
|
const char *name,
|
|
pa_stream_direction_t dir,
|
|
const char *dev,
|
|
const pa_sample_spec *ss,
|
|
const pa_channel_map *map,
|
|
const pa_buffer_attr *attr,
|
|
int *rerror)
|
|
{
|
|
int r;
|
|
pa_stream *stream;
|
|
|
|
pa_threaded_mainloop_lock (g->mainloop);
|
|
|
|
stream = pa_stream_new (g->context, name, ss, map);
|
|
if (!stream) {
|
|
goto fail;
|
|
}
|
|
|
|
pa_stream_set_state_callback (stream, stream_state_cb, g);
|
|
pa_stream_set_read_callback (stream, stream_request_cb, g);
|
|
pa_stream_set_write_callback (stream, stream_request_cb, g);
|
|
|
|
if (dir == PA_STREAM_PLAYBACK) {
|
|
r = pa_stream_connect_playback (stream, dev, attr,
|
|
PA_STREAM_INTERPOLATE_TIMING
|
|
#ifdef PA_STREAM_ADJUST_LATENCY
|
|
|PA_STREAM_ADJUST_LATENCY
|
|
#endif
|
|
|PA_STREAM_AUTO_TIMING_UPDATE, NULL, NULL);
|
|
} else {
|
|
r = pa_stream_connect_record (stream, dev, attr,
|
|
PA_STREAM_INTERPOLATE_TIMING
|
|
#ifdef PA_STREAM_ADJUST_LATENCY
|
|
|PA_STREAM_ADJUST_LATENCY
|
|
#endif
|
|
|PA_STREAM_AUTO_TIMING_UPDATE);
|
|
}
|
|
|
|
if (r < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
|
|
return stream;
|
|
|
|
fail:
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
|
|
if (stream) {
|
|
pa_stream_unref (stream);
|
|
}
|
|
|
|
*rerror = pa_context_errno (g->context);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int qpa_init_out(HWVoiceOut *hw, struct audsettings *as,
|
|
void *drv_opaque)
|
|
{
|
|
int error;
|
|
pa_sample_spec ss;
|
|
pa_buffer_attr ba;
|
|
struct audsettings obt_as = *as;
|
|
PAVoiceOut *pa = (PAVoiceOut *) hw;
|
|
paaudio *g = pa->g = drv_opaque;
|
|
AudiodevPaOptions *popts = &g->dev->u.pa;
|
|
AudiodevPaPerDirectionOptions *ppdo = popts->out;
|
|
|
|
ss.format = audfmt_to_pa (as->fmt, as->endianness);
|
|
ss.channels = as->nchannels;
|
|
ss.rate = as->freq;
|
|
|
|
ba.tlength = pa_usec_to_bytes(ppdo->latency, &ss);
|
|
ba.minreq = -1;
|
|
ba.maxlength = -1;
|
|
ba.prebuf = -1;
|
|
|
|
obt_as.fmt = pa_to_audfmt (ss.format, &obt_as.endianness);
|
|
|
|
pa->stream = qpa_simple_new (
|
|
g,
|
|
"qemu",
|
|
PA_STREAM_PLAYBACK,
|
|
ppdo->has_name ? ppdo->name : NULL,
|
|
&ss,
|
|
NULL, /* channel map */
|
|
&ba, /* buffering attributes */
|
|
&error
|
|
);
|
|
if (!pa->stream) {
|
|
qpa_logerr (error, "pa_simple_new for playback failed\n");
|
|
goto fail1;
|
|
}
|
|
|
|
audio_pcm_init_info (&hw->info, &obt_as);
|
|
hw->samples = pa->samples = audio_buffer_samples(
|
|
qapi_AudiodevPaPerDirectionOptions_base(ppdo),
|
|
&obt_as, ppdo->buffer_length);
|
|
pa->pcm_buf = audio_calloc(__func__, hw->samples, 1 << hw->info.shift);
|
|
pa->rpos = hw->rpos;
|
|
if (!pa->pcm_buf) {
|
|
dolog ("Could not allocate buffer (%d bytes)\n",
|
|
hw->samples << hw->info.shift);
|
|
goto fail2;
|
|
}
|
|
|
|
if (audio_pt_init(&pa->pt, qpa_thread_out, hw, AUDIO_CAP, __func__)) {
|
|
goto fail3;
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail3:
|
|
g_free (pa->pcm_buf);
|
|
pa->pcm_buf = NULL;
|
|
fail2:
|
|
if (pa->stream) {
|
|
pa_stream_unref (pa->stream);
|
|
pa->stream = NULL;
|
|
}
|
|
fail1:
|
|
return -1;
|
|
}
|
|
|
|
static int qpa_init_in(HWVoiceIn *hw, struct audsettings *as, void *drv_opaque)
|
|
{
|
|
int error;
|
|
pa_sample_spec ss;
|
|
pa_buffer_attr ba;
|
|
struct audsettings obt_as = *as;
|
|
PAVoiceIn *pa = (PAVoiceIn *) hw;
|
|
paaudio *g = pa->g = drv_opaque;
|
|
AudiodevPaOptions *popts = &g->dev->u.pa;
|
|
AudiodevPaPerDirectionOptions *ppdo = popts->in;
|
|
|
|
ss.format = audfmt_to_pa (as->fmt, as->endianness);
|
|
ss.channels = as->nchannels;
|
|
ss.rate = as->freq;
|
|
|
|
ba.fragsize = pa_usec_to_bytes(ppdo->latency, &ss);
|
|
ba.maxlength = pa_usec_to_bytes(ppdo->latency * 2, &ss);
|
|
ba.minreq = -1;
|
|
ba.prebuf = -1;
|
|
|
|
obt_as.fmt = pa_to_audfmt (ss.format, &obt_as.endianness);
|
|
|
|
pa->stream = qpa_simple_new (
|
|
g,
|
|
"qemu",
|
|
PA_STREAM_RECORD,
|
|
ppdo->has_name ? ppdo->name : NULL,
|
|
&ss,
|
|
NULL, /* channel map */
|
|
&ba, /* buffering attributes */
|
|
&error
|
|
);
|
|
if (!pa->stream) {
|
|
qpa_logerr (error, "pa_simple_new for capture failed\n");
|
|
goto fail1;
|
|
}
|
|
|
|
audio_pcm_init_info (&hw->info, &obt_as);
|
|
hw->samples = pa->samples = audio_buffer_samples(
|
|
qapi_AudiodevPaPerDirectionOptions_base(ppdo),
|
|
&obt_as, ppdo->buffer_length);
|
|
pa->pcm_buf = audio_calloc(__func__, hw->samples, 1 << hw->info.shift);
|
|
pa->wpos = hw->wpos;
|
|
if (!pa->pcm_buf) {
|
|
dolog ("Could not allocate buffer (%d bytes)\n",
|
|
hw->samples << hw->info.shift);
|
|
goto fail2;
|
|
}
|
|
|
|
if (audio_pt_init(&pa->pt, qpa_thread_in, hw, AUDIO_CAP, __func__)) {
|
|
goto fail3;
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail3:
|
|
g_free (pa->pcm_buf);
|
|
pa->pcm_buf = NULL;
|
|
fail2:
|
|
if (pa->stream) {
|
|
pa_stream_unref (pa->stream);
|
|
pa->stream = NULL;
|
|
}
|
|
fail1:
|
|
return -1;
|
|
}
|
|
|
|
static void qpa_fini_out (HWVoiceOut *hw)
|
|
{
|
|
void *ret;
|
|
PAVoiceOut *pa = (PAVoiceOut *) hw;
|
|
|
|
audio_pt_lock(&pa->pt, __func__);
|
|
pa->done = 1;
|
|
audio_pt_unlock_and_signal(&pa->pt, __func__);
|
|
audio_pt_join(&pa->pt, &ret, __func__);
|
|
|
|
if (pa->stream) {
|
|
pa_stream_unref (pa->stream);
|
|
pa->stream = NULL;
|
|
}
|
|
|
|
audio_pt_fini(&pa->pt, __func__);
|
|
g_free (pa->pcm_buf);
|
|
pa->pcm_buf = NULL;
|
|
}
|
|
|
|
static void qpa_fini_in (HWVoiceIn *hw)
|
|
{
|
|
void *ret;
|
|
PAVoiceIn *pa = (PAVoiceIn *) hw;
|
|
|
|
audio_pt_lock(&pa->pt, __func__);
|
|
pa->done = 1;
|
|
audio_pt_unlock_and_signal(&pa->pt, __func__);
|
|
audio_pt_join(&pa->pt, &ret, __func__);
|
|
|
|
if (pa->stream) {
|
|
pa_stream_unref (pa->stream);
|
|
pa->stream = NULL;
|
|
}
|
|
|
|
audio_pt_fini(&pa->pt, __func__);
|
|
g_free (pa->pcm_buf);
|
|
pa->pcm_buf = NULL;
|
|
}
|
|
|
|
static int qpa_ctl_out (HWVoiceOut *hw, int cmd, ...)
|
|
{
|
|
PAVoiceOut *pa = (PAVoiceOut *) hw;
|
|
pa_operation *op;
|
|
pa_cvolume v;
|
|
paaudio *g = pa->g;
|
|
|
|
#ifdef PA_CHECK_VERSION /* macro is present in 0.9.16+ */
|
|
pa_cvolume_init (&v); /* function is present in 0.9.13+ */
|
|
#endif
|
|
|
|
switch (cmd) {
|
|
case VOICE_VOLUME:
|
|
{
|
|
SWVoiceOut *sw;
|
|
va_list ap;
|
|
|
|
va_start (ap, cmd);
|
|
sw = va_arg (ap, SWVoiceOut *);
|
|
va_end (ap);
|
|
|
|
v.channels = 2;
|
|
v.values[0] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * sw->vol.l) / UINT32_MAX;
|
|
v.values[1] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * sw->vol.r) / UINT32_MAX;
|
|
|
|
pa_threaded_mainloop_lock (g->mainloop);
|
|
|
|
op = pa_context_set_sink_input_volume (g->context,
|
|
pa_stream_get_index (pa->stream),
|
|
&v, NULL, NULL);
|
|
if (!op)
|
|
qpa_logerr (pa_context_errno (g->context),
|
|
"set_sink_input_volume() failed\n");
|
|
else
|
|
pa_operation_unref (op);
|
|
|
|
op = pa_context_set_sink_input_mute (g->context,
|
|
pa_stream_get_index (pa->stream),
|
|
sw->vol.mute, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr (pa_context_errno (g->context),
|
|
"set_sink_input_mute() failed\n");
|
|
} else {
|
|
pa_operation_unref (op);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int qpa_ctl_in (HWVoiceIn *hw, int cmd, ...)
|
|
{
|
|
PAVoiceIn *pa = (PAVoiceIn *) hw;
|
|
pa_operation *op;
|
|
pa_cvolume v;
|
|
paaudio *g = pa->g;
|
|
|
|
#ifdef PA_CHECK_VERSION
|
|
pa_cvolume_init (&v);
|
|
#endif
|
|
|
|
switch (cmd) {
|
|
case VOICE_VOLUME:
|
|
{
|
|
SWVoiceIn *sw;
|
|
va_list ap;
|
|
|
|
va_start (ap, cmd);
|
|
sw = va_arg (ap, SWVoiceIn *);
|
|
va_end (ap);
|
|
|
|
v.channels = 2;
|
|
v.values[0] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * sw->vol.l) / UINT32_MAX;
|
|
v.values[1] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * sw->vol.r) / UINT32_MAX;
|
|
|
|
pa_threaded_mainloop_lock (g->mainloop);
|
|
|
|
op = pa_context_set_source_output_volume (g->context,
|
|
pa_stream_get_index (pa->stream),
|
|
&v, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr (pa_context_errno (g->context),
|
|
"set_source_output_volume() failed\n");
|
|
} else {
|
|
pa_operation_unref(op);
|
|
}
|
|
|
|
op = pa_context_set_source_output_mute (g->context,
|
|
pa_stream_get_index (pa->stream),
|
|
sw->vol.mute, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr (pa_context_errno (g->context),
|
|
"set_source_output_mute() failed\n");
|
|
} else {
|
|
pa_operation_unref (op);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int qpa_validate_per_direction_opts(Audiodev *dev,
|
|
AudiodevPaPerDirectionOptions *pdo)
|
|
{
|
|
if (!pdo->has_buffer_length) {
|
|
pdo->has_buffer_length = true;
|
|
pdo->buffer_length = 46440;
|
|
}
|
|
if (!pdo->has_latency) {
|
|
pdo->has_latency = true;
|
|
pdo->latency = 15000;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void *qpa_audio_init(Audiodev *dev)
|
|
{
|
|
paaudio *g;
|
|
AudiodevPaOptions *popts = &dev->u.pa;
|
|
const char *server;
|
|
|
|
if (!popts->has_server) {
|
|
char pidfile[64];
|
|
char *runtime;
|
|
struct stat st;
|
|
|
|
runtime = getenv("XDG_RUNTIME_DIR");
|
|
if (!runtime) {
|
|
return NULL;
|
|
}
|
|
snprintf(pidfile, sizeof(pidfile), "%s/pulse/pid", runtime);
|
|
if (stat(pidfile, &st) != 0) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
assert(dev->driver == AUDIODEV_DRIVER_PA);
|
|
|
|
g = g_malloc(sizeof(paaudio));
|
|
server = popts->has_server ? popts->server : NULL;
|
|
|
|
if (!qpa_validate_per_direction_opts(dev, popts->in)) {
|
|
goto fail;
|
|
}
|
|
if (!qpa_validate_per_direction_opts(dev, popts->out)) {
|
|
goto fail;
|
|
}
|
|
|
|
g->dev = dev;
|
|
g->mainloop = NULL;
|
|
g->context = NULL;
|
|
|
|
g->mainloop = pa_threaded_mainloop_new ();
|
|
if (!g->mainloop) {
|
|
goto fail;
|
|
}
|
|
|
|
g->context = pa_context_new (pa_threaded_mainloop_get_api (g->mainloop),
|
|
server);
|
|
if (!g->context) {
|
|
goto fail;
|
|
}
|
|
|
|
pa_context_set_state_callback (g->context, context_state_cb, g);
|
|
|
|
if (pa_context_connect(g->context, server, 0, NULL) < 0) {
|
|
qpa_logerr (pa_context_errno (g->context),
|
|
"pa_context_connect() failed\n");
|
|
goto fail;
|
|
}
|
|
|
|
pa_threaded_mainloop_lock (g->mainloop);
|
|
|
|
if (pa_threaded_mainloop_start (g->mainloop) < 0) {
|
|
goto unlock_and_fail;
|
|
}
|
|
|
|
for (;;) {
|
|
pa_context_state_t state;
|
|
|
|
state = pa_context_get_state (g->context);
|
|
|
|
if (state == PA_CONTEXT_READY) {
|
|
break;
|
|
}
|
|
|
|
if (!PA_CONTEXT_IS_GOOD (state)) {
|
|
qpa_logerr (pa_context_errno (g->context),
|
|
"Wrong context state\n");
|
|
goto unlock_and_fail;
|
|
}
|
|
|
|
/* Wait until the context is ready */
|
|
pa_threaded_mainloop_wait (g->mainloop);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
|
|
return g;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock (g->mainloop);
|
|
fail:
|
|
AUD_log (AUDIO_CAP, "Failed to initialize PA context");
|
|
qpa_audio_fini(g);
|
|
return NULL;
|
|
}
|
|
|
|
static void qpa_audio_fini (void *opaque)
|
|
{
|
|
paaudio *g = opaque;
|
|
|
|
if (g->mainloop) {
|
|
pa_threaded_mainloop_stop (g->mainloop);
|
|
}
|
|
|
|
if (g->context) {
|
|
pa_context_disconnect (g->context);
|
|
pa_context_unref (g->context);
|
|
}
|
|
|
|
if (g->mainloop) {
|
|
pa_threaded_mainloop_free (g->mainloop);
|
|
}
|
|
|
|
g_free(g);
|
|
}
|
|
|
|
static struct audio_pcm_ops qpa_pcm_ops = {
|
|
.init_out = qpa_init_out,
|
|
.fini_out = qpa_fini_out,
|
|
.run_out = qpa_run_out,
|
|
.write = qpa_write,
|
|
.ctl_out = qpa_ctl_out,
|
|
|
|
.init_in = qpa_init_in,
|
|
.fini_in = qpa_fini_in,
|
|
.run_in = qpa_run_in,
|
|
.read = qpa_read,
|
|
.ctl_in = qpa_ctl_in
|
|
};
|
|
|
|
static struct audio_driver pa_audio_driver = {
|
|
.name = "pa",
|
|
.descr = "http://www.pulseaudio.org/",
|
|
.init = qpa_audio_init,
|
|
.fini = qpa_audio_fini,
|
|
.pcm_ops = &qpa_pcm_ops,
|
|
.can_be_default = 1,
|
|
.max_voices_out = INT_MAX,
|
|
.max_voices_in = INT_MAX,
|
|
.voice_size_out = sizeof (PAVoiceOut),
|
|
.voice_size_in = sizeof (PAVoiceIn),
|
|
.ctl_caps = VOICE_VOLUME_CAP
|
|
};
|
|
|
|
static void register_audio_pa(void)
|
|
{
|
|
audio_driver_register(&pa_audio_driver);
|
|
}
|
|
type_init(register_audio_pa);
|