371 lines
8.5 KiB
C++
371 lines
8.5 KiB
C++
#ifdef USE_PULSEAUDIO
|
|
#include "audiostream.h"
|
|
|
|
#ifdef PULSEAUDIO_SIMPLE
|
|
#include <pulse/simple.h>
|
|
#include <pulse/error.h>
|
|
|
|
static pa_simple *pulse_stream;
|
|
static pa_simple *pulse_record;
|
|
|
|
static void pulseaudio_simple_init()
|
|
{
|
|
pa_sample_spec ss;
|
|
ss.format = PA_SAMPLE_S16LE;
|
|
ss.channels = 2;
|
|
ss.rate = 44100;
|
|
|
|
/* Create a new playback stream */
|
|
int error;
|
|
pulse_stream = pa_simple_new(NULL, "flycast", PA_STREAM_PLAYBACK, NULL, "flycast", &ss, NULL, NULL, &error);
|
|
if (!pulse_stream)
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_simple_new failed: %s", pa_strerror(error));
|
|
}
|
|
|
|
static u32 pulseaudio_simple_push(const void* frame, u32 samples, bool wait)
|
|
{
|
|
if (pa_simple_write(pulse_stream, frame, (size_t)samples * 4, NULL) < 0)
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_simple_write() failed!");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void pulseaudio_simple_term()
|
|
{
|
|
if (pulse_stream != NULL)
|
|
{
|
|
// Make sure that every single sample was played
|
|
if (pa_simple_drain(pulse_stream, NULL) < 0)
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_simple_drain() failed!");
|
|
pa_simple_free(pulse_stream);
|
|
}
|
|
}
|
|
|
|
static bool pulseaudio_init_record(u32 sampling_freq)
|
|
{
|
|
static const pa_sample_spec ss = {
|
|
.format = PA_SAMPLE_S16LE,
|
|
.rate = sampling_freq,
|
|
.channels = 1
|
|
};
|
|
int error;
|
|
pulse_record = pa_simple_new(NULL, "flycast", PA_STREAM_RECORD, NULL, "flycast", &ss, NULL, NULL, &error);
|
|
if (pulse_record == nullptr)
|
|
{
|
|
INFO_LOG(AUDIO, "PulseAudio: pa_simple_new() failed: %s", pa_strerror(error));
|
|
return false;
|
|
}
|
|
INFO_LOG(AUDIO, "PulseAudio: Successfully initialized capture device");
|
|
|
|
return true;
|
|
}
|
|
|
|
static void pulseaudio_term_record()
|
|
{
|
|
if (pulse_record != nullptr)
|
|
{
|
|
pa_simple_free(pulse_record);
|
|
pulse_record = nullptr;
|
|
}
|
|
}
|
|
|
|
static u32 pulseaudio_record(void *buffer, u32 samples)
|
|
{
|
|
if (pulse_record == nullptr)
|
|
return 0;
|
|
int error;
|
|
if (pa_simple_read(pulse_record, buffer, samples * 2, &error) < 0)
|
|
{
|
|
INFO_LOG(AUDIO, "PulseAudio: pa_simple_read() failed: %s", pa_strerror(error));
|
|
return 0;
|
|
}
|
|
return samples;
|
|
}
|
|
|
|
static audiobackend_t audiobackend_pulseaudio = {
|
|
"pulse", // Slug
|
|
"PulseAudio", // Name
|
|
&pulseaudio_simple_init,
|
|
&pulseaudio_simple_push,
|
|
&pulseaudio_simple_term,
|
|
NULL,
|
|
&pulseaudio_init_record,
|
|
&pulseaudio_record,
|
|
&pulseaudio_term_record,
|
|
};
|
|
|
|
#else // !PULSEAUDIO_SIMPLE
|
|
|
|
#include <pulse/pulseaudio.h>
|
|
|
|
pa_threaded_mainloop *mainloop;
|
|
pa_context *context;
|
|
pa_stream *stream;
|
|
pa_stream *record_stream;
|
|
|
|
static void context_state_cb(pa_context *c, void *userdata)
|
|
{
|
|
switch (pa_context_get_state(c))
|
|
{
|
|
case PA_CONTEXT_READY:
|
|
case PA_CONTEXT_TERMINATED:
|
|
case PA_CONTEXT_FAILED:
|
|
pa_threaded_mainloop_signal(mainloop, 0);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static void stream_state_cb(pa_stream *s, void *data)
|
|
{
|
|
switch (pa_stream_get_state(s))
|
|
{
|
|
case PA_STREAM_READY:
|
|
case PA_STREAM_FAILED:
|
|
case PA_STREAM_TERMINATED:
|
|
pa_threaded_mainloop_signal(mainloop, 0);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void stream_latency_update_cb(pa_stream *s, void *data)
|
|
{
|
|
pa_threaded_mainloop_signal(mainloop, 0);
|
|
}
|
|
|
|
static void underrun_update_cb(pa_stream *s, void *data)
|
|
{
|
|
//DEBUG_LOG(AUDIO, "PulseAudio: buffer underrun");
|
|
}
|
|
|
|
static void buffer_attr_cb(pa_stream *s, void *data)
|
|
{
|
|
const pa_buffer_attr *server_attr = pa_stream_get_buffer_attr(s);
|
|
if (server_attr)
|
|
{
|
|
u32 buffer_size = server_attr->tlength;
|
|
DEBUG_LOG(AUDIO, "PulseAudio: new buffer size %d", buffer_size);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void stream_request_cb(pa_stream *s, size_t length, void *data)
|
|
{
|
|
pa_threaded_mainloop_signal(mainloop, 0);
|
|
}
|
|
|
|
static void pulseaudio_init()
|
|
{
|
|
mainloop = pa_threaded_mainloop_new();
|
|
if (!mainloop)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_threaded_mainloop_new failed");
|
|
return;
|
|
}
|
|
context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "flycast");
|
|
if (!context)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_context_new failed");
|
|
return;
|
|
}
|
|
pa_context_set_state_callback(context, context_state_cb, nullptr);
|
|
|
|
if (pa_context_connect(context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_context_connect failed");
|
|
return;
|
|
}
|
|
|
|
pa_threaded_mainloop_lock(mainloop);
|
|
if (pa_threaded_mainloop_start(mainloop) < 0)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_threaded_mainloop_start failed");
|
|
return;
|
|
}
|
|
pa_threaded_mainloop_wait(mainloop);
|
|
|
|
if (pa_context_get_state(context) != PA_CONTEXT_READY)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: context isn't ready");
|
|
return;
|
|
}
|
|
pa_sample_spec spec{ PA_SAMPLE_S16LE, 44100, 2 };
|
|
|
|
stream = pa_stream_new(context, "audio", &spec, NULL);
|
|
if (!stream)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_stream_new failed");
|
|
return;
|
|
}
|
|
|
|
//pa_stream_set_state_callback(stream, stream_state_cb, nullptr);
|
|
pa_stream_set_write_callback(stream, stream_request_cb, nullptr);
|
|
//pa_stream_set_latency_update_callback(stream, stream_latency_update_cb, nullptr);
|
|
//pa_stream_set_underflow_callback(stream, underrun_update_cb, nullptr);
|
|
//pa_stream_set_buffer_attr_callback(stream, buffer_attr_cb, nullptr);
|
|
|
|
pa_buffer_attr buffer_attr;
|
|
buffer_attr.maxlength = -1;
|
|
buffer_attr.tlength = pa_usec_to_bytes(config::AudioBufferSize * PA_USEC_PER_SEC / 44100, &spec);
|
|
buffer_attr.prebuf = -1;
|
|
buffer_attr.minreq = -1;
|
|
buffer_attr.fragsize = -1;
|
|
|
|
if (pa_stream_connect_playback(stream, nullptr, &buffer_attr, PA_STREAM_ADJUST_LATENCY, nullptr, nullptr) < 0)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: pa_stream_connect_playback failed");
|
|
return;
|
|
}
|
|
|
|
pa_threaded_mainloop_wait(mainloop);
|
|
|
|
if (pa_stream_get_state(stream) != PA_STREAM_READY)
|
|
{
|
|
WARN_LOG(AUDIO, "PulseAudio: stream isn't ready");
|
|
return;
|
|
}
|
|
|
|
const pa_buffer_attr *server_attr = pa_stream_get_buffer_attr(stream);
|
|
if (server_attr)
|
|
DEBUG_LOG(AUDIO, "PulseAudio: requested %d samples buffer, got %d", buffer_attr.tlength / 4, server_attr->tlength / 4);
|
|
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
}
|
|
|
|
static u32 pulseaudio_push(const void* frame, u32 samples, bool wait)
|
|
{
|
|
const u8 *buf = (const u8 *)frame;
|
|
size_t size = samples * 4;
|
|
|
|
pa_threaded_mainloop_lock(mainloop);
|
|
while (size)
|
|
{
|
|
size_t writable = std::min(size, pa_stream_writable_size(stream));
|
|
|
|
if (writable)
|
|
{
|
|
pa_stream_write(stream, buf, writable, NULL, 0, PA_SEEK_RELATIVE);
|
|
buf += writable;
|
|
size -= writable;
|
|
}
|
|
else if (wait)
|
|
pa_threaded_mainloop_wait(mainloop);
|
|
else
|
|
break;
|
|
}
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void pulseaudio_term()
|
|
{
|
|
if (mainloop)
|
|
pa_threaded_mainloop_stop(mainloop);
|
|
|
|
if (stream)
|
|
{
|
|
pa_stream_disconnect(stream);
|
|
pa_stream_unref(stream);
|
|
}
|
|
if (context)
|
|
{
|
|
pa_context_disconnect(context);
|
|
pa_context_unref(context);
|
|
}
|
|
|
|
if (mainloop)
|
|
pa_threaded_mainloop_free(mainloop);
|
|
}
|
|
|
|
static bool pulseaudio_init_record(u32 sampling_freq)
|
|
{
|
|
pa_sample_spec spec{ PA_SAMPLE_S16LE, sampling_freq, 1 };
|
|
|
|
record_stream = pa_stream_new(context, "record", &spec, NULL);
|
|
if (!record_stream)
|
|
{
|
|
INFO_LOG(AUDIO, "PulseAudio: pa_stream_new failed");
|
|
return false;
|
|
}
|
|
|
|
pa_threaded_mainloop_lock(mainloop);
|
|
|
|
pa_buffer_attr buffer_attr;
|
|
buffer_attr.fragsize = 240 * 2;
|
|
buffer_attr.maxlength = buffer_attr.fragsize * 2;
|
|
|
|
if (pa_stream_connect_record(record_stream, nullptr, &buffer_attr, PA_STREAM_NOFLAGS) < 0)
|
|
{
|
|
INFO_LOG(AUDIO, "PulseAudio: pa_stream_connect_record failed");
|
|
pa_stream_unref(record_stream);
|
|
record_stream = nullptr;
|
|
return false;
|
|
}
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
INFO_LOG(AUDIO, "PulseAudio: Successfully initialized capture device");
|
|
|
|
return true;
|
|
}
|
|
|
|
static void pulseaudio_term_record()
|
|
{
|
|
if (record_stream != nullptr)
|
|
{
|
|
pa_threaded_mainloop_lock(mainloop);
|
|
pa_stream_disconnect(record_stream);
|
|
pa_stream_unref(record_stream);
|
|
record_stream = nullptr;
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
}
|
|
}
|
|
|
|
static u32 pulseaudio_record(void *buffer, u32 samples)
|
|
{
|
|
if (record_stream == nullptr)
|
|
return 0;
|
|
pa_threaded_mainloop_lock(mainloop);
|
|
const void *data;
|
|
size_t size;
|
|
if (pa_stream_peek(record_stream, &data, &size) < 0)
|
|
{
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
DEBUG_LOG(AUDIO, "PulseAudio: pa_stream_peek error");
|
|
return 0;
|
|
}
|
|
if (size == 0)
|
|
{
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
return 0;
|
|
}
|
|
size = std::min((size_t)samples * 2, size);
|
|
if (data != nullptr)
|
|
memcpy(buffer, data, size);
|
|
else
|
|
memset(buffer, 0, size);
|
|
pa_stream_drop(record_stream);
|
|
pa_threaded_mainloop_unlock(mainloop);
|
|
|
|
return size / 2;
|
|
}
|
|
|
|
static audiobackend_t audiobackend_pulseaudio = {
|
|
"pulse", // Slug
|
|
"PulseAudio", // Name
|
|
&pulseaudio_init,
|
|
&pulseaudio_push,
|
|
&pulseaudio_term,
|
|
NULL,
|
|
&pulseaudio_init_record,
|
|
&pulseaudio_record,
|
|
&pulseaudio_term_record,
|
|
};
|
|
#endif // !PULSEAUDIO_SIMPLE
|
|
|
|
static bool pulse = RegisterAudioBackend(&audiobackend_pulseaudio);
|
|
#endif
|