/* -*- 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 . */ #include #include "spice-client.h" #include "spice-common.h" #include "spice-channel-priv.h" #include "spice-marshal.h" #include "spice-session-priv.h" /** * SECTION:channel-record * @short_description: audio stream for recording * @title: Record Channel * @section_id: * @see_also: #SpiceChannel, and #SpiceAudio * @stability: Stable * @include: channel-record.h * * #SpiceRecordChannel class handles an audio recording stream. The * audio stream should start when #SpiceRecordChannel::record-start is * emitted and should be stopped when #SpiceRecordChannel::record-stop * is received. * * The audio is sent to the guest by calling spice_record_send_data() * with the recorded PCM data. * * Note: You may be interested to let the #SpiceAudio class play and * record audio channels for your application. */ #define SPICE_RECORD_CHANNEL_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_RECORD_CHANNEL, SpiceRecordChannelPrivate)) struct _SpiceRecordChannelPrivate { int mode; gboolean started; CELTMode *celt_mode; CELTEncoder *celt_encoder; gsize frame_bytes; guint8 *last_frame; gsize last_frame_current; guint8 nchannels; guint16 *volume; guint8 mute; }; G_DEFINE_TYPE(SpiceRecordChannel, spice_record_channel, SPICE_TYPE_CHANNEL) /* Properties */ enum { PROP_0, PROP_NCHANNELS, PROP_VOLUME, PROP_MUTE, }; /* Signals */ enum { SPICE_RECORD_START, SPICE_RECORD_STOP, SPICE_RECORD_LAST_SIGNAL, }; static guint signals[SPICE_RECORD_LAST_SIGNAL]; static void spice_record_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg); static void channel_up(SpiceChannel *channel); #define FRAME_SIZE 256 #define CELT_BIT_RATE (64 * 1024) /* ------------------------------------------------------------------ */ static void spice_record_channel_init(SpiceRecordChannel *channel) { channel->priv = SPICE_RECORD_CHANNEL_GET_PRIVATE(channel); spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_RECORD_CAP_CELT_0_5_1); spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_RECORD_CAP_VOLUME); } static void spice_record_channel_finalize(GObject *obj) { SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(obj)->priv; g_free(c->last_frame); c->last_frame = NULL; if (c->celt_encoder) { celt051_encoder_destroy(c->celt_encoder); c->celt_encoder = NULL; } if (c->celt_mode) { celt051_mode_destroy(c->celt_mode); c->celt_mode = NULL; } g_free(c->volume); c->volume = NULL; if (G_OBJECT_CLASS(spice_record_channel_parent_class)->finalize) G_OBJECT_CLASS(spice_record_channel_parent_class)->finalize(obj); } static void spice_record_channel_get_property(GObject *gobject, guint prop_id, GValue *value, GParamSpec *pspec) { SpiceRecordChannel *channel = SPICE_RECORD_CHANNEL(gobject); SpiceRecordChannelPrivate *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; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; } } static void spice_record_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; } } static void channel_reset(SpiceChannel *channel, gboolean migrating) { SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv; g_free(c->last_frame); c->last_frame = NULL; if (c->celt_encoder) { celt051_encoder_destroy(c->celt_encoder); c->celt_encoder = NULL; } if (c->celt_mode) { celt051_mode_destroy(c->celt_mode); c->celt_mode = NULL; } SPICE_CHANNEL_CLASS(spice_record_channel_parent_class)->channel_reset(channel, migrating); } static void spice_record_channel_class_init(SpiceRecordChannelClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass); gobject_class->finalize = spice_record_channel_finalize; gobject_class->get_property = spice_record_channel_get_property; gobject_class->set_property = spice_record_channel_set_property; channel_class->handle_msg = spice_record_handle_msg; channel_class->channel_up = channel_up; channel_class->channel_reset = channel_reset; 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", "", 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)); /** * SpiceRecordChannel::record-start: * @channel: the #SpiceRecordChannel that emitted the signal * @format: a #SPICE_AUDIO_FMT * @channels: number of channels * @rate: audio rate * * Notify when the recording should start, and provide audio format * characteristics. **/ signals[SPICE_RECORD_START] = g_signal_new("record-start", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceRecordChannelClass, record_start), NULL, NULL, g_cclosure_user_marshal_VOID__INT_INT_INT, G_TYPE_NONE, 3, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT); /** * SpiceRecordChannel::record-stop: * @channel: the #SpiceRecordChannel that emitted the signal * * Notify when the recording should stop. **/ signals[SPICE_RECORD_STOP] = g_signal_new("record-stop", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceRecordChannelClass, record_stop), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); g_type_class_add_private(klass, sizeof(SpiceRecordChannelPrivate)); } /* signal trampoline---------------------------------------------------------- */ struct SPICE_RECORD_START { gint format; gint channels; gint frequency; }; struct SPICE_RECORD_STOP { }; /* main context */ static void do_emit_main_context(GObject *object, int signum, gpointer params) { switch (signum) { case SPICE_RECORD_START: { struct SPICE_RECORD_START *p = params; g_signal_emit(object, signals[signum], 0, p->format, p->channels, p->frequency); break; } case SPICE_RECORD_STOP: { g_signal_emit(object, signals[signum], 0); break; } default: g_warn_if_reached(); } } /* main context */ static void spice_record_mode(SpiceRecordChannel *channel, uint32_t time, uint32_t mode, uint8_t *data, uint32_t data_size) { SpiceMsgcRecordMode m = {0, }; SpiceMsgOut *msg; g_return_if_fail(channel != NULL); if (spice_channel_get_read_only(SPICE_CHANNEL(channel))) return; m.mode = mode; m.time = time; m.data = data; m.data_size = data_size; msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_RECORD_MODE); msg->marshallers->msgc_record_mode(msg->marshaller, &m); spice_msg_out_send(msg); } /* coroutine context */ static void channel_up(SpiceChannel *channel) { SpiceRecordChannelPrivate *rc; rc = SPICE_RECORD_CHANNEL(channel)->priv; if (spice_channel_test_capability(channel, SPICE_RECORD_CAP_CELT_0_5_1)) { rc->mode = SPICE_AUDIO_DATA_MODE_CELT_0_5_1; } else { rc->mode = SPICE_AUDIO_DATA_MODE_RAW; } } /* main context */ static void spice_record_start_mark(SpiceRecordChannel *channel, uint32_t time) { SpiceMsgcRecordStartMark m = {0, }; SpiceMsgOut *msg; g_return_if_fail(channel != NULL); if (spice_channel_get_read_only(SPICE_CHANNEL(channel))) return; m.time = time; msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_RECORD_START_MARK); msg->marshallers->msgc_record_start_mark(msg->marshaller, &m); spice_msg_out_send(msg); } /** * spice_record_send_data: * @channel: * @data: PCM data * @bytes: size of @data * @time: stream timestamp * * Send recorded PCM data to the guest. **/ void spice_record_send_data(SpiceRecordChannel *channel, gpointer data, gsize bytes, uint32_t time) { SpiceRecordChannelPrivate *rc; SpiceMsgcRecordPacket p = {0, }; int celt_compressed_frame_bytes = FRAME_SIZE * CELT_BIT_RATE / 44100 / 8; uint8_t *celt_buf = NULL; g_return_if_fail(channel != NULL); g_return_if_fail(spice_channel_get_read_only(SPICE_CHANNEL(channel)) == FALSE); rc = channel->priv; if (!rc->started) { spice_record_mode(channel, time, rc->mode, NULL, 0); spice_record_start_mark(channel, time); rc->started = TRUE; } if (rc->mode == SPICE_AUDIO_DATA_MODE_CELT_0_5_1) celt_buf = g_alloca(celt_compressed_frame_bytes); p.time = time; while (bytes > 0) { gsize n; int frame_size; SpiceMsgOut *msg; uint8_t *frame; if (rc->last_frame_current > 0) { /* complete previous frame */ n = MIN(bytes, rc->frame_bytes - rc->last_frame_current); memcpy(rc->last_frame + rc->last_frame_current, data, n); rc->last_frame_current += n; if (rc->last_frame_current < rc->frame_bytes) /* if the frame is still incomplete, return */ break; frame = rc->last_frame; frame_size = rc->frame_bytes; } else { n = MIN(bytes, rc->frame_bytes); frame_size = n; frame = data; } if (rc->last_frame_current == 0 && n < rc->frame_bytes) { /* start a new frame */ memcpy(rc->last_frame, data, n); rc->last_frame_current = n; break; } if (rc->mode == SPICE_AUDIO_DATA_MODE_CELT_0_5_1) { frame_size = celt051_encode(rc->celt_encoder, (celt_int16_t *)frame, NULL, celt_buf, celt_compressed_frame_bytes); if (frame_size < 0) { g_warning("celt encode failed"); return; } frame = celt_buf; } msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_RECORD_DATA); msg->marshallers->msgc_record_data(msg->marshaller, &p); spice_marshaller_add(msg->marshaller, frame, frame_size); spice_msg_out_send(msg); if (rc->last_frame_current == rc->frame_bytes) rc->last_frame_current = 0; bytes -= n; data = (guint8*)data + n; } } /* ------------------------------------------------------------------ */ /* coroutine context */ static void record_handle_start(SpiceChannel *channel, SpiceMsgIn *in) { SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv; SpiceMsgRecordStart *start = spice_msg_in_parsed(in); SPICE_DEBUG("%s: fmt %d channels %d freq %d", __FUNCTION__, start->format, start->channels, start->frequency); c->frame_bytes = FRAME_SIZE * 16 * start->channels / 8; g_free(c->last_frame); c->last_frame = g_malloc(c->frame_bytes); c->last_frame_current = 0; switch (c->mode) { case SPICE_AUDIO_DATA_MODE_RAW: emit_main_context(channel, SPICE_RECORD_START, start->format, start->channels, start->frequency); break; case SPICE_AUDIO_DATA_MODE_CELT_0_5_1: { int celt_mode_err; g_return_if_fail(start->format == SPICE_AUDIO_FMT_S16); if (!c->celt_mode) c->celt_mode = celt051_mode_create(start->frequency, start->channels, FRAME_SIZE, &celt_mode_err); if (!c->celt_mode) g_warning("Failed to create celt mode"); if (!c->celt_encoder) c->celt_encoder = celt051_encoder_create(c->celt_mode); if (!c->celt_encoder) g_warning("Failed to create celt encoder"); emit_main_context(channel, SPICE_RECORD_START, start->format, start->channels, start->frequency); break; } default: g_warning("%s: unhandled mode %d", __FUNCTION__, c->mode); break; } } /* coroutine context */ static void record_handle_stop(SpiceChannel *channel, SpiceMsgIn *in) { SpiceRecordChannelPrivate *rc = SPICE_RECORD_CHANNEL(channel)->priv; emit_main_context(channel, SPICE_RECORD_STOP); rc->started = FALSE; } /* coroutine context */ static void record_handle_set_volume(SpiceChannel *channel, SpiceMsgIn *in) { SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv; SpiceMsgAudioVolume *vol = spice_msg_in_parsed(in); 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_object_notify_main_context(G_OBJECT(channel), "volume"); } /* coroutine context */ static void record_handle_set_mute(SpiceChannel *channel, SpiceMsgIn *in) { SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv; SpiceMsgAudioMute *m = spice_msg_in_parsed(in); c->mute = m->mute; g_object_notify_main_context(G_OBJECT(channel), "mute"); } static const spice_msg_handler record_handlers[] = { [ SPICE_MSG_RECORD_START ] = record_handle_start, [ SPICE_MSG_RECORD_STOP ] = record_handle_stop, [ SPICE_MSG_RECORD_VOLUME ] = record_handle_set_volume, [ SPICE_MSG_RECORD_MUTE ] = record_handle_set_mute, }; /* coroutine context */ static void spice_record_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg) { int type = spice_msg_in_type(msg); SpiceChannelClass *parent_class; g_return_if_fail(type < SPICE_N_ELEMENTS(record_handlers)); parent_class = SPICE_CHANNEL_CLASS(spice_record_channel_parent_class); if (record_handlers[type] != NULL) record_handlers[type](channel, msg); else if (parent_class->handle_msg) parent_class->handle_msg(channel, msg); else g_return_if_reached(); }