mirror of
https://gitlab.uni-freiburg.de/opensourcevdi/spice
synced 2025-12-25 22:18:58 +00:00
Do not offset the time attempting to fix client latency. Client should handle it by itself. This remove entirely the delay introduced by the server. This avoids surely possible time drifts in the client. The server just sends it's concept of time without trying to force any delay. Only one end should handle this delay in an attempt to synchronize audio and video instead that doing it in both ends. Signed-off-by: Frediano Ziglio <freddy77@gmail.com>
1263 lines
36 KiB
C++
1263 lines
36 KiB
C++
/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
/*
|
|
Copyright (C) 2009 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 <cerrno>
|
|
#include <climits>
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/types.h>
|
|
#ifndef _WIN32
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/ip.h>
|
|
#include <netinet/tcp.h>
|
|
#endif
|
|
|
|
#include <common/generated_server_marshallers.h>
|
|
#include <common/snd_codec.h>
|
|
|
|
#include "glib-compat.h"
|
|
#include "spice-wrapped.h"
|
|
#include "red-common.h"
|
|
#include "main-channel.h"
|
|
#include "reds.h"
|
|
#include "red-channel-client.h"
|
|
#include "red-client.h"
|
|
#include "sound.h"
|
|
#include "main-channel-client.h"
|
|
|
|
#define SND_RECEIVE_BUF_SIZE (16 * 1024 * 2)
|
|
#define RECORD_SAMPLES_SIZE (SND_RECEIVE_BUF_SIZE >> 2)
|
|
|
|
enum SndCommand {
|
|
SND_MIGRATE,
|
|
SND_CTRL,
|
|
SND_VOLUME,
|
|
SND_MUTE,
|
|
SND_END_COMMAND,
|
|
};
|
|
|
|
enum PlaybackCommand {
|
|
SND_PLAYBACK_MODE = SND_END_COMMAND,
|
|
SND_PLAYBACK_PCM,
|
|
};
|
|
|
|
#define SND_MIGRATE_MASK (1 << SND_MIGRATE)
|
|
#define SND_CTRL_MASK (1 << SND_CTRL)
|
|
#define SND_VOLUME_MASK (1 << SND_VOLUME)
|
|
#define SND_MUTE_MASK (1 << SND_MUTE)
|
|
#define SND_VOLUME_MUTE_MASK (SND_VOLUME_MASK|SND_MUTE_MASK)
|
|
|
|
#define SND_PLAYBACK_MODE_MASK (1 << SND_PLAYBACK_MODE)
|
|
#define SND_PLAYBACK_PCM_MASK (1 << SND_PLAYBACK_PCM)
|
|
|
|
class SndChannelClient;
|
|
struct SndChannel;
|
|
class PlaybackChannelClient;
|
|
class RecordChannelClient;
|
|
struct AudioFrame;
|
|
struct AudioFrameContainer;
|
|
|
|
enum {
|
|
RED_PIPE_ITEM_PERSISTENT = RED_PIPE_ITEM_TYPE_CHANNEL_BASE,
|
|
};
|
|
|
|
/* This pipe item is never deleted and added to the queue when messages
|
|
* have to be sent.
|
|
* This is used to have a simple item in RedChannelClient queue but to send
|
|
* multiple messages in a row if possible.
|
|
* During realtime sound transmission you usually don't want to queue too
|
|
* much data or having retransmission preferring instead loosing some
|
|
* samples.
|
|
*/
|
|
struct PersistentPipeItem final: public RedPipeItemNum<RED_PIPE_ITEM_PERSISTENT>
|
|
{
|
|
PersistentPipeItem();
|
|
};
|
|
|
|
/* Connects an audio client to a Spice client */
|
|
class SndChannelClient: public RedChannelClient
|
|
{
|
|
public:
|
|
using RedChannelClient::RedChannelClient;
|
|
|
|
bool active;
|
|
bool client_active;
|
|
|
|
uint32_t command;
|
|
|
|
PersistentPipeItem persistent_pipe_item;
|
|
|
|
inline SndChannel* get_channel();
|
|
|
|
bool config_socket() override;
|
|
uint8_t *alloc_recv_buf(uint16_t type, uint32_t size) override;
|
|
void release_recv_buf(uint16_t type, uint32_t size, uint8_t *msg) override;
|
|
void migrate() override;
|
|
|
|
private:
|
|
/* we don't expect very big messages so don't allocate too much
|
|
* bytes, data will be cached in RecordChannelClient::samples */
|
|
uint8_t receive_buf[SND_CODEC_MAX_FRAME_BYTES + 64];
|
|
};
|
|
|
|
static void snd_playback_alloc_frames(PlaybackChannelClient *playback);
|
|
|
|
|
|
struct AudioFrame {
|
|
uint32_t time;
|
|
uint32_t samples[SND_CODEC_MAX_FRAME_SIZE];
|
|
PlaybackChannelClient *client;
|
|
AudioFrame *next;
|
|
AudioFrameContainer *container;
|
|
bool allocated;
|
|
};
|
|
|
|
#define NUM_AUDIO_FRAMES 3
|
|
struct AudioFrameContainer
|
|
{
|
|
int refs;
|
|
AudioFrame items[NUM_AUDIO_FRAMES];
|
|
};
|
|
|
|
class PlaybackChannelClient final: public SndChannelClient
|
|
{
|
|
protected:
|
|
~PlaybackChannelClient() override;
|
|
public:
|
|
PlaybackChannelClient(PlaybackChannel *channel,
|
|
RedClient *client,
|
|
RedStream *stream,
|
|
RedChannelCapabilities *caps);
|
|
bool init() override;
|
|
|
|
AudioFrameContainer *frames = nullptr;
|
|
AudioFrame *free_frames = nullptr;
|
|
AudioFrame *in_progress = nullptr; /* Frame being sent to the client */
|
|
AudioFrame *pending_frame = nullptr; /* Next frame to send to the client */
|
|
SpiceAudioDataMode mode = SPICE_AUDIO_DATA_MODE_RAW;
|
|
SndCodec codec = nullptr;
|
|
uint8_t encode_buf[SND_CODEC_MAX_COMPRESSED_BYTES];
|
|
|
|
static void on_message_marshalled(uint8_t *data, void *opaque);
|
|
protected:
|
|
void send_item(RedPipeItem *item) override;
|
|
};
|
|
|
|
struct SpiceVolumeState {
|
|
uint16_t *volume;
|
|
uint8_t volume_nchannels;
|
|
int mute;
|
|
};
|
|
|
|
/* Base class for PlaybackChannel and RecordChannel */
|
|
struct SndChannel: public RedChannel
|
|
{
|
|
using RedChannel::RedChannel;
|
|
~SndChannel() override;
|
|
void set_peer_common();
|
|
bool active;
|
|
SpiceVolumeState volume;
|
|
uint32_t frequency = SND_CODEC_OPUS_PLAYBACK_FREQ;
|
|
};
|
|
|
|
inline SndChannel* SndChannelClient::get_channel()
|
|
{
|
|
return static_cast<SndChannel*>(RedChannelClient::get_channel());
|
|
}
|
|
|
|
struct PlaybackChannel final: public SndChannel
|
|
{
|
|
explicit PlaybackChannel(RedsState *reds);
|
|
void on_connect(RedClient *client, RedStream *stream,
|
|
int migration, RedChannelCapabilities *caps) override;
|
|
};
|
|
|
|
|
|
struct RecordChannel final: public SndChannel
|
|
{
|
|
explicit RecordChannel(RedsState *reds);
|
|
void on_connect(RedClient *client, RedStream *stream,
|
|
int migration, RedChannelCapabilities *caps) override;
|
|
};
|
|
|
|
|
|
class RecordChannelClient final: public SndChannelClient
|
|
{
|
|
protected:
|
|
~RecordChannelClient() override;
|
|
public:
|
|
using SndChannelClient::SndChannelClient;
|
|
bool init() override;
|
|
|
|
uint32_t samples[RECORD_SAMPLES_SIZE];
|
|
uint32_t write_pos = 0;
|
|
uint32_t read_pos = 0;
|
|
SpiceAudioDataMode mode = SPICE_AUDIO_DATA_MODE_RAW;
|
|
uint32_t mode_time = 0;
|
|
uint32_t start_time = 0;
|
|
SndCodec codec = nullptr;
|
|
uint8_t decode_buf[SND_CODEC_MAX_FRAME_BYTES];
|
|
protected:
|
|
bool handle_message(uint16_t type, uint32_t size, void *message) override;
|
|
void send_item(RedPipeItem *item) override;
|
|
};
|
|
|
|
|
|
/* A list of all Spice{Playback,Record}State objects */
|
|
static GList *snd_channels;
|
|
|
|
static void snd_send(SndChannelClient * client);
|
|
|
|
/* sound channels only support a single client */
|
|
static SndChannelClient *snd_channel_get_client(SndChannel *channel)
|
|
{
|
|
GList *clients = channel->get_clients();
|
|
if (clients == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
return static_cast<SndChannelClient *>(clients->data);
|
|
}
|
|
|
|
static RedsState* snd_channel_get_server(SndChannelClient *client)
|
|
{
|
|
g_return_val_if_fail(client != nullptr, NULL);
|
|
return client->get_channel()->get_server();
|
|
}
|
|
|
|
static void snd_playback_free_frame(PlaybackChannelClient *playback_client, AudioFrame *frame)
|
|
{
|
|
frame->client = playback_client;
|
|
frame->next = playback_client->free_frames;
|
|
playback_client->free_frames = frame;
|
|
}
|
|
|
|
void PlaybackChannelClient::on_message_marshalled(uint8_t *, void *opaque)
|
|
{
|
|
auto client = reinterpret_cast<PlaybackChannelClient*>(opaque);
|
|
|
|
if (client->in_progress) {
|
|
snd_playback_free_frame(client, client->in_progress);
|
|
client->in_progress = nullptr;
|
|
if (client->pending_frame) {
|
|
client->command |= SND_PLAYBACK_PCM_MASK;
|
|
snd_send(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool snd_record_handle_write(RecordChannelClient *record_client, size_t size, void *message)
|
|
{
|
|
SpiceMsgcRecordPacket *packet;
|
|
uint32_t write_pos;
|
|
uint8_t* data;
|
|
uint32_t len;
|
|
uint32_t now;
|
|
|
|
if (!record_client) {
|
|
return false;
|
|
}
|
|
|
|
packet = static_cast<SpiceMsgcRecordPacket *>(message);
|
|
|
|
if (record_client->mode == SPICE_AUDIO_DATA_MODE_RAW) {
|
|
data = packet->data;
|
|
size = packet->data_size >> 2;
|
|
size = MIN(size, RECORD_SAMPLES_SIZE);
|
|
} else {
|
|
int decode_size;
|
|
decode_size = sizeof(record_client->decode_buf);
|
|
if (snd_codec_decode(record_client->codec, packet->data, packet->data_size,
|
|
record_client->decode_buf, &decode_size) != SND_CODEC_OK)
|
|
return false;
|
|
data = record_client->decode_buf;
|
|
size = decode_size >> 2;
|
|
}
|
|
|
|
write_pos = record_client->write_pos % RECORD_SAMPLES_SIZE;
|
|
record_client->write_pos += size;
|
|
len = RECORD_SAMPLES_SIZE - write_pos;
|
|
now = MIN(len, size);
|
|
size -= now;
|
|
memcpy(record_client->samples + write_pos, data, now << 2);
|
|
|
|
if (size) {
|
|
memcpy(record_client->samples, data + (now << 2), size << 2);
|
|
}
|
|
|
|
if (record_client->write_pos - record_client->read_pos > RECORD_SAMPLES_SIZE) {
|
|
record_client->read_pos = record_client->write_pos - RECORD_SAMPLES_SIZE;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static const char*
|
|
spice_audio_data_mode_to_string(SpiceAudioDataMode mode)
|
|
{
|
|
switch (mode) {
|
|
case SPICE_AUDIO_DATA_MODE_INVALID:
|
|
return "invalid";
|
|
case SPICE_AUDIO_DATA_MODE_RAW:
|
|
return "raw";
|
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
|
case SPICE_AUDIO_DATA_MODE_CELT_0_5_1:
|
|
return "celt";
|
|
G_GNUC_END_IGNORE_DEPRECATIONS
|
|
case SPICE_AUDIO_DATA_MODE_OPUS:
|
|
return "opus";
|
|
case SPICE_AUDIO_DATA_MODE_ENUM_END:
|
|
break;
|
|
}
|
|
return "unknown audio codec";
|
|
}
|
|
|
|
XXX_CAST(RedChannelClient, RecordChannelClient, RECORD_CHANNEL_CLIENT)
|
|
|
|
bool RecordChannelClient::handle_message(uint16_t type, uint32_t size, void *message)
|
|
{
|
|
switch (type) {
|
|
case SPICE_MSGC_RECORD_DATA:
|
|
return snd_record_handle_write(this, size, message);
|
|
case SPICE_MSGC_RECORD_MODE: {
|
|
auto msg_mode = static_cast<SpiceMsgcRecordMode *>(message);
|
|
SndChannel *channel = get_channel();
|
|
mode_time = msg_mode->time;
|
|
auto new_mode = static_cast<SpiceAudioDataMode>(msg_mode->mode);
|
|
if (new_mode != SPICE_AUDIO_DATA_MODE_RAW) {
|
|
if (snd_codec_is_capable(new_mode, channel->frequency)) {
|
|
if (snd_codec_create(&codec, new_mode,
|
|
channel->frequency, SND_CODEC_DECODE) == SND_CODEC_OK) {
|
|
mode = new_mode;
|
|
} else {
|
|
red_channel_warning(channel, "create decoder failed");
|
|
return false;
|
|
}
|
|
} else {
|
|
red_channel_warning(channel, "unsupported mode %d", mode);
|
|
return false;
|
|
}
|
|
} else {
|
|
mode = new_mode;
|
|
}
|
|
|
|
spice_debug("record client %p using mode %s", this,
|
|
spice_audio_data_mode_to_string(mode));
|
|
break;
|
|
}
|
|
|
|
case SPICE_MSGC_RECORD_START_MARK: {
|
|
auto mark = static_cast<SpiceMsgcRecordStartMark *>(message);
|
|
start_time = mark->time;
|
|
break;
|
|
}
|
|
default:
|
|
return RedChannelClient::handle_message(type, size, message);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool snd_channel_send_migrate(SndChannelClient *client)
|
|
{
|
|
RedChannelClient *rcc = client;
|
|
SpiceMarshaller *m = rcc->get_marshaller();
|
|
SpiceMsgMigrate migrate;
|
|
|
|
rcc->init_send_data(SPICE_MSG_MIGRATE);
|
|
migrate.flags = 0;
|
|
spice_marshall_msg_migrate(m, &migrate);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static bool snd_playback_send_migrate(PlaybackChannelClient *client)
|
|
{
|
|
return snd_channel_send_migrate(client);
|
|
}
|
|
|
|
static bool snd_send_volume(SndChannelClient *client, uint32_t cap, int msg)
|
|
{
|
|
SpiceMsgAudioVolume *vol;
|
|
uint8_t c;
|
|
RedChannelClient *rcc = client;
|
|
SpiceMarshaller *m = rcc->get_marshaller();
|
|
SndChannel *channel = client->get_channel();
|
|
SpiceVolumeState *st = &channel->volume;
|
|
|
|
if (!rcc->test_remote_cap(cap)) {
|
|
return false;
|
|
}
|
|
|
|
vol = static_cast<SpiceMsgAudioVolume *>(
|
|
alloca(sizeof(SpiceMsgAudioVolume) + st->volume_nchannels * sizeof(uint16_t)));
|
|
rcc->init_send_data(msg);
|
|
vol->nchannels = st->volume_nchannels;
|
|
for (c = 0; c < st->volume_nchannels; ++c) {
|
|
vol->volume[c] = st->volume[c];
|
|
}
|
|
spice_marshall_SpiceMsgAudioVolume(m, vol);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static bool snd_playback_send_volume(PlaybackChannelClient *playback_client)
|
|
{
|
|
return snd_send_volume(playback_client, SPICE_PLAYBACK_CAP_VOLUME,
|
|
SPICE_MSG_PLAYBACK_VOLUME);
|
|
}
|
|
|
|
static bool snd_send_mute(SndChannelClient *client, uint32_t cap, int msg)
|
|
{
|
|
SpiceMsgAudioMute mute;
|
|
RedChannelClient *rcc = client;
|
|
SpiceMarshaller *m = rcc->get_marshaller();
|
|
SndChannel *channel = client->get_channel();
|
|
SpiceVolumeState *st = &channel->volume;
|
|
|
|
if (!rcc->test_remote_cap(cap)) {
|
|
return false;
|
|
}
|
|
|
|
rcc->init_send_data(msg);
|
|
mute.mute = st->mute;
|
|
spice_marshall_SpiceMsgAudioMute(m, &mute);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static bool snd_playback_send_mute(PlaybackChannelClient *playback_client)
|
|
{
|
|
return snd_send_mute(playback_client, SPICE_PLAYBACK_CAP_VOLUME,
|
|
SPICE_MSG_PLAYBACK_MUTE);
|
|
}
|
|
|
|
static bool snd_playback_send_start(PlaybackChannelClient *playback_client)
|
|
{
|
|
SpiceMarshaller *m = playback_client->get_marshaller();
|
|
SpiceMsgPlaybackStart start;
|
|
|
|
playback_client->init_send_data(SPICE_MSG_PLAYBACK_START);
|
|
start.channels = SPICE_INTERFACE_PLAYBACK_CHAN;
|
|
start.frequency = playback_client->get_channel()->frequency;
|
|
spice_assert(SPICE_INTERFACE_PLAYBACK_FMT == SPICE_INTERFACE_AUDIO_FMT_S16);
|
|
start.format = SPICE_AUDIO_FMT_S16;
|
|
start.time = reds_get_mm_time();
|
|
spice_marshall_msg_playback_start(m, &start);
|
|
|
|
playback_client->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static bool snd_playback_send_stop(PlaybackChannelClient *playback_client)
|
|
{
|
|
RedChannelClient *rcc = playback_client;
|
|
|
|
rcc->init_send_data(SPICE_MSG_PLAYBACK_STOP);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static int snd_playback_send_ctl(PlaybackChannelClient *playback_client)
|
|
{
|
|
SndChannelClient *client = playback_client;
|
|
|
|
if ((client->client_active = client->active)) {
|
|
return snd_playback_send_start(playback_client);
|
|
}
|
|
|
|
return snd_playback_send_stop(playback_client);
|
|
}
|
|
|
|
static bool snd_record_send_start(RecordChannelClient *record_client)
|
|
{
|
|
RedChannelClient *rcc = record_client;
|
|
SpiceMarshaller *m = rcc->get_marshaller();
|
|
SpiceMsgRecordStart start;
|
|
|
|
rcc->init_send_data(SPICE_MSG_RECORD_START);
|
|
|
|
start.channels = SPICE_INTERFACE_RECORD_CHAN;
|
|
start.frequency = record_client->get_channel()->frequency;
|
|
spice_assert(SPICE_INTERFACE_RECORD_FMT == SPICE_INTERFACE_AUDIO_FMT_S16);
|
|
start.format = SPICE_AUDIO_FMT_S16;
|
|
spice_marshall_msg_record_start(m, &start);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static bool snd_record_send_stop(RecordChannelClient *record_client)
|
|
{
|
|
RedChannelClient *rcc = record_client;
|
|
|
|
rcc->init_send_data(SPICE_MSG_RECORD_STOP);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static int snd_record_send_ctl(RecordChannelClient *record_client)
|
|
{
|
|
SndChannelClient *client = record_client;
|
|
|
|
if ((client->client_active = client->active)) {
|
|
return snd_record_send_start(record_client);
|
|
}
|
|
|
|
return snd_record_send_stop(record_client);
|
|
}
|
|
|
|
static bool snd_record_send_volume(RecordChannelClient *record_client)
|
|
{
|
|
return snd_send_volume(record_client, SPICE_RECORD_CAP_VOLUME,
|
|
SPICE_MSG_RECORD_VOLUME);
|
|
}
|
|
|
|
static bool snd_record_send_mute(RecordChannelClient *record_client)
|
|
{
|
|
return snd_send_mute(record_client, SPICE_RECORD_CAP_VOLUME,
|
|
SPICE_MSG_RECORD_MUTE);
|
|
}
|
|
|
|
static bool snd_record_send_migrate(RecordChannelClient *record_client)
|
|
{
|
|
/* No need for migration data: if recording has started before migration,
|
|
* the client receives RECORD_STOP from the src before the migration completion
|
|
* notification (when the vm is stopped).
|
|
* Afterwards, when the vm starts on the dest, the client receives RECORD_START. */
|
|
return snd_channel_send_migrate(record_client);
|
|
}
|
|
|
|
static bool snd_playback_send_write(PlaybackChannelClient *playback_client)
|
|
{
|
|
RedChannelClient *rcc = playback_client;
|
|
SpiceMarshaller *m = rcc->get_marshaller();
|
|
AudioFrame *frame;
|
|
SpiceMsgPlaybackPacket msg;
|
|
|
|
rcc->init_send_data(SPICE_MSG_PLAYBACK_DATA);
|
|
|
|
frame = playback_client->in_progress;
|
|
msg.time = frame->time;
|
|
|
|
spice_marshall_msg_playback_data(m, &msg);
|
|
|
|
if (playback_client->mode == SPICE_AUDIO_DATA_MODE_RAW) {
|
|
spice_marshaller_add_by_ref_full(
|
|
m, reinterpret_cast<uint8_t *>(frame->samples),
|
|
snd_codec_frame_size(playback_client->codec) * sizeof(frame->samples[0]),
|
|
PlaybackChannelClient::on_message_marshalled, playback_client);
|
|
}
|
|
else {
|
|
int n = sizeof(playback_client->encode_buf);
|
|
if (snd_codec_encode(playback_client->codec, reinterpret_cast<uint8_t *>(frame->samples),
|
|
snd_codec_frame_size(playback_client->codec) *
|
|
sizeof(frame->samples[0]),
|
|
playback_client->encode_buf, &n) != SND_CODEC_OK) {
|
|
red_channel_warning(rcc->get_channel(), "encode failed");
|
|
rcc->disconnect();
|
|
return false;
|
|
}
|
|
spice_marshaller_add_by_ref_full(m, playback_client->encode_buf, n,
|
|
PlaybackChannelClient::on_message_marshalled,
|
|
playback_client);
|
|
}
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
static bool playback_send_mode(PlaybackChannelClient *playback_client)
|
|
{
|
|
RedChannelClient *rcc = playback_client;
|
|
SpiceMarshaller *m = rcc->get_marshaller();
|
|
SpiceMsgPlaybackMode mode;
|
|
|
|
rcc->init_send_data(SPICE_MSG_PLAYBACK_MODE);
|
|
mode.time = reds_get_mm_time();
|
|
mode.mode = playback_client->mode;
|
|
spice_marshall_msg_playback_mode(m, &mode);
|
|
|
|
rcc->begin_send_message();
|
|
return true;
|
|
}
|
|
|
|
PersistentPipeItem::PersistentPipeItem()
|
|
{
|
|
// force this item to stay alive
|
|
shared_ptr_add_ref(this);
|
|
}
|
|
|
|
static void snd_send(SndChannelClient * client)
|
|
{
|
|
if (!client->pipe_is_empty()|| !client->command) {
|
|
return;
|
|
}
|
|
// just append a dummy item and push!
|
|
RedPipeItemPtr item(&client->persistent_pipe_item);
|
|
client->pipe_add_push(std::move(item));
|
|
}
|
|
|
|
XXX_CAST(RedChannelClient, PlaybackChannelClient, PLAYBACK_CHANNEL_CLIENT)
|
|
|
|
void PlaybackChannelClient::send_item(G_GNUC_UNUSED RedPipeItem *item)
|
|
{
|
|
command &= SND_PLAYBACK_MODE_MASK|SND_PLAYBACK_PCM_MASK|
|
|
SND_CTRL_MASK|SND_VOLUME_MUTE_MASK|
|
|
SND_MIGRATE_MASK;
|
|
while (command) {
|
|
if (command & SND_PLAYBACK_MODE_MASK) {
|
|
command &= ~SND_PLAYBACK_MODE_MASK;
|
|
if (playback_send_mode(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_PLAYBACK_PCM_MASK) {
|
|
spice_assert(!in_progress && pending_frame);
|
|
in_progress = pending_frame;
|
|
pending_frame = nullptr;
|
|
command &= ~SND_PLAYBACK_PCM_MASK;
|
|
if (snd_playback_send_write(this)) {
|
|
break;
|
|
}
|
|
red_channel_warning(get_channel(),
|
|
"snd_send_playback_write failed");
|
|
}
|
|
if (command & SND_CTRL_MASK) {
|
|
command &= ~SND_CTRL_MASK;
|
|
if (snd_playback_send_ctl(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_VOLUME_MASK) {
|
|
command &= ~SND_VOLUME_MASK;
|
|
if (snd_playback_send_volume(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_MUTE_MASK) {
|
|
command &= ~SND_MUTE_MASK;
|
|
if (snd_playback_send_mute(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_MIGRATE_MASK) {
|
|
command &= ~SND_MIGRATE_MASK;
|
|
if (snd_playback_send_migrate(this)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
snd_send(this);
|
|
}
|
|
|
|
void RecordChannelClient::send_item(G_GNUC_UNUSED RedPipeItem *item)
|
|
{
|
|
command &= SND_CTRL_MASK|SND_VOLUME_MUTE_MASK|SND_MIGRATE_MASK;
|
|
while (command) {
|
|
if (command & SND_CTRL_MASK) {
|
|
command &= ~SND_CTRL_MASK;
|
|
if (snd_record_send_ctl(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_VOLUME_MASK) {
|
|
command &= ~SND_VOLUME_MASK;
|
|
if (snd_record_send_volume(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_MUTE_MASK) {
|
|
command &= ~SND_MUTE_MASK;
|
|
if (snd_record_send_mute(this)) {
|
|
break;
|
|
}
|
|
}
|
|
if (command & SND_MIGRATE_MASK) {
|
|
command &= ~SND_MIGRATE_MASK;
|
|
if (snd_record_send_migrate(this)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
snd_send(this);
|
|
}
|
|
|
|
bool SndChannelClient::config_socket()
|
|
{
|
|
RedStream *stream = get_stream();
|
|
RedClient *red_client = get_client();
|
|
MainChannelClient *mcc = red_client->get_main();
|
|
|
|
#ifdef SO_PRIORITY
|
|
int priority = 6;
|
|
if (setsockopt(stream->socket, SOL_SOCKET, SO_PRIORITY, (void*)&priority,
|
|
sizeof(priority)) == -1) {
|
|
if (errno != ENOTSUP) {
|
|
red_channel_warning(get_channel(),
|
|
"setsockopt failed, %s", strerror(errno));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef IPTOS_LOWDELAY
|
|
int tos = IPTOS_LOWDELAY;
|
|
if (setsockopt(stream->socket, IPPROTO_IP, IP_TOS, (void*)&tos, sizeof(tos)) == -1) {
|
|
if (errno != ENOTSUP) {
|
|
red_channel_warning(get_channel(),
|
|
"setsockopt failed, %s",
|
|
strerror(errno));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
red_stream_set_no_delay(stream, !mcc->is_low_bandwidth());
|
|
|
|
return true;
|
|
}
|
|
|
|
uint8_t*
|
|
SndChannelClient::alloc_recv_buf(uint16_t type, uint32_t size)
|
|
{
|
|
// If message is too big allocate one, this should never happen
|
|
if (size > sizeof(receive_buf)) {
|
|
return static_cast<uint8_t *>(g_malloc(size));
|
|
}
|
|
return receive_buf;
|
|
}
|
|
|
|
void
|
|
SndChannelClient::release_recv_buf(uint16_t type, uint32_t size, uint8_t *msg)
|
|
{
|
|
if (msg != receive_buf) {
|
|
g_free(msg);
|
|
}
|
|
}
|
|
|
|
static void snd_set_command(SndChannelClient *client, uint32_t command)
|
|
{
|
|
if (!client) {
|
|
return;
|
|
}
|
|
client->command |= command;
|
|
}
|
|
|
|
static void snd_channel_set_volume(SndChannel *channel,
|
|
uint8_t nchannels, uint16_t *volume)
|
|
{
|
|
SpiceVolumeState *st = &channel->volume;
|
|
SndChannelClient *client = snd_channel_get_client(channel);
|
|
|
|
st->volume_nchannels = nchannels;
|
|
g_free(st->volume);
|
|
st->volume = static_cast<uint16_t *>(g_memdup2(volume, sizeof(uint16_t) * nchannels));
|
|
|
|
if (!client || nchannels == 0)
|
|
return;
|
|
|
|
snd_set_command(client, SND_VOLUME_MASK);
|
|
snd_send(client);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_playback_set_volume(SpicePlaybackInstance *sin,
|
|
uint8_t nchannels,
|
|
uint16_t *volume)
|
|
{
|
|
snd_channel_set_volume(sin->st, nchannels, volume);
|
|
}
|
|
|
|
static void snd_channel_set_mute(SndChannel *channel, uint8_t mute)
|
|
{
|
|
SpiceVolumeState *st = &channel->volume;
|
|
SndChannelClient *client = snd_channel_get_client(channel);
|
|
|
|
st->mute = mute;
|
|
|
|
if (!client)
|
|
return;
|
|
|
|
snd_set_command(client, SND_MUTE_MASK);
|
|
snd_send(client);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_playback_set_mute(SpicePlaybackInstance *sin, uint8_t mute)
|
|
{
|
|
snd_channel_set_mute(sin->st, mute);
|
|
}
|
|
|
|
static void snd_channel_client_start(SndChannelClient *client)
|
|
{
|
|
spice_assert(!client->active);
|
|
client->active = true;
|
|
if (!client->client_active) {
|
|
snd_set_command(client, SND_CTRL_MASK);
|
|
snd_send(client);
|
|
} else {
|
|
client->command &= ~SND_CTRL_MASK;
|
|
}
|
|
}
|
|
|
|
static void playback_channel_client_start(SndChannelClient *client)
|
|
{
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
reds_disable_mm_time(snd_channel_get_server(client));
|
|
snd_channel_client_start(client);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_playback_start(SpicePlaybackInstance *sin)
|
|
{
|
|
SndChannel *channel = sin->st;
|
|
channel->active = true;
|
|
return playback_channel_client_start(snd_channel_get_client(channel));
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_playback_stop(SpicePlaybackInstance *sin)
|
|
{
|
|
SndChannelClient *client = snd_channel_get_client(sin->st);
|
|
|
|
sin->st->active = false;
|
|
if (!client)
|
|
return;
|
|
PlaybackChannelClient *playback_client = PLAYBACK_CHANNEL_CLIENT(client);
|
|
spice_assert(client->active);
|
|
reds_enable_mm_time(snd_channel_get_server(client));
|
|
client->active = false;
|
|
if (client->client_active) {
|
|
snd_set_command(client, SND_CTRL_MASK);
|
|
snd_send(client);
|
|
} else {
|
|
client->command &= ~SND_CTRL_MASK;
|
|
client->command &= ~SND_PLAYBACK_PCM_MASK;
|
|
|
|
if (playback_client->pending_frame) {
|
|
spice_assert(!playback_client->in_progress);
|
|
snd_playback_free_frame(playback_client,
|
|
playback_client->pending_frame);
|
|
playback_client->pending_frame = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_playback_get_buffer(SpicePlaybackInstance *sin,
|
|
uint32_t **samples,
|
|
uint32_t *num_samples)
|
|
{
|
|
SndChannelClient *client = snd_channel_get_client(sin->st);
|
|
|
|
*samples = nullptr;
|
|
*num_samples = 0;
|
|
if (!client) {
|
|
return;
|
|
}
|
|
PlaybackChannelClient *playback_client = PLAYBACK_CHANNEL_CLIENT(client);
|
|
if (!playback_client->free_frames) {
|
|
return;
|
|
}
|
|
spice_assert(client->active);
|
|
if (!playback_client->free_frames->allocated) {
|
|
playback_client->free_frames->allocated = true;
|
|
++playback_client->frames->refs;
|
|
}
|
|
|
|
*samples = playback_client->free_frames->samples;
|
|
playback_client->free_frames = playback_client->free_frames->next;
|
|
*num_samples = snd_codec_frame_size(playback_client->codec);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_playback_put_samples(SpicePlaybackInstance *sin, uint32_t *samples)
|
|
{
|
|
PlaybackChannelClient *playback_client;
|
|
AudioFrame *frame;
|
|
|
|
frame = SPICE_CONTAINEROF(samples, AudioFrame, samples[0]);
|
|
if (frame->allocated) {
|
|
frame->allocated = false;
|
|
if (--frame->container->refs == 0) {
|
|
g_free(frame->container);
|
|
return;
|
|
}
|
|
}
|
|
playback_client = frame->client;
|
|
if (!playback_client || snd_channel_get_client(sin->st) != playback_client) {
|
|
/* lost last reference, client has been destroyed previously */
|
|
spice_debug("audio samples belong to a disconnected client");
|
|
return;
|
|
}
|
|
spice_assert(playback_client->active);
|
|
|
|
if (playback_client->pending_frame) {
|
|
snd_playback_free_frame(playback_client, playback_client->pending_frame);
|
|
}
|
|
frame->time = reds_get_mm_time();
|
|
playback_client->pending_frame = frame;
|
|
snd_set_command(playback_client, SND_PLAYBACK_PCM_MASK);
|
|
snd_send(playback_client);
|
|
}
|
|
|
|
static SpiceAudioDataMode
|
|
snd_desired_audio_mode(bool playback_compression, int frequency, bool client_can_opus)
|
|
{
|
|
if (!playback_compression)
|
|
return SPICE_AUDIO_DATA_MODE_RAW;
|
|
|
|
if (client_can_opus && snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_OPUS, frequency))
|
|
return SPICE_AUDIO_DATA_MODE_OPUS;
|
|
|
|
return SPICE_AUDIO_DATA_MODE_RAW;
|
|
}
|
|
|
|
PlaybackChannelClient::~PlaybackChannelClient()
|
|
{
|
|
// free frames, unref them
|
|
for (auto& item : frames->items) {
|
|
item.client = nullptr;
|
|
}
|
|
if (--frames->refs == 0) {
|
|
g_free(frames);
|
|
}
|
|
|
|
if (active) {
|
|
reds_enable_mm_time(snd_channel_get_server(this));
|
|
}
|
|
|
|
snd_codec_destroy(&codec);
|
|
}
|
|
|
|
|
|
PlaybackChannelClient::PlaybackChannelClient(PlaybackChannel *channel,
|
|
RedClient *client,
|
|
RedStream *stream,
|
|
RedChannelCapabilities *caps):
|
|
SndChannelClient(channel, client, stream, caps)
|
|
{
|
|
snd_playback_alloc_frames(this);
|
|
|
|
bool client_can_opus = test_remote_cap(SPICE_PLAYBACK_CAP_OPUS);
|
|
bool playback_compression =
|
|
reds_config_get_playback_compression(channel->get_server());
|
|
auto desired_mode =
|
|
snd_desired_audio_mode(playback_compression, channel->frequency, client_can_opus);
|
|
if (desired_mode != SPICE_AUDIO_DATA_MODE_RAW) {
|
|
if (snd_codec_create(&codec, static_cast<SpiceAudioDataMode>(desired_mode),
|
|
channel->frequency, SND_CODEC_ENCODE) == SND_CODEC_OK) {
|
|
mode = desired_mode;
|
|
} else {
|
|
red_channel_warning(channel, "create encoder failed");
|
|
}
|
|
}
|
|
|
|
spice_debug("playback client %p using mode %s", this,
|
|
spice_audio_data_mode_to_string(mode));
|
|
}
|
|
|
|
bool PlaybackChannelClient::init()
|
|
{
|
|
RedClient *red_client = get_client();
|
|
SndChannelClient *scc = this;
|
|
SndChannel *channel = get_channel();
|
|
|
|
if (!SndChannelClient::init()) {
|
|
return false;
|
|
}
|
|
|
|
if (!red_client->during_migrate_at_target()) {
|
|
snd_set_command(scc, SND_PLAYBACK_MODE_MASK);
|
|
if (channel->volume.volume_nchannels) {
|
|
snd_set_command(scc, SND_VOLUME_MUTE_MASK);
|
|
}
|
|
}
|
|
|
|
if (channel->active) {
|
|
playback_channel_client_start(scc);
|
|
}
|
|
snd_send(scc);
|
|
|
|
return true;
|
|
}
|
|
|
|
void SndChannel::set_peer_common()
|
|
{
|
|
SndChannelClient *snd_client = snd_channel_get_client(this);
|
|
|
|
/* sound channels currently only support a single client */
|
|
if (snd_client) {
|
|
snd_client->disconnect();
|
|
}
|
|
}
|
|
|
|
void PlaybackChannel::on_connect(RedClient *client, RedStream *stream,
|
|
int migration, RedChannelCapabilities *caps)
|
|
{
|
|
set_peer_common();
|
|
|
|
auto peer =
|
|
red::make_shared<PlaybackChannelClient>(this, client, stream, caps);
|
|
peer->init();
|
|
}
|
|
|
|
void SndChannelClient::migrate()
|
|
{
|
|
snd_set_command(this, SND_MIGRATE_MASK);
|
|
snd_send(this);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_record_set_volume(SpiceRecordInstance *sin,
|
|
uint8_t nchannels,
|
|
uint16_t *volume)
|
|
{
|
|
snd_channel_set_volume(sin->st, nchannels, volume);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_record_set_mute(SpiceRecordInstance *sin, uint8_t mute)
|
|
{
|
|
snd_channel_set_mute(sin->st, mute);
|
|
}
|
|
|
|
static void record_channel_client_start(SndChannelClient *client)
|
|
{
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
RecordChannelClient *record_client = RECORD_CHANNEL_CLIENT(client);
|
|
record_client->read_pos = record_client->write_pos = 0; //todo: improve by
|
|
//stream generation
|
|
snd_channel_client_start(client);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_record_start(SpiceRecordInstance *sin)
|
|
{
|
|
SndChannel *channel = sin->st;
|
|
channel->active = true;
|
|
record_channel_client_start(snd_channel_get_client(channel));
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_record_stop(SpiceRecordInstance *sin)
|
|
{
|
|
SndChannelClient *client = snd_channel_get_client(sin->st);
|
|
|
|
sin->st->active = false;
|
|
if (!client)
|
|
return;
|
|
spice_assert(client->active);
|
|
client->active = false;
|
|
if (client->client_active) {
|
|
snd_set_command(client, SND_CTRL_MASK);
|
|
snd_send(client);
|
|
} else {
|
|
client->command &= ~SND_CTRL_MASK;
|
|
}
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE uint32_t spice_server_record_get_samples(SpiceRecordInstance *sin,
|
|
uint32_t *samples, uint32_t bufsize)
|
|
{
|
|
SndChannelClient *client = snd_channel_get_client(sin->st);
|
|
uint32_t read_pos;
|
|
uint32_t now;
|
|
uint32_t len;
|
|
|
|
if (!client)
|
|
return 0;
|
|
RecordChannelClient *record_client = RECORD_CHANNEL_CLIENT(client);
|
|
spice_assert(client->active);
|
|
|
|
if (record_client->write_pos < RECORD_SAMPLES_SIZE / 2) {
|
|
return 0;
|
|
}
|
|
|
|
len = MIN(record_client->write_pos - record_client->read_pos, bufsize);
|
|
|
|
read_pos = record_client->read_pos % RECORD_SAMPLES_SIZE;
|
|
record_client->read_pos += len;
|
|
now = MIN(len, RECORD_SAMPLES_SIZE - read_pos);
|
|
memcpy(samples, &record_client->samples[read_pos], now * 4);
|
|
if (now < len) {
|
|
memcpy(samples + now, record_client->samples, (len - now) * 4);
|
|
}
|
|
return len;
|
|
}
|
|
|
|
static void snd_set_rate(SndChannel *channel, uint32_t frequency, uint32_t cap_opus)
|
|
{
|
|
channel->frequency = frequency;
|
|
if (channel && snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_OPUS, frequency)) {
|
|
channel->set_cap(cap_opus);
|
|
}
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE uint32_t spice_server_get_best_playback_rate(SpicePlaybackInstance *sin)
|
|
{
|
|
return SND_CODEC_OPUS_PLAYBACK_FREQ;
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_set_playback_rate(SpicePlaybackInstance *sin, uint32_t frequency)
|
|
{
|
|
snd_set_rate(sin->st, frequency, SPICE_PLAYBACK_CAP_OPUS);
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE uint32_t spice_server_get_best_record_rate(SpiceRecordInstance *sin)
|
|
{
|
|
return SND_CODEC_OPUS_PLAYBACK_FREQ;
|
|
}
|
|
|
|
SPICE_GNUC_VISIBLE void spice_server_set_record_rate(SpiceRecordInstance *sin, uint32_t frequency)
|
|
{
|
|
snd_set_rate(sin->st, frequency, SPICE_RECORD_CAP_OPUS);
|
|
}
|
|
|
|
RecordChannelClient::~RecordChannelClient()
|
|
{
|
|
snd_codec_destroy(&codec);
|
|
}
|
|
|
|
bool RecordChannelClient::init()
|
|
{
|
|
SndChannel *channel = get_channel();
|
|
|
|
if (!SndChannelClient::init()) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (channel->volume.volume_nchannels) {
|
|
snd_set_command(this, SND_VOLUME_MUTE_MASK);
|
|
}
|
|
|
|
if (channel->active) {
|
|
record_channel_client_start(this);
|
|
}
|
|
snd_send(this);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void RecordChannel::on_connect(RedClient *client, RedStream *stream,
|
|
int migration, RedChannelCapabilities *caps)
|
|
{
|
|
set_peer_common();
|
|
|
|
auto peer =
|
|
red::make_shared<RecordChannelClient>(this, client, stream, caps);
|
|
peer->init();
|
|
}
|
|
|
|
static void add_channel(SndChannel *channel)
|
|
{
|
|
snd_channels = g_list_prepend(snd_channels, channel);
|
|
}
|
|
|
|
static void remove_channel(SndChannel *channel)
|
|
{
|
|
snd_channels = g_list_remove(snd_channels, channel);
|
|
}
|
|
|
|
SndChannel::~SndChannel()
|
|
{
|
|
remove_channel(this);
|
|
|
|
g_free(volume.volume);
|
|
volume.volume = nullptr;
|
|
}
|
|
|
|
PlaybackChannel::PlaybackChannel(RedsState *reds):
|
|
SndChannel(reds, SPICE_CHANNEL_PLAYBACK, 0)
|
|
{
|
|
set_cap(SPICE_PLAYBACK_CAP_VOLUME);
|
|
|
|
add_channel(this);
|
|
reds_register_channel(reds, this);
|
|
}
|
|
|
|
void snd_attach_playback(RedsState *reds, SpicePlaybackInstance *sin)
|
|
{
|
|
sin->st = new PlaybackChannel(reds); // XXX make_shared
|
|
}
|
|
|
|
RecordChannel::RecordChannel(RedsState *reds):
|
|
SndChannel(reds, SPICE_CHANNEL_RECORD, 0)
|
|
{
|
|
set_cap(SPICE_RECORD_CAP_VOLUME);
|
|
|
|
add_channel(this);
|
|
reds_register_channel(reds, this);
|
|
}
|
|
|
|
void snd_attach_record(RedsState *reds, SpiceRecordInstance *sin)
|
|
{
|
|
sin->st = new RecordChannel(reds); // XXX make_shared
|
|
}
|
|
|
|
static void snd_detach_common(SndChannel *channel)
|
|
{
|
|
if (!channel) {
|
|
return;
|
|
}
|
|
|
|
channel->destroy();
|
|
}
|
|
|
|
void snd_detach_playback(SpicePlaybackInstance *sin)
|
|
{
|
|
snd_detach_common(sin->st);
|
|
}
|
|
|
|
void snd_detach_record(SpiceRecordInstance *sin)
|
|
{
|
|
snd_detach_common(sin->st);
|
|
}
|
|
|
|
void snd_set_playback_compression(bool on)
|
|
{
|
|
GList *l;
|
|
|
|
for (l = snd_channels; l != nullptr; l = l->next) {
|
|
auto now = static_cast<SndChannel *>(l->data);
|
|
SndChannelClient *client = snd_channel_get_client(now);
|
|
if (now->type() == SPICE_CHANNEL_PLAYBACK && client) {
|
|
PlaybackChannelClient* playback = PLAYBACK_CHANNEL_CLIENT(client);
|
|
RedChannelClient *rcc = playback;
|
|
bool client_can_opus = rcc->test_remote_cap(SPICE_PLAYBACK_CAP_OPUS);
|
|
auto desired_mode = snd_desired_audio_mode(on, now->frequency, client_can_opus);
|
|
if (playback->mode != desired_mode) {
|
|
playback->mode = desired_mode;
|
|
snd_set_command(client, SND_PLAYBACK_MODE_MASK);
|
|
spice_debug("playback client %p using mode %s", playback,
|
|
spice_audio_data_mode_to_string(playback->mode));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void snd_playback_alloc_frames(PlaybackChannelClient *playback)
|
|
{
|
|
playback->frames = g_new0(AudioFrameContainer, 1);
|
|
playback->frames->refs = 1;
|
|
for (auto& item : playback->frames->items) {
|
|
item.container = playback->frames;
|
|
snd_playback_free_frame(playback, &item);
|
|
}
|
|
}
|