mirror of
https://github.com/xemu-project/xemu.git
synced 2024-11-27 13:30:52 +00:00
30ff5e24a3
This is a patch to improve the pulseaudio playback experience. Asking pulseaudio for a playback latency of 15ms is quite demanding. Increase this to 46ms. The total playback latency now is 31ms larger. One of the next patches will reduce the total playback latency again by more than 46ms. Here is a quote from the PulseAudio Latency Control documentation: 'For the sake of (...) drop-out safety always make sure to pick the highest latency possible that fulfills your needs.' Signed-off-by: Volker Rümelin <vr_qemu@t-online.de> Message-Id: <20220301191311.26695-5-vr_qemu@t-online.de> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
934 lines
24 KiB
C
934 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"
|
|
|
|
typedef struct PAConnection {
|
|
char *server;
|
|
int refcount;
|
|
QTAILQ_ENTRY(PAConnection) list;
|
|
|
|
pa_threaded_mainloop *mainloop;
|
|
pa_context *context;
|
|
} PAConnection;
|
|
|
|
static QTAILQ_HEAD(PAConnectionHead, PAConnection) pa_conns =
|
|
QTAILQ_HEAD_INITIALIZER(pa_conns);
|
|
|
|
typedef struct {
|
|
Audiodev *dev;
|
|
PAConnection *conn;
|
|
} paaudio;
|
|
|
|
typedef struct {
|
|
HWVoiceOut hw;
|
|
pa_stream *stream;
|
|
paaudio *g;
|
|
} PAVoiceOut;
|
|
|
|
typedef struct {
|
|
HWVoiceIn hw;
|
|
pa_stream *stream;
|
|
const void *read_data;
|
|
size_t read_length;
|
|
paaudio *g;
|
|
} PAVoiceIn;
|
|
|
|
static void qpa_conn_fini(PAConnection *c);
|
|
|
|
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, expression, label, msg) \
|
|
do { \
|
|
if (!(expression)) { \
|
|
qpa_logerr(pa_context_errno((c)->context), msg); \
|
|
goto label; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define CHECK_DEAD_GOTO(c, stream, label, msg) \
|
|
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)) { \
|
|
qpa_logerr(pa_context_errno((c)->context), msg); \
|
|
} else { \
|
|
qpa_logerr(PA_ERR_BADSTATE, msg); \
|
|
} \
|
|
goto label; \
|
|
} \
|
|
} while (0)
|
|
|
|
static void *qpa_get_buffer_in(HWVoiceIn *hw, size_t *size)
|
|
{
|
|
PAVoiceIn *p = (PAVoiceIn *) hw;
|
|
PAConnection *c = p->g->conn;
|
|
int r;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail,
|
|
"pa_threaded_mainloop_lock failed\n");
|
|
|
|
if (!p->read_length) {
|
|
r = pa_stream_peek(p->stream, &p->read_data, &p->read_length);
|
|
CHECK_SUCCESS_GOTO(c, r == 0, unlock_and_fail,
|
|
"pa_stream_peek failed\n");
|
|
}
|
|
|
|
*size = MIN(p->read_length, *size);
|
|
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return (void *) p->read_data;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
*size = 0;
|
|
return NULL;
|
|
}
|
|
|
|
static void qpa_put_buffer_in(HWVoiceIn *hw, void *buf, size_t size)
|
|
{
|
|
PAVoiceIn *p = (PAVoiceIn *) hw;
|
|
PAConnection *c = p->g->conn;
|
|
int r;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
CHECK_DEAD_GOTO(c, p->stream, unlock,
|
|
"pa_threaded_mainloop_lock failed\n");
|
|
|
|
assert(buf == p->read_data && size <= p->read_length);
|
|
|
|
p->read_data += size;
|
|
p->read_length -= size;
|
|
|
|
if (size && !p->read_length) {
|
|
r = pa_stream_drop(p->stream);
|
|
CHECK_SUCCESS_GOTO(c, r == 0, unlock, "pa_stream_drop failed\n");
|
|
}
|
|
|
|
unlock:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
}
|
|
|
|
static size_t qpa_read(HWVoiceIn *hw, void *data, size_t length)
|
|
{
|
|
PAVoiceIn *p = (PAVoiceIn *) hw;
|
|
PAConnection *c = p->g->conn;
|
|
size_t total = 0;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail,
|
|
"pa_threaded_mainloop_lock failed\n");
|
|
if (pa_stream_get_state(p->stream) != PA_STREAM_READY) {
|
|
/* wait for stream to become ready */
|
|
goto unlock;
|
|
}
|
|
|
|
while (total < length) {
|
|
size_t l;
|
|
int r;
|
|
|
|
if (!p->read_length) {
|
|
r = pa_stream_peek(p->stream, &p->read_data, &p->read_length);
|
|
CHECK_SUCCESS_GOTO(c, r == 0, unlock_and_fail,
|
|
"pa_stream_peek failed\n");
|
|
if (!p->read_length) {
|
|
/* buffer is empty */
|
|
break;
|
|
}
|
|
}
|
|
|
|
l = MIN(p->read_length, length - total);
|
|
memcpy((char *)data + total, p->read_data, l);
|
|
|
|
p->read_data += l;
|
|
p->read_length -= l;
|
|
total += l;
|
|
|
|
if (!p->read_length) {
|
|
r = pa_stream_drop(p->stream);
|
|
CHECK_SUCCESS_GOTO(c, r == 0, unlock_and_fail,
|
|
"pa_stream_drop failed\n");
|
|
}
|
|
}
|
|
|
|
unlock:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return total;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return 0;
|
|
}
|
|
|
|
static void *qpa_get_buffer_out(HWVoiceOut *hw, size_t *size)
|
|
{
|
|
PAVoiceOut *p = (PAVoiceOut *) hw;
|
|
PAConnection *c = p->g->conn;
|
|
void *ret;
|
|
size_t l;
|
|
int r;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail,
|
|
"pa_threaded_mainloop_lock failed\n");
|
|
if (pa_stream_get_state(p->stream) != PA_STREAM_READY) {
|
|
/* wait for stream to become ready */
|
|
l = 0;
|
|
ret = NULL;
|
|
goto unlock;
|
|
}
|
|
|
|
l = pa_stream_writable_size(p->stream);
|
|
CHECK_SUCCESS_GOTO(c, l != (size_t) -1, unlock_and_fail,
|
|
"pa_stream_writable_size failed\n");
|
|
|
|
*size = -1;
|
|
r = pa_stream_begin_write(p->stream, &ret, size);
|
|
CHECK_SUCCESS_GOTO(c, r >= 0, unlock_and_fail,
|
|
"pa_stream_begin_write failed\n");
|
|
|
|
unlock:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
if (*size > l) {
|
|
*size = l;
|
|
}
|
|
return ret;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
*size = 0;
|
|
return NULL;
|
|
}
|
|
|
|
static size_t qpa_put_buffer_out(HWVoiceOut *hw, void *data, size_t length)
|
|
{
|
|
PAVoiceOut *p = (PAVoiceOut *)hw;
|
|
PAConnection *c = p->g->conn;
|
|
int r;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail,
|
|
"pa_threaded_mainloop_lock failed\n");
|
|
|
|
r = pa_stream_write(p->stream, data, length, NULL, 0LL, PA_SEEK_RELATIVE);
|
|
CHECK_SUCCESS_GOTO(c, r >= 0, unlock_and_fail, "pa_stream_write failed\n");
|
|
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return length;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return 0;
|
|
}
|
|
|
|
static size_t qpa_write(HWVoiceOut *hw, void *data, size_t length)
|
|
{
|
|
PAVoiceOut *p = (PAVoiceOut *) hw;
|
|
PAConnection *c = p->g->conn;
|
|
size_t l;
|
|
int r;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
CHECK_DEAD_GOTO(c, p->stream, unlock_and_fail,
|
|
"pa_threaded_mainloop_lock failed\n");
|
|
if (pa_stream_get_state(p->stream) != PA_STREAM_READY) {
|
|
/* wait for stream to become ready */
|
|
l = 0;
|
|
goto unlock;
|
|
}
|
|
|
|
l = pa_stream_writable_size(p->stream);
|
|
|
|
CHECK_SUCCESS_GOTO(c, l != (size_t) -1, unlock_and_fail,
|
|
"pa_stream_writable_size failed\n");
|
|
|
|
if (l > length) {
|
|
l = length;
|
|
}
|
|
|
|
r = pa_stream_write(p->stream, data, l, NULL, 0LL, PA_SEEK_RELATIVE);
|
|
CHECK_SUCCESS_GOTO(c, r >= 0, unlock_and_fail, "pa_stream_write failed\n");
|
|
|
|
unlock:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return l;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
case AUDIO_FORMAT_F32:
|
|
format = endianness ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE;
|
|
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;
|
|
case PA_SAMPLE_FLOAT32BE:
|
|
*endianness = 1;
|
|
return AUDIO_FORMAT_F32;
|
|
case PA_SAMPLE_FLOAT32LE:
|
|
*endianness = 0;
|
|
return AUDIO_FORMAT_F32;
|
|
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)
|
|
{
|
|
PAConnection *conn = userdata;
|
|
|
|
switch (pa_context_get_state(c)) {
|
|
case PA_CONTEXT_READY:
|
|
case PA_CONTEXT_TERMINATED:
|
|
case PA_CONTEXT_FAILED:
|
|
pa_threaded_mainloop_signal(conn->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)
|
|
{
|
|
PAConnection *c = userdata;
|
|
|
|
switch (pa_stream_get_state (s)) {
|
|
|
|
case PA_STREAM_READY:
|
|
case PA_STREAM_FAILED:
|
|
case PA_STREAM_TERMINATED:
|
|
pa_threaded_mainloop_signal(c->mainloop, 0);
|
|
break;
|
|
|
|
case PA_STREAM_UNCONNECTED:
|
|
case PA_STREAM_CREATING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static pa_stream *qpa_simple_new (
|
|
PAConnection *c,
|
|
const char *name,
|
|
pa_stream_direction_t dir,
|
|
const char *dev,
|
|
const pa_sample_spec *ss,
|
|
const pa_buffer_attr *attr,
|
|
int *rerror)
|
|
{
|
|
int r;
|
|
pa_stream *stream = NULL;
|
|
pa_stream_flags_t flags;
|
|
pa_channel_map map;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
pa_channel_map_init(&map);
|
|
map.channels = ss->channels;
|
|
|
|
/*
|
|
* TODO: This currently expects the only frontend supporting more than 2
|
|
* channels is the usb-audio. We will need some means to set channel
|
|
* order when a new frontend gains multi-channel support.
|
|
*/
|
|
switch (ss->channels) {
|
|
case 1:
|
|
map.map[0] = PA_CHANNEL_POSITION_MONO;
|
|
break;
|
|
|
|
case 2:
|
|
map.map[0] = PA_CHANNEL_POSITION_LEFT;
|
|
map.map[1] = PA_CHANNEL_POSITION_RIGHT;
|
|
break;
|
|
|
|
case 6:
|
|
map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
|
|
map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
|
|
map.map[2] = PA_CHANNEL_POSITION_CENTER;
|
|
map.map[3] = PA_CHANNEL_POSITION_LFE;
|
|
map.map[4] = PA_CHANNEL_POSITION_REAR_LEFT;
|
|
map.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT;
|
|
break;
|
|
|
|
case 8:
|
|
map.map[0] = PA_CHANNEL_POSITION_FRONT_LEFT;
|
|
map.map[1] = PA_CHANNEL_POSITION_FRONT_RIGHT;
|
|
map.map[2] = PA_CHANNEL_POSITION_CENTER;
|
|
map.map[3] = PA_CHANNEL_POSITION_LFE;
|
|
map.map[4] = PA_CHANNEL_POSITION_REAR_LEFT;
|
|
map.map[5] = PA_CHANNEL_POSITION_REAR_RIGHT;
|
|
map.map[6] = PA_CHANNEL_POSITION_SIDE_LEFT;
|
|
map.map[7] = PA_CHANNEL_POSITION_SIDE_RIGHT;
|
|
break;
|
|
|
|
default:
|
|
dolog("Internal error: unsupported channel count %d\n", ss->channels);
|
|
goto fail;
|
|
}
|
|
|
|
stream = pa_stream_new(c->context, name, ss, &map);
|
|
if (!stream) {
|
|
goto fail;
|
|
}
|
|
|
|
pa_stream_set_state_callback(stream, stream_state_cb, c);
|
|
|
|
flags = PA_STREAM_EARLY_REQUESTS;
|
|
|
|
if (dev) {
|
|
/* don't move the stream if the user specified a sink/source */
|
|
flags |= PA_STREAM_DONT_MOVE;
|
|
}
|
|
|
|
if (dir == PA_STREAM_PLAYBACK) {
|
|
r = pa_stream_connect_playback(stream, dev, attr, flags, NULL, NULL);
|
|
} else {
|
|
r = pa_stream_connect_record(stream, dev, attr, flags);
|
|
}
|
|
|
|
if (r < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
|
|
return stream;
|
|
|
|
fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
|
|
if (stream) {
|
|
pa_stream_unref (stream);
|
|
}
|
|
|
|
*rerror = pa_context_errno(c->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;
|
|
PAConnection *c = g->conn;
|
|
|
|
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 = pa_usec_to_bytes(MIN(ppdo->latency >> 2,
|
|
(g->dev->timer_period >> 2) * 3), &ss);
|
|
ba.maxlength = -1;
|
|
ba.prebuf = -1;
|
|
|
|
obt_as.fmt = pa_to_audfmt (ss.format, &obt_as.endianness);
|
|
|
|
pa->stream = qpa_simple_new (
|
|
c,
|
|
ppdo->has_stream_name ? ppdo->stream_name : g->dev->id,
|
|
PA_STREAM_PLAYBACK,
|
|
ppdo->has_name ? ppdo->name : NULL,
|
|
&ss,
|
|
&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);
|
|
/*
|
|
* This is wrong. hw->samples counts in frames. hw->samples will be
|
|
* number of channels times larger than expected.
|
|
*/
|
|
hw->samples = audio_buffer_samples(
|
|
qapi_AudiodevPaPerDirectionOptions_base(ppdo), &obt_as, 46440);
|
|
|
|
return 0;
|
|
|
|
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;
|
|
PAConnection *c = g->conn;
|
|
|
|
ss.format = audfmt_to_pa (as->fmt, as->endianness);
|
|
ss.channels = as->nchannels;
|
|
ss.rate = as->freq;
|
|
|
|
ba.fragsize = pa_usec_to_bytes((g->dev->timer_period >> 1) * 3, &ss);
|
|
ba.maxlength = pa_usec_to_bytes(
|
|
MAX(ppdo->latency, g->dev->timer_period * 3), &ss);
|
|
ba.minreq = -1;
|
|
ba.prebuf = -1;
|
|
|
|
obt_as.fmt = pa_to_audfmt (ss.format, &obt_as.endianness);
|
|
|
|
pa->stream = qpa_simple_new (
|
|
c,
|
|
ppdo->has_stream_name ? ppdo->stream_name : g->dev->id,
|
|
PA_STREAM_RECORD,
|
|
ppdo->has_name ? ppdo->name : NULL,
|
|
&ss,
|
|
&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);
|
|
/*
|
|
* This is wrong. hw->samples counts in frames. hw->samples will be
|
|
* number of channels times larger than expected.
|
|
*/
|
|
hw->samples = audio_buffer_samples(
|
|
qapi_AudiodevPaPerDirectionOptions_base(ppdo), &obt_as, 46440);
|
|
|
|
return 0;
|
|
|
|
fail1:
|
|
return -1;
|
|
}
|
|
|
|
static void qpa_simple_disconnect(PAConnection *c, pa_stream *stream)
|
|
{
|
|
int err;
|
|
|
|
/*
|
|
* wait until actually connects. workaround pa bug #247
|
|
* https://gitlab.freedesktop.org/pulseaudio/pulseaudio/issues/247
|
|
*/
|
|
while (pa_stream_get_state(stream) == PA_STREAM_CREATING) {
|
|
pa_threaded_mainloop_wait(c->mainloop);
|
|
}
|
|
|
|
err = pa_stream_disconnect(stream);
|
|
if (err != 0) {
|
|
dolog("Failed to disconnect! err=%d\n", err);
|
|
}
|
|
pa_stream_unref(stream);
|
|
}
|
|
|
|
static void qpa_fini_out (HWVoiceOut *hw)
|
|
{
|
|
PAVoiceOut *pa = (PAVoiceOut *) hw;
|
|
|
|
if (pa->stream) {
|
|
PAConnection *c = pa->g->conn;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
qpa_simple_disconnect(c, pa->stream);
|
|
pa->stream = NULL;
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
}
|
|
}
|
|
|
|
static void qpa_fini_in (HWVoiceIn *hw)
|
|
{
|
|
PAVoiceIn *pa = (PAVoiceIn *) hw;
|
|
|
|
if (pa->stream) {
|
|
PAConnection *c = pa->g->conn;
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
if (pa->read_length) {
|
|
int r = pa_stream_drop(pa->stream);
|
|
if (r) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"pa_stream_drop failed\n");
|
|
}
|
|
pa->read_length = 0;
|
|
}
|
|
qpa_simple_disconnect(c, pa->stream);
|
|
pa->stream = NULL;
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
}
|
|
}
|
|
|
|
static void qpa_volume_out(HWVoiceOut *hw, Volume *vol)
|
|
{
|
|
PAVoiceOut *pa = (PAVoiceOut *) hw;
|
|
pa_operation *op;
|
|
pa_cvolume v;
|
|
PAConnection *c = pa->g->conn;
|
|
int i;
|
|
|
|
#ifdef PA_CHECK_VERSION /* macro is present in 0.9.16+ */
|
|
pa_cvolume_init (&v); /* function is present in 0.9.13+ */
|
|
#endif
|
|
|
|
v.channels = vol->channels;
|
|
for (i = 0; i < vol->channels; ++i) {
|
|
v.values[i] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * vol->vol[i]) / 255;
|
|
}
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
op = pa_context_set_sink_input_volume(c->context,
|
|
pa_stream_get_index(pa->stream),
|
|
&v, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"set_sink_input_volume() failed\n");
|
|
} else {
|
|
pa_operation_unref(op);
|
|
}
|
|
|
|
op = pa_context_set_sink_input_mute(c->context,
|
|
pa_stream_get_index(pa->stream),
|
|
vol->mute, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"set_sink_input_mute() failed\n");
|
|
} else {
|
|
pa_operation_unref(op);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
}
|
|
|
|
static void qpa_volume_in(HWVoiceIn *hw, Volume *vol)
|
|
{
|
|
PAVoiceIn *pa = (PAVoiceIn *) hw;
|
|
pa_operation *op;
|
|
pa_cvolume v;
|
|
PAConnection *c = pa->g->conn;
|
|
int i;
|
|
|
|
#ifdef PA_CHECK_VERSION
|
|
pa_cvolume_init (&v);
|
|
#endif
|
|
|
|
v.channels = vol->channels;
|
|
for (i = 0; i < vol->channels; ++i) {
|
|
v.values[i] = ((PA_VOLUME_NORM - PA_VOLUME_MUTED) * vol->vol[i]) / 255;
|
|
}
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
op = pa_context_set_source_output_volume(c->context,
|
|
pa_stream_get_index(pa->stream),
|
|
&v, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"set_source_output_volume() failed\n");
|
|
} else {
|
|
pa_operation_unref(op);
|
|
}
|
|
|
|
op = pa_context_set_source_output_mute(c->context,
|
|
pa_stream_get_index(pa->stream),
|
|
vol->mute, NULL, NULL);
|
|
if (!op) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"set_source_output_mute() failed\n");
|
|
} else {
|
|
pa_operation_unref(op);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
}
|
|
|
|
static int qpa_validate_per_direction_opts(Audiodev *dev,
|
|
AudiodevPaPerDirectionOptions *pdo)
|
|
{
|
|
if (!pdo->has_latency) {
|
|
pdo->has_latency = true;
|
|
pdo->latency = 46440;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* common */
|
|
static void *qpa_conn_init(const char *server)
|
|
{
|
|
PAConnection *c = g_malloc0(sizeof(PAConnection));
|
|
QTAILQ_INSERT_TAIL(&pa_conns, c, list);
|
|
|
|
c->mainloop = pa_threaded_mainloop_new();
|
|
if (!c->mainloop) {
|
|
goto fail;
|
|
}
|
|
|
|
c->context = pa_context_new(pa_threaded_mainloop_get_api(c->mainloop),
|
|
audio_application_name());
|
|
if (!c->context) {
|
|
goto fail;
|
|
}
|
|
|
|
pa_context_set_state_callback(c->context, context_state_cb, c);
|
|
|
|
if (pa_context_connect(c->context, server, 0, NULL) < 0) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"pa_context_connect() failed\n");
|
|
goto fail;
|
|
}
|
|
|
|
pa_threaded_mainloop_lock(c->mainloop);
|
|
|
|
if (pa_threaded_mainloop_start(c->mainloop) < 0) {
|
|
goto unlock_and_fail;
|
|
}
|
|
|
|
for (;;) {
|
|
pa_context_state_t state;
|
|
|
|
state = pa_context_get_state(c->context);
|
|
|
|
if (state == PA_CONTEXT_READY) {
|
|
break;
|
|
}
|
|
|
|
if (!PA_CONTEXT_IS_GOOD(state)) {
|
|
qpa_logerr(pa_context_errno(c->context),
|
|
"Wrong context state\n");
|
|
goto unlock_and_fail;
|
|
}
|
|
|
|
/* Wait until the context is ready */
|
|
pa_threaded_mainloop_wait(c->mainloop);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
return c;
|
|
|
|
unlock_and_fail:
|
|
pa_threaded_mainloop_unlock(c->mainloop);
|
|
fail:
|
|
AUD_log (AUDIO_CAP, "Failed to initialize PA context");
|
|
qpa_conn_fini(c);
|
|
return NULL;
|
|
}
|
|
|
|
static void *qpa_audio_init(Audiodev *dev)
|
|
{
|
|
paaudio *g;
|
|
AudiodevPaOptions *popts = &dev->u.pa;
|
|
const char *server;
|
|
PAConnection *c;
|
|
|
|
assert(dev->driver == AUDIODEV_DRIVER_PA);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (!qpa_validate_per_direction_opts(dev, popts->in)) {
|
|
return NULL;
|
|
}
|
|
if (!qpa_validate_per_direction_opts(dev, popts->out)) {
|
|
return NULL;
|
|
}
|
|
|
|
g = g_malloc0(sizeof(paaudio));
|
|
server = popts->has_server ? popts->server : NULL;
|
|
|
|
g->dev = dev;
|
|
|
|
QTAILQ_FOREACH(c, &pa_conns, list) {
|
|
if (server == NULL || c->server == NULL ?
|
|
server == c->server :
|
|
strcmp(server, c->server) == 0) {
|
|
g->conn = c;
|
|
break;
|
|
}
|
|
}
|
|
if (!g->conn) {
|
|
g->conn = qpa_conn_init(server);
|
|
}
|
|
if (!g->conn) {
|
|
g_free(g);
|
|
return NULL;
|
|
}
|
|
|
|
++g->conn->refcount;
|
|
return g;
|
|
}
|
|
|
|
static void qpa_conn_fini(PAConnection *c)
|
|
{
|
|
if (c->mainloop) {
|
|
pa_threaded_mainloop_stop(c->mainloop);
|
|
}
|
|
|
|
if (c->context) {
|
|
pa_context_disconnect(c->context);
|
|
pa_context_unref(c->context);
|
|
}
|
|
|
|
if (c->mainloop) {
|
|
pa_threaded_mainloop_free(c->mainloop);
|
|
}
|
|
|
|
QTAILQ_REMOVE(&pa_conns, c, list);
|
|
g_free(c);
|
|
}
|
|
|
|
static void qpa_audio_fini (void *opaque)
|
|
{
|
|
paaudio *g = opaque;
|
|
PAConnection *c = g->conn;
|
|
|
|
if (--c->refcount == 0) {
|
|
qpa_conn_fini(c);
|
|
}
|
|
|
|
g_free(g);
|
|
}
|
|
|
|
static struct audio_pcm_ops qpa_pcm_ops = {
|
|
.init_out = qpa_init_out,
|
|
.fini_out = qpa_fini_out,
|
|
.write = qpa_write,
|
|
.get_buffer_out = qpa_get_buffer_out,
|
|
.put_buffer_out = qpa_put_buffer_out,
|
|
.volume_out = qpa_volume_out,
|
|
|
|
.init_in = qpa_init_in,
|
|
.fini_in = qpa_fini_in,
|
|
.read = qpa_read,
|
|
.get_buffer_in = qpa_get_buffer_in,
|
|
.put_buffer_in = qpa_put_buffer_in,
|
|
.volume_in = qpa_volume_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),
|
|
};
|
|
|
|
static void register_audio_pa(void)
|
|
{
|
|
audio_driver_register(&pa_audio_driver);
|
|
}
|
|
type_init(register_audio_pa);
|