summaryrefslogtreecommitdiff
path: root/src/channel-playback.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/channel-playback.c')
-rw-r--r--src/channel-playback.c496
1 files changed, 496 insertions, 0 deletions
diff --git a/src/channel-playback.c b/src/channel-playback.c
new file mode 100644
index 0000000..d8a181e
--- /dev/null
+++ b/src/channel-playback.c
@@ -0,0 +1,496 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2010 Red Hat, Inc.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+#include "config.h"
+
+#include "spice-client.h"
+#include "spice-common.h"
+#include "spice-channel-priv.h"
+#include "spice-session-priv.h"
+
+#include "spice-marshal.h"
+
+#include "common/snd_codec.h"
+
+/**
+ * SECTION:channel-playback
+ * @short_description: audio stream for playback
+ * @title: Playback Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and #SpiceAudio
+ * @stability: Stable
+ * @include: channel-playback.h
+ *
+ * #SpicePlaybackChannel class handles an audio playback stream. The
+ * audio data is received via #SpicePlaybackChannel::playback-data
+ * signal, and is controlled by the guest with
+ * #SpicePlaybackChannel::playback-stop and
+ * #SpicePlaybackChannel::playback-start signal events.
+ *
+ * Note: You may be interested to let the #SpiceAudio class play and
+ * record audio channels for your application.
+ */
+
+#define SPICE_PLAYBACK_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_PLAYBACK_CHANNEL, SpicePlaybackChannelPrivate))
+
+struct _SpicePlaybackChannelPrivate {
+ int mode;
+ SndCodec codec;
+ guint32 frame_count;
+ guint32 last_time;
+ guint8 nchannels;
+ guint16 *volume;
+ guint8 mute;
+ gboolean is_active;
+ guint32 latency;
+ guint32 min_latency;
+};
+
+G_DEFINE_TYPE(SpicePlaybackChannel, spice_playback_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_NCHANNELS,
+ PROP_VOLUME,
+ PROP_MUTE,
+ PROP_MIN_LATENCY,
+};
+
+/* Signals */
+enum {
+ SPICE_PLAYBACK_START,
+ SPICE_PLAYBACK_DATA,
+ SPICE_PLAYBACK_STOP,
+ SPICE_PLAYBACK_GET_DELAY,
+
+ SPICE_PLAYBACK_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_PLAYBACK_LAST_SIGNAL];
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+/* ------------------------------------------------------------------ */
+
+#define SPICE_PLAYBACK_DEFAULT_LATENCY_MS 200
+
+static void spice_playback_channel_reset_capabilities(SpiceChannel *channel)
+{
+ if (!g_getenv("SPICE_DISABLE_CELT"))
+ if (snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_CELT_0_5_1, SND_CODEC_ANY_FREQUENCY))
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_CELT_0_5_1);
+ if (!g_getenv("SPICE_DISABLE_OPUS"))
+ if (snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_OPUS, SND_CODEC_ANY_FREQUENCY))
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_OPUS);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_VOLUME);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_LATENCY);
+}
+
+static void spice_playback_channel_init(SpicePlaybackChannel *channel)
+{
+ channel->priv = SPICE_PLAYBACK_CHANNEL_GET_PRIVATE(channel);
+
+ spice_playback_channel_reset_capabilities(SPICE_CHANNEL(channel));
+}
+
+static void spice_playback_channel_finalize(GObject *obj)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(obj)->priv;
+
+ snd_codec_destroy(&c->codec);
+
+ g_free(c->volume);
+ c->volume = NULL;
+
+ if (G_OBJECT_CLASS(spice_playback_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_playback_channel_parent_class)->finalize(obj);
+}
+
+static void spice_playback_channel_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpicePlaybackChannel *channel = SPICE_PLAYBACK_CHANNEL(gobject);
+ SpicePlaybackChannelPrivate *c = channel->priv;
+
+ switch (prop_id) {
+ case PROP_VOLUME:
+ g_value_set_pointer(value, c->volume);
+ break;
+ case PROP_NCHANNELS:
+ g_value_set_uint(value, c->nchannels);
+ break;
+ case PROP_MUTE:
+ g_value_set_boolean(value, c->mute);
+ break;
+ case PROP_MIN_LATENCY:
+ g_value_set_uint(value, c->min_latency);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_playback_channel_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id) {
+ case PROP_VOLUME:
+ /* TODO: request guest volume change */
+ break;
+ case PROP_MUTE:
+ /* TODO: request guest mute change */
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+/* main or coroutine context */
+static void spice_playback_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+
+ snd_codec_destroy(&c->codec);
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_STOP], 0);
+ c->is_active = FALSE;
+
+ SPICE_CHANNEL_CLASS(spice_playback_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_playback_channel_class_init(SpicePlaybackChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_playback_channel_finalize;
+ gobject_class->get_property = spice_playback_channel_get_property;
+ gobject_class->set_property = spice_playback_channel_set_property;
+
+ channel_class->channel_reset = spice_playback_channel_reset;
+ channel_class->channel_reset_capabilities = spice_playback_channel_reset_capabilities;
+
+ g_object_class_install_property
+ (gobject_class, PROP_NCHANNELS,
+ g_param_spec_uint("nchannels",
+ "Number of Channels",
+ "Number of Channels",
+ 0, G_MAXUINT8, 2,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_VOLUME,
+ g_param_spec_pointer("volume",
+ "Playback volume",
+ "Playback volume",
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_MUTE,
+ g_param_spec_boolean("mute",
+ "Mute",
+ "Mute",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property
+ (gobject_class, PROP_MIN_LATENCY,
+ g_param_spec_uint("min-latency",
+ "Playback min buffer size (ms)",
+ "Playback min buffer size (ms)",
+ 0, G_MAXUINT32, SPICE_PLAYBACK_DEFAULT_LATENCY_MS,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+ /**
+ * SpicePlaybackChannel::playback-start:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ * @format: a #SPICE_AUDIO_FMT
+ * @channels: number of channels
+ * @rate: audio rate
+ * @latency: minimum playback latency in ms
+ *
+ * Notify when the playback should start, and provide audio format
+ * characteristics.
+ **/
+ signals[SPICE_PLAYBACK_START] =
+ g_signal_new("playback-start",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpicePlaybackChannelClass, playback_start),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT_INT,
+ G_TYPE_NONE,
+ 3,
+ G_TYPE_INT, G_TYPE_INT, G_TYPE_INT);
+
+ /**
+ * SpicePlaybackChannel::playback-data:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ * @data: pointer to audio data
+ * @data_size: size in byte of @data
+ *
+ * Provide audio data to be played.
+ **/
+ signals[SPICE_PLAYBACK_DATA] =
+ g_signal_new("playback-data",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpicePlaybackChannelClass, playback_data),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__POINTER_INT,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_POINTER, G_TYPE_INT);
+
+ /**
+ * SpicePlaybackChannel::playback-stop:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ *
+ * Notify when the playback should stop.
+ **/
+ signals[SPICE_PLAYBACK_STOP] =
+ g_signal_new("playback-stop",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpicePlaybackChannelClass, playback_stop),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * SpicePlaybackChannel::playback-get-delay:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ *
+ * Notify when the current playback delay is requested
+ **/
+ signals[SPICE_PLAYBACK_GET_DELAY] =
+ g_signal_new("playback-get-delay",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ g_type_class_add_private(klass, sizeof(SpicePlaybackChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* ------------------------------------------------------------------ */
+
+/* coroutine context */
+static void playback_handle_data(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackPacket *packet = spice_msg_in_parsed(in);
+
+#ifdef DEBUG
+ CHANNEL_DEBUG(channel, "%s: time %d data %p size %d", __FUNCTION__,
+ packet->time, packet->data, packet->data_size);
+#endif
+
+ if (c->last_time > packet->time)
+ g_warn_if_reached();
+
+ c->last_time = packet->time;
+
+ uint8_t *data = packet->data;
+ int n = packet->data_size;
+ uint8_t pcm[SND_CODEC_MAX_FRAME_SIZE * 2 * 2];
+
+ if (c->mode != SPICE_AUDIO_DATA_MODE_RAW) {
+ n = sizeof(pcm);
+ data = pcm;
+
+ if (snd_codec_decode(c->codec, packet->data, packet->data_size,
+ pcm, &n) != SND_CODEC_OK) {
+ g_warning("snd_codec_decode() error");
+ return;
+ }
+ }
+
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_DATA], 0, data, n);
+
+ if ((c->frame_count++ % 100) == 0) {
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_GET_DELAY], 0);
+ }
+}
+
+/* coroutine context */
+static void playback_handle_mode(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackMode *mode = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "%s: time %d mode %d data %p size %d", __FUNCTION__,
+ mode->time, mode->mode, mode->data, mode->data_size);
+
+ c->mode = mode->mode;
+ switch (c->mode) {
+ case SPICE_AUDIO_DATA_MODE_RAW:
+ case SPICE_AUDIO_DATA_MODE_CELT_0_5_1:
+ case SPICE_AUDIO_DATA_MODE_OPUS:
+ break;
+ default:
+ g_warning("%s: unhandled mode", __FUNCTION__);
+ break;
+ }
+}
+
+/* coroutine context */
+static void playback_handle_start(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackStart *start = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "%s: fmt %d channels %d freq %d time %d", __FUNCTION__,
+ start->format, start->channels, start->frequency, start->time);
+
+ c->frame_count = 0;
+ c->last_time = start->time;
+ c->is_active = TRUE;
+ c->min_latency = SPICE_PLAYBACK_DEFAULT_LATENCY_MS;
+ snd_codec_destroy(&c->codec);
+
+ if (c->mode != SPICE_AUDIO_DATA_MODE_RAW) {
+ if (snd_codec_create(&c->codec, c->mode, start->frequency, SND_CODEC_DECODE) != SND_CODEC_OK) {
+ g_warning("create decoder failed");
+ return;
+ }
+ }
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_START], 0,
+ start->format, start->channels, start->frequency);
+}
+
+/* coroutine context */
+static void playback_handle_stop(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_STOP], 0);
+ c->is_active = FALSE;
+}
+
+/* coroutine context */
+static void playback_handle_set_volume(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgAudioVolume *vol = spice_msg_in_parsed(in);
+
+ if (vol->nchannels == 0) {
+ g_warning("spice-server send audio-volume-msg with 0 channels");
+ return;
+ }
+
+ g_free(c->volume);
+ c->nchannels = vol->nchannels;
+ c->volume = g_new(guint16, c->nchannels);
+ memcpy(c->volume, vol->volume, sizeof(guint16) * c->nchannels);
+ g_coroutine_object_notify(G_OBJECT(channel), "volume");
+}
+
+/* coroutine context */
+static void playback_handle_set_mute(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgAudioMute *m = spice_msg_in_parsed(in);
+
+ c->mute = m->mute;
+ g_coroutine_object_notify(G_OBJECT(channel), "mute");
+}
+
+/* coroutine context */
+static void playback_handle_set_latency(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackLatency *msg = spice_msg_in_parsed(in);
+
+ c->min_latency = msg->latency_ms;
+ SPICE_DEBUG("%s: notify latency update %u", __FUNCTION__, c->min_latency);
+ g_coroutine_object_notify(G_OBJECT(channel), "min-latency");
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_PLAYBACK_DATA ] = playback_handle_data,
+ [ SPICE_MSG_PLAYBACK_MODE ] = playback_handle_mode,
+ [ SPICE_MSG_PLAYBACK_START ] = playback_handle_start,
+ [ SPICE_MSG_PLAYBACK_STOP ] = playback_handle_stop,
+ [ SPICE_MSG_PLAYBACK_VOLUME ] = playback_handle_set_volume,
+ [ SPICE_MSG_PLAYBACK_MUTE ] = playback_handle_set_mute,
+ [ SPICE_MSG_PLAYBACK_LATENCY ] = playback_handle_set_latency,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+void spice_playback_channel_set_delay(SpicePlaybackChannel *channel, guint32 delay_ms)
+{
+ SpicePlaybackChannelPrivate *c;
+ SpiceSession *session;
+
+ g_return_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel));
+
+ CHANNEL_DEBUG(channel, "playback set_delay %u ms", delay_ms);
+
+ c = channel->priv;
+ c->latency = delay_ms;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(channel));
+ if (session) {
+ spice_session_set_mm_time(session, c->last_time - delay_ms);
+ } else {
+ CHANNEL_DEBUG(channel, "channel detached from session, mm time skipped");
+ }
+}
+
+G_GNUC_INTERNAL
+gboolean spice_playback_channel_is_active(SpicePlaybackChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel), FALSE);
+ return channel->priv->is_active;
+}
+
+G_GNUC_INTERNAL
+guint32 spice_playback_channel_get_latency(SpicePlaybackChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel), 0);
+ if (!channel->priv->is_active) {
+ return 0;
+ }
+ return channel->priv->latency;
+}
+
+G_GNUC_INTERNAL
+void spice_playback_channel_sync_latency(SpicePlaybackChannel *channel)
+{
+ g_return_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel));
+ g_return_if_fail(channel->priv->is_active);
+ SPICE_DEBUG("%s: notify latency update %u", __FUNCTION__, channel->priv->min_latency);
+ g_coroutine_object_notify(G_OBJECT(SPICE_CHANNEL(channel)), "min-latency");
+}