alsa-utils/axfer/xfer-libffado.c

556 lines
14 KiB
C
Raw Normal View History

// 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),
};