From 6548d1358295751235268eaa4c249c6083d69138 Mon Sep 17 00:00:00 2001 From: Takashi Sakamoto Date: Tue, 13 Nov 2018 15:41:47 +0900 Subject: [PATCH] axfer: add support for libffado transmission backend At present, axfer is designed to use several types of backend for transmission of data frames. This commit is an implementation example of the backend. Libffado is a userspace library for transmission of data frames according to protocols similar to IEC 61883-1/6. This library handles audio and music units on IEEE 1394 bus. Unfortunately, this library executes ctor/dtor of instances for some objects in startup/finish routines of C runtime. As a result, it outputs some superfluous messages even if the backend is not actually used. Furthermore, this library brings memory leak internally. Therefore, it's not practical to build this backend for generic purposes. Although the backend implementation works fine, this commit is just for technical preview. Signed-off-by: Takashi Sakamoto Signed-off-by: Takashi Iwai --- axfer/Makefile.am | 5 + axfer/xfer-libffado.c | 555 ++++++++++++++++++++++++++++++++++++++++++ axfer/xfer.c | 6 + axfer/xfer.h | 9 + configure.ac | 12 + 5 files changed, 587 insertions(+) create mode 100644 axfer/xfer-libffado.c diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 497b9ff..39f414c 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -55,3 +55,8 @@ axfer_SOURCES = \ waiter-select.c \ waiter-epoll.c \ xfer-libasound-timer-mmap.c + +if HAVE_FFADO +axfer_SOURCES += xfer-libffado.c +LDADD += -lffado +endif diff --git a/axfer/xfer-libffado.c b/axfer/xfer-libffado.c new file mode 100644 index 0000000..3b52e2c --- /dev/null +++ b/axfer/xfer-libffado.c @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libffado.c - receive/transmit frames by libffado. +// +// Copyright (c) 2018 Takashi Sakamoto +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer.h" +#include "misc.h" + +#include "frame-cache.h" + +#include +#include + +#include + +struct libffado_state { + ffado_device_t *handle; + enum ffado_direction direction; + + char *port_literal; + char *node_literal; + char *guid_literal; + unsigned int frames_per_period; + unsigned int periods_per_buffer; + unsigned int sched_priority; + bool slave_mode:1; + bool snoop_mode:1; + + unsigned int data_ch_count; + ffado_streaming_stream_type *data_ch_map; + + int (*process_frames)(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + + struct frame_cache cache; +}; + +enum no_short_opts { + OPT_FRAMES_PER_PERIOD = 200, + OPT_PERIODS_PER_BUFFER, + OPT_SLAVE_MODE, + OPT_SNOOP_MODE, + OPT_SCHED_PRIORITY, +}; + +#define S_OPTS "p:n:g:" +static const struct option l_opts[] = { + {"port", 1, 0, 'p'}, + {"node", 1, 0, 'n'}, + {"guid", 1, 0, 'g'}, + {"frames-per-period", 1, 0, OPT_FRAMES_PER_PERIOD}, + {"periods-per-buffer", 1, 0, OPT_PERIODS_PER_BUFFER}, + {"slave", 0, 0, OPT_SLAVE_MODE}, + {"snoop", 0, 0, OPT_SNOOP_MODE}, + {"sched-priority", 1, 0, OPT_SCHED_PRIORITY}, // to SCHED_FIFO +}; + +static int xfer_libffado_init(struct xfer_context *xfer, + snd_pcm_stream_t direction) +{ + struct libffado_state *state = xfer->private_data; + + if (direction == SND_PCM_STREAM_CAPTURE) + state->direction = FFADO_CAPTURE; + else if (direction == SND_PCM_STREAM_PLAYBACK) + state->direction = FFADO_PLAYBACK; + else + return -EINVAL; + + return 0; +} + +static int xfer_libffado_parse_opt(struct xfer_context *xfer, int key, + const char *optarg) +{ + struct libffado_state *state = xfer->private_data; + int err; + + if (key == 'p') + state->port_literal = arg_duplicate_string(optarg, &err); + else if (key == 'n') + state->node_literal = arg_duplicate_string(optarg, &err); + else if (key == 'g') + state->guid_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_FRAMES_PER_PERIOD) + state->frames_per_period = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_PERIODS_PER_BUFFER) + state->periods_per_buffer = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_SLAVE_MODE) + state->slave_mode = true; + else if (key == OPT_SNOOP_MODE) + state->snoop_mode = true; + else if (key == OPT_SCHED_PRIORITY) + state->sched_priority = arg_parse_decimal_num(optarg, &err); + else + err = -ENXIO; + + return err; +} + +static int validate_sched_priority(struct libffado_state *state) +{ + int val; + + val = sched_get_priority_max(SCHED_FIFO); + if (val < 0) + return -errno; + if (state->sched_priority > val) + return -EINVAL; + + val = sched_get_priority_min(SCHED_FIFO); + if (val < 0) + return -errno; + if (state->sched_priority < val) + return -EINVAL; + + return 0; +} + +static int xfer_libffado_validate_opts(struct xfer_context *xfer) +{ + struct libffado_state *state = xfer->private_data; + int err; + + if (state->node_literal != NULL) { + if (state->port_literal == NULL) { + fprintf(stderr, + "'n' option should correspond 'p' option.\n"); + return -EINVAL; + } + } + + if (state->port_literal != NULL && state->guid_literal != NULL) { + fprintf(stderr, + "Neither 'p' option nor 'g' option is available at the " + "same time.\n"); + return -EINVAL; + } + + if (state->guid_literal != NULL) { + if (strstr(state->guid_literal, "0x") != state->guid_literal) { + fprintf(stderr, + "A value of 'g' option should have '0x' as its " + "prefix for hexadecimal number.\n"); + return -EINVAL; + } + } + + if (state->slave_mode && state->snoop_mode) { + fprintf(stderr, "Neither slave mode nor snoop mode is available" + "at the same time.\n"); + return -EINVAL; + } + + if (state->sched_priority > 0) { + err = validate_sched_priority(state); + if (err < 0) + return err; + } + + if (state->frames_per_period == 0) + state->frames_per_period = 512; + if (state->periods_per_buffer == 0) + state->periods_per_buffer = 2; + + return 0; +} + +static int r_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libffado_state *state = xfer->private_data; + unsigned int avail_count; + unsigned int bytes_per_frame; + unsigned int consumed_count; + int err; + + // Trim up to expected frame count. + avail_count = state->frames_per_period; + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&state->cache)) { + int ch; + int pos; + + // Register buffers. + pos = 0; + bytes_per_frame = state->cache.bytes_per_sample * + state->cache.samples_per_frame; + for (ch = 0; ch < state->data_ch_count; ++ch) { + char *buf; + + if (state->data_ch_map[ch] != ffado_stream_type_audio) + continue; + + buf = state->cache.buf_ptr; + buf += ch * bytes_per_frame; + if (ffado_streaming_set_capture_stream_buffer(state->handle, + ch, buf)) + return -EIO; + ++pos; + } + + assert(pos == xfer->samples_per_frame); + + // Move data to the buffer from intermediate buffer. + if (!ffado_streaming_transfer_buffers(state->handle)) + return -EIO; + + frame_cache_increase_count(&state->cache, + state->frames_per_period); + } + + // Write out to file descriptors. + consumed_count = frame_cache_get_count(&state->cache); + err = mapper_context_process_frames(mapper, state->cache.buf, + &consumed_count, cntrs); + if (err < 0) + return err; + + frame_cache_reduce(&state->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int w_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libffado_state *state = xfer->private_data; + unsigned int avail_count; + int pos; + int ch; + unsigned int bytes_per_frame; + unsigned int consumed_count; + int err; + + // Trim up to expected frame_count. + avail_count = state->frames_per_period; + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&state->cache)) { + avail_count -= frame_cache_get_count(&state->cache); + + err = mapper_context_process_frames(mapper, state->cache.buf_ptr, + &avail_count, cntrs); + if (err < 0) + return err; + frame_cache_increase_count(&state->cache, avail_count); + avail_count = state->cache.remained_count; + } + + // Register buffers. + pos = 0; + bytes_per_frame = state->cache.bytes_per_sample * + state->cache.samples_per_frame; + for (ch = 0; ch < state->data_ch_count; ++ch) { + char *buf; + + if (state->data_ch_map[ch] != ffado_stream_type_audio) + continue; + + buf = state->cache.buf; + buf += bytes_per_frame; + if (ffado_streaming_set_playback_stream_buffer(state->handle, + ch, buf)) + return -EIO; + ++pos; + } + + assert(pos == xfer->samples_per_frame); + + // Move data on the buffer for transmission. + if (!ffado_streaming_transfer_buffers(state->handle)) + return -EIO; + consumed_count = state->frames_per_period; + + frame_cache_reduce(&state->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int open_handle(struct xfer_context *xfer, + unsigned int frames_per_second) +{ + struct libffado_state *state = xfer->private_data; + ffado_options_t options = {0}; + ffado_device_info_t info = {0}; + + char str[32] = {0}; + char *strings[1]; + + // Set target unit if given. + if (state->port_literal != NULL) { + if (state->node_literal != NULL) { + snprintf(str, sizeof(str), "hw:%s,%s", + state->port_literal, state->node_literal); + } else { + snprintf(str, sizeof(str), "hw:%s", + state->port_literal); + } + } else if (state->guid_literal != NULL) { + snprintf(str, sizeof(str), "guid:%s", state->guid_literal); + } + if (str[0] != '\0') { + info.nb_device_spec_strings = 1; + strings[0] = str; + info.device_spec_strings = strings; + } + + // Set common options. + options.sample_rate = frames_per_second; + options.period_size = state->frames_per_period; + options.nb_buffers = state->periods_per_buffer; + options.realtime = !!(state->sched_priority > 0); + options.packetizer_priority = state->sched_priority; + options.slave_mode = state->slave_mode; + options.snoop_mode = state->snoop_mode; + options.verbose = xfer->verbose; + + state->handle = ffado_streaming_init(info, options); + if (state->handle == NULL) + return -EINVAL; + + return 0; +} + +static int enable_mbla_data_ch(struct libffado_state *state, + unsigned int *samples_per_frame) +{ + int (*func_type)(ffado_device_t *handle, int pos); + int (*func_onoff)(ffado_device_t *handle, int pos, int on); + int count; + int ch; + + if (state->direction == FFADO_CAPTURE) { + func_type = ffado_streaming_get_capture_stream_type; + func_onoff = ffado_streaming_capture_stream_onoff; + count = ffado_streaming_get_nb_capture_streams(state->handle); + } else { + func_type = ffado_streaming_get_playback_stream_type; + func_onoff = ffado_streaming_playback_stream_onoff; + count = ffado_streaming_get_nb_playback_streams(state->handle); + } + if (count <= 0) + return -EIO; + + state->data_ch_map = calloc(count, sizeof(*state->data_ch_map)); + if (state->data_ch_map == NULL) + return -ENOMEM; + state->data_ch_count = count; + + // When a data ch is off, data in the ch is truncated. This helps to + // align PCM frames in interleaved order. + *samples_per_frame = 0; + for (ch = 0; ch < count; ++ch) { + int on; + + state->data_ch_map[ch] = func_type(state->handle, ch); + + on = !!(state->data_ch_map[ch] == ffado_stream_type_audio); + if (func_onoff(state->handle, ch, on)) + return -EIO; + if (on) + ++(*samples_per_frame); + } + + return 0; +} + +static int xfer_libffado_pre_process(struct xfer_context *xfer, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer) +{ + struct libffado_state *state = xfer->private_data; + unsigned int channels; + int err; + + // Supported format of sample is 24 bit multi bit linear audio in + // AM824 format or the others. + if (state->direction == FFADO_CAPTURE) { + if (*format == SND_PCM_FORMAT_UNKNOWN) + *format = SND_PCM_FORMAT_S24; + } + if (*format != SND_PCM_FORMAT_S24) { + fprintf(stderr, + "A libffado backend supports S24 only.\n"); + return -EINVAL; + } + + // The backend requires the number of frames per second for its + // initialization. + if (state->direction == FFADO_CAPTURE) { + if (*frames_per_second == 0) + *frames_per_second = 48000; + } + if (*frames_per_second < 32000 || *frames_per_second > 192000) { + fprintf(stderr, + "A libffado backend supports sampling rate between " + "32000 and 192000, discretely.\n"); + return -EINVAL; + } + + err = open_handle(xfer, *frames_per_second); + if (err < 0) + return err; + + if (ffado_streaming_set_audio_datatype(state->handle, + ffado_audio_datatype_int24)) + return -EINVAL; + + // Decide buffer layout. + err = enable_mbla_data_ch(state, &channels); + if (err < 0) + return err; + + // This backend doesn't support resampling. + if (state->direction == FFADO_CAPTURE) { + if (*samples_per_frame == 0) + *samples_per_frame = channels; + } + if (*samples_per_frame != channels) { + fprintf(stderr, + "The number of samples per frame should be %i.\n", + channels); + return -EINVAL; + } + + // A buffer has interleaved-aligned PCM frames. + *access = SND_PCM_ACCESS_RW_INTERLEAVED; + *frames_per_buffer = + state->frames_per_period * state->periods_per_buffer; + + // Use cache for double number of frames per period. + err = frame_cache_init(&state->cache, *access, + snd_pcm_format_physical_width(*format) / 8, + *samples_per_frame, state->frames_per_period * 2); + if (err < 0) + return err; + + if (state->direction == FFADO_CAPTURE) + state->process_frames = r_process_frames; + else + state->process_frames = w_process_frames; + + if (ffado_streaming_prepare(state->handle)) + return -EIO; + + if (ffado_streaming_start(state->handle)) + return -EIO; + + return 0; +} + +static int xfer_libffado_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libffado_state *state = xfer->private_data; + ffado_wait_response res; + int err; + + res = ffado_streaming_wait(state->handle); + if (res == ffado_wait_shutdown || res == ffado_wait_error) { + err = -EIO; + } else if (res == ffado_wait_xrun) { + // No way to recover in this backend. + err = -EPIPE; + } else if (res == ffado_wait_ok) { + err = state->process_frames(xfer, frame_count, mapper, cntrs); + } else { + err = -ENXIO; + } + + if (err < 0) + *frame_count = 0; + + return err; +} + +static void xfer_libffado_pause(struct xfer_context *xfer, bool enable) +{ + struct libffado_state *state = xfer->private_data; + + // This is an emergency avoidance because this backend doesn't support + // suspend/aresume operation. + if (enable) { + ffado_streaming_stop(state->handle); + ffado_streaming_finish(state->handle); + exit(EXIT_FAILURE); + } +} + +static void xfer_libffado_post_process(struct xfer_context *xfer) +{ + struct libffado_state *state = xfer->private_data; + + if (state->handle != NULL) { + ffado_streaming_stop(state->handle); + ffado_streaming_finish(state->handle); + } + + frame_cache_destroy(&state->cache); + free(state->data_ch_map); + state->data_ch_map = NULL; +} + +static void xfer_libffado_destroy(struct xfer_context *xfer) +{ + struct libffado_state *state = xfer->private_data; + + free(state->port_literal); + free(state->node_literal); + free(state->guid_literal); + state->port_literal = NULL; + state->node_literal = NULL; + state->guid_literal = NULL; +} + +const struct xfer_data xfer_libffado = { + .s_opts = S_OPTS, + .l_opts = l_opts, + .l_opts_count = ARRAY_SIZE(l_opts), + .ops = { + .init = xfer_libffado_init, + .parse_opt = xfer_libffado_parse_opt, + .validate_opts = xfer_libffado_validate_opts, + .pre_process = xfer_libffado_pre_process, + .process_frames = xfer_libffado_process_frames, + .pause = xfer_libffado_pause, + .post_process = xfer_libffado_post_process, + .destroy = xfer_libffado_destroy, + }, + .private_size = sizeof(struct libffado_state), +}; diff --git a/axfer/xfer.c b/axfer/xfer.c index 21595eb..fdf6e60 100644 --- a/axfer/xfer.c +++ b/axfer/xfer.c @@ -13,6 +13,9 @@ static const char *const xfer_type_labels[] = { [XFER_TYPE_LIBASOUND] = "libasound", +#if WITH_FFADO + [XFER_TYPE_LIBFFADO] = "libffado", +#endif }; enum xfer_type xfer_type_from_label(const char *label) @@ -35,6 +38,9 @@ int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, const struct xfer_data *data; } *entry, entries[] = { {XFER_TYPE_LIBASOUND, &xfer_libasound}, +#if WITH_FFADO + {XFER_TYPE_LIBFFADO, &xfer_libffado}, +#endif }; int i; int err; diff --git a/axfer/xfer.h b/axfer/xfer.h index 21ab85d..db2e296 100644 --- a/axfer/xfer.h +++ b/axfer/xfer.h @@ -13,9 +13,14 @@ #include +#include "aconfig.h" + enum xfer_type { XFER_TYPE_UNSUPPORTED = -1, XFER_TYPE_LIBASOUND = 0, +#if WITH_FFADO + XFER_TYPE_LIBFFADO, +#endif XFER_TYPE_COUNT, }; @@ -103,4 +108,8 @@ struct xfer_data { extern const struct xfer_data xfer_libasound; +#if WITH_FFADO + extern const struct xfer_data xfer_libffado; +#endif + #endif diff --git a/configure.ac b/configure.ac index 1c64617..3c11f35 100644 --- a/configure.ac +++ b/configure.ac @@ -50,6 +50,17 @@ if test "$HAVE_SEQ_CLIENT_INFO_GET_PID" = "yes" ; then AC_DEFINE([HAVE_SEQ_CLIENT_INFO_GET_PID], 1, [alsa-lib supports snd_seq_client_info_get_pid]) fi +# +# NOTE: The library 'libffado' (at least v2.4.1) executes ctor/dtor of instances +# for some objects in startup/finish routines of C runtime. As a result, it +# outputs some superfluos messages. Furthermore, it brings much memory leak +# internally. Totally, libffado support is not recommended at all in usual +# purposes except for technical preview. +# +AC_CHECK_LIB([ffado], [ffado_streaming_init], [have_ffado="yes"], [have_ffado="no"]) +AS_IF([test x"$have_ffado" = xyes], + [AC_DEFINE([WITH_FFADO], [1], [Define if FFADO library is available])]) + AM_CONDITIONAL(HAVE_PCM, test "$have_pcm" = "yes") AM_CONDITIONAL(HAVE_MIXER, test "$have_mixer" = "yes") AM_CONDITIONAL(HAVE_RAWMIDI, test "$have_rawmidi" = "yes") @@ -57,6 +68,7 @@ AM_CONDITIONAL(HAVE_SEQ, test "$have_seq" = "yes") AM_CONDITIONAL(HAVE_UCM, test "$have_ucm" = "yes") AM_CONDITIONAL(HAVE_TOPOLOGY, test "$have_topology" = "yes") AM_CONDITIONAL(HAVE_SAMPLERATE, test "$have_samplerate" = "yes") +AM_CONDITIONAL(HAVE_FFADO, test "$have_ffado" = "yes") dnl Use tinyalsa alsabat_backend_tiny=