mirror of
https://github.com/alsa-project/alsa-utils
synced 2024-11-10 05:55:42 +01:00
6548d13582
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 <o-takashi@sakamocchi.jp> Signed-off-by: Takashi Iwai <tiwai@suse.de>
555 lines
14 KiB
C
555 lines
14 KiB
C
// 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),
|
|
};
|