diff --git a/meson_options.txt b/meson_options.txt index af72586b0..e273deab6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -284,7 +284,7 @@ option('gsettings', option('compress-offload', description: 'Enable ALSA Compress-Offload support', type: 'feature', - value: 'disabled') + value: 'auto') option('pam-defaults-install', description: 'Install limits.d file modifying defaults for all PAM users. Only for old kernels/systemd!', type: 'boolean', diff --git a/spa/meson.build b/spa/meson.build index f88b5f9a3..521d0382c 100644 --- a/spa/meson.build +++ b/spa/meson.build @@ -92,9 +92,9 @@ if get_option('spa-plugins').allowed() libcamera_dep = dependency('libcamera', required: get_option('libcamera')) summary({'libcamera': libcamera_dep.found()}, bool_yn: true, section: 'Backend') - tinycompress_dep = cc.find_library('tinycompress', has_headers: ['tinycompress/tinycompress.h' ], required: get_option('compress-offload')) - summary({'Compress-Offload': tinycompress_dep.found()}, bool_yn: true, section: 'Backend') - cdata.set('HAVE_ALSA_COMPRESS_OFFLOAD', tinycompress_dep.found()) + compress_offload_option = get_option('compress-offload') + summary({'Compress-Offload': compress_offload_option.allowed()}, bool_yn: true, section: 'Backend') + cdata.set('HAVE_ALSA_COMPRESS_OFFLOAD', compress_offload_option.allowed()) # common dependencies libudev_dep = dependency('libudev', required: alsa_dep.found() or get_option('udev').enabled() or get_option('v4l2').enabled()) diff --git a/spa/plugins/alsa/alsa-compress-offload-sink.c b/spa/plugins/alsa/alsa-compress-offload-sink.c index e3b0a3a27..712f952dc 100644 --- a/spa/plugins/alsa/alsa-compress-offload-sink.c +++ b/spa/plugins/alsa/alsa-compress-offload-sink.c @@ -1,260 +1,1152 @@ /* 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 +#include #include -#include -#include -#include -#include +#include +#include #include -#include -#include -#include +#include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include #include -#include -#include +#include #include -#include -#include -#include -#include #include -#include +#include +#include +#include +#include +#include +#include + +#include "alsa.h" +#include "compress-offload-api.h" -#include -#include /* - * This creates a PipeWire sink node which uses the tinycompress user space - * library to use the ALSA Compress-Offload API for writing compressed data - * like MP3, FLAC etc. to an DSP that can handle such data directly. + * 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. - * - * root@dragonboard-845c:~# ls /dev/snd - * by-path comprC0D3 controlC0 pcmC0D0c pcmC0D0p pcmC0D1c pcmC0D1p pcmC0D2c pcmC0D2p timer + * 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 = spa-node-factory + * { factory = adapter * args = { - * factory.name = api.alsa.compress.offload.sink - * node.name = Compress-Offload-Sink + * 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: - * - Clocking - * - Implement pause and resume - * - Having a better wait mechanism + * - DLL for adjusting driver timer intervals to match the device timestamps in on_driver_timeout() * - Automatic loading using alsa-udev - * */ -#define NAME "compress-offload-audio-sink" -#define DEFAULT_CHANNELS 2 -#define DEFAULT_RATE 44100 -#define MAX_BUFFERS 4 -#define MAX_PORTS 1 -#define MAX_CODECS 32 /* See include/sound/compress_params.h */ -#define MIN_FRAGMENT_SIZE (4 * 1024) -#define MAX_FRAGMENT_SIZE (64 * 1024) -#define MIN_NUM_FRAGMENTS (4) -#define MAX_NUM_FRAGMENTS (8 * 4) +/* 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 -struct props { - uint32_t channels; - uint32_t rate; - uint32_t pos[SPA_AUDIO_MAX_CHANNELS]; - char device[64]; -}; +/* 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 -static void reset_props(struct props *props) -{ - props->channels = 0; - props->rate = 0; -} +#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 *outbuf; + struct spa_buffer *buf; + struct spa_list link; }; -struct impl; -struct port { - uint64_t info_all; - struct spa_port_info info; - struct spa_param_info params[5]; - - struct spa_io_buffers *io; - - bool have_format; - struct spa_audio_info current_format; - - struct buffer buffers[MAX_BUFFERS]; - uint32_t n_buffers; - uint32_t written; +/* Node properties. These are accessible through SPA_PARAM_Props. */ +struct props { + /* The'"hw::" device. */ + char device[128]; + /* These are the card and device numbers from the + * from the "hw::" 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 props props; - struct spa_node_info info; - struct spa_param_info params[1]; + struct spa_log *log; + struct spa_loop *data_loop; + struct spa_system *data_system; struct spa_hook_list hooks; struct spa_callbacks callbacks; - struct port port; - unsigned int started_node:1; - unsigned int started_compress:1; - uint64_t info_all; - uint32_t quantum_limit; + struct props props; - struct compr_config compr_conf; - struct snd_codec codec; - struct compress *compress; + bool have_format; + struct spa_audio_info current_audio_info; - int32_t codecs_supported[MAX_CODECS]; - uint32_t num_codecs; + /* 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; }; -#define CHECK_PORT(this,d,p) ((d) == SPA_DIRECTION_INPUT && (p) < MAX_PORTS) -static const struct spa_dict_item node_info_items[] = { - { SPA_KEY_DEVICE_API, "alsa" }, - { SPA_KEY_MEDIA_CLASS, "Audio/Sink" }, - { SPA_KEY_NODE_DRIVER, "false" }, - { SPA_KEY_NODE_PAUSE_ON_IDLE, "false" }, -}; -static const struct codec_id { +/* 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); +static const char * spa_command_to_string(const struct spa_command *command); + +/* 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; -} codec_info[] = { - { SND_AUDIOCODEC_MP3, }, - { SND_AUDIOCODEC_AAC, }, - { SND_AUDIOCODEC_WMA, }, - { SND_AUDIOCODEC_VORBIS, }, - { SND_AUDIOCODEC_FLAC, }, - { SND_AUDIOCODEC_ALAC, }, - { SND_AUDIOCODEC_APE, }, - { SND_AUDIOCODEC_REAL, }, - { SND_AUDIOCODEC_AMR, }, - { SND_AUDIOCODEC_AMRWB, }, + uint32_t media_subtype; + const char *name; }; -static int -open_compress(struct impl *this) -{ - struct compress *compress; +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" }, +}; - compress = compress_open_by_name(this->props.device, COMPRESS_IN, &this->compr_conf); - if (!compress || !is_compress_ready(compress)) { - spa_log_error(this->log, NAME " %p: Unable to open compress device", this); - return -EINVAL; +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; } - this->compress = compress; + memset(&this->audio_codec_info, 0, sizeof(this->audio_codec_info)); + codec = &this->audio_codec_info; - compress_nonblock(this->compress, 1); + 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 -write_compress(struct impl *this, void *buf, int32_t size) +static int device_open(struct impl *this) { - int32_t wrote; - int32_t to_write = size; - struct port *port = &this->port; + assert(this->device_context == NULL); -retry: - wrote = compress_write(this->compress, buf, to_write); - if (wrote < 0) { - spa_log_error(this->log, NAME " %p: Error playing sample: %s", - this, compress_get_error(this->compress)); - return wrote; - } - port->written += wrote; + spa_log_info(this->log, "%p: opening Compress-Offload device, card #%d device #%d", + this, this->props.card_nr, this->props.device_nr); - spa_log_debug(this->log, NAME " %p: We wrote %d, DSP accepted %d\n", this, size, wrote); + this->device_context = compress_offload_api_open(this->props.card_nr, this->props.device_nr, this->log); + if (this->device_context == NULL) + return -errno; - if (wrote < to_write) { - /* - * The choice of 20ms as the time to wait is - * completely arbitrary. - */ - compress_wait(this->compress, 20); - buf = (uint8_t *)buf + wrote; - to_write = to_write - wrote; - goto retry; - } - - /* - * One write has to happen before starting the compressed node. Calling - * compress_start before writing MIN_NUM_FRAGMENTS * MIN_FRAGMENT_SIZE - * will result in a distorted audio playback. - */ - if (!this->started_compress && - (port->written >= (MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS))) { - compress_start(this->compress); - this->started_compress = true; - } - - return size; + 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; +} + +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_invoke(this->data_loop, do_remove_driver_timer_source, 0, NULL, 0, true, 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; + } + } + + check_position_and_clock_config(this); + + 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->position += this->cycle_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", 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_invoke(this->data_loop, do_reevaluate_following_state, 0, NULL, 0, true, 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:," 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) { + char *value_label; + + switch (value_index) { + case 0: value_label = "card"; break; + case 1: value_label = "device"; break; + default: assert(false); + } + + 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; + + if (!this->have_format) + return -EIO; + if (this->n_buffers == 0) + return -EIO; + + 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 accomodate + * 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. 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; +} + + +static const char * spa_command_to_string(const struct spa_command *command) +{ + switch (SPA_NODE_COMMAND_ID(command)) { + case SPA_NODE_COMMAND_Suspend: return "Suspend"; + case SPA_NODE_COMMAND_Pause: return "Pause"; + case SPA_NODE_COMMAND_Start: return "Start"; + case SPA_NODE_COMMAND_Enable: return "Enable"; + case SPA_NODE_COMMAND_Disable: return "Disable"; + case SPA_NODE_COMMAND_Flush: return "Flush"; + case SPA_NODE_COMMAND_Drain: return "Drain"; + case SPA_NODE_COMMAND_Marker: return "Marker"; + case SPA_NODE_COMMAND_ParamBegin: return "ParamBegin"; + case SPA_NODE_COMMAND_ParamEnd: return "ParamEnd"; + case SPA_NODE_COMMAND_RequestProcess: return "RequestProcess"; + default: return ""; + } +} + + + +/* 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->info.change_mask : 0; + uint64_t old = full ? this->node_info.change_mask : 0; + if (full) - this->info.change_mask = this->info_all; - if (this->info.change_mask) { - this->info.props = &SPA_DICT_INIT_ARRAY(node_info_items); - spa_node_emit_info(&this->hooks, &this->info); - this->info.change_mask = old; + 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, struct port *port, bool full) +static void emit_port_info(struct impl *this, bool full) { - uint64_t old = full ? port->info.change_mask : 0; + uint64_t old = full ? this->port_info.change_mask : 0; + if (full) - port->info.change_mask = port->info_all; - if (port->info.change_mask) { + 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, &port->info); - port->info.change_mask = old; + 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) + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) { struct impl *this = object; struct spa_pod *param; @@ -268,25 +1160,88 @@ static int impl_node_enum_params(void *object, int seq, result.id = id; result.next = start; - next: +next: result.index = result.next++; spa_pod_builder_init(&b, buffer, sizeof(buffer)); switch (id) { - case SPA_PARAM_EnumPortConfig: - case SPA_PARAM_PortConfig: + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + switch (result.index) { case 0: 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)); + 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; } @@ -302,65 +1257,72 @@ static int impl_node_enum_params(void *object, int seq, return 0; } -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_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) { - return -ENOTSUP; -} + struct impl *this = object; + int res; -static int -impl_node_remove_port(void *object, enum spa_direction direction, uint32_t port_id) -{ - return -ENOTSUP; + 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) { - return -ENOTSUP; -} + struct impl *this = object; -static int do_start(struct impl *this) -{ - if (this->started_node) - return 0; + spa_return_val_if_fail(this != NULL, -EINVAL); - spa_log_debug(this->log, "Open compressed device: %s", this->props.device); - if (open_compress(this) < 0) - return -EINVAL; - - this->started_node = true; - this->started_compress = false; - - return 0; -} - -static int do_drain(struct impl *this) -{ - if (!this->started_node) - return 0; - - if (this->started_compress) { - spa_log_debug(this->log, NAME " %p: Issuing drain command", this); - compress_drain(this->compress); - spa_log_debug(this->log, NAME " %p: Finished drain", this); + 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; } - return 0; -} - -static int do_stop(struct impl *this) -{ - if (!this->started_node) - return 0; - - compress_stop(this->compress); - compress_close(this->compress); - spa_log_info(this->log, NAME " %p: Closed compress device", this); - - this->compress = NULL; - this->started_node = false; - this->started_compress = false; + reevaluate_following_state(this); + reevaluate_freewheel_state(this); return 0; } @@ -368,197 +1330,177 @@ static int do_stop(struct impl *this) static int impl_node_send_command(void *object, const struct spa_command *command) { struct impl *this = object; - struct port *port; + int res; spa_return_val_if_fail(this != NULL, -EINVAL); spa_return_val_if_fail(command != NULL, -EINVAL); - port = &this->port; + spa_log_debug(this->log, "%p: got new command: %s", this, spa_command_to_string(command)); switch (SPA_NODE_COMMAND_ID(command)) { - case SPA_NODE_COMMAND_Start: - { - if (!port->have_format) - return -EIO; - if (port->n_buffers == 0) - return -EIO; - - do_start(this); + case SPA_NODE_COMMAND_ParamBegin: + if (SPA_UNLIKELY((res = device_open(this)) < 0)) + return res; break; - } - case SPA_NODE_COMMAND_Pause: + + case SPA_NODE_COMMAND_ParamEnd: + device_close(this); + break; + + case SPA_NODE_COMMAND_Start: + if (SPA_UNLIKELY((res = do_start(this)) < 0)) + return res; + break; + case SPA_NODE_COMMAND_Suspend: - do_drain(this); + case SPA_NODE_COMMAND_Pause: do_stop(this); break; default: return -ENOTSUP; } - return 0; -} - -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, &this->port, 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) +static int impl_node_add_port(void *object, enum spa_direction direction, uint32_t port_id, + const struct spa_dict *props) { - struct impl *this = object; - - spa_return_val_if_fail(this != NULL, -EINVAL); - - this->callbacks = SPA_CALLBACKS_INIT(callbacks, data); - - return 0; + return -ENOTSUP; } -static int -port_enum_formats(struct impl *this, - enum spa_direction direction, uint32_t port_id, - uint32_t index, - struct spa_pod **param, - struct spa_pod_builder *builder) +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; - uint32_t codec; - if (index >= this->num_codecs) - return 0; + device_opened = (this->device_context != NULL); + device_started = this->device_started; - codec = this->codecs_supported[index]; + 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 (!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; - switch (codec) { - case SND_AUDIOCODEC_MP3: - info.media_subtype = SPA_MEDIA_SUBTYPE_mp3; - info.info.mp3.rate = this->props.rate; - info.info.mp3.channels = this->props.channels; - break; - case SND_AUDIOCODEC_AAC: - info.media_subtype = SPA_MEDIA_SUBTYPE_aac; - info.info.aac.rate = this->props.rate; - info.info.aac.channels = this->props.channels; - break; - case SND_AUDIOCODEC_WMA: - info.media_subtype = SPA_MEDIA_SUBTYPE_wma; - info.info.wma.rate = this->props.rate; - info.info.wma.channels = this->props.channels; - break; - case SND_AUDIOCODEC_VORBIS: - info.media_subtype = SPA_MEDIA_SUBTYPE_vorbis; - info.info.vorbis.rate = this->props.rate; - info.info.vorbis.channels = this->props.channels; - break; - case SND_AUDIOCODEC_FLAC: - info.media_subtype = SPA_MEDIA_SUBTYPE_flac; - info.info.flac.rate = this->props.rate; - info.info.flac.channels = this->props.channels; - break; - case SND_AUDIOCODEC_ALAC: - info.media_subtype = SPA_MEDIA_SUBTYPE_alac; - info.info.alac.rate = this->props.rate; - info.info.alac.channels = this->props.channels; - break; - case SND_AUDIOCODEC_APE: - info.media_subtype = SPA_MEDIA_SUBTYPE_ape; - info.info.ape.rate = this->props.rate; - info.info.ape.channels = this->props.channels; - break; - case SND_AUDIOCODEC_REAL: - info.media_subtype = SPA_MEDIA_SUBTYPE_ra; - info.info.ra.rate = this->props.rate; - info.info.ra.channels = this->props.channels; - break; - case SND_AUDIOCODEC_AMR: - info.media_subtype = SPA_MEDIA_SUBTYPE_amr; - info.info.amr.rate = this->props.rate; - info.info.amr.channels = this->props.channels; - info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_NB; - break; - case SND_AUDIOCODEC_AMRWB: - info.media_subtype = SPA_MEDIA_SUBTYPE_amr; - info.info.amr.rate = this->props.rate; - info.info.amr.channels = this->props.channels; - info.info.amr.band_mode = SPA_AUDIO_AMR_BAND_MODE_WB; - break; - default: - return -ENOTSUP; + 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 ((*param = spa_format_audio_build(builder, SPA_PARAM_EnumFormat, &info)) == NULL) - return -errno; - return 1; + + 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_node_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 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 port *port; + struct spa_pod *param = NULL; struct spa_pod_builder b = { 0 }; - uint8_t buffer[1024]; - struct spa_pod *param; + uint8_t buffer[4096]; struct spa_result_node_params result; uint32_t count = 0; - int res; 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); - port = &this->port; - result.id = id; result.next = start; - next: +next: result.index = result.next++; spa_pod_builder_init(&b, buffer, sizeof(buffer)); switch (id) { case SPA_PARAM_EnumFormat: - if ((res = port_enum_formats(this, direction, port_id, - result.index, ¶m, &b)) <= 0) - return res; - break; + return port_enum_formats(this, seq, start, num, filter, &b); case SPA_PARAM_Format: - if (!port->have_format) + if (!this->have_format) return -EIO; if (result.index > 0) return 0; - param = spa_format_audio_build(&b, id, &port->current_format); + 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 (!port->have_format) + if (!this->have_format) return -EIO; if (result.index > 0) return 0; @@ -566,25 +1508,16 @@ impl_node_port_enum_params(void *object, int seq, 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), - SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(0), + /* 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( - MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS, - MIN_FRAGMENT_SIZE * MIN_NUM_FRAGMENTS, - MAX_FRAGMENT_SIZE), + 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; - 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; + default: return -ENOENT; } @@ -600,280 +1533,189 @@ impl_node_port_enum_params(void *object, int seq, return 0; } -static int clear_buffers(struct impl *this, struct port *port) -{ - if (port->n_buffers > 0) { - spa_log_info(this->log, NAME " %p: clear buffers", this); - port->n_buffers = 0; - this->started_node = false; - } - return 0; -} - -static int -compress_setup(struct impl *this, struct spa_audio_info *info, uint32_t *out_rate) -{ - struct compr_config *config; - struct snd_codec *codec; - uint32_t channels, rate; - - memset(&this->codec, 0, sizeof(this->codec)); - memset(&this->compr_conf, 0, sizeof(this->compr_conf)); - - config = &this->compr_conf; - codec = &this->codec; - - switch (info->media_subtype) { - case SPA_MEDIA_SUBTYPE_vorbis: - codec->id = SND_AUDIOCODEC_VORBIS; - rate = info->info.vorbis.rate; - channels = info->info.vorbis.channels; - break; - case SPA_MEDIA_SUBTYPE_mp3: - codec->id = SND_AUDIOCODEC_MP3; - rate = info->info.mp3.rate; - channels = info->info.mp3.channels; - break; - case SPA_MEDIA_SUBTYPE_aac: - codec->id = SND_AUDIOCODEC_AAC; - rate = info->info.aac.rate; - channels = info->info.aac.channels; - break; - case SPA_MEDIA_SUBTYPE_flac: - codec->id = SND_AUDIOCODEC_FLAC; - /* - * Taken from the fcplay utility in tinycompress. Required for - * FLAC to work. - */ - codec->options.flac_d.sample_size = 16; - 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; - rate = info->info.flac.rate; - channels = info->info.flac.channels; - break; - case SPA_MEDIA_SUBTYPE_wma: - 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; - break; - case SPA_AUDIO_WMA_PROFILE_WMA9_PRO: - codec->profile = SND_AUDIOPROFILE_WMA9_PRO; - break; - case SPA_AUDIO_WMA_PROFILE_WMA9_LOSSLESS: - codec->profile = SND_AUDIOPROFILE_WMA9_LOSSLESS; - break; - case SPA_AUDIO_WMA_PROFILE_WMA10: - codec->profile = SND_AUDIOPROFILE_WMA10; - break; - case SPA_AUDIO_WMA_PROFILE_WMA10_LOSSLESS: - codec->profile = SND_AUDIOPROFILE_WMA10_LOSSLESS; - break; - default: - spa_log_error(this->log, NAME " %p: Invalid WMA codec 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; - break; - case SPA_MEDIA_SUBTYPE_alac: - codec->id = SND_AUDIOCODEC_ALAC; - rate = info->info.alac.rate; - channels = info->info.alac.channels; - break; - case SPA_MEDIA_SUBTYPE_ape: - codec->id = SND_AUDIOCODEC_APE; - rate = info->info.ape.rate; - channels = info->info.ape.channels; - break; - case SPA_MEDIA_SUBTYPE_ra: - codec->id = SND_AUDIOCODEC_REAL; - rate = info->info.ra.rate; - channels = info->info.ra.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; - break; - break; - default: - return -ENOTSUP; - } - - codec->ch_in = channels; - codec->ch_out = channels; - codec->sample_rate = rate; - *out_rate = rate; - - codec->rate_control = 0; - codec->level = 0; - codec->ch_mode = 0; - codec->format = 0; - - spa_log_info(this->log, NAME " %p: Codec info, profile: %d align: %d rate: %d bitrate: %d", - this, codec->profile, codec->align, codec->sample_rate, codec->bit_rate); - - if (!is_codec_supported_by_name(this->props.device, 0, codec)) { - spa_log_error(this->log, NAME " %p: Requested codec is not supported by DSP", this); - return -EINVAL; - } - - config->codec = codec; - config->fragment_size = MIN_FRAGMENT_SIZE; - config->fragments = MIN_NUM_FRAGMENTS; - - return 0; -} - -static int -port_set_format(struct impl *this, - enum spa_direction direction, - uint32_t port_id, - uint32_t flags, - const struct spa_pod *format) +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; - struct port *port = &this->port; if (format == NULL) { - port->have_format = false; - clear_buffers(this, port); + if (!this->have_format) + return 0; + + spa_log_debug(this->log, "%p: clearing format and closing device", this); + device_close(this); + clear_buffers(this); + + this->have_format = false; } 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, NAME " %p: format parse error: %s", this, - spa_strerror(res)); + spa_log_error(this->log, "%p: error while parsing audio format: %s", + this, spa_strerror(res)); return res; } - if ((res = compress_setup(this, &info, &rate)) < 0) { - spa_log_error(this->log, NAME " %p: can't setup compress: %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); } - port->current_format = info; - port->have_format = true; - port->info.rate = SPA_FRACTION(1, rate); + 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->info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; - this->info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; + this->node_info.change_mask |= SPA_NODE_CHANGE_MASK_FLAGS; + this->node_info.flags &= ~SPA_NODE_FLAG_NEED_CONFIGURE; emit_node_info(this, false); - port->info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; - port->info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_RATE; + this->port_info.change_mask |= SPA_PORT_CHANGE_MASK_PARAMS; - if (port->have_format) { - port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_READWRITE); - port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, SPA_PARAM_INFO_READ); + 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 { - port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); - port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); + 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, port, false); + emit_port_info(this, false); return 0; } -static int -impl_node_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_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: - return port_set_format(this, direction, port_id, flags, param); + res = port_set_format(this, direction, port_id, flags, param); + break; default: - return -ENOENT; + res = -ENOENT; + break; } - return 0; + return res; } -static int -impl_node_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_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; - struct port *port; uint32_t i; spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; + 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); + } - if (!port->have_format) + 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; - - clear_buffers(this, port); + if (n_buffers > MAX_BUFFERS) + return -ENOSPC; for (i = 0; i < n_buffers; i++) { - struct buffer *b; + struct buffer *b = &this->buffers[i]; struct spa_data *d = buffers[i]->datas; - b = &port->buffers[i]; b->id = i; - b->flags = 0; - b->outbuf = buffers[i]; + b->flags = BUFFER_FLAG_AVAILABLE_FOR_NEW_DATA; + b->buf = buffers[i]; if (d[0].data == NULL) { - spa_log_error(this->log, NAME " %p: invalid memory on buffer %p", this, - buffers[i]); + 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); } - port->n_buffers = n_buffers; + + this->n_buffers = n_buffers; return 0; } -static int -impl_node_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_set_io(void *object, + enum spa_direction direction, + uint32_t port_id, + uint32_t id, + void *data, size_t size) { struct impl *this = object; - struct port *port; spa_return_val_if_fail(this != NULL, -EINVAL); - spa_return_val_if_fail(CHECK_PORT(this, direction, port_id), -EINVAL); - port = &this->port; - switch (id) { case SPA_IO_Buffers: - port->io = data; + spa_log_debug(this->log, "%p: got buffers IO with data %p", this, data); + this->port_buffers_io = data; break; default: return -ENOENT; @@ -881,70 +1723,94 @@ impl_node_port_set_io(void *object, 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 port *port; struct spa_io_buffers *io; struct buffer *b; - uint32_t i; + int res; spa_return_val_if_fail(this != NULL, -EINVAL); - port = &this->port; - - io = port->io; + io = this->port_buffers_io; spa_return_val_if_fail(io != NULL, -EIO); - if (io->status != SPA_STATUS_HAVE_DATA) - return io->status; - - if (io->buffer_id >= port->n_buffers) { - io->status = -EINVAL; - return io->status; + /* 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; } - b = &port->buffers[io->buffer_id]; + /* 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]; - for (i = 0; i < b->outbuf->n_datas; i++) { - int32_t offs, size; - int32_t wrote; - void *buf; + 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; + } - struct spa_data *d = b->outbuf->datas; - d = b->outbuf->datas; + 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; + } + } - offs = SPA_MIN(d->chunk->offset, d->maxsize); - size = SPA_MIN(d->maxsize - offs, d->chunk->size); - buf = SPA_PTROFF(d[0].data, offs, void); + 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; - wrote = write_compress(this, buf, size); - if (wrote < 0) { - spa_log_error(this->log, NAME " %p: Error playing sample: %s", - this, compress_get_error(this->compress)); - io->status = wrote; + if (SPA_UNLIKELY((res = write_queued_output_buffers(this)) < 0)) { + io->status = res; return SPA_STATUS_STOPPED; } - } - io->status = SPA_STATUS_OK; + 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_node_port_enum_params, - .port_set_param = impl_node_port_set_param, - .port_use_buffers = impl_node_port_use_buffers, - .port_set_io = impl_node_port_set_io, + .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, }; @@ -967,27 +1833,36 @@ static int impl_get_interface(struct spa_handle *handle, const char *type, void 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) +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) +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; - struct port *port; - const char *str; uint32_t i; + int res = 0; spa_return_val_if_fail(factory != NULL, -EINVAL); spa_return_val_if_fail(handle != NULL, -EINVAL); @@ -998,86 +1873,107 @@ impl_init(const struct spa_handle_factory *factory, 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); - this->node.iface = SPA_INTERFACE_INIT( - SPA_TYPE_INTERFACE_Node, - SPA_VERSION_NODE, - &impl_node, this); - - this->info_all |= SPA_NODE_CHANGE_MASK_FLAGS | - SPA_NODE_CHANGE_MASK_PARAMS; - this->info = SPA_NODE_INFO_INIT(); - this->info.max_input_ports = MAX_PORTS; - this->info.max_output_ports = 0; - this->info.flags = SPA_NODE_FLAG_RT | - SPA_NODE_FLAG_IN_PORT_CONFIG | - SPA_NODE_FLAG_NEED_CONFIGURE; - this->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumPortConfig, SPA_PARAM_INFO_READ); - this->info.params = this->params; - this->info.n_params = 1; reset_props(&this->props); - port = &this->port; - port->info_all = SPA_PORT_CHANGE_MASK_FLAGS | - SPA_PORT_CHANGE_MASK_PARAMS; - port->info = SPA_PORT_INFO_INIT(); - port->info.flags = SPA_PORT_FLAG_NO_REF | - SPA_PORT_FLAG_LIVE | - SPA_PORT_FLAG_PHYSICAL | - SPA_PORT_FLAG_TERMINAL; - port->params[0] = SPA_PARAM_INFO(SPA_PARAM_EnumFormat, SPA_PARAM_INFO_READ); - port->params[1] = SPA_PARAM_INFO(SPA_PARAM_Format, SPA_PARAM_INFO_WRITE); - port->params[2] = SPA_PARAM_INFO(SPA_PARAM_IO, SPA_PARAM_INFO_READ); - port->params[3] = SPA_PARAM_INFO(SPA_PARAM_Buffers, 0); - port->info.params = port->params; - port->info.n_params = 4; - port->written = 0; + 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, "clock.quantum-limit")) { - spa_atou32(s, &this->quantum_limit, 0); - } else if (spa_streq(k, SPA_KEY_AUDIO_CHANNELS)) { - this->props.channels = atoi(s); - } else if (spa_streq(k, SPA_KEY_AUDIO_RATE)) { - this->props.rate = atoi(s); + 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; } } - if (info && (str = spa_dict_lookup(info, SPA_KEY_API_ALSA_PATH))) { - if ((str[0] == 'h') || (str[1] == 'w') || (str[2] == ':')) { - snprintf(this->props.device, sizeof(this->props.device), "%s", str); - } else { - spa_log_error(this->log, NAME " %p: Invalid Compress-Offload hw %s", this, str); - return -EINVAL; - } - } else { - spa_log_error(this->log, NAME " %p: Invalid compress hw", this); - return -EINVAL; - } +finish: + return res; - /* - * TODO: - * - * Move this to use new compress_get_supported_codecs_by_name API once - * merged upstream. - * - * Right now, we pretend all codecs are supported and then error out - * at runtime in port_set_format during compress_setup if not - * supported. - */ - this->num_codecs = SPA_N_ELEMENTS (codec_info); - for (i = 0; i < this->num_codecs; i++) { - this->codecs_supported[i] = codec_info[i].codec_id; - } - - spa_log_info(this->log, NAME " %p: Initialized Compress-Offload sink %s", - this, this->props.device); - - return 0; +error: + impl_clear((struct spa_handle *)this); + goto finish; } static const struct spa_interface_info impl_interfaces[] = { @@ -1086,8 +1982,7 @@ static const struct spa_interface_info impl_interfaces[] = { static int impl_enum_interface_info(const struct spa_handle_factory *factory, - const struct spa_interface_info **info, - uint32_t *index) + 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); @@ -1101,12 +1996,15 @@ impl_enum_interface_info(const struct spa_handle_factory *factory, return 0; } (*index)++; - return 1; } + + +/* Factory info */ + static const struct spa_dict_item info_items[] = { - { SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity " }, + { SPA_KEY_FACTORY_AUTHOR, "Sanchayan Maity , Carlos Rafael Giani " }, { 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"=]" }, }; diff --git a/spa/plugins/alsa/compress-offload-api.c b/spa/plugins/alsa/compress-offload-api.c new file mode 100644 index 000000000..3f140c7a0 --- /dev/null +++ b/spa/plugins/alsa/compress-offload-api.c @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "compress-offload-api.h" + + +struct compress_offload_api_context { + int fd; + struct snd_compr_caps caps; + struct spa_log *log; + bool was_configured; + uint32_t fragment_size; + uint32_t num_fragments; +}; + + +struct compress_offload_api_context* compress_offload_api_open(int card_nr, int device_nr, struct spa_log *log) +{ + struct compress_offload_api_context *context; + char fn[256]; + + assert(card_nr >= 0); + assert(device_nr >= 0); + assert(log != NULL); + + context = calloc(1, sizeof(struct compress_offload_api_context)); + if (context == NULL) { + errno = ENOMEM; + return NULL; + } + + context->log = log; + + snprintf(fn, sizeof(fn), "/dev/snd/comprC%uD%u", card_nr, device_nr); + + context->fd = open(fn, O_WRONLY); + if (context->fd < 0) { + spa_log_error(context->log, "could not open device \"%s\": %s (%d)", fn, strerror(errno), errno); + goto error; + } + + if (ioctl(context->fd, SNDRV_COMPRESS_GET_CAPS, &(context->caps)) != 0) { + spa_log_error(context->log, "could not get device caps: %s (%d)", strerror(errno), errno); + goto error; + } + + return context; + +error: + compress_offload_api_close(context); + if (errno == 0) + errno = EIO; + return NULL; +} + + +void compress_offload_api_close(struct compress_offload_api_context *context) +{ + if (context == NULL) + return; + + if (context->fd > 0) + close(context->fd); + + free(context); +} + + +int compress_offload_api_get_fd(struct compress_offload_api_context *context) +{ + assert(context != NULL); + return context->fd; +} + + +int compress_offload_api_set_params(struct compress_offload_api_context *context, struct snd_codec *codec, + uint32_t fragment_size, uint32_t num_fragments) +{ + struct snd_compr_params params; + + assert(context != NULL); + assert(codec != NULL); + assert( + (fragment_size == 0) || + ((fragment_size >= context->caps.min_fragment_size) && (fragment_size <= context->caps.max_fragment_size)) + ); + assert( + (num_fragments == 0) || + ((num_fragments >= context->caps.min_fragments) && (fragment_size <= context->caps.max_fragments)) + ); + + context->fragment_size = (fragment_size != 0) ? fragment_size : context->caps.min_fragment_size; + context->num_fragments = (num_fragments != 0) ? num_fragments : context->caps.max_fragments; + + memset(¶ms, 0, sizeof(params)); + params.buffer.fragment_size = context->fragment_size; + params.buffer.fragments = context->num_fragments; + memcpy(&(params.codec), codec, sizeof(struct snd_codec)); + + if (ioctl(context->fd, SNDRV_COMPRESS_SET_PARAMS, ¶ms) != 0) { + spa_log_error(context->log, "could not set params: %s (%d)", strerror(errno), errno); + return -errno; + } + + context->was_configured = true; + + return 0; +} + + +void compress_offload_api_get_fragment_config(struct compress_offload_api_context *context, + uint32_t *fragment_size, uint32_t *num_fragments) +{ + assert(context != NULL); + assert(fragment_size != NULL); + assert(num_fragments != NULL); + + *fragment_size = context->fragment_size; + *num_fragments = context->num_fragments; +} + + +const struct snd_compr_caps * compress_offload_api_get_caps(struct compress_offload_api_context *context) +{ + assert(context != NULL); + return &(context->caps); +} + + +int compress_offload_api_get_codec_caps(struct compress_offload_api_context *context, + uint32_t codec_id, struct snd_compr_codec_caps *codec_caps) +{ + assert(context != NULL); + assert(codec_id < SND_AUDIOCODEC_MAX); + assert(codec_caps != NULL); + + memset(codec_caps, 0, sizeof(struct snd_compr_codec_caps)); + codec_caps->codec = codec_id; + + if (ioctl(context->fd, SNDRV_COMPRESS_GET_CODEC_CAPS, codec_caps) != 0) { + spa_log_error(context->log, "could not get caps for codec with ID %#08x: %s (%d)", + codec_id, strerror(errno), errno); + return -errno; + } + + return 0; +} + + +bool compress_offload_api_supports_codec(struct compress_offload_api_context *context, uint32_t codec_id) +{ + uint32_t codec_index; + + assert(context != NULL); + assert(codec_id < SND_AUDIOCODEC_MAX); + + for (codec_index = 0; codec_index < context->caps.num_codecs; ++codec_index) { + if (context->caps.codecs[codec_index] == codec_id) + return true; + } + + return false; +} + + +#define RUN_SIMPLE_COMMAND(CONTEXT, CMD, CMD_NAME) \ +{ \ + assert((CONTEXT) != NULL); \ + assert((CMD_NAME) != NULL); \ +\ + if (ioctl((CONTEXT)->fd, (CMD)) < 0) { \ + spa_log_error((CONTEXT)->log, "could not %s device: %s (%d)", (CMD_NAME), strerror(errno), errno); \ + return -errno; \ + } \ +\ + return 0; \ +} + + +int compress_offload_api_start(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_START, "start"); +} + + +int compress_offload_api_stop(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_STOP, "stop"); +} + + +int compress_offload_api_pause(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_PAUSE, "pause"); +} + + +int compress_offload_api_resume(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_RESUME, "resume"); +} + + +int compress_offload_api_drain(struct compress_offload_api_context *context) +{ + RUN_SIMPLE_COMMAND(context, SNDRV_COMPRESS_DRAIN, "drain"); +} + + +int compress_offload_api_get_timestamp(struct compress_offload_api_context *context, + struct snd_compr_tstamp *timestamp) +{ + assert(context != NULL); + assert(timestamp != NULL); + + if (ioctl(context->fd, SNDRV_COMPRESS_TSTAMP, timestamp) < 0) { + spa_log_error(context->log, "could not get timestamp device: %s (%d)", + strerror(errno), errno); + return -errno; + } + + return 0; +} + + +int compress_offload_api_get_available_space(struct compress_offload_api_context *context, + struct snd_compr_avail *available_space) +{ + assert(context != NULL); + assert(available_space != NULL); + + if (ioctl(context->fd, SNDRV_COMPRESS_AVAIL, available_space) < 0) { + spa_log_error(context->log, "could not get available space from device: %s (%d)", + strerror(errno), errno); + return -errno; + } + + return 0; +} + + +int compress_offload_api_write(struct compress_offload_api_context *context, const void *data, size_t size) +{ + int num_bytes_written; + + assert(context != NULL); + assert(data != NULL); + + num_bytes_written = write(context->fd, data, size); + if (num_bytes_written < 0) { + switch (errno) { + case EBADFD: + /* EBADFD indicates that the device is paused and thus is not an error. */ + break; + default: + spa_log_error(context->log, "could not write %zu byte(s): %s (%d)", + size, strerror(errno), errno); + break; + } + + return -errno; + } + + return num_bytes_written; +} diff --git a/spa/plugins/alsa/compress-offload-api.h b/spa/plugins/alsa/compress-offload-api.h new file mode 100644 index 000000000..0097226ff --- /dev/null +++ b/spa/plugins/alsa/compress-offload-api.h @@ -0,0 +1,76 @@ +#ifndef COMPRESS_OFFLOAD_API_H +#define COMPRESS_OFFLOAD_API_H + +#include +#include +#include +#include +#include + + +struct compress_offload_api_context; + + +#if defined(__GNUC__) && __GNUC__ >= 4 +#define COMPR_API_PRIVATE __attribute__((visibility("hidden"))) +#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590) +#define COMPR_API_PRIVATE __attribute__((visibility("hidden"))) +#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550) +#define COMPR_API_PRIVATE __hidden +#else +#define COMPR_API_PRIVATE +#endif + + +/* This is a simple encapsulation of the ALSA Compress-Offload API + * and its ioctl calls. It is intentionally not using any PipeWire + * or SPA headers to allow for porting it or extracting it as its + * own library in the future if needed. It functions as an alternative + * to tinycompress, and was written, because tinycompress lacks + * critical functionality (it does not expose important device caps) + * and adds little value in this particular use case. + * + * Encapsulating the ioctls behind this API also allows for using + * different backends. This might be interesting in the future for + * testing purposes; for example, an alternative backend could exist + * that emulates a compress-offload device by decoding with FFmpeg. + * This would be useful for debugging compressed audio related issues + * in PipeWire on the PC - an important advantage, since getting to + * actual compress-offload hardware can sometimes be difficult. */ + + +COMPR_API_PRIVATE struct compress_offload_api_context* compress_offload_api_open(int card_nr, int device_nr, + struct spa_log *log); +COMPR_API_PRIVATE void compress_offload_api_close(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_get_fd(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_set_params(struct compress_offload_api_context *context, + struct snd_codec *codec, uint32_t fragment_size, + uint32_t num_fragments); +COMPR_API_PRIVATE void compress_offload_api_get_fragment_config(struct compress_offload_api_context *context, + uint32_t *fragment_size, uint32_t *num_fragments); + +COMPR_API_PRIVATE const struct snd_compr_caps * compress_offload_api_get_caps(struct compress_offload_api_context *context); +COMPR_API_PRIVATE int compress_offload_api_get_codec_caps(struct compress_offload_api_context *context, + uint32_t codec_id, struct snd_compr_codec_caps *codec_caps); +COMPR_API_PRIVATE bool compress_offload_api_supports_codec(struct compress_offload_api_context *context, uint32_t codec_id); + +COMPR_API_PRIVATE int compress_offload_api_start(struct compress_offload_api_context *context); +COMPR_API_PRIVATE int compress_offload_api_stop(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_pause(struct compress_offload_api_context *context); +COMPR_API_PRIVATE int compress_offload_api_resume(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_drain(struct compress_offload_api_context *context); + +COMPR_API_PRIVATE int compress_offload_api_get_timestamp(struct compress_offload_api_context *context, + struct snd_compr_tstamp *timestamp); +COMPR_API_PRIVATE int compress_offload_api_get_available_space(struct compress_offload_api_context *context, + struct snd_compr_avail *available_space); + +COMPR_API_PRIVATE int compress_offload_api_write(struct compress_offload_api_context *context, + const void *data, size_t size); + + +#endif /* COMPRESS_OFFLOAD_API_H */ diff --git a/spa/plugins/alsa/meson.build b/spa/plugins/alsa/meson.build index 7dedc8705..c918ff59f 100644 --- a/spa/plugins/alsa/meson.build +++ b/spa/plugins/alsa/meson.build @@ -14,9 +14,8 @@ spa_alsa_sources = ['alsa.c', 'alsa-seq-bridge.c', 'alsa-seq.c'] -if tinycompress_dep.found() - spa_alsa_sources += [ 'alsa-compress-offload-sink.c' ] - spa_alsa_dependencies += tinycompress_dep +if compress_offload_option.allowed() + spa_alsa_sources += [ 'alsa-compress-offload-sink.c', 'compress-offload-api.c' ] endif spa_alsa = shared_library(