mirror of
https://github.com/morgan9e/grd
synced 2026-04-14 00:14:18 +09:00
388 lines
13 KiB
C
388 lines
13 KiB
C
/*
|
|
* Copyright (C) 2022 Pascal Nowack
|
|
*
|
|
* 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 "config.h"
|
|
|
|
#include "grd-rdp-audio-output-stream.h"
|
|
|
|
#include <gio/gio.h>
|
|
#include <spa/param/audio/format-utils.h>
|
|
#include <spa/param/props.h>
|
|
|
|
#include "grd-rdp-dvc-audio-playback.h"
|
|
|
|
struct _GrdRdpAudioOutputStream
|
|
{
|
|
GObject parent;
|
|
|
|
uint32_t target_node_id;
|
|
|
|
GrdRdpDvcAudioPlayback *audio_playback;
|
|
|
|
struct pw_node *pipewire_node;
|
|
struct spa_hook pipewire_node_listener;
|
|
|
|
struct pw_stream *pipewire_stream;
|
|
struct spa_hook pipewire_stream_listener;
|
|
|
|
GMutex volume_mutex;
|
|
gboolean audio_muted;
|
|
float volumes[SPA_AUDIO_MAX_CHANNELS];
|
|
uint32_t n_volumes;
|
|
};
|
|
|
|
G_DEFINE_TYPE (GrdRdpAudioOutputStream, grd_rdp_audio_output_stream, G_TYPE_OBJECT)
|
|
|
|
void
|
|
grd_rdp_audio_output_stream_set_active (GrdRdpAudioOutputStream *audio_output_stream,
|
|
gboolean active)
|
|
{
|
|
pw_stream_set_active (audio_output_stream->pipewire_stream, active);
|
|
}
|
|
|
|
void
|
|
grd_rdp_audio_output_stream_get_volume_data (GrdRdpAudioOutputStream *audio_output_stream,
|
|
GrdRdpAudioVolumeData *volume_data)
|
|
{
|
|
uint32_t i;
|
|
|
|
g_mutex_lock (&audio_output_stream->volume_mutex);
|
|
volume_data->audio_muted = audio_output_stream->audio_muted;
|
|
volume_data->n_volumes = audio_output_stream->n_volumes;
|
|
|
|
for (i = 0; i < audio_output_stream->n_volumes; ++i)
|
|
volume_data->volumes[i] = audio_output_stream->volumes[i];
|
|
g_mutex_unlock (&audio_output_stream->volume_mutex);
|
|
}
|
|
|
|
static void
|
|
pipewire_node_info (void *user_data,
|
|
const struct pw_node_info *info)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream = user_data;
|
|
uint32_t i;
|
|
|
|
if (!(info->change_mask & PW_NODE_CHANGE_MASK_PARAMS))
|
|
return;
|
|
|
|
for (i = 0; i < info->n_params; ++i)
|
|
{
|
|
if (info->params[i].id == SPA_PARAM_Props &&
|
|
info->params[i].flags & SPA_PARAM_INFO_READ)
|
|
{
|
|
pw_node_enum_params (audio_output_stream->pipewire_node,
|
|
0, SPA_PARAM_Props, 0, -1, NULL);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
pipewire_node_param (void *user_data,
|
|
int seq,
|
|
uint32_t id,
|
|
uint32_t index,
|
|
uint32_t next,
|
|
const struct spa_pod *param)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream = user_data;
|
|
const struct spa_pod_object *object = (const struct spa_pod_object *) param;
|
|
struct spa_pod_prop *prop;
|
|
|
|
if (id != SPA_PARAM_Props)
|
|
return;
|
|
|
|
SPA_POD_OBJECT_FOREACH (object, prop)
|
|
{
|
|
if (prop->key == SPA_PROP_mute)
|
|
{
|
|
bool mute;
|
|
|
|
if (spa_pod_get_bool (&prop->value, &mute) < 0)
|
|
continue;
|
|
|
|
g_mutex_lock (&audio_output_stream->volume_mutex);
|
|
audio_output_stream->audio_muted = !!mute;
|
|
g_mutex_unlock (&audio_output_stream->volume_mutex);
|
|
}
|
|
else if (prop->key == SPA_PROP_channelVolumes)
|
|
{
|
|
g_mutex_lock (&audio_output_stream->volume_mutex);
|
|
audio_output_stream->n_volumes =
|
|
spa_pod_copy_array (&prop->value, SPA_TYPE_Float,
|
|
audio_output_stream->volumes,
|
|
SPA_AUDIO_MAX_CHANNELS);
|
|
g_mutex_unlock (&audio_output_stream->volume_mutex);
|
|
}
|
|
}
|
|
}
|
|
|
|
static const struct pw_node_events pipewire_node_events =
|
|
{
|
|
.version = PW_VERSION_NODE_EVENTS,
|
|
.info = pipewire_node_info,
|
|
.param = pipewire_node_param,
|
|
};
|
|
|
|
static void
|
|
pipewire_stream_state_changed (void *user_data,
|
|
enum pw_stream_state old,
|
|
enum pw_stream_state state,
|
|
const char *error)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream = user_data;
|
|
|
|
g_debug ("[RDP.AUDIO_PLAYBACK] Node %u: PipeWire stream state changed from "
|
|
"%s to %s", audio_output_stream->target_node_id,
|
|
pw_stream_state_as_string (old),
|
|
pw_stream_state_as_string (state));
|
|
|
|
switch (state)
|
|
{
|
|
case PW_STREAM_STATE_ERROR:
|
|
g_warning ("[RDP.AUDIO_PLAYBACK] Node %u: PipeWire stream error: %s",
|
|
audio_output_stream->target_node_id, error);
|
|
break;
|
|
case PW_STREAM_STATE_PAUSED:
|
|
case PW_STREAM_STATE_STREAMING:
|
|
case PW_STREAM_STATE_UNCONNECTED:
|
|
case PW_STREAM_STATE_CONNECTING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
pipewire_stream_param_changed (void *user_data,
|
|
uint32_t id,
|
|
const struct spa_pod *param)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream = user_data;
|
|
struct spa_pod_builder pod_builder;
|
|
const struct spa_pod *params[1];
|
|
uint8_t params_buffer[1024];
|
|
|
|
if (!param || id != SPA_PARAM_Format)
|
|
return;
|
|
|
|
pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer));
|
|
|
|
params[0] = spa_pod_builder_add_object (
|
|
&pod_builder,
|
|
SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
|
|
SPA_PARAM_BUFFERS_dataType, SPA_POD_Int (1 << SPA_DATA_MemPtr));
|
|
|
|
pw_stream_update_params (audio_output_stream->pipewire_stream, params, 1);
|
|
}
|
|
|
|
static void
|
|
pipewire_stream_process (void *user_data)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream = user_data;
|
|
struct pw_buffer *next_buffer;
|
|
struct pw_buffer *buffer = NULL;
|
|
int16_t *data;
|
|
uint32_t offset;
|
|
uint32_t size;
|
|
|
|
next_buffer = pw_stream_dequeue_buffer (audio_output_stream->pipewire_stream);
|
|
while (next_buffer)
|
|
{
|
|
buffer = next_buffer;
|
|
next_buffer = pw_stream_dequeue_buffer (audio_output_stream->pipewire_stream);
|
|
|
|
if (next_buffer)
|
|
pw_stream_queue_buffer (audio_output_stream->pipewire_stream, buffer);
|
|
}
|
|
if (!buffer)
|
|
return;
|
|
|
|
offset = buffer->buffer->datas[0].chunk->offset;
|
|
size = buffer->buffer->datas[0].chunk->size;
|
|
data = buffer->buffer->datas[0].data + offset;
|
|
|
|
if (size > 0)
|
|
{
|
|
GrdRdpDvcAudioPlayback *audio_playback =
|
|
audio_output_stream->audio_playback;
|
|
uint32_t node_id = audio_output_stream->target_node_id;
|
|
GrdRdpAudioVolumeData volume_data = {};
|
|
|
|
grd_rdp_audio_output_stream_get_volume_data (audio_output_stream,
|
|
&volume_data);
|
|
grd_rdp_dvc_audio_playback_maybe_submit_samples (audio_playback, node_id,
|
|
&volume_data, data, size);
|
|
}
|
|
|
|
pw_stream_queue_buffer (audio_output_stream->pipewire_stream, buffer);
|
|
}
|
|
|
|
static const struct pw_stream_events pipewire_stream_events =
|
|
{
|
|
.version = PW_VERSION_STREAM_EVENTS,
|
|
.state_changed = pipewire_stream_state_changed,
|
|
.param_changed = pipewire_stream_param_changed,
|
|
.process = pipewire_stream_process,
|
|
};
|
|
|
|
static gboolean
|
|
connect_to_stream (GrdRdpAudioOutputStream *audio_output_stream,
|
|
struct pw_core *pipewire_core,
|
|
uint32_t target_node_id,
|
|
uint32_t n_samples_per_sec,
|
|
uint32_t n_channels,
|
|
uint32_t *position,
|
|
GError **error)
|
|
{
|
|
struct spa_pod_builder pod_builder;
|
|
const struct spa_pod *params[1];
|
|
uint8_t params_buffer[1024];
|
|
int result;
|
|
|
|
pod_builder = SPA_POD_BUILDER_INIT (params_buffer, sizeof (params_buffer));
|
|
params[0] = spa_pod_builder_add_object (
|
|
&pod_builder,
|
|
SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
|
|
SPA_FORMAT_mediaType, SPA_POD_Id (SPA_MEDIA_TYPE_audio),
|
|
SPA_FORMAT_mediaSubtype, SPA_POD_Id (SPA_MEDIA_SUBTYPE_raw),
|
|
SPA_FORMAT_AUDIO_format, SPA_POD_Id (SPA_AUDIO_FORMAT_S16),
|
|
SPA_FORMAT_AUDIO_rate, SPA_POD_Int (n_samples_per_sec),
|
|
SPA_FORMAT_AUDIO_channels, SPA_POD_Int (n_channels),
|
|
SPA_FORMAT_AUDIO_position, SPA_POD_Array (sizeof (uint32_t), SPA_TYPE_Id,
|
|
n_channels, position),
|
|
0);
|
|
|
|
audio_output_stream->pipewire_stream =
|
|
pw_stream_new (pipewire_core,
|
|
"GRD::RDP::AUDIO_PLAYBACK",
|
|
pw_properties_new (PW_KEY_MEDIA_TYPE, "Audio",
|
|
PW_KEY_MEDIA_CATEGORY, "Capture",
|
|
PW_KEY_NODE_FORCE_QUANTUM, "256",
|
|
NULL));
|
|
if (!audio_output_stream->pipewire_stream)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Failed to create PipeWire stream");
|
|
return FALSE;
|
|
}
|
|
|
|
pw_stream_add_listener (audio_output_stream->pipewire_stream,
|
|
&audio_output_stream->pipewire_stream_listener,
|
|
&pipewire_stream_events, audio_output_stream);
|
|
|
|
result = pw_stream_connect (audio_output_stream->pipewire_stream,
|
|
PW_DIRECTION_INPUT, target_node_id,
|
|
PW_STREAM_FLAG_RT_PROCESS |
|
|
PW_STREAM_FLAG_AUTOCONNECT |
|
|
PW_STREAM_FLAG_INACTIVE |
|
|
PW_STREAM_FLAG_MAP_BUFFERS,
|
|
params, 1);
|
|
if (result < 0)
|
|
{
|
|
g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (-result),
|
|
strerror (-result));
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GrdRdpAudioOutputStream *
|
|
grd_rdp_audio_output_stream_new (GrdRdpDvcAudioPlayback *audio_playback,
|
|
struct pw_core *pipewire_core,
|
|
struct pw_registry *pipewire_registry,
|
|
uint32_t target_node_id,
|
|
uint32_t n_samples_per_sec,
|
|
uint32_t n_channels,
|
|
uint32_t *position,
|
|
GError **error)
|
|
{
|
|
g_autoptr (GrdRdpAudioOutputStream) audio_output_stream = NULL;
|
|
|
|
audio_output_stream = g_object_new (GRD_TYPE_RDP_AUDIO_OUTPUT_STREAM, NULL);
|
|
audio_output_stream->audio_playback = audio_playback;
|
|
audio_output_stream->target_node_id = target_node_id;
|
|
|
|
audio_output_stream->pipewire_node = pw_registry_bind (pipewire_registry,
|
|
target_node_id,
|
|
PW_TYPE_INTERFACE_Node,
|
|
PW_VERSION_NODE, 0);
|
|
if (!audio_output_stream->pipewire_node)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Failed to bind PipeWire node");
|
|
return NULL;
|
|
}
|
|
|
|
pw_node_add_listener (audio_output_stream->pipewire_node,
|
|
&audio_output_stream->pipewire_node_listener,
|
|
&pipewire_node_events, audio_output_stream);
|
|
|
|
if (!connect_to_stream (audio_output_stream, pipewire_core, target_node_id,
|
|
n_samples_per_sec, n_channels, position, error))
|
|
return NULL;
|
|
|
|
return g_steal_pointer (&audio_output_stream);
|
|
}
|
|
|
|
static void
|
|
grd_rdp_audio_output_stream_dispose (GObject *object)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream =
|
|
GRD_RDP_AUDIO_OUTPUT_STREAM (object);
|
|
|
|
if (audio_output_stream->pipewire_stream)
|
|
{
|
|
pw_stream_destroy (audio_output_stream->pipewire_stream);
|
|
audio_output_stream->pipewire_stream = NULL;
|
|
}
|
|
if (audio_output_stream->pipewire_node)
|
|
{
|
|
spa_hook_remove (&audio_output_stream->pipewire_node_listener);
|
|
pw_proxy_destroy ((struct pw_proxy *) audio_output_stream->pipewire_node);
|
|
audio_output_stream->pipewire_node = NULL;
|
|
}
|
|
|
|
G_OBJECT_CLASS (grd_rdp_audio_output_stream_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
grd_rdp_audio_output_stream_finalize (GObject *object)
|
|
{
|
|
GrdRdpAudioOutputStream *audio_output_stream =
|
|
GRD_RDP_AUDIO_OUTPUT_STREAM (object);
|
|
|
|
g_mutex_clear (&audio_output_stream->volume_mutex);
|
|
|
|
G_OBJECT_CLASS (grd_rdp_audio_output_stream_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
grd_rdp_audio_output_stream_init (GrdRdpAudioOutputStream *audio_output_stream)
|
|
{
|
|
g_mutex_init (&audio_output_stream->volume_mutex);
|
|
}
|
|
|
|
static void
|
|
grd_rdp_audio_output_stream_class_init (GrdRdpAudioOutputStreamClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->dispose = grd_rdp_audio_output_stream_dispose;
|
|
object_class->finalize = grd_rdp_audio_output_stream_finalize;
|
|
}
|