1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
|
/* This file is part of Xmms2-libvisual.
*
* Xmms2-libvisual 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 Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Xmms2-libvisual 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 Xmms2-libvisual. If not, see <http://www.gnu.org/licenses/>.
*/
#include <libvisual/libvisual.h>
#include <pulse/mainloop.h>
#include <pulse/stream.h>
#include <pulse/thread-mainloop.h>
VISUAL_PLUGIN_API_VERSION_VALIDATOR
#define SAMPLE_RATE 44100
#define SAMPLE_RATE_TYPE_LV VISUAL_AUDIO_SAMPLE_RATE_44100
#define CHANNELS 2
#define CHANNELS_TYPE_LV VISUAL_AUDIO_SAMPLE_CHANNEL_STEREO
#define SAMPLE_TYPE int16_t
#define SAMPLE_FORMAT_PA PA_SAMPLE_S16LE
#define SAMPLE_FORMAT_LV VISUAL_AUDIO_SAMPLE_FORMAT_S16
#define FRAMES 512
#define CHUNK_SIZE_BYTES (FRAMES * CHANNELS * sizeof(SAMPLE_TYPE))
#define CHUNKS (2 * SAMPLE_RATE / FRAMES + 1) // i.e. 2+ seconds of audio
#if defined(__clang__) || defined(__GNUC__)
# define ATOMIC_CONSUMER_LOAD_64(source, target) __atomic_load(&source, &target, __ATOMIC_ACQUIRE)
# define ATOMIC_PRODUCER_STORE_64(target, source) __atomic_store(&target, &source, __ATOMIC_RELEASE)
#else
# error We need GCC or Clang for __atomic_load and __atomic_store
#endif
pa_sample_spec sample_spec = {
.format = SAMPLE_FORMAT_PA,
.rate = SAMPLE_RATE,
.channels = CHANNELS
};
typedef struct {
pa_threaded_mainloop * mainloop;
pa_context * context;
pa_stream * input_stream;
SAMPLE_TYPE pcm_data[CHUNKS][FRAMES * CHANNELS]; // ringbuffer of chunks
uint64_t chunks_written;
uint64_t chunk_write_offset_bytes;
uint64_t chunks_read;
} pulseaudio_priv_t;
static int inp_pulseaudio_init (VisPluginData *plugin);
static int inp_pulseaudio_cleanup (VisPluginData *plugin);
static int inp_pulseaudio_upload (VisPluginData *plugin, VisAudio *audio);
static void on_input_stream_data (pa_stream *p, size_t nbytes, void *userdata);
const VisPluginInfo *get_plugin_info( void ) {
static VisInputPlugin input = {
.upload = inp_pulseaudio_upload
};
static VisPluginInfo info = {
.type = VISUAL_PLUGIN_TYPE_INPUT,
.plugname = "pulseaudio",
.name = "Pulseaudio input plugin",
.author = "Scott Sibley <scott@starlon.net>"
", "
"Sebastian Pipping <sebastian@pipping.org>",
.version = "2.0",
.about = "Use input data from pulseaudio",
.help = "",
.license = VISUAL_PLUGIN_LICENSE_GPL, // v3 or later, see header
.init = inp_pulseaudio_init,
.cleanup = inp_pulseaudio_cleanup,
.plugin = VISUAL_OBJECT (&input)
};
return &info;
}
static int inp_pulseaudio_init( VisPluginData *plugin ) {
pulseaudio_priv_t *priv = visual_mem_new0(pulseaudio_priv_t, 1);
visual_object_set_private(VISUAL_OBJECT (plugin), priv);
priv->mainloop = pa_threaded_mainloop_new();
visual_log_return_val_if_fail( priv->mainloop != NULL, -1 );
pa_mainloop_api *const mainloop_api = pa_threaded_mainloop_get_api(priv->mainloop);
visual_log_return_val_if_fail( mainloop_api != NULL, -2 );
priv->context = pa_context_new(mainloop_api, "lv-pulseaudio");
visual_log_return_val_if_fail( priv->context != NULL, -3 );
const int connect_res =
pa_context_connect(priv->context, NULL, PA_CONTEXT_NOFLAGS, NULL);
visual_log_return_val_if_fail( connect_res == 0, -4 );
// NOTE: Starting the main loop prior to pa_context_connect would get us into
// this situation:
// Assertion 'c->callback' failed at
// ../pulseaudio-16.1/src/pulsecore/socket-client.c:126, function
// do_call(). Aborting.
// https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/991
const int mainloop_start_ret = pa_threaded_mainloop_start(priv->mainloop);
visual_log_return_val_if_fail( mainloop_start_ret == 0, -5 );
while (pa_context_get_state(priv->context) < PA_CONTEXT_READY) {
visual_time_usleep(1000);
}
priv->input_stream = pa_stream_new(priv->context, "Recording", &sample_spec, NULL);
visual_log_return_val_if_fail( priv->input_stream != NULL, -6 );
pa_stream_set_read_callback(priv->input_stream, on_input_stream_data, priv);
const pa_buffer_attr input_buffer_attr = {
.fragsize = CHUNK_SIZE_BYTES,
.maxlength = CHUNK_SIZE_BYTES,
};
const int input_connect_res = pa_stream_connect_record(
priv->input_stream, NULL, &input_buffer_attr, PA_STREAM_ADJUST_LATENCY);
visual_log_return_val_if_fail( input_connect_res == 0, -7 );
return 0;
}
static int inp_pulseaudio_cleanup( VisPluginData *plugin )
{
pulseaudio_priv_t *priv = visual_object_get_private(VISUAL_OBJECT (plugin));
visual_log_return_val_if_fail( priv != NULL, -1 );
pa_stream_disconnect(priv->input_stream);
pa_stream_unref(priv->input_stream);
pa_context_disconnect(priv->context);
pa_context_unref(priv->context);
pa_threaded_mainloop_stop(priv->mainloop);
pa_threaded_mainloop_free(priv->mainloop);
visual_mem_free (priv);
return 0;
}
static int inp_pulseaudio_upload( VisPluginData *plugin, VisAudio *audio )
{
pulseaudio_priv_t *priv = visual_object_get_private(VISUAL_OBJECT (plugin));
visual_log_return_val_if_fail( priv != NULL, -1 );
// `priv->chunks_written` is monotonically increasing in another thread in parallel,
// so we make a snapshot to work with a single consistent value below.
// Also, plain reads to 64bit are not atomic on 32bit platforms, so we add protection.
// This is attomic `priv->chunks_written = frozen_chunks_written`.
uint64_t frozen_chunks_written;
ATOMIC_CONSUMER_LOAD_64(priv->chunks_written, frozen_chunks_written);
assert(priv->chunks_read <= frozen_chunks_written);
if (priv->chunks_read == frozen_chunks_written) {
return 0;
}
// The writing head keeps moving "in parallel" in another thread
// and without locking. So if the reader get too far behind, the writer
// overtakes the reader, in theory. It is not likely to happen because
// (1) the reader drains all available bytes at once, (2) the buffer
// is large enough to protect against temporary jittering, and (3)
// because `.upload` is guaranteed to be called at least at rendering
// FPS frequency.
const uint64_t tolerable_behind_by = CHUNKS / 2;
uint64_t behind_by = frozen_chunks_written - priv->chunks_read;
if (behind_by > tolerable_behind_by) {
priv->chunks_read = frozen_chunks_written - tolerable_behind_by;
behind_by = tolerable_behind_by;
}
const uint64_t target_chunks_read = priv->chunks_read + behind_by;
while (priv->chunks_read < target_chunks_read) {
void *const data = priv->pcm_data[priv->chunks_read % CHUNKS];
VisBuffer visbuffer;
visual_buffer_init(&visbuffer, data, CHUNK_SIZE_BYTES, NULL);
visual_audio_samplepool_input(audio->samplepool, &visbuffer, SAMPLE_RATE_TYPE_LV,
SAMPLE_FORMAT_LV, CHANNELS_TYPE_LV);
priv->chunks_read++;
}
return 0;
}
static void on_input_stream_data(pa_stream *p, size_t nbytes, void *userdata) {
pulseaudio_priv_t * const priv = (pulseaudio_priv_t *)userdata;
visual_log_return_if_fail( priv != NULL );
const void *source = NULL;
const int peek_res = pa_stream_peek(p, &source, &nbytes);
visual_log_return_if_fail( peek_res == 0 );
visual_log_return_if_fail( source != 0 );
const int drop_res = pa_stream_drop(p);
visual_log_return_if_fail( drop_res == 0 );
// Copy all readable bytes from `source` to the right place in `priv->pcm_data`.
while (nbytes > 0) {
void *const target = (void *)priv->pcm_data[priv->chunks_written % CHUNKS] + priv->chunk_write_offset_bytes;
size_t round_nbytes = nbytes;
// Would a full write overflow the current chunk?
if (priv->chunk_write_offset_bytes + round_nbytes > CHUNK_SIZE_BYTES) {
// Cut down to fit the chunk
round_nbytes = CHUNK_SIZE_BYTES - priv->chunk_write_offset_bytes;
}
visual_mem_copy(target, source, round_nbytes);
// Figure out write offset location for the next round
if (priv->chunk_write_offset_bytes + round_nbytes < CHUNK_SIZE_BYTES) {
// Same chunk but further behind
priv->chunk_write_offset_bytes += round_nbytes;
} else {
// Start of the next chunk
priv->chunk_write_offset_bytes = 0;
// This is atomic `priv->chunks_written++`
const uint64_t new_chunks_written = priv->chunks_written + 1;
ATOMIC_PRODUCER_STORE_64(priv->chunks_written, new_chunks_written);
}
nbytes -= round_nbytes;
}
}
|