2021-12-24 07:43:20 +00:00
|
|
|
/**
|
|
|
|
* Looking Glass
|
2024-02-01 06:16:31 +00:00
|
|
|
* Copyright © 2017-2024 The Looking Glass Authors
|
2021-12-24 07:43:20 +00:00
|
|
|
* https://looking-glass.io
|
|
|
|
*
|
|
|
|
* This program 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 2 of the License, or (at your option)
|
|
|
|
* any later version.
|
|
|
|
*
|
|
|
|
* This program 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 this program; if not, write to the Free Software Foundation, Inc., 59
|
|
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "interface/audiodev.h"
|
|
|
|
|
|
|
|
#include <spa/param/audio/format-utils.h>
|
2021-12-25 09:37:52 +00:00
|
|
|
#include <spa/param/props.h>
|
2021-12-24 07:43:20 +00:00
|
|
|
#include <pipewire/pipewire.h>
|
2021-12-25 09:53:11 +00:00
|
|
|
#include <math.h>
|
2021-12-24 07:43:20 +00:00
|
|
|
|
|
|
|
#include "common/debug.h"
|
2022-01-10 21:27:35 +00:00
|
|
|
#include "common/stringutils.h"
|
2022-01-06 13:54:44 +00:00
|
|
|
#include "common/util.h"
|
2023-01-24 02:34:05 +00:00
|
|
|
#include "common/option.h"
|
2021-12-24 07:43:20 +00:00
|
|
|
|
2022-01-10 20:45:05 +00:00
|
|
|
typedef enum
|
|
|
|
{
|
|
|
|
STREAM_STATE_INACTIVE,
|
|
|
|
STREAM_STATE_ACTIVE,
|
2022-01-24 21:04:17 +00:00
|
|
|
STREAM_STATE_DRAINING
|
2022-01-10 20:45:05 +00:00
|
|
|
}
|
|
|
|
StreamState;
|
|
|
|
|
2021-12-24 07:43:20 +00:00
|
|
|
struct PipeWire
|
|
|
|
{
|
|
|
|
struct pw_loop * loop;
|
2022-02-13 16:59:09 +00:00
|
|
|
struct pw_context * context;
|
2021-12-24 07:43:20 +00:00
|
|
|
struct pw_thread_loop * thread;
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
struct
|
|
|
|
{
|
|
|
|
struct pw_stream * stream;
|
2022-01-10 21:46:34 +00:00
|
|
|
struct spa_io_rate_match * rateMatch;
|
2023-12-06 11:26:20 +00:00
|
|
|
struct pw_time time;
|
2022-01-06 12:56:12 +00:00
|
|
|
|
2022-01-17 22:02:44 +00:00
|
|
|
int channels;
|
|
|
|
int sampleRate;
|
|
|
|
int stride;
|
|
|
|
LG_AudioPullFn pullFn;
|
2022-01-30 13:53:46 +00:00
|
|
|
int maxPeriodFrames;
|
2022-02-20 21:27:22 +00:00
|
|
|
int startFrames;
|
2022-01-06 12:56:12 +00:00
|
|
|
|
2022-01-10 20:45:05 +00:00
|
|
|
StreamState state;
|
2022-01-06 12:56:12 +00:00
|
|
|
}
|
|
|
|
playback;
|
2022-01-06 13:54:44 +00:00
|
|
|
|
|
|
|
struct
|
|
|
|
{
|
|
|
|
struct pw_stream * stream;
|
|
|
|
|
2022-01-17 22:02:44 +00:00
|
|
|
int channels;
|
|
|
|
int sampleRate;
|
|
|
|
int stride;
|
|
|
|
LG_AudioPushFn pushFn;
|
2022-01-06 13:54:44 +00:00
|
|
|
|
|
|
|
bool active;
|
|
|
|
}
|
|
|
|
record;
|
2021-12-24 07:43:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static struct PipeWire pw = {0};
|
|
|
|
|
2022-01-10 21:46:34 +00:00
|
|
|
static void pipewire_onPlaybackIoChanged(void * userdata, uint32_t id,
|
|
|
|
void * data, uint32_t size)
|
|
|
|
{
|
|
|
|
switch (id)
|
|
|
|
{
|
|
|
|
case SPA_IO_RateMatch:
|
|
|
|
pw.playback.rateMatch = data;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-06 13:54:44 +00:00
|
|
|
static void pipewire_onPlaybackProcess(void * userdata)
|
2021-12-24 07:43:20 +00:00
|
|
|
{
|
|
|
|
struct pw_buffer * pbuf;
|
|
|
|
|
2023-12-06 11:26:20 +00:00
|
|
|
#if PW_CHECK_VERSION(0, 3, 50)
|
|
|
|
if (pw_stream_get_time_n(pw.playback.stream, &pw.playback.time,
|
|
|
|
sizeof(pw.playback.time)) < 0)
|
|
|
|
#else
|
|
|
|
if (pw_stream_get_time(pw.playback.stream, &pw.playback.time) < 0)
|
|
|
|
#endif
|
|
|
|
DEBUG_ERROR("pw_stream_get_time failed");
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
if (!(pbuf = pw_stream_dequeue_buffer(pw.playback.stream)))
|
|
|
|
{
|
2021-12-24 07:43:20 +00:00
|
|
|
DEBUG_WARN("out of buffers");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct spa_buffer * sbuf = pbuf->buffer;
|
|
|
|
uint8_t * dst;
|
|
|
|
|
|
|
|
if (!(dst = sbuf->datas[0].data))
|
|
|
|
return;
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
int frames = sbuf->datas[0].maxsize / pw.playback.stride;
|
2022-01-10 21:46:34 +00:00
|
|
|
if (pw.playback.rateMatch && pw.playback.rateMatch->size > 0)
|
|
|
|
frames = min(frames, pw.playback.rateMatch->size);
|
|
|
|
|
2022-01-18 23:29:49 +00:00
|
|
|
frames = pw.playback.pullFn(dst, frames);
|
2022-01-17 22:02:44 +00:00
|
|
|
if (!frames)
|
|
|
|
{
|
|
|
|
sbuf->datas[0].chunk->size = 0;
|
|
|
|
pw_stream_queue_buffer(pw.playback.stream, pbuf);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-12-06 11:26:20 +00:00
|
|
|
pbuf->size = frames;
|
2021-12-24 07:43:20 +00:00
|
|
|
sbuf->datas[0].chunk->offset = 0;
|
2022-01-06 12:56:12 +00:00
|
|
|
sbuf->datas[0].chunk->stride = pw.playback.stride;
|
|
|
|
sbuf->datas[0].chunk->size = frames * pw.playback.stride;
|
2021-12-24 07:43:20 +00:00
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
pw_stream_queue_buffer(pw.playback.stream, pbuf);
|
2021-12-24 07:43:20 +00:00
|
|
|
}
|
|
|
|
|
2022-01-10 20:45:05 +00:00
|
|
|
static void pipewire_onPlaybackDrained(void * userdata)
|
|
|
|
{
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-01-24 21:04:17 +00:00
|
|
|
pw_stream_set_active(pw.playback.stream, false);
|
|
|
|
pw.playback.state = STREAM_STATE_INACTIVE;
|
2022-01-10 20:45:05 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
2023-01-24 02:34:05 +00:00
|
|
|
static struct Option pipewire_options[] =
|
|
|
|
{
|
|
|
|
{
|
|
|
|
.module = "pipewire",
|
|
|
|
.name = "outDevice",
|
|
|
|
.description = "The default playback device to use",
|
|
|
|
.type = OPTION_TYPE_STRING
|
|
|
|
},
|
|
|
|
{
|
|
|
|
.module = "pipewire",
|
|
|
|
.name = "recDevice",
|
|
|
|
.description = "The default record device to use",
|
|
|
|
.type = OPTION_TYPE_STRING
|
|
|
|
},
|
2023-01-24 02:46:22 +00:00
|
|
|
|
|
|
|
{0}
|
2023-01-24 02:34:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
static void pipewire_earlyInit(void)
|
|
|
|
{
|
|
|
|
option_register(pipewire_options);
|
|
|
|
}
|
|
|
|
|
2021-12-24 07:43:20 +00:00
|
|
|
static bool pipewire_init(void)
|
|
|
|
{
|
|
|
|
pw_init(NULL, NULL);
|
|
|
|
|
|
|
|
pw.loop = pw_loop_new(NULL);
|
2022-02-13 16:59:09 +00:00
|
|
|
pw.context = pw_context_new(
|
|
|
|
pw.loop,
|
|
|
|
pw_properties_new(
|
|
|
|
// Request real-time priority on the PipeWire threads
|
|
|
|
PW_KEY_CONFIG_NAME, "client-rt.conf",
|
|
|
|
NULL
|
|
|
|
),
|
|
|
|
0);
|
|
|
|
if (!pw.context)
|
2021-12-24 07:43:20 +00:00
|
|
|
{
|
|
|
|
DEBUG_ERROR("Failed to create a context");
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* this is just to test for PipeWire availabillity */
|
2022-02-13 16:59:09 +00:00
|
|
|
struct pw_core * core = pw_context_connect(pw.context, NULL, 0);
|
2021-12-24 07:43:20 +00:00
|
|
|
if (!core)
|
|
|
|
goto err_context;
|
|
|
|
|
|
|
|
/* PipeWire is available so create the loop thread and start it */
|
2022-01-06 12:56:12 +00:00
|
|
|
pw.thread = pw_thread_loop_new_full(pw.loop, "PipeWire", NULL);
|
2021-12-24 07:43:20 +00:00
|
|
|
if (!pw.thread)
|
|
|
|
{
|
|
|
|
DEBUG_ERROR("Failed to create the thread loop");
|
2022-02-13 16:59:09 +00:00
|
|
|
goto err_context;
|
2021-12-24 07:43:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pw_thread_loop_start(pw.thread);
|
|
|
|
return true;
|
|
|
|
|
|
|
|
err_context:
|
2022-02-13 16:59:09 +00:00
|
|
|
pw_context_destroy(pw.context);
|
2021-12-24 07:43:20 +00:00
|
|
|
|
|
|
|
err:
|
2021-12-25 04:03:50 +00:00
|
|
|
pw_loop_destroy(pw.loop);
|
2021-12-24 07:43:20 +00:00
|
|
|
pw_deinit();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-01-06 13:09:34 +00:00
|
|
|
static void pipewire_playbackStopStream(void)
|
2021-12-24 15:38:11 +00:00
|
|
|
{
|
2022-01-06 12:56:12 +00:00
|
|
|
if (!pw.playback.stream)
|
2021-12-24 15:38:11 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-01-06 12:56:12 +00:00
|
|
|
pw_stream_destroy(pw.playback.stream);
|
2022-01-10 21:46:34 +00:00
|
|
|
pw.playback.stream = NULL;
|
|
|
|
pw.playback.rateMatch = NULL;
|
2021-12-24 15:38:11 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
2022-01-17 22:02:44 +00:00
|
|
|
static void pipewire_playbackSetup(int channels, int sampleRate,
|
2022-02-20 21:27:22 +00:00
|
|
|
int requestedPeriodFrames, int * maxPeriodFrames, int * startFrames,
|
|
|
|
LG_AudioPullFn pullFn)
|
2021-12-24 07:43:20 +00:00
|
|
|
{
|
|
|
|
const struct spa_pod * params[1];
|
|
|
|
uint8_t buffer[1024];
|
|
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
|
|
static const struct pw_stream_events events =
|
|
|
|
{
|
2022-01-10 21:46:34 +00:00
|
|
|
.version = PW_VERSION_STREAM_EVENTS,
|
|
|
|
.io_changed = pipewire_onPlaybackIoChanged,
|
|
|
|
.process = pipewire_onPlaybackProcess,
|
|
|
|
.drained = pipewire_onPlaybackDrained
|
2021-12-24 07:43:20 +00:00
|
|
|
};
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
if (pw.playback.stream &&
|
|
|
|
pw.playback.channels == channels &&
|
|
|
|
pw.playback.sampleRate == sampleRate)
|
2022-01-29 19:23:28 +00:00
|
|
|
{
|
2022-01-30 13:53:46 +00:00
|
|
|
*maxPeriodFrames = pw.playback.maxPeriodFrames;
|
2022-02-20 21:27:22 +00:00
|
|
|
*startFrames = pw.playback.startFrames;
|
2021-12-25 02:44:40 +00:00
|
|
|
return;
|
2022-01-29 19:23:28 +00:00
|
|
|
}
|
2021-12-25 02:44:40 +00:00
|
|
|
|
2022-01-06 13:09:34 +00:00
|
|
|
pipewire_playbackStopStream();
|
2021-12-24 15:38:11 +00:00
|
|
|
|
2022-02-20 21:27:22 +00:00
|
|
|
char requestedNodeLatency[32];
|
|
|
|
snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d",
|
|
|
|
requestedPeriodFrames, sampleRate);
|
2022-01-10 21:27:35 +00:00
|
|
|
|
2022-01-18 14:52:19 +00:00
|
|
|
pw.playback.channels = channels;
|
|
|
|
pw.playback.sampleRate = sampleRate;
|
[client] audio: adjust playback speed to match audio device clock
This change is based on the techniques described in [1] and [2].
The input audio stream from Spice is not synchronised to the audio playback
device. While the input and output may be both nominally running at 48 kHz,
when compared against each other, they will differ by a tiny fraction of a
percent. Given enough time (typically on the order of a few hours), this
will result in the ring buffer becoming completely full or completely
empty. It will stay in this state permanently, periodically resulting in
glitches as the buffer repeatedly underruns or overruns.
To address this, adjust the speed of the received data to match the rate at
which it is being consumed by the audio device. This will result in a
slight pitch shift, but the changes should be small and smooth enough that
this is unnoticeable to the user.
The process works roughly as follows:
1. Every time audio data is received from Spice, or consumed by the audio
device, sample the current time. These are fed into a pair of delay
locked loops to produce smoothed approximations of the two clocks.
2. Compute the difference between the two clocks and compare this against
the target latency to produce an error value. This error value will be
quite stable during normal operation, but can change quite rapidly due
to external factors, particularly at the start of playback. To smooth
out any sudden changes in playback speed, which would be noticeable to
the user, this value is also filtered through another delay locked loop.
3. Feed this error value into a PI controller to produce a ratio value.
This is the target playback speed in order to bring the error value
towards zero.
4. Resample the input audio using the computed ratio to apply the speed
change. The output of the resampler is what is ultimately inserted into
the ring buffer for consumption by the audio device.
Since this process targets a specific latency value, rather than simply
trying to rate match the input and output, it also has the effect of
'correcting' latency issues. If a high latency application (such as a media
player) is already running, the time between requesting the start of
playback and the audio device actually starting to consume samples can be
very high, easily in the hundreds of milliseconds. The changes here will
automatically adjust the playback speed over the course of a few minutes to
bring the latency back down to the target value.
[1] https://kokkinizita.linuxaudio.org/papers/adapt-resamp.pdf
[2] https://kokkinizita.linuxaudio.org/papers/usingdll.pdf
2022-01-26 20:55:24 +00:00
|
|
|
pw.playback.stride = sizeof(float) * channels;
|
2022-01-18 14:52:19 +00:00
|
|
|
pw.playback.pullFn = pullFn;
|
|
|
|
|
2021-12-24 07:43:20 +00:00
|
|
|
pw_thread_loop_lock(pw.thread);
|
2023-01-24 02:34:05 +00:00
|
|
|
|
|
|
|
struct pw_properties * props =
|
2021-12-24 07:43:20 +00:00
|
|
|
pw_properties_new(
|
2022-01-29 19:23:28 +00:00
|
|
|
PW_KEY_NODE_NAME , "Looking Glass",
|
|
|
|
PW_KEY_MEDIA_TYPE , "Audio",
|
|
|
|
PW_KEY_MEDIA_CATEGORY, "Playback",
|
|
|
|
PW_KEY_MEDIA_ROLE , "Music",
|
2022-02-20 21:27:22 +00:00
|
|
|
PW_KEY_NODE_LATENCY , requestedNodeLatency,
|
2021-12-24 07:43:20 +00:00
|
|
|
NULL
|
2023-01-24 02:34:05 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const char * device = option_get_string("pipewire", "outDevice");
|
|
|
|
if (device)
|
2023-01-24 02:43:17 +00:00
|
|
|
{
|
|
|
|
#ifdef PW_KEY_TARGET_OBJECT
|
2023-01-24 02:34:05 +00:00
|
|
|
pw_properties_set(props, PW_KEY_TARGET_OBJECT, device);
|
2023-01-24 02:43:17 +00:00
|
|
|
#else
|
|
|
|
pw_properties_set(props, PW_KEY_NODE_TARGET, device);
|
|
|
|
#endif
|
|
|
|
}
|
2023-01-24 02:34:05 +00:00
|
|
|
|
|
|
|
pw.playback.stream = pw_stream_new_simple(
|
|
|
|
pw.loop,
|
|
|
|
"Looking Glass",
|
|
|
|
props,
|
2021-12-24 07:43:20 +00:00
|
|
|
&events,
|
|
|
|
NULL
|
|
|
|
);
|
|
|
|
|
2022-01-29 19:23:28 +00:00
|
|
|
// The user can override the default node latency with the PIPEWIRE_LATENCY
|
|
|
|
// environment variable, so get the actual node latency value from the stream.
|
|
|
|
// The actual quantum size may be lower than this value depending on what else
|
|
|
|
// is using the audio device, but we can treat this value as a maximum
|
|
|
|
const struct pw_properties * properties =
|
|
|
|
pw_stream_get_properties(pw.playback.stream);
|
|
|
|
const char *actualNodeLatency =
|
|
|
|
pw_properties_get(properties, PW_KEY_NODE_LATENCY);
|
|
|
|
DEBUG_ASSERT(actualNodeLatency != NULL);
|
|
|
|
|
|
|
|
unsigned num, denom;
|
|
|
|
if (sscanf(actualNodeLatency, "%u/%u", &num, &denom) != 2 ||
|
|
|
|
denom != sampleRate)
|
|
|
|
{
|
|
|
|
DEBUG_WARN(
|
|
|
|
"PIPEWIRE_LATENCY value '%s' is invalid or does not match stream sample "
|
2022-02-20 21:27:22 +00:00
|
|
|
"rate; using %d/%d", actualNodeLatency, requestedPeriodFrames,
|
2022-01-29 19:23:28 +00:00
|
|
|
sampleRate);
|
|
|
|
|
|
|
|
struct spa_dict_item items[] = {
|
2022-02-20 21:27:22 +00:00
|
|
|
{ PW_KEY_NODE_LATENCY, requestedNodeLatency }
|
2022-01-29 19:23:28 +00:00
|
|
|
};
|
|
|
|
pw_stream_update_properties(pw.playback.stream,
|
|
|
|
&SPA_DICT_INIT_ARRAY(items));
|
|
|
|
|
2022-02-20 21:27:22 +00:00
|
|
|
pw.playback.maxPeriodFrames = requestedPeriodFrames;
|
2022-01-29 19:23:28 +00:00
|
|
|
}
|
|
|
|
else
|
2022-01-30 13:53:46 +00:00
|
|
|
pw.playback.maxPeriodFrames = num;
|
2022-01-29 19:23:28 +00:00
|
|
|
|
2022-02-20 21:27:22 +00:00
|
|
|
// If the previous quantum size was very small, PipeWire can request two full
|
|
|
|
// periods almost immediately at the start of playback
|
|
|
|
pw.playback.startFrames = pw.playback.maxPeriodFrames * 2;
|
|
|
|
|
2022-01-30 13:53:46 +00:00
|
|
|
*maxPeriodFrames = pw.playback.maxPeriodFrames;
|
2022-02-20 21:27:22 +00:00
|
|
|
*startFrames = pw.playback.startFrames;
|
2022-01-29 19:23:28 +00:00
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
if (!pw.playback.stream)
|
2021-12-24 07:43:20 +00:00
|
|
|
{
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
DEBUG_ERROR("Failed to create the stream");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
|
|
|
|
&SPA_AUDIO_INFO_RAW_INIT(
|
[client] audio: adjust playback speed to match audio device clock
This change is based on the techniques described in [1] and [2].
The input audio stream from Spice is not synchronised to the audio playback
device. While the input and output may be both nominally running at 48 kHz,
when compared against each other, they will differ by a tiny fraction of a
percent. Given enough time (typically on the order of a few hours), this
will result in the ring buffer becoming completely full or completely
empty. It will stay in this state permanently, periodically resulting in
glitches as the buffer repeatedly underruns or overruns.
To address this, adjust the speed of the received data to match the rate at
which it is being consumed by the audio device. This will result in a
slight pitch shift, but the changes should be small and smooth enough that
this is unnoticeable to the user.
The process works roughly as follows:
1. Every time audio data is received from Spice, or consumed by the audio
device, sample the current time. These are fed into a pair of delay
locked loops to produce smoothed approximations of the two clocks.
2. Compute the difference between the two clocks and compare this against
the target latency to produce an error value. This error value will be
quite stable during normal operation, but can change quite rapidly due
to external factors, particularly at the start of playback. To smooth
out any sudden changes in playback speed, which would be noticeable to
the user, this value is also filtered through another delay locked loop.
3. Feed this error value into a PI controller to produce a ratio value.
This is the target playback speed in order to bring the error value
towards zero.
4. Resample the input audio using the computed ratio to apply the speed
change. The output of the resampler is what is ultimately inserted into
the ring buffer for consumption by the audio device.
Since this process targets a specific latency value, rather than simply
trying to rate match the input and output, it also has the effect of
'correcting' latency issues. If a high latency application (such as a media
player) is already running, the time between requesting the start of
playback and the audio device actually starting to consume samples can be
very high, easily in the hundreds of milliseconds. The changes here will
automatically adjust the playback speed over the course of a few minutes to
bring the latency back down to the target value.
[1] https://kokkinizita.linuxaudio.org/papers/adapt-resamp.pdf
[2] https://kokkinizita.linuxaudio.org/papers/usingdll.pdf
2022-01-26 20:55:24 +00:00
|
|
|
.format = SPA_AUDIO_FORMAT_F32,
|
2021-12-24 07:43:20 +00:00
|
|
|
.channels = channels,
|
|
|
|
.rate = sampleRate
|
|
|
|
));
|
|
|
|
|
|
|
|
pw_stream_connect(
|
2022-01-06 12:56:12 +00:00
|
|
|
pw.playback.stream,
|
2021-12-24 07:43:20 +00:00
|
|
|
PW_DIRECTION_OUTPUT,
|
|
|
|
PW_ID_ANY,
|
|
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
2022-01-29 19:23:28 +00:00
|
|
|
PW_STREAM_FLAG_RT_PROCESS |
|
|
|
|
PW_STREAM_FLAG_INACTIVE,
|
2021-12-24 07:43:20 +00:00
|
|
|
params, 1);
|
|
|
|
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
2022-02-05 09:53:06 +00:00
|
|
|
static void pipewire_playbackStart(void)
|
2021-12-24 07:43:20 +00:00
|
|
|
{
|
2022-01-06 12:56:12 +00:00
|
|
|
if (!pw.playback.stream)
|
2022-02-05 09:53:06 +00:00
|
|
|
return;
|
2021-12-24 07:43:20 +00:00
|
|
|
|
2022-01-24 21:04:17 +00:00
|
|
|
if (pw.playback.state != STREAM_STATE_ACTIVE)
|
2021-12-24 22:40:41 +00:00
|
|
|
{
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-01-10 20:45:05 +00:00
|
|
|
|
2022-01-17 22:02:44 +00:00
|
|
|
switch (pw.playback.state)
|
|
|
|
{
|
2022-01-10 20:45:05 +00:00
|
|
|
case STREAM_STATE_INACTIVE:
|
2022-02-05 09:53:06 +00:00
|
|
|
pw_stream_set_active(pw.playback.stream, true);
|
|
|
|
pw.playback.state = STREAM_STATE_ACTIVE;
|
2022-01-10 20:45:05 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case STREAM_STATE_DRAINING:
|
2022-01-24 21:04:17 +00:00
|
|
|
// We are in the middle of draining the PipeWire buffers; we need to
|
|
|
|
// wait for this to complete before allowing the new playback to start
|
2022-01-10 20:45:05 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
DEBUG_UNREACHABLE();
|
|
|
|
}
|
|
|
|
|
2021-12-24 22:40:41 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
2021-12-24 07:43:20 +00:00
|
|
|
}
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
static void pipewire_playbackStop(void)
|
2021-12-24 07:43:20 +00:00
|
|
|
{
|
2022-01-24 21:04:17 +00:00
|
|
|
if (pw.playback.state != STREAM_STATE_ACTIVE)
|
2021-12-24 22:40:41 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-01-24 21:04:17 +00:00
|
|
|
pw_stream_flush(pw.playback.stream, true);
|
|
|
|
pw.playback.state = STREAM_STATE_DRAINING;
|
2021-12-24 22:40:41 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
2021-12-24 07:43:20 +00:00
|
|
|
}
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
static void pipewire_playbackVolume(int channels, const uint16_t volume[])
|
2021-12-25 09:37:52 +00:00
|
|
|
{
|
2022-01-06 12:56:12 +00:00
|
|
|
if (channels != pw.playback.channels)
|
2021-12-25 09:37:52 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
float param[channels];
|
|
|
|
for(int i = 0; i < channels; ++i)
|
2021-12-25 09:53:11 +00:00
|
|
|
param[i] = 9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787;
|
2021-12-25 09:37:52 +00:00
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-01-06 12:56:12 +00:00
|
|
|
pw_stream_set_control(pw.playback.stream, SPA_PROP_channelVolumes,
|
|
|
|
channels, param, 0);
|
2021-12-25 09:37:52 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
2022-01-06 12:56:12 +00:00
|
|
|
static void pipewire_playbackMute(bool mute)
|
2021-12-25 09:37:52 +00:00
|
|
|
{
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-04-09 06:12:17 +00:00
|
|
|
float val = mute ? 1.0f : 0.0f;
|
|
|
|
pw_stream_set_control(pw.playback.stream, SPA_PROP_mute, 1, &val, 0);
|
2021-12-25 09:37:52 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
2023-12-06 11:26:20 +00:00
|
|
|
static uint64_t pipewire_playbackLatency(void)
|
2022-01-17 11:13:41 +00:00
|
|
|
{
|
2022-04-09 14:10:40 +00:00
|
|
|
#if PW_CHECK_VERSION(0, 3, 50)
|
2023-12-06 11:26:20 +00:00
|
|
|
if (pw.playback.time.rate.num == 0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
|
|
|
|
|
|
// diff in ns
|
|
|
|
int64_t diff = SPA_TIMESPEC_TO_NSEC(&ts) - pw.playback.time.now;
|
|
|
|
|
|
|
|
// elapsed frames
|
|
|
|
int64_t elapsed =
|
|
|
|
(pw.playback.time.rate.denom * diff) /
|
|
|
|
(pw.playback.time.rate.num * SPA_NSEC_PER_SEC);
|
|
|
|
|
|
|
|
const int64_t buffered = pw.playback.time.buffered + pw.playback.time.queued;
|
|
|
|
int64_t latency = (buffered * 1000 / pw.playback.sampleRate) +
|
|
|
|
((pw.playback.time.delay - elapsed) * 1000 *
|
|
|
|
pw.playback.time.rate.num / pw.playback.time.rate.denom);
|
|
|
|
|
|
|
|
return max(0, -latency);
|
2022-04-09 14:10:40 +00:00
|
|
|
#else
|
2023-12-06 11:26:20 +00:00
|
|
|
return pw.playback.time.delay + pw.playback.time.queued / pw.playback.stride;
|
2022-04-09 14:10:40 +00:00
|
|
|
#endif
|
2022-01-17 11:13:41 +00:00
|
|
|
}
|
|
|
|
|
2022-01-06 13:54:44 +00:00
|
|
|
static void pipewire_recordStopStream(void)
|
|
|
|
{
|
|
|
|
if (!pw.record.stream)
|
|
|
|
return;
|
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
|
|
pw_stream_destroy(pw.record.stream);
|
|
|
|
pw.record.stream = NULL;
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pipewire_onRecordProcess(void * userdata)
|
|
|
|
{
|
|
|
|
struct pw_buffer * pbuf;
|
|
|
|
|
|
|
|
if (!(pbuf = pw_stream_dequeue_buffer(pw.record.stream)))
|
|
|
|
{
|
|
|
|
DEBUG_WARN("out of buffers");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct spa_buffer * sbuf = pbuf->buffer;
|
|
|
|
uint8_t * dst;
|
|
|
|
|
|
|
|
if (!(dst = sbuf->datas[0].data))
|
|
|
|
return;
|
|
|
|
|
|
|
|
dst += sbuf->datas[0].chunk->offset;
|
2022-01-17 22:02:44 +00:00
|
|
|
pw.record.pushFn(dst,
|
2022-01-06 13:54:44 +00:00
|
|
|
min(
|
|
|
|
sbuf->datas[0].chunk->size,
|
2022-01-17 22:02:44 +00:00
|
|
|
sbuf->datas[0].maxsize - sbuf->datas[0].chunk->offset) / pw.record.stride
|
2022-01-06 13:54:44 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
pw_stream_queue_buffer(pw.record.stream, pbuf);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pipewire_recordStart(int channels, int sampleRate,
|
2022-01-17 22:02:44 +00:00
|
|
|
LG_AudioPushFn pushFn)
|
2022-01-06 13:54:44 +00:00
|
|
|
{
|
|
|
|
const struct spa_pod * params[1];
|
|
|
|
uint8_t buffer[1024];
|
|
|
|
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
|
|
static const struct pw_stream_events events =
|
|
|
|
{
|
|
|
|
.version = PW_VERSION_STREAM_EVENTS,
|
|
|
|
.process = pipewire_onRecordProcess
|
|
|
|
};
|
|
|
|
|
|
|
|
if (pw.record.stream &&
|
|
|
|
pw.record.channels == channels &&
|
|
|
|
pw.record.sampleRate == sampleRate)
|
2022-03-18 05:53:06 +00:00
|
|
|
{
|
|
|
|
if (!pw.record.active)
|
|
|
|
{
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
|
|
pw_stream_set_active(pw.record.stream, true);
|
|
|
|
pw.record.active = true;
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
2022-01-06 13:54:44 +00:00
|
|
|
return;
|
2022-03-18 05:53:06 +00:00
|
|
|
}
|
2022-01-06 13:54:44 +00:00
|
|
|
|
|
|
|
pipewire_recordStopStream();
|
|
|
|
|
|
|
|
pw.record.channels = channels;
|
|
|
|
pw.record.sampleRate = sampleRate;
|
|
|
|
pw.record.stride = sizeof(uint16_t) * channels;
|
2022-01-17 22:02:44 +00:00
|
|
|
pw.record.pushFn = pushFn;
|
2022-01-06 13:54:44 +00:00
|
|
|
|
2023-01-24 02:34:05 +00:00
|
|
|
struct pw_properties * props =
|
2022-01-06 13:54:44 +00:00
|
|
|
pw_properties_new(
|
|
|
|
PW_KEY_NODE_NAME , "Looking Glass",
|
|
|
|
PW_KEY_MEDIA_TYPE , "Audio",
|
|
|
|
PW_KEY_MEDIA_CATEGORY, "Capture",
|
|
|
|
PW_KEY_MEDIA_ROLE , "Music",
|
|
|
|
NULL
|
2023-01-24 02:34:05 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const char * device = option_get_string("pipewire", "recDevice");
|
|
|
|
if (device)
|
2023-01-24 02:43:17 +00:00
|
|
|
{
|
|
|
|
#ifdef PW_KEY_TARGET_OBJECT
|
2023-01-24 02:34:05 +00:00
|
|
|
pw_properties_set(props, PW_KEY_TARGET_OBJECT, device);
|
2023-01-24 02:43:17 +00:00
|
|
|
#else
|
|
|
|
pw_properties_set(props, PW_KEY_NODE_TARGET, device);
|
|
|
|
#endif
|
|
|
|
}
|
2023-01-24 02:34:05 +00:00
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
|
|
pw.record.stream = pw_stream_new_simple(
|
|
|
|
pw.loop,
|
|
|
|
"Looking Glass",
|
|
|
|
props,
|
2022-01-06 13:54:44 +00:00
|
|
|
&events,
|
|
|
|
NULL
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!pw.record.stream)
|
|
|
|
{
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
DEBUG_ERROR("Failed to create the stream");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
|
|
|
|
&SPA_AUDIO_INFO_RAW_INIT(
|
|
|
|
.format = SPA_AUDIO_FORMAT_S16,
|
|
|
|
.channels = channels,
|
|
|
|
.rate = sampleRate
|
|
|
|
));
|
|
|
|
|
|
|
|
pw_stream_connect(
|
|
|
|
pw.record.stream,
|
|
|
|
PW_DIRECTION_INPUT,
|
|
|
|
PW_ID_ANY,
|
|
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
|
|
PW_STREAM_FLAG_MAP_BUFFERS |
|
|
|
|
PW_STREAM_FLAG_RT_PROCESS,
|
|
|
|
params, 1);
|
|
|
|
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
2022-03-18 05:53:06 +00:00
|
|
|
pw.record.active = true;
|
2022-01-06 13:54:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void pipewire_recordStop(void)
|
|
|
|
{
|
|
|
|
if (!pw.record.active)
|
|
|
|
return;
|
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
|
|
pw_stream_set_active(pw.record.stream, false);
|
|
|
|
pw.record.active = false;
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pipewire_recordVolume(int channels, const uint16_t volume[])
|
|
|
|
{
|
|
|
|
if (channels != pw.record.channels)
|
|
|
|
return;
|
|
|
|
|
|
|
|
float param[channels];
|
|
|
|
for(int i = 0; i < channels; ++i)
|
|
|
|
param[i] = 9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787;
|
|
|
|
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
|
|
|
pw_stream_set_control(pw.record.stream, SPA_PROP_channelVolumes,
|
|
|
|
channels, param, 0);
|
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pipewire_recordMute(bool mute)
|
|
|
|
{
|
|
|
|
pw_thread_loop_lock(pw.thread);
|
2022-04-09 06:12:17 +00:00
|
|
|
float val = mute ? 1.0f : 0.0f;
|
|
|
|
pw_stream_set_control(pw.record.stream, SPA_PROP_mute, 1, &val, 0);
|
2022-01-06 13:54:44 +00:00
|
|
|
pw_thread_loop_unlock(pw.thread);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pipewire_free(void)
|
|
|
|
{
|
|
|
|
pipewire_playbackStopStream();
|
|
|
|
pipewire_recordStopStream();
|
|
|
|
pw_thread_loop_stop(pw.thread);
|
|
|
|
pw_thread_loop_destroy(pw.thread);
|
2022-02-13 16:59:09 +00:00
|
|
|
pw_context_destroy(pw.context);
|
2022-01-06 13:54:44 +00:00
|
|
|
pw_loop_destroy(pw.loop);
|
|
|
|
|
2022-02-13 16:59:09 +00:00
|
|
|
pw.loop = NULL;
|
|
|
|
pw.context = NULL;
|
|
|
|
pw.thread = NULL;
|
2022-01-06 13:54:44 +00:00
|
|
|
|
|
|
|
pw_deinit();
|
|
|
|
}
|
|
|
|
|
2021-12-24 07:43:20 +00:00
|
|
|
struct LG_AudioDevOps LGAD_PipeWire =
|
|
|
|
{
|
2023-01-24 02:34:05 +00:00
|
|
|
.name = "PipeWire",
|
|
|
|
.earlyInit = pipewire_earlyInit,
|
|
|
|
.init = pipewire_init,
|
|
|
|
.free = pipewire_free,
|
2022-01-06 11:47:22 +00:00
|
|
|
.playback =
|
|
|
|
{
|
2022-01-17 22:02:44 +00:00
|
|
|
.setup = pipewire_playbackSetup,
|
2022-01-17 11:13:41 +00:00
|
|
|
.start = pipewire_playbackStart,
|
|
|
|
.stop = pipewire_playbackStop,
|
|
|
|
.volume = pipewire_playbackVolume,
|
|
|
|
.mute = pipewire_playbackMute,
|
|
|
|
.latency = pipewire_playbackLatency
|
2022-01-06 13:54:44 +00:00
|
|
|
},
|
|
|
|
.record =
|
|
|
|
{
|
|
|
|
.start = pipewire_recordStart,
|
|
|
|
.stop = pipewire_recordStop,
|
|
|
|
.volume = pipewire_recordVolume,
|
|
|
|
.mute = pipewire_recordMute
|
2022-01-06 11:47:22 +00:00
|
|
|
}
|
2021-12-24 07:43:20 +00:00
|
|
|
};
|