--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+//
+// xfer-libffado.c - receive/transmit frames by libffado.
+//
+// Copyright (c) 2018 Takashi Sakamoto <o-takashi@sakamocchi.jp>
+//
+// Licensed under the terms of the GNU General Public License, version 2.
+
+#include "xfer.h"
+#include "misc.h"
+
+#include "frame-cache.h"
+
+#include <stdio.h>
+#include <sched.h>
+
+#include <libffado/ffado.h>
+
+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),
+};