diff mbox

[RFC,14/23] aplay: add an implementation for transferring by ALSA PCM APIs

Message ID 20170817120004.15326-15-o-takashi@sakamocchi.jp (mailing list archive)
State New, archived
Headers show

Commit Message

Takashi Sakamoto Aug. 17, 2017, 11:59 a.m. UTC
This commit adds an implementation of xfer to transfer data frames
via ALSA PCM interface. Actually, APIs in alsa-lib are used. Hardware
and software parameters are configured directly by this implementation.
There're two ways to handle PCM frames; on mapped page frames or call
system call with buffer in user space. For these ways, this
implementation has an abstraction named as 'xfer_alsa_io_ops'.

Furthermore, in a recent decade, timer-based scheduling model is
introduced by PulseAudio developers. To support this model, this
implementation has an abstraction named as 'xfer_alsa_sched_ops'.
---
 aplay/Makefile.am |   7 +-
 aplay/xfer-alsa.c | 537 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 aplay/xfer-alsa.h |  57 ++++++
 aplay/xfer.c      |   2 +-
 4 files changed, 600 insertions(+), 3 deletions(-)
 create mode 100644 aplay/xfer-alsa.c
 create mode 100644 aplay/xfer-alsa.h
diff mbox

Patch

diff --git a/aplay/Makefile.am b/aplay/Makefile.am
index 9056f56..9108049 100644
--- a/aplay/Makefile.am
+++ b/aplay/Makefile.am
@@ -11,7 +11,8 @@  noinst_HEADERS = \
 	aligner.h \
 	waiter.h \
 	options.h \
-	xfer.h
+	xfer.h \
+	xfer-alsa.h
 
 aplay_SOURCES = \
 	formats.h \
@@ -32,7 +33,9 @@  aplay_SOURCES = \
 	options.h \
 	options.c \
 	xfer.h \
-	xfer.c
+	xfer.c \
+	xfer-alsa.h \
+	xfer-alsa.c
 
 EXTRA_DIST = aplay.1 arecord.1
 EXTRA_CLEAN = arecord
diff --git a/aplay/xfer-alsa.c b/aplay/xfer-alsa.c
new file mode 100644
index 0000000..4a84153
--- /dev/null
+++ b/aplay/xfer-alsa.c
@@ -0,0 +1,537 @@ 
+/*
+ * xfer-alsa.c - receive/transmit frames by ALSA.
+ *
+ * Copyright (c) 2017 Takashi Sakamoto <o-takashi@sakamocchi.jp>
+ *
+ * Licensed under the terms of the GNU General Public License, version 2.
+ */
+
+#include "xfer-alsa.h"
+#include <gettext.h>
+
+static int get_mode(struct context_options *options)
+{
+	int mode = 0;
+
+	if (options->nonblock)
+		mode |= SND_PCM_NONBLOCK;
+	if (options->no_auto_resample)
+		mode |= SND_PCM_NO_AUTO_RESAMPLE;
+	if (options->no_auto_channels)
+		mode |= SND_PCM_NO_AUTO_CHANNELS;
+	if (options->no_auto_format)
+		mode |= SND_PCM_NO_AUTO_FORMAT;
+	if (options->no_softvol)
+		mode |= SND_PCM_NO_SOFTVOL;
+
+	return mode;
+}
+
+static int set_access_hw_param(snd_pcm_t *handle,
+			       snd_pcm_hw_params_t *hw_params,
+			       struct context_options *opts)
+{
+	snd_pcm_access_mask_t *mask;
+	int err;
+
+	err = snd_pcm_access_mask_malloc(&mask);
+	if (err < 0)
+		return err;
+	snd_pcm_access_mask_none(mask);
+	if (opts->mmap) {
+		snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_INTERLEAVED);
+		snd_pcm_access_mask_set(mask,
+					SND_PCM_ACCESS_MMAP_NONINTERLEAVED);
+	} else {
+		snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_RW_INTERLEAVED);
+		snd_pcm_access_mask_set(mask,
+					SND_PCM_ACCESS_RW_NONINTERLEAVED);
+	}
+	err = snd_pcm_hw_params_set_access_mask(handle, hw_params, mask);
+	snd_pcm_access_mask_free(mask);
+
+	return err;
+}
+
+static void dump_available_hw_params(snd_pcm_hw_params_t *hw_params,
+				     const char *const node)
+{
+	unsigned int min_i, max_i;
+	snd_pcm_uframes_t min_l, max_l;
+	snd_pcm_access_mask_t *access_mask;
+	snd_pcm_format_mask_t *format_mask;
+	snd_pcm_subformat_mask_t *subformat_mask;
+	int i;
+	int err;
+
+	printf("Avail params for node: %s\n", node);
+
+	err = snd_pcm_hw_params_get_channels_min(hw_params, &min_i);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_channels_max(hw_params, &max_i);
+	if (err < 0)
+		return;
+	printf("  samples/frame: %u to %u\n", min_i, max_i);
+
+	err = snd_pcm_hw_params_get_rate_min(hw_params, &min_i, NULL);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_rate_max(hw_params, &max_i, NULL);
+	if (err < 0)
+		return;
+	printf("  frames/second: %u to %u\n", min_i, max_i);
+
+	err = snd_pcm_hw_params_get_period_time_min(hw_params, &min_i, NULL);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_period_time_max(hw_params, &max_i, NULL);
+	if (err < 0)
+		return;
+	printf("  msec/period: %u to %u\n", min_i, max_i);
+
+	err = snd_pcm_hw_params_get_period_size_min(hw_params, &min_l, NULL);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_period_size_max(hw_params, &max_l, NULL);
+	if (err < 0)
+		return;
+	printf("  frames/period: %lu to %lu\n", min_l, max_l);
+
+	err = snd_pcm_hw_params_get_periods_min(hw_params, &min_i, NULL);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_periods_max(hw_params, &max_i, NULL);
+	if (err < 0)
+		return;
+	printf("  periods/buffer: %u to %u\n", min_i, max_i);
+
+	err = snd_pcm_hw_params_get_buffer_time_min(hw_params, &min_i, NULL);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_buffer_time_max(hw_params, &max_i, NULL);
+	if (err < 0)
+		return;
+	printf("  msec/buffer: %u to %u\n", min_i, max_i);
+
+	err = snd_pcm_hw_params_get_buffer_size_min(hw_params, &min_l);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_buffer_size_max(hw_params, &max_l);
+	if (err < 0)
+		return;
+	printf("  frames/buffer: %lu to %lu\n", min_l, max_l);
+
+	err = snd_pcm_access_mask_malloc(&access_mask);
+	if (err < 0)
+		return;
+	err = snd_pcm_hw_params_get_access_mask(hw_params, access_mask);
+	if (err < 0)
+		return;
+	printf("  supported access methods:\n");
+	for (i = 0; i <= SND_PCM_ACCESS_LAST; ++i) {
+		if (!snd_pcm_access_mask_test(access_mask, i))
+			continue;
+		printf("    '%s'\n", snd_pcm_access_name(i));
+	}
+	snd_pcm_access_mask_free(access_mask);
+
+	err = snd_pcm_format_mask_malloc(&format_mask);
+	if (err < 0)
+		return;
+	snd_pcm_hw_params_get_format_mask(hw_params, format_mask);
+	printf("  supported sample formats:\n");
+	for (i = 0; i <= SND_PCM_FORMAT_LAST; ++i) {
+		if (!snd_pcm_format_mask_test(format_mask, i))
+			continue;
+		printf("    '%s'\n", snd_pcm_format_name(i));
+	}
+	snd_pcm_format_mask_free(format_mask);
+
+	err = snd_pcm_subformat_mask_malloc(&subformat_mask);
+	if (err < 0)
+		return;
+	snd_pcm_hw_params_get_subformat_mask(hw_params, subformat_mask);
+	printf("  supported sample sub-formats:\n");
+	for (i = 0; i <= SND_PCM_SUBFORMAT_LAST; ++i) {
+		if (!snd_pcm_subformat_mask_test(subformat_mask, i))
+			continue;
+		printf("    '%s'\n", snd_pcm_subformat_name(i));
+	}
+	printf("\n");
+}
+
+static int xfer_alsa_init(struct xfer_context *xfer,
+			  snd_pcm_stream_t direction,
+			  struct context_options *opts)
+{
+	struct alsa_state *state = xfer->private_data;
+	char *node;
+	int mode = 0;
+	int err;
+
+	state->verbose = xfer->verbose;
+
+	if (opts->node == NULL)
+		node = "default";
+	else
+		node = opts->node;
+
+	mode = get_mode(opts);
+	if (mode & SND_PCM_NONBLOCK) {
+		state->waiter = malloc(sizeof(*state->waiter));
+		if (state->waiter == NULL)
+			return -ENOMEM;
+
+		/* TODO: configurable. */
+		err = waiter_context_init(state->waiter, WAITER_TYPE_POLL);
+		if (err < 0)
+			return err;
+	}
+
+	err = snd_pcm_open(&state->handle, node, direction, mode);
+	if (err < 0)
+		return err;
+
+	err = snd_pcm_hw_params_malloc(&state->hw_params);
+	if (err < 0)
+		return err;
+	err = snd_pcm_sw_params_malloc(&state->sw_params);
+	if (err < 0)
+		return err;
+
+	err = snd_pcm_hw_params_any(state->handle, state->hw_params);
+	if (err < 0)
+		return err;
+
+	if (xfer->verbose)
+		dump_available_hw_params(state->hw_params, node);
+
+	return set_access_hw_param(state->handle, state->hw_params, opts);
+}
+
+static int prepare_waiter(struct alsa_state *state)
+{
+	int fd_count;
+	int *fds;
+	struct pollfd *pfds;
+	int i;
+	int err;
+
+	fd_count = snd_pcm_poll_descriptors_count(state->handle);
+
+	fds = calloc(fd_count, sizeof(int));
+	if (fds == NULL)
+		return -ENOMEM;
+
+	pfds = calloc(fd_count, sizeof(struct pollfd));
+	if (pfds == NULL) {
+		free(fds);
+		return -ENOMEM;
+	}
+
+	err = snd_pcm_poll_descriptors(state->handle, pfds, fd_count);
+	if (err < 0)
+		goto end;
+
+	for (i = 0; i < fd_count; ++i)
+		fds[i] = pfds[i].fd;
+
+	err = waiter_context_prepare(state->waiter, fds, fd_count);
+end:
+	free(pfds);
+	free(fds);
+	return err;
+}
+
+static int configure_requested_params(struct alsa_state *state,
+				      snd_pcm_format_t format,
+				      unsigned int samples_per_frame,
+				      unsigned int frames_per_second)
+{
+	int err;
+
+	if (format != SND_PCM_FORMAT_UNKNOWN) {
+		err = snd_pcm_hw_params_set_format(state->handle,
+						   state->hw_params, format);
+		if (err < 0) {
+			printf(_("Sample format '%s' is not available: %s\n"),
+			       snd_pcm_format_name(format), snd_strerror(err));
+			return err;
+		}
+	}
+
+	if (samples_per_frame > 0) {
+		err = snd_pcm_hw_params_set_channels(state->handle,
+						     state->hw_params,
+						     samples_per_frame);
+		if (err < 0) {
+			printf(_("Channels count '%u' is not available: %s\n"),
+			       samples_per_frame, snd_strerror(err));
+			return err;
+		}
+	}
+
+	if (frames_per_second > 0) {
+		err = snd_pcm_hw_params_set_rate(state->handle,
+						 state->hw_params,
+						 frames_per_second, 0);
+		if (err < 0) {
+			printf(_("Sampling rate '%u' is not available: %s\n"),
+			       frames_per_second, snd_strerror(err));
+			return err;
+		}
+	}
+
+	return 0;
+}
+
+static int retrieve_actual_params(snd_pcm_hw_params_t *hw_params,
+				  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)
+{
+	int err;
+
+	err = snd_pcm_hw_params_get_format(hw_params, format);
+	if (err < 0)
+		return err;
+
+	err = snd_pcm_hw_params_get_channels(hw_params,
+					     samples_per_frame);
+	if (err < 0)
+		return err;
+
+	err = snd_pcm_hw_params_get_rate(hw_params, frames_per_second,
+					 NULL);
+	if (err < 0)
+		return err;
+
+	err = snd_pcm_hw_params_get_access(hw_params, access);
+	if (err < 0)
+		return err;
+
+	return snd_pcm_hw_params_get_buffer_size(hw_params, frames_per_buffer);
+}
+
+static void dump_sw_params(struct alsa_state *state)
+{
+	snd_pcm_uframes_t val_l;
+	int val_i;
+	int err;
+
+	printf("sw params\n");
+
+	err = snd_pcm_sw_params_get_avail_min(state->sw_params, &val_l);
+	if (err < 0)
+		return;
+	printf("  avail-min: %lu\n", val_l);
+
+	err = snd_pcm_sw_params_get_period_event(state->sw_params, &val_i);
+	if (err < 0)
+		return;
+	printf("  period-event: %d\n", val_i);
+
+	err = snd_pcm_sw_params_get_start_threshold(state->sw_params, &val_l);
+	if (err < 0)
+		return;
+	printf("  start-threshold: %lu\n", val_l);
+
+	err = snd_pcm_sw_params_get_stop_threshold(state->sw_params, &val_l);
+	if (err < 0)
+		return;
+	printf("  stop-threshold: %lu\n", val_l);
+
+	err = snd_pcm_sw_params_get_silence_threshold(state->sw_params, &val_l);
+	if (err < 0)
+		return;
+	printf("  silence-threshold: %lu\n", val_l);
+
+	err = snd_pcm_sw_params_get_silence_size(state->sw_params, &val_l);
+	if (err < 0)
+		return;
+	printf("  silence-size: %lu\n", val_l);
+
+	printf("\n");
+}
+
+static int xfer_alsa_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 alsa_state *state = xfer->private_data;
+	int err;
+
+	err = configure_requested_params(state, *format, *samples_per_frame,
+					 *frames_per_second);
+	if (err < 0)
+		return err;
+
+	/* Configure hardware parameters. */
+	err = snd_pcm_hw_params(state->handle, state->hw_params);
+	if (err < 0)
+		return err;
+
+	/* Retrieve actual parameters. */
+	err = retrieve_actual_params(state->hw_params, format,
+				     samples_per_frame, frames_per_second,
+				     access, frames_per_buffer);
+	if (err < 0)
+		return err;
+	state->frames_per_buffer = *frames_per_buffer;
+
+	/* Query software parameters. */
+	err = snd_pcm_sw_params_current(state->handle, state->sw_params);
+	if (err < 0)
+		return err;
+
+	/* Assign I/O operation. */
+	if (state->io_ops->private_size > 0) {
+		state->io_private_data = malloc(state->io_ops->private_size);
+		if (state->io_private_data == NULL)
+			return -ENOMEM;
+		memset(state->io_private_data, 0, state->io_ops->private_size);
+	}
+	state->access = *access;
+	err = state->io_ops->pre_process(state);
+	if (err < 0)
+		return err;
+
+	/* Assign scheduling operation. */
+	err = state->sched_ops->pre_process(xfer);
+	if (err < 0)
+		return err;
+
+	err = snd_pcm_sw_params(state->handle, state->sw_params);
+	if (err < 0)
+		return err;
+
+	if (state->waiter) {
+		/*
+		 * NOTE: This should be after configuring sw_params due to
+		 * timer descriptor.
+		 */
+		err = prepare_waiter(state);
+		if (err < 0)
+			return err;
+	}
+
+	if (xfer->verbose)
+		dump_sw_params(state);
+
+	return 0;
+}
+
+static int xfer_alsa_process_frames(struct xfer_context *xfer,
+				    unsigned int *frame_count,
+				    struct aligner_context *aligner,
+				    struct container_context *cntrs)
+{
+	struct alsa_state *state = xfer->private_data;
+	int err;
+	err = state->io_ops->process_frames(state, frame_count, aligner, cntrs);
+	if (err < 0) {
+		if (err == -EPIPE)
+			err = snd_pcm_prepare(state->handle);
+	}
+
+	return err;
+}
+
+static void xfer_alsa_pause(struct xfer_context *xfer, bool enable)
+{
+	struct alsa_state *state = xfer->private_data;
+	snd_pcm_state_t s = snd_pcm_state(state->handle);
+	int err;
+
+	if (enable) {
+		if (s != SND_PCM_STATE_RUNNING)
+			return;
+	} else {
+		if (s != SND_PCM_STATE_PAUSED)
+			return;
+	}
+
+	/* Not supported. Leave the substream to enter XRUN state. */
+	if (!snd_pcm_hw_params_can_pause(state->hw_params))
+		return;
+
+	err = snd_pcm_pause(state->handle, enable);
+	if (err < 0 && xfer->verbose > 0) {
+		printf("snd_pcm_pause(): %s\n",
+		       snd_strerror(err));
+	}
+}
+
+static void xfer_alsa_post_process(struct xfer_context *xfer)
+{
+	struct alsa_state *state = xfer->private_data;
+	snd_pcm_state_t pcm_state;
+	int err;
+
+	pcm_state = snd_pcm_state(state->handle);
+	if (pcm_state != SND_PCM_STATE_OPEN &&
+	    pcm_state != SND_PCM_STATE_DISCONNECTED) {
+		if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) {
+			err = snd_pcm_drop(state->handle);
+			if (err < 0)
+				printf("snd_pcm_drop(): %s\n",
+				       snd_strerror(err));
+		} else {
+			err = snd_pcm_drain(state->handle);
+			if (err < 0)
+				printf("snd_pcm_drain(): %s\n",
+				       snd_strerror(err));
+		}
+	}
+
+	err = snd_pcm_hw_free(state->handle);
+	if (err < 0)
+		printf("snd_pcm_hw_free(): %s\n", snd_strerror(err));
+
+	snd_pcm_close(state->handle);
+	state->handle = NULL;
+
+	if (state->io_ops && state->io_ops->post_process)
+		state->io_ops->post_process(state);
+	if (state->io_private_data)
+		free(state->io_private_data);
+	state->io_private_data = NULL;
+
+	if (state->waiter)
+		waiter_context_release(state->waiter);
+}
+
+static void xfer_alsa_destroy(struct xfer_context *xfer)
+{
+	struct alsa_state *state = xfer->private_data;
+
+	if (state->waiter)
+		waiter_context_destroy(state->waiter);
+	free(state->waiter);
+	state->waiter = NULL;
+
+	if (state->hw_params)
+		snd_pcm_hw_params_free(state->hw_params);
+	if (state->sw_params)
+		snd_pcm_sw_params_free(state->sw_params);
+	state->hw_params = NULL;
+	state->sw_params = NULL;
+}
+
+const struct xfer_data xfer_alsa = {
+	.ops = {
+		.init		= xfer_alsa_init,
+		.pre_process	= xfer_alsa_pre_process,
+		.process_frames	= xfer_alsa_process_frames,
+		.pause		= xfer_alsa_pause,
+		.post_process	= xfer_alsa_post_process,
+		.destroy	= xfer_alsa_destroy,
+	},
+	.private_size = sizeof(struct alsa_state),
+};
diff --git a/aplay/xfer-alsa.h b/aplay/xfer-alsa.h
new file mode 100644
index 0000000..10c417b
--- /dev/null
+++ b/aplay/xfer-alsa.h
@@ -0,0 +1,57 @@ 
+/*
+ * xfer-alsa.h - a header for receiver/transmitter of frames by ALSA.
+ *
+ * Copyright (c) 2017 Takashi Sakamoto <o-takashi@sakamocchi.jp>
+ *
+ * Licensed under the terms of the GNU General Public License, version 2.
+ */
+
+#ifndef __ALSA_UTILS_APLAY_XFER_ALSA__H_
+#define __ALSA_UTILS_APLAY_XFER_ALSA__H_
+
+#include "xfer.h"
+#include "waiter.h"
+
+enum xfer_alsa_sched_mode {
+	XFER_ALSA_SCHED_MODE_IRQ = 0,
+	XFER_ALSA_SCHED_MODE_TIMER,
+};
+
+struct xfer_alsa_io_ops;
+struct xfer_alsa_sched_ops;
+
+struct alsa_state {
+	snd_pcm_t *handle;
+	snd_pcm_hw_params_t *hw_params;
+	snd_pcm_sw_params_t *sw_params;
+
+	struct waiter_context *waiter;
+
+	snd_pcm_access_t access;
+	snd_pcm_uframes_t frames_per_buffer;
+	bool running;
+
+	const struct xfer_alsa_io_ops *io_ops;
+	void *io_private_data;
+
+	const struct xfer_alsa_sched_ops *sched_ops;
+	void *private_data;
+
+	bool verbose;
+};
+
+struct xfer_alsa_io_ops {
+	int (*pre_process)(struct alsa_state *state);
+	int (*process_frames)(struct alsa_state *state,
+			      unsigned int *frame_count,
+			      struct aligner_context *aligner,
+			      struct container_context *cntrs);
+	void (*post_process)(struct alsa_state *state);
+	unsigned int private_size;
+};
+
+struct xfer_alsa_sched_ops {
+	int (*pre_process)(struct xfer_context *xfer);
+};
+
+#endif
diff --git a/aplay/xfer.c b/aplay/xfer.c
index 3fe094e..977f00a 100644
--- a/aplay/xfer.c
+++ b/aplay/xfer.c
@@ -21,7 +21,7 @@  int xfer_context_init(struct xfer_context *xfer, enum xfer_type type,
 		enum xfer_type type;
 		const struct xfer_data *data;
 	} entries[] = {
-		{XFER_TYPE_ALSA, NULL},
+		{XFER_TYPE_ALSA, &xfer_alsa},
 	};
 	int i;