pipewire/spa/plugins/alsa/alsa-compress-offload-sink.c

2028 lines
59 KiB
C

/* Spa ALSA Compress-Offload sink */
/* SPDX-FileCopyrightText: Copyright © 2022 Wim Taymans */
/* SPDX-FileCopyrightText: Copyright © 2022 Asymptotic Inc. */
/* SPDX-FileCopyrightText: Copyright @ 2023 Carlos Rafael Giani */
/* SPDX-License-Identifier: MIT */
#include <assert.h>
#include <stddef.h>
#include <limits.h>
#include <linux/version.h>
#include <spa/monitor/device.h>
#include <spa/debug/types.h>
#include <spa/support/loop.h>
#include <spa/support/plugin.h>
#include <spa/support/system.h>
#include <spa/node/keys.h>
#include <spa/node/node.h>
#include <spa/node/utils.h>
#include <spa/param/audio/format.h>
#include <spa/param/audio/format-utils.h>
#include <spa/pod/filter.h>
#include <spa/pod/parser.h>
#include <spa/utils/defs.h>
#include <spa/utils/keys.h>
#include <spa/utils/names.h>
#include <spa/utils/result.h>
#include <spa/utils/string.h>
#include "alsa.h"
#include "compress-offload-api.h"
/*
* This creates a PipeWire sink node which uses the ALSA Compress-Offload API
* for writing compressed data ike MP3, FLAC etc. to an DSP that can handle
* such data directly.
*
* These show up under /dev/snd like "comprCxDx", as opposed to regular
* ALSA PCM devices. This sink node still refers to those devices in
* regular ALSA fashion as "hw:x,y" devices, where x = card number and
* y = device number. For example, "hw:4,7" maps to /dev/snd/comprC4D7.
*
* ## Example configuration
*\code{.unparsed}
* context.objects = [
* { factory = adapter
* args = {
* factory.name = "api.alsa.compress.offload.sink"
* node.name = "Compress-Offload-Sink"
* node.description = "Audio sink for compressed audio"
* media.class = "Audio/Sink"
* api.alsa.path = "hw:0,3"
* node.param.PortConfig = {
* direction = Input
* mode = passthrough
* }
* }
* }
*]
*\endcode
*
* TODO:
* - DLL for adjusting driver timer intervals to match the device timestamps in on_driver_timeout()
* - Automatic loading using alsa-udev
*/
/* FLAC support has been present in kernel headers older than 5.5.
* However, those older versions don't support FLAC decoding params. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 5, 0)
#define COMPRESS_OFFLOAD_HAS_FLAC_DEC_PARAMS
#endif
/* Prior to kernel 5.7, WMA9 Pro/Lossless and WMA10 Lossless
* codec profiles were missing.
* As for ALAC and Monkey's Audio (APE), those are new in 5.7. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0)
#define COMPRESS_OFFLOAD_SUPPORTS_WMA9_PRO
#define COMPRESS_OFFLOAD_SUPPORTS_WMA9_LOSSLESS
#define COMPRESS_OFFLOAD_SUPPORTS_WMA10_LOSSLESS
#define COMPRESS_OFFLOAD_SUPPORTS_ALAC
#define COMPRESS_OFFLOAD_SUPPORTS_APE
#endif
#define CHECK_PORT(this, d, p) (((d) == SPA_DIRECTION_INPUT) && ((p) == 0))
#define MAX_BUFFERS (32)
#define BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA (1 << 0)
/* Information about a buffer that got allocated by the PW graph. */
struct buffer {
uint32_t id;
uint32_t flags;
struct spa_buffer *buf;
struct spa_list link;
};
/* Node properties. These are accessible through SPA_PARAM_Props. */
struct props {
/* The'"hw:<cardnr>:<devicenr>" device. */
char device[128];
/* These are the card and device numbers from the
* from the "hw:<cardnr>:<devicenr>" device.*/
int card_nr;
int device_nr;
bool device_name_set;
};
/* Main sink node structure. */
struct impl {
/* Basic states */
struct spa_handle handle;
struct spa_node node;
struct spa_log *log;
struct spa_loop *data_loop;
struct spa_system *data_system;
struct spa_hook_list hooks;
struct spa_callbacks callbacks;
struct props props;
bool have_format;
struct spa_audio_info current_audio_info;
/* This is set to true when the SPA_NODE_COMMAND_Start is
* received, and set back to false when SPA_NODE_COMMAND_Pause
* or SPA_NODE_COMMAND_Suspend is received. */
bool started;
bool freewheel;
/* SPA buffer states */
struct buffer buffers[MAX_BUFFERS];
unsigned int n_buffers;
struct spa_list queued_output_buffers;
size_t offset_within_oldest_output_buffer;
/* Driver and cycle specific states */
int driver_timerfd;
struct spa_source driver_timer_source;
uint64_t next_driver_time;
bool following;
/* Duration and rate of one graph cycle.
* The duration equals the quantum size. */
uint32_t cycle_duration;
int cycle_rate;
/* Node specific states */
uint64_t node_info_all;
struct spa_node_info node_info;
#define NODE_PropInfo 0
#define NODE_Props 1
#define NODE_IO 2
#define NODE_EnumPortConfig 3
#define N_NODE_PARAMS 4
struct spa_param_info node_params[N_NODE_PARAMS];
struct spa_io_clock *node_clock_io;
struct spa_io_position *node_position_io;
/* Port specific states */
uint64_t port_info_all;
struct spa_port_info port_info;
#define PORT_EnumFormat 0
#define PORT_Format 1
#define PORT_IO 2
#define PORT_Buffers 3
#define N_PORT_PARAMS 4
struct spa_param_info port_params[N_PORT_PARAMS];
struct spa_io_buffers *port_buffers_io;
/* Compress-Offload specific states */
struct compress_offload_api_context *device_context;
struct snd_codec audio_codec_info;
bool device_started;
uint32_t min_fragment_size;
uint32_t max_fragment_size;
uint32_t min_num_fragments;
uint32_t max_num_fragments;
uint32_t configured_fragment_size;
uint32_t configured_num_fragments;
bool device_is_paused;
};
/* Compress-Offload device and audio codec functions */
static int init_audio_codec_info(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate);
static int device_open(struct impl *this);
static void device_close(struct impl *this);
static int device_start(struct impl *this);
static int device_pause(struct impl *this);
static int device_resume(struct impl *this);
static int device_write(struct impl *this, const void *data, uint32_t size);
/* Driver timer functions */
static int set_driver_timeout(struct impl *this, uint64_t time);
static int configure_driver_timer(struct impl *this);
static int start_driver_timer(struct impl *this);
static void stop_driver_timer(struct impl *this);
static void on_driver_timeout(struct spa_source *source);
static inline void check_position_and_clock_config(struct impl *this);
static void reevaluate_following_state(struct impl *this);
static void reevaluate_freewheel_state(struct impl *this);
/* Miscellaneous functions */
static int parse_device(struct impl *this);
static void reset_props(struct props *props);
static void clear_buffers(struct impl *this);
static inline bool is_following(struct impl *this);
static int do_start(struct impl *this);
static void do_stop(struct impl *this);
static int write_queued_output_buffers(struct impl *this);
/* Node and port functions */
static void emit_node_info(struct impl *this, bool full);
static void emit_port_info(struct impl *this, bool full);
static int impl_node_add_listener(void *object,
struct spa_hook *listener,
const struct spa_node_events *events,
void *data);
static int impl_node_set_callbacks(void *object,
const struct spa_node_callbacks *callbacks,
void *data);
static int impl_node_sync(void *object, int seq);
static int impl_node_enum_params(void *object, int seq,
uint32_t id, uint32_t start, uint32_t num,
const struct spa_pod *filter);
static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
const struct spa_pod *param);
static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size);
static int impl_node_send_command(void *object, const struct spa_command *command);
static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
const struct spa_dict *props);
static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id);
static int port_enum_formats(struct impl *this, int seq, uint32_t start, uint32_t num,
const struct spa_pod *filter, struct spa_pod_builder *b);
static int impl_port_enum_params(void *object, int seq,
enum spa_direction direction, uint32_t port_id,
uint32_t id, uint32_t start, uint32_t num,
const struct spa_pod *filter);
static int port_set_format(void *object,
enum spa_direction direction, uint32_t port_id,
uint32_t flags,
const struct spa_pod *format);
static int impl_port_set_param(void *object,
enum spa_direction direction, uint32_t port_id,
uint32_t id, uint32_t flags,
const struct spa_pod *param);
static int impl_port_use_buffers(void *object,
enum spa_direction direction, uint32_t port_id,
uint32_t flags,
struct spa_buffer **buffers, uint32_t n_buffers);
static int impl_port_set_io(void *object,
enum spa_direction direction,
uint32_t port_id,
uint32_t id,
void *data, size_t size);
static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id);
static int impl_node_process(void *object);
/* Compress-Offload device and audio codec functions */
struct known_codec_info {
uint32_t codec_id;
uint32_t media_subtype;
const char *name;
};
static struct known_codec_info known_codecs[] = {
{ SND_AUDIOCODEC_VORBIS, SPA_MEDIA_SUBTYPE_vorbis, "Ogg Vorbis" },
{ SND_AUDIOCODEC_MP3, SPA_MEDIA_SUBTYPE_mp3, "MP3" },
{ SND_AUDIOCODEC_AAC, SPA_MEDIA_SUBTYPE_aac, "AAC" },
{ SND_AUDIOCODEC_FLAC, SPA_MEDIA_SUBTYPE_flac, "FLAC" },
{ SND_AUDIOCODEC_WMA, SPA_MEDIA_SUBTYPE_wma, "WMA" },
#ifdef COMPRESS_OFFLOAD_SUPPORTS_ALAC
{ SND_AUDIOCODEC_ALAC, SPA_MEDIA_SUBTYPE_alac, "ALAC" },
#endif
#ifdef COMPRESS_OFFLOAD_SUPPORTS_APE
{ SND_AUDIOCODEC_APE, SPA_MEDIA_SUBTYPE_ape, "Monkey's Audio (APE)" },
#endif
{ SND_AUDIOCODEC_REAL, SPA_MEDIA_SUBTYPE_ra, "Real Audio" },
{ SND_AUDIOCODEC_AMRWB, SPA_MEDIA_SUBTYPE_amr, "AMR wideband" },
{ SND_AUDIOCODEC_AMR, SPA_MEDIA_SUBTYPE_amr, "AMR" },
};
static int init_audio_codec_info(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate)
{
struct snd_codec *codec;
uint32_t channels, rate;
const struct spa_type_info *media_subtype_info;
media_subtype_info = spa_debug_type_find(spa_type_media_subtype, info->media_subtype);
if (media_subtype_info == NULL) {
spa_log_error(this->log, "%p: media subtype %d is unknown", this, info->media_subtype);
return -ENOTSUP;
}
memset(&this->audio_codec_info, 0, sizeof(this->audio_codec_info));
codec = &this->audio_codec_info;
switch (info->media_subtype) {
case SPA_MEDIA_SUBTYPE_vorbis:
codec->id = SND_AUDIOCODEC_VORBIS;
rate = info->info.vorbis.rate;
channels = info->info.vorbis.channels;
spa_log_info(this->log, "%p: initialized codec info to Vorbis; rate: %"
PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
case SPA_MEDIA_SUBTYPE_mp3:
codec->id = SND_AUDIOCODEC_MP3;
rate = info->info.mp3.rate;
channels = info->info.mp3.channels;
spa_log_info(this->log, "%p: initialized codec info to MP3; rate: %"
PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
case SPA_MEDIA_SUBTYPE_aac:
codec->id = SND_AUDIOCODEC_AAC;
rate = info->info.aac.rate;
channels = info->info.aac.channels;
spa_log_info(this->log, "%p: initialized codec info to AAC; rate: %"
PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
case SPA_MEDIA_SUBTYPE_flac:
codec->id = SND_AUDIOCODEC_FLAC;
#ifdef COMPRESS_OFFLOAD_HAS_FLAC_DEC_PARAMS
/* The min/max block sizes are from the FLAC specification:
* https://xiph.org/flac/format.html#blocking
*
* The smallest valid frame possible is 11, which
* is why min_frame_size is set to this quantity.
*
* FFmpeg's flac.h specifies 8192 as the average frame size.
* tinycompress' fcplay uses 4x that amount as the max frame
* size to have enough headroom to be safe.
* We do the same here.
*
* sample_size is set to 0. According to the FLAC spec, this
* is OK to do if a STREAMINFO block was sent into the device
* (see: https://xiph.org/flac/format.html#frame_header), and
* we deal with full FLAC streams here, not just single frames. */
codec->options.flac_d.min_blk_size = 16;
codec->options.flac_d.max_blk_size = 65535;
codec->options.flac_d.min_frame_size = 11;
codec->options.flac_d.max_frame_size = 8192 * 4;
codec->options.flac_d.sample_size = 0;
#endif
rate = info->info.flac.rate;
channels = info->info.flac.channels;
spa_log_info(this->log, "%p: initialized codec info to FLAC; rate: %"
PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
case SPA_MEDIA_SUBTYPE_wma: {
const char *profile_name;
codec->id = SND_AUDIOCODEC_WMA;
/* WMA does not work with Compress-Offload
* if codec profile is not set. */
switch (info->info.wma.profile) {
case SPA_AUDIO_WMA_PROFILE_WMA9:
codec->profile = SND_AUDIOPROFILE_WMA9;
profile_name = "WMA9";
break;
case SPA_AUDIO_WMA_PROFILE_WMA10:
codec->profile = SND_AUDIOPROFILE_WMA10;
profile_name = "WMA10";
break;
#ifdef COMPRESS_OFFLOAD_SUPPORTS_WMA9_PRO
case SPA_AUDIO_WMA_PROFILE_WMA9_PRO:
codec->profile = SND_AUDIOPROFILE_WMA9_PRO;
profile_name = "WMA9 Pro";
break;
#endif
#ifdef COMPRESS_OFFLOAD_SUPPORTS_WMA9_LOSSLESS
case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS:
codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS;
profile_name = "WMA9 Lossless";
break;
#endif
#ifdef COMPRESS_OFFLOAD_SUPPORTS_WMA10_LOSSLESS
case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS:
codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS;
profile_name = "WMA10 Lossless";
break;
#endif
default:
spa_log_error(this->log, "%p: Invalid WMA profile", this);
return -EINVAL;
}
codec->bit_rate = info->info.wma.bitrate;
codec->align = info->info.wma.block_align;
rate = info->info.wma.rate;
channels = info->info.wma.channels;
spa_log_info(this->log, "%p: initialized codec info to WMA; rate: %"
PRIu32 "; channels: %" PRIu32 "; profile %s", this,
rate, channels, profile_name);
break;
}
#ifdef COMPRESS_OFFLOAD_SUPPORTS_ALAC
case SPA_MEDIA_SUBTYPE_alac:
codec->id = SND_AUDIOCODEC_ALAC;
rate = info->info.alac.rate;
channels = info->info.alac.channels;
spa_log_info(this->log, "%p: initialized codec info to ALAC; rate: %"
PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
#endif
#ifdef COMPRESS_OFFLOAD_SUPPORTS_APE
case SPA_MEDIA_SUBTYPE_ape:
codec->id = SND_AUDIOCODEC_APE;
rate = info->info.ape.rate;
channels = info->info.ape.channels;
spa_log_info(this->log, "%p: initialized codec info to APE (Monkey's Audio);"
" rate: %" PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
#endif
case SPA_MEDIA_SUBTYPE_ra:
codec->id = SND_AUDIOCODEC_REAL;
rate = info->info.ra.rate;
channels = info->info.ra.channels;
spa_log_info(this->log, "%p: initialized codec info to Real Audio; rate: %"
PRIu32 "; channels: %" PRIu32, this, rate, channels);
break;
case SPA_MEDIA_SUBTYPE_amr:
if (info->info.amr.band_mode == SPA_AUDIO_AMR_BAND_MODE_WB)
codec->id = SND_AUDIOCODEC_AMRWB;
else
codec->id = SND_AUDIOCODEC_AMR;
rate = info->info.amr.rate;
channels = info->info.amr.channels;
spa_log_info(this->log, "%p: initialized codec info to %s; rate: %"
PRIu32 "; channels: %" PRIu32, this,
(codec->id == SND_AUDIOCODEC_AMRWB) ? "AMR wideband" : "AMR",
rate, channels);
break;
default:
spa_log_error(this->log, "%p: media subtype %s is not supported", this, media_subtype_info->name);
return -ENOTSUP;
}
codec->ch_in = channels;
codec->ch_out = channels;
codec->sample_rate = rate;
codec->rate_control = 0;
codec->level = 0;
codec->ch_mode = 0;
codec->format = 0;
*out_rate = rate;
return 0;
}
static int device_open(struct impl *this)
{
assert(this->device_context == NULL);
spa_log_info(this->log, "%p: opening Compress-Offload device, card #%d device #%d",
this, this->props.card_nr, this->props.device_nr);
this->device_context = compress_offload_api_open(this->props.card_nr, this->props.device_nr, this->log);
if (this->device_context == NULL)
return -errno;
return 0;
}
static void device_close(struct impl *this)
{
if (this->device_context == NULL)
return;
spa_log_info(this->log, "%p: closing Compress-Offload device, card #%d device #%d",
this, this->props.card_nr, this->props.device_nr);
if (this->device_started)
compress_offload_api_stop(this->device_context);
compress_offload_api_close(this->device_context);
this->device_context = NULL;
this->device_started = false;
this->device_is_paused = false;
this->have_format = false;
}
static int device_start(struct impl *this)
{
assert(this->device_context != NULL);
if (compress_offload_api_start(this->device_context) < 0)
return -errno;
this->device_started = true;
return 0;
}
static int device_pause(struct impl *this)
{
/* device_pause() can sometimes be called when the device context is already
* gone. In particular, this can happen when the suspend command is received
* after the pause command. */
if (this->device_context == NULL)
return 0;
if (this->device_is_paused)
return 0;
if (compress_offload_api_pause(this->device_context) < 0)
return -errno;
this->device_is_paused = true;
return 0;
}
static int device_resume(struct impl *this)
{
assert(this->device_context != NULL);
if (!this->device_is_paused)
return 0;
if (compress_offload_api_resume(this->device_context) < 0)
return -errno;
this->device_is_paused = false;
return 0;
}
static int device_write(struct impl *this, const void *data, uint32_t size)
{
int res;
uint32_t num_bytes_to_write;
struct snd_compr_avail available_space;
/* In here, try to write out as much data as possible,
* in a non-blocking manner, retaining the unwritten
* data for the next write call. */
if (SPA_UNLIKELY((res = compress_offload_api_get_available_space(
this->device_context, &available_space)) < 0))
return res;
/* We can only write data if there is at least enough space for one
* fragment's worth of data, or if the data we want to write is
* small (smaller than a fragment). The latter can happen when we
* are writing the last few bits of the compressed audio medium.
* When the former happens, we try to write as much data as we
* can, limited by the amount of space available in the device. */
if ((available_space.avail < this->min_fragment_size) && (available_space.avail < size))
return 0;
num_bytes_to_write = SPA_MIN(size, available_space.avail);
res = compress_offload_api_write(this->device_context, data, num_bytes_to_write);
if (SPA_UNLIKELY(res < 0)) {
if (res == -EBADFD)
spa_log_debug(this->log, "%p: device is paused", this);
else
spa_log_error(this->log, "%p: write error: %s", this, spa_strerror(res));
return res;
}
spa_log_trace_fp(this->log, "%p: wrote %d bytes; original request: %" PRIu32 "; adjusted "
"for available space in device: %" PRIu32, this, res, size, num_bytes_to_write);
if (SPA_UNLIKELY(((uint32_t)res) > num_bytes_to_write)) {
spa_log_error(this->log, "%p: wrote more bytes than what was requested; "
"requested: %" PRId32 " wrote: %d", this, num_bytes_to_write, res);
return -EIO;
}
return res;
}
/* Driver timer functions */
static int set_driver_timeout(struct impl *this, uint64_t time)
{
struct itimerspec ts;
ts.it_value.tv_sec = time / SPA_NSEC_PER_SEC;
ts.it_value.tv_nsec = time % SPA_NSEC_PER_SEC;
ts.it_interval.tv_sec = 0;
ts.it_interval.tv_nsec = 0;
spa_system_timerfd_settime(this->data_system, this->driver_timerfd,
SPA_FD_TIMER_ABSTIME, &ts, NULL);
return 0;
}
static int configure_driver_timer(struct impl *this)
{
struct timespec now;
int res;
if ((res = spa_system_clock_gettime(this->data_system, CLOCK_MONOTONIC, &now)) < 0) {
spa_log_error(this->log, "%p: could not get time from monotonic sysclock: %s",
this, spa_strerror(res));
return res;
}
this->next_driver_time = SPA_TIMESPEC_TO_NSEC(&now);
if (this->following)
set_driver_timeout(this, 0);
else
set_driver_timeout(this, this->next_driver_time);
return 0;
}
static int start_driver_timer(struct impl *this)
{
int res;
spa_log_debug(this->log, "%p: starting driver timer", this);
this->driver_timer_source.func = on_driver_timeout;
this->driver_timer_source.data = this;
this->driver_timer_source.fd = this->driver_timerfd;
this->driver_timer_source.mask = SPA_IO_IN;
this->driver_timer_source.rmask = 0;
spa_loop_add_source(this->data_loop, &this->driver_timer_source);
if (SPA_UNLIKELY((res = configure_driver_timer(this)) < 0))
return res;
return 0;
}
static int do_remove_driver_timer_source(struct spa_loop *loop,
bool async,
uint32_t seq,
const void *data,
size_t size,
void *user_data)
{
struct impl *this = user_data;
spa_loop_remove_source(this->data_loop, &this->driver_timer_source);
set_driver_timeout(this, 0);
return 0;
}
static void stop_driver_timer(struct impl *this)
{
spa_log_debug(this->log, "%p: stopping driver timer", this);
/* Perform the actual stop within
* the dataloop to avoid data races. */
spa_loop_locked(this->data_loop, do_remove_driver_timer_source, 0, NULL, 0, this);
}
static void on_driver_timeout(struct spa_source *source)
{
struct impl *this = source->data;
uint64_t expire, current_time;
int res;
if (SPA_LIKELY(this->started)) {
if (SPA_UNLIKELY((res = spa_system_timerfd_read(this->data_system,
this->driver_timerfd, &expire)) < 0)) {
if (res != -EAGAIN)
spa_log_warn(this->log, "%p: error reading from timerfd: %s",
this, spa_strerror(res));
return;
}
}
if (SPA_LIKELY(this->node_position_io != NULL)) {
this->cycle_duration = this->node_position_io->clock.target_duration;
this->cycle_rate = this->node_position_io->clock.target_rate.denom;
} else {
/* This can happen at the very beginning if node_position_io
* isn't passed to this node in time. */
this->cycle_duration = 1024;
this->cycle_rate = 48000;
}
current_time = this->next_driver_time;
this->next_driver_time += ((uint64_t)(this->cycle_duration)) * 1000000000ULL / this->cycle_rate;
if (this->node_clock_io != NULL) {
this->node_clock_io->nsec = current_time;
this->node_clock_io->rate = this->node_clock_io->target_rate;
this->node_clock_io->position += this->node_clock_io->duration;
this->node_clock_io->duration = this->cycle_duration;
this->node_clock_io->delay = 0;
this->node_clock_io->rate_diff = 1.0;
this->node_clock_io->next_nsec = this->next_driver_time;
spa_log_trace_fp(this->log, "%p: clock IO updated to: nsec %" PRIu64
" pos %" PRIu64 " dur %" PRIu64 " next-nsec %" PRIu64, this,
this->node_clock_io->nsec, this->node_clock_io->position,
this->node_clock_io->duration, this->node_clock_io->next_nsec);
}
/* Adapt the graph cycle progression to the needs of the sink.
* If the sink still has data to output, don't advance. */
if (spa_list_is_empty(&this->queued_output_buffers)) {
struct spa_io_buffers *io = this->port_buffers_io;
if (SPA_LIKELY(io != NULL)) {
spa_log_trace_fp(this->log, "%p: ran out of buffers to output, "
"need more; IO status: %d", this, io->status);
io->status = SPA_STATUS_NEED_DATA;
spa_node_call_ready(&this->callbacks, SPA_STATUS_NEED_DATA);
} else {
/* This should not happen. If it does, then there may be
* an error in when the timer is stopped. When it happens,
* do not schedule a next timeout. */
spa_log_warn(this->log, "%p: buffers IO was set to NULL before "
"the driver timer was stopped", this);
set_driver_timeout(this, 0);
return;
}
} else {
write_queued_output_buffers(this);
}
// TODO check for impossible timeouts (only relevant when taking device timestamps into account)
set_driver_timeout(this, this->next_driver_time);
}
static inline void check_position_and_clock_config(struct impl *this)
{
if (SPA_LIKELY(this->node_position_io != NULL)) {
this->cycle_duration = this->node_position_io->clock.duration;
this->cycle_rate = this->node_position_io->clock.rate.denom;
} else {
/* This can happen at the very beginning if node_position_io
* isn't passed to this node in time. */
this->cycle_duration = 1024;
this->cycle_rate = 48000;
}
}
static int do_reevaluate_following_state(struct spa_loop *loop,
bool async,
uint32_t seq,
const void *data,
size_t size,
void *user_data)
{
struct impl *this = user_data;
configure_driver_timer(this);
return 0;
}
static void reevaluate_following_state(struct impl *this)
{
bool following;
if (!this->started)
return;
following = is_following(this);
if (following != this->following) {
spa_log_debug(this->log, "%p: following state changed: %d->%d", this, this->following, following);
this->following = following;
spa_loop_locked(this->data_loop, do_reevaluate_following_state, 0, NULL, 0, this);
}
}
static void reevaluate_freewheel_state(struct impl *this)
{
bool freewheel;
if (!this->started)
return;
freewheel = (this->node_position_io != NULL) &&
SPA_FLAG_IS_SET(this->node_position_io->clock.flags, SPA_IO_CLOCK_FLAG_FREEWHEEL);
if (this->freewheel != freewheel) {
spa_log_debug(this->log, "%p: freewheel state changed: %d->%d", this, this->freewheel, freewheel);
this->freewheel = freewheel;
if (freewheel)
device_pause(this);
else
device_resume(this);
}
}
/* Miscellaneous functions */
static int parse_device(struct impl *this)
{
char *device;
char *nextptr;
#define NUM_DEVICE_VALUES (2)
long values[NUM_DEVICE_VALUES];
int value_index;
device = this->props.device;
/* Valid devices always match the "hw:<cardnr>,<devicenr>" pattern. */
if (strncmp(device, "hw:", 3) != 0) {
spa_log_error(this->log, "%p: device \"%s\" does not begin with \"hw:\"", this, device);
return -EINVAL;
}
nextptr = device + 3;
for (value_index = 0; ; ++value_index) {
const char *value_label;
switch (value_index) {
case 0: value_label = "card"; break;
case 1: value_label = "device"; break;
default: spa_assert_not_reached();
}
errno = 0;
values[value_index] = strtol(nextptr, &nextptr, 10);
if (errno != 0) {
spa_log_error(this->log, "%p: device \"%s\" has invalid %s value",
this, device, value_label);
return -EINVAL;
}
if (values[value_index] < 0) {
spa_log_error(this->log, "%p: device \"%s\" has negative %s value",
this, device, value_label);
return -EINVAL;
}
if (values[value_index] > INT_MAX) {
spa_log_error(this->log, "%p: device \"%s\" has %s value larger than %d",
this, device, value_label, INT_MAX);
return -EINVAL;
}
if (value_index >= (NUM_DEVICE_VALUES - 1))
break;
if ((*nextptr) != ',') {
spa_log_error(this->log, "%p: expected ',' separator between numbers in "
"device \"%s\", got '%c'", this, device, *nextptr);
return -EINVAL;
}
/* Skip the comma between the values. */
nextptr++;
}
this->props.card_nr = values[0];
this->props.device_nr = values[1];
return 0;
}
static void reset_props(struct props *props)
{
memset(props->device, 0, sizeof(props->device));
props->card_nr = 0;
props->device_nr = 0;
props->device_name_set = false;
}
static void clear_buffers(struct impl *this)
{
if (this->n_buffers > 0) {
spa_log_debug(this->log, "%p: clearing buffers", this);
spa_list_init(&this->queued_output_buffers);
this->n_buffers = 0;
}
}
static inline bool is_following(struct impl *this)
{
return (this->node_position_io != NULL) &&
(this->node_clock_io != NULL) &&
(this->node_position_io->clock.id != this->node_clock_io->id);
}
static int do_start(struct impl *this)
{
int res;
if (this->started)
return 0;
this->following = is_following(this);
spa_log_debug(this->log, "%p: starting output; starting as follower: %d",
this, this->following);
if (SPA_UNLIKELY((res = start_driver_timer(this)) < 0))
return res;
this->started = true;
/* Not starting the compress-offload device here right away.
* That's because we first need to give it at least one
* fragment's worth of data. Starting the device prior to
* that results in buffer underflows inside the device. */
return 0;
}
static void do_stop(struct impl *this)
{
if (!this->started)
return;
spa_log_debug(this->log, "%p: stopping output", this);
device_pause(this);
this->started = false;
stop_driver_timer(this);
}
static int write_queued_output_buffers(struct impl *this)
{
int res;
uint32_t total_num_written_bytes;
bool wrote_data = false;
check_position_and_clock_config(this);
/* In here, we write as much data as possible. The device may
* initially not have sufficient space, but it is possible
* that due to ongoing data consumption, it can accommodate
* for more data in a next attempt, hence the "again" label.
*
* If during the write attempts, only a portion of a chunk
* is written, we must keep track of the portion that hasn't
* been consumed yet. offset_within_oldest_output_buffer
* exists for this purpose. This can happen when the
* device_write() call below returns 0. The loop is then
* aborted, and the chunk is not fully written.
*
* In this sink node, each SPA buffer has exactly one chunk,
* so when a chunk is fully consumed, the corresponding buffer
* is removed from the queued_output_buffers list, marked as
* available, and returned to the pool through
* spa_node_call_reuse_buffer(). */
again:
total_num_written_bytes = 0;
while (!spa_list_is_empty(&this->queued_output_buffers)) {
struct buffer *b;
struct spa_data *d;
uint32_t chunk_start_offset, chunk_size, pending_data_size;
bool reuse_buffer = false;
b = spa_list_first(&this->queued_output_buffers, struct buffer, link);
d = b->buf->datas;
assert(b->buf->n_datas >= 1);
chunk_start_offset = d[0].chunk->offset + this->offset_within_oldest_output_buffer;
chunk_size = d[0].chunk->size;
/* An empty chunk signals that the source is skipping this cycle. This
* is normal and necessary in cases when the compressed data frames are
* longer than the quantum size. The source then has to keep track of
* the excess lengths, and if these sum up to the length of a quantum,
* it sends a buffer with an empty chunk to compensate. If this is not
* done, there will eventually be an overflow, this sink will miss
* cycles, and audible errors will occur. */
if (chunk_size != 0) {
int num_written_bytes;
pending_data_size = chunk_size - this->offset_within_oldest_output_buffer;
chunk_start_offset = SPA_MIN(chunk_start_offset, d[0].maxsize);
pending_data_size = SPA_MIN(pending_data_size, d[0].maxsize - chunk_start_offset);
num_written_bytes = device_write(this, SPA_PTROFF(d[0].data, chunk_start_offset, void), pending_data_size);
if (SPA_UNLIKELY(num_written_bytes < 0))
return num_written_bytes;
if (num_written_bytes == 0)
break;
this->offset_within_oldest_output_buffer += num_written_bytes;
total_num_written_bytes += num_written_bytes;
wrote_data = wrote_data || (num_written_bytes > 0);
if (this->offset_within_oldest_output_buffer >= chunk_size) {
spa_log_trace_fp(this->log, "%p: buffer with ID %u was fully written; reusing this buffer", this, b->id);
reuse_buffer = true;
this->offset_within_oldest_output_buffer = 0;
}
} else {
spa_log_trace_fp(this->log, "%p: buffer with ID %u has empty chunk; reusing this buffer", this, b->id);
reuse_buffer = true;
}
if (reuse_buffer) {
spa_list_remove(&b->link);
SPA_FLAG_SET(b->flags, BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA);
this->port_buffers_io->buffer_id = b->id;
spa_node_call_reuse_buffer(&this->callbacks, 0, b->id);
}
}
if (!spa_list_is_empty(&this->queued_output_buffers) && (total_num_written_bytes > 0))
goto again;
/* We start the device only after having written data to avoid
* underruns due to an under-populated device ringbuffer. */
if (wrote_data && !this->device_started) {
spa_log_debug(this->log, "%p: starting device", this);
if ((res = device_start(this)) < 0) {
spa_log_error(this->log, "%p: starting device failed: %s", this, spa_strerror(res));
return res;
}
this->device_started = true;
}
return 0;
}
/* Node and port functions */
static const struct spa_dict_item node_info_items[] = {
{ SPA_KEY_DEVICE_API, "alsa" },
{ SPA_KEY_MEDIA_CLASS, "Audio/Sink" },
{ SPA_KEY_NODE_DRIVER, "true" },
{ SPA_KEY_NODE_PAUSE_ON_IDLE, "true" },
};
static void emit_node_info(struct impl *this, bool full)
{
uint64_t old = full ? this->node_info.change_mask : 0;
if (full)
this->node_info.change_mask = this->node_info_all;
if (this->node_info.change_mask) {
this->node_info.props = &SPA_DICT_INIT_ARRAY(node_info_items);
spa_node_emit_info(&this->hooks, &this->node_info);
this->node_info.change_mask = old;
}
}
static void emit_port_info(struct impl *this, bool full)
{
uint64_t old = full ? this->port_info.change_mask : 0;
if (full)
this->port_info.change_mask = this->port_info_all;
if (this->port_info.change_mask) {
spa_node_emit_port_info(&this->hooks,
SPA_DIRECTION_INPUT, 0, &this->port_info);
this->port_info.change_mask = old;
}
}
static int impl_node_add_listener(void *object,
struct spa_hook *listener,
const struct spa_node_events *events,
void *data)
{
struct impl *this = object;
struct spa_hook_list save;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_hook_list_isolate(&this->hooks, &save, listener, events, data);
emit_node_info(this, true);
emit_port_info(this, true);
spa_hook_list_join(&this->hooks, &save);
return 0;
}
static int impl_node_set_callbacks(void *object,
const struct spa_node_callbacks *callbacks,
void *data)
{
struct impl *this = object;
spa_return_val_if_fail(this != NULL, -EINVAL);
this->callbacks = SPA_CALLBACKS_INIT(callbacks, data);
return 0;
}
static int impl_node_sync(void *object, int seq)
{
struct impl *this = object;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_node_emit_result(&this->hooks, seq, 0, 0, NULL);
return 0;
}
static int impl_node_enum_params(void *object, int seq,
uint32_t id, uint32_t start, uint32_t num,
const struct spa_pod *filter)
{
struct impl *this = object;
struct spa_pod *param;
struct spa_pod_builder b = { 0 };
uint8_t buffer[4096];
struct spa_result_node_params result;
uint32_t count = 0;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_return_val_if_fail(num != 0, -EINVAL);
result.id = id;
result.next = start;
next:
result.index = result.next++;
spa_pod_builder_init(&b, buffer, sizeof(buffer));
switch (id) {
case SPA_PARAM_PropInfo:
{
struct props *p = &this->props;
switch (result.index) {
case 0:
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_PropInfo, id,
SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_device),
SPA_PROP_INFO_name, SPA_POD_String(SPA_KEY_API_ALSA_PATH),
SPA_PROP_INFO_description, SPA_POD_String("The ALSA Compress-Offload device"),
SPA_PROP_INFO_type, SPA_POD_Stringn(p->device, sizeof(p->device)));
break;
default:
return 0;
}
break;
}
case SPA_PARAM_Props:
{
struct props *p = &this->props;
switch (result.index) {
case 0:
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_Props, id,
SPA_PROP_device, SPA_POD_Stringn(p->device, sizeof(p->device))
);
break;
default:
return 0;
}
break;
}
case SPA_PARAM_IO:
switch (result.index) {
case 0:
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_ParamIO, id,
SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Clock),
SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_clock)));
break;
case 1:
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_ParamIO, id,
SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Position),
SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_position)));
break;
default:
return 0;
}
break;
case SPA_PARAM_EnumPortConfig:
{
switch (result.index) {
case 0:
/* Force ports to be configured to run in passthrough mode.
* This is essential when dealing with compressed data. */
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_ParamPortConfig, id,
SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(SPA_DIRECTION_INPUT),
SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_passthrough)
);
break;
default:
return 0;
}
break;
}
default:
return -ENOENT;
}
if (spa_pod_filter(&b, &result.param, param, filter) < 0)
goto next;
spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
if (++count != num)
goto next;
return 0;
}
static int impl_node_set_param(void *object, uint32_t id, uint32_t flags,
const struct spa_pod *param)
{
struct impl *this = object;
int res;
spa_return_val_if_fail(this != NULL, -EINVAL);
switch (id) {
case SPA_PARAM_Props:
{
struct props *p = &this->props;
if (param == NULL) {
reset_props(p);
return 0;
}
spa_pod_parse_object(param,
SPA_TYPE_OBJECT_Props, NULL,
SPA_PROP_device, SPA_POD_OPT_Stringn(p->device, sizeof(p->device))
);
spa_log_debug(this->log, "%p: setting device name to \"%s\"", this, p->device);
p->device_name_set = true;
if ((res = parse_device(this)) < 0) {
p->device_name_set = false;
return res;
}
emit_node_info(this, false);
break;
}
default:
res = -ENOENT;
break;
}
return res;
}
static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size)
{
struct impl *this = object;
spa_return_val_if_fail(this != NULL, -EINVAL);
switch (id) {
case SPA_IO_Clock:
spa_log_debug(this->log, "%p: got clock IO", this);
this->node_clock_io = data;
break;
case SPA_IO_Position:
spa_log_debug(this->log, "%p: got position IO", this);
this->node_position_io = data;
break;
default:
return -ENOENT;
}
reevaluate_following_state(this);
reevaluate_freewheel_state(this);
return 0;
}
static int impl_node_send_command(void *object, const struct spa_command *command)
{
struct impl *this = object;
int res;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_return_val_if_fail(command != NULL, -EINVAL);
spa_log_debug(this->log, "%p: got new command: %s", this,
spa_debug_type_find_name(spa_type_node_command_id, SPA_NODE_COMMAND_ID(command)));
switch (SPA_NODE_COMMAND_ID(command)) {
case SPA_NODE_COMMAND_ParamBegin:
if (SPA_UNLIKELY((res = device_open(this)) < 0))
return res;
break;
case SPA_NODE_COMMAND_ParamEnd:
device_close(this);
break;
case SPA_NODE_COMMAND_Start:
if (!this->have_format)
return -EIO;
if (this->n_buffers == 0)
return -EIO;
if (SPA_UNLIKELY((res = do_start(this)) < 0))
return res;
break;
case SPA_NODE_COMMAND_Suspend:
case SPA_NODE_COMMAND_Pause:
do_stop(this);
break;
default:
return -ENOTSUP;
}
return 0;
}
static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id,
const struct spa_dict *props)
{
return -ENOTSUP;
}
static int impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id)
{
return -ENOTSUP;
}
static int port_enum_formats(struct impl *this, int seq, uint32_t start, uint32_t num,
const struct spa_pod *filter, struct spa_pod_builder *b)
{
bool device_started, device_opened;
struct spa_result_node_params result;
struct spa_pod *fmt;
const struct known_codec_info *codec_info;
uint32_t count = 0;
int res;
bool codec_supported;
struct spa_audio_info info;
device_opened = (this->device_context != NULL);
device_started = this->device_started;
spa_log_debug(this->log, "%p: about to enumerate supported codecs: "
"device opened: %d have configured format: %d device started: %d",
this, device_opened, this->have_format, device_started);
if (!this->started && this->have_format) {
spa_log_debug(this->log, "%p: closing device to reset configured format", this);
device_close(this);
device_opened = false;
}
if (!device_opened) {
if ((res = device_open(this)) < 0)
return res;
}
spa_zero(result);
result.id = SPA_PARAM_EnumFormat;
result.next = start;
next:
result.index = result.next++;
if (result.index >= SPA_N_ELEMENTS(known_codecs))
goto enum_end;
codec_info = &(known_codecs[result.index]);
codec_supported = compress_offload_api_supports_codec(this->device_context, codec_info->codec_id);
spa_log_debug(this->log, "%p: codec %s supported: %s", this,
codec_info->name, codec_supported ? "yes" : "no");
if (!codec_supported)
goto next;
spa_zero(info);
info.media_type = SPA_MEDIA_TYPE_audio;
info.media_subtype = codec_info->media_subtype;
if ((fmt = spa_format_audio_build(b, SPA_PARAM_EnumFormat, &info)) == NULL) {
res = -errno;
spa_log_error(this->log, "%p: error while building enumerated audio info: %s",
this, spa_strerror(res));
return res;
}
if (spa_pod_filter(b, &result.param, fmt, filter) < 0)
goto next;
spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
if (++count != num)
goto next;
enum_end:
res = 0;
if (!device_opened)
device_close(this);
spa_log_debug(this->log, "%p: done enumerating supported codecs", this);
return res;
}
static int impl_port_enum_params(void *object, int seq,
enum spa_direction direction, uint32_t port_id,
uint32_t id, uint32_t start, uint32_t num,
const struct spa_pod *filter)
{
struct impl *this = object;
struct spa_pod *param = NULL;
struct spa_pod_builder b = { 0 };
uint8_t buffer[4096];
struct spa_result_node_params result;
uint32_t count = 0;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_return_val_if_fail(num != 0, -EINVAL);
spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
result.id = id;
result.next = start;
next:
result.index = result.next++;
spa_pod_builder_init(&b, buffer, sizeof(buffer));
switch (id) {
case SPA_PARAM_EnumFormat:
return port_enum_formats(this, seq, start, num, filter, &b);
case SPA_PARAM_Format:
if (!this->have_format) {
spa_log_debug(this->log, "%p: attempted to enumerate current "
"format, but no current audio info set", this);
return -EIO;
}
if (result.index > 0)
return 0;
spa_log_debug(this->log, "%p: current audio info is set; "
"enumerating currently set format", this);
param = spa_format_audio_build(&b, id, &this->current_audio_info);
break;
case SPA_PARAM_IO:
switch (result.index) {
case 0:
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_ParamIO, id,
SPA_PARAM_IO_id, SPA_POD_Id(SPA_IO_Buffers),
SPA_PARAM_IO_size, SPA_POD_Int(sizeof(struct spa_io_buffers)));
break;
default:
return 0;
}
break;
case SPA_PARAM_Buffers:
if (!this->have_format)
return -EIO;
if (result.index > 0)
return 0;
param = spa_pod_builder_add_object(&b,
SPA_TYPE_OBJECT_ParamBuffers, id,
SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, MAX_BUFFERS),
/* blocks is set to 1 since we don't have planar data */
SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(
this->configured_fragment_size * this->configured_num_fragments,
this->configured_fragment_size * this->configured_num_fragments,
this->max_fragment_size * this->max_num_fragments),
/* "stride" has no meaning when dealing with compressed data */
SPA_PARAM_BUFFERS_stride, SPA_POD_Int(0));
break;
default:
return -ENOENT;
}
if (spa_pod_filter(&b, &result.param, param, filter) < 0)
goto next;
spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result);
if (++count != num)
goto next;
return 0;
}
static int port_set_format(void *object,
enum spa_direction direction, uint32_t port_id,
uint32_t flags,
const struct spa_pod *format)
{
struct impl *this = object;
int res;
if (format == NULL) {
if (!this->have_format)
return 0;
spa_log_debug(this->log, "%p: clearing format and closing device", this);
device_close(this);
clear_buffers(this);
} else {
struct spa_audio_info info = { 0 };
uint32_t rate;
const struct snd_compr_caps *compress_offload_caps;
spa_log_debug(this->log, "%p: about to set format", this);
if ((res = spa_format_audio_parse(format, &info)) < 0) {
spa_log_error(this->log, "%p: error while parsing audio format: %s",
this, spa_strerror(res));
return res;
}
if (this->device_context != NULL) {
spa_log_debug(this->log, "%p: need to close device to be able to reopen it with new format", this);
device_close(this);
}
if ((res = init_audio_codec_info(this, &info, &rate)) < 0)
return res;
if ((res = device_open(this)) < 0)
return res;
if (!compress_offload_api_supports_codec(this->device_context, this->audio_codec_info.id)) {
spa_log_error(this->log, "%p: codec is not supported by the device", this);
device_close(this);
return -ENOTSUP;
}
if ((res = compress_offload_api_set_params(this->device_context, &(this->audio_codec_info), 0, 0)) < 0)
return res;
compress_offload_caps = compress_offload_api_get_caps(this->device_context);
this->min_fragment_size = compress_offload_caps->min_fragment_size;
this->max_fragment_size = compress_offload_caps->max_fragment_size;
this->min_num_fragments = compress_offload_caps->min_fragments;
this->max_num_fragments = compress_offload_caps->max_fragments;
spa_log_debug(
this->log,
"%p: min/max fragment size: %" PRIu32 "/%" PRIu32 " min/max num fragments: %" PRIu32 "/%" PRIu32,
this,
this->min_fragment_size, this->max_fragment_size,
this->min_num_fragments, this->max_num_fragments
);
compress_offload_api_get_fragment_config(this->device_context,
&(this->configured_fragment_size),
&(this->configured_num_fragments));
spa_log_debug(
this->log, "%p: configured fragment size: %" PRIu32 " configured num fragments: %" PRIu32,
this,
this->configured_fragment_size, this->configured_num_fragments
);
this->current_audio_info = info;
this->have_format = true;
this->port_info.rate = SPA_FRACTION(1, rate);
}
this->node_info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS;
this->node_info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE;
emit_node_info(this, false);
this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE;
this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS;
if (this->have_format) {
this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE);
this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ);
} else {
this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
}
emit_port_info(this, false);
return 0;
}
static int impl_port_set_param(void *object,
enum spa_direction direction, uint32_t port_id,
uint32_t id, uint32_t flags,
const struct spa_pod *param)
{
struct impl *this = object;
int res;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
switch (id) {
case SPA_PARAM_Format:
res = port_set_format(this, direction, port_id, flags, param);
break;
default:
res = -ENOENT;
break;
}
return res;
}
static int impl_port_use_buffers(void *object,
enum spa_direction direction, uint32_t port_id,
uint32_t flags,
struct spa_buffer **buffers, uint32_t n_buffers)
{
struct impl *this = object;
uint32_t i;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
if (this->n_buffers > 0) {
spa_log_debug(this->log, "%p: %u buffers currently already in use; stopping device "
"to remove them before using new ones", this, this->n_buffers);
do_stop(this);
clear_buffers(this);
}
spa_log_debug(this->log, "%p: using a pool with %d buffer(s)", this, n_buffers);
if (n_buffers > 0 && !this->have_format)
return -EIO;
if (n_buffers > MAX_BUFFERS)
return -ENOSPC;
for (i = 0; i < n_buffers; i++) {
struct buffer *b = &this->buffers[i];
struct spa_data *d = buffers[i]->datas;
b->id = i;
b->flags = BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA;
b->buf = buffers[i];
if (d[0].data == NULL) {
spa_log_error(this->log, "%p: need mapped memory", this);
return -EINVAL;
}
spa_log_debug(this->log, "%p: got buffer with ID %d bufptr %p data %p", this, i, b->buf, d[0].data);
}
this->n_buffers = n_buffers;
return 0;
}
static int impl_port_set_io(void *object,
enum spa_direction direction,
uint32_t port_id,
uint32_t id,
void *data, size_t size)
{
struct impl *this = object;
spa_return_val_if_fail(this != NULL, -EINVAL);
spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL);
switch (id) {
case SPA_IO_Buffers:
spa_log_debug(this->log, "%p: got buffers IO with data %p", this, data);
this->port_buffers_io = data;
break;
default:
return -ENOENT;
}
return 0;
}
static int impl_port_reuse_buffer(void *object, uint32_t port_id, uint32_t buffer_id)
{
return -ENOTSUP;
}
static int impl_node_process(void *object)
{
struct impl *this = object;
struct spa_io_buffers *io;
struct buffer *b;
int res;
spa_return_val_if_fail(this != NULL, -EINVAL);
io = this->port_buffers_io;
spa_return_val_if_fail(io != NULL, -EIO);
/* Sinks aren't supposed to actually consume anything
* when the graph runs in freewheel mode. */
if (this->node_position_io && this->node_position_io->clock.flags & SPA_IO_CLOCK_FLAG_FREEWHEEL) {
io->status = SPA_STATUS_NEED_DATA;
return SPA_STATUS_HAVE_DATA;
}
/* Add the incoming data if there is some. We place the data in
* a queue instead of just consuming it directly. This allows for
* adjusting driver cycles to the needs of the sink - if the sink
* already has data queued, it does not yet need to schedule a next
* cycle. See on_driver_timeout() for details. This is only relevnt
* if the sink is running as the graph's driver. */
if ((io->status == SPA_STATUS_HAVE_DATA) && (io->buffer_id < this->n_buffers)) {
b = &this->buffers[io->buffer_id];
if (!SPA_FLAG_IS_SET(b->flags, BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA)) {
spa_log_warn(this->log, "%p: buffer %u in use", this, io->buffer_id);
io->status = -EINVAL;
return -EINVAL;
}
if (this->device_is_paused) {
spa_log_debug(this->log, "%p: resuming paused device", this);
if ((res = device_resume(this)) < 0) {
io->status = res;
return SPA_STATUS_STOPPED;
}
}
spa_log_trace_fp(this->log, "%p: queuing buffer %u", this, io->buffer_id);
spa_list_append(&this->queued_output_buffers, &b->link);
SPA_FLAG_CLEAR(b->flags, BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA);
/* This is essential to be able to hold back this buffer
* (which is because we queued it in a custom list for late
* consumption). By setting buffer_id to SPA_ID_INVALID,
* we essentially inform the graph that it must not attempt
* to return this buffer to the buffer pool. */
io->buffer_id = SPA_ID_INVALID;
if (SPA_UNLIKELY((res = write_queued_output_buffers(this)) < 0)) {
io->status = res;
return SPA_STATUS_STOPPED;
}
io->status = SPA_STATUS_OK;
}
return SPA_STATUS_HAVE_DATA;
}
/* SPA node information and init / clear procedures */
static const struct spa_node_methods impl_node = {
SPA_VERSION_NODE_METHODS,
.add_listener = impl_node_add_listener,
.set_callbacks = impl_node_set_callbacks,
.sync = impl_node_sync,
.enum_params = impl_node_enum_params,
.set_param = impl_node_set_param,
.set_io = impl_node_set_io,
.send_command = impl_node_send_command,
.add_port = impl_node_add_port,
.remove_port = impl_node_remove_port,
.port_enum_params = impl_port_enum_params,
.port_set_param = impl_port_set_param,
.port_use_buffers = impl_port_use_buffers,
.port_set_io = impl_port_set_io,
.port_reuse_buffer = impl_port_reuse_buffer,
.process = impl_node_process,
};
static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
{
struct impl *this;
spa_return_val_if_fail(handle != NULL, -EINVAL);
spa_return_val_if_fail(interface != NULL, -EINVAL);
this = (struct impl *) handle;
if (spa_streq(type, SPA_TYPE_INTERFACE_Node))
*interface = &this->node;
else
return -ENOENT;
return 0;
}
static int impl_clear(struct spa_handle *handle)
{
struct impl *this;
spa_return_val_if_fail(handle != NULL, -EINVAL);
this = (struct impl *) handle;
device_close(this);
if (this->driver_timerfd > 0) {
spa_system_close(this->data_system, this->driver_timerfd);
this->driver_timerfd = -1;
}
spa_log_info(this->log, "%p: created Compress-Offload sink", this);
return 0;
}
static size_t impl_get_size(const struct spa_handle_factory *factory,
const struct spa_dict *params)
{
return sizeof(struct impl);
}
static int
impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle,
const struct spa_dict *info, const struct spa_support *support, uint32_t n_support)
{
struct impl *this;
uint32_t i;
int res = 0;
spa_return_val_if_fail(factory != NULL, -EINVAL);
spa_return_val_if_fail(handle != NULL, -EINVAL);
handle->get_interface = impl_get_interface;
handle->clear = impl_clear;
this = (struct impl *) handle;
this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
/* A logger must always exist, otherwise something is very wrong. */
assert(this->log != NULL);
alsa_log_topic_init(this->log);
this->data_loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataLoop);
if (this->data_loop == NULL) {
spa_log_error(this->log, "%p: could not find a loop", this);
res = -EINVAL;
goto error;
}
this->data_system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_DataSystem);
if (this->data_system == NULL) {
spa_log_error(this->log, "%p: could not find a data system", this);
res = -EINVAL;
goto error;
}
this->node.iface = SPA_INTERFACE_INIT(
SPA_TYPE_INTERFACE_Node,
SPA_VERSION_NODE,
&impl_node, this);
spa_hook_list_init(&this->hooks);
reset_props(&this->props);
this->have_format = false;
this->started = false;
this->freewheel = false;
this->n_buffers = 0;
spa_list_init(&this->queued_output_buffers);
this->offset_within_oldest_output_buffer = 0;
res = this->driver_timerfd = spa_system_timerfd_create(this->data_system, CLOCK_MONOTONIC,
SPA_FD_CLOEXEC | SPA_FD_NONBLOCK);
if (SPA_UNLIKELY(res < 0)) {
spa_log_error(this->log, "%p: could not create driver timerfd: %s", this, spa_strerror(res));
goto error;
}
this->next_driver_time = 0;
this->following = false;
this->node_info_all = SPA_NODE_CHANGE_MASK_FLAGS |
SPA_NODE_CHANGE_MASK_PROPS |
SPA_NODE_CHANGE_MASK_PARAMS;
this->node_info = SPA_NODE_INFO_INIT();
this->node_info.max_input_ports = 1;
this->node_info.flags = SPA_NODE_FLAG_RT |
SPA_NODE_FLAG_IN_PORT_CONFIG |
SPA_NODE_FLAG_NEED_CONFIGURE;
this->node_params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ);
this->node_params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE);
this->node_params[NODE_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
this->node_params[NODE_EnumPortConfig] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ);
this->node_info.params = this->node_params;
this->node_info.n_params = N_NODE_PARAMS;
this->node_clock_io = NULL;
this->node_position_io = NULL;
this->port_info_all = SPA_PORT_CHANGE_MASK_FLAGS |
SPA_PORT_CHANGE_MASK_PARAMS;
this->port_info = SPA_PORT_INFO_INIT();
this->port_info.flags = SPA_PORT_FLAG_LIVE |
SPA_PORT_FLAG_PHYSICAL |
SPA_PORT_FLAG_TERMINAL;
this->port_params[PORT_EnumFormat] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ);
this->port_params[PORT_Format] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE);
this->port_params[PORT_IO] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ);
this->port_params[PORT_Buffers] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0);
this->port_info.params = this->port_params;
this->port_info.n_params = N_PORT_PARAMS;
this->port_buffers_io = NULL;
this->device_context = NULL;
this->device_started = false;
memset(&this->audio_codec_info, 0, sizeof(this->audio_codec_info));
this->device_is_paused = false;
spa_log_info(this->log, "%p: initialized Compress-Offload sink", this);
for (i = 0; info && i < info->n_items; i++) {
const char *k = info->items[i].key;
const char *s = info->items[i].value;
if (spa_streq(k, SPA_KEY_API_ALSA_PATH)) {
snprintf(this->props.device, sizeof(this->props.device), "%s", s);
if ((res = parse_device(this)) < 0)
return res;
}
}
finish:
return res;
error:
impl_clear((struct spa_handle *)this);
goto finish;
}
static const struct spa_interface_info impl_interfaces[] = {
{SPA_TYPE_INTERFACE_Node,},
};
static int
impl_enum_interface_info(const struct spa_handle_factory *factory,
const struct spa_interface_info **info, uint32_t *index)
{
spa_return_val_if_fail(factory != NULL, -EINVAL);
spa_return_val_if_fail(info != NULL, -EINVAL);
spa_return_val_if_fail(index != NULL, -EINVAL);
switch (*index) {
case 0:
*info = &impl_interfaces[*index];
break;
default:
return 0;
}
(*index)++;
return 1;
}
/* Factory info */
static const struct spa_dict_item info_items[] = {
{ SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity <sanchayan@asymptotic.io>, Carlos Rafael Giani <crg7475@mailbox.org>" },
{ SPA_KEY_FACTORY_DESCRIPTION, "Play compressed audio (like MP3 or AAC) with the ALSA Compress-Offload API" },
{ SPA_KEY_FACTORY_USAGE, "["SPA_KEY_API_ALSA_PATH"=<path>]" },
};
static const struct spa_dict info = SPA_DICT_INIT_ARRAY(info_items);
const struct spa_handle_factory spa_alsa_compress_offload_sink_factory = {
SPA_VERSION_HANDLE_FACTORY,
SPA_NAME_API_ALSA_COMPRESS_OFFLOAD_SINK,
&info,
impl_get_size,
impl_init,
impl_enum_interface_info,
};