[alsa-devel] alsa-utils: axfer: rewrite aplay, adding 'timer-based scheduling' option
alsa-project/alsa-utils pull request #3 was opened from takaswie:
Hi,
This patchset is an updated version of my previous RFCv3, and for merging to upstream.
[alsa-devel] [RFCv3][PATCH 00/39] axfer: rewrite aplay, adding 'timer-based scheduling' option http://mailman.alsa-project.org/pipermail/alsa-devel/2017-October/125961.htm...
As I noted in RFCv3, some patches are dropped in this patchset, due to some technical issues. In detail, please refer to RFCv3.
Changes from RFCv3: - rename scheduling option to 'sched-model' - improve waiter implementation and add support for select(2) - categorize supported options as common and backend-dependent - release alsa-lib configuration space at the end of runtime - valgrind test and fix some memory leaks - add 'frame-cache' internal interface to share implementation by several backends. - add 'libffado' backend just for technical preview (linkage of this library brings problems. Please refet to patch comment.)
I apologize but in this time I don't prepare any help document and manuals as well as RFCv3. Let me put them to a part of task for this development period.
Regards
Takashi Sakamoto (35): axfer: add an entry point for this command axfer: add a sub-command to print list of PCMs/devices axfer: add a common interface to handle a file with audio-specific data format axfer: add support for a container of Microsoft/IBM RIFF/Wave format axfer: add support for a container of Sparc AU format axfer: add support for a container of Creative Tech. voice format axfer: add support for a container of raw data axfer: add unit test for container interface axfer: add a common interface to align data frames on different layout axfer: add support for a mapper for single target axfer: add support for a mapper for multiple target axfer: add a unit test for mapper interface axfer: add a common interface to transfer data frames axfer: add a parser for command-line options axfer: add support to transfer data frames by alsa-lib PCM APIs axfer: add support for blocking data transmission operation of alsa-lib PCM API axfer: add a sub-command to transfer data frames axfer: add informative output and an option to suppress it axfer: add an option to dump available hardware parameters axfer: add options related to duration and obsolete '--max-file-size' option axfer: add an option to finish transmission at XRUN axfer: add support for non-blocking operation axfer: add support for MMAP PCM operation axfer: add an option to suppress event waiting axfer: add options for buffer arrangement axfer: add options for software parameters of PCM substream axfer: add options for plugins in alsa-lib axfer: add a common interface of waiter for I/O event notification axfer: add an option for waiter type axfer: add an implementation of waiter for poll(2) axfer: add an implementation of waiter for select(2) axfer: add an implementation of waiter for epoll(7) axfer: add support for timer-based scheduling model with MMAP operation axfer: obsolete some unimplemented options axfer: add support for libffado transmission backend
``` Makefile.am | 2 +- axfer/Makefile.am | 62 +++ axfer/container-au.c | 203 +++++++ axfer/container-raw.c | 66 +++ axfer/container-riff-wave.c | 575 +++++++++++++++++++ axfer/container-voc.c | 833 +++++++++++++++++++++++++++ axfer/container.c | 457 +++++++++++++++ axfer/container.h | 126 +++++ axfer/frame-cache.c | 111 ++++ axfer/frame-cache.h | 47 ++ axfer/main.c | 229 ++++++++ axfer/mapper-multiple.c | 259 +++++++++ axfer/mapper-single.c | 191 +++++++ axfer/mapper.c | 150 +++++ axfer/mapper.h | 87 +++ axfer/misc.h | 19 + axfer/subcmd-list.c | 234 ++++++++ axfer/subcmd-transfer.c | 467 ++++++++++++++++ axfer/subcmd.h | 18 + axfer/test/Makefile.am | 31 ++ axfer/test/container-test.c | 299 ++++++++++ axfer/test/generator.c | 260 +++++++++ axfer/test/generator.h | 47 ++ axfer/test/mapper-test.c | 491 ++++++++++++++++ axfer/waiter-epoll.c | 107 ++++ axfer/waiter-poll.c | 45 ++ axfer/waiter-select.c | 100 ++++ axfer/waiter.c | 104 ++++ axfer/waiter.h | 61 ++ axfer/xfer-libasound-irq-mmap.c | 282 ++++++++++ axfer/xfer-libasound-irq-rw.c | 427 ++++++++++++++ axfer/xfer-libasound-timer-mmap.c | 455 +++++++++++++++ axfer/xfer-libasound.c | 898 ++++++++++++++++++++++++++++++ axfer/xfer-libasound.h | 92 +++ axfer/xfer-libffado.c | 555 ++++++++++++++++++ axfer/xfer-options.c | 589 ++++++++++++++++++++ axfer/xfer.c | 254 +++++++++ axfer/xfer.h | 115 ++++ configure.ac | 15 +- 39 files changed, 9361 insertions(+), 2 deletions(-) create mode 100644 axfer/Makefile.am create mode 100644 axfer/container-au.c create mode 100644 axfer/container-raw.c create mode 100644 axfer/container-riff-wave.c create mode 100644 axfer/container-voc.c create mode 100644 axfer/container.c create mode 100644 axfer/container.h create mode 100644 axfer/frame-cache.c create mode 100644 axfer/frame-cache.h create mode 100644 axfer/main.c create mode 100644 axfer/mapper-multiple.c create mode 100644 axfer/mapper-single.c create mode 100644 axfer/mapper.c create mode 100644 axfer/mapper.h create mode 100644 axfer/misc.h create mode 100644 axfer/subcmd-list.c create mode 100644 axfer/subcmd-transfer.c create mode 100644 axfer/subcmd.h create mode 100644 axfer/test/Makefile.am create mode 100644 axfer/test/container-test.c create mode 100644 axfer/test/generator.c create mode 100644 axfer/test/generator.h create mode 100644 axfer/test/mapper-test.c create mode 100644 axfer/waiter-epoll.c create mode 100644 axfer/waiter-poll.c create mode 100644 axfer/waiter-select.c create mode 100644 axfer/waiter.c create mode 100644 axfer/waiter.h create mode 100644 axfer/xfer-libasound-irq-mmap.c create mode 100644 axfer/xfer-libasound-irq-rw.c create mode 100644 axfer/xfer-libasound-timer-mmap.c create mode 100644 axfer/xfer-libasound.c create mode 100644 axfer/xfer-libasound.h create mode 100644 axfer/xfer-libffado.c create mode 100644 axfer/xfer-options.c create mode 100644 axfer/xfer.c create mode 100644 axfer/xfer.h ```
Request URL : https://github.com/alsa-project/alsa-utils/pull/3 Patch URL : https://github.com/alsa-project/alsa-utils/pull/3.patch Repository URL: https://github.com/alsa-project/alsa-utils
This commit adds a new command, 'axfer' ('ALSA transfer'), to transfer data frames described in asound.h. This command is intended to replace current aplay. The most of features and command line parameters come from aplay as much as possible, while it has more better feature and code to maintain.
This commit adds an entry point for this command. Current option system of aplay is still available, while this command has a sub-command system like commands in iproute2.
Currently, two sub-commands are supported; 'list' and 'transfer'. The 'list' sub-command has the same effect as '-l' and '-L' options of aplay. The 'transfer' sub-command has the same effect as the main feature of aplay. For the sub-command system, an option for stream direction is required; '-P' for playback and '-C' for capture. If you create symbolic links to this binary for aplay/arecord, please execute: $ ln -s axfer aplay $ ln -s axfer arecord
Actual code for each sub-command will be implemented in later commits.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- Makefile.am | 2 +- axfer/Makefile.am | 23 ++++++ axfer/main.c | 193 ++++++++++++++++++++++++++++++++++++++++++++++ axfer/misc.h | 16 ++++ axfer/subcmd.h | 14 ++++ configure.ac | 2 +- 6 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 axfer/Makefile.am create mode 100644 axfer/main.c create mode 100644 axfer/misc.h create mode 100644 axfer/subcmd.h
diff --git a/Makefile.am b/Makefile.am index 3d24b87..a4e25ae 100644 --- a/Makefile.am +++ b/Makefile.am @@ -14,7 +14,7 @@ if ALSACONF SUBDIRS += alsaconf endif if HAVE_PCM -SUBDIRS += aplay iecset speaker-test +SUBDIRS += aplay iecset speaker-test axfer if ALSALOOP SUBDIRS += alsaloop endif diff --git a/axfer/Makefile.am b/axfer/Makefile.am new file mode 100644 index 00000000..b48b9c5 --- /dev/null +++ b/axfer/Makefile.am @@ -0,0 +1,23 @@ +bin_PROGRAMS = \ + axfer + +# To include headers for gettext and version. +AM_CPPFLAGS = \ + -I$(top_srcdir)/include + +# Unit tests. +SUBDIRS = + +LIBRT = @LIBRT@ +LDADD = \ + $(LIBINTL) \ + $(LIBRT) + +noinst_HEADERS = \ + misc.h \ + subcmd.h + +axfer_SOURCES = \ + misc.h \ + subcmd.h \ + main.c diff --git a/axfer/main.c b/axfer/main.c new file mode 100644 index 00000000..1e92941 --- /dev/null +++ b/axfer/main.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0 +// main.c - an entry point for this program. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Originally written as 'aplay', by Michael Beck and Jaroslav Kysela. +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "subcmd.h" +#include "misc.h" + +#include "version.h" + +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> + +enum subcmds { + SUBCMD_TRANSFER = 0, + SUBCMD_LIST, + SUBCMD_HELP, + SUBCMD_VERSION, +}; + +static void print_version(const char *const cmdname) +{ + printf("%s: version %s\n", cmdname, SND_UTIL_VERSION_STR); +} + +static void print_help(void) +{ + printf("help\n"); +} + +static void decide_subcmd(int argc, char *const *argv, enum subcmds *subcmd) +{ + static const char *const subcmds[] = { + [SUBCMD_TRANSFER] = "transfer", + [SUBCMD_LIST] = "list", + [SUBCMD_HELP] = "help", + [SUBCMD_VERSION] = "version", + }; + static const struct { + const char *const name; + enum subcmds subcmd; + } long_opts[] = { + {"--list-devices", SUBCMD_LIST}, + {"--list-pcms", SUBCMD_LIST}, + {"--help", SUBCMD_HELP}, + {"--version", SUBCMD_VERSION}, + }; + static const struct { + unsigned char c; + enum subcmds subcmd; + } short_opts[] = { + {'l', SUBCMD_LIST}, + {'L', SUBCMD_LIST}, + {'h', SUBCMD_HELP}, + }; + char *pos; + int i, j; + + if (argc == 1) { + *subcmd = SUBCMD_HELP; + return; + } + + // sub-command system. + for (i = 0; i < ARRAY_SIZE(subcmds); ++i) { + if (!strcmp(argv[1], subcmds[i])) { + *subcmd = i; + return; + } + } + + // Original command system. For long options. + for (i = 0; i < ARRAY_SIZE(long_opts); ++i) { + for (j = 0; j < argc; ++j) { + if (!strcmp(long_opts[i].name, argv[j])) { + *subcmd = long_opts[i].subcmd; + return; + } + } + } + + // Original command system. For short options. + for (i = 1; i < argc; ++i) { + // Pick up short options only. + if (argv[i][0] != '-' || argv[i][0] == '\0' || + argv[i][1] == '-' || argv[i][1] == '\0') + continue; + for (pos = argv[i]; *pos != '\0'; ++pos) { + for (j = 0; j < ARRAY_SIZE(short_opts); ++j) { + if (*pos == short_opts[j].c) { + *subcmd = short_opts[j].subcmd; + return; + } + } + } + } + + *subcmd = SUBCMD_TRANSFER; +} + +static bool decide_direction(int argc, char *const *argv, + snd_pcm_stream_t *direction) +{ + static const struct { + const char *const name; + snd_pcm_stream_t direction; + } long_opts[] = { + {"--capture", SND_PCM_STREAM_CAPTURE}, + {"--playback", SND_PCM_STREAM_PLAYBACK}, + }; + static const struct { + unsigned char c; + snd_pcm_stream_t direction; + } short_opts[] = { + {'C', SND_PCM_STREAM_CAPTURE}, + {'P', SND_PCM_STREAM_PLAYBACK}, + }; + static const char *const aliases[] = { + [SND_PCM_STREAM_CAPTURE] = "arecord", + [SND_PCM_STREAM_PLAYBACK] = "aplay", + }; + int i, j; + char *pos; + + // Original command system. For long options. + for (i = 0; i < ARRAY_SIZE(long_opts); ++i) { + for (j = 0; j < argc; ++j) { + if (!strcmp(long_opts[i].name, argv[j])) { + *direction = long_opts[i].direction; + return true; + } + } + } + + // Original command system. For short options. + for (i = 1; i < argc; ++i) { + // Pick up short options only. + if (argv[i][0] != '-' || argv[i][0] == '\0' || + argv[i][1] == '-' || argv[i][1] == '\0') + continue; + for (pos = argv[i]; *pos != '\0'; ++pos) { + for (j = 0; j < ARRAY_SIZE(short_opts); ++j) { + if (*pos == short_opts[j].c) { + *direction = short_opts[j].direction; + return true; + } + } + } + } + + // If not decided yet, judge according to command name. + for (i = 0; i < ARRAY_SIZE(aliases); ++i) { + for (pos = argv[0] + strlen(argv[0]); pos != argv[0]; --pos) { + if (strstr(pos, aliases[i]) != NULL) { + *direction = i; + return true; + } + } + } + + return false; +} + +int main(int argc, char *const *argv) +{ + snd_pcm_stream_t direction; + enum subcmds subcmd; + int err = 0; + + if (!decide_direction(argc, argv, &direction)) + subcmd = SUBCMD_HELP; + else + decide_subcmd(argc, argv, &subcmd); + + if (subcmd == SUBCMD_TRANSFER) + printf("execute 'transfer' subcmd.\n"); + else if (subcmd == SUBCMD_LIST) + printf("execute 'list' subcmd.\n"); + else if (subcmd == SUBCMD_VERSION) + print_version(argv[0]); + else + print_help(); + if (err < 0) + return EXIT_FAILURE; + + return EXIT_SUCCESS; +} diff --git a/axfer/misc.h b/axfer/misc.h new file mode 100644 index 00000000..7c8bfb3 --- /dev/null +++ b/axfer/misc.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// misc.h - a header file for miscellaneous tools. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_MISC__H_ +#define __ALSA_UTILS_AXFER_MISC__H_ + +#include <gettext.h> + +#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0])) + +#endif diff --git a/axfer/subcmd.h b/axfer/subcmd.h new file mode 100644 index 00000000..470a415 --- /dev/null +++ b/axfer/subcmd.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// subcmd.h - a header for each sub-commands. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_SUBCMD__H_ +#define __ALSA_UTILS_AXFER_SUBCMD__H_ + +#include <alsa/asoundlib.h> + +#endif diff --git a/configure.ac b/configure.ac index c37b8a5..404fa16 100644 --- a/configure.ac +++ b/configure.ac @@ -430,4 +430,4 @@ AC_OUTPUT(Makefile alsactl/Makefile alsactl/init/Makefile \ utils/alsa-utils.spec seq/Makefile seq/aconnect/Makefile \ seq/aplaymidi/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \ speaker-test/Makefile speaker-test/samples/Makefile \ - alsaloop/Makefile alsa-info/Makefile) + alsaloop/Makefile alsa-info/Makefile axfer/Makefile)
Original aplay implementation has a feature to output two types of list; devices and PCMs. The list of devices is a result to query sound card and pcm component structured maintained in kernel land. The list of PCMs is a result to parse runtime configuration files in alsa-lib. Entries in the former list is corresponding to ALSA PCM character device ('/dev/snd/pcm%uC%uD[p|c]'), while entries in the latter list includes some 'virtual' instances in application runtime.
This commit adds an implementation for the above functionality. This is executed by taking 'list' sub-command. A 'device' option has the same effect as '--list-devices' and '-L' of aplay. A 'pcm' option has the same effect as '--list-pcms' and '-l' of aplay. In both cases, an additional option is required for stream direction. Below is examples of new command system for this sub-command.
$ axfer list device -C (= arecord --list-devices) $ axfer list pcm -P (= aplay -l)
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/main.c | 2 +- axfer/subcmd-list.c | 234 ++++++++++++++++++++++++++++++++++++++++++++ axfer/subcmd.h | 2 + 4 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 axfer/subcmd-list.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index b48b9c5..4e37b92 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -20,4 +20,5 @@ noinst_HEADERS = \ axfer_SOURCES = \ misc.h \ subcmd.h \ - main.c + main.c \ + subcmd-list.c diff --git a/axfer/main.c b/axfer/main.c index 1e92941..c9cf104 100644 --- a/axfer/main.c +++ b/axfer/main.c @@ -181,7 +181,7 @@ int main(int argc, char *const *argv) if (subcmd == SUBCMD_TRANSFER) printf("execute 'transfer' subcmd.\n"); else if (subcmd == SUBCMD_LIST) - printf("execute 'list' subcmd.\n"); + err = subcmd_list(argc, argv, direction); else if (subcmd == SUBCMD_VERSION) print_version(argv[0]); else diff --git a/axfer/subcmd-list.c b/axfer/subcmd-list.c new file mode 100644 index 00000000..f4fd18b --- /dev/null +++ b/axfer/subcmd-list.c @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// subcmd-list.c - operations for list sub command. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "subcmd.h" +#include "misc.h" + +#include <getopt.h> + +static int dump_device(snd_ctl_t *handle, const char *id, const char *name, + snd_pcm_stream_t direction, snd_pcm_info_t *info) +{ + unsigned int count; + int i; + int err; + + printf("card %i: %s [%s], device %i: %s [%s]\n", + snd_pcm_info_get_card(info), id, name, + snd_pcm_info_get_device(info), snd_pcm_info_get_id(info), + snd_pcm_info_get_name(info)); + + count = snd_pcm_info_get_subdevices_count(info); + printf(" Subdevices: %i/%i\n", + snd_pcm_info_get_subdevices_avail(info), count); + + for (i = 0; i < count; ++i) { + snd_pcm_info_set_subdevice(info, i); + + err = snd_ctl_pcm_info(handle, info); + if (err < 0) { + printf("control digital audio playback info (%i): %s", + snd_pcm_info_get_card(info), snd_strerror(err)); + continue; + } + + printf(" Subdevice #%i: %s\n", + i, snd_pcm_info_get_subdevice_name(info)); + } + + return 0; +} + +static int dump_devices(snd_ctl_t *handle, const char *id, const char *name, + snd_pcm_stream_t direction) +{ + snd_pcm_info_t *info; + int device = -1; + int err; + + err = snd_pcm_info_malloc(&info); + if (err < 0) + return err; + + while (1) { + err = snd_ctl_pcm_next_device(handle, &device); + if (err < 0) + break; + if (device < 0) + break; + + snd_pcm_info_set_device(info, device); + snd_pcm_info_set_subdevice(info, 0); + snd_pcm_info_set_stream(info, direction); + err = snd_ctl_pcm_info(handle, info); + if (err < 0) + continue; + + err = dump_device(handle, id, name, direction, info); + if (err < 0) + break; + } + + free(info); + return err; +} + +static int list_devices(snd_pcm_stream_t direction) +{ + int card = -1; + char name[32]; + snd_ctl_t *handle; + snd_ctl_card_info_t *info; + int err; + + err = snd_ctl_card_info_malloc(&info); + if (err < 0) + return err; + + // Not found. + if (snd_card_next(&card) < 0 || card < 0) + goto end; + + printf("**** List of %s Hardware Devices ****\n", + snd_pcm_stream_name(direction)); + + while (card >= 0) { + sprintf(name, "hw:%d", card); + err = snd_ctl_open(&handle, name, 0); + if (err < 0) { + printf("control open (%i): %s", + card, snd_strerror(err)); + } else { + err = snd_ctl_card_info(handle, info); + if (err < 0) { + printf("control hardware info (%i): %s", + card, snd_strerror(err)); + } else { + err = dump_devices(handle, + snd_ctl_card_info_get_id(info), + snd_ctl_card_info_get_name(info), + direction); + } + snd_ctl_close(handle); + } + + if (err < 0) + break; + + // Go to next. + if (snd_card_next(&card) < 0) { + printf("snd_card_next"); + break; + } + } +end: + free(info); + return err; +} + +static int list_pcms(snd_pcm_stream_t direction) +{ + static const char *const filters[] = { + [SND_PCM_STREAM_CAPTURE] = "Input", + [SND_PCM_STREAM_PLAYBACK] = "Output", + }; + const char *filter; + void **hints; + void **n; + char *io; + char *name; + char *desc; + + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return -EINVAL; + + filter = filters[direction]; + + n = hints; + for (n = hints; *n != NULL; ++n) { + io = snd_device_name_get_hint(*n, "IOID"); + if (io != NULL && strcmp(io, filter) != 0) { + free(io); + continue; + } + + name = snd_device_name_get_hint(*n, "NAME"); + desc = snd_device_name_get_hint(*n, "DESC"); + + printf("%s\n", name); + if (desc == NULL) { + free(name); + free(desc); + continue; + } + + + printf(" "); + while (*desc) { + if (*desc == '\n') + printf("\n "); + else + putchar(*desc); + desc++; + } + putchar('\n'); + } + + snd_device_name_free_hint(hints); + + return 0; +} + +static void print_help(void) +{ + printf("help for list sub-command.\n"); +} + +int subcmd_list(int argc, char *const *argv, snd_pcm_stream_t direction) +{ + static const struct { + const char *const category; + int (*func)(snd_pcm_stream_t direction); + } ops[] = { + {"device", list_devices}, + {"pcm", list_pcms}, + }; + int i; + static const char *s_opts = "hlL"; + static const struct option l_opts[] = { + {"list-devices", 0, NULL, 'l'}, + {"list-pcms", 0, NULL, 'L'}, + {NULL, 0, NULL, 0} + }; + int c; + + // Renewed command system. + if (argc > 2 && !strcmp(argv[1], "list")) { + for (i = 0; i < ARRAY_SIZE(ops); ++i) { + if (!strcmp(ops[i].category, argv[2])) + return ops[i].func(direction); + } + } + + // Original command system. + optind = 0; + opterr = 0; + while (1) { + c = getopt_long(argc, argv, s_opts, l_opts, NULL); + if (c < 0) + break; + if (c == 'l') + return list_devices(direction); + if (c == 'L') + return list_pcms(direction); + } + + print_help(); + + return 0; +} diff --git a/axfer/subcmd.h b/axfer/subcmd.h index 470a415..f78b766 100644 --- a/axfer/subcmd.h +++ b/axfer/subcmd.h @@ -11,4 +11,6 @@
#include <alsa/asoundlib.h>
+int subcmd_list(int argc, char *const *argv, snd_pcm_stream_t direction); + #endif
Current aplay supports several types of data format for file; Microsoft/IBM RIFF/Wave (.wav), Sparc AU (.au) and Creative Tech. voice (.voc). These formats were designed to handle audio-related data with interleaved frame alignment.
This commit adds a common interface to handle the file format, named as 'container' module. This includes several functions to build/parse the format data from any file descriptors. Furthermore, this includes several helper functions for implementations of each builder/parser.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 7 +- axfer/container.c | 423 ++++++++++++++++++++++++++++++++++++++++++++++ axfer/container.h | 110 ++++++++++++ 3 files changed, 538 insertions(+), 2 deletions(-) create mode 100644 axfer/container.c create mode 100644 axfer/container.h
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 4e37b92..3913d0c 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -15,10 +15,13 @@ LDADD = \
noinst_HEADERS = \ misc.h \ - subcmd.h + subcmd.h \ + container.h
axfer_SOURCES = \ misc.h \ subcmd.h \ main.c \ - subcmd-list.c + subcmd-list.c \ + container.h \ + container.c diff --git a/axfer/container.c b/axfer/container.c new file mode 100644 index 00000000..77bbd6c --- /dev/null +++ b/axfer/container.c @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container.c - an interface of parser/builder for formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <fcntl.h> + +static const char *const cntr_type_labels[] = { + [CONTAINER_TYPE_PARSER] = "parser", + [CONTAINER_TYPE_BUILDER] = "builder", +}; + +static const char *const cntr_format_labels[] = { + [CONTAINER_FORMAT_COUNT] = "", +}; + +static const char *const suffixes[] = { + [CONTAINER_FORMAT_COUNT] = "", +}; + +const char *const container_suffix_from_format(enum container_format format) +{ + return suffixes[format]; +} + +int container_recursive_read(struct container_context *cntr, void *buf, + unsigned int byte_count) +{ + char *dst = buf; + ssize_t result; + size_t consumed = 0; + + while (consumed < byte_count && !cntr->interrupted) { + result = read(cntr->fd, dst + consumed, byte_count - consumed); + if (result < 0) { + // This descriptor was configured with non-blocking + // mode. EINTR is not cought when get any interrupts. + if (cntr->interrupted) + return -EINTR; + if (errno == EAGAIN) + continue; + return -errno; + } + // Reach EOF. + if (result == 0) { + cntr->eof = true; + return 0; + } + + consumed += result; + } + + return 0; +} + +int container_recursive_write(struct container_context *cntr, void *buf, + unsigned int byte_count) +{ + char *src = buf; + ssize_t result; + size_t consumed = 0; + + while (consumed < byte_count && !cntr->interrupted) { + result = write(cntr->fd, src + consumed, byte_count - consumed); + if (result < 0) { + // This descriptor was configured with non-blocking + // mode. EINTR is not cought when get any interrupts. + if (cntr->interrupted) + return -EINTR; + if (errno == EAGAIN) + continue; + return -errno; + } + + consumed += result; + } + + return 0; +} + +enum container_format container_format_from_path(const char *path) +{ + const char *suffix; + const char *pos; + int i; + + for (i = 0; i < ARRAY_SIZE(suffixes); ++i) { + suffix = suffixes[i]; + + // Check last part of the string. + pos = path + strlen(path) - strlen(suffix); + if (!strcmp(pos, suffix)) + return i; + } + + // Unsupported. + return CONTAINER_FORMAT_COUNT; +} + +int container_seek_offset(struct container_context *cntr, off64_t offset) +{ + off64_t pos; + + pos = lseek64(cntr->fd, offset, SEEK_SET); + if (pos < 0) + return -errno; + if (pos != offset) + return -EIO; + + return 0; +} + +// To avoid blocking execution at system call iteration after receiving UNIX +// signals. +static int set_nonblock_flag(int fd) +{ + int flags; + + flags = fcntl(fd, F_GETFL); + if (flags < 0) + return -errno; + + flags |= O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) < 0) + return -errno; + + return 0; +} + +int container_parser_init(struct container_context *cntr, + const char *const path, unsigned int verbose) +{ + const struct container_parser *parsers[] = { + NULL, + }; + const struct container_parser *parser; + unsigned int size; + int i; + int err; + + assert(cntr); + assert(path); + assert(path[0] != '\0'); + + // Detect forgotten to destruct. + assert(cntr->fd == 0); + assert(cntr->private_data == NULL); + + memset(cntr, 0, sizeof(*cntr)); + + // Open a target descriptor. + if (!strcmp(path, "-")) { + cntr->fd = fileno(stdin); + err = set_nonblock_flag(cntr->fd); + if (err < 0) + return err; + cntr->stdio = true; + } else { + cntr->fd = open(path, O_RDONLY | O_NONBLOCK); + if (cntr->fd < 0) + return -errno; + } + + // 4 bytes are enough to detect supported containers. + err = container_recursive_read(cntr, cntr->magic, sizeof(cntr->magic)); + if (err < 0) + return err; + for (i = 0; i < ARRAY_SIZE(parsers); ++i) { + parser = parsers[i]; + size = strlen(parser->magic); + if (size > 4) + size = 4; + if (!strncmp(cntr->magic, parser->magic, size)) + break; + } + + // Don't forget that the first 4 bytes were already read for magic + // bytes. + cntr->magic_handled = false; + + // Unless detected, use raw container. + if (i == ARRAY_SIZE(parsers)) + return -EINVAL; + + // Allocate private data for the parser. + if (parser->private_size > 0) { + cntr->private_data = malloc(parser->private_size); + if (cntr->private_data == NULL) + return -ENOMEM; + memset(cntr->private_data, 0, parser->private_size); + } + + cntr->type = CONTAINER_TYPE_PARSER; + cntr->process_bytes = container_recursive_read; + cntr->format = parser->format; + cntr->ops = &parser->ops; + cntr->max_size = parser->max_size; + cntr->verbose = verbose; + + return 0; +} + +int container_builder_init(struct container_context *cntr, + const char *const path, enum container_format format, + unsigned int verbose) +{ + const struct container_builder *builders[] = { + NULL, + }; + const struct container_builder *builder; + int err; + + assert(cntr); + assert(path); + assert(path[0] != '\0'); + + // Detect forgotten to destruct. + assert(cntr->fd == 0); + assert(cntr->private_data == NULL); + + memset(cntr, 0, sizeof(*cntr)); + + // Open a target descriptor. + if (path == NULL || *path == '\0') + return -EINVAL; + if (!strcmp(path, "-")) { + cntr->fd = fileno(stdout); + err = set_nonblock_flag(cntr->fd); + if (err < 0) + return err; + cntr->stdio = true; + } else { + cntr->fd = open(path, O_RDWR | O_NONBLOCK | O_CREAT | O_TRUNC, + 0644); + if (cntr->fd < 0) + return -errno; + } + + builder = builders[format]; + + // Allocate private data for the builder. + if (builder->private_size > 0) { + cntr->private_data = malloc(builder->private_size); + if (cntr->private_data == NULL) + return -ENOMEM; + memset(cntr->private_data, 0, builder->private_size); + } + + cntr->type = CONTAINER_TYPE_BUILDER; + cntr->process_bytes = container_recursive_write; + cntr->format = builder->format; + cntr->ops = &builder->ops; + cntr->max_size = builder->max_size; + cntr->verbose = verbose; + + return 0; +} + +int container_context_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *frame_count) +{ + uint64_t byte_count; + unsigned int bytes_per_frame; + int err; + + assert(cntr); + assert(format); + assert(samples_per_frame); + assert(frames_per_second); + assert(frame_count); + + if (cntr->type == CONTAINER_TYPE_BUILDER) + byte_count = cntr->max_size; + + if (cntr->ops->pre_process) { + err = cntr->ops->pre_process(cntr, format, samples_per_frame, + frames_per_second, &byte_count); + if (err < 0) + return err; + if (cntr->eof) + return 0; + } + + assert(*format >= SND_PCM_FORMAT_S8); + assert(*format <= SND_PCM_FORMAT_LAST); + assert(*samples_per_frame > 0); + assert(*frames_per_second > 0); + assert(byte_count > 0); + + cntr->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + cntr->samples_per_frame = *samples_per_frame; + cntr->frames_per_second = *frames_per_second; + + bytes_per_frame = cntr->bytes_per_sample * *samples_per_frame; + *frame_count = byte_count / bytes_per_frame; + cntr->max_size -= cntr->max_size / bytes_per_frame; + + if (cntr->verbose > 0) { + fprintf(stderr, "Container: %s\n", + cntr_type_labels[cntr->type]); + fprintf(stderr, " format: %s\n", + cntr_format_labels[cntr->format]); + fprintf(stderr, " sample format: %s\n", + snd_pcm_format_name(*format)); + fprintf(stderr, " bytes/sample: %u\n", + cntr->bytes_per_sample); + fprintf(stderr, " samples/frame: %u\n", + cntr->samples_per_frame); + fprintf(stderr, " frames/second: %u\n", + cntr->frames_per_second); + if (cntr->type == CONTAINER_TYPE_PARSER) { + fprintf(stderr, " frames: %lu\n", + *frame_count); + } else { + fprintf(stderr, " max frames: %lu\n", + *frame_count); + } + } + + return 0; +} + +int container_context_process_frames(struct container_context *cntr, + void *frame_buffer, + unsigned int *frame_count) +{ + char *buf = frame_buffer; + unsigned int bytes_per_frame; + unsigned int byte_count; + int err; + + assert(cntr); + assert(!cntr->eof); + assert(frame_buffer); + assert(frame_count); + + bytes_per_frame = cntr->bytes_per_sample * cntr->samples_per_frame; + byte_count = *frame_count * bytes_per_frame; + + // Each container has limitation for its volume for sample data. + if (cntr->handled_byte_count > cntr->max_size - byte_count) + byte_count = cntr->max_size - cntr->handled_byte_count; + + // All of supported containers include interleaved PCM frames. + // TODO: process frames for truncate case. + err = cntr->process_bytes(cntr, buf, byte_count); + if (err < 0) { + *frame_count = 0; + return err; + } + + cntr->handled_byte_count += byte_count; + if (cntr->handled_byte_count == cntr->max_size) + cntr->eof = true; + + *frame_count = byte_count / bytes_per_frame; + + return 0; +} + +int container_context_post_process(struct container_context *cntr, + uint64_t *frame_count) +{ + int err = 0; + + assert(cntr); + assert(frame_count); + + if (cntr->verbose && cntr->handled_byte_count > 0) { + fprintf(stderr, " Handled bytes: %lu\n", + cntr->handled_byte_count); + } + + // NOTE* we cannot seek when using standard input/output. + if (!cntr->stdio && cntr->ops && cntr->ops->post_process) { + // Usually, need to write out processed bytes in container + // header even it this program is interrupted. + cntr->interrupted = false; + + err = cntr->ops->post_process(cntr, cntr->handled_byte_count); + } + + // Ensure to perform write-back from disk cache. + if (cntr->type == CONTAINER_TYPE_BUILDER) + fsync(cntr->fd); + + if (err < 0) + return err; + + if (cntr->bytes_per_sample == 0 || cntr->samples_per_frame == 0) { + *frame_count = 0; + } else { + *frame_count = cntr->handled_byte_count / + cntr->bytes_per_sample / + cntr->samples_per_frame; + } + + return 0; +} + +void container_context_destroy(struct container_context *cntr) +{ + assert(cntr); + + close(cntr->fd); + if (cntr->private_data) + free(cntr->private_data); + + cntr->fd = 0; + cntr->private_data = NULL; +} diff --git a/axfer/container.h b/axfer/container.h new file mode 100644 index 00000000..4a94d7f --- /dev/null +++ b/axfer/container.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container.h - an interface of parser/builder for formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_CONTAINER__H_ +#define __ALSA_UTILS_AXFER_CONTAINER__H_ + +#define _LARGEFILE64_SOURCE +#include <sys/types.h> +#include <unistd.h> + +#include <stdbool.h> +#include <stdint.h> + +#include <alsa/asoundlib.h> + +enum container_type { + CONTAINER_TYPE_PARSER = 0, + CONTAINER_TYPE_BUILDER, + CONTAINER_TYPE_COUNT, +}; + +enum container_format { + CONTAINER_FORMAT_COUNT, +}; + +struct container_ops; + +struct container_context { + enum container_type type; + int fd; + int (*process_bytes)(struct container_context *cntr, + void *buffer, unsigned int byte_count); + bool magic_handled; + bool eof; + bool interrupted; + bool stdio; + + enum container_format format; + uint64_t max_size; + char magic[4]; + const struct container_ops *ops; + void *private_data; + + // Available after pre-process. + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_second; + + unsigned int verbose; + uint64_t handled_byte_count; +}; + +const char *const container_suffix_from_format(enum container_format format); +enum container_format container_format_from_path(const char *path); +int container_parser_init(struct container_context *cntr, + const char *const path, unsigned int verbose); +int container_builder_init(struct container_context *cntr, + const char *const path, enum container_format format, + unsigned int verbose); +void container_context_destroy(struct container_context *cntr); +int container_context_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *frame_count); +int container_context_process_frames(struct container_context *cntr, + void *frame_buffer, + unsigned int *frame_count); +int container_context_post_process(struct container_context *cntr, + uint64_t *frame_count); + +// For internal use in 'container' module. + +struct container_ops { + int (*pre_process)(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count); + int (*post_process)(struct container_context *cntr, + uint64_t handled_byte_count); +}; +struct container_parser { + enum container_format format; + const char *const magic; + uint64_t max_size; + struct container_ops ops; + unsigned int private_size; +}; + +struct container_builder { + enum container_format format; + const char *const suffix; + uint64_t max_size; + struct container_ops ops; + unsigned int private_size; +}; + +int container_recursive_read(struct container_context *cntr, void *buf, + unsigned int byte_count); +int container_recursive_write(struct container_context *cntr, void *buf, + unsigned int byte_count); +int container_seek_offset(struct container_context *cntr, off64_t offset); + +#endif
This commit adds support for data of Microsoft/IBM RIFF/Wave format. In this data format, values in each of field are encoded in both bit/little byte order but inner a file the same order is used. Magic bytes in the beginning of data indicated which byte order is used for the file.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/container-riff-wave.c | 575 ++++++++++++++++++++++++++++++++++++ axfer/container.c | 9 +- axfer/container.h | 4 + 4 files changed, 586 insertions(+), 5 deletions(-) create mode 100644 axfer/container-riff-wave.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 3913d0c..092c966 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -24,4 +24,5 @@ axfer_SOURCES = \ main.c \ subcmd-list.c \ container.h \ - container.c + container.c \ + container-riff-wave.c diff --git a/axfer/container-riff-wave.c b/axfer/container-riff-wave.c new file mode 100644 index 00000000..db82493 --- /dev/null +++ b/axfer/container-riff-wave.c @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-riff-wave.c - a parser/builder for a container of RIFF/Wave File. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +// Not portable to all of UNIX platforms. +#include <endian.h> + +// References: +// - 'Resource Interchange File Format (RIFF)' at msdn.microsoft.com +// - 'Multiple channel audio data and WAVE files' at msdn.microsoft.com +// - RFC 2361 'WAVE and AVI Codec Registries' at ietf.org +// - 'mmreg.h' in Wine project +// - 'mmreg.h' in ReactOS project + +#define RIFF_MAGIC "RIF" // A common part. + +#define RIFF_CHUNK_ID_LE "RIFF" +#define RIFF_CHUNK_ID_BE "RIFX" +#define RIFF_FORM_WAVE "WAVE" +#define FMT_SUBCHUNK_ID "fmt " +#define DATA_SUBCHUNK_ID "data" + +// See 'WAVE and AVI Codec Registries (Historic Registry)' in 'iana.org'. +// https://www.iana.org/assignments/wave-avi-codec-registry/ +enum wave_format { + WAVE_FORMAT_PCM = 0x0001, + WAVE_FORMAT_ADPCM = 0x0002, + WAVE_FORMAT_IEEE_FLOAT = 0x0003, + WAVE_FORMAT_ALAW = 0x0006, + WAVE_FORMAT_MULAW = 0x0007, + WAVE_FORMAT_G723_ADPCM = 0x0014, + // The others are not supported. +}; + +struct format_map { + enum wave_format wformat; + snd_pcm_format_t format; +}; + +static const struct format_map format_maps[] = { + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_U8}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S16_LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S16_BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S32_LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S32_BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_3LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S24_3BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S20_3LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S20_3BE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S18_3LE}, + {WAVE_FORMAT_PCM, SND_PCM_FORMAT_S18_3BE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT_LE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT_BE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT64_LE}, + {WAVE_FORMAT_IEEE_FLOAT, SND_PCM_FORMAT_FLOAT64_BE}, + {WAVE_FORMAT_ALAW, SND_PCM_FORMAT_A_LAW}, + {WAVE_FORMAT_MULAW, SND_PCM_FORMAT_MU_LAW}, + // Below sample formats are not currently supported, due to width of + // its sample. + // - WAVE_FORMAT_ADPCM + // - WAVE_FORMAT_G723_ADPCM + // - WAVE_FORMAT_G723_ADPCM + // - WAVE_FORMAT_G723_ADPCM + // - WAVE_FORMAT_G723_ADPCM +}; + +struct riff_chunk { + uint8_t id[4]; + uint32_t size; + + uint8_t data[0]; +}; + +struct riff_chunk_data { + uint8_t id[4]; + + uint8_t subchunks[0]; +}; + +struct riff_subchunk { + uint8_t id[4]; + uint32_t size; + + uint8_t data[0]; +}; + +struct wave_fmt_subchunk { + uint8_t id[4]; + uint32_t size; + + uint16_t format; + uint16_t samples_per_frame; + uint32_t frames_per_second; + uint32_t average_bytes_per_second; + uint16_t bytes_per_frame; + uint16_t bits_per_sample; + uint8_t extension[0]; +}; + +struct wave_data_subchunk { + uint8_t id[4]; + uint32_t size; + + uint8_t frames[0]; +}; + +struct parser_state { + bool be; + enum wave_format format; + unsigned int samples_per_frame; + unsigned int frames_per_second; + unsigned int average_bytes_per_second; + unsigned int bytes_per_frame; + unsigned int bytes_per_sample; + unsigned int avail_bits_in_sample; + unsigned int byte_count; +}; + +static int parse_riff_chunk_header(struct parser_state *state, + struct riff_chunk *chunk, + uint64_t *byte_count) +{ + if (!memcmp(chunk->id, RIFF_CHUNK_ID_BE, sizeof(chunk->id))) + state->be = true; + else if (!memcmp(chunk->id, RIFF_CHUNK_ID_LE, sizeof(chunk->id))) + state->be = false; + else + return -EINVAL; + + if (state->be) + *byte_count = be32toh(chunk->size); + else + *byte_count = le32toh(chunk->size); + + return 0; +} + +static int parse_riff_chunk(struct container_context *cntr, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + union { + struct riff_chunk chunk; + struct riff_chunk_data chunk_data; + } buf = {0}; + int err; + + // Chunk header. 4 bytes were alread read to detect container type. + memcpy(buf.chunk.id, cntr->magic, sizeof(cntr->magic)); + err = container_recursive_read(cntr, + (char *)&buf.chunk + sizeof(cntr->magic), + sizeof(buf.chunk) - sizeof(cntr->magic)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + err = parse_riff_chunk_header(state, &buf.chunk, byte_count); + if (err < 0) + return err; + + // Chunk data header. + err = container_recursive_read(cntr, &buf, sizeof(buf.chunk_data)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + if (memcmp(buf.chunk_data.id, RIFF_FORM_WAVE, + sizeof(buf.chunk_data.id))) + return -EINVAL; + + return 0; +} + +static int parse_wave_fmt_subchunk(struct parser_state *state, + struct wave_fmt_subchunk *subchunk) +{ + if (state->be) { + state->format = be16toh(subchunk->format); + state->samples_per_frame = be16toh(subchunk->samples_per_frame); + state->frames_per_second = be32toh(subchunk->frames_per_second); + state->average_bytes_per_second = + be32toh(subchunk->average_bytes_per_second); + state->bytes_per_frame = be16toh(subchunk->bytes_per_frame); + state->avail_bits_in_sample = + be16toh(subchunk->bits_per_sample); + } else { + state->format = le16toh(subchunk->format); + state->samples_per_frame = le16toh(subchunk->samples_per_frame); + state->frames_per_second = le32toh(subchunk->frames_per_second); + state->average_bytes_per_second = + le32toh(subchunk->average_bytes_per_second); + state->bytes_per_frame = le16toh(subchunk->bytes_per_frame); + state->avail_bits_in_sample = + le16toh(subchunk->bits_per_sample); + } + + if (state->average_bytes_per_second != + state->bytes_per_frame * state->frames_per_second) + return -EINVAL; + + return 0; +} + +static int parse_wave_data_subchunk(struct parser_state *state, + struct wave_data_subchunk *subchunk) +{ + if (state->be) + state->byte_count = be32toh(subchunk->size); + else + state->byte_count = le32toh(subchunk->size); + + return 0; +} + +static int parse_wave_subchunk(struct container_context *cntr) +{ + union { + struct riff_subchunk subchunk; + struct wave_fmt_subchunk fmt_subchunk; + struct wave_data_subchunk data_subchunk; + } buf = {0}; + enum { + SUBCHUNK_TYPE_UNKNOWN = -1, + SUBCHUNK_TYPE_FMT, + SUBCHUNK_TYPE_DATA, + } subchunk_type; + struct parser_state *state = cntr->private_data; + unsigned int required_size; + unsigned int subchunk_data_size; + int err; + + while (1) { + err = container_recursive_read(cntr, &buf, + sizeof(buf.subchunk)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + // Calculate the size of subchunk data. + if (state->be) + subchunk_data_size = be32toh(buf.subchunk.size); + else + subchunk_data_size = le32toh(buf.subchunk.size); + + // Detect type of subchunk. + if (!memcmp(buf.subchunk.id, FMT_SUBCHUNK_ID, + sizeof(buf.subchunk.id))) { + subchunk_type = SUBCHUNK_TYPE_FMT; + } else if (!memcmp(buf.subchunk.id, DATA_SUBCHUNK_ID, + sizeof(buf.subchunk.id))) { + subchunk_type = SUBCHUNK_TYPE_DATA; + } else { + subchunk_type = SUBCHUNK_TYPE_UNKNOWN; + } + + if (subchunk_type != SUBCHUNK_TYPE_UNKNOWN) { + // Parse data of this subchunk. + if (subchunk_type == SUBCHUNK_TYPE_FMT) { + required_size = + sizeof(struct wave_fmt_subchunk) - + sizeof(struct riff_chunk); + } else { + required_size = + sizeof(struct wave_data_subchunk)- + sizeof(struct riff_chunk); + } + + if (subchunk_data_size < required_size) + return -EINVAL; + + err = container_recursive_read(cntr, &buf.subchunk.data, + required_size); + if (err < 0) + return err; + if (cntr->eof) + return 0; + subchunk_data_size -= required_size; + + if (subchunk_type == SUBCHUNK_TYPE_FMT) { + err = parse_wave_fmt_subchunk(state, + &buf.fmt_subchunk); + } else if (subchunk_type == SUBCHUNK_TYPE_DATA) { + err = parse_wave_data_subchunk(state, + &buf.data_subchunk); + } + if (err < 0) + return err; + + // Found frame data. + if (subchunk_type == SUBCHUNK_TYPE_DATA) + break; + } + + // Go to next subchunk. + while (subchunk_data_size > 0) { + unsigned int consume; + + if (subchunk_data_size > sizeof(buf)) + consume = sizeof(buf); + else + consume = subchunk_data_size; + + err = container_recursive_read(cntr, &buf, consume); + if (err < 0) + return err; + if (cntr->eof) + return 0; + subchunk_data_size -= consume; + } + } + + return 0; +} + +static int parse_riff_wave_format(struct container_context *cntr) +{ + uint64_t byte_count; + int err; + + err = parse_riff_chunk(cntr, &byte_count); + if (err < 0) + return err; + + err = parse_wave_subchunk(cntr); + if (err < 0) + return err; + + return 0; +} + +static int wave_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + int phys_width; + const struct format_map *map; + int i; + int err; + + err = parse_riff_wave_format(cntr); + if (err < 0) + return err; + + phys_width = 8 * state->average_bytes_per_second / + state->samples_per_frame / state->frames_per_second; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + map = &format_maps[i]; + if (state->format != map->wformat) + continue; + if (state->avail_bits_in_sample != + snd_pcm_format_width(map->format)) + continue; + if (phys_width != snd_pcm_format_physical_width(map->format)) + continue; + + if (state->be && snd_pcm_format_big_endian(map->format) != 1) + continue; + + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + // Set parameters. + *format = format_maps[i].format; + *samples_per_frame = state->samples_per_frame; + *frames_per_second = state->frames_per_second; + phys_width /= 8; + *byte_count = state->byte_count; + + return 0; +} + +struct builder_state { + bool be; + enum wave_format format; + unsigned int avail_bits_in_sample; + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_second; +}; + +static void build_riff_chunk_header(struct riff_chunk *chunk, + uint64_t byte_count, bool be) +{ + uint64_t data_size = sizeof(struct riff_chunk_data) + + sizeof(struct wave_fmt_subchunk) + + sizeof(struct wave_data_subchunk) + byte_count; + + if (be) { + memcpy(chunk->id, RIFF_CHUNK_ID_BE, sizeof(chunk->id)); + chunk->size = htobe32(data_size); + } else { + memcpy(chunk->id, RIFF_CHUNK_ID_LE, sizeof(chunk->id)); + chunk->size = htole32(data_size); + } +} + +static void build_subchunk_header(struct riff_subchunk *subchunk, + const char *const form, uint64_t size, + bool be) +{ + memcpy(subchunk->id, form, sizeof(subchunk->id)); + if (be) + subchunk->size = htobe32(size); + else + subchunk->size = htole32(size); +} + +static void build_wave_format_subchunk(struct wave_fmt_subchunk *subchunk, + struct builder_state *state) +{ + unsigned int bytes_per_frame = + state->bytes_per_sample * state->samples_per_frame; + unsigned int average_bytes_per_second = state->bytes_per_sample * + state->samples_per_frame * state->frames_per_second; + uint64_t size; + + // No extensions. + size = sizeof(struct wave_fmt_subchunk) - sizeof(struct riff_subchunk); + build_subchunk_header((struct riff_subchunk *)subchunk, FMT_SUBCHUNK_ID, + size, state->be); + + if (state->be) { + subchunk->format = htobe16(state->format); + subchunk->samples_per_frame = htobe16(state->samples_per_frame); + subchunk->frames_per_second = htobe32(state->frames_per_second); + subchunk->average_bytes_per_second = + htobe32(average_bytes_per_second); + subchunk->bytes_per_frame = htobe16(bytes_per_frame); + subchunk->bits_per_sample = + htobe16(state->avail_bits_in_sample); + } else { + subchunk->format = htole16(state->format); + subchunk->samples_per_frame = htole16(state->samples_per_frame); + subchunk->frames_per_second = htole32(state->frames_per_second); + subchunk->average_bytes_per_second = + htole32(average_bytes_per_second); + subchunk->bytes_per_frame = htole16(bytes_per_frame); + subchunk->bits_per_sample = + htole16(state->avail_bits_in_sample); + } +} + +static void build_wave_data_subchunk(struct wave_data_subchunk *subchunk, + uint64_t byte_count, bool be) +{ + build_subchunk_header((struct riff_subchunk *)subchunk, + DATA_SUBCHUNK_ID, byte_count, be); +} + +static int write_riff_chunk_for_wave(struct container_context *cntr, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + union { + struct riff_chunk chunk; + struct riff_chunk_data chunk_data; + struct wave_fmt_subchunk fmt_subchunk; + struct wave_data_subchunk data_subchunk; + } buf = {0}; + uint64_t total_byte_count; + int err; + + // Chunk header. + total_byte_count = sizeof(struct riff_chunk_data) + + sizeof(struct wave_fmt_subchunk) + + sizeof(struct wave_data_subchunk); + if (byte_count > cntr->max_size - total_byte_count) + total_byte_count = cntr->max_size; + else + total_byte_count += byte_count; + build_riff_chunk_header(&buf.chunk, total_byte_count, state->be); + err = container_recursive_write(cntr, &buf, sizeof(buf.chunk)); + if (err < 0) + return err; + + // Chunk data header. + memcpy(buf.chunk_data.id, RIFF_FORM_WAVE, sizeof(buf.chunk_data.id)); + err = container_recursive_write(cntr, &buf, sizeof(buf.chunk_data)); + if (err < 0) + return err; + + // A subchunk in the chunk data for WAVE format. + build_wave_format_subchunk(&buf.fmt_subchunk, state); + err = container_recursive_write(cntr, &buf, sizeof(buf.fmt_subchunk)); + if (err < 0) + return err; + + // A subchunk in the chunk data for WAVE data. + build_wave_data_subchunk(&buf.data_subchunk, byte_count, state->be); + return container_recursive_write(cntr, &buf, sizeof(buf.data_subchunk)); +} + +static int wave_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct builder_state *state = cntr->private_data; + int i; + + // Validate parameters. + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].format == *format) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + state->format = format_maps[i].wformat; + state->avail_bits_in_sample = snd_pcm_format_width(*format); + state->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + state->samples_per_frame = *samples_per_frame; + state->frames_per_second = *frames_per_second; + + state->be = (snd_pcm_format_big_endian(*format) == 1); + + return write_riff_chunk_for_wave(cntr, *byte_count); +} + +static int wave_builder_post_process(struct container_context *cntr, + uint64_t handled_byte_count) +{ + int err; + + err = container_seek_offset(cntr, 0); + if (err < 0) + return err; + + return write_riff_chunk_for_wave(cntr, handled_byte_count); +} + +const struct container_parser container_parser_riff_wave = { + .format = CONTAINER_FORMAT_RIFF_WAVE, + .magic = RIFF_MAGIC, + .max_size = UINT32_MAX - + sizeof(struct riff_chunk_data) - + sizeof(struct wave_fmt_subchunk) - + sizeof(struct wave_data_subchunk), + .ops = { + .pre_process = wave_parser_pre_process, + }, + .private_size = sizeof(struct parser_state), +}; + +const struct container_builder container_builder_riff_wave = { + .format = CONTAINER_FORMAT_RIFF_WAVE, + .max_size = UINT32_MAX - + sizeof(struct riff_chunk_data) - + sizeof(struct wave_fmt_subchunk) - + sizeof(struct wave_data_subchunk), + .ops = { + .pre_process = wave_builder_pre_process, + .post_process = wave_builder_post_process, + }, + .private_size = sizeof(struct builder_state), +}; diff --git a/axfer/container.c b/axfer/container.c index 77bbd6c..04042da 100644 --- a/axfer/container.c +++ b/axfer/container.c @@ -20,13 +20,14 @@ static const char *const cntr_type_labels[] = { };
static const char *const cntr_format_labels[] = { - [CONTAINER_FORMAT_COUNT] = "", + [CONTAINER_FORMAT_RIFF_WAVE] = "riff/wave", };
static const char *const suffixes[] = { - [CONTAINER_FORMAT_COUNT] = "", + [CONTAINER_FORMAT_RIFF_WAVE] = ".wav", };
+ const char *const container_suffix_from_format(enum container_format format) { return suffixes[format]; @@ -140,7 +141,7 @@ int container_parser_init(struct container_context *cntr, const char *const path, unsigned int verbose) { const struct container_parser *parsers[] = { - NULL, + [CONTAINER_FORMAT_RIFF_WAVE] = &container_parser_riff_wave, }; const struct container_parser *parser; unsigned int size; @@ -214,7 +215,7 @@ int container_builder_init(struct container_context *cntr, unsigned int verbose) { const struct container_builder *builders[] = { - NULL, + [CONTAINER_FORMAT_RIFF_WAVE] = &container_builder_riff_wave, }; const struct container_builder *builder; int err; diff --git a/axfer/container.h b/axfer/container.h index 4a94d7f..5808933 100644 --- a/axfer/container.h +++ b/axfer/container.h @@ -25,6 +25,7 @@ enum container_type { };
enum container_format { + CONTAINER_FORMAT_RIFF_WAVE = 0, CONTAINER_FORMAT_COUNT, };
@@ -107,4 +108,7 @@ int container_recursive_write(struct container_context *cntr, void *buf, unsigned int byte_count); int container_seek_offset(struct container_context *cntr, off64_t offset);
+extern const struct container_parser container_parser_riff_wave; +extern const struct container_builder container_builder_riff_wave; + #endif
This commit adds support for data of Sparc AU format. In this data format, values in each of field are encoded in big-endian byte order and available formats of data sample are restricted in big-endian byte order.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/container-au.c | 203 +++++++++++++++++++++++++++++++++++++++++++ axfer/container.c | 6 +- axfer/container.h | 4 + 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 axfer/container-au.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 092c966..35aa226 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -25,4 +25,5 @@ axfer_SOURCES = \ subcmd-list.c \ container.h \ container.c \ - container-riff-wave.c + container-riff-wave.c \ + container-au.c diff --git a/axfer/container-au.c b/axfer/container-au.c new file mode 100644 index 00000000..6459b16 --- /dev/null +++ b/axfer/container-au.c @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-au.c - a parser/builder for a container of Sun Audio File. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +// Not portable to all of UNIX platforms. +#include <endian.h> + +// Reference: +// * http://pubs.opengroup.org/external/auformat.html + +#define AU_MAGIC ".snd" +#define UNKNOWN_SIZE UINT32_MAX + +enum code_id { + CODE_ID_CCIT_MU_LAW_BE = 0x01, + CODE_ID_GENERIC_MBLA_S8 = 0x02, + CODE_ID_GENERIC_MBLA_S16_BE = 0x03, + CODE_ID_GENERIC_MBLA_S32_BE = 0x05, + CODE_ID_IEEE754_FLOAT_S32_BE = 0x06, + CODE_ID_IEEE754_DOUBLE_S64_BE = 0x07, + CODE_ID_CCIT_ADPCM_G721_4BIT_BE = 0x17, + CODE_ID_CCIT_ADPCM_G723_3BIT_BE = 0x19, + CODE_ID_CCIT_A_LAW_BE = 0x1b, +}; + +struct format_map { + enum code_id code_id; + snd_pcm_format_t format; +}; + +static const struct format_map format_maps[] = { + {CODE_ID_GENERIC_MBLA_S8, SND_PCM_FORMAT_S8}, + {CODE_ID_GENERIC_MBLA_S16_BE, SND_PCM_FORMAT_S16_BE}, + {CODE_ID_GENERIC_MBLA_S32_BE, SND_PCM_FORMAT_S32_BE}, + {CODE_ID_IEEE754_FLOAT_S32_BE, SND_PCM_FORMAT_FLOAT_BE}, + {CODE_ID_IEEE754_DOUBLE_S64_BE, SND_PCM_FORMAT_FLOAT64_BE}, + // CODE_ID_CCIT_ADPCM_G721_4BIT_BE is not supported by ALSA. + // CODE_ID_CCIT_ADPCM_G723_3BIT_BE is not supported due to width of + // its sample. + {CODE_ID_CCIT_A_LAW_BE, SND_PCM_FORMAT_A_LAW}, + {CODE_ID_CCIT_MU_LAW_BE, SND_PCM_FORMAT_MU_LAW}, +}; + +struct container_header { + uint8_t magic[4]; + uint32_t hdr_size; + uint32_t data_size; + uint32_t code_id; + uint32_t frames_per_second; + uint32_t samples_per_frame; +}; + +struct container_annotation { + uint32_t chunks[0]; +}; + +struct parser_state { + enum code_id code_id; + unsigned int samples_per_frame; + unsigned int bytes_per_sample; +}; + +static int au_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + struct container_header header; + enum code_id code_id; + int i; + int err; + + // Parse header. 4 bytes are enough to detect supported containers. + memcpy(&header.magic, cntr->magic, sizeof(cntr->magic)); + err = container_recursive_read(cntr, + (char *)&header + sizeof(cntr->magic), + sizeof(header) - sizeof(cntr->magic)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + if (memcmp(header.magic, AU_MAGIC, sizeof(header.magic)) != 0) + return -EINVAL; + if (be32toh(header.hdr_size) != sizeof(struct container_header)) + return -EINVAL; + + code_id = be32toh(header.code_id); + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].code_id == code_id) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + *format = format_maps[i].format; + *frames_per_second = be32toh(header.frames_per_second); + *samples_per_frame = be32toh(header.samples_per_frame); + + state->code_id = code_id; + state->samples_per_frame = *samples_per_frame; + state->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + + *byte_count = be32toh(header.data_size); + + return 0; +} + +struct builder_state { + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_second; + enum code_id code_id; +}; + +static void build_container_header(struct builder_state *state, + struct container_header *header, + unsigned int frames_per_second, + uint64_t byte_count) +{ + memcpy(header->magic, AU_MAGIC, sizeof(header->magic)); + header->hdr_size = htobe32(sizeof(struct container_header)); + header->data_size = htobe32(byte_count); + header->code_id = htobe32(state->code_id); + header->frames_per_second = htobe32(frames_per_second); + header->samples_per_frame = htobe32(state->samples_per_frame); +} + +static int write_container_header(struct container_context *cntr, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + struct container_header header; + + build_container_header(state, &header, state->frames_per_second, + byte_count); + + return container_recursive_write(cntr, &header, sizeof(header)); +} + +static int au_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct builder_state *status = cntr->private_data; + int i; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].format == *format) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + status->code_id = format_maps[i].code_id; + status->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + status->frames_per_second = *frames_per_second; + status->samples_per_frame = *samples_per_frame; + + return write_container_header(cntr, *byte_count); +} + +static int au_builder_post_process(struct container_context *cntr, + uint64_t handled_byte_count) +{ + int err; + + err = container_seek_offset(cntr, 0); + if (err < 0) + return err; + + return write_container_header(cntr, handled_byte_count); +} + +const struct container_parser container_parser_au = { + .format = CONTAINER_FORMAT_AU, + .magic = AU_MAGIC, + .max_size = UINT32_MAX, + .ops = { + .pre_process = au_parser_pre_process, + }, + .private_size = sizeof(struct parser_state), +}; + +const struct container_builder container_builder_au = { + .format = CONTAINER_FORMAT_AU, + .max_size = UINT32_MAX, + .ops = { + .pre_process = au_builder_pre_process, + .post_process = au_builder_post_process, + }, + .private_size = sizeof(struct builder_state), +}; diff --git a/axfer/container.c b/axfer/container.c index 04042da..0e25605 100644 --- a/axfer/container.c +++ b/axfer/container.c @@ -21,10 +21,12 @@ static const char *const cntr_type_labels[] = {
static const char *const cntr_format_labels[] = { [CONTAINER_FORMAT_RIFF_WAVE] = "riff/wave", + [CONTAINER_FORMAT_AU] = "au", };
static const char *const suffixes[] = { - [CONTAINER_FORMAT_RIFF_WAVE] = ".wav", + [CONTAINER_FORMAT_RIFF_WAVE] = ".wav", + [CONTAINER_FORMAT_AU] = ".au", };
@@ -142,6 +144,7 @@ int container_parser_init(struct container_context *cntr, { const struct container_parser *parsers[] = { [CONTAINER_FORMAT_RIFF_WAVE] = &container_parser_riff_wave, + [CONTAINER_FORMAT_AU] = &container_parser_au, }; const struct container_parser *parser; unsigned int size; @@ -216,6 +219,7 @@ int container_builder_init(struct container_context *cntr, { const struct container_builder *builders[] = { [CONTAINER_FORMAT_RIFF_WAVE] = &container_builder_riff_wave, + [CONTAINER_FORMAT_AU] = &container_builder_au, }; const struct container_builder *builder; int err; diff --git a/axfer/container.h b/axfer/container.h index 5808933..19ef672 100644 --- a/axfer/container.h +++ b/axfer/container.h @@ -26,6 +26,7 @@ enum container_type {
enum container_format { CONTAINER_FORMAT_RIFF_WAVE = 0, + CONTAINER_FORMAT_AU, CONTAINER_FORMAT_COUNT, };
@@ -111,4 +112,7 @@ int container_seek_offset(struct container_context *cntr, off64_t offset); extern const struct container_parser container_parser_riff_wave; extern const struct container_builder container_builder_riff_wave;
+extern const struct container_parser container_parser_au; +extern const struct container_builder container_builder_au; + #endif
This commit adds support for data of Creative Tech. voice format. In this data format, values in each of field are represented in little-endian byte order and available formats of data sample are restricted in little-endian byte order.
In version 1.10 of this format, sampling rate is represented with reciprocal number of the rate, thus we cannot calculate original sampling rate precisely just from its header. For example at 44.1kHz, file header includes 233 (=256-1,000,000/44,100), but we cannot recover the value just from the code (43478.2...). For my convenience, this commit adds a pre-computed table and lookup major rates from the table.
Additionally, this format can includes several blocks with different sample format. When handling this type of file, we need to start/stop substream for each of the block, while this brings complicated code. This type of format is enough ancient and presently quite minor. This commit takes a compromise and handles a first sample block only.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/container-voc.c | 833 ++++++++++++++++++++++++++++++++++++++++++ axfer/container.c | 4 + axfer/container.h | 4 + 4 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 axfer/container-voc.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 35aa226..48d046e 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -26,4 +26,5 @@ axfer_SOURCES = \ container.h \ container.c \ container-riff-wave.c \ - container-au.c + container-au.c \ + container-voc.c diff --git a/axfer/container-voc.c b/axfer/container-voc.c new file mode 100644 index 00000000..92e9c83 --- /dev/null +++ b/axfer/container-voc.c @@ -0,0 +1,833 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-voc.c - a parser/builder for a container of Creative Voice File. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +// Not portable to all of UNIX platforms. +#include <endian.h> + +// References: +// - http://sox.sourceforge.net/ + +#define VOC_MAGIC "Creative Voice File\x1a" +#define VOC_VERSION_1_10 0x010a +#define VOC_VERSION_1_20 0x0114 + +enum block_type { + BLOCK_TYPE_TERMINATOR = 0x00, + BLOCK_TYPE_V110_DATA = 0x01, + BLOCK_TYPE_CONTINUOUS_DATA = 0x02, + BLOCK_TYPE_SILENCE = 0x03, + BLOCK_TYPE_MARKER = 0x04, + BLOCK_TYPE_STRING = 0x05, + BLOCK_TYPE_REPEAT_START = 0x06, + BLOCK_TYPE_REPEAT_END = 0x07, + BLOCK_TYPE_EXTENDED_V110_FORMAT = 0x08, + BLOCK_TYPE_V120_DATA = 0x09, +}; + +enum code_id { + // Version 1.10. + CODE_ID_GENERIC_MBLA_U8 = 0x00, + CODE_ID_CREATIVE_ADPCM_8BIT_TO_4BIT_LE = 0x01, + CODE_ID_CREATIVE_ADPCM_8BIT_TO_3BIT_LE = 0x02, + CODE_ID_CREATIVE_ADPCM_8BIT_TO_2BIT_LE = 0x03, + // Version 1.20. + CODE_ID_GENERIC_MBLA_S16_LE = 0x04, + CODE_ID_CCIT_A_LAW_LE = 0x06, + CODE_ID_CCIT_MU_LAW_LE = 0x07, + CODE_ID_CREATIVE_ADPCM_16BIT_TO_4BIT_LE = 0x2000, +}; + +struct format_map { + unsigned int minimal_version; + enum code_id code_id; + snd_pcm_format_t format; +}; + +static const struct format_map format_maps[] = { + {VOC_VERSION_1_10, CODE_ID_GENERIC_MBLA_U8, SND_PCM_FORMAT_U8}, + {VOC_VERSION_1_20, CODE_ID_GENERIC_MBLA_S16_LE, SND_PCM_FORMAT_S16_LE}, + {VOC_VERSION_1_20, CODE_ID_CCIT_A_LAW_LE, SND_PCM_FORMAT_A_LAW}, + {VOC_VERSION_1_20, CODE_ID_CCIT_MU_LAW_LE, SND_PCM_FORMAT_MU_LAW}, + // The other formats are not supported by ALSA. +}; + +struct container_header { + uint8_t magic[20]; + uint16_t hdr_size; + uint16_t version; + uint16_t version_compr; +}; + +// A format for data blocks except for terminator type. +struct block_header { + uint8_t type; + uint8_t size[3]; + + uint8_t data[0]; +}; + +// Data block for terminator type has an exceptional format. +struct block_terminator { + uint8_t type; +}; + +struct time_const { + unsigned int frames_per_second; + uint16_t code; +}; + +static const struct time_const v110_time_consts[] = { + {5512, 74}, + {8000, 130}, + {11025, 165}, + {16000, 193}, + {22050, 210}, + {32000, 224}, + {44100, 233}, + {48000, 235}, + {64000, 240}, + // Time constant for the upper sampling rate is not identical. +}; + +static const struct time_const ex_v110_time_consts[] = { + {5512, 19092}, + {8000, 33536}, + {11025, 42317}, + {16000, 49536}, + {22050, 53927}, + {32000, 57536}, + {44100, 59732}, + {48000, 60203}, + {64000, 61536}, + {88200, 62634}, + {96000, 62870}, + {176400, 64085}, + {192000, 64203}, + // This support up to 192.0 kHz. The rest is for cases with 2ch. + {352800, 64811}, + {384000, 64870}, +}; + +// v1.10 format: +// - monaural. +// - frames_per_second = 1,000,000 / (256 - time_const) +struct block_v110_data { + uint8_t type; + uint8_t size[3]; // Equals to (2 + the size of frames). + + uint8_t time_const; + uint8_t code_id; + uint8_t frames[0]; // Aligned to little-endian. +}; + +struct block_continuous_data { + uint8_t type; + uint8_t size[3]; // Equals to the size of frames. + + uint8_t frames[0]; // Aligned to little-endian. +}; + +// v1.10 format: +// - monaural. +// - frames_per_second = 1,000,000 / (256 - time_const). +struct block_silence { + uint8_t type; + uint8_t size[3]; // Equals to 3. + + uint16_t frame_count; + uint8_t time_const; +}; + +struct block_marker { + uint8_t type; + uint8_t size[3]; // Equals to 2. + + uint16_t mark; +}; + +struct block_string { + uint8_t type; + uint8_t size[3]; // Equals to the length of string with 0x00. + + uint8_t chars[0]; +}; + +struct block_repeat_start { + uint8_t type; + uint8_t size[3]; // Equals to 2. + + uint16_t count; +}; + +struct block_repeat_end { + uint8_t type; + uint8_t size[3]; // Equals to 0. +}; + +// Extended v1.10 format: +// - manaural/stereo. +// - frames_per_second = +// 256,000,000 / (samples_per_frame * (65536 - time_const)). +// - Appear just before v110_data block. +struct block_extended_v110_format { + uint8_t type; + uint8_t size[3]; // Equals to 4. + + uint16_t time_const; + uint8_t code_id; + uint8_t ch_mode; // 0 is monaural, 1 is stereo. +}; + +// v1.20 format: +// - monaural/stereo. +// - 8/16 bits_per_sample. +// - time_const is not used. +// - code_id is extended. +struct block_v120_format { + uint8_t type; + uint8_t size[3]; // Equals to (12 + ). + + uint32_t frames_per_second; + uint8_t bits_per_sample; + uint8_t samples_per_frame; + uint16_t code_id; + uint8_t reserved[4]; + + uint8_t frames[0]; // Aligned to little-endian. +}; + +// Aligned to little endian order but 24 bits field. +static uint32_t parse_block_data_size(uint8_t fields[3]) +{ + return (fields[2] << 16) | (fields[1] << 8) | fields[0]; +} + +static void build_block_data_size(uint8_t fields[3], unsigned int size) +{ + fields[0] = (size & 0x0000ff); + fields[1] = (size & 0x00ff00) >> 8; + fields[2] = (size & 0xff0000) >> 16; +} + +static int build_time_constant(unsigned int frames_per_second, + unsigned int samples_per_frame, uint16_t *code, + bool extended) +{ + int i; + + // 16 bits are available for this purpose. + if (extended) { + if (samples_per_frame > 2) + return -EINVAL; + frames_per_second *= samples_per_frame; + + for (i = 0; i < ARRAY_SIZE(ex_v110_time_consts); ++i) { + if (ex_v110_time_consts[i].frames_per_second == + frames_per_second) + break; + } + if (i < ARRAY_SIZE(ex_v110_time_consts) || + frames_per_second <= 192000) { + *code = ex_v110_time_consts[i].code; + } else { + *code = 65536 - 256000000 / frames_per_second; + } + } else { + if (samples_per_frame != 1) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(v110_time_consts); ++i) { + if (v110_time_consts[i].frames_per_second == + frames_per_second) + break; + } + // Should be within 8 bit. + if (i < ARRAY_SIZE(v110_time_consts)) + *code = (uint8_t)v110_time_consts[i].code; + else + *code = 256 - 1000000 / frames_per_second; + } + + return 0; +} + +static unsigned int parse_time_constant(uint16_t code, + unsigned int samples_per_frame, + unsigned int *frames_per_second, + bool extended) +{ + int i; + + if (extended) { + if (samples_per_frame > 2) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(ex_v110_time_consts); ++i) { + if (ex_v110_time_consts[i].code == code || + ex_v110_time_consts[i].code - 1 == code) + break; + } + if (i < ARRAY_SIZE(ex_v110_time_consts)) { + *frames_per_second = + ex_v110_time_consts[i].frames_per_second / + samples_per_frame; + } else { + *frames_per_second = 256000000 / samples_per_frame / + (65536 - code); + } + } else { + if (samples_per_frame != 1) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(v110_time_consts); ++i) { + if (v110_time_consts[i].code == code || + v110_time_consts[i].code - 1 == code) + break; + } + if (i < ARRAY_SIZE(v110_time_consts)) { + *frames_per_second = + v110_time_consts[i].frames_per_second; + } else { + *frames_per_second = 1000000 / (256 - code); + } + } + + return 0; +} + +struct parser_state { + unsigned int version; + bool extended; + + unsigned int frames_per_second; + unsigned int samples_per_frame; + unsigned int bytes_per_sample; + enum code_id code_id; + uint32_t byte_count; +}; + +static int parse_container_header(struct parser_state *state, + struct container_header *header) +{ + uint16_t hdr_size; + uint16_t version; + uint16_t version_compr; + + hdr_size = le16toh(header->hdr_size); + version = le16toh(header->version); + version_compr = le16toh(header->version_compr); + + if (memcmp(header->magic, VOC_MAGIC, sizeof(header->magic))) + return -EIO; + + if (hdr_size != sizeof(*header)) + return -EIO; + + if (version_compr != 0x1234 + ~version) + return -EIO; + + if (version != VOC_VERSION_1_10 && version != VOC_VERSION_1_20) + return -EIO; + + state->version = version; + + return 0; +} + +static bool check_code_id(uint8_t code_id, unsigned int version) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (code_id != format_maps[i].code_id) + continue; + if (version >= format_maps[i].minimal_version) + return true; + } + + return false; +} + +static int parse_v120_format_block(struct parser_state *state, + struct block_v120_format *block) +{ + state->frames_per_second = le32toh(block->frames_per_second); + state->bytes_per_sample = block->bits_per_sample / 8; + state->samples_per_frame = block->samples_per_frame; + state->code_id = le16toh(block->code_id); + state->byte_count = parse_block_data_size(block->size) - 12; + + if (!check_code_id(state->code_id, VOC_VERSION_1_20)) + return -EIO; + + return 0; +} + +static int parse_extended_v110_format(struct parser_state *state, + struct block_extended_v110_format *block) +{ + unsigned int time_const; + unsigned int frames_per_second; + int err; + + state->code_id = block->code_id; + if (!check_code_id(state->code_id, VOC_VERSION_1_10)) + return -EIO; + + if (block->ch_mode == 0) + state->samples_per_frame = 1; + else if (block->ch_mode == 1) + state->samples_per_frame = 2; + else + return -EIO; + + time_const = le16toh(block->time_const); + err = parse_time_constant(time_const, state->samples_per_frame, + &frames_per_second, true); + if (err < 0) + return err; + state->frames_per_second = frames_per_second; + + state->extended = true; + + return 0; +} + +static int parse_v110_data(struct parser_state *state, + struct block_v110_data *block) +{ + unsigned int time_const; + unsigned int frames_per_second; + int err; + + if (!state->extended) { + state->code_id = block->code_id; + if (!check_code_id(state->code_id, VOC_VERSION_1_10)) + return -EIO; + + time_const = block->time_const; + err = parse_time_constant(time_const, 1, &frames_per_second, + false); + if (err < 0) + return err; + state->frames_per_second = frames_per_second; + state->samples_per_frame = 1; + } + + state->bytes_per_sample = 1; + state->byte_count = parse_block_data_size(block->size) - 2; + + return 0; +} + +static int detect_container_version(struct container_context *cntr) +{ + struct parser_state *state = cntr->private_data; + struct container_header header = {0}; + int err; + + // 4 bytes were alread read to detect container type. + memcpy(&header.magic, cntr->magic, sizeof(cntr->magic)); + err = container_recursive_read(cntr, + (char *)&header + sizeof(cntr->magic), + sizeof(header) - sizeof(cntr->magic)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + return parse_container_header(state, &header); +} + +static int allocate_for_block_cache(struct container_context *cntr, + struct block_header *header, void **buf) +{ + uint32_t block_size; + char *cache; + int err; + + if (header->type == BLOCK_TYPE_V110_DATA) + block_size = sizeof(struct block_v110_data); + else if (header->type == BLOCK_TYPE_CONTINUOUS_DATA) + block_size = sizeof(struct block_continuous_data); + else if (header->type == BLOCK_TYPE_EXTENDED_V110_FORMAT) + block_size = sizeof(struct block_extended_v110_format); + else if (header->type == BLOCK_TYPE_V120_DATA) + block_size = sizeof(struct block_v120_format); + else + block_size = parse_block_data_size(header->size); + + cache = malloc(block_size); + if (cache == NULL) + return -ENOMEM; + memset(cache, 0, block_size); + + memcpy(cache, header, sizeof(*header)); + err = container_recursive_read(cntr, cache + sizeof(*header), + block_size - sizeof(*header)); + if (err < 0) { + free(cache); + return err; + } + if (cntr->eof) { + free(cache); + return 0; + } + + *buf = cache; + + return 0; +} + +static int cache_data_block(struct container_context *cntr, + struct block_header *header, void **buf) +{ + int err; + + // Check type of this block. + err = container_recursive_read(cntr, &header->type, + sizeof(header->type)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + if (header->type > BLOCK_TYPE_V120_DATA) + return -EIO; + if (header->type == BLOCK_TYPE_TERMINATOR) + return 0; + + // Check size of this block. If the block includes a batch of data, + err = container_recursive_read(cntr, &header->size, + sizeof(header->size)); + if (err < 0) + return err; + if (cntr->eof) + return 0; + + return allocate_for_block_cache(cntr, header, buf); +} + +static int detect_format_block(struct container_context *cntr) +{ + struct parser_state *state = cntr->private_data; + struct block_header header; + void *buf = NULL; + int err; + +again: + err = cache_data_block(cntr, &header, &buf); + if (err < 0) + return err; + + if (header.type == BLOCK_TYPE_EXTENDED_V110_FORMAT) { + err = parse_extended_v110_format(state, buf); + } else if (header.type == BLOCK_TYPE_V120_DATA) { + err = parse_v120_format_block(state, buf); + } else if (header.type == BLOCK_TYPE_V110_DATA) { + err = parse_v110_data(state, buf); + } else { + free(buf); + goto again; + } + + free(buf); + + if (err < 0) + return err; + + // Expect to detect block_v110_data. + if (header.type == BLOCK_TYPE_EXTENDED_V110_FORMAT) + goto again; + + return 0; +} + +static int voc_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct parser_state *state = cntr->private_data; + int i; + int err; + + err = detect_container_version(cntr); + if (err < 0) + return err; + + err = detect_format_block(cntr); + if (err < 0) + return err; + + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].code_id == state->code_id) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + + *format = format_maps[i].format; + *samples_per_frame = state->samples_per_frame; + *frames_per_second = state->frames_per_second; + + // This program handles PCM frames in this data block only. + *byte_count = state->byte_count; + + return 0; +} + +struct builder_state { + unsigned int version; + bool extended; + enum code_id code_id; + + unsigned int samples_per_frame; + unsigned int bytes_per_sample; +}; + +static int write_container_header(struct container_context *cntr, + struct container_header *header) +{ + struct builder_state *state = cntr->private_data; + + // Process container header. + memcpy(header->magic, VOC_MAGIC, sizeof(header->magic)); + header->hdr_size = htole16(sizeof(*header)); + header->version = htole16(state->version); + header->version_compr = htole16(0x1234 + ~state->version); + + return container_recursive_write(cntr, header, sizeof(*header)); +} + +static int write_v120_format_block(struct container_context *cntr, + struct block_v120_format *block, + unsigned int frames_per_second, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + + block->type = BLOCK_TYPE_V120_DATA; + build_block_data_size(block->size, 12 + byte_count); + + block->frames_per_second = htole32(frames_per_second); + block->bits_per_sample = htole16(state->bytes_per_sample * 8); + block->samples_per_frame = htole16(state->samples_per_frame); + block->code_id = htole16(state->code_id); + + return container_recursive_write(cntr, block, sizeof(*block)); +} + +static int write_extended_v110_format_block(struct container_context *cntr, + unsigned int frames_per_second, + struct block_extended_v110_format *block) +{ + struct builder_state *state = cntr->private_data; + uint16_t time_const; + int err; + + block->type = BLOCK_TYPE_EXTENDED_V110_FORMAT; + build_block_data_size(block->size, 4); + + // 16 bits are available for this purpose. + err = build_time_constant(frames_per_second, state->samples_per_frame, + &time_const, true); + if (err < 0) + return err; + block->time_const = htole16(time_const); + block->code_id = htole16(state->code_id); + + if (state->samples_per_frame == 1) + block->ch_mode = 0; + else + block->ch_mode = 1; + + return container_recursive_write(cntr, block, sizeof(*block)); +} + +static int write_v110_format_block(struct container_context *cntr, + struct block_v110_data *block, + unsigned int frames_per_second, + uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + uint16_t time_const; + int err; + + block->type = BLOCK_TYPE_V110_DATA; + build_block_data_size(block->size, 2 + byte_count); + + // These fields were obsoleted by extension. + err = build_time_constant(frames_per_second, 1, &time_const, false); + if (err < 0) + return err; + block->time_const = (uint8_t)time_const; + block->code_id = state->code_id; + return container_recursive_write(cntr, block, sizeof(*block)); +} + +static int write_data_blocks(struct container_context *cntr, + unsigned int frames_per_second, + uint64_t byte_count) +{ + union { + struct container_header header; + struct block_v110_data v110_data; + struct block_extended_v110_format extended_v110_format; + struct block_v120_format v120_format; + } buf = {0}; + struct builder_state *state = cntr->private_data; + int err; + + err = write_container_header(cntr, &buf.header); + if (err < 0) + return err; + + if (state->version == VOC_VERSION_1_20) { + err = write_v120_format_block(cntr, &buf.v120_format, + frames_per_second, byte_count); + } else { + if (state->extended) { + err = write_extended_v110_format_block(cntr, + frames_per_second, + &buf.extended_v110_format); + if (err < 0) + return err; + } + err = write_v110_format_block(cntr, &buf.v110_data, + frames_per_second, byte_count); + } + + return err; +} + +static int voc_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct builder_state *state = cntr->private_data; + int i; + + // Validate parameters. + for (i = 0; i < ARRAY_SIZE(format_maps); ++i) { + if (format_maps[i].format == *format) + break; + } + if (i == ARRAY_SIZE(format_maps)) + return -EINVAL; + state->code_id = format_maps[i].code_id; + + // Decide container version. + if (*samples_per_frame > 2) + state->version = VOC_VERSION_1_20; + else + state->version = format_maps[i].minimal_version; + if (state->version == VOC_VERSION_1_10) { + if (*samples_per_frame == 2) { + for (i = 0; + i < ARRAY_SIZE(ex_v110_time_consts); ++i) { + if (ex_v110_time_consts[i].frames_per_second == + *frames_per_second) + break; + } + if (i == ARRAY_SIZE(ex_v110_time_consts)) + state->version = VOC_VERSION_1_20; + else + state->extended = true; + } else { + for (i = 0; i < ARRAY_SIZE(v110_time_consts); ++i) { + if (v110_time_consts[i].frames_per_second == + *frames_per_second) + break; + } + if (i == ARRAY_SIZE(v110_time_consts)) + state->version = VOC_VERSION_1_20; + } + } + + state->bytes_per_sample = snd_pcm_format_physical_width(*format) / 8; + state->samples_per_frame = *samples_per_frame; + + return write_data_blocks(cntr, *frames_per_second, *byte_count); +} + +static int write_block_terminator(struct container_context *cntr) +{ + struct block_terminator block = {0}; + + block.type = BLOCK_TYPE_TERMINATOR; + return container_recursive_write(cntr, &block, sizeof(block)); +} + +static int write_data_size(struct container_context *cntr, uint64_t byte_count) +{ + struct builder_state *state = cntr->private_data; + off64_t offset; + uint8_t size_field[3]; + int err; + + offset = sizeof(struct container_header) + sizeof(uint8_t); + if (state->version == VOC_VERSION_1_10 && state->extended) + offset += sizeof(struct block_extended_v110_format); + err = container_seek_offset(cntr, offset); + if (err < 0) + return err; + + if (state->version == VOC_VERSION_1_10) + offset = 2; + else + offset = 12; + + if (byte_count > cntr->max_size - offset) + byte_count = cntr->max_size; + else + byte_count += offset; + build_block_data_size(size_field, byte_count); + + return container_recursive_write(cntr, &size_field, sizeof(size_field)); +} + +static int voc_builder_post_process(struct container_context *cntr, + uint64_t handled_byte_count) +{ + int err; + + err = write_block_terminator(cntr); + if (err < 0) + return err; + + return write_data_size(cntr, handled_byte_count); +} + +const struct container_parser container_parser_voc = { + .format = CONTAINER_FORMAT_VOC, + .magic = VOC_MAGIC, + .max_size = 0xffffff - // = UINT24_MAX. + sizeof(struct block_terminator), + .ops = { + .pre_process = voc_parser_pre_process, + }, + .private_size = sizeof(struct parser_state), +}; + +const struct container_builder container_builder_voc = { + .format = CONTAINER_FORMAT_VOC, + .max_size = 0xffffff - // = UINT24_MAX. + sizeof(struct block_terminator), + .ops = { + .pre_process = voc_builder_pre_process, + .post_process = voc_builder_post_process, + }, + .private_size = sizeof(struct builder_state), +}; diff --git a/axfer/container.c b/axfer/container.c index 0e25605..94afb2b 100644 --- a/axfer/container.c +++ b/axfer/container.c @@ -22,11 +22,13 @@ static const char *const cntr_type_labels[] = { static const char *const cntr_format_labels[] = { [CONTAINER_FORMAT_RIFF_WAVE] = "riff/wave", [CONTAINER_FORMAT_AU] = "au", + [CONTAINER_FORMAT_VOC] = "voc", };
static const char *const suffixes[] = { [CONTAINER_FORMAT_RIFF_WAVE] = ".wav", [CONTAINER_FORMAT_AU] = ".au", + [CONTAINER_FORMAT_VOC] = ".voc", };
@@ -145,6 +147,7 @@ int container_parser_init(struct container_context *cntr, const struct container_parser *parsers[] = { [CONTAINER_FORMAT_RIFF_WAVE] = &container_parser_riff_wave, [CONTAINER_FORMAT_AU] = &container_parser_au, + [CONTAINER_FORMAT_VOC] = &container_parser_voc, }; const struct container_parser *parser; unsigned int size; @@ -220,6 +223,7 @@ int container_builder_init(struct container_context *cntr, const struct container_builder *builders[] = { [CONTAINER_FORMAT_RIFF_WAVE] = &container_builder_riff_wave, [CONTAINER_FORMAT_AU] = &container_builder_au, + [CONTAINER_FORMAT_VOC] = &container_builder_voc, }; const struct container_builder *builder; int err; diff --git a/axfer/container.h b/axfer/container.h index 19ef672..71e188c 100644 --- a/axfer/container.h +++ b/axfer/container.h @@ -27,6 +27,7 @@ enum container_type { enum container_format { CONTAINER_FORMAT_RIFF_WAVE = 0, CONTAINER_FORMAT_AU, + CONTAINER_FORMAT_VOC, CONTAINER_FORMAT_COUNT, };
@@ -115,4 +116,7 @@ extern const struct container_builder container_builder_riff_wave; extern const struct container_parser container_parser_au; extern const struct container_builder container_builder_au;
+extern const struct container_parser container_parser_voc; +extern const struct container_builder container_builder_voc; + #endif
This commit adds support for raw data without any headers/chunks/blocks. A parser of container cannot recognize format of sample without supplemental information.
Additionally, it includes no magic bytes. A parser of container should process first several bytes as a part of PCM frames, instead of magic bytes.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/container-raw.c | 66 +++++++++++++++++++++++++++++++++++++++++++ axfer/container.c | 37 ++++++++++++++++++++---- axfer/container.h | 4 +++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 axfer/container-raw.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 48d046e..f1ec1d1 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -27,4 +27,5 @@ axfer_SOURCES = \ container.c \ container-riff-wave.c \ container-au.c \ - container-voc.c + container-voc.c \ + container-raw.c diff --git a/axfer/container-raw.c b/axfer/container-raw.c new file mode 100644 index 00000000..9b0022e --- /dev/null +++ b/axfer/container-raw.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-raw.c - a parser/builder for a container with raw data frame. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "container.h" +#include "misc.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +static int raw_builder_pre_process(struct container_context *cntr, + snd_pcm_format_t *sample_format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + *byte_count = UINT64_MAX; + + return 0; +} + +static int raw_parser_pre_process(struct container_context *cntr, + snd_pcm_format_t *sample_format, + unsigned int *samples_per_frame, + unsigned int *frames_per_second, + uint64_t *byte_count) +{ + struct stat buf = {0}; + int err; + + if (cntr->stdio) { + *byte_count = UINT64_MAX; + return 0; + } + + err = fstat(cntr->fd, &buf); + if (err < 0) + return err; + + *byte_count = buf.st_size; + if (*byte_count == 0) + *byte_count = UINT64_MAX; + + return 0; +} + +const struct container_parser container_parser_raw = { + .format = CONTAINER_FORMAT_RAW, + .max_size = UINT64_MAX, + .ops = { + .pre_process = raw_parser_pre_process, + }, +}; + +const struct container_builder container_builder_raw = { + .format = CONTAINER_FORMAT_RAW, + .max_size = UINT64_MAX, + .ops = { + .pre_process = raw_builder_pre_process, + }, +}; diff --git a/axfer/container.c b/axfer/container.c index 94afb2b..690fe5b 100644 --- a/axfer/container.c +++ b/axfer/container.c @@ -23,15 +23,16 @@ static const char *const cntr_format_labels[] = { [CONTAINER_FORMAT_RIFF_WAVE] = "riff/wave", [CONTAINER_FORMAT_AU] = "au", [CONTAINER_FORMAT_VOC] = "voc", + [CONTAINER_FORMAT_RAW] = "raw", };
static const char *const suffixes[] = { [CONTAINER_FORMAT_RIFF_WAVE] = ".wav", [CONTAINER_FORMAT_AU] = ".au", [CONTAINER_FORMAT_VOC] = ".voc", + [CONTAINER_FORMAT_RAW] = "", };
- const char *const container_suffix_from_format(enum container_format format) { return suffixes[format]; @@ -108,7 +109,7 @@ enum container_format container_format_from_path(const char *path) }
// Unsupported. - return CONTAINER_FORMAT_COUNT; + return CONTAINER_FORMAT_RAW; }
int container_seek_offset(struct container_context *cntr, off64_t offset) @@ -196,7 +197,7 @@ int container_parser_init(struct container_context *cntr,
// Unless detected, use raw container. if (i == ARRAY_SIZE(parsers)) - return -EINVAL; + parser = &container_parser_raw;
// Allocate private data for the parser. if (parser->private_size > 0) { @@ -224,6 +225,7 @@ int container_builder_init(struct container_context *cntr, [CONTAINER_FORMAT_RIFF_WAVE] = &container_builder_riff_wave, [CONTAINER_FORMAT_AU] = &container_builder_au, [CONTAINER_FORMAT_VOC] = &container_builder_voc, + [CONTAINER_FORMAT_RAW] = &container_builder_raw, }; const struct container_builder *builder; int err; @@ -302,6 +304,16 @@ int container_context_pre_process(struct container_context *cntr, return 0; }
+ if (cntr->format == CONTAINER_FORMAT_RAW) { + if (*format == SND_PCM_FORMAT_UNKNOWN || + *samples_per_frame == 0 || *frames_per_second == 0) { + fprintf(stderr, + "Any file format is not detected. Need to " + "indicate all of sample format, channels and " + "rate explicitly.\n"); + return -EINVAL; + } + } assert(*format >= SND_PCM_FORMAT_S8); assert(*format <= SND_PCM_FORMAT_LAST); assert(*samples_per_frame > 0); @@ -348,6 +360,7 @@ int container_context_process_frames(struct container_context *cntr, char *buf = frame_buffer; unsigned int bytes_per_frame; unsigned int byte_count; + unsigned int target_byte_count; int err;
assert(cntr); @@ -356,7 +369,19 @@ int container_context_process_frames(struct container_context *cntr, assert(frame_count);
bytes_per_frame = cntr->bytes_per_sample * cntr->samples_per_frame; - byte_count = *frame_count * bytes_per_frame; + target_byte_count = *frame_count * bytes_per_frame; + + // A parser of cotainers already read first 4 bytes to detect format + // of container, however they includes PCM frames when any format was + // undetected. Surely to write out them. + byte_count = target_byte_count; + if (cntr->format == CONTAINER_FORMAT_RAW && + cntr->type == CONTAINER_TYPE_PARSER && !cntr->magic_handled) { + memcpy(buf, cntr->magic, sizeof(cntr->magic)); + buf += sizeof(cntr->magic); + byte_count -= sizeof(cntr->magic); + cntr->magic_handled = true; + }
// Each container has limitation for its volume for sample data. if (cntr->handled_byte_count > cntr->max_size - byte_count) @@ -370,11 +395,11 @@ int container_context_process_frames(struct container_context *cntr, return err; }
- cntr->handled_byte_count += byte_count; + cntr->handled_byte_count += target_byte_count; if (cntr->handled_byte_count == cntr->max_size) cntr->eof = true;
- *frame_count = byte_count / bytes_per_frame; + *frame_count = target_byte_count / bytes_per_frame;
return 0; } diff --git a/axfer/container.h b/axfer/container.h index 71e188c..2c0c4cf 100644 --- a/axfer/container.h +++ b/axfer/container.h @@ -28,6 +28,7 @@ enum container_format { CONTAINER_FORMAT_RIFF_WAVE = 0, CONTAINER_FORMAT_AU, CONTAINER_FORMAT_VOC, + CONTAINER_FORMAT_RAW, CONTAINER_FORMAT_COUNT, };
@@ -119,4 +120,7 @@ extern const struct container_builder container_builder_au; extern const struct container_parser container_parser_voc; extern const struct container_builder container_builder_voc;
+const struct container_parser container_parser_raw; +const struct container_builder container_builder_raw; + #endif
In former commits, container module gets supports of parser/builder for several types of file format. This commit adds a unit test for them. This includes positive test cases only. The test cases actually generate I/O to file systems for many test cases. It takes a long time to finish.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/test/Makefile.am | 15 ++ axfer/test/container-test.c | 299 ++++++++++++++++++++++++++++++++++++ axfer/test/generator.c | 260 +++++++++++++++++++++++++++++++ axfer/test/generator.h | 47 ++++++ configure.ac | 3 +- 6 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 axfer/test/Makefile.am create mode 100644 axfer/test/container-test.c create mode 100644 axfer/test/generator.c create mode 100644 axfer/test/generator.h
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index f1ec1d1..55fcf71 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -6,7 +6,8 @@ AM_CPPFLAGS = \ -I$(top_srcdir)/include
# Unit tests. -SUBDIRS = +SUBDIRS = \ + test
LIBRT = @LIBRT@ LDADD = \ diff --git a/axfer/test/Makefile.am b/axfer/test/Makefile.am new file mode 100644 index 00000000..66b30ef --- /dev/null +++ b/axfer/test/Makefile.am @@ -0,0 +1,15 @@ +TESTS = \ + container-test + +check_PROGRAMS = \ + container-test + +container_test_SOURCES = \ + ../container.h \ + ../container.c \ + ../container-riff-wave.c \ + ../container-au.c \ + ../container-voc.c \ + ../container-raw.c \ + generator.c \ + container-test.c diff --git a/axfer/test/container-test.c b/axfer/test/container-test.c new file mode 100644 index 00000000..0e2e6e9 --- /dev/null +++ b/axfer/test/container-test.c @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// container-io.c - a unit test for parser/builder of supported containers. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "../container.h" +#include "../misc.h" + +#include "generator.h" + +#include <stdlib.h> +#include <unistd.h> +#include <stdbool.h> + +#include <assert.h> + +struct container_trial { + enum container_format format; + + struct container_context cntr; + bool verbose; +}; + +static void test_builder(struct container_context *cntr, + enum container_format format, const char *const name, + snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + void *frame_buffer, unsigned int frame_count, + bool verbose) +{ + snd_pcm_format_t sample; + unsigned int channels; + unsigned int rate; + uint64_t max_frame_count; + unsigned int handled_frame_count; + uint64_t total_frame_count; + int err; + + err = container_builder_init(cntr, name, format, verbose); + assert(err == 0); + + sample = sample_format; + channels = samples_per_frame; + rate = frames_per_second; + max_frame_count = 0; + err = container_context_pre_process(cntr, &sample, &channels, &rate, + &max_frame_count); + assert(err == 0); + assert(sample == sample_format); + assert(channels == samples_per_frame); + assert(rate == frames_per_second); + assert(max_frame_count > 0); + + handled_frame_count = frame_count; + err = container_context_process_frames(cntr, frame_buffer, + &handled_frame_count); + assert(err == 0); + assert(handled_frame_count > 0); + assert(handled_frame_count <= frame_count); + + total_frame_count = 0; + err = container_context_post_process(cntr, &total_frame_count); + assert(err == 0); + assert(total_frame_count == frame_count); + + container_context_destroy(cntr); +} + +static void test_parser(struct container_context *cntr, + enum container_format format, const char *const name, + snd_pcm_access_t access, snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + void *frame_buffer, unsigned int frame_count, + bool verbose) +{ + snd_pcm_format_t sample; + unsigned int channels; + unsigned int rate; + uint64_t total_frame_count; + unsigned int handled_frame_count; + int err; + + err = container_parser_init(cntr, name, verbose); + assert(err == 0); + + sample = sample_format; + channels = samples_per_frame; + rate = frames_per_second; + total_frame_count = 0; + err = container_context_pre_process(cntr, &sample, &channels, &rate, + &total_frame_count); + assert(err == 0); + assert(sample == sample_format); + assert(channels == samples_per_frame); + assert(rate == frames_per_second); + assert(total_frame_count == frame_count); + + handled_frame_count = total_frame_count; + err = container_context_process_frames(cntr, frame_buffer, + &handled_frame_count); + assert(err == 0); + assert(handled_frame_count == frame_count); + + total_frame_count = 0; + err = container_context_post_process(cntr, &total_frame_count); + assert(err == 0); + assert(total_frame_count == handled_frame_count); + + container_context_destroy(cntr); +} + +static int callback(struct test_generator *gen, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, void *frame_buffer, + unsigned int frame_count) +{ + static const unsigned int entries[] = { + [0] = 44100, + [1] = 48000, + [2] = 88200, + [3] = 96000, + [4] = 176400, + [5] = 192000, + }; + struct container_trial *trial = gen->private_data; + unsigned int frames_per_second; + const char *const name = "hoge"; + unsigned int size; + void *buf; + int i; + int err = 0; + + size = frame_count * samples_per_frame * + snd_pcm_format_physical_width(sample_format) / 8; + buf = malloc(size); + if (buf == NULL) + return -ENOMEM; + + // Remove a result of a previous trial. + unlink(name); + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + frames_per_second = entries[i]; + + test_builder(&trial->cntr, trial->format, name, access, + sample_format, samples_per_frame, + frames_per_second, frame_buffer, frame_count, + trial->verbose); + + test_parser(&trial->cntr, trial->format, name, access, + sample_format, samples_per_frame, frames_per_second, + buf, frame_count, trial->verbose); + + err = memcmp(buf, frame_buffer, size); + assert(err == 0); + + unlink(name); + } + + free(buf); + + return 0; +} + +int main(int argc, const char *argv[]) +{ + static const uint64_t sample_format_masks[] = { + [CONTAINER_FORMAT_RIFF_WAVE] = + (1ul << SND_PCM_FORMAT_U8) | + (1ul << SND_PCM_FORMAT_S16_LE) | + (1ul << SND_PCM_FORMAT_S16_BE) | + (1ul << SND_PCM_FORMAT_S24_LE) | + (1ul << SND_PCM_FORMAT_S24_BE) | + (1ul << SND_PCM_FORMAT_S32_LE) | + (1ul << SND_PCM_FORMAT_S32_BE) | + (1ul << SND_PCM_FORMAT_FLOAT_LE) | + (1ul << SND_PCM_FORMAT_FLOAT_BE) | + (1ul << SND_PCM_FORMAT_FLOAT64_LE) | + (1ul << SND_PCM_FORMAT_FLOAT64_BE) | + (1ul << SND_PCM_FORMAT_MU_LAW) | + (1ul << SND_PCM_FORMAT_A_LAW) | + (1ul << SND_PCM_FORMAT_S24_3LE) | + (1ul << SND_PCM_FORMAT_S24_3BE) | + (1ul << SND_PCM_FORMAT_S20_3LE) | + (1ul << SND_PCM_FORMAT_S20_3BE) | + (1ul << SND_PCM_FORMAT_S18_3LE) | + (1ul << SND_PCM_FORMAT_S18_3BE), + [CONTAINER_FORMAT_AU] = + (1ul << SND_PCM_FORMAT_S8) | + (1ul << SND_PCM_FORMAT_S16_BE) | + (1ul << SND_PCM_FORMAT_S32_BE) | + (1ul << SND_PCM_FORMAT_FLOAT_BE) | + (1ul << SND_PCM_FORMAT_FLOAT64_BE) | + (1ul << SND_PCM_FORMAT_MU_LAW) | + (1ul << SND_PCM_FORMAT_A_LAW), + [CONTAINER_FORMAT_VOC] = + (1ul << SND_PCM_FORMAT_U8) | + (1ul << SND_PCM_FORMAT_S16_LE) | + (1ul << SND_PCM_FORMAT_MU_LAW) | + (1ul << SND_PCM_FORMAT_A_LAW), + [CONTAINER_FORMAT_RAW] = + (1ul << SND_PCM_FORMAT_S8) | + (1ul << SND_PCM_FORMAT_U8) | + (1ul << SND_PCM_FORMAT_S16_LE) | + (1ul << SND_PCM_FORMAT_S16_BE) | + (1ul << SND_PCM_FORMAT_U16_LE) | + (1ul << SND_PCM_FORMAT_U16_BE) | + (1ul << SND_PCM_FORMAT_S24_LE) | + (1ul << SND_PCM_FORMAT_S24_BE) | + (1ul << SND_PCM_FORMAT_U24_LE) | + (1ul << SND_PCM_FORMAT_U24_BE) | + (1ul << SND_PCM_FORMAT_S32_LE) | + (1ul << SND_PCM_FORMAT_S32_BE) | + (1ul << SND_PCM_FORMAT_U32_LE) | + (1ul << SND_PCM_FORMAT_U32_BE) | + (1ul << SND_PCM_FORMAT_FLOAT_LE) | + (1ul << SND_PCM_FORMAT_FLOAT_BE) | + (1ul << SND_PCM_FORMAT_FLOAT64_LE) | + (1ul << SND_PCM_FORMAT_FLOAT64_BE) | + (1ul << SND_PCM_FORMAT_IEC958_SUBFRAME_LE) | + (1ul << SND_PCM_FORMAT_IEC958_SUBFRAME_BE) | + (1ul << SND_PCM_FORMAT_MU_LAW) | + (1ul << SND_PCM_FORMAT_A_LAW) | + (1ul << SND_PCM_FORMAT_S24_3LE) | + (1ul << SND_PCM_FORMAT_S24_3BE) | + (1ul << SND_PCM_FORMAT_U24_3LE) | + (1ul << SND_PCM_FORMAT_U24_3BE) | + (1ul << SND_PCM_FORMAT_S20_3LE) | + (1ul << SND_PCM_FORMAT_S20_3BE) | + (1ul << SND_PCM_FORMAT_U20_3LE) | + (1ul << SND_PCM_FORMAT_U20_3BE) | + (1ul << SND_PCM_FORMAT_S18_3LE) | + (1ul << SND_PCM_FORMAT_S18_3BE) | + (1ul << SND_PCM_FORMAT_U18_3LE) | + (1ul << SND_PCM_FORMAT_U18_3BE) | + (1ul << SND_PCM_FORMAT_DSD_U8) | + (1ul << SND_PCM_FORMAT_DSD_U16_LE) | + (1ul << SND_PCM_FORMAT_DSD_U32_LE) | + (1ul << SND_PCM_FORMAT_DSD_U16_BE) | + (1ul << SND_PCM_FORMAT_DSD_U32_BE), + }; + static const uint64_t access_mask = + (1ul << SND_PCM_ACCESS_MMAP_INTERLEAVED) | + (1ul << SND_PCM_ACCESS_RW_INTERLEAVED); + struct test_generator gen = {0}; + struct container_trial *trial; + int i; + int begin; + int end; + bool verbose; + int err; + + if (argc > 1) { + char *term; + begin = strtol(argv[1], &term, 10); + if (errno || *term != '\0') + return EXIT_FAILURE; + if (begin < CONTAINER_FORMAT_RIFF_WAVE && + begin > CONTAINER_FORMAT_RAW) + return -EXIT_FAILURE; + end = begin + 1; + verbose = true; + } else { + begin = CONTAINER_FORMAT_RIFF_WAVE; + end = CONTAINER_FORMAT_RAW + 1; + verbose = false; + } + + for (i = begin; i < end; ++i) { + err = generator_context_init(&gen, access_mask, + sample_format_masks[i], + 1, 128, 23, 4500, 1024, + sizeof(struct container_trial)); + if (err >= 0) { + trial = gen.private_data; + trial->format = i; + trial->verbose = verbose; + err = generator_context_run(&gen, callback); + } + + generator_context_destroy(&gen); + + if (err < 0) + break; + } + + if (err < 0) { + printf("%s\n", strerror(-err)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/axfer/test/generator.c b/axfer/test/generator.c new file mode 100644 index 00000000..cdea2c9 --- /dev/null +++ b/axfer/test/generator.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// allocator.h - a header of a generator for test with buffers of PCM frames. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "generator.h" + +#include <stdlib.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include <unistd.h> + +int generator_context_init(struct test_generator *gen, + uint64_t access_mask, uint64_t sample_format_mask, + unsigned int min_samples_per_frame, + unsigned int max_samples_per_frame, + unsigned int min_frame_count, + unsigned int max_frame_count, + unsigned int step_frame_count, + unsigned int private_size) +{ + gen->fd = open("/dev/urandom", O_RDONLY); + if (gen->fd < 0) + return -errno; + + gen->private_data = malloc(private_size); + if (gen->private_data == NULL) + return -ENOMEM; + memset(gen->private_data, 0, private_size); + + gen->access_mask = access_mask; + gen->sample_format_mask = sample_format_mask; + gen->min_samples_per_frame = min_samples_per_frame; + gen->max_samples_per_frame = max_samples_per_frame; + gen->min_frame_count = min_frame_count; + gen->max_frame_count = max_frame_count; + gen->step_frame_count = step_frame_count; + + return 0; +} + +static void *allocate_buf(snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frame_count) +{ + unsigned int bytes_per_sample; + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + + return calloc(samples_per_frame * frame_count, bytes_per_sample); +} + +static void *allocate_vector(snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frame_count) +{ + unsigned int bytes_per_sample; + char **bufs; + int i; + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + + bufs = calloc(samples_per_frame, sizeof(char *)); + if (bufs == NULL) + return NULL; + + for (i = 0; i < samples_per_frame; ++i) { + bufs[i] = calloc(frame_count, bytes_per_sample); + if (bufs[i] == NULL) { + for (; i >= 0; --i) + free(bufs[i]); + free(bufs); + return NULL; + } + } + + return bufs; +} + +static int fill_buf(int fd, void *frame_buffer, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, unsigned int frame_count) +{ + unsigned int size; + int len; + + size = snd_pcm_format_physical_width(sample_format) / 8 * + samples_per_frame * frame_count; + while (size > 0) { + len = read(fd, frame_buffer, size); + if (len < 0) + return len; + size -= len; + } + + return 0; +} + +static int fill_vector(int fd, void *frame_buffer, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, unsigned int frame_count) +{ + char **bufs = frame_buffer; + unsigned int size; + int len; + int i; + + for (i = 0; i < samples_per_frame; ++i) { + size = frame_count * + snd_pcm_format_physical_width(sample_format) / 8; + + while (size > 0) { + len = read(fd, bufs[i], size); + if (len < 0) + return len; + size -= len; + } + } + + return 0; +} + +static void deallocate_buf(void *frame_buffer, unsigned int samples_per_frame) +{ + free(frame_buffer); +} + +static void deallocate_vector(void *frame_buffer, + unsigned int samples_per_frame) +{ + char **bufs = frame_buffer; + int i; + + for (i = 0; i < samples_per_frame; ++i) + free(bufs[i]); + + free(bufs); +} + +static int test_frame_count(struct test_generator *gen, + snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame) +{ + void *(*allocator)(snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frame_count); + int (*fill)(int fd, void *frame_buffer, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, unsigned int frame_count); + void (*deallocator)(void *frame_buffer, unsigned int samples_per_frame); + void *frame_buffer; + int i; + int err = 0; + + if (access != SND_PCM_ACCESS_RW_NONINTERLEAVED) { + allocator = allocate_buf; + fill = fill_buf; + deallocator = deallocate_buf; + } else { + allocator = allocate_vector; + fill = fill_vector; + deallocator = deallocate_vector; + } + + frame_buffer = allocator(access, sample_format, samples_per_frame, + gen->max_frame_count); + if (frame_buffer == NULL) + return -ENOMEM; + + err = fill(gen->fd, frame_buffer, access, sample_format, + samples_per_frame, gen->max_frame_count); + if (err < 0) + goto end; + + + for (i = gen->min_frame_count; + i <= gen->max_frame_count; i += gen->step_frame_count) { + err = gen->cb(gen, access ,sample_format, samples_per_frame, + frame_buffer, i); + if (err < 0) + break; + } +end: + deallocator(frame_buffer, samples_per_frame); + + return err; +} + +static int test_samples_per_frame(struct test_generator *gen, + snd_pcm_access_t access, + snd_pcm_format_t sample_format) +{ + int i; + int err = 0; + + for (i = gen->min_samples_per_frame; + i <= gen->max_samples_per_frame; ++i) { + err = test_frame_count(gen, access, sample_format, i); + if (err < 0) + break; + } + + return err; +} + +static int test_sample_format(struct test_generator *gen, + snd_pcm_access_t access) +{ + int i; + int err = 0; + + for (i = 0; i <= SND_PCM_FORMAT_LAST; ++i) { + if (!((1ul << i) & gen->sample_format_mask)) + continue; + + err = test_samples_per_frame(gen, access, i); + if (err < 0) + break; + } + + return err; +} + +static int test_access(struct test_generator *gen) +{ + int i; + int err = 0; + + for (i = 0; i <= SND_PCM_ACCESS_LAST; ++i) { + if (!((1ul << i) & gen->access_mask)) + continue; + + err = test_sample_format(gen, i); + if (err < 0) + break; + } + return err; +} + +int generator_context_run(struct test_generator *gen, generator_cb_t cb) +{ + gen->cb = cb; + return test_access(gen); +} + +void generator_context_destroy(struct test_generator *gen) +{ + free(gen->private_data); + close(gen->fd); +} diff --git a/axfer/test/generator.h b/axfer/test/generator.h new file mode 100644 index 00000000..c35ed98 --- /dev/null +++ b/axfer/test/generator.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// generator.c - a generator for test with buffers of PCM frames. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_TEST_GENERATOR__H_ +#define __ALSA_UTILS_AXFER_TEST_GENERATOR__H_ + +#include <stdint.h> +#include <alsa/asoundlib.h> + +struct test_generator; +typedef int (*generator_cb_t)(struct test_generator *gen, + snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + void *frame_buffer, unsigned int frame_count); + +struct test_generator { + int fd; + uint64_t access_mask; + uint64_t sample_format_mask; + unsigned int min_samples_per_frame; + unsigned int max_samples_per_frame; + unsigned int min_frame_count; + unsigned int max_frame_count; + unsigned int step_frame_count; + + generator_cb_t cb; + void *private_data; +}; + +int generator_context_init(struct test_generator *gen, + uint64_t access_mask, uint64_t sample_format_mask, + unsigned int min_samples_per_frame, + unsigned int max_samples_per_frame, + unsigned int min_frame_count, + unsigned int max_frame_count, + unsigned int step_frame_count, + unsigned int private_size); +int generator_context_run(struct test_generator *gen, generator_cb_t cb); +void generator_context_destroy(struct test_generator *gen); + +#endif diff --git a/configure.ac b/configure.ac index 404fa16..1c64617 100644 --- a/configure.ac +++ b/configure.ac @@ -430,4 +430,5 @@ AC_OUTPUT(Makefile alsactl/Makefile alsactl/init/Makefile \ utils/alsa-utils.spec seq/Makefile seq/aconnect/Makefile \ seq/aplaymidi/Makefile seq/aseqdump/Makefile seq/aseqnet/Makefile \ speaker-test/Makefile speaker-test/samples/Makefile \ - alsaloop/Makefile alsa-info/Makefile axfer/Makefile) + alsaloop/Makefile alsa-info/Makefile \ + axfer/Makefile axfer/test/Makefile)
In current aplay, several files can be handled as source of data frames for playback, or destination of captured data frames by an option '--separate-channels' (-I).
On the other hand, in ALSA PCM kernel/user interface, several types of buffer are used to communicate between application/hardware; - mapped page frame for data frames with interleaved alignment - mapped page frame for data frames with non-interleaved alignment - buffer in user space for data frames with interleaved alignment - a list of buffer in user space for data frames with non-interleaved alignment
This commit adds a common interface, named as 'mapper' to convert frame alignment between these two sides. This interface includes two types; 'muxer' and 'demuxer'. The 'muxer' is for playback direction, to construct playback buffer with PCM frames from several files. The 'demuxer' is for capture direction, to split PCM frames from capture buffer to each of file. Unlike multimedia containers such as MPEG 2/4 Systems, the 'muxer' and 'demuxer' are for playback/capture buffer, not for file contents.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 7 ++- axfer/mapper.c | 125 ++++++++++++++++++++++++++++++++++++++++++++++ axfer/mapper.h | 79 +++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 axfer/mapper.c create mode 100644 axfer/mapper.h
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 55fcf71..cb4b188 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -17,7 +17,8 @@ LDADD = \ noinst_HEADERS = \ misc.h \ subcmd.h \ - container.h + container.h \ + mapper.h
axfer_SOURCES = \ misc.h \ @@ -29,4 +30,6 @@ axfer_SOURCES = \ container-riff-wave.c \ container-au.c \ container-voc.c \ - container-raw.c + container-raw.c \ + mapper.h \ + mapper.c diff --git a/axfer/mapper.c b/axfer/mapper.c new file mode 100644 index 00000000..eb63a0e --- /dev/null +++ b/axfer/mapper.c @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper.c - an interface of muxer/demuxer between buffer with data frames and +// formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "mapper.h" +#include "misc.h" + +#include <stdio.h> + +static const char *const mapper_type_labels[] = { + [MAPPER_TYPE_MUXER] = "muxer", + [MAPPER_TYPE_DEMUXER] = "demuxer", +}; + +static const char *const mapper_target_labels[] = { + [MAPPER_TARGET_COUNT] = "", +}; + +int mapper_context_init(struct mapper_context *mapper, + enum mapper_type type, unsigned int cntr_count, + unsigned int verbose) +{ + const struct mapper_data *data = NULL; + + assert(mapper); + assert(cntr_count > 0); + + // Detect forgotten to destruct. + assert(mapper->private_data == NULL); + + memset(mapper, 0, sizeof(*mapper)); + + mapper->ops = &data->ops; + mapper->type = type; + + mapper->private_data = malloc(data->private_size); + if (mapper->private_data == NULL) + return -ENOMEM; + memset(mapper->private_data, 0, data->private_size); + + mapper->cntr_count = cntr_count; + mapper->verbose = verbose; + + return 0; +} + +int mapper_context_pre_process(struct mapper_context *mapper, + snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + struct container_context *cntrs) +{ + int err; + + assert(mapper); + assert(access >= SND_PCM_ACCESS_MMAP_INTERLEAVED); + assert(access <= SND_PCM_ACCESS_RW_NONINTERLEAVED); + assert(bytes_per_sample > 0); + assert(samples_per_frame > 0); + assert(cntrs); + + mapper->access = access; + mapper->bytes_per_sample = bytes_per_sample; + mapper->samples_per_frame = samples_per_frame; + mapper->frames_per_buffer = frames_per_buffer; + + err = mapper->ops->pre_process(mapper, cntrs, mapper->cntr_count); + if (err < 0) + return err; + + if (mapper->verbose > 0) { + fprintf(stderr, "Mapper: %s\n", + mapper_type_labels[mapper->type]); + fprintf(stderr, " target: %s\n", + mapper_target_labels[mapper->target]); + fprintf(stderr, " access: %s\n", + snd_pcm_access_name(mapper->access)); + fprintf(stderr, " bytes/sample: %u\n", + mapper->bytes_per_sample); + fprintf(stderr, " samples/frame: %u\n", + mapper->samples_per_frame); + fprintf(stderr, " frames/buffer: %lu\n", + mapper->frames_per_buffer); + } + + return 0; +} + +int mapper_context_process_frames(struct mapper_context *mapper, + void *frame_buffer, + unsigned int *frame_count, + struct container_context *cntrs) +{ + assert(mapper); + assert(frame_buffer); + assert(frame_count); + assert(*frame_count <= mapper->frames_per_buffer); + assert(cntrs); + + return mapper->ops->process_frames(mapper, frame_buffer, frame_count, + cntrs, mapper->cntr_count); +} + +void mapper_context_post_process(struct mapper_context *mapper) +{ + assert(mapper); + + if (mapper->ops && mapper->ops->post_process) + mapper->ops->post_process(mapper); +} + +void mapper_context_destroy(struct mapper_context *mapper) +{ + assert(mapper); + + if (mapper->private_data) + free(mapper->private_data); + mapper->private_data = NULL; +} diff --git a/axfer/mapper.h b/axfer/mapper.h new file mode 100644 index 00000000..c4caf09 --- /dev/null +++ b/axfer/mapper.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper.h - an interface of muxer/demuxer between buffer with data frames and +// formatted files. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_MAPPER__H_ +#define __ALSA_UTILS_AXFER_MAPPER__H_ + +#include "container.h" + +enum mapper_type { + MAPPER_TYPE_MUXER = 0, + MAPPER_TYPE_DEMUXER, + MAPPER_TYPE_COUNT, +}; + +enum mapper_target { + MAPPER_TARGET_COUNT, +}; + +struct mapper_ops; + +struct mapper_context { + enum mapper_type type; + enum mapper_target target; + const struct mapper_ops *ops; + unsigned int private_size; + + void *private_data; + unsigned int cntr_count; + + // A part of parameters of PCM substream. + snd_pcm_access_t access; + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + snd_pcm_uframes_t frames_per_buffer; + + unsigned int verbose; +}; + +int mapper_context_init(struct mapper_context *mapper, + enum mapper_type type, unsigned int cntr_count, + unsigned int verbose); +int mapper_context_pre_process(struct mapper_context *mapper, + snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + struct container_context *cntrs); +int mapper_context_process_frames(struct mapper_context *mapper, + void *frame_buffer, + unsigned int *frame_count, + struct container_context *cntrs); +void mapper_context_post_process(struct mapper_context *mapper); +void mapper_context_destroy(struct mapper_context *mapper); + +// For internal use in 'mapper' module. + +struct mapper_ops { + int (*pre_process)(struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int cntr_count); + int (*process_frames)(struct mapper_context *mapper, + void *frame_buffer, unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count); + void (*post_process)(struct mapper_context *mapper); +}; + +struct mapper_data { + struct mapper_ops ops; + unsigned int private_size; +}; + +#endif
In usual use case of aplay, single file is used to playback or capture data frames.
This commit adds support of single type mapper for this use case. All of supported file format can include data frame with interleaved alignment, thus this mapper have a functionality to convert from several types of data frame alignment to interleaved alignment or vise versa. When handling non-interleaved buffer, a caller should use an array of buffer for each of channels with non-interleaved data frames.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/mapper-single.c | 191 ++++++++++++++++++++++++++++++++++++++++++ axfer/mapper.c | 14 +++- axfer/mapper.h | 4 + 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 axfer/mapper-single.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index cb4b188..baf9b89 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -32,4 +32,5 @@ axfer_SOURCES = \ container-voc.c \ container-raw.c \ mapper.h \ - mapper.c + mapper.c \ + mapper-single.c diff --git a/axfer/mapper-single.c b/axfer/mapper-single.c new file mode 100644 index 00000000..aa8aa19 --- /dev/null +++ b/axfer/mapper-single.c @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper-single.c - a muxer/demuxer for single containers. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "mapper.h" +#include "misc.h" + +struct single_state { + void (*align_frames)(void *frame_buf, unsigned int frame_count, + char *buf, unsigned int bytes_per_sample, + unsigned int samples_per_frame); + char *buf; +}; + +static void align_to_vector(void *frame_buf, unsigned int frame_count, + char *src, unsigned int bytes_per_sample, + unsigned samples_per_frame) +{ + char **dst_bufs = frame_buf; + char *dst; + unsigned int src_pos; + unsigned int dst_pos; + int i, j; + + // src: interleaved => dst: a set of interleaved buffers. + for (i = 0; i < samples_per_frame; ++i) { + dst = dst_bufs[i]; + for (j = 0; j < frame_count; ++j) { + src_pos = bytes_per_sample * (samples_per_frame * j + i); + dst_pos = bytes_per_sample * j; + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static void align_from_vector(void *frame_buf, unsigned int frame_count, + char *dst, unsigned int bytes_per_sample, + unsigned int samples_per_frame) +{ + char **src_bufs = frame_buf; + char *src; + unsigned int dst_pos; + unsigned int src_pos; + int i, j; + + // src: a set of interleaved buffers => dst:interleaved. + for (i = 0; i < samples_per_frame; ++i) { + src = src_bufs[i]; + for (j = 0; j < frame_count; ++j) { + src_pos = bytes_per_sample * j; + dst_pos = bytes_per_sample * (samples_per_frame * j + i); + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static int single_pre_process(struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct single_state *state = mapper->private_data; + unsigned int bytes_per_buffer; + + if (cntrs->bytes_per_sample != mapper->bytes_per_sample || + cntrs->samples_per_frame != mapper->samples_per_frame) + return -EINVAL; + + // Decide method to align frames. + if (mapper->type == MAPPER_TYPE_DEMUXER) { + if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = align_from_vector; + else if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } else { + if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = align_to_vector; + else if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } + + if (state->align_frames) { + // Allocate intermediate buffer as the same size as a period. + bytes_per_buffer = mapper->bytes_per_sample * + mapper->samples_per_frame * + mapper->frames_per_buffer; + state->buf = malloc(bytes_per_buffer); + if (state->buf == NULL) + return -ENOMEM; + memset(state->buf, 0, bytes_per_buffer); + } + + return 0; +} + +static int single_muxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct single_state *state = mapper->private_data; + void *src; + int err; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + src = frame_buf; + } else { + src = state->buf; + } + err = container_context_process_frames(cntrs, src, frame_count); + if (err < 0) + return err; + + // Unlikely. + if (src != frame_buf && *frame_count > 0) + state->align_frames(frame_buf, *frame_count, src, + mapper->bytes_per_sample, + mapper->samples_per_frame); + + return 0; +} + +static int single_demuxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct single_state *state = mapper->private_data; + void *dst; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + dst = frame_buf; + } else { + state->align_frames(frame_buf, *frame_count, state->buf, + mapper->bytes_per_sample, + mapper->samples_per_frame); + dst = state->buf; + } + + return container_context_process_frames(cntrs, dst, frame_count); +} + +static void single_post_process(struct mapper_context *mapper) +{ + struct single_state *state = mapper->private_data; + + if (state->buf) + free(state->buf); + + state->buf = NULL; + state->align_frames = NULL; +} + +const struct mapper_data mapper_muxer_single = { + .ops = { + .pre_process = single_pre_process, + .process_frames = single_muxer_process_frames, + .post_process = single_post_process, + }, + .private_size = sizeof(struct single_state), +}; + +const struct mapper_data mapper_demuxer_single = { + .ops = { + .pre_process = single_pre_process, + .process_frames = single_demuxer_process_frames, + .post_process = single_post_process, + }, + .private_size = sizeof(struct single_state), +}; diff --git a/axfer/mapper.c b/axfer/mapper.c index eb63a0e..07ca595 100644 --- a/axfer/mapper.c +++ b/axfer/mapper.c @@ -18,7 +18,7 @@ static const char *const mapper_type_labels[] = { };
static const char *const mapper_target_labels[] = { - [MAPPER_TARGET_COUNT] = "", + [MAPPER_TARGET_SINGLE] = "single", };
int mapper_context_init(struct mapper_context *mapper, @@ -35,6 +35,18 @@ int mapper_context_init(struct mapper_context *mapper,
memset(mapper, 0, sizeof(*mapper));
+ if (type == MAPPER_TYPE_MUXER) { + if (cntr_count == 1) { + data = &mapper_muxer_single; + mapper->target = MAPPER_TARGET_SINGLE; + } + } else { + if (cntr_count == 1) { + data = &mapper_demuxer_single; + mapper->target = MAPPER_TARGET_SINGLE; + } + } + mapper->ops = &data->ops; mapper->type = type;
diff --git a/axfer/mapper.h b/axfer/mapper.h index c4caf09..3a95d9f 100644 --- a/axfer/mapper.h +++ b/axfer/mapper.h @@ -19,6 +19,7 @@ enum mapper_type { };
enum mapper_target { + MAPPER_TARGET_SINGLE = 0, MAPPER_TARGET_COUNT, };
@@ -76,4 +77,7 @@ struct mapper_data { unsigned int private_size; };
+extern const struct mapper_data mapper_muxer_single; +extern const struct mapper_data mapper_demuxer_single; + #endif
This commit adds support of mapper for 'multiple' target. This handles several files via 'container' functions, and constructs data frame buffer for playback, or splits data frames from data frame buffer for capture. When playback source files includes data frames with several channels, the first channel is used to construct buffer. For capture direction, each of channel of data frame is stored in one file, thus the file includes one channel of data frame. When handling non-interleaved buffer, a caller should use an array of buffer for each of channels with non-interleaved data frames.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/mapper-multiple.c | 259 ++++++++++++++++++++++++++++++++++++++++ axfer/mapper.c | 13 ++ axfer/mapper.h | 4 + 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 axfer/mapper-multiple.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index baf9b89..f17e59b 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -33,4 +33,5 @@ axfer_SOURCES = \ container-raw.c \ mapper.h \ mapper.c \ - mapper-single.c + mapper-single.c \ + mapper-multiple.c diff --git a/axfer/mapper-multiple.c b/axfer/mapper-multiple.c new file mode 100644 index 00000000..a0978b9 --- /dev/null +++ b/axfer/mapper-multiple.c @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper-multiple.c - a muxer/demuxer for multiple containers. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "mapper.h" +#include "misc.h" + +struct multiple_state { + void (*align_frames)(void *frame_buf, unsigned int frame_count, + char **buf, unsigned int bytes_per_sample, + struct container_context *cntrs, + unsigned int cntr_count); + char **bufs; + unsigned int cntr_count; +}; + +static void align_to_i(void *frame_buf, unsigned int frame_count, + char **src_bufs, unsigned int bytes_per_sample, + struct container_context *cntrs, unsigned int cntr_count) +{ + char *dst = frame_buf; + char *src; + unsigned int dst_pos; + unsigned int src_pos; + struct container_context *cntr; + int i, j; + + // src: first channel in each of interleaved buffers in containers => + // dst:interleaved. + for (i = 0; i < cntr_count; ++i) { + src = src_bufs[i]; + cntr = cntrs + i; + + for (j = 0; j < frame_count; ++j) { + // Use first src channel for each of dst channel. + src_pos = bytes_per_sample * cntr->samples_per_frame * j; + dst_pos = bytes_per_sample * (cntr_count * j + i); + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static void align_from_i(void *frame_buf, unsigned int frame_count, + char **dst_bufs, unsigned int bytes_per_sample, + struct container_context *cntrs, + unsigned int cntr_count) +{ + char *src = frame_buf; + char *dst; + unsigned int src_pos; + unsigned int dst_pos; + struct container_context *cntr; + int i, j; + + for (i = 0; i < cntr_count; ++i) { + dst = dst_bufs[i]; + cntr = cntrs + i; + + for (j = 0; j < frame_count; ++j) { + // Use first src channel for each of dst channel. + src_pos = bytes_per_sample * (cntr_count * j + i); + dst_pos = bytes_per_sample * cntr->samples_per_frame * j; + + memcpy(dst + dst_pos, src + src_pos, bytes_per_sample); + } + } +} + +static int multiple_pre_process(struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct multiple_state *state = mapper->private_data; + struct container_context *cntr; + int i; + + // Additionally, format of samples in the containers should be the same + // as the format in PCM substream. + for (i = 0; i < cntr_count; ++i) { + cntr = cntrs + i; + if (mapper->bytes_per_sample != cntr->bytes_per_sample) + return -EINVAL; + } + state->cntr_count = cntr_count; + + // Decide method to align frames. + if (mapper->type == MAPPER_TYPE_DEMUXER) { + if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = align_from_i; + else if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } else { + if (mapper->access == SND_PCM_ACCESS_RW_INTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_INTERLEAVED) + state->align_frames = align_to_i; + else if (mapper->access == SND_PCM_ACCESS_RW_NONINTERLEAVED || + mapper->access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + state->align_frames = NULL; + else + return -EINVAL; + } + + if (state->align_frames) { + // Furthermore, in demuxer case, each container should be + // configured to store one sample per frame. + if (mapper->type == MAPPER_TYPE_DEMUXER) { + for (i = 0; i < cntr_count; ++i) { + cntr = cntrs + i; + if (cntrs->samples_per_frame != 1) + return -EINVAL; + } + } + + state->bufs = calloc(cntr_count, sizeof(char *)); + if (state->bufs == NULL) + return -ENOMEM; + + for (i = 0; i < cntr_count; ++i) { + unsigned int bytes_per_buffer; + + // Allocate intermediate buffer as the same size as a + // period for each of containers. + cntr = cntrs + i; + + bytes_per_buffer = mapper->bytes_per_sample * + cntr->samples_per_frame * + mapper->frames_per_buffer; + + state->bufs[i] = malloc(bytes_per_buffer); + if (state->bufs[i] == NULL) + return -ENOMEM; + memset(state->bufs[i], 0, bytes_per_buffer); + } + } + + return 0; +} + +static int process_containers(char **src_bufs, unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct container_context *cntr; + char *src; + int i; + int err = 0; + + // TODO: arrangement for *frame_count. + for (i = 0; i < cntr_count; ++i) { + cntr = &cntrs[i]; + src = src_bufs[i]; + + err = container_context_process_frames(cntr, src, frame_count); + if (err < 0) + break; + } + + return err; +} + +static int multiple_muxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct multiple_state *state = mapper->private_data; + char **src_bufs; + int err; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + src_bufs = frame_buf; + } else { + src_bufs = state->bufs; + } + err = process_containers(src_bufs, frame_count, cntrs, cntr_count); + if (err < 0) + return err; + + // Unlikely. + if (src_bufs != frame_buf && *frame_count > 0) { + state->align_frames(frame_buf, *frame_count, src_bufs, + mapper->bytes_per_sample, cntrs, + cntr_count); + } + + return 0; +} + +static int multiple_demuxer_process_frames(struct mapper_context *mapper, + void *frame_buf, + unsigned int *frame_count, + struct container_context *cntrs, + unsigned int cntr_count) +{ + struct multiple_state *state = mapper->private_data; + char **dst_bufs; + + // If need to align PCM frames, process PCM frames to the intermediate + // buffer once. + if (!state->align_frames) { + // The most likely. + dst_bufs = frame_buf; + } else { + dst_bufs = state->bufs; + state->align_frames(frame_buf, *frame_count, dst_bufs, + mapper->bytes_per_sample, cntrs, + cntr_count); + } + + return process_containers(dst_bufs, frame_count, cntrs, cntr_count); +} + +static void multiple_post_process(struct mapper_context *mapper) +{ + struct multiple_state *state = mapper->private_data; + int i; + + if (state->bufs) { + for (i = 0; i < state->cntr_count; ++i) { + if (state->bufs[i]) + free(state->bufs[i]); + } + free(state->bufs); + } + + state->bufs = NULL; + state->align_frames = NULL; +} + +const struct mapper_data mapper_muxer_multiple = { + .ops = { + .pre_process = multiple_pre_process, + .process_frames = multiple_muxer_process_frames, + .post_process = multiple_post_process, + }, + .private_size = sizeof(struct multiple_state), +}; + +const struct mapper_data mapper_demuxer_multiple = { + .ops = { + .pre_process = multiple_pre_process, + .process_frames = multiple_demuxer_process_frames, + .post_process = multiple_post_process, + }, + .private_size = sizeof(struct multiple_state), +}; diff --git a/axfer/mapper.c b/axfer/mapper.c index 07ca595..4c3f0e3 100644 --- a/axfer/mapper.c +++ b/axfer/mapper.c @@ -19,6 +19,7 @@ static const char *const mapper_type_labels[] = {
static const char *const mapper_target_labels[] = { [MAPPER_TARGET_SINGLE] = "single", + [MAPPER_TARGET_MULTIPLE] = "multiple", };
int mapper_context_init(struct mapper_context *mapper, @@ -39,11 +40,17 @@ int mapper_context_init(struct mapper_context *mapper, if (cntr_count == 1) { data = &mapper_muxer_single; mapper->target = MAPPER_TARGET_SINGLE; + } else { + data = &mapper_muxer_multiple; + mapper->target = MAPPER_TARGET_MULTIPLE; } } else { if (cntr_count == 1) { data = &mapper_demuxer_single; mapper->target = MAPPER_TARGET_SINGLE; + } else { + data = &mapper_demuxer_multiple; + mapper->target = MAPPER_TARGET_MULTIPLE; } }
@@ -77,6 +84,12 @@ int mapper_context_pre_process(struct mapper_context *mapper, assert(samples_per_frame > 0); assert(cntrs);
+ // The purpose of multiple target is to mux/demux each channels to/from + // containers. + if (mapper->target == MAPPER_TARGET_MULTIPLE && + samples_per_frame != mapper->cntr_count) + return -EINVAL; + mapper->access = access; mapper->bytes_per_sample = bytes_per_sample; mapper->samples_per_frame = samples_per_frame; diff --git a/axfer/mapper.h b/axfer/mapper.h index 3a95d9f..58b6118 100644 --- a/axfer/mapper.h +++ b/axfer/mapper.h @@ -20,6 +20,7 @@ enum mapper_type {
enum mapper_target { MAPPER_TARGET_SINGLE = 0, + MAPPER_TARGET_MULTIPLE, MAPPER_TARGET_COUNT, };
@@ -80,4 +81,7 @@ struct mapper_data { extern const struct mapper_data mapper_muxer_single; extern const struct mapper_data mapper_demuxer_single;
+extern const struct mapper_data mapper_muxer_multiple; +extern const struct mapper_data mapper_demuxer_multiple; + #endif
In former commits, mapper module gets supports of muxer/demuxer for single/multiple targets for playback source or capture destination. This commit adds a unit test for them. This includes positive test cases only. The test cases actually generate I/O to file systems for many test cases. It takes a bit long time to finish.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/test/Makefile.am | 20 +- axfer/test/mapper-test.c | 491 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 axfer/test/mapper-test.c
diff --git a/axfer/test/Makefile.am b/axfer/test/Makefile.am index 66b30ef..76a93bf 100644 --- a/axfer/test/Makefile.am +++ b/axfer/test/Makefile.am @@ -1,8 +1,10 @@ TESTS = \ - container-test + container-test \ + mapper-test
check_PROGRAMS = \ - container-test + container-test \ + mapper-test
container_test_SOURCES = \ ../container.h \ @@ -13,3 +15,17 @@ container_test_SOURCES = \ ../container-raw.c \ generator.c \ container-test.c + +mapper_test_SOURCES = \ + ../container.h \ + ../container.c \ + ../container-riff-wave.c \ + ../container-au.c \ + ../container-voc.c \ + ../container-raw.c \ + ../mapper.h \ + ../mapper.c \ + ../mapper-single.c \ + ../mapper-multiple.c \ + generator.c \ + mapper-test.c diff --git a/axfer/test/mapper-test.c b/axfer/test/mapper-test.c new file mode 100644 index 00000000..9f005aa --- /dev/null +++ b/axfer/test/mapper-test.c @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// mapper-io.c - a unit test for muxer/demuxer for PCM frames on buffer. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "../mapper.h" +#include "../misc.h" + +#include "generator.h" + +#include <stdlib.h> +#include <unistd.h> +#include <stdbool.h> + +#include <assert.h> + +struct mapper_trial { + enum container_format cntr_format; + struct container_context *cntrs; + + char **paths; + + struct mapper_context mapper; + bool verbose; +}; + +static void test_demuxer(struct mapper_context *mapper, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + struct container_context *cntrs, + unsigned int cntr_count, bool verbose) +{ + unsigned int total_frame_count; + int err; + + err = mapper_context_init(mapper, MAPPER_TYPE_DEMUXER, cntr_count, + verbose); + assert(err == 0); + + err = mapper_context_pre_process(mapper, access, bytes_per_sample, + samples_per_frame, frames_per_buffer, + cntrs); + assert(err == 0); + + total_frame_count = frame_count; + err = mapper_context_process_frames(mapper, frame_buffer, + &total_frame_count, cntrs); + assert(err == 0); + assert(total_frame_count == frame_count); + + mapper_context_post_process(mapper); + mapper_context_destroy(mapper); +} + +static int test_demux(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + unsigned int cntr_count) +{ + struct container_context *cntrs = trial->cntrs; + char **paths = trial->paths; + enum container_format cntr_format = trial->cntr_format; + unsigned int bytes_per_sample; + uint64_t total_frame_count; + int i; + int err; + + for (i = 0; i < cntr_count; ++i) { + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + + err = container_builder_init(cntrs + i, paths[i], cntr_format, + 0); + if (err < 0) + goto end; + + format = sample_format; + rate = frames_per_second; + total_frame_count = frame_count; + if (cntr_count > 1) + channels = 1; + else + channels = samples_per_frame; + err = container_context_pre_process(cntrs + i, &format, + &channels, &rate, + &total_frame_count); + if (err < 0) + goto end; + assert(format == sample_format); + assert(rate == frames_per_second); + assert(total_frame_count >= 0); + if (cntr_count > 1) + assert(channels == 1); + else + assert(channels == samples_per_frame); + } + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + test_demuxer(&trial->mapper, access, bytes_per_sample, + samples_per_frame, frames_per_buffer, frame_buffer, + frame_count, cntrs, cntr_count, trial->verbose); + + for (i = 0; i < cntr_count; ++i) { + container_context_post_process(cntrs + i, &total_frame_count); + assert(total_frame_count == frame_count); + } +end: + for (i = 0; i < cntr_count; ++i) + container_context_destroy(cntrs + i); + + return err; +} + +static void test_muxer(struct mapper_context *mapper, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + struct container_context *cntrs, + unsigned int cntr_count, bool verbose) +{ + unsigned int total_frame_count; + int err; + + err = mapper_context_init(mapper, MAPPER_TYPE_MUXER, cntr_count, + verbose); + assert(err == 0); + + err = mapper_context_pre_process(mapper, access, bytes_per_sample, + samples_per_frame, frames_per_buffer, + cntrs); + assert(err == 0); + + total_frame_count = frame_count; + err = mapper_context_process_frames(mapper, frame_buffer, + &total_frame_count, cntrs); + assert(err == 0); + assert(total_frame_count == frame_count); + + mapper_context_post_process(mapper); + mapper_context_destroy(mapper); +} + +static int test_mux(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, + unsigned int frames_per_buffer, + void *frame_buffer, unsigned int frame_count, + unsigned int cntr_count) +{ + struct container_context *cntrs = trial->cntrs; + char **paths = trial->paths; + unsigned int bytes_per_sample; + uint64_t total_frame_count; + int i; + int err; + + for (i = 0; i < cntr_count; ++i) { + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + + err = container_parser_init(cntrs + i, paths[i], 0); + if (err < 0) + goto end; + + format = sample_format; + rate = frames_per_second; + if (cntr_count > 1) + channels = 1; + else + channels = samples_per_frame; + err = container_context_pre_process(cntrs + i, &format, + &channels, &rate, + &total_frame_count); + if (err < 0) + goto end; + + assert(format == sample_format); + assert(rate == frames_per_second); + assert(total_frame_count == frame_count); + if (cntr_count > 1) + assert(channels == 1); + else + assert(channels == samples_per_frame); + } + + bytes_per_sample = snd_pcm_format_physical_width(sample_format) / 8; + test_muxer(&trial->mapper, access, bytes_per_sample, samples_per_frame, + frames_per_buffer, frame_buffer, frame_count, cntrs, + cntr_count, trial->verbose); + + for (i = 0; i < cntr_count; ++i) { + container_context_post_process(cntrs + i, &total_frame_count); + assert(total_frame_count == frame_count); + } +end: + for (i = 0; i < cntr_count; ++i) + container_context_destroy(cntrs + i); + + return err; +} + +static int test_mapper(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + void *check_buffer, unsigned int frame_count, + unsigned int cntr_count) +{ + unsigned int frames_per_buffer; + int i; + int err; + + // Use a buffer aligned by typical size of page frame. + frames_per_buffer = ((frame_count + 4096) / 4096) * 4096; + + err = test_demux(trial, access, sample_format, samples_per_frame, + frames_per_second, frames_per_buffer, frame_buffer, + frame_count, cntr_count); + if (err < 0) + goto end; + + err = test_mux(trial, access, sample_format, samples_per_frame, + frames_per_second, frames_per_buffer, check_buffer, + frame_count, cntr_count); +end: + for (i = 0; i < cntr_count; ++i) + unlink(trial->paths[i]); + + return err; +} + +static int test_i_buf(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count) +{ + unsigned int size; + char *buf; + int err; + + size = frame_count * samples_per_frame * + snd_pcm_format_physical_width(sample_format) / 8; + buf = malloc(size); + if (buf == 0) + return -ENOMEM; + memset(buf, 0, size); + + // Test multiple target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, buf, + frame_count, cntr_count); + if (err < 0) + goto end; + err = memcmp(frame_buffer, buf, size); + assert(err == 0); + + // Test single target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, buf, + frame_count, 1); + if (err < 0) + goto end; + err = memcmp(frame_buffer, buf, size); + assert(err == 0); +end: + free(buf); + + return err; +} + +static int test_vector(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count) +{ + unsigned int size; + char **bufs; + int i; + int err; + + bufs = calloc(cntr_count, sizeof(*bufs)); + if (bufs == NULL) + return -ENOMEM; + + size = frame_count * snd_pcm_format_physical_width(sample_format) / 8; + + for (i = 0; i < cntr_count; ++i) { + bufs[i] = malloc(size); + if (bufs[i] == NULL) + goto end; + memset(bufs[i], 0, size); + } + + // Test multiple target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, bufs, + frame_count, cntr_count); + if (err < 0) + goto end; + for (i = 0; i < cntr_count; ++i) { + char **target = frame_buffer; + err = memcmp(target[i], bufs[i], size); + assert(err == 0); + } + + // Test single target. + err = test_mapper(trial, access, sample_format, samples_per_frame, + frames_per_second, frame_buffer, bufs, + frame_count, 1); + if (err < 0) + goto end; + for (i = 0; i < cntr_count; ++i) { + char **target = frame_buffer; + err = memcmp(target[i], bufs[i], size); + assert(err == 0); + } +end: + for (i = 0; i < cntr_count; ++i) { + if (bufs[i]) + free(bufs[i]); + } + free(bufs); + + return err; +} + +static int test_n_buf(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count) +{ + char *test_buf = frame_buffer; + unsigned int size; + char **test_vec; + int i; + int err; + + size = frame_count * snd_pcm_format_physical_width(sample_format) / 8; + + test_vec = calloc(cntr_count * 2, sizeof(*test_vec)); + if (test_vec == NULL) + return -ENOMEM; + + for (i = 0; i < cntr_count; ++i) + test_vec[i] = test_buf + size * i; + + err = test_vector(trial, access, sample_format, samples_per_frame, + frames_per_second, test_vec, frame_count, cntr_count); + free(test_vec); + + return err; +} + +static int callback(struct test_generator *gen, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, void *frame_buffer, + unsigned int frame_count) +{ + + int (*handler)(struct mapper_trial *trial, snd_pcm_access_t access, + snd_pcm_format_t sample_format, + unsigned int samples_per_frame, + unsigned int frames_per_second, void *frame_buffer, + unsigned int frame_count, unsigned int cntr_count); + struct mapper_trial *trial = gen->private_data; + + if (access == SND_PCM_ACCESS_RW_NONINTERLEAVED) + handler = test_vector; + else if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) + handler = test_n_buf; + else + handler = test_i_buf; + + return handler(trial, access, sample_format, samples_per_frame, 48000, + frame_buffer, frame_count, samples_per_frame); +}; + +int main(int argc, const char *argv[]) +{ + // Test 8/16/18/20/24/32/64 bytes per sample. + static const uint64_t sample_format_mask = + (1ul << SND_PCM_FORMAT_U8) | + (1ul << SND_PCM_FORMAT_S16_LE) | + (1ul << SND_PCM_FORMAT_S18_3LE) | + (1ul << SND_PCM_FORMAT_S20_3LE) | + (1ul << SND_PCM_FORMAT_S24_LE) | + (1ul << SND_PCM_FORMAT_S32_LE) | + (1ul << SND_PCM_FORMAT_FLOAT64_LE); + uint64_t access_mask; + struct test_generator gen = {0}; + struct mapper_trial *trial; + struct container_context *cntrs; + unsigned int samples_per_frame; + char **paths = NULL; + snd_pcm_access_t access; + bool verbose; + int i; + int err; + + // Test up to 32 channels. + samples_per_frame = 32; + cntrs = calloc(samples_per_frame, sizeof(*cntrs)); + if (cntrs == NULL) + return -ENOMEM; + + paths = calloc(samples_per_frame, sizeof(*paths)); + if (paths == NULL) { + err = -ENOMEM; + goto end; + } + for (i = 0; i < samples_per_frame; ++i) { + paths[i] = malloc(8); + if (paths[i] == NULL) { + err = -ENOMEM; + goto end; + } + snprintf(paths[i], 8, "hoge%d", i); + } + + if (argc > 1) { + char *term; + access = strtol(argv[1], &term, 10); + if (errno != 0 || *term != '\0') { + err = -EINVAL;; + goto end; + } + if (access < SND_PCM_ACCESS_MMAP_INTERLEAVED && + access > SND_PCM_ACCESS_RW_NONINTERLEAVED) { + err = -EINVAL; + goto end; + } + if (access == SND_PCM_ACCESS_MMAP_COMPLEX) { + err = -EINVAL; + goto end; + } + + access_mask = 1ul << access; + verbose = true; + } else { + access_mask = (1ul << SND_PCM_ACCESS_MMAP_INTERLEAVED) | + (1ul << SND_PCM_ACCESS_MMAP_NONINTERLEAVED) | + (1ul << SND_PCM_ACCESS_RW_INTERLEAVED) | + (1ul << SND_PCM_ACCESS_RW_NONINTERLEAVED); + verbose = false; + } + + err = generator_context_init(&gen, access_mask, sample_format_mask, + 1, samples_per_frame, + 23, 4500, 1024, + sizeof(struct mapper_trial)); + if (err < 0) + goto end; + + trial = gen.private_data; + trial->cntrs = cntrs; + trial->cntr_format = CONTAINER_FORMAT_RIFF_WAVE; + trial->paths = paths; + trial->verbose = verbose; + err = generator_context_run(&gen, callback); + + generator_context_destroy(&gen); +end: + if (paths) { + for (i = 0; i < samples_per_frame; ++i) + free(paths[i]); + free(paths); + } + free(cntrs); + + if (err < 0) { + printf("%s\n", strerror(-err)); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}
ALSA has PCM interface to transfer data frames. In userspace, there're some implementation to utilize this interface to produce application programming interface; alsa-lib (libasound) and tinyalsa. However, it's possible to use the interface with raw I/O operations.
This commit adds an common interface to transfer data frames for this program, named as 'xfer'. This internal interface is designed for users to select several backend for data transmission. This includes some functions expected to be called by main program just for data transmission. In an aspect to maintain PCM substream, suspend feature is required to handle a pair of SIGTSTP/SIGCONT UNIX signals.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 7 +- axfer/xfer.c | 170 ++++++++++++++++++++++++++++++++++++++++++++++ axfer/xfer.h | 80 ++++++++++++++++++++++ 3 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 axfer/xfer.c create mode 100644 axfer/xfer.h
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index f17e59b..9371365 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -18,7 +18,8 @@ noinst_HEADERS = \ misc.h \ subcmd.h \ container.h \ - mapper.h + mapper.h \ + xfer.h
axfer_SOURCES = \ misc.h \ @@ -34,4 +35,6 @@ axfer_SOURCES = \ mapper.h \ mapper.c \ mapper-single.c \ - mapper-multiple.c + mapper-multiple.c \ + xfer.h \ + xfer.c diff --git a/axfer/xfer.c b/axfer/xfer.c new file mode 100644 index 00000000..7c41a65 --- /dev/null +++ b/axfer/xfer.c @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer.c - receiver/transmiter of data frames. +// +// 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 <stdio.h> + +static const char *const xfer_type_labels[] = { + [XFER_TYPE_COUNT] = "", +}; + +enum xfer_type xfer_type_from_label(const char *label) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(xfer_type_labels); ++i) { + if (!strcmp(xfer_type_labels[i], label)) + return i; + } + + return XFER_TYPE_UNSUPPORTED; +} + +int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, + snd_pcm_stream_t direction, int argc, char *const *argv) +{ + struct { + enum xfer_type type; + const struct xfer_data *data; + } *entry, entries[] = { + {XFER_TYPE_COUNT, NULL}, + }; + int i; + int err; + + assert(xfer); + assert(direction >= SND_PCM_STREAM_PLAYBACK); + assert(direction <= SND_PCM_STREAM_CAPTURE); + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + if (entries[i].type == type) + break; + } + if (i == ARRAY_SIZE(entries)) + return -EINVAL; + entry = &entries[i]; + + xfer->direction = direction; + xfer->type = type; + xfer->ops = &entry->data->ops; + + xfer->private_data = malloc(entry->data->private_size); + if (xfer->private_data == NULL) + return -ENOMEM; + memset(xfer->private_data, 0, entry->data->private_size); + + err = xfer->ops->init(xfer, direction); + if (err < 0) + return err; + + return xfer->ops->validate_opts(xfer); +} + +void xfer_context_destroy(struct xfer_context *xfer) +{ + assert(xfer); + + if (!xfer->ops) + return; + + if (xfer->ops->destroy) + xfer->ops->destroy(xfer); + if (xfer->private_data) + free(xfer->private_data); +} + +int xfer_context_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) +{ + int err; + + assert(xfer); + assert(format); + assert(samples_per_frame); + assert(frames_per_second); + assert(access); + assert(frames_per_buffer); + + if (!xfer->ops) + return -ENXIO; + + err = xfer->ops->pre_process(xfer, format, samples_per_frame, + frames_per_second, access, + frames_per_buffer); + if (err < 0) + return err; + + assert(*format >= SND_PCM_FORMAT_S8); + assert(*format <= SND_PCM_FORMAT_LAST); + assert(*samples_per_frame > 0); + assert(*frames_per_second > 0); + assert(*access >= SND_PCM_ACCESS_MMAP_INTERLEAVED); + assert(*access <= SND_PCM_ACCESS_LAST); + assert(*frames_per_buffer > 0); + + if (xfer->verbose > 1) { + fprintf(stderr, "Transfer: %s\n", + xfer_type_labels[xfer->type]); + fprintf(stderr, " access: %s\n", + snd_pcm_access_name(*access)); + fprintf(stderr, " sample format: %s\n", + snd_pcm_format_name(*format)); + fprintf(stderr, " bytes/sample: %u\n", + snd_pcm_format_physical_width(*format) / 8); + fprintf(stderr, " samples/frame: %u\n", + *samples_per_frame); + fprintf(stderr, " frames/second: %u\n", + *frames_per_second); + fprintf(stderr, " frames/buffer: %lu\n", + *frames_per_buffer); + } + + return 0; +} + +int xfer_context_process_frames(struct xfer_context *xfer, + struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int *frame_count) +{ + assert(xfer); + assert(mapper); + assert(cntrs); + assert(frame_count); + + if (!xfer->ops) + return -ENXIO; + + return xfer->ops->process_frames(xfer, frame_count, mapper, cntrs); +} + +void xfer_context_pause(struct xfer_context *xfer, bool enable) +{ + assert(xfer); + + if (!xfer->ops) + return; + + xfer->ops->pause(xfer, enable); +} + +void xfer_context_post_process(struct xfer_context *xfer) +{ + assert(xfer); + + if (!xfer->ops) + return; + + xfer->ops->post_process(xfer); +} diff --git a/axfer/xfer.h b/axfer/xfer.h new file mode 100644 index 00000000..3faaec0 --- /dev/null +++ b/axfer/xfer.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer.h - a header for receiver/transmiter of data frames. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_XFER__H_ +#define __ALSA_UTILS_AXFER_XFER__H_ + +#include "mapper.h" + +#include <getopt.h> + +enum xfer_type { + XFER_TYPE_UNSUPPORTED = -1, + XFER_TYPE_COUNT, +}; + +struct xfer_ops; + +struct xfer_context { + snd_pcm_stream_t direction; + enum xfer_type type; + const struct xfer_ops *ops; + void *private_data; + + unsigned int verbose; +}; + +enum xfer_type xfer_type_from_label(const char *label); + +int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, + snd_pcm_stream_t direction, int argc, char *const *argv); +void xfer_context_destroy(struct xfer_context *xfer); +int xfer_context_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); +int xfer_context_process_frames(struct xfer_context *xfer, + struct mapper_context *mapper, + struct container_context *cntrs, + unsigned int *frame_count); +void xfer_context_pause(struct xfer_context *xfer, bool enable); +void xfer_context_post_process(struct xfer_context *xfer); + +// For internal use in 'xfer' module. + +struct xfer_ops { + int (*init)(struct xfer_context *xfer, snd_pcm_stream_t direction); + int (*parse_opt)(struct xfer_context *xfer, int key, const char *optarg); + int (*validate_opts)(struct xfer_context *xfer); + int (*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); + int (*process_frames)(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + void (*post_process)(struct xfer_context *xfer); + void (*destroy)(struct xfer_context *xfer); + void (*pause)(struct xfer_context *xfer, bool enable); +}; + +struct xfer_data { + const char *s_opts; + const struct option *l_opts; + unsigned int l_opts_count; + struct xfer_ops ops; + unsigned int private_size; +}; + +extern const struct xfer_data xfer_alsa; + +#endif
In aplay, many command-line options are supported. Some of them have dependency or conflicts. Furthemore, some of them are just for runtime configuration of alsa-lib(libasound), and some options can be used by several xfer backends commonly; e.g. options for file name, sample format and sampling rate.
This commit adds a parser for the common options below. * --help (-h) * Just output 'help' string (not written yet). * --verbose (-v) * For verbose output, including information about xfer, mapper and container. * --format (-f): string. format literals or one of ['cd'|'cdr'|'dat'] * For sample format supported by ALSA PCM interface. Special format can be used. For playback, this is auto-detected according to actual file format. * --channels (-c) * For the number of samples included in one data frame. For playback, this is auto-detected according to actual file format, except for 'raw' format. This option can conflict to above format option. * --rate (-r) * For the number of data frames transferred in one second. For playback, this is auto-detected according to actual file format, except for 'raw' format. This option can conflict to format option above. * --file-type (-f): string. one of ['wav'|'au'|'voc'|'raw'] * For format of files of given paths. For playback, this is optional because the format is auto-detected. For capture, this is optional too because the format is decided according to suffix of given path. Anyway, this option is used for cases to fail to detect or decide. * --separate-channels (-I) * When using several files as source or destination for transmission of data frame, this option can be used with several file paths.
When '--separate-channels' option is used, users can give several file paths to source/destination of data transmission, else they can give single file path for the purpose. When multiple files are handled by this option, for playback, data frames in first channel is used to construct buffer for data transmission with multi channel. For capture, data frames in each channel of buffer are written to each of given path. Furthermore, when a single path is given for capture, file paths are auto-generated according to available number of channels. For example, 'name.wav' is given for 2 channels capture, 'name-0.wav' and 'name-1.wav' are generated. In a case of no suffix, 'name-0' and 'name-1' are generated.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/main.c | 36 +++ axfer/misc.h | 3 + axfer/xfer-options.c | 535 +++++++++++++++++++++++++++++++++++++++++++ axfer/xfer.c | 78 +++++++ axfer/xfer.h | 19 ++ 6 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 axfer/xfer-options.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 9371365..5f0a6cf 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -37,4 +37,5 @@ axfer_SOURCES = \ mapper-single.c \ mapper-multiple.c \ xfer.h \ - xfer.c + xfer.c \ + xfer-options.c diff --git a/axfer/main.c b/axfer/main.c index c9cf104..f141439 100644 --- a/axfer/main.c +++ b/axfer/main.c @@ -24,6 +24,42 @@ enum subcmds { SUBCMD_VERSION, };
+char *arg_duplicate_string(const char *str, int *err) +{ + char *ptr; + + // For safe. + if (strlen(str) > 1024) { + *err = -EINVAL; + return NULL; + } + + ptr = strdup(str); + if (ptr == NULL) + *err = -ENOMEM; + + return ptr; +} + +long arg_parse_decimal_num(const char *str, int *err) +{ + long val; + char *endptr; + + errno = 0; + val = strtol(str, &endptr, 0); + if (errno > 0) { + *err = -errno; + return 0; + } + if (*endptr != '\0') { + *err = -EINVAL; + return 0; + } + + return val; +} + static void print_version(const char *const cmdname) { printf("%s: version %s\n", cmdname, SND_UTIL_VERSION_STR); diff --git a/axfer/misc.h b/axfer/misc.h index 7c8bfb3..5f27d28 100644 --- a/axfer/misc.h +++ b/axfer/misc.h @@ -13,4 +13,7 @@
#define ARRAY_SIZE(array) (sizeof(array)/sizeof(array[0]))
+char *arg_duplicate_string(const char *str, int *err); +long arg_parse_decimal_num(const char *str, int *err); + #endif diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c new file mode 100644 index 00000000..fb71244 --- /dev/null +++ b/axfer/xfer-options.c @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-options.c - a parser of commandline options for xfer. +// +// 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 <getopt.h> +#include <math.h> +#include <limits.h> + +enum no_short_opts { + // 128 or later belong to non us-ascii character set. + OPT_XFER_TYPE = 128, +}; + +static int allocate_paths(struct xfer_context *xfer, char *const *paths, + unsigned int count) +{ + bool stdio = false; + int i; + + if (count == 0) { + stdio = true; + count = 1; + } + + xfer->paths = calloc(count, sizeof(xfer->paths[0])); + if (xfer->paths == NULL) + return -ENOMEM; + xfer->path_count = count; + + if (stdio) { + xfer->paths[0] = strndup("-", PATH_MAX); + if (xfer->paths[0] == NULL) + return -ENOMEM; + } else { + for (i = 0; i < count; ++i) { + xfer->paths[i] = strndup(paths[i], PATH_MAX); + if (xfer->paths[i] == NULL) + return -ENOMEM; + } + } + + return 0; +} + +static int verify_cntr_format(struct xfer_context *xfer) +{ + static const struct { + const char *const literal; + enum container_format cntr_format; + } *entry, entries[] = { + {"raw", CONTAINER_FORMAT_RAW}, + {"voc", CONTAINER_FORMAT_VOC}, + {"wav", CONTAINER_FORMAT_RIFF_WAVE}, + {"au", CONTAINER_FORMAT_AU}, + {"sparc", CONTAINER_FORMAT_AU}, + }; + int i; + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + entry = &entries[i]; + if (strcasecmp(xfer->cntr_format_literal, entry->literal)) + continue; + + xfer->cntr_format = entry->cntr_format; + return 0; + } + + fprintf(stderr, "unrecognized file format '%s'\n", + xfer->cntr_format_literal); + + return -EINVAL; +} + +// This should be called after 'verify_cntr_format()'. +static int verify_sample_format(struct xfer_context *xfer) +{ + static const struct { + const char *const literal; + unsigned int frames_per_second; + unsigned int samples_per_frame; + snd_pcm_format_t le_format; + snd_pcm_format_t be_format; + } *entry, entries[] = { + {"cd", 44100, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, + {"cdr", 44100, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, + {"dat", 48000, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, + }; + int i; + + xfer->sample_format = snd_pcm_format_value(xfer->sample_format_literal); + if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) + return 0; + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + entry = &entries[i]; + if (strcmp(entry->literal, xfer->sample_format_literal)) + continue; + + if (xfer->frames_per_second > 0 && + xfer->frames_per_second != entry->frames_per_second) { + fprintf(stderr, + "'%s' format can't be used with rate except " + "for %u.\n", + entry->literal, entry->frames_per_second); + return -EINVAL; + } + + if (xfer->samples_per_frame > 0 && + xfer->samples_per_frame != entry->samples_per_frame) { + fprintf(stderr, + "'%s' format can't be used with channel except " + "for %u.\n", + entry->literal, entry->samples_per_frame); + return -EINVAL; + } + + xfer->frames_per_second = entry->frames_per_second; + xfer->samples_per_frame = entry->samples_per_frame; + if (xfer->cntr_format == CONTAINER_FORMAT_AU) + xfer->sample_format = entry->be_format; + else + xfer->sample_format = entry->le_format; + + return 0; + } + + fprintf(stderr, "wrong extended format '%s'\n", + xfer->sample_format_literal); + + return -EINVAL; +} + +static int validate_options(struct xfer_context *xfer) +{ + unsigned int val; + int err = 0; + + if (xfer->cntr_format_literal == NULL) { + if (xfer->direction == SND_PCM_STREAM_CAPTURE) { + // To stdout. + if (xfer->path_count == 1 && + !strcmp(xfer->paths[0], "-")) { + xfer->cntr_format = CONTAINER_FORMAT_RAW; + } else { + // Use first path as a representative. + xfer->cntr_format = container_format_from_path( + xfer->paths[0]); + } + } + // For playback, perform auto-detection. + } else { + err = verify_cntr_format(xfer); + } + if (err < 0) + return err; + + if (xfer->multiple_cntrs) { + if (!strcmp(xfer->paths[0], "-")) { + fprintf(stderr, + "An option for separated channels is not " + "available with stdin/stdout.\n"); + return -EINVAL; + } + + // For captured PCM frames, even if one path is given for + // container files, it can be used to generate several paths. + // For this purpose, please see + // 'xfer_options_fixup_paths()'. + if (xfer->direction == SND_PCM_STREAM_PLAYBACK) { + // Require several paths for containers. + if (xfer->path_count == 1) { + fprintf(stderr, + "An option for separated channels " + "requires several files to playback " + "PCM frames.\n"); + return -EINVAL; + } + } + } else { + // A single path is available only. + if (xfer->path_count > 1) { + fprintf(stderr, + "When using several files, an option for " + "sepatated channels is used with.\n"); + return -EINVAL; + } + } + + xfer->sample_format = SND_PCM_FORMAT_UNKNOWN; + if (xfer->sample_format_literal) { + err = verify_sample_format(xfer); + if (err < 0) + return err; + } + + val = xfer->frames_per_second; + if (xfer->frames_per_second == 0) + xfer->frames_per_second = 8000; + if (xfer->frames_per_second < 1000) + xfer->frames_per_second *= 1000; + if (xfer->frames_per_second < 2000 || + xfer->frames_per_second > 192000) { + fprintf(stderr, "bad speed value '%i'\n", val); + return -EINVAL; + } + + if (xfer->samples_per_frame > 0) { + if (xfer->samples_per_frame < 1 || + xfer->samples_per_frame > 256) { + fprintf(stderr, "invalid channels argument '%u'\n", + xfer->samples_per_frame); + return -EINVAL; + } + } + + return err; +} + +int xfer_options_parse_args(struct xfer_context *xfer, + const struct xfer_data *data, int argc, + char *const *argv) +{ + static const char *short_opts = "CPhvf:c:r:t:I"; + static const struct option long_opts[] = { + // For generic purposes. + {"capture", 0, 0, 'C'}, + {"playback", 0, 0, 'P'}, + {"xfer-type", 1, 0, OPT_XFER_TYPE}, + {"help", 0, 0, 'h'}, + {"verbose", 0, 0, 'v'}, + // For transfer backend. + {"format", 1, 0, 'f'}, + {"channels", 1, 0, 'c'}, + {"rate", 1, 0, 'r'}, + // For containers. + {"file-type", 1, 0, 't'}, + // For mapper. + {"separate-channels", 0, 0, 'I'}, + }; + char *s_opts; + struct option *l_opts; + int l_index; + int key; + int err = 0; + + // Concatenate short options. + s_opts = malloc(strlen(data->s_opts) + strlen(short_opts) + 1); + if (s_opts == NULL) + return -ENOMEM; + strcpy(s_opts, data->s_opts); + strcpy(s_opts + strlen(s_opts), short_opts); + s_opts[strlen(data->s_opts) + strlen(short_opts)] = '\0'; + + // Concatenate long options, including a sentinel. + l_opts = calloc(ARRAY_SIZE(long_opts) * data->l_opts_count + 1, + sizeof(*l_opts)); + if (l_opts == NULL) { + free(s_opts); + return -ENOMEM; + } + memcpy(l_opts, long_opts, ARRAY_SIZE(long_opts) * sizeof(*l_opts)); + memcpy(&l_opts[ARRAY_SIZE(long_opts)], data->l_opts, + data->l_opts_count * sizeof(*l_opts)); + + // Parse options. + l_index = 0; + optarg = NULL; + optind = 1; + opterr = 1; // use error output. + optopt = 0; + while (1) { + key = getopt_long(argc, argv, s_opts, l_opts, &l_index); + if (key < 0) + break; + else if (key == 'C') + ; // already parsed. + else if (key == 'P') + ; // already parsed. + else if (key == OPT_XFER_TYPE) + ; // already parsed. + else if (key == 'h') + xfer->help = true; + else if (key == 'v') + ++xfer->verbose; + else if (key == 'f') + xfer->sample_format_literal = arg_duplicate_string(optarg, &err); + else if (key == 'c') + xfer->samples_per_frame = arg_parse_decimal_num(optarg, &err); + else if (key == 'r') + xfer->frames_per_second = arg_parse_decimal_num(optarg, &err); + else if (key == 't') + xfer->cntr_format_literal = arg_duplicate_string(optarg, &err); + else if (key == 'I') + xfer->multiple_cntrs = true; + else if (key == '?') + return -EINVAL; + else { + err = xfer->ops->parse_opt(xfer, key, optarg); + if (err < 0 && err != -ENXIO) + break; + } + } + + free(l_opts); + free(s_opts); + + err = allocate_paths(xfer, argv + optind, argc - optind); + if (err < 0) + return err; + + return validate_options(xfer); +} + +static const char *const allowed_duplication[] = { + "/dev/null", + "/dev/zero", + "/dev/full", + "/dev/random", + "/dev/urandom", +}; + +static int generate_path_with_suffix(struct xfer_context *xfer, + const char *template, unsigned int index, + const char *suffix) +{ + static const char *const single_format = "%s%s"; + static const char *const multiple_format = "%s-%i%s"; + unsigned int len; + + len = strlen(template) + strlen(suffix) + 1; + if (xfer->path_count > 1) + len += (unsigned int)log10(xfer->path_count) + 2; + + xfer->paths[index] = malloc(len); + if (xfer->paths[index] == NULL) + return -ENOMEM; + + if (xfer->path_count == 1) { + snprintf(xfer->paths[index], len, single_format, template, + suffix); + } else { + snprintf(xfer->paths[index], len, multiple_format, template, + index, suffix); + } + + return 0; +} + +static int generate_path_without_suffix(struct xfer_context *xfer, + const char *template, + unsigned int index, const char *suffix) +{ + static const char *const single_format = "%s"; + static const char *const multiple_format = "%s-%i"; + unsigned int len; + + len = strlen(template) + 1; + if (xfer->path_count > 1) + len += (unsigned int)log10(xfer->path_count) + 2; + + xfer->paths[index] = malloc(len); + if (xfer->paths[index] == NULL) + return -ENOMEM; + + if (xfer->path_count == 1) { + snprintf(xfer->paths[index], len, single_format, template); + } else { + snprintf(xfer->paths[index], len, multiple_format, template, + index); + } + + return 0; +} + +static int generate_path(struct xfer_context *xfer, char *template, + unsigned int index, const char *suffix) +{ + int (*generator)(struct xfer_context *xfer, const char *template, + unsigned int index, const char *suffix); + char *pos; + + if (strlen(suffix) > 0) { + pos = template + strlen(template) - strlen(suffix); + // Separate filename and suffix. + if (!strcmp(pos, suffix)) + *pos = '\0'; + } + + // Select handlers. + if (strlen(suffix) > 0) + generator = generate_path_with_suffix; + else + generator = generate_path_without_suffix; + + return generator(xfer, template, index, suffix); +} + +static int create_paths(struct xfer_context *xfer, unsigned int path_count) +{ + char *template; + const char *suffix; + int i, j; + int err = 0; + + // Can cause memory leak. + assert(xfer->path_count == 1); + assert(xfer->paths); + assert(xfer->paths[0]); + assert(xfer->paths[0][0] != '\0'); + + // Release at first. + template = xfer->paths[0]; + free(xfer->paths); + xfer->paths = NULL; + + // Allocate again. + xfer->paths = calloc(path_count, sizeof(*xfer->paths)); + if (xfer->paths == NULL) { + err = -ENOMEM; + goto end; + } + xfer->path_count = path_count; + + suffix = container_suffix_from_format(xfer->cntr_format); + + for (i = 0; i < xfer->path_count; ++i) { + // Some file names are allowed to be duplicated. + for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { + if (!strcmp(template, allowed_duplication[j])) + break; + } + if (j < ARRAY_SIZE(allowed_duplication)) + continue; + + err = generate_path(xfer, template, i, suffix); + if (err < 0) + break; + } +end: + free(template); + + return err; +} + +static int fixup_paths(struct xfer_context *xfer) +{ + const char *suffix; + char *template; + int i, j; + int err = 0; + + suffix = container_suffix_from_format(xfer->cntr_format); + + for (i = 0; i < xfer->path_count; ++i) { + // Some file names are allowed to be duplicated. + for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { + if (!strcmp(xfer->paths[i], allowed_duplication[j])) + break; + } + if (j < ARRAY_SIZE(allowed_duplication)) + continue; + + template = xfer->paths[i]; + xfer->paths[i] = NULL; + err = generate_path(xfer, template, i, suffix); + free(template); + if (err < 0) + break; + } + + return err; +} + +int xfer_options_fixup_paths(struct xfer_context *xfer) +{ + int i, j; + int err; + + if (xfer->path_count == 1) { + // Nothing to do for sign of stdin/stdout. + if (!strcmp(xfer->paths[0], "-")) + return 0; + if (!xfer->multiple_cntrs) + err = fixup_paths(xfer); + else + err = create_paths(xfer, xfer->samples_per_frame); + } else { + if (!xfer->multiple_cntrs) + return -EINVAL; + if (xfer->path_count != xfer->samples_per_frame) + return -EINVAL; + else + err = fixup_paths(xfer); + } + if (err < 0) + return err; + + // Check duplication of the paths. + for (i = 0; i < xfer->path_count - 1; ++i) { + // Some file names are allowed to be duplicated. + for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { + if (!strcmp(xfer->paths[i], allowed_duplication[j])) + break; + } + if (j < ARRAY_SIZE(allowed_duplication)) + continue; + + for (j = i + 1; j < xfer->path_count; ++j) { + if (!strcmp(xfer->paths[i], xfer->paths[j])) { + fprintf(stderr, + "Detect duplicated file names:\n"); + err = -EINVAL; + break; + } + } + if (j < xfer->path_count) + break; + } + + if (xfer->verbose > 1) + fprintf(stderr, "Handled file names:\n"); + if (err < 0 || xfer->verbose > 1) { + for (i = 0; i < xfer->path_count; ++i) + fprintf(stderr, " %d: %s\n", i, xfer->paths[i]); + } + + return err; +} diff --git a/axfer/xfer.c b/axfer/xfer.c index 7c41a65..594abca 100644 --- a/axfer/xfer.c +++ b/axfer/xfer.c @@ -64,11 +64,17 @@ int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, if (err < 0) return err;
+ err = xfer_options_parse_args(xfer, entry->data, argc, argv); + if (err < 0) + return err; + return xfer->ops->validate_opts(xfer); }
void xfer_context_destroy(struct xfer_context *xfer) { + int i; + assert(xfer);
if (!xfer->ops) @@ -78,6 +84,20 @@ void xfer_context_destroy(struct xfer_context *xfer) xfer->ops->destroy(xfer); if (xfer->private_data) free(xfer->private_data); + + if (xfer->paths) { + for (i = 0; i < xfer->path_count; ++i) + free(xfer->paths[i]); + free(xfer->paths); + } + + xfer->paths = NULL; + + free(xfer->sample_format_literal); + xfer->sample_format_literal = NULL; + + free(xfer->cntr_format_literal); + xfer->cntr_format_literal = NULL; }
int xfer_context_pre_process(struct xfer_context *xfer, @@ -99,6 +119,54 @@ int xfer_context_pre_process(struct xfer_context *xfer, if (!xfer->ops) return -ENXIO;
+ if (xfer->direction == SND_PCM_STREAM_CAPTURE) { + // For capture direction, use values in options if given. + if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) + *format = xfer->sample_format; + if (xfer->samples_per_frame > 0) + *samples_per_frame = xfer->samples_per_frame; + if (xfer->frames_per_second > 0) + *frames_per_second = xfer->frames_per_second; + } else if (xfer->direction == SND_PCM_STREAM_PLAYBACK) { + // For playback direction, check values in given options so that + // they don't mismatch to parameters from media container. + if (*format != xfer->sample_format) { + // Not initial value. + if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) { + fprintf(stderr, + "Sample format mismatch: %s is given " + "but %s by files\n", + snd_pcm_format_name(xfer->sample_format), + snd_pcm_format_name(*format)); + return -EINVAL; + } + } + + if (*samples_per_frame != xfer->samples_per_frame) { + // Not initial value. + if (xfer->samples_per_frame > 0) { + fprintf(stderr, + "The number of channels mismatch: %u " + "is given but %u by files\n", + xfer->samples_per_frame, + *samples_per_frame); + return -EINVAL; + } + } + + if (*frames_per_second != xfer->frames_per_second) { + // Not initial value. + if (xfer->frames_per_second != 8000) { + fprintf(stderr, + "Sampling rate mismatch: %u is given " + "but %u by files\n", + xfer->frames_per_second, + *frames_per_second); + return -EINVAL; + } + } + } + err = xfer->ops->pre_process(xfer, format, samples_per_frame, frames_per_second, access, frames_per_buffer); @@ -113,6 +181,16 @@ int xfer_context_pre_process(struct xfer_context *xfer, assert(*access <= SND_PCM_ACCESS_LAST); assert(*frames_per_buffer > 0);
+ xfer->sample_format = *format; + xfer->samples_per_frame = *samples_per_frame; + xfer->frames_per_second = *frames_per_second; + + if (xfer->direction == SND_PCM_STREAM_CAPTURE) { + err = xfer_options_fixup_paths(xfer); + if (err < 0) + return err; + } + if (xfer->verbose > 1) { fprintf(stderr, "Transfer: %s\n", xfer_type_labels[xfer->type]); diff --git a/axfer/xfer.h b/axfer/xfer.h index 3faaec0..3eaa045 100644 --- a/axfer/xfer.h +++ b/axfer/xfer.h @@ -26,7 +26,20 @@ struct xfer_context { const struct xfer_ops *ops; void *private_data;
+ char *sample_format_literal; + char *cntr_format_literal; unsigned int verbose; + unsigned int frames_per_second; + unsigned int samples_per_frame; + bool help:1; + bool multiple_cntrs:1; // For mapper. + + snd_pcm_format_t sample_format; + + // For containers. + char **paths; + unsigned int path_count; + enum container_format cntr_format; };
enum xfer_type xfer_type_from_label(const char *label); @@ -47,6 +60,12 @@ int xfer_context_process_frames(struct xfer_context *xfer, void xfer_context_pause(struct xfer_context *xfer, bool enable); void xfer_context_post_process(struct xfer_context *xfer);
+struct xfer_data; +int xfer_options_parse_args(struct xfer_context *xfer, + const struct xfer_data *data, int argc, + char *const *argv); +int xfer_options_fixup_paths(struct xfer_context *xfer); + // For internal use in 'xfer' module.
struct xfer_ops {
This commit adds support fo alsa-lib PCM API as a backend of 'xfer' module. In a set of alsa-lib PCM API, there're two ways to handle data frames; by calling ioctl(2) with some specific commands with buffer in user space, or copying data frames on mapped page frames. To support both ways, this commit adds an operation structure as abstraction.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 7 +- axfer/xfer-libasound.c | 415 +++++++++++++++++++++++++++++++++++++++++ axfer/xfer-libasound.h | 46 +++++ axfer/xfer.c | 4 +- axfer/xfer.h | 3 +- 5 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 axfer/xfer-libasound.c create mode 100644 axfer/xfer-libasound.h
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 5f0a6cf..1ec4ab4 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -19,7 +19,8 @@ noinst_HEADERS = \ subcmd.h \ container.h \ mapper.h \ - xfer.h + xfer.h \ + xfer-libasound.h
axfer_SOURCES = \ misc.h \ @@ -38,4 +39,6 @@ axfer_SOURCES = \ mapper-multiple.c \ xfer.h \ xfer.c \ - xfer-options.c + xfer-options.c \ + xfer-libasound.h \ + xfer-libasound.c diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c new file mode 100644 index 00000000..2bca465 --- /dev/null +++ b/axfer/xfer-libasound.c @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound.c - receive/transmit frames by alsa-lib. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" + +#define S_OPTS "D:" +static const struct option l_opts[] = { + {"device", 1, 0, 'D'}, +}; + +static int xfer_libasound_init(struct xfer_context *xfer, + snd_pcm_stream_t direction) +{ + struct libasound_state *state = xfer->private_data; + int err; + + err = snd_output_stdio_attach(&state->log, stderr, 0); + if (err < 0) + return err; + + err = snd_pcm_hw_params_malloc(&state->hw_params); + if (err < 0) + return err; + + return snd_pcm_sw_params_malloc(&state->sw_params); +} + +static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, + const char *optarg) +{ + struct libasound_state *state = xfer->private_data; + int err = 0; + + if (key == 'D') + state->node_literal = arg_duplicate_string(optarg, &err); + else + err = -ENXIO; + + return err; +} + +int xfer_libasound_validate_opts(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + int err = 0; + + state->verbose = xfer->verbose > 1; + + if (state->node_literal == NULL) { + state->node_literal = strdup("default"); + if (state->node_literal == NULL) + return -ENOMEM; + } + + return err; +} + +static int set_access_hw_param(struct libasound_state *state) +{ + 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); + 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(state->handle, state->hw_params, + mask); + snd_pcm_access_mask_free(mask); + + return err; +} + +static int open_handle(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + int err; + + err = snd_pcm_open(&state->handle, state->node_literal, xfer->direction, + 0); + if (err < 0) { + logging(state, "Fail to open libasound PCM node for %s: %s\n", + snd_pcm_stream_name(xfer->direction), + state->node_literal); + return err; + } + + err = snd_pcm_hw_params_any(state->handle, state->hw_params); + if (err < 0) + return err; + + // TODO: Applying NO_PERIOD_WAKEUP should be done here. + + return set_access_hw_param(state); +} + +static int configure_hw_params(struct libasound_state *state, + snd_pcm_format_t format, + unsigned int samples_per_frame, + unsigned int frames_per_second) +{ + int err; + + // Configure sample format. + if (format == SND_PCM_FORMAT_UNKNOWN) { + snd_pcm_format_mask_t *mask; + + err = snd_pcm_format_mask_malloc(&mask); + if (err < 0) + return err; + snd_pcm_hw_params_get_format_mask(state->hw_params, mask); + for (format = 0; format <= SND_PCM_FORMAT_LAST; ++format) { + if (snd_pcm_format_mask_test(mask, format)) + break; + } + snd_pcm_format_mask_free(mask); + if (format > SND_PCM_FORMAT_LAST) { + logging(state, + "Any sample format is not available.\n"); + return -EINVAL; + } + } + err = snd_pcm_hw_params_set_format(state->handle, state->hw_params, + format); + if (err < 0) { + logging(state, + "Sample format '%s' is not available: %s\n", + snd_pcm_format_name(format), snd_strerror(err)); + return err; + } + + // Configure channels. + if (samples_per_frame == 0) { + err = snd_pcm_hw_params_get_channels_min(state->hw_params, + &samples_per_frame); + if (err < 0) { + logging(state, + "Any channel number is not available.\n"); + return err; + } + } + err = snd_pcm_hw_params_set_channels(state->handle, state->hw_params, + samples_per_frame); + if (err < 0) { + logging(state, + "Channels count '%u' is not available: %s\n", + samples_per_frame, snd_strerror(err)); + return err; + } + + // Configure rate. + if (frames_per_second == 0) { + err = snd_pcm_hw_params_get_rate_min(state->hw_params, + &frames_per_second, NULL); + if (err < 0) { + logging(state, + "Any rate is not available.\n"); + return err; + } + + } + err = snd_pcm_hw_params_set_rate(state->handle, state->hw_params, + frames_per_second, 0); + if (err < 0) { + logging(state, + "Sampling rate '%u' is not available: %s\n", + frames_per_second, snd_strerror(err)); + return err; + } + + return snd_pcm_hw_params(state->handle, state->hw_params); +} + +static int retrieve_actual_hw_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 int configure_sw_params(struct libasound_state *state, + unsigned int frames_per_second, + unsigned int frames_per_buffer) +{ + return snd_pcm_sw_params(state->handle, state->sw_params); +} + +static int xfer_libasound_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 libasound_state *state = xfer->private_data; + int err; + + err = open_handle(xfer); + if (err < 0) + return -ENXIO; + + err = configure_hw_params(state, *format, *samples_per_frame, + *frames_per_second); + if (err < 0) { + logging(state, "Current hardware parameters:\n"); + snd_pcm_hw_params_dump(state->hw_params, state->log); + return err; + } + + // Retrieve actual parameters. + err = retrieve_actual_hw_params(state->hw_params, format, + samples_per_frame, frames_per_second, + access, frames_per_buffer); + if (err < 0) + return err; + + // 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->ops->private_size > 0) { + state->private_data = malloc(state->ops->private_size); + if (state->private_data == NULL) + return -ENOMEM; + memset(state->private_data, 0, state->ops->private_size); + } + err = state->ops->pre_process(state); + if (err < 0) + return err; + + err = configure_sw_params(state, *frames_per_second, + *frames_per_buffer); + if (err < 0) { + logging(state, "Current software parameters:\n"); + snd_pcm_sw_params_dump(state->sw_params, state->log); + return err; + } + + if (xfer->verbose > 0) + snd_pcm_dump(state->handle, state->log); + + return 0; +} + +static int xfer_libasound_process_frames(struct xfer_context *xfer, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct libasound_state *state = xfer->private_data; + int err; + + if (state->handle == NULL) + return -ENXIO; + + err = state->ops->process_frames(state, frame_count, mapper, cntrs); + if (err < 0) { + if (err == -EAGAIN) + return err; + if (err == -EPIPE) { + // Recover the stream and continue processing + // immediately. In this program -EPIPE comes from + // libasound implementation instead of file I/O. + err = snd_pcm_prepare(state->handle); + } + + if (err < 0) { + // TODO: -EIO from libasound for hw PCM node means + // that IRQ disorder. This should be reported to help + // developers for drivers. + logging(state, "Fail to process frames: %s\n", + snd_strerror(err)); + } + } + + return err; +} + +static void xfer_libasound_pause(struct xfer_context *xfer, bool enable) +{ + struct libasound_state *state = xfer->private_data; + snd_pcm_state_t s = snd_pcm_state(state->handle); + int err; + + if (state->handle == NULL) + return; + + 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 && state->verbose) { + logging(state, "snd_pcm_pause(): %s\n", snd_strerror(err)); + } +} + +static void xfer_libasound_post_process(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + snd_pcm_state_t pcm_state; + int err; + + if (state->handle == NULL) + return; + + 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) + logging(state, "snd_pcm_drop(): %s\n", + snd_strerror(err)); + } else { + err = snd_pcm_drain(state->handle); + if (err < 0) + logging(state, "snd_pcm_drain(): %s\n", + snd_strerror(err)); + } + } + + err = snd_pcm_hw_free(state->handle); + if (err < 0) + logging(state, "snd_pcm_hw_free(): %s\n", snd_strerror(err)); + + snd_pcm_close(state->handle); + state->handle = NULL; + + if (state->ops && state->ops->post_process) + state->ops->post_process(state); + free(state->private_data); + state->private_data = NULL; + + // Free cache of content for configuration files so that memory leaks + // are not detected. + snd_config_update_free_global(); +} + +static void xfer_libasound_destroy(struct xfer_context *xfer) +{ + struct libasound_state *state = xfer->private_data; + + free(state->node_literal); + state->node_literal = 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; + + if (state->log) + snd_output_close(state->log); + state->log = NULL; +} + +const struct xfer_data xfer_libasound = { + .s_opts = S_OPTS, + .l_opts = l_opts, + .l_opts_count = ARRAY_SIZE(l_opts), + .ops = { + .init = xfer_libasound_init, + .parse_opt = xfer_libasound_parse_opt, + .validate_opts = xfer_libasound_validate_opts, + .pre_process = xfer_libasound_pre_process, + .process_frames = xfer_libasound_process_frames, + .pause = xfer_libasound_pause, + .post_process = xfer_libasound_post_process, + .destroy = xfer_libasound_destroy, + }, + .private_size = sizeof(struct libasound_state), +}; diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h new file mode 100644 index 00000000..57fa867 --- /dev/null +++ b/axfer/xfer-libasound.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound.h - a header for receiver/transmitter of frames by alsa-lib. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_XFER_LIBASOUND__H_ +#define __ALSA_UTILS_AXFER_XFER_LIBASOUND__H_ + +#include "xfer.h" + +#define logging(state, ...) \ + snd_output_printf(state->log, __VA_ARGS__) + +struct xfer_libasound_ops; + +struct libasound_state { + snd_pcm_t *handle; + + snd_output_t *log; + snd_pcm_hw_params_t *hw_params; + snd_pcm_sw_params_t *sw_params; + + const struct xfer_libasound_ops *ops; + void *private_data; + + bool verbose; + + char *node_literal; +}; + +// For internal use in 'libasound' module. + +struct xfer_libasound_ops { + int (*pre_process)(struct libasound_state *state); + int (*process_frames)(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + void (*post_process)(struct libasound_state *state); + unsigned int private_size; +}; + +#endif diff --git a/axfer/xfer.c b/axfer/xfer.c index 594abca..21595eb 100644 --- a/axfer/xfer.c +++ b/axfer/xfer.c @@ -12,7 +12,7 @@ #include <stdio.h>
static const char *const xfer_type_labels[] = { - [XFER_TYPE_COUNT] = "", + [XFER_TYPE_LIBASOUND] = "libasound", };
enum xfer_type xfer_type_from_label(const char *label) @@ -34,7 +34,7 @@ int xfer_context_init(struct xfer_context *xfer, enum xfer_type type, enum xfer_type type; const struct xfer_data *data; } *entry, entries[] = { - {XFER_TYPE_COUNT, NULL}, + {XFER_TYPE_LIBASOUND, &xfer_libasound}, }; int i; int err; diff --git a/axfer/xfer.h b/axfer/xfer.h index 3eaa045..df43d1c 100644 --- a/axfer/xfer.h +++ b/axfer/xfer.h @@ -15,6 +15,7 @@
enum xfer_type { XFER_TYPE_UNSUPPORTED = -1, + XFER_TYPE_LIBASOUND = 0, XFER_TYPE_COUNT, };
@@ -94,6 +95,6 @@ struct xfer_data { unsigned int private_size; };
-extern const struct xfer_data xfer_alsa; +extern const struct xfer_data xfer_libasound;
#endif
In alsa-lib PCM API, snd_pcm_read[i|n]() and snd_pcm_write[i|n]() are used to transfer data frames from/to hardware. When a handler is not opened with specific flags, these functions perform blocking operation; i.e. the function call doesn't return till all of request number of data frames are actually handled, or call is interrupted by Unix signals, or PCM substeam corrupts due to hardware reasons.
This commit adds support for this type of data transmission. For cases that requested data frames are not processed by container interface, this commit adds internal cache mechanism to handle rest of data frames in next timing.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 8 +- axfer/frame-cache.c | 111 ++++++++++++ axfer/frame-cache.h | 47 ++++++ axfer/xfer-libasound-irq-rw.c | 307 ++++++++++++++++++++++++++++++++++ axfer/xfer-libasound.c | 6 + axfer/xfer-libasound.h | 2 + 6 files changed, 479 insertions(+), 2 deletions(-) create mode 100644 axfer/frame-cache.c create mode 100644 axfer/frame-cache.h create mode 100644 axfer/xfer-libasound-irq-rw.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 1ec4ab4..07c1fb3 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -20,7 +20,8 @@ noinst_HEADERS = \ container.h \ mapper.h \ xfer.h \ - xfer-libasound.h + xfer-libasound.h \ + frame-cache.h
axfer_SOURCES = \ misc.h \ @@ -41,4 +42,7 @@ axfer_SOURCES = \ xfer.c \ xfer-options.c \ xfer-libasound.h \ - xfer-libasound.c + xfer-libasound.c \ + frame-cache.h \ + frame-cache.c \ + xfer-libasound-irq-rw.c diff --git a/axfer/frame-cache.c b/axfer/frame-cache.c new file mode 100644 index 00000000..882568f --- /dev/null +++ b/axfer/frame-cache.c @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// frame-cache.c - maintainer of cache for data frame. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "frame-cache.h" + +static void align_frames_in_i(struct frame_cache *cache, + unsigned int consumed_count) +{ + char *buf = cache->buf; + unsigned int offset; + unsigned int size; + + cache->remained_count -= consumed_count; + + offset = cache->bytes_per_sample * cache->samples_per_frame * + consumed_count; + size = cache->bytes_per_sample * cache->samples_per_frame * + cache->remained_count; + memmove(buf, buf + offset, size); + + cache->buf_ptr = buf + size; +} + +static void align_frames_in_n(struct frame_cache *cache, + unsigned int consumed_count) +{ + char **bufs = cache->buf; + char **buf_ptrs = cache->buf_ptr; + unsigned int offset; + unsigned int size; + int i; + + cache->remained_count -= consumed_count; + + for (i = 0; i < cache->samples_per_frame; ++i) { + offset = cache->bytes_per_sample * consumed_count; + size = cache->bytes_per_sample * cache->remained_count; + memmove(bufs[i], bufs[i] + offset, size); + buf_ptrs[i] = bufs[i] + size; + } +} + +int frame_cache_init(struct frame_cache *cache, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_cache) +{ + if (access == SND_PCM_ACCESS_RW_INTERLEAVED) + cache->align_frames = align_frames_in_i; + else if (access == SND_PCM_ACCESS_RW_NONINTERLEAVED) + cache->align_frames = align_frames_in_n; + else + return -EINVAL; + cache->access = access; + + if (access == SND_PCM_ACCESS_RW_INTERLEAVED) { + char *buf; + + buf = calloc(frames_per_cache, + bytes_per_sample * samples_per_frame); + if (buf == NULL) + return -ENOMEM; + cache->buf = buf; + cache->buf_ptr = buf; + } else { + char **bufs; + char **buf_ptrs; + int i; + + bufs = calloc(samples_per_frame, sizeof(*bufs)); + if (bufs == NULL) + return -ENOMEM; + buf_ptrs = calloc(samples_per_frame, sizeof(*buf_ptrs)); + if (buf_ptrs == NULL) + return -ENOMEM; + for (i = 0; i < samples_per_frame; ++i) { + bufs[i] = calloc(frames_per_cache, bytes_per_sample); + if (bufs[i] == NULL) + return -ENOMEM; + buf_ptrs[i] = bufs[i]; + } + cache->buf = bufs; + cache->buf_ptr = buf_ptrs; + } + + cache->remained_count = 0; + cache->bytes_per_sample = bytes_per_sample; + cache->samples_per_frame = samples_per_frame; + cache->frames_per_cache = frames_per_cache; + + return 0; +} + +void frame_cache_destroy(struct frame_cache *cache) +{ + if (cache->access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { + int i; + for (i = 0; i < cache->samples_per_frame; ++i) { + char **bufs = cache->buf; + free(bufs[i]); + } + free(cache->buf_ptr); + } + free(cache->buf); + memset(cache, 0, sizeof(*cache)); +} diff --git a/axfer/frame-cache.h b/axfer/frame-cache.h new file mode 100644 index 00000000..7191333 --- /dev/null +++ b/axfer/frame-cache.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// frame-cache.h - maintainer of cache for data frame. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include <alsa/asoundlib.h> + +struct frame_cache { + void *buf; + void *buf_ptr; + + unsigned int remained_count; + + snd_pcm_access_t access; + unsigned int bytes_per_sample; + unsigned int samples_per_frame; + unsigned int frames_per_cache; + + void (*align_frames)(struct frame_cache *cache, + unsigned int consumed_count); +}; + +int frame_cache_init(struct frame_cache *cache, snd_pcm_access_t access, + unsigned int bytes_per_sample, + unsigned int samples_per_frame, + unsigned int frames_per_cache); +void frame_cache_destroy(struct frame_cache *cache); + +static inline unsigned int frame_cache_get_count(struct frame_cache *cache) +{ + return cache->remained_count; +} + +static inline void frame_cache_increase_count(struct frame_cache *cache, + unsigned int frame_count) +{ + cache->remained_count += frame_count; +} + +static inline void frame_cache_reduce(struct frame_cache *cache, + unsigned int consumed_count) +{ + cache->align_frames(cache, consumed_count); +} diff --git a/axfer/xfer-libasound-irq-rw.c b/axfer/xfer-libasound-irq-rw.c new file mode 100644 index 00000000..59634b4 --- /dev/null +++ b/axfer/xfer-libasound-irq-rw.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-rw.c - IRQ-based scheduling model for read/write operation. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" +#include "frame-cache.h" + +struct rw_closure { + snd_pcm_access_t access; + int (*process_frames)(struct libasound_state *state, + snd_pcm_state_t status, unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs); + struct frame_cache cache; +}; + +static int read_frames(struct libasound_state *state, unsigned int *frame_count, + unsigned int avail_count, struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_sframes_t handled_frame_count; + unsigned int consumed_count; + int err; + + // Trim according up to expected frame count. + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&closure->cache)) { + avail_count -= frame_cache_get_count(&closure->cache); + + // Execute write operation according to the shape of buffer. + // These operations automatically start the substream. + if (closure->access == SND_PCM_ACCESS_RW_INTERLEAVED) { + handled_frame_count = snd_pcm_readi(state->handle, + closure->cache.buf_ptr, + avail_count); + } else { + handled_frame_count = snd_pcm_readn(state->handle, + closure->cache.buf_ptr, + avail_count); + } + if (handled_frame_count < 0) { + err = handled_frame_count; + return err; + } + frame_cache_increase_count(&closure->cache, handled_frame_count); + avail_count = frame_cache_get_count(&closure->cache); + } + + // Write out to file descriptors. + consumed_count = avail_count; + err = mapper_context_process_frames(mapper, closure->cache.buf, + &consumed_count, cntrs); + if (err < 0) + return err; + + frame_cache_reduce(&closure->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int r_process_frames_blocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + snd_pcm_uframes_t avail_count; + int err = 0; + + if (status == SND_PCM_STATE_RUNNING) { + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (snd_pcm_uframes_t)avail; + + if (avail_count == 0) { + // Request data frames so that blocking is just + // released. + err = snd_pcm_sw_params_get_avail_min(state->sw_params, + &avail_count); + if (err < 0) + goto error; + } + } else { + // Request data frames so that the PCM substream starts. + snd_pcm_uframes_t frame_count; + err = snd_pcm_sw_params_get_start_threshold(state->sw_params, + &frame_count); + if (err < 0) + goto error; + + avail_count = (unsigned int)frame_count; + } + + err = read_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int write_frames(struct libasound_state *state, + unsigned int *frame_count, unsigned int avail_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_uframes_t consumed_count; + snd_pcm_sframes_t handled_frame_count; + int err; + + // Trim according up to expected frame count. + if (*frame_count < avail_count) + avail_count = *frame_count; + + // Cache required amount of frames. + if (avail_count > frame_cache_get_count(&closure->cache)) { + avail_count -= frame_cache_get_count(&closure->cache); + + // Read frames to transfer. + err = mapper_context_process_frames(mapper, + closure->cache.buf_ptr, &avail_count, cntrs); + if (err < 0) + return err; + frame_cache_increase_count(&closure->cache, avail_count); + avail_count = frame_cache_get_count(&closure->cache); + } + + // Execute write operation according to the shape of buffer. These + // operations automatically start the stream. + consumed_count = avail_count; + if (closure->access == SND_PCM_ACCESS_RW_INTERLEAVED) { + handled_frame_count = snd_pcm_writei(state->handle, + closure->cache.buf, consumed_count); + } else { + handled_frame_count = snd_pcm_writen(state->handle, + closure->cache.buf, consumed_count); + } + if (handled_frame_count < 0) { + err = handled_frame_count; + return err; + } + + consumed_count = handled_frame_count; + frame_cache_reduce(&closure->cache, consumed_count); + + *frame_count = consumed_count; + + return 0; +} + +static int w_process_frames_blocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + unsigned int avail_count; + int err; + + if (status == SND_PCM_STATE_RUNNING) { + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (unsigned int)avail; + + if (avail_count == 0) { + // Fill with data frames so that blocking is just + // released. + snd_pcm_uframes_t avail_min; + err = snd_pcm_sw_params_get_avail_min(state->sw_params, + &avail_min); + if (err < 0) + goto error; + avail_count = (unsigned int)avail_min; + } + } else { + snd_pcm_uframes_t frames_for_start_threshold; + snd_pcm_uframes_t frames_per_period; + + // Fill with data frames so that the PCM substream starts. + err = snd_pcm_sw_params_get_start_threshold(state->sw_params, + &frames_for_start_threshold); + if (err < 0) + goto error; + + // But the above number can be too small and cause XRUN because + // I/O operation is done per period. + err = snd_pcm_hw_params_get_period_size(state->hw_params, + &frames_per_period, NULL); + if (err < 0) + goto error; + + // Use larger one to prevent from both of XRUN and successive + // blocking. + if (frames_for_start_threshold > frames_per_period) + avail_count = (unsigned int)frames_for_start_threshold; + else + avail_count = (unsigned int)frames_per_period; + } + + err = write_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int irq_rw_pre_process(struct libasound_state *state) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_format_t format; + snd_pcm_uframes_t frames_per_buffer; + int bytes_per_sample; + unsigned int samples_per_frame; + int err; + + err = snd_pcm_hw_params_get_format(state->hw_params, &format); + if (err < 0) + return err; + bytes_per_sample = snd_pcm_format_physical_width(format) / 8; + if (bytes_per_sample <= 0) + return -ENXIO; + + err = snd_pcm_hw_params_get_channels(state->hw_params, + &samples_per_frame); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_buffer_size(state->hw_params, + &frames_per_buffer); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(state->hw_params, &closure->access); + if (err < 0) + return err; + + err = frame_cache_init(&closure->cache, closure->access, + bytes_per_sample, samples_per_frame, + frames_per_buffer); + if (err < 0) + return err; + + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + closure->process_frames = r_process_frames_blocking; + else + closure->process_frames = w_process_frames_blocking; + + return 0; +} + +static int irq_rw_process_frames(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct rw_closure *closure = state->private_data; + snd_pcm_state_t status; + + // Need to recover the stream. + status = snd_pcm_state(state->handle); + if (status != SND_PCM_STATE_RUNNING && status != SND_PCM_STATE_PREPARED) + return -EPIPE; + + // NOTE: Actually, status can be shift always. + return closure->process_frames(state, status, frame_count, mapper, cntrs); +} + +static void irq_rw_post_process(struct libasound_state *state) +{ + struct rw_closure *closure = state->private_data; + + frame_cache_destroy(&closure->cache); +} + +const struct xfer_libasound_ops xfer_libasound_irq_rw_ops = { + .pre_process = irq_rw_pre_process, + .process_frames = irq_rw_process_frames, + .post_process = irq_rw_post_process, + .private_size = sizeof(struct rw_closure), +}; diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index 2bca465..bf1b056 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -251,6 +251,12 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return err;
// Assign I/O operation. + if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || + *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { + state->ops = &xfer_libasound_irq_rw_ops; + } else { + return -ENXIO; + } if (state->ops->private_size > 0) { state->private_data = malloc(state->ops->private_size); if (state->private_data == NULL) diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 57fa867..3f3ae6e 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -43,4 +43,6 @@ struct xfer_libasound_ops { unsigned int private_size; };
+extern const struct xfer_libasound_ops xfer_libasound_irq_rw_ops; + #endif
In current aplay, default action is to transfer data frames from/to devices. This commit adds support for this functionality.
Event loop is included in an added file. In the loop, the number of handled data frames is manipulated by an appropriate way. As a result, users can stop data transmission frames by frame.
Unlike aplay, when catching SIGSTP, this application performs to suspend PCM substream. When catching SIGCONT, it performs to resume the PCM substream. The aim of this design is to avoid XRUN state of the PCM substream. If users/developers need to any XRUN-recovery test, it's better to work for the other ways.
Below lines are examples to execute: $ axfer transfer -P -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer -C -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/main.c | 2 +- axfer/subcmd-transfer.c | 438 ++++++++++++++++++++++++++++++++++++++++ axfer/subcmd.h | 2 + 4 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 axfer/subcmd-transfer.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 07c1fb3..386f8e2 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -45,4 +45,5 @@ axfer_SOURCES = \ xfer-libasound.c \ frame-cache.h \ frame-cache.c \ - xfer-libasound-irq-rw.c + xfer-libasound-irq-rw.c \ + subcmd-transfer.c diff --git a/axfer/main.c b/axfer/main.c index f141439..655d1e0 100644 --- a/axfer/main.c +++ b/axfer/main.c @@ -215,7 +215,7 @@ int main(int argc, char *const *argv) decide_subcmd(argc, argv, &subcmd);
if (subcmd == SUBCMD_TRANSFER) - printf("execute 'transfer' subcmd.\n"); + err = subcmd_transfer(argc, argv, direction); else if (subcmd == SUBCMD_LIST) err = subcmd_list(argc, argv, direction); else if (subcmd == SUBCMD_VERSION) diff --git a/axfer/subcmd-transfer.c b/axfer/subcmd-transfer.c new file mode 100644 index 00000000..188589f --- /dev/null +++ b/axfer/subcmd-transfer.c @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// subcmd-transfer.c - operations for transfer sub command. +// +// 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 "subcmd.h" +#include "misc.h" + +#include <signal.h> + +struct context { + struct xfer_context xfer; + struct mapper_context mapper; + struct container_context *cntrs; + unsigned int cntr_count; + + // NOTE: To handling Unix signal. + bool interrupted; + int signal; +}; + +// NOTE: To handling Unix signal. +static struct context *ctx_ptr; + +static void handle_unix_signal_for_finish(int sig) +{ + int i; + + for (i = 0; i < ctx_ptr->cntr_count; ++i) + ctx_ptr->cntrs[i].interrupted = true; + + ctx_ptr->signal = sig; + ctx_ptr->interrupted = true; +} + +static void handle_unix_signal_for_suspend(int sig) +{ + sigset_t curr, prev; + struct sigaction sa = {0}; + + // 1. suspend substream. + xfer_context_pause(&ctx_ptr->xfer, true); + + // 2. Prepare for default handler(SIG_DFL) of SIGTSTP to stop this + // process. + if (sigaction(SIGTSTP, NULL, &sa) < 0) { + fprintf(stderr, "sigaction(2)\n"); + exit(EXIT_FAILURE); + } + if (sa.sa_handler == SIG_ERR) + exit(EXIT_FAILURE); + if (sa.sa_handler == handle_unix_signal_for_suspend) + sa.sa_handler = SIG_DFL; + if (sigaction(SIGTSTP, &sa, NULL) < 0) { + fprintf(stderr, "sigaction(2)\n"); + exit(EXIT_FAILURE); + } + + // Queue SIGTSTP. + raise(SIGTSTP); + + // Release the queued signal from being blocked. This causes an + // additional interrupt for the default handler. + sigemptyset(&curr); + sigaddset(&curr, SIGTSTP); + if (sigprocmask(SIG_UNBLOCK, &curr, &prev) < 0) { + fprintf(stderr, "sigprocmask(2)\n"); + exit(EXIT_FAILURE); + } + + // 3. SIGCONT is cought and rescheduled. Recover blocking status of + // UNIX signals. + if (sigprocmask(SIG_SETMASK, &prev, NULL) < 0) { + fprintf(stderr, "sigprocmask(2)\n"); + exit(EXIT_FAILURE); + } + + // Reconfigure this handler for SIGTSTP, instead of default one. + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sa.sa_handler = handle_unix_signal_for_suspend; + if (sigaction(SIGTSTP, &sa, NULL) < 0) { + fprintf(stderr, "sigaction(2)\n"); + exit(EXIT_FAILURE); + } + + // 4. Continue the PCM substream. + xfer_context_pause(&ctx_ptr->xfer, false); +} + +static int prepare_signal_handler(struct context *ctx) +{ + struct sigaction sa = {0}; + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = handle_unix_signal_for_finish; + + if (sigaction(SIGINT, &sa, NULL) < 0) + return -errno; + if (sigaction(SIGTERM, &sa, NULL) < 0) + return -errno; + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = handle_unix_signal_for_suspend; + if (sigaction(SIGTSTP, &sa, NULL) < 0) + return -errno; + + ctx_ptr = ctx; + + return 0; +} + +static int context_init(struct context *ctx, snd_pcm_stream_t direction, + int argc, char *const *argv) +{ + const char *xfer_type_literal; + enum xfer_type xfer_type; + int i; + + // Decide transfer backend before option parser runs. + xfer_type_literal = NULL; + for (i = 0; i < argc; ++i) { + if (strstr(argv[i], "--xfer-type") != argv[i]) + continue; + xfer_type_literal = argv[i] + 12; + } + if (xfer_type_literal == NULL) { + xfer_type = XFER_TYPE_LIBASOUND; + } else { + xfer_type = xfer_type_from_label(xfer_type_literal); + if (xfer_type == XFER_TYPE_UNSUPPORTED) { + fprintf(stderr, "The '%s' xfer type is not supported\n", + xfer_type_literal); + return -EINVAL; + } + } + + // Initialize transfer. + return xfer_context_init(&ctx->xfer, xfer_type, direction, argc, argv); +} + +static int capture_pre_process(struct context *ctx, snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer, + uint64_t *total_frame_count) +{ + snd_pcm_format_t sample_format = SND_PCM_FORMAT_UNKNOWN; + unsigned int samples_per_frame = 0; + unsigned int frames_per_second = 0; + unsigned int channels; + int i; + int err; + + err = xfer_context_pre_process(&ctx->xfer, &sample_format, + &samples_per_frame, &frames_per_second, + access, frames_per_buffer); + if (err < 0) + return err; + + // Prepare for containers. + ctx->cntrs = calloc(ctx->xfer.path_count, sizeof(*ctx->cntrs)); + if (ctx->cntrs == NULL) + return -ENOMEM; + ctx->cntr_count = ctx->xfer.path_count; + + if (ctx->cntr_count > 1) + channels = 1; + else + channels = samples_per_frame; + + *total_frame_count = 0; + for (i = 0; i < ctx->cntr_count; ++i) { + uint64_t frame_count; + + err = container_builder_init(ctx->cntrs + i, + ctx->xfer.paths[i], + ctx->xfer.cntr_format, + ctx->xfer.verbose > 1); + if (err < 0) + return err; + + err = container_context_pre_process(ctx->cntrs + i, + &sample_format, &channels, + &frames_per_second, + &frame_count); + if (err < 0) + return err; + + if (*total_frame_count == 0) + *total_frame_count = frame_count; + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } + + return 0; +} + +static int playback_pre_process(struct context *ctx, snd_pcm_access_t *access, + snd_pcm_uframes_t *frames_per_buffer, + uint64_t *total_frame_count) +{ + snd_pcm_format_t sample_format = SND_PCM_FORMAT_UNKNOWN; + unsigned int samples_per_frame = 0; + unsigned int frames_per_second = 0; + int i; + int err; + + // Prepare for containers. + ctx->cntrs = calloc(ctx->xfer.path_count, sizeof(*ctx->cntrs)); + if (ctx->cntrs == NULL) + return -ENOMEM; + ctx->cntr_count = ctx->xfer.path_count; + + for (i = 0; i < ctx->cntr_count; ++i) { + snd_pcm_format_t format; + unsigned int channels; + unsigned int rate; + uint64_t frame_count; + + err = container_parser_init(ctx->cntrs + i, + ctx->xfer.paths[i], + ctx->xfer.verbose > 1); + if (err < 0) + return err; + + if (i == 0) { + // For a raw container. + format = ctx->xfer.sample_format; + channels = ctx->xfer.samples_per_frame; + rate = ctx->xfer.frames_per_second; + } else { + format = sample_format; + channels = samples_per_frame; + rate = frames_per_second; + } + + err = container_context_pre_process(ctx->cntrs + i, &format, + &channels, &rate, + &frame_count); + if (err < 0) + return err; + + if (format == SND_PCM_FORMAT_UNKNOWN || channels == 0 || + rate == 0) { + fprintf(stderr, + "Sample format, channels and rate should be " + "indicated for given files.\n"); + return -EINVAL; + } + + if (i == 0) { + sample_format = format; + samples_per_frame = channels; + frames_per_second = rate; + *total_frame_count = frame_count; + } else { + if (format != sample_format) { + fprintf(stderr, + "When using several files, they " + "should include the same sample " + "format.\n"); + return -EINVAL; + } + + // No need to check channels to handle multiple + // containers. + if (rate != frames_per_second) { + fprintf(stderr, + "When using several files, they " + "should include samples at the same " + "sampling rate.\n"); + return -EINVAL; + } + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } + } + + if (ctx->cntr_count > 1) + samples_per_frame = ctx->cntr_count; + + // Configure hardware with these parameters. + return xfer_context_pre_process(&ctx->xfer, &sample_format, + &samples_per_frame, &frames_per_second, + access, frames_per_buffer); +} + +static int context_pre_process(struct context *ctx, snd_pcm_stream_t direction, + uint64_t *total_frame_count) +{ + snd_pcm_access_t access; + snd_pcm_uframes_t frames_per_buffer = 0; + unsigned int bytes_per_sample = 0; + enum mapper_type mapper_type; + int err; + + if (direction == SND_PCM_STREAM_CAPTURE) { + mapper_type = MAPPER_TYPE_DEMUXER; + err = capture_pre_process(ctx, &access, &frames_per_buffer, + total_frame_count); + } else { + mapper_type = MAPPER_TYPE_MUXER; + err = playback_pre_process(ctx, &access, &frames_per_buffer, + total_frame_count); + } + if (err < 0) + return err; + + // Prepare for mapper. + err = mapper_context_init(&ctx->mapper, mapper_type, ctx->cntr_count, + ctx->xfer.verbose > 1); + if (err < 0) + return err; + + bytes_per_sample = + snd_pcm_format_physical_width(ctx->xfer.sample_format) / 8; + if (bytes_per_sample <= 0) + return -ENXIO; + err = mapper_context_pre_process(&ctx->mapper, access, bytes_per_sample, + ctx->xfer.samples_per_frame, + frames_per_buffer, ctx->cntrs); + if (err < 0) + return err; + + return 0; +} + +static int context_process_frames(struct context *ctx, + snd_pcm_stream_t direction, + uint64_t expected_frame_count, + uint64_t *actual_frame_count) +{ + bool verbose = ctx->xfer.verbose > 2; + unsigned int frame_count; + int i; + int err = 0; + + *actual_frame_count = 0; + while (!ctx->interrupted) { + struct container_context *cntr; + + // Tell remains to expected frame count. + frame_count = expected_frame_count - *actual_frame_count; + err = xfer_context_process_frames(&ctx->xfer, &ctx->mapper, + ctx->cntrs, &frame_count); + if (err < 0) { + if (err == -EAGAIN || err == -EINTR) + continue; + break; + } + if (verbose) { + fprintf(stderr, + " handled: %u\n", frame_count); + } + for (i = 0; i < ctx->cntr_count; ++i) { + cntr = &ctx->cntrs[i]; + if (cntr->eof) + break; + } + if (i < ctx->cntr_count) + break; + + *actual_frame_count += frame_count; + if (*actual_frame_count >= expected_frame_count) + break; + } + + return err; +} + +static void context_post_process(struct context *ctx, + uint64_t accumulated_frame_count) +{ + uint64_t total_frame_count; + int i; + + xfer_context_post_process(&ctx->xfer); + + if (ctx->cntrs) { + for (i = 0; i < ctx->cntr_count; ++i) { + container_context_post_process(ctx->cntrs + i, + &total_frame_count); + container_context_destroy(ctx->cntrs + i); + } + free(ctx->cntrs); + } + + mapper_context_post_process(&ctx->mapper); + mapper_context_destroy(&ctx->mapper); +} + +static void context_destroy(struct context *ctx) +{ + xfer_context_destroy(&ctx->xfer); +} + +int subcmd_transfer(int argc, char *const *argv, snd_pcm_stream_t direction) +{ + struct context ctx = {0}; + uint64_t expected_frame_count = 0; + uint64_t actual_frame_count = 0; + int err = 0; + + // Renewed command system. + if (argc > 2 && !strcmp(argv[1], "transfer")) { + // Go ahead to parse file paths properly. + --argc; + ++argv; + } + + err = prepare_signal_handler(&ctx); + if (err < 0) + return err; + + err = context_init(&ctx, direction, argc, argv); + if (err < 0) + goto end; + if (ctx.xfer.help) + goto end; + + err = context_pre_process(&ctx, direction, &expected_frame_count); + if (err < 0) + goto end; + + err = context_process_frames(&ctx, direction, expected_frame_count, + &actual_frame_count); +end: + context_post_process(&ctx, actual_frame_count); + + context_destroy(&ctx); + + return err; +} diff --git a/axfer/subcmd.h b/axfer/subcmd.h index f78b766..5ffe1e4 100644 --- a/axfer/subcmd.h +++ b/axfer/subcmd.h @@ -13,4 +13,6 @@
int subcmd_list(int argc, char *const *argv, snd_pcm_stream_t direction);
+int subcmd_transfer(int argc, char *const *argv, snd_pcm_stream_t direction); + #endif
In current aplay, some informative output is available as a default. This can be suppressed by a quiet option. This commit adds support for it.
An original aplay implementation has no effect of this option in a case to handle multiple files. However, in a point of usability, this commit support this case.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/subcmd-transfer.c | 27 +++++++++++++++++++++++++++ axfer/xfer-options.c | 5 ++++- axfer/xfer.h | 1 + 3 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/axfer/subcmd-transfer.c b/axfer/subcmd-transfer.c index 188589f..a165a93 100644 --- a/axfer/subcmd-transfer.c +++ b/axfer/subcmd-transfer.c @@ -340,6 +340,21 @@ static int context_process_frames(struct context *ctx, int i; int err = 0;
+ if (!ctx->xfer.quiet) { + fprintf(stderr, + "%s: Format '%s', Rate %u Hz, Channels ", + snd_pcm_stream_name(direction), + snd_pcm_format_description(ctx->xfer.sample_format), + ctx->xfer.frames_per_second); + if (ctx->xfer.samples_per_frame == 1) + fprintf(stderr, "'monaural'"); + else if (ctx->xfer.samples_per_frame == 2) + fprintf(stderr, "'Stereo'"); + else + fprintf(stderr, "%u", ctx->xfer.samples_per_frame); + fprintf(stderr, "\n"); + } + *actual_frame_count = 0; while (!ctx->interrupted) { struct container_context *cntr; @@ -370,6 +385,18 @@ static int context_process_frames(struct context *ctx, break; }
+ if (!ctx->xfer.quiet) { + fprintf(stderr, + "%s: Expected %lu frames, Actual %lu frames\n", + snd_pcm_stream_name(direction), expected_frame_count, + *actual_frame_count); + if (ctx->interrupted) { + fprintf(stderr, "Aborted by signal: %s\n", + strsignal(ctx->signal)); + return 0; + } + } + return err; }
diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c index fb71244..7790ea9 100644 --- a/axfer/xfer-options.c +++ b/axfer/xfer-options.c @@ -227,7 +227,7 @@ int xfer_options_parse_args(struct xfer_context *xfer, const struct xfer_data *data, int argc, char *const *argv) { - static const char *short_opts = "CPhvf:c:r:t:I"; + static const char *short_opts = "CPhvqf:c:r:t:I"; static const struct option long_opts[] = { // For generic purposes. {"capture", 0, 0, 'C'}, @@ -235,6 +235,7 @@ int xfer_options_parse_args(struct xfer_context *xfer, {"xfer-type", 1, 0, OPT_XFER_TYPE}, {"help", 0, 0, 'h'}, {"verbose", 0, 0, 'v'}, + {"quiet", 0, 0, 'q'}, // For transfer backend. {"format", 1, 0, 'f'}, {"channels", 1, 0, 'c'}, @@ -289,6 +290,8 @@ int xfer_options_parse_args(struct xfer_context *xfer, xfer->help = true; else if (key == 'v') ++xfer->verbose; + else if (key == 'q') + xfer->quiet = true; else if (key == 'f') xfer->sample_format_literal = arg_duplicate_string(optarg, &err); else if (key == 'c') diff --git a/axfer/xfer.h b/axfer/xfer.h index df43d1c..0ed84e4 100644 --- a/axfer/xfer.h +++ b/axfer/xfer.h @@ -33,6 +33,7 @@ struct xfer_context { unsigned int frames_per_second; unsigned int samples_per_frame; bool help:1; + bool quiet:1; bool multiple_cntrs:1; // For mapper.
snd_pcm_format_t sample_format;
In ALSA PCM interface, before configuring hardware actually, applications can request available set of hardware parameters for runtime of PCM substream. The set of parameters are represented and delivered by a structure.
In alsa-lib PCM API, the above design is abstracted by a series of snd_pcm_hw_params_xxx() functions. An actual layout of the structure is hidden from applications by an opaque pointer.
In aplay, '--dump-hw-params' option is for this purpose. With this option, the command output available set of the hardware parameters.
This commit adds support for the option. Unlike aplay, this commit takes this program to finish after dumping the parameters for simplicity of usage.
I note that all of combinations in the set are not necessarily available when the PCM substream includes dependencies of parameters described by constraints and rules.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/subcmd-transfer.c | 2 +- axfer/xfer-libasound.c | 9 +++++++++ axfer/xfer-options.c | 5 +++++ axfer/xfer.h | 1 + 4 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/axfer/subcmd-transfer.c b/axfer/subcmd-transfer.c index a165a93..36817f3 100644 --- a/axfer/subcmd-transfer.c +++ b/axfer/subcmd-transfer.c @@ -447,7 +447,7 @@ int subcmd_transfer(int argc, char *const *argv, snd_pcm_stream_t direction) err = context_init(&ctx, direction, argc, argv); if (err < 0) goto end; - if (ctx.xfer.help) + if (ctx.xfer.help || ctx.xfer.dump_hw_params) goto end;
err = context_pre_process(&ctx, direction, &expected_frame_count); diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index bf1b056..60e9aab 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -99,6 +99,15 @@ static int open_handle(struct xfer_context *xfer)
// TODO: Applying NO_PERIOD_WAKEUP should be done here.
+ if (xfer->dump_hw_params) { + logging(state, "Available HW Params of node: %s\n", + snd_pcm_name(state->handle)); + snd_pcm_hw_params_dump(state->hw_params, state->log); + // TODO: there're more parameters which are not dumped by + // alsa-lib. + return 0; + } + return set_access_hw_param(state); }
diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c index 7790ea9..21fc6bb 100644 --- a/axfer/xfer-options.c +++ b/axfer/xfer-options.c @@ -16,6 +16,7 @@ enum no_short_opts { // 128 or later belong to non us-ascii character set. OPT_XFER_TYPE = 128, + OPT_DUMP_HW_PARAMS, };
static int allocate_paths(struct xfer_context *xfer, char *const *paths, @@ -244,6 +245,8 @@ int xfer_options_parse_args(struct xfer_context *xfer, {"file-type", 1, 0, 't'}, // For mapper. {"separate-channels", 0, 0, 'I'}, + // For debugging. + {"dump-hw-params", 0, 0, OPT_DUMP_HW_PARAMS}, }; char *s_opts; struct option *l_opts; @@ -302,6 +305,8 @@ int xfer_options_parse_args(struct xfer_context *xfer, xfer->cntr_format_literal = arg_duplicate_string(optarg, &err); else if (key == 'I') xfer->multiple_cntrs = true; + else if (key == OPT_DUMP_HW_PARAMS) + xfer->dump_hw_params = true; else if (key == '?') return -EINVAL; else { diff --git a/axfer/xfer.h b/axfer/xfer.h index 0ed84e4..a234851 100644 --- a/axfer/xfer.h +++ b/axfer/xfer.h @@ -34,6 +34,7 @@ struct xfer_context { unsigned int samples_per_frame; bool help:1; bool quiet:1; + bool dump_hw_params:1; bool multiple_cntrs:1; // For mapper.
snd_pcm_format_t sample_format;
In aplay, some options are available to stop data transmission by frame unit. This commit adds support for the options below: * --duration (-d) * For duration seconds. The number of data frames transferred in this * runtime is calculated by this value and sampling rate. * --samples (-s) * For the number of data frames to handle in this runtime.
An original aplay has a similar option; '--max-file-time'. This option is used for capture data transmission to switch file to write data frame up to maximum number of frames which container format supports, instead of terminating. However, this may brings complicated file handling to this program. To reduce maintaining cost, this option is obsoleted. Additionally, a handler for SIGUSR1 Unix signal has similar feature to switch the file. For the same reason, the handler is also obsoleted.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/subcmd-transfer.c | 2 ++ axfer/xfer-options.c | 38 ++++++++++++++++++++++++++++++++++++-- axfer/xfer.h | 4 ++++ 3 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/axfer/subcmd-transfer.c b/axfer/subcmd-transfer.c index 36817f3..d5f16cb 100644 --- a/axfer/subcmd-transfer.c +++ b/axfer/subcmd-transfer.c @@ -327,6 +327,8 @@ static int context_pre_process(struct context *ctx, snd_pcm_stream_t direction, if (err < 0) return err;
+ xfer_options_calculate_duration(&ctx->xfer, total_frame_count); + return 0; }
diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c index 21fc6bb..9233647 100644 --- a/axfer/xfer-options.c +++ b/axfer/xfer-options.c @@ -17,6 +17,8 @@ enum no_short_opts { // 128 or later belong to non us-ascii character set. OPT_XFER_TYPE = 128, OPT_DUMP_HW_PARAMS, + // Obsoleted. + OPT_MAX_FILE_TIME, };
static int allocate_paths(struct xfer_context *xfer, char *const *paths, @@ -228,7 +230,7 @@ int xfer_options_parse_args(struct xfer_context *xfer, const struct xfer_data *data, int argc, char *const *argv) { - static const char *short_opts = "CPhvqf:c:r:t:I"; + static const char *short_opts = "CPhvqd:s:f:c:r:t:I"; static const struct option long_opts[] = { // For generic purposes. {"capture", 0, 0, 'C'}, @@ -237,6 +239,8 @@ int xfer_options_parse_args(struct xfer_context *xfer, {"help", 0, 0, 'h'}, {"verbose", 0, 0, 'v'}, {"quiet", 0, 0, 'q'}, + {"duration", 1, 0, 'd'}, + {"samples", 1, 0, 's'}, // For transfer backend. {"format", 1, 0, 'f'}, {"channels", 1, 0, 'c'}, @@ -247,6 +251,8 @@ int xfer_options_parse_args(struct xfer_context *xfer, {"separate-channels", 0, 0, 'I'}, // For debugging. {"dump-hw-params", 0, 0, OPT_DUMP_HW_PARAMS}, + // Obsoleted. + {"max-file-time", 1, 0, OPT_MAX_FILE_TIME}, }; char *s_opts; struct option *l_opts; @@ -295,6 +301,10 @@ int xfer_options_parse_args(struct xfer_context *xfer, ++xfer->verbose; else if (key == 'q') xfer->quiet = true; + else if (key == 'd') + xfer->duration_seconds = arg_parse_decimal_num(optarg, &err); + else if (key == 's') + xfer->duration_frames = arg_parse_decimal_num(optarg, &err); else if (key == 'f') xfer->sample_format_literal = arg_duplicate_string(optarg, &err); else if (key == 'c') @@ -309,7 +319,13 @@ int xfer_options_parse_args(struct xfer_context *xfer, xfer->dump_hw_params = true; else if (key == '?') return -EINVAL; - else { + else if (key == OPT_MAX_FILE_TIME) { + fprintf(stderr, + "An option '--%s' is obsoleted and has no " + "effect.\n", + l_opts[l_index].name); + err = -EINVAL; + } else { err = xfer->ops->parse_opt(xfer, key, optarg); if (err < 0 && err != -ENXIO) break; @@ -326,6 +342,24 @@ int xfer_options_parse_args(struct xfer_context *xfer, return validate_options(xfer); }
+void xfer_options_calculate_duration(struct xfer_context *xfer, + uint64_t *total_frame_count) +{ + uint64_t frame_count; + + if (xfer->duration_seconds > 0) { + frame_count = xfer->duration_seconds * xfer->frames_per_second; + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } + + if (xfer->duration_frames > 0) { + frame_count = xfer->duration_frames; + if (frame_count < *total_frame_count) + *total_frame_count = frame_count; + } +} + static const char *const allowed_duplication[] = { "/dev/null", "/dev/zero", diff --git a/axfer/xfer.h b/axfer/xfer.h index a234851..21ab85d 100644 --- a/axfer/xfer.h +++ b/axfer/xfer.h @@ -30,6 +30,8 @@ struct xfer_context { char *sample_format_literal; char *cntr_format_literal; unsigned int verbose; + unsigned int duration_seconds; + unsigned int duration_frames; unsigned int frames_per_second; unsigned int samples_per_frame; bool help:1; @@ -68,6 +70,8 @@ int xfer_options_parse_args(struct xfer_context *xfer, const struct xfer_data *data, int argc, char *const *argv); int xfer_options_fixup_paths(struct xfer_context *xfer); +void xfer_options_calculate_duration(struct xfer_context *xfer, + uint64_t *total_frame_count);
// For internal use in 'xfer' module.
In aplay, '--fatal-errors' option has an effect to give up recovery of PCM substream from XRUN state. This commit adds support for this option.
In original implementation, this option brings program abort. This seems to generate core dump of process VMA. However, typically, XRUN comes from timing mismatch between hardware and application, therefore core dump has less helpful. This commit finishes this program in usual way with this option at XRUN.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound.c | 11 ++++++++++- axfer/xfer-libasound.h | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index 60e9aab..77c142e 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -9,9 +9,16 @@ #include "xfer-libasound.h" #include "misc.h"
+enum no_short_opts { + // 200 or later belong to non us-ascii character set. + OPT_FATAL_ERRORS = 200, +}; + #define S_OPTS "D:" static const struct option l_opts[] = { {"device", 1, 0, 'D'}, + // For debugging. + {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, };
static int xfer_libasound_init(struct xfer_context *xfer, @@ -39,6 +46,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key,
if (key == 'D') state->node_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_FATAL_ERRORS) + state->finish_at_xrun = true; else err = -ENXIO;
@@ -305,7 +314,7 @@ static int xfer_libasound_process_frames(struct xfer_context *xfer, if (err < 0) { if (err == -EAGAIN) return err; - if (err == -EPIPE) { + if (err == -EPIPE && !state->finish_at_xrun) { // Recover the stream and continue processing // immediately. In this program -EPIPE comes from // libasound implementation instead of file I/O. diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 3f3ae6e..270288d 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -29,6 +29,8 @@ struct libasound_state { bool verbose;
char *node_literal; + + bool finish_at_xrun:1; };
// For internal use in 'libasound' module.
In alsa-lib PCM API, snd_pcm_read[i|n]() and snd_pcm_write[i|n] can be used with non-blocking mode. This is available when SND_PCM_NONBLOCK is used as 'mode' argument for a call of snd_pcm_open().
This commit adds support this type of operation. To reduce CPU usage, this commit uses 'snd_pcm_wait()' to wait for event notification.
Below lines are examples to execute: $ axfer transfer -N -P -d 2 -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer -N -C -d 2 -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound-irq-rw.c | 102 ++++++++++++++++++++++++++++++++-- axfer/xfer-libasound.c | 16 +++++- axfer/xfer-libasound.h | 1 + 3 files changed, 113 insertions(+), 6 deletions(-)
diff --git a/axfer/xfer-libasound-irq-rw.c b/axfer/xfer-libasound-irq-rw.c index 59634b4..f05ac4b 100644 --- a/axfer/xfer-libasound-irq-rw.c +++ b/axfer/xfer-libasound-irq-rw.c @@ -117,6 +117,51 @@ error: return err; }
+static int r_process_frames_nonblocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + snd_pcm_uframes_t avail_count; + int err = 0; + + if (status != SND_PCM_STATE_RUNNING) { + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + } + + // Wait for hardware IRQ when no available space. + err = snd_pcm_wait(state->handle, -1); + if (err < 0) + goto error; + + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (snd_pcm_uframes_t)avail; + + if (avail_count == 0) { + // Let's go to a next iteration. + err = 0; + goto error; + } + + err = read_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + static int write_frames(struct libasound_state *state, unsigned int *frame_count, unsigned int avail_count, struct mapper_context *mapper, @@ -231,6 +276,48 @@ error: return err; }
+static int w_process_frames_nonblocking(struct libasound_state *state, + snd_pcm_state_t status, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + snd_pcm_sframes_t avail; + unsigned int avail_count; + int err; + + // Wait for hardware IRQ when no left space. + err = snd_pcm_wait(state->handle, -1); + if (err < 0) + goto error; + + // Check available space on the buffer. + avail = snd_pcm_avail(state->handle); + if (avail < 0) { + err = avail; + goto error; + } + avail_count = (unsigned int)avail; + + if (avail_count == 0) { + // Let's go to a next iteration. + err = 0; + goto error; + } + + err = write_frames(state, frame_count, avail_count, mapper, cntrs); + if (err < 0) + goto error; + + // NOTE: The substream starts automatically when the accumulated number + // of queued data frame exceeds start_threshold. + + return 0; +error: + *frame_count = 0; + return err; +} + static int irq_rw_pre_process(struct libasound_state *state) { struct rw_closure *closure = state->private_data; @@ -267,10 +354,17 @@ static int irq_rw_pre_process(struct libasound_state *state) if (err < 0) return err;
- if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) - closure->process_frames = r_process_frames_blocking; - else - closure->process_frames = w_process_frames_blocking; + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) { + if (state->nonblock) + closure->process_frames = r_process_frames_nonblocking; + else + closure->process_frames = r_process_frames_blocking; + } else { + if (state->nonblock) + closure->process_frames = w_process_frames_nonblocking; + else + closure->process_frames = w_process_frames_blocking; + }
return 0; } diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index 77c142e..cb26b69 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -14,9 +14,10 @@ enum no_short_opts { OPT_FATAL_ERRORS = 200, };
-#define S_OPTS "D:" +#define S_OPTS "D:N" static const struct option l_opts[] = { {"device", 1, 0, 'D'}, + {"nonblock", 0, 0, 'N'}, // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, }; @@ -46,6 +47,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key,
if (key == 'D') state->node_literal = arg_duplicate_string(optarg, &err); + else if (key == 'N') + state->nonblock = true; else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; else @@ -91,10 +94,14 @@ static int set_access_hw_param(struct libasound_state *state) static int open_handle(struct xfer_context *xfer) { struct libasound_state *state = xfer->private_data; + int mode = 0; int err;
+ if (state->nonblock) + mode |= SND_PCM_NONBLOCK; + err = snd_pcm_open(&state->handle, state->node_literal, xfer->direction, - 0); + mode); if (err < 0) { logging(state, "Fail to open libasound PCM node for %s: %s\n", snd_pcm_stream_name(xfer->direction), @@ -378,7 +385,12 @@ static void xfer_libasound_post_process(struct xfer_context *xfer) logging(state, "snd_pcm_drop(): %s\n", snd_strerror(err)); } else { + // TODO: this is a bug in kernel land. + if (state->nonblock) + snd_pcm_nonblock(state->handle, 0); err = snd_pcm_drain(state->handle); + if (state->nonblock) + snd_pcm_nonblock(state->handle, 1); if (err < 0) logging(state, "snd_pcm_drain(): %s\n", snd_strerror(err)); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 270288d..6656aeb 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -31,6 +31,7 @@ struct libasound_state { char *node_literal;
bool finish_at_xrun:1; + bool nonblock:1; };
// For internal use in 'libasound' module.
In alsa-lib PCM API, data frames can be handled in mapped page frame, instead of calling any system calls.
This commit support for this type of operation. To reduce CPU usage, this commit uses 'snd_pcm_wait()' to wait for event notification.
Below lines are examples to execute: $ axfer transfer -M -P -d 2 -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer -M -C -d 2 -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/xfer-libasound-irq-mmap.c | 268 ++++++++++++++++++++++++++++++++ axfer/xfer-libasound.c | 27 +++- axfer/xfer-libasound.h | 4 + 4 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 axfer/xfer-libasound-irq-mmap.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 386f8e2..960811e 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -46,4 +46,5 @@ axfer_SOURCES = \ frame-cache.h \ frame-cache.c \ xfer-libasound-irq-rw.c \ - subcmd-transfer.c + subcmd-transfer.c \ + xfer-libasound-irq-mmap.c diff --git a/axfer/xfer-libasound-irq-mmap.c b/axfer/xfer-libasound-irq-mmap.c new file mode 100644 index 00000000..87ef7e0 --- /dev/null +++ b/axfer/xfer-libasound-irq-mmap.c @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-mmap.c - IRQ-based scheduling model for mmap operation. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" + +struct map_layout { + snd_pcm_status_t *status; + + char **vector; + unsigned int samples_per_frame; +}; + +static int irq_mmap_pre_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_access_t access; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail = 0; + int i; + int err; + + err = snd_pcm_status_malloc(&layout->status); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(state->hw_params, &access); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_channels(state->hw_params, + &layout->samples_per_frame); + if (err < 0) + return err; + + if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + layout->vector = calloc(layout->samples_per_frame, + sizeof(*layout->vector)); + if (layout->vector == NULL) + return err; + } + + if (state->verbose) { + const snd_pcm_channel_area_t *areas; + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail); + if (err < 0) + return err; + + logging(state, "attributes for mapped page frame:\n"); + for (i = 0; i < layout->samples_per_frame; ++i) { + const snd_pcm_channel_area_t *area = areas + i; + + logging(state, " sample number: %d\n", i); + logging(state, " address: %p\n", area->addr); + logging(state, " bits for offset: %u\n", area->first); + logging(state, " bits/frame: %u\n", area->step); + } + logging(state, "\n"); + } + + return 0; +} + +static int irq_mmap_process_frames(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail; + unsigned int avail_count; + void *frame_buf; + snd_pcm_sframes_t consumed_count; + int err; + + // Wait for hardware IRQ when no avail space in buffer. + err = snd_pcm_wait(state->handle, -1); + if (err < 0) + return err; + + // Sync cache in user space to data in kernel space to calculate avail + // frames according to the latest positions on PCM buffer. + // + // This has an additional advantage to handle libasound PCM plugins. + // Most of libasound PCM plugins perform resampling in .avail_update() + // callback for capture PCM substream, then update positions on buffer. + // + // MEMO: either snd_pcm_avail_update() and snd_pcm_mmap_begin() can + // return the same number of available frames. + avail = snd_pcm_avail_update(state->handle); + if (avail < 0) + return (int)avail; + if (*frame_count < avail) + avail = *frame_count; + + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, &avail); + if (err < 0) + return err; + + // Trim according up to expected frame count. + if (*frame_count < avail) + avail_count = *frame_count; + else + avail_count = (unsigned int)avail; + + // TODO: Perhaps, the complex layout can be supported as a variation of + // vector type. However, there's no driver with this layout. + if (layout->vector == NULL) { + frame_buf = areas[0].addr; + frame_buf += snd_pcm_frames_to_bytes(state->handle, + frame_offset); + } else { + int i; + for (i = 0; i < layout->samples_per_frame; ++i) { + layout->vector[i] = areas[i].addr; + layout->vector[i] += snd_pcm_samples_to_bytes( + state->handle, frame_offset); + } + frame_buf = layout->vector; + } + + err = mapper_context_process_frames(mapper, frame_buf, &avail_count, + cntrs); + if (err < 0) + return err; + if (avail_count == 0) { + *frame_count = 0; + return 0; + } + + consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset, + avail_count); + if (consumed_count < 0) + return (int)consumed_count; + if (consumed_count != avail_count) + logging(state, "A bug of access plugin for this PCM node.\n"); + + *frame_count = consumed_count; + + return 0; +} + +static int irq_mmap_r_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // To querying current status of hardware, we need to care of + // synchronization between 3 levels: + // 1. status to actual hardware by driver. + // 2. status data in kernel space. + // 3. status data in user space. + // + // Kernel driver query 1 and sync 2, according to requests of some + // ioctl(2) commands. For synchronization between 2 and 3, ALSA PCM core + // supports mmap(2) operation on cache coherent architectures, some + // ioctl(2) commands on cache incoherent architecture. In usage of the + // former mechanism, we need to care of concurrent access by IRQ context + // and process context to the mapped page frame. + // In a call of ioctl(2) with SNDRV_PCM_IOCTL_STATUS and + // SNDRV_PCM_IOCTL_STATUS_EXT, the above care is needless because + // mapped page frame is unused regardless of architectures in a point of + // cache coherency. + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + // For capture direction, need to start stream explicitly. + if (s != SND_PCM_STATE_RUNNING) { + if (s != SND_PCM_STATE_PREPARED) { + err = -EPIPE; + goto error; + } + + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + } + + err = irq_mmap_process_frames(state, frame_count, mapper, cntrs); + if (err < 0) + goto error; + + return 0; +error: + *frame_count = 0; + return err; +} + +static int irq_mmap_w_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // Read my comment in 'irq_mmap_r_process_frames(). + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + err = irq_mmap_process_frames(state, frame_count, mapper, cntrs); + if (err < 0) + goto error; + + // Need to start playback stream explicitly + if (s != SND_PCM_STATE_RUNNING) { + if (s != SND_PCM_STATE_PREPARED) { + err = -EPIPE; + goto error; + } + + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + } + + return 0; +error: + *frame_count = 0; + return err; +} + +static void irq_mmap_post_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + + if (layout->status) + snd_pcm_status_free(layout->status); + layout->status = NULL; + + free(layout->vector); + layout->vector = NULL; +} + +const struct xfer_libasound_ops xfer_libasound_irq_mmap_w_ops = { + .pre_process = irq_mmap_pre_process, + .process_frames = irq_mmap_w_process_frames, + .post_process = irq_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; + +const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops = { + .pre_process = irq_mmap_pre_process, + .process_frames = irq_mmap_r_process_frames, + .post_process = irq_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index cb26b69..c2e1282 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -14,10 +14,11 @@ enum no_short_opts { OPT_FATAL_ERRORS = 200, };
-#define S_OPTS "D:N" +#define S_OPTS "D:NM" static const struct option l_opts[] = { {"device", 1, 0, 'D'}, {"nonblock", 0, 0, 'N'}, + {"mmap", 0, 0, 'M'}, // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, }; @@ -49,6 +50,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->node_literal = arg_duplicate_string(optarg, &err); else if (key == 'N') state->nonblock = true; + else if (key == 'M') + state->mmap = true; else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; else @@ -70,6 +73,13 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) return -ENOMEM; }
+ if (state->mmap && state->nonblock) { + fprintf(stderr, + "An option for mmap operation should not be used with " + "nonblocking option.\n"); + return -EINVAL; + } + return err; }
@@ -82,8 +92,13 @@ static int set_access_hw_param(struct libasound_state *state) if (err < 0) return err; snd_pcm_access_mask_none(mask); - snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_RW_INTERLEAVED); - snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_RW_NONINTERLEAVED); + if (state->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(state->handle, state->hw_params, mask); snd_pcm_access_mask_free(mask); @@ -279,6 +294,12 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { state->ops = &xfer_libasound_irq_rw_ops; + } else if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_irq_mmap_r_ops; + else + state->ops = &xfer_libasound_irq_mmap_w_ops; } else { return -ENXIO; } diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 6656aeb..550b1c2 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -32,6 +32,7 @@ struct libasound_state {
bool finish_at_xrun:1; bool nonblock:1; + bool mmap:1; };
// For internal use in 'libasound' module. @@ -48,4 +49,7 @@ struct xfer_libasound_ops {
extern const struct xfer_libasound_ops xfer_libasound_irq_rw_ops;
+extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops; +extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_w_ops; + #endif
In aplay, '--test-nowait' is used to suppress calls of snd_pcm_wait() when I/O operations return -EAGAIN or process truncated number of data frames. This seems to be for debugging purpose. In this program, this option is equivalent to suppress event waiting.
This commit adds support for this option.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound-irq-mmap.c | 10 ++++++---- axfer/xfer-libasound-irq-rw.c | 20 ++++++++++++-------- axfer/xfer-libasound.c | 16 ++++++++++++++++ axfer/xfer-libasound.h | 3 +++ 4 files changed, 37 insertions(+), 12 deletions(-)
diff --git a/axfer/xfer-libasound-irq-mmap.c b/axfer/xfer-libasound-irq-mmap.c index 87ef7e0..18f6dfe 100644 --- a/axfer/xfer-libasound-irq-mmap.c +++ b/axfer/xfer-libasound-irq-mmap.c @@ -81,10 +81,12 @@ static int irq_mmap_process_frames(struct libasound_state *state, snd_pcm_sframes_t consumed_count; int err;
- // Wait for hardware IRQ when no avail space in buffer. - err = snd_pcm_wait(state->handle, -1); - if (err < 0) - return err; + if (state->use_waiter) { + // Wait for hardware IRQ when no avail space in buffer. + err = snd_pcm_wait(state->handle, -1); + if (err < 0) + return err; + }
// Sync cache in user space to data in kernel space to calculate avail // frames according to the latest positions on PCM buffer. diff --git a/axfer/xfer-libasound-irq-rw.c b/axfer/xfer-libasound-irq-rw.c index f05ac4b..625c095 100644 --- a/axfer/xfer-libasound-irq-rw.c +++ b/axfer/xfer-libasound-irq-rw.c @@ -133,10 +133,12 @@ static int r_process_frames_nonblocking(struct libasound_state *state, goto error; }
- // Wait for hardware IRQ when no available space. - err = snd_pcm_wait(state->handle, -1); - if (err < 0) - goto error; + if (state->use_waiter) { + // Wait for hardware IRQ when no available space. + err = snd_pcm_wait(state->handle, -1); + if (err < 0) + goto error; + }
// Check available space on the buffer. avail = snd_pcm_avail(state->handle); @@ -286,10 +288,12 @@ static int w_process_frames_nonblocking(struct libasound_state *state, unsigned int avail_count; int err;
- // Wait for hardware IRQ when no left space. - err = snd_pcm_wait(state->handle, -1); - if (err < 0) - goto error; + if (state->use_waiter) { + // Wait for hardware IRQ when no left space. + err = snd_pcm_wait(state->handle, -1); + if (err < 0) + goto error; + }
// Check available space on the buffer. avail = snd_pcm_avail(state->handle); diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index c2e1282..61ae115 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -12,6 +12,7 @@ enum no_short_opts { // 200 or later belong to non us-ascii character set. OPT_FATAL_ERRORS = 200, + OPT_TEST_NOWAIT, };
#define S_OPTS "D:NM" @@ -21,6 +22,7 @@ static const struct option l_opts[] = { {"mmap", 0, 0, 'M'}, // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, + {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, };
static int xfer_libasound_init(struct xfer_context *xfer, @@ -54,6 +56,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->mmap = true; else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; + else if (key == OPT_TEST_NOWAIT) + state->test_nowait = true; else err = -ENXIO;
@@ -80,6 +84,15 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) return -EINVAL; }
+ if (state->test_nowait) { + if (!state->nonblock && !state->mmap) { + fprintf(stderr, + "An option for nowait test should be used with " + "nonblock or mmap options.\n"); + return -EINVAL; + } + } + return err; }
@@ -124,6 +137,9 @@ static int open_handle(struct xfer_context *xfer) return err; }
+ if ((state->nonblock || state->mmap) && !state->test_nowait) + state->use_waiter = true; + err = snd_pcm_hw_params_any(state->handle, state->hw_params); if (err < 0) return err; diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 550b1c2..f3ce73f 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -33,6 +33,9 @@ struct libasound_state { bool finish_at_xrun:1; bool nonblock:1; bool mmap:1; + bool test_nowait:1; + + bool use_waiter:1; };
// For internal use in 'libasound' module.
In ALSA PCM interface, two parameters are used for size of intermediate buffer for data frames; period size and buffer size. Actual effects of these sizes differs depending on hardware, but basically the size of period is used for intervals of hardware interrupts and the size of buffer is used to maintain the intermediate buffer as ring buffer. These parameters can be configured as a part of hardware parameters by data frame unit or micro second. PCM API in alsa-lib also includes helper functions to configure them by the two units.
This commit adds support for options to the parameters by both units. When no options are given, default values are applied according to current aplay; available maximum size of buffer up to 500msec, a quarter of the size of buffer for period size. However, these calculation should be reconsidered somehow.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound.c | 122 +++++++++++++++++++++++++++++++++++++++-- axfer/xfer-libasound.h | 5 ++ axfer/xfer-options.c | 2 + 3 files changed, 125 insertions(+), 4 deletions(-)
diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index 61ae115..a23021e 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -11,15 +11,21 @@
enum no_short_opts { // 200 or later belong to non us-ascii character set. - OPT_FATAL_ERRORS = 200, + OPT_PERIOD_SIZE = 200, + OPT_BUFFER_SIZE, + OPT_FATAL_ERRORS, OPT_TEST_NOWAIT, };
-#define S_OPTS "D:NM" +#define S_OPTS "D:NMF:B:" static const struct option l_opts[] = { {"device", 1, 0, 'D'}, {"nonblock", 0, 0, 'N'}, {"mmap", 0, 0, 'M'}, + {"period-time", 1, 0, 'F'}, + {"buffer-time", 1, 0, 'B'}, + {"period-size", 1, 0, OPT_PERIOD_SIZE}, + {"buffer-size", 1, 0, OPT_BUFFER_SIZE}, // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, @@ -54,6 +60,14 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->nonblock = true; else if (key == 'M') state->mmap = true; + else if (key == 'F') + state->msec_per_period = arg_parse_decimal_num(optarg, &err); + else if (key == 'B') + state->msec_per_buffer = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_PERIOD_SIZE) + state->frames_per_period = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_BUFFER_SIZE) + state->frames_per_buffer = arg_parse_decimal_num(optarg, &err); else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; else if (key == OPT_TEST_NOWAIT) @@ -93,6 +107,20 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) } }
+ if (state->msec_per_period > 0 && state->msec_per_buffer > 0) { + if (state->msec_per_period > state->msec_per_buffer) { + state->msec_per_period = state->msec_per_buffer; + state->msec_per_buffer = 0; + } + } + + if (state->frames_per_period > 0 && state->frames_per_buffer > 0) { + if (state->frames_per_period > state->frames_per_buffer) { + state->frames_per_period = state->frames_per_buffer; + state->frames_per_buffer = 0; + } + } + return err; }
@@ -161,7 +189,11 @@ static int open_handle(struct xfer_context *xfer) static int configure_hw_params(struct libasound_state *state, snd_pcm_format_t format, unsigned int samples_per_frame, - unsigned int frames_per_second) + unsigned int frames_per_second, + unsigned int msec_per_period, + unsigned int msec_per_buffer, + snd_pcm_uframes_t frames_per_period, + snd_pcm_uframes_t frames_per_buffer) { int err;
@@ -232,6 +264,84 @@ static int configure_hw_params(struct libasound_state *state, return err; }
+ // Keep one of 'frames_per_buffer' and 'msec_per_buffer'. + if (frames_per_buffer == 0) { + if (msec_per_buffer == 0) { + err = snd_pcm_hw_params_get_buffer_time_max( + state->hw_params, &msec_per_buffer, NULL); + if (err < 0) { + logging(state, + "The maximum msec per buffer is not " + "available.\n"); + return err; + } + if (msec_per_buffer > 500000) + msec_per_buffer = 500000; + } + } else if (msec_per_buffer > 0) { + uint64_t msec; + + msec = 1000000 * frames_per_buffer / frames_per_second; + if (msec < msec_per_buffer) + msec_per_buffer = 0; + } + + // Keep one of 'frames_per_period' and 'msec_per_period'. + if (frames_per_period == 0) { + if (msec_per_period == 0) { + if (msec_per_buffer > 0) + msec_per_period = msec_per_buffer / 4; + else + frames_per_period = frames_per_buffer / 4; + } + } else if (msec_per_period > 0) { + uint64_t msec; + + msec = 1000000 * frames_per_period / frames_per_second; + if (msec < msec_per_period) + msec_per_period = 0; + } + + if (msec_per_period) { + err = snd_pcm_hw_params_set_period_time_near(state->handle, + state->hw_params, &msec_per_period, NULL); + if (err < 0) { + logging(state, + "Fail to configure period time: %u msec\n", + msec_per_period); + return err; + } + } else { + err = snd_pcm_hw_params_set_period_size_near(state->handle, + state->hw_params, &frames_per_period, NULL); + if (err < 0) { + logging(state, + "Fail to configure period size: %lu frames\n", + frames_per_period); + return err; + } + } + + if (msec_per_buffer) { + err = snd_pcm_hw_params_set_buffer_time_near(state->handle, + state->hw_params, &msec_per_buffer, NULL); + if (err < 0) { + logging(state, + "Fail to configure buffer time: %u msec\n", + msec_per_buffer); + return err; + } + } else { + err = snd_pcm_hw_params_set_buffer_size_near(state->handle, + state->hw_params, &frames_per_buffer); + if (err < 0) { + logging(state, + "Fail to configure buffer size: %lu frames\n", + frames_per_buffer); + return err; + } + } + return snd_pcm_hw_params(state->handle, state->hw_params); }
@@ -287,7 +397,11 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return -ENXIO;
err = configure_hw_params(state, *format, *samples_per_frame, - *frames_per_second); + *frames_per_second, + state->msec_per_period, + state->msec_per_buffer, + state->frames_per_period, + state->frames_per_buffer); if (err < 0) { logging(state, "Current hardware parameters:\n"); snd_pcm_hw_params_dump(state->hw_params, state->log); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index f3ce73f..4456fab 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -30,6 +30,11 @@ struct libasound_state {
char *node_literal;
+ unsigned int msec_per_period; + unsigned int msec_per_buffer; + unsigned int frames_per_period; + unsigned int frames_per_buffer; + bool finish_at_xrun:1; bool nonblock:1; bool mmap:1; diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c index 9233647..d899134 100644 --- a/axfer/xfer-options.c +++ b/axfer/xfer-options.c @@ -17,6 +17,8 @@ enum no_short_opts { // 128 or later belong to non us-ascii character set. OPT_XFER_TYPE = 128, OPT_DUMP_HW_PARAMS, + OPT_PERIOD_SIZE, + OPT_BUFFER_SIZE, // Obsoleted. OPT_MAX_FILE_TIME, };
In ALSA PCM interface, some parameters are used to configure runtime of PCM substream independently of actual hardware. These parameters are mainly used to decide the detailed timing to start/stop PCM substream and release I/O blocking state of application. These parameters are represented and delivered by a structure.
In alsa-lib PCM API, the structure is hidden from userspace applications. The applications can set/get actual parameters by helper functions.
In aplay, three of the parameters are configurable. This commit adds support for them. When no options are given, default values are used.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound.c | 86 ++++++++++++++++++++++++++++++++++++++++-- axfer/xfer-libasound.h | 4 ++ 2 files changed, 87 insertions(+), 3 deletions(-)
diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index a23021e..1e709e0 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -17,7 +17,7 @@ enum no_short_opts { OPT_TEST_NOWAIT, };
-#define S_OPTS "D:NMF:B:" +#define S_OPTS "D:NMF:B:A:R:T:" static const struct option l_opts[] = { {"device", 1, 0, 'D'}, {"nonblock", 0, 0, 'N'}, @@ -26,6 +26,9 @@ static const struct option l_opts[] = { {"buffer-time", 1, 0, 'B'}, {"period-size", 1, 0, OPT_PERIOD_SIZE}, {"buffer-size", 1, 0, OPT_BUFFER_SIZE}, + {"avail-min", 1, 0, 'A'}, + {"start-delay", 1, 0, 'R'}, + {"stop-delay", 1, 0, 'T'}, // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, @@ -68,6 +71,12 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->frames_per_period = arg_parse_decimal_num(optarg, &err); else if (key == OPT_BUFFER_SIZE) state->frames_per_buffer = arg_parse_decimal_num(optarg, &err); + else if (key == 'A') + state->msec_for_avail_min = arg_parse_decimal_num(optarg, &err); + else if (key == 'R') + state->msec_for_start_threshold = arg_parse_decimal_num(optarg, &err); + else if (key == 'T') + state->msec_for_stop_threshold = arg_parse_decimal_num(optarg, &err); else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; else if (key == OPT_TEST_NOWAIT) @@ -377,8 +386,76 @@ static int retrieve_actual_hw_params(snd_pcm_hw_params_t *hw_params,
static int configure_sw_params(struct libasound_state *state, unsigned int frames_per_second, - unsigned int frames_per_buffer) + unsigned int frames_per_buffer, + unsigned int msec_for_avail_min, + unsigned int msec_for_start_threshold, + unsigned int msec_for_stop_threshold) { + snd_pcm_uframes_t frame_count; + int err; + + if (msec_for_avail_min > 0) { + frame_count = msec_for_avail_min * frames_per_second / 1000000; + if (frame_count == 0 || frame_count > frames_per_buffer) { + logging(state, + "The msec for 'avail_min' is too %s: %u " + "msec (%lu frames at %u).\n", + frame_count == 0 ? "small" : "large", + msec_for_avail_min, frame_count, + frames_per_second); + return -EINVAL; + } + err = snd_pcm_sw_params_set_avail_min(state->handle, + state->sw_params, frame_count); + if (err < 0) { + logging(state, + "Fail to configure 'avail-min'.\n"); + return -EINVAL; + } + } + + if (msec_for_start_threshold > 0) { + frame_count = msec_for_start_threshold * frames_per_second / + 1000000; + if (frame_count == 0 || frame_count > frames_per_buffer) { + logging(state, + "The msec for 'start-delay' is too %s: %u " + "msec (%lu frames at %u).\n", + frame_count == 0 ? "small" : "large", + msec_for_start_threshold, frame_count, + frames_per_second); + return -EINVAL; + } + err = snd_pcm_sw_params_set_start_threshold(state->handle, + state->sw_params, frame_count); + if (err < 0) { + logging(state, + "Fail to configure 'start-delay'.\n"); + return -EINVAL; + } + } + + if (msec_for_stop_threshold > 0) { + frame_count = msec_for_stop_threshold * frames_per_second / + 1000000; + if (frame_count == 0 || frame_count > frames_per_buffer) { + logging(state, + "The msec for 'stop-delay' is too %s: %u " + "msec (%lu frames at %u).\n", + frame_count == 0 ? "small" : "large", + msec_for_stop_threshold, frame_count, + frames_per_second); + return -EINVAL; + } + err = snd_pcm_sw_params_set_stop_threshold(state->handle, + state->sw_params, frame_count); + if (err < 0) { + logging(state, + "Fail to configure 'stop-delay'.\n"); + return -EINVAL; + } + } + return snd_pcm_sw_params(state->handle, state->sw_params); }
@@ -444,7 +521,10 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return err;
err = configure_sw_params(state, *frames_per_second, - *frames_per_buffer); + *frames_per_buffer, + state->msec_for_avail_min, + state->msec_for_start_threshold, + state->msec_for_stop_threshold); if (err < 0) { logging(state, "Current software parameters:\n"); snd_pcm_sw_params_dump(state->sw_params, state->log); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 4456fab..113c1b9 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -35,6 +35,10 @@ struct libasound_state { unsigned int frames_per_period; unsigned int frames_per_buffer;
+ unsigned int msec_for_avail_min; + unsigned int msec_for_start_threshold; + unsigned int msec_for_stop_threshold; + bool finish_at_xrun:1; bool nonblock:1; bool mmap:1;
As of 2017, two userspace library implementations are known; alsa-lib and tinyalsa. The latter is simple I/O library to use ALSA PCM interface. On the other hand, alsa-lib is more complicated than it. This is because it's designed to add features to transmission of data frames; e.g. sample resampling. To achieve this, alsa-lib has its configuration space and plugin system.
In aplay, some options are implemented as a flag for the plugins in alsa-lib. The flag is given to snd_pcm_open(). This commit adds support for the flags.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound.c | 25 +++++++++++++++++++++++++ axfer/xfer-libasound.h | 4 ++++ 2 files changed, 29 insertions(+)
diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index 1e709e0..a731423 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -13,6 +13,10 @@ enum no_short_opts { // 200 or later belong to non us-ascii character set. OPT_PERIOD_SIZE = 200, OPT_BUFFER_SIZE, + OPT_DISABLE_RESAMPLE, + OPT_DISABLE_CHANNELS, + OPT_DISABLE_FORMAT, + OPT_DISABLE_SOFTVOL, OPT_FATAL_ERRORS, OPT_TEST_NOWAIT, }; @@ -29,6 +33,11 @@ static const struct option l_opts[] = { {"avail-min", 1, 0, 'A'}, {"start-delay", 1, 0, 'R'}, {"stop-delay", 1, 0, 'T'}, + // For plugins in alsa-lib. + {"disable-resample", 0, 0, OPT_DISABLE_RESAMPLE}, + {"disable-channels", 0, 0, OPT_DISABLE_CHANNELS}, + {"disable-format", 0, 0, OPT_DISABLE_FORMAT}, + {"disable-softvol", 0, 0, OPT_DISABLE_SOFTVOL}, // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, @@ -77,6 +86,14 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->msec_for_start_threshold = arg_parse_decimal_num(optarg, &err); else if (key == 'T') state->msec_for_stop_threshold = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_DISABLE_RESAMPLE) + state->no_auto_resample = true; + else if (key == OPT_DISABLE_CHANNELS) + state->no_auto_channels = true; + else if (key == OPT_DISABLE_FORMAT) + state->no_auto_format = true; + else if (key == OPT_DISABLE_SOFTVOL) + state->no_softvol = true; else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; else if (key == OPT_TEST_NOWAIT) @@ -164,6 +181,14 @@ static int open_handle(struct xfer_context *xfer)
if (state->nonblock) mode |= SND_PCM_NONBLOCK; + if (state->no_auto_resample) + mode |= SND_PCM_NO_AUTO_RESAMPLE; + if (state->no_auto_channels) + mode |= SND_PCM_NO_AUTO_CHANNELS; + if (state->no_auto_format) + mode |= SND_PCM_NO_AUTO_FORMAT; + if (state->no_softvol) + mode |= SND_PCM_NO_SOFTVOL;
err = snd_pcm_open(&state->handle, state->node_literal, xfer->direction, mode); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 113c1b9..cf940e5 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -43,6 +43,10 @@ struct libasound_state { bool nonblock:1; bool mmap:1; bool test_nowait:1; + bool no_auto_resample:1; + bool no_auto_channels:1; + bool no_auto_format:1; + bool no_softvol:1;
bool use_waiter:1; };
There're several types of system calls for multiplexed I/O. They're used to receive notifications of I/O events. Typically, userspace applications call them against file descriptor to yield CPU. When I/O is enabled on any of the descriptors, a task of the application is rescheduled, then the application execute I/O calls.
This commit adds a common interface for this type of system calls, named as 'waiter'. This is expected to be used with non-blocking file operation and operations on mapped page frame.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 5 +- axfer/waiter.c | 99 +++++++++++++++++++++++++++++++++ axfer/waiter.h | 54 ++++++++++++++++++ axfer/xfer-libasound-irq-mmap.c | 14 ++++- axfer/xfer-libasound-irq-rw.c | 26 ++++++++- axfer/xfer-libasound.c | 76 +++++++++++++++++++++++++ axfer/xfer-libasound.h | 7 +++ 7 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 axfer/waiter.c create mode 100644 axfer/waiter.h
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 960811e..e425a28 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -22,6 +22,7 @@ noinst_HEADERS = \ xfer.h \ xfer-libasound.h \ frame-cache.h + waiter.h
axfer_SOURCES = \ misc.h \ @@ -47,4 +48,6 @@ axfer_SOURCES = \ frame-cache.c \ xfer-libasound-irq-rw.c \ subcmd-transfer.c \ - xfer-libasound-irq-mmap.c + xfer-libasound-irq-mmap.c \ + waiter.h \ + waiter.c diff --git a/axfer/waiter.c b/axfer/waiter.c new file mode 100644 index 00000000..0bc4740 --- /dev/null +++ b/axfer/waiter.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter.c - I/O event waiter. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" + +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include "misc.h" + +static const char *const waiter_type_labels[] = { + [WAITER_TYPE_DEFAULT] = "default", +}; + +enum waiter_type waiter_type_from_label(const char *label) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(waiter_type_labels); ++i) { + if (!strcmp(waiter_type_labels[i], label)) + return i; + } + + return WAITER_TYPE_DEFAULT; +} + +const char *waiter_label_from_type(enum waiter_type type) +{ + return waiter_type_labels[type]; +} + +int waiter_context_init(struct waiter_context *waiter, + enum waiter_type type, unsigned int pfd_count) +{ + struct { + enum waiter_type type; + const struct waiter_data *waiter; + } entries[] = { + {WAITER_TYPE_COUNT, NULL}, + }; + int i; + + if (pfd_count == 0) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(entries); ++i) { + if (entries[i].type == type) + break; + } + if (i == ARRAY_SIZE(entries)) + return -EINVAL; + + waiter->private_data = malloc(entries[i].waiter->private_size); + if (waiter->private_data == NULL) + return -ENOMEM; + memset(waiter->private_data, 0, entries[i].waiter->private_size); + + waiter->type = type; + waiter->ops = &entries[i].waiter->ops; + + waiter->pfds = calloc(pfd_count, sizeof(*waiter->pfds)); + if (waiter->pfds == NULL) + return -ENOMEM; + waiter->pfd_count = pfd_count; + + return 0; +} + +int waiter_context_prepare(struct waiter_context *waiter) +{ + return waiter->ops->prepare(waiter); +} + +int waiter_context_wait_event(struct waiter_context *waiter, + int timeout_msec) +{ + return waiter->ops->wait_event(waiter, timeout_msec); +} + +void waiter_context_release(struct waiter_context *waiter) +{ + waiter->ops->release(waiter); +} + +void waiter_context_destroy(struct waiter_context *waiter) +{ + if (waiter->pfds) + free(waiter->pfds); + waiter->pfd_count = 0; + if (waiter->private_data) + free(waiter->private_data); + waiter->private_data = NULL; +} diff --git a/axfer/waiter.h b/axfer/waiter.h new file mode 100644 index 00000000..17e01cb --- /dev/null +++ b/axfer/waiter.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter.h - a header for I/O event waiter. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#ifndef __ALSA_UTILS_AXFER_WAITER__H_ +#define __ALSA_UTILS_AXFER_WAITER__H_ + +#include <poll.h> + +enum waiter_type { + WAITER_TYPE_DEFAULT = 0, + WAITER_TYPE_COUNT, +}; + +struct waiter_ops; + +struct waiter_context { + enum waiter_type type; + const struct waiter_ops *ops; + void *private_data; + + struct pollfd *pfds; + unsigned int pfd_count; +}; + +enum waiter_type waiter_type_from_label(const char *label); +const char *waiter_label_from_type(enum waiter_type type); + +int waiter_context_init(struct waiter_context *waiter, + enum waiter_type type, unsigned int pfd_count); +int waiter_context_prepare(struct waiter_context *waiter); +int waiter_context_wait_event(struct waiter_context *waiter, + int timeout_msec); +void waiter_context_release(struct waiter_context *waiter); +void waiter_context_destroy(struct waiter_context *waiter); + +// For internal use in 'waiter' module. + +struct waiter_ops { + int (*prepare)(struct waiter_context *waiter); + int (*wait_event)(struct waiter_context *waiter, int timeout_msec); + void (*release)(struct waiter_context *waiter); +}; + +struct waiter_data { + struct waiter_ops ops; + unsigned int private_size; +}; + +#endif diff --git a/axfer/xfer-libasound-irq-mmap.c b/axfer/xfer-libasound-irq-mmap.c index 18f6dfe..0c96ee5 100644 --- a/axfer/xfer-libasound-irq-mmap.c +++ b/axfer/xfer-libasound-irq-mmap.c @@ -82,10 +82,22 @@ static int irq_mmap_process_frames(struct libasound_state *state, int err;
if (state->use_waiter) { + unsigned short revents; + // Wait for hardware IRQ when no avail space in buffer. - err = snd_pcm_wait(state->handle, -1); + err = xfer_libasound_wait_event(state, -1, &revents); if (err < 0) return err; + if (revents & POLLERR) { + // TODO: error reporting? + return -EIO; + } + if (!(revents & (POLLIN | POLLOUT))) + return -EAGAIN; + + // When rescheduled, current position of data transmission was + // queried to actual hardware by a handler of IRQ. No need to + // perform it; e.g. ioctl(2) with SNDRV_PCM_IOCTL_HWSYNC. }
// Sync cache in user space to data in kernel space to calculate avail diff --git a/axfer/xfer-libasound-irq-rw.c b/axfer/xfer-libasound-irq-rw.c index 625c095..cf4155f 100644 --- a/axfer/xfer-libasound-irq-rw.c +++ b/axfer/xfer-libasound-irq-rw.c @@ -134,10 +134,21 @@ static int r_process_frames_nonblocking(struct libasound_state *state, }
if (state->use_waiter) { + unsigned short revents; + // Wait for hardware IRQ when no available space. - err = snd_pcm_wait(state->handle, -1); + err = xfer_libasound_wait_event(state, -1, &revents); if (err < 0) goto error; + if (revents & POLLERR) { + // TODO: error reporting. + err = -EIO; + goto error; + } + if (!(revents & POLLIN)) { + err = -EAGAIN; + goto error; + } }
// Check available space on the buffer. @@ -289,10 +300,21 @@ static int w_process_frames_nonblocking(struct libasound_state *state, int err;
if (state->use_waiter) { + unsigned short revents; + // Wait for hardware IRQ when no left space. - err = snd_pcm_wait(state->handle, -1); + err = xfer_libasound_wait_event(state, -1, &revents); if (err < 0) goto error; + if (revents & POLLERR) { + // TODO: error reporting. + err = -EIO; + goto error; + } + if (!(revents & POLLOUT)) { + err = -EAGAIN; + goto error; + } }
// Check available space on the buffer. diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index a731423..5cef9f1 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -201,6 +201,7 @@ static int open_handle(struct xfer_context *xfer)
if ((state->nonblock || state->mmap) && !state->test_nowait) state->use_waiter = true; + state->waiter_type = WAITER_TYPE_DEFAULT;
err = snd_pcm_hw_params_any(state->handle, state->hw_params); if (err < 0) @@ -220,6 +221,66 @@ static int open_handle(struct xfer_context *xfer) return set_access_hw_param(state); }
+static int prepare_waiter(struct libasound_state *state) +{ + unsigned int pfd_count; + int err; + + // Nothing to do for dafault waiter (=snd_pcm_wait()). + if (state->waiter_type == WAITER_TYPE_DEFAULT) + return 0; + + err = snd_pcm_poll_descriptors_count(state->handle); + if (err < 0) + return err; + if (err == 0) + return -ENXIO; + pfd_count = (unsigned int)err; + + state->waiter = malloc(sizeof(*state->waiter)); + if (state->waiter == NULL) + return -ENOMEM; + + err = waiter_context_init(state->waiter, state->waiter_type, pfd_count); + if (err < 0) + return err; + + err = snd_pcm_poll_descriptors(state->handle, state->waiter->pfds, + pfd_count); + if (err < 0) + return err; + + return waiter_context_prepare(state->waiter); +} + +int xfer_libasound_wait_event(struct libasound_state *state, int timeout_msec, + unsigned short *revents) +{ + int err; + + if (state->waiter_type != WAITER_TYPE_DEFAULT) { + struct waiter_context *waiter = state->waiter; + + err = waiter_context_wait_event(waiter, timeout_msec); + if (err < 0) + return err; + + err = snd_pcm_poll_descriptors_revents(state->handle, + waiter->pfds, waiter->pfd_count, revents); + } else { + err = snd_pcm_wait(state->handle, timeout_msec); + if (err < 0) + return err; + + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_PLAYBACK) + *revents = POLLOUT; + else + *revents = POLLIN; + } + + return err; +} + static int configure_hw_params(struct libasound_state *state, snd_pcm_format_t format, unsigned int samples_per_frame, @@ -559,6 +620,21 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, if (xfer->verbose > 0) snd_pcm_dump(state->handle, state->log);
+ if (state->use_waiter) { + // NOTE: This should be after configuring sw_params due to + // timer descriptor for time-based scheduling model. + err = prepare_waiter(state); + if (err < 0) + return err; + + if (xfer->verbose > 0) { + logging(state, "Waiter type:\n"); + logging(state, + " %s\n", + waiter_label_from_type(state->waiter_type)); + } + } + return 0; }
diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index cf940e5..0bcfb40 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -10,6 +10,7 @@ #define __ALSA_UTILS_AXFER_XFER_LIBASOUND__H_
#include "xfer.h" +#include "waiter.h"
#define logging(state, ...) \ snd_output_printf(state->log, __VA_ARGS__) @@ -49,6 +50,9 @@ struct libasound_state { bool no_softvol:1;
bool use_waiter:1; + + enum waiter_type waiter_type; + struct waiter_context *waiter; };
// For internal use in 'libasound' module. @@ -63,6 +67,9 @@ struct xfer_libasound_ops { unsigned int private_size; };
+int xfer_libasound_wait_event(struct libasound_state *state, int timeout_msec, + unsigned short *revents); + extern const struct xfer_libasound_ops xfer_libasound_irq_rw_ops;
extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops;
This commit is an integration to add an option for users to choose waiter type. Users give the type to value to '--waiter-type' ('-w') option to choose it. Currently, 'snd_pcm_wait()' is just supported as a default. This alsa-lib API is implemented with a call of poll(2).
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound.c | 26 +++++++++++++++++++++++++- axfer/xfer-libasound.h | 1 + 2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index 5cef9f1..fa72839 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -13,6 +13,7 @@ enum no_short_opts { // 200 or later belong to non us-ascii character set. OPT_PERIOD_SIZE = 200, OPT_BUFFER_SIZE, + OPT_WAITER_TYPE, OPT_DISABLE_RESAMPLE, OPT_DISABLE_CHANNELS, OPT_DISABLE_FORMAT, @@ -33,6 +34,7 @@ static const struct option l_opts[] = { {"avail-min", 1, 0, 'A'}, {"start-delay", 1, 0, 'R'}, {"stop-delay", 1, 0, 'T'}, + {"waiter-type", 1, 0, OPT_WAITER_TYPE}, // For plugins in alsa-lib. {"disable-resample", 0, 0, OPT_DISABLE_RESAMPLE}, {"disable-channels", 0, 0, OPT_DISABLE_CHANNELS}, @@ -86,6 +88,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->msec_for_start_threshold = arg_parse_decimal_num(optarg, &err); else if (key == 'T') state->msec_for_stop_threshold = arg_parse_decimal_num(optarg, &err); + else if (key == OPT_WAITER_TYPE) + state->waiter_type_literal = arg_duplicate_string(optarg, &err); else if (key == OPT_DISABLE_RESAMPLE) state->no_auto_resample = true; else if (key == OPT_DISABLE_CHANNELS) @@ -147,6 +151,25 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) } }
+ if (state->waiter_type_literal != NULL) { + if (state->test_nowait) { + fprintf(stderr, + "An option for waiter type should not be " + "used with nowait test option.\n"); + return -EINVAL; + } + if (!state->nonblock && !state->mmap) { + fprintf(stderr, + "An option for waiter type should be used " + "with nonblock or mmap options.\n"); + return -EINVAL; + } + state->waiter_type = + waiter_type_from_label(state->waiter_type_literal); + } else { + state->waiter_type = WAITER_TYPE_DEFAULT; + } + return err; }
@@ -201,7 +224,6 @@ static int open_handle(struct xfer_context *xfer)
if ((state->nonblock || state->mmap) && !state->test_nowait) state->use_waiter = true; - state->waiter_type = WAITER_TYPE_DEFAULT;
err = snd_pcm_hw_params_any(state->handle, state->hw_params); if (err < 0) @@ -751,7 +773,9 @@ static void xfer_libasound_destroy(struct xfer_context *xfer) struct libasound_state *state = xfer->private_data;
free(state->node_literal); + free(state->waiter_type_literal); state->node_literal = NULL; + state->waiter_type_literal = NULL;
if (state->hw_params) snd_pcm_hw_params_free(state->hw_params); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 0bcfb40..20b0d74 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -30,6 +30,7 @@ struct libasound_state { bool verbose;
char *node_literal; + char *waiter_type_literal;
unsigned int msec_per_period; unsigned int msec_per_buffer;
This commit adds support of waiter for poll(2) system call.
Below lines are examples to use this option: $ axfer transfer --waiter-type=poll -M -P -d 2 -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer --waiter-type=poll -M -C -d 2 -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 ++- axfer/waiter-poll.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ axfer/waiter.c | 3 ++- axfer/waiter.h | 3 +++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 axfer/waiter-poll.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index e425a28..7123006 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -50,4 +50,5 @@ axfer_SOURCES = \ subcmd-transfer.c \ xfer-libasound-irq-mmap.c \ waiter.h \ - waiter.c + waiter.c \ + waiter-poll.c diff --git a/axfer/waiter-poll.c b/axfer/waiter-poll.c new file mode 100644 index 00000000..61aa1a0 --- /dev/null +++ b/axfer/waiter-poll.c @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter-poll.c - Waiter for event notification by poll(2). +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" +#include "misc.h" + +#include <stdlib.h> +#include <errno.h> +#include <poll.h> + +static int poll_prepare(struct waiter_context *waiter) +{ + // Nothing to do because an instance of waiter has required data. + return 0; +} + +static int poll_wait_event(struct waiter_context *waiter, int timeout_msec) +{ + int err; + + err = poll(waiter->pfds, waiter->pfd_count, timeout_msec); + if (err < 0) + return -errno; + + return err; +} + +static void poll_release(struct waiter_context *waiter) +{ + // Nothing to do because an instance of waiter has required data. + return; +} + +const struct waiter_data waiter_poll = { + .ops = { + .prepare = poll_prepare, + .wait_event = poll_wait_event, + .release = poll_release, + }, +}; diff --git a/axfer/waiter.c b/axfer/waiter.c index 0bc4740..446e617 100644 --- a/axfer/waiter.c +++ b/axfer/waiter.c @@ -16,6 +16,7 @@
static const char *const waiter_type_labels[] = { [WAITER_TYPE_DEFAULT] = "default", + [WAITER_TYPE_POLL] = "poll", };
enum waiter_type waiter_type_from_label(const char *label) @@ -42,7 +43,7 @@ int waiter_context_init(struct waiter_context *waiter, enum waiter_type type; const struct waiter_data *waiter; } entries[] = { - {WAITER_TYPE_COUNT, NULL}, + {WAITER_TYPE_POLL, &waiter_poll}, }; int i;
diff --git a/axfer/waiter.h b/axfer/waiter.h index 17e01cb..9366724 100644 --- a/axfer/waiter.h +++ b/axfer/waiter.h @@ -13,6 +13,7 @@
enum waiter_type { WAITER_TYPE_DEFAULT = 0, + WAITER_TYPE_POLL, WAITER_TYPE_COUNT, };
@@ -51,4 +52,6 @@ struct waiter_data { unsigned int private_size; };
+extern const struct waiter_data waiter_poll; + #endif
This commit adds support of waiter for select(2) system call.
Below lines are examples to use this option: $ axfer transfer --waiter-type=select -M -P -d 2 -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer --waiter-type=select -M -C -d 2 -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/waiter-select.c | 100 ++++++++++++++++++++++++++++++++++++++++++ axfer/waiter.c | 2 + axfer/waiter.h | 2 + 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 axfer/waiter-select.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 7123006..867007b 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -51,4 +51,5 @@ axfer_SOURCES = \ xfer-libasound-irq-mmap.c \ waiter.h \ waiter.c \ - waiter-poll.c + waiter-poll.c \ + waiter-select.c diff --git a/axfer/waiter-select.c b/axfer/waiter-select.c new file mode 100644 index 00000000..a35ea85 --- /dev/null +++ b/axfer/waiter-select.c @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter-select.c - Waiter for event notification by select(2). +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" + +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <sys/select.h> + +// Except for POLLERR. +#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP) +#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT) +#define POLLEX_SET (POLLPRI) + +struct select_state { + fd_set rfds_rd; + fd_set rfds_wr; + fd_set rfds_ex; +}; + +static int select_prepare(struct waiter_context *waiter) +{ + return 0; +} + +static int select_wait_event(struct waiter_context *waiter, int timeout_msec) +{ + struct select_state *state = waiter->private_data; + struct pollfd *pfd; + int fd_max; + struct timeval tv, *tv_ptr; + int i; + int err; + + FD_ZERO(&state->rfds_rd); + FD_ZERO(&state->rfds_wr); + FD_ZERO(&state->rfds_ex); + + fd_max = 0; + for (i = 0; i < waiter->pfd_count; ++i) { + pfd = &waiter->pfds[i]; + + if (pfd->events & POLLIN_SET) + FD_SET(pfd->fd, &state->rfds_rd); + if (pfd->events & POLLOUT_SET) + FD_SET(pfd->fd, &state->rfds_wr); + if (pfd->events & POLLEX_SET) + FD_SET(pfd->fd, &state->rfds_ex); + if (pfd->fd > fd_max) + fd_max = pfd->fd; + } + + if (timeout_msec < 0) { + tv_ptr = NULL; + } else { + tv.tv_sec = 0; + tv.tv_usec = timeout_msec * 1000; + tv_ptr = &tv; + } + + err = select(fd_max + 1, &state->rfds_rd, &state->rfds_wr, + &state->rfds_ex, tv_ptr); + if (err < 0) + return err; + + for (i = 0; i < waiter->pfd_count; ++i) { + pfd = &waiter->pfds[i]; + + pfd->revents = 0; + if (FD_ISSET(pfd->fd, &state->rfds_rd)) + pfd->revents |= POLLIN; + if (FD_ISSET(pfd->fd, &state->rfds_wr)) + pfd->revents |= POLLOUT; + if (FD_ISSET(pfd->fd, &state->rfds_ex)) + pfd->revents |= POLLHUP; + } + + return 0; +} + +static void select_release(struct waiter_context *waiter) +{ + return; +} + +const struct waiter_data waiter_select = { + .ops = { + .prepare = select_prepare, + .wait_event = select_wait_event, + .release = select_release, + }, + .private_size = sizeof(struct select_state), +}; diff --git a/axfer/waiter.c b/axfer/waiter.c index 446e617..08428e3 100644 --- a/axfer/waiter.c +++ b/axfer/waiter.c @@ -17,6 +17,7 @@ static const char *const waiter_type_labels[] = { [WAITER_TYPE_DEFAULT] = "default", [WAITER_TYPE_POLL] = "poll", + [WAITER_TYPE_SELECT] = "select", };
enum waiter_type waiter_type_from_label(const char *label) @@ -44,6 +45,7 @@ int waiter_context_init(struct waiter_context *waiter, const struct waiter_data *waiter; } entries[] = { {WAITER_TYPE_POLL, &waiter_poll}, + {WAITER_TYPE_SELECT, &waiter_select}, }; int i;
diff --git a/axfer/waiter.h b/axfer/waiter.h index 9366724..fec16b7 100644 --- a/axfer/waiter.h +++ b/axfer/waiter.h @@ -14,6 +14,7 @@ enum waiter_type { WAITER_TYPE_DEFAULT = 0, WAITER_TYPE_POLL, + WAITER_TYPE_SELECT, WAITER_TYPE_COUNT, };
@@ -53,5 +54,6 @@ struct waiter_data { };
extern const struct waiter_data waiter_poll; +extern const struct waiter_data waiter_select;
#endif
This commit adds support of waiter for Linux specific epoll(7) system call. For portability to the other Unix-like systems such as xBSD, modification of Makefile.am may be required for conditional build, but this commit includes no changes for it.
Below lines are examples to use this option: $ axfer transfer --waiter-type=epoll -M -P -d 2 -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer --waiter-type=epoll -M -C -d 2 -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/waiter-epoll.c | 107 +++++++++++++++++++++++++++++++++++++++++++ axfer/waiter.c | 2 + axfer/waiter.h | 2 + 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 axfer/waiter-epoll.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index 867007b..ab1d0b4 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -52,4 +52,5 @@ axfer_SOURCES = \ waiter.h \ waiter.c \ waiter-poll.c \ - waiter-select.c + waiter-select.c \ + waiter-epoll.c diff --git a/axfer/waiter-epoll.c b/axfer/waiter-epoll.c new file mode 100644 index 00000000..8f084f1 --- /dev/null +++ b/axfer/waiter-epoll.c @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// waiter-epoll.c - Waiter for event notification by epoll(7). +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "waiter.h" +#include "misc.h" + +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/epoll.h> + +struct epoll_state { + int epfd; + struct epoll_event *events; + unsigned int ev_count; +}; + +static int epoll_prepare(struct waiter_context *waiter) +{ + struct epoll_state *state = waiter->private_data; + int i; + + state->ev_count = waiter->pfd_count; + state->events = calloc(state->ev_count, sizeof(*state->events)); + if (state->events == NULL) + return -ENOMEM; + + state->epfd = epoll_create(1); + if (state->epfd < 0) + return -errno; + + for (i = 0; i < waiter->pfd_count; ++i) { + struct epoll_event ev = { + .data.fd = waiter->pfds[i].fd, + .events = waiter->pfds[i].events, + }; + if (epoll_ctl(state->epfd, EPOLL_CTL_ADD, ev.data.fd, &ev) < 0) + return -errno; + } + + return 0; +} + +static int epoll_wait_event(struct waiter_context *waiter, int timeout_msec) +{ + struct epoll_state *state = waiter->private_data; + unsigned int ev_count; + int i, j; + int err; + + memset(state->events, 0, state->ev_count * sizeof(*state->events)); + err = epoll_wait(state->epfd, state->events, state->ev_count, + timeout_msec); + if (err < 0) + return -errno; + ev_count = (unsigned int)err; + + if (ev_count > 0) { + // Reconstruct data of pollfd structure. + for (i = 0; i < ev_count; ++i) { + struct epoll_event *ev = &state->events[i]; + for (j = 0; j < waiter->pfd_count; ++j) { + if (waiter->pfds[i].fd == ev->data.fd) { + waiter->pfds[i].revents = ev->events; + break; + } + } + } + } + + return ev_count; +} + +static void epoll_release(struct waiter_context *waiter) +{ + struct epoll_state *state = waiter->private_data; + int i; + + for (i = 0; i < waiter->pfd_count; ++i) { + int fd = waiter->pfds[i].fd; + epoll_ctl(state->epfd, EPOLL_CTL_DEL, fd, NULL); + } + + free(state->events); + state->events = NULL; + + close(state->epfd); + + state->ev_count = 0; + state->epfd = 0; +} + +const struct waiter_data waiter_epoll = { + .ops = { + .prepare = epoll_prepare, + .wait_event = epoll_wait_event, + .release = epoll_release, + }, + .private_size = sizeof(struct epoll_state), +}; diff --git a/axfer/waiter.c b/axfer/waiter.c index 08428e3..1e9c811 100644 --- a/axfer/waiter.c +++ b/axfer/waiter.c @@ -18,6 +18,7 @@ static const char *const waiter_type_labels[] = { [WAITER_TYPE_DEFAULT] = "default", [WAITER_TYPE_POLL] = "poll", [WAITER_TYPE_SELECT] = "select", + [WAITER_TYPE_EPOLL] = "epoll", };
enum waiter_type waiter_type_from_label(const char *label) @@ -46,6 +47,7 @@ int waiter_context_init(struct waiter_context *waiter, } entries[] = { {WAITER_TYPE_POLL, &waiter_poll}, {WAITER_TYPE_SELECT, &waiter_select}, + {WAITER_TYPE_EPOLL, &waiter_epoll}, }; int i;
diff --git a/axfer/waiter.h b/axfer/waiter.h index fec16b7..db18e33 100644 --- a/axfer/waiter.h +++ b/axfer/waiter.h @@ -15,6 +15,7 @@ enum waiter_type { WAITER_TYPE_DEFAULT = 0, WAITER_TYPE_POLL, WAITER_TYPE_SELECT, + WAITER_TYPE_EPOLL, WAITER_TYPE_COUNT, };
@@ -55,5 +56,6 @@ struct waiter_data {
extern const struct waiter_data waiter_poll; extern const struct waiter_data waiter_select; +extern const struct waiter_data waiter_epoll;
#endif
In 2010, ALSA PCM interface got an flag of hardware parameters to suppress periodical interrupts, according to a request from PulseAudio developer.
In typical PCM operation for usual hardware, PCM drivers configure the hardware to generate the periodical interrupts to notify that the same amount of data frames as a period of PCM buffer is actually transferred via serial sound interface. The flag can suppress this if the driver support it.
There's some merits of this configuration: - No interrupt context run for PCM substream. The PCM substream is handled in any process context only. No need to care of race conditions between interrupt/process contexts. This is good for developers of drivers and applications. - CPU time is not used for handlers on the interrupt context. The CPU time can be dedicated for the other tasks. This is good in a point of Time Sharing System. - Hardware is not configured to generate interrupts. This is good in a point of reduction of overall power consumption.
Disabling period interrupt is used for 'Timer-based scheduling' to consume data frames on PCM buffer independently of interrupt context. As noted, no interrupt context runs for PCM substream, thus any blocking operation is not released. Furthermore, system calls for multiplexed I/O is not also released without timeout.
In this scheduling model, applications need to care of available space on PCM buffer by lapse of time, typically by yielding CPU and wait for rescheduling. For the yielding, timeout is calculated for preferable amount of PCM frames to process. This is an additional merit for applications, like sound servers. when an I/O thread of the server wait for the timeout, the other threads can process data frames for server clients. Furthermore, with usage of rewinding/forwarding, applications can achieve low latency between transmission position and handling position even if they uses large size of PCM buffers.
But the timeout should be calculated with enough care of hardware capabilities. To disable period interrupt, used hardware should satisfy some requirements for data transmission: 1. Even if drivers don't handle interrupts to queue next data transmission, hardware voluntarily perform the data transmission when needed (typically by requesting DMA automatically). 2. hardware has a capability to report current position of data transmission with enough accuracy against the data transmission. developers refer this as 'granularity'. If hardware can always reports updated position after the data transmission finishes, the granularity equals to the size of period of PCM buffer. 3. a fine size of data transmission in one time. This size is decided depending on configuration of hardware or DMA controller, but for efficiency it may not be one byte. Thus some amount of data frame is transferred by one data transmission. Developers refer this as 'burst-ness'.
The timeout should be calculated according to the item 2 and 3, however in current ALSA PCM interface supplemental information is not delivered from drivers to applications. Although at present userspace applications should be written by a speculative way for this point, there's few problems because there're a few hardware which satisfy the above items. However, when more drivers supports this feature, the problem may largely be exposed and bothers application developers.
This commit adds an option to use 'timer-based scheduling' for data transmission. This commit adds '--sched-model' option, and the scheduling mode is enabled when 'timer' is assigned to the option by equal sign.
Although there's some TODOs, you can see the scheduling mode in this simple program, like:
$ axfer transfer --sched-model=timer -P -d 2 -D hw:0,3 /dev/urandom -f dat -vvv $ axfer transfer --sched-model=timer -C -d 2 -D hw:1,0 /dev/null -r 48000 -vvv
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/Makefile.am | 3 +- axfer/xfer-libasound-timer-mmap.c | 455 ++++++++++++++++++++++++++++++ axfer/xfer-libasound.c | 108 ++++++- axfer/xfer-libasound.h | 13 + 4 files changed, 564 insertions(+), 15 deletions(-) create mode 100644 axfer/xfer-libasound-timer-mmap.c
diff --git a/axfer/Makefile.am b/axfer/Makefile.am index ab1d0b4..497b9ff 100644 --- a/axfer/Makefile.am +++ b/axfer/Makefile.am @@ -53,4 +53,5 @@ axfer_SOURCES = \ waiter.c \ waiter-poll.c \ waiter-select.c \ - waiter-epoll.c + waiter-epoll.c \ + xfer-libasound-timer-mmap.c diff --git a/axfer/xfer-libasound-timer-mmap.c b/axfer/xfer-libasound-timer-mmap.c new file mode 100644 index 00000000..1c642fe --- /dev/null +++ b/axfer/xfer-libasound-timer-mmap.c @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// xfer-libasound-irq-mmap.c - Timer-based scheduling model for mmap operation. +// +// Copyright (c) 2018 Takashi Sakamoto o-takashi@sakamocchi.jp +// +// Licensed under the terms of the GNU General Public License, version 2. + +#include "xfer-libasound.h" +#include "misc.h" + +struct map_layout { + snd_pcm_status_t *status; + bool need_forward_or_rewind; + char **vector; + + unsigned int frames_per_second; + unsigned int samples_per_frame; + unsigned int frames_per_buffer; +}; + +static int timer_mmap_pre_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_access_t access; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail = 0; + snd_pcm_uframes_t frames_per_buffer; + int i; + int err; + + // This parameter, 'period event', is a software feature in alsa-lib. + // This switch a handler in 'hw' PCM plugin from irq-based one to + // timer-based one. This handler has two file descriptors for + // ALSA PCM character device and ALSA timer device. The latter is used + // to catch suspend/resume events as wakeup event. + err = snd_pcm_sw_params_set_period_event(state->handle, + state->sw_params, 1); + if (err < 0) + return err; + + err = snd_pcm_status_malloc(&layout->status); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_access(state->hw_params, &access); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_channels(state->hw_params, + &layout->samples_per_frame); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_rate(state->hw_params, + &layout->frames_per_second, NULL); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_buffer_size(state->hw_params, + &frames_per_buffer); + if (err < 0) + return err; + layout->frames_per_buffer = (unsigned int)frames_per_buffer; + + if (access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + layout->vector = calloc(layout->samples_per_frame, + sizeof(*layout->vector)); + if (layout->vector == NULL) + return err; + } + + if (state->verbose) { + const snd_pcm_channel_area_t *areas; + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail); + if (err < 0) + return err; + + logging(state, "attributes for mapped page frame:\n"); + for (i = 0; i < layout->samples_per_frame; ++i) { + const snd_pcm_channel_area_t *area = areas + i; + + logging(state, " sample number: %d\n", i); + logging(state, " address: %p\n", area->addr); + logging(state, " bits for offset: %u\n", area->first); + logging(state, " bits/frame: %u\n", area->step); + } + } + + return 0; +} + +static void *get_buffer(struct libasound_state *state, + const snd_pcm_channel_area_t *areas, + snd_pcm_uframes_t frame_offset) +{ + struct map_layout *layout = state->private_data; + void *frame_buf; + + if (layout->vector == NULL) { + char *buf; + buf = areas[0].addr + snd_pcm_frames_to_bytes(state->handle, + frame_offset); + frame_buf = buf; + } else { + int i; + for (i = 0; i < layout->samples_per_frame; ++i) { + layout->vector[i] = areas[i].addr; + layout->vector[i] += snd_pcm_samples_to_bytes( + state->handle, frame_offset); + } + frame_buf = layout->vector; + } + + return frame_buf; +} + +static int timer_mmap_process_frames(struct libasound_state *state, + unsigned int *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_uframes_t planned_count; + snd_pcm_sframes_t avail; + snd_pcm_uframes_t avail_count; + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t frame_offset; + void *frame_buf; + snd_pcm_sframes_t consumed_count; + int err; + + // Retrieve avail space on PCM buffer between kernel/user spaces. + // On cache incoherent architectures, still care of data + // synchronization. + avail = snd_pcm_avail_update(state->handle); + if (avail < 0) + return (int)avail; + + // Retrieve pointers of the buffer and left space up to the boundary. + avail_count = (snd_pcm_uframes_t)avail; + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail_count); + if (err < 0) + return err; + + // MEMO: Use the amount of data frames as you like. + planned_count = layout->frames_per_buffer * random() / RAND_MAX; + if (frame_offset + planned_count > layout->frames_per_buffer) + planned_count = layout->frames_per_buffer - frame_offset; + + // Trim up to expected frame count. + if (*frame_count < planned_count) + planned_count = *frame_count; + + // Yield this CPU till planned amount of frames become available. + if (avail_count < planned_count) { + unsigned short revents; + int timeout_msec; + + // TODO; precise granularity of timeout; e.g. ppoll(2). + // Furthermore, wrap up according to granularity of reported + // value for hw_ptr. + timeout_msec = ((planned_count - avail_count) * 1000 + + layout->frames_per_second - 1) / + layout->frames_per_second; + + // TODO: However, experimentally, the above is not enough to + // keep planned amount of frames when waking up. I don't know + // exactly the mechanism yet. + err = xfer_libasound_wait_event(state, timeout_msec, + &revents); + if (err < 0) + return err; + if (revents & POLLERR) { + // TODO: error reporting. + return -EIO; + } + if (!(revents & (POLLIN | POLLOUT))) + return -EAGAIN; + + // MEMO: Need to perform hwsync explicitly because hwptr is not + // synchronized to actual position of data frame transmission + // on hardware because IRQ handlers are not used in this + // scheduling strategy. + avail = snd_pcm_avail(state->handle); + if (avail < 0) + return (int)avail; + if (avail < planned_count) { + logging(state, + "Wake up but not enough space: %lu %lu %u\n", + planned_count, avail, timeout_msec); + planned_count = avail; + } + } + + // Let's process data frames. + *frame_count = planned_count; + frame_buf = get_buffer(state, areas, frame_offset); + err = mapper_context_process_frames(mapper, frame_buf, frame_count, + cntrs); + if (err < 0) + return err; + + consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset, + *frame_count); + if (consumed_count != *frame_count) { + logging(state, + "A bug of 'hw' PCM plugin or driver for this PCM " + "node.\n"); + } + *frame_count = consumed_count; + + return 0; +} + +static int forward_appl_ptr(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_uframes_t forwardable_count; + snd_pcm_sframes_t forward_count; + + forward_count = snd_pcm_forwardable(state->handle); + if (forward_count < 0) + return (int)forward_count; + forwardable_count = forward_count; + + // No need to add safe-gurard because hwptr goes ahead. + forward_count = snd_pcm_forward(state->handle, forwardable_count); + if (forward_count < 0) + return (int)forward_count; + + if (state->verbose) { + logging(state, + " forwarded: %lu/%u\n", + forward_count, layout->frames_per_buffer); + } + + return 0; +} + +static int timer_mmap_r_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // SNDRV_PCM_HW_PARAMS_NO_PERIOD_WAKEUP suppresses any IRQ to notify + // period elapse for data transmission, therefore no need to care of + // concurrent access by IRQ context and process context, unlike + // IRQ-based operations. + // Here, this is just to query current status to hardware, for later + // processing. + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + if (s == SND_PCM_STATE_RUNNING) { + // Reduce delay between sampling on hardware and handling by + // this program. + if (layout->need_forward_or_rewind) { + err = forward_appl_ptr(state); + if (err < 0) + goto error; + layout->need_forward_or_rewind = false; + } + + err = timer_mmap_process_frames(state, frame_count, mapper, + cntrs); + if (err < 0) + goto error; + } else { + if (s == SND_PCM_STATE_PREPARED) { + // For capture direction, need to start stream + // explicitly. + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + layout->need_forward_or_rewind = true; + // Not yet. + *frame_count = 0; + } else { + err = -EPIPE; + goto error; + } + } + + return 0; +error: + *frame_count = 0; + return err; +} + +static int rewind_appl_ptr(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + snd_pcm_uframes_t rewindable_count; + snd_pcm_sframes_t rewind_count; + + rewind_count = snd_pcm_rewindable(state->handle); + if (rewind_count < 0) + return (int)rewind_count; + rewindable_count = rewind_count; + + // If appl_ptr were rewound just to position of hw_ptr, at next time, + // hw_ptr could catch up appl_ptr. This is overrun. We need a space + // between these two pointers to prevent this XRUN. + // This space is largely affected by time to process data frames later. + // + // TODO: a generous way to estimate a good value. + if (rewindable_count < 32) + return 0; + rewindable_count -= 32; + + rewind_count = snd_pcm_rewind(state->handle, rewindable_count); + if (rewind_count < 0) + return (int)rewind_count; + + if (state->verbose) { + logging(state, + " rewound: %lu/%u\n", + rewind_count, layout->frames_per_buffer); + } + + return 0; +} + +static int fill_buffer_with_zero_samples(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + const snd_pcm_channel_area_t *areas; + snd_pcm_uframes_t frame_offset; + snd_pcm_uframes_t avail_count; + snd_pcm_format_t sample_format; + snd_pcm_uframes_t consumed_count; + int err; + + err = snd_pcm_hw_params_get_buffer_size(state->hw_params, + &avail_count); + if (err < 0) + return err; + + err = snd_pcm_mmap_begin(state->handle, &areas, &frame_offset, + &avail_count); + if (err < 0) + return err; + + err = snd_pcm_hw_params_get_format(state->hw_params, &sample_format); + if (err < 0) + return err; + + err = snd_pcm_areas_silence(areas, frame_offset, + layout->samples_per_frame, avail_count, + sample_format); + if (err < 0) + return err; + + consumed_count = snd_pcm_mmap_commit(state->handle, frame_offset, + avail_count); + if (consumed_count != avail_count) + logging(state, "A bug of access plugin for this PCM node.\n"); + + return 0; +} + +static int timer_mmap_w_process_frames(struct libasound_state *state, + unsigned *frame_count, + struct mapper_context *mapper, + struct container_context *cntrs) +{ + struct map_layout *layout = state->private_data; + snd_pcm_state_t s; + int err; + + // Read my comment in 'timer_mmap_w_process_frames()'. + err = snd_pcm_status(state->handle, layout->status); + if (err < 0) + goto error; + s = snd_pcm_status_get_state(layout->status); + + // TODO: if reporting something, do here with the status data. + + if (s == SND_PCM_STATE_RUNNING) { + // Reduce delay between queueing by this program and presenting + // on hardware. + if (layout->need_forward_or_rewind) { + err = rewind_appl_ptr(state); + if (err < 0) + goto error; + layout->need_forward_or_rewind = false; + } + + err = timer_mmap_process_frames(state, frame_count, mapper, + cntrs); + if (err < 0) + goto error; + } else { + // Need to start playback stream explicitly + if (s == SND_PCM_STATE_PREPARED) { + err = fill_buffer_with_zero_samples(state); + if (err < 0) + goto error; + + err = snd_pcm_start(state->handle); + if (err < 0) + goto error; + + layout->need_forward_or_rewind = true; + // Not yet. + *frame_count = 0; + } else { + err = -EPIPE; + goto error; + } + } + + return 0; +error: + *frame_count = 0; + return err; +} + +static void timer_mmap_post_process(struct libasound_state *state) +{ + struct map_layout *layout = state->private_data; + + if (layout->status) + snd_pcm_status_free(layout->status); + layout->status = NULL; + + if (layout->vector) + free(layout->vector); + layout->vector = NULL; +} + +const struct xfer_libasound_ops xfer_libasound_timer_mmap_w_ops = { + .pre_process = timer_mmap_pre_process, + .process_frames = timer_mmap_w_process_frames, + .post_process = timer_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; + +const struct xfer_libasound_ops xfer_libasound_timer_mmap_r_ops = { + .pre_process = timer_mmap_pre_process, + .process_frames = timer_mmap_r_process_frames, + .post_process = timer_mmap_post_process, + .private_size = sizeof(struct map_layout), +}; diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index fa72839..f6d0515 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -9,11 +9,17 @@ #include "xfer-libasound.h" #include "misc.h"
+static const char *const sched_model_labels [] = { + [SCHED_MODEL_IRQ] = "irq", + [SCHED_MODEL_TIMER] = "timer", +}; + enum no_short_opts { // 200 or later belong to non us-ascii character set. OPT_PERIOD_SIZE = 200, OPT_BUFFER_SIZE, OPT_WAITER_TYPE, + OPT_SCHED_MODEL, OPT_DISABLE_RESAMPLE, OPT_DISABLE_CHANNELS, OPT_DISABLE_FORMAT, @@ -35,6 +41,7 @@ static const struct option l_opts[] = { {"start-delay", 1, 0, 'R'}, {"stop-delay", 1, 0, 'T'}, {"waiter-type", 1, 0, OPT_WAITER_TYPE}, + {"sched-model", 1, 0, OPT_SCHED_MODEL}, // For plugins in alsa-lib. {"disable-resample", 0, 0, OPT_DISABLE_RESAMPLE}, {"disable-channels", 0, 0, OPT_DISABLE_CHANNELS}, @@ -90,6 +97,8 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->msec_for_stop_threshold = arg_parse_decimal_num(optarg, &err); else if (key == OPT_WAITER_TYPE) state->waiter_type_literal = arg_duplicate_string(optarg, &err); + else if (key == OPT_SCHED_MODEL) + state->sched_model_literal = arg_duplicate_string(optarg, &err); else if (key == OPT_DISABLE_RESAMPLE) state->no_auto_resample = true; else if (key == OPT_DISABLE_CHANNELS) @@ -151,6 +160,15 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) } }
+ state->sched_model = SCHED_MODEL_IRQ; + if (state->sched_model_literal != NULL) { + if (!strcmp(state->sched_model_literal, "timer")) { + state->sched_model = SCHED_MODEL_TIMER; + state->mmap = true; + state->nonblock = true; + } + } + if (state->waiter_type_literal != NULL) { if (state->test_nowait) { fprintf(stderr, @@ -161,7 +179,8 @@ int xfer_libasound_validate_opts(struct xfer_context *xfer) if (!state->nonblock && !state->mmap) { fprintf(stderr, "An option for waiter type should be used " - "with nonblock or mmap options.\n"); + "with nonblock or mmap or timer-based " + "scheduling options.\n"); return -EINVAL; } state->waiter_type = @@ -196,6 +215,37 @@ static int set_access_hw_param(struct libasound_state *state) return err; }
+static int disable_period_wakeup(struct libasound_state *state) +{ + int err; + + if (snd_pcm_type(state->handle) != SND_PCM_TYPE_HW) { + logging(state, + "Timer-based scheduling is only available for 'hw' " + "PCM plugin.\n"); + return -ENXIO; + } + + if (!snd_pcm_hw_params_can_disable_period_wakeup(state->hw_params)) { + logging(state, + "This hardware doesn't support the mode of no-period-" + "wakeup. In this case, timer-based scheduling is not " + "available.\n"); + return -EIO; + } + + err = snd_pcm_hw_params_set_period_wakeup(state->handle, + state->hw_params, 0); + if (err < 0) { + logging(state, + "Fail to disable period wakeup so that the hardware " + "generates no IRQs during transmission of data " + "frames.\n"); + } + + return err; +} + static int open_handle(struct xfer_context *xfer) { struct libasound_state *state = xfer->private_data; @@ -229,7 +279,11 @@ static int open_handle(struct xfer_context *xfer) if (err < 0) return err;
- // TODO: Applying NO_PERIOD_WAKEUP should be done here. + if (state->sched_model == SCHED_MODEL_TIMER) { + err = disable_period_wakeup(state); + if (err < 0) + return err; + }
if (xfer->dump_hw_params) { logging(state, "Available HW Params of node: %s\n", @@ -575,6 +629,7 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, snd_pcm_uframes_t *frames_per_buffer) { struct libasound_state *state = xfer->private_data; + unsigned int flag; int err;
err = open_handle(xfer); @@ -606,24 +661,43 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return err;
// Assign I/O operation. - if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || - *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { - state->ops = &xfer_libasound_irq_rw_ops; - } else if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || - *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { - if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) - state->ops = &xfer_libasound_irq_mmap_r_ops; - else - state->ops = &xfer_libasound_irq_mmap_w_ops; + err = snd_pcm_hw_params_get_period_wakeup(state->handle, + state->hw_params, &flag); + if (err < 0) + return err; + + if (flag) { + if (*access == SND_PCM_ACCESS_RW_INTERLEAVED || + *access == SND_PCM_ACCESS_RW_NONINTERLEAVED) { + state->ops = &xfer_libasound_irq_rw_ops; + } else if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_irq_mmap_r_ops; + else + state->ops = &xfer_libasound_irq_mmap_w_ops; + } else { + return -ENXIO; + } } else { - return -ENXIO; + if (*access == SND_PCM_ACCESS_MMAP_INTERLEAVED || + *access == SND_PCM_ACCESS_MMAP_NONINTERLEAVED) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE) + state->ops = &xfer_libasound_timer_mmap_r_ops; + else + state->ops = &xfer_libasound_timer_mmap_w_ops; + } else { + return -ENXIO; + } } + if (state->ops->private_size > 0) { state->private_data = malloc(state->ops->private_size); if (state->private_data == NULL) return -ENOMEM; memset(state->private_data, 0, state->ops->private_size); } + err = state->ops->pre_process(state); if (err < 0) return err; @@ -639,8 +713,11 @@ static int xfer_libasound_pre_process(struct xfer_context *xfer, return err; }
- if (xfer->verbose > 0) + if (xfer->verbose > 0) { snd_pcm_dump(state->handle, state->log); + logging(state, "Scheduling model:\n"); + logging(state, " %s\n", sched_model_labels[state->sched_model]); + }
if (state->use_waiter) { // NOTE: This should be after configuring sw_params due to @@ -733,7 +810,8 @@ static void xfer_libasound_post_process(struct xfer_context *xfer) 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) { + if (snd_pcm_stream(state->handle) == SND_PCM_STREAM_CAPTURE || + state->ops == &xfer_libasound_timer_mmap_w_ops) { err = snd_pcm_drop(state->handle); if (err < 0) logging(state, "snd_pcm_drop(): %s\n", @@ -774,8 +852,10 @@ static void xfer_libasound_destroy(struct xfer_context *xfer)
free(state->node_literal); free(state->waiter_type_literal); + free(state->sched_model_literal); state->node_literal = NULL; state->waiter_type_literal = NULL; + state->sched_model_literal = NULL;
if (state->hw_params) snd_pcm_hw_params_free(state->hw_params); diff --git a/axfer/xfer-libasound.h b/axfer/xfer-libasound.h index 20b0d74..dfe8577 100644 --- a/axfer/xfer-libasound.h +++ b/axfer/xfer-libasound.h @@ -15,6 +15,12 @@ #define logging(state, ...) \ snd_output_printf(state->log, __VA_ARGS__)
+enum sched_model { + SCHED_MODEL_IRQ = 0, + SCHED_MODEL_TIMER, + SCHED_MODEL_COUNT, +}; + struct xfer_libasound_ops;
struct libasound_state { @@ -31,6 +37,7 @@ struct libasound_state {
char *node_literal; char *waiter_type_literal; + char *sched_model_literal;
unsigned int msec_per_period; unsigned int msec_per_buffer; @@ -54,6 +61,9 @@ struct libasound_state {
enum waiter_type waiter_type; struct waiter_context *waiter; + + // For scheduling type. + enum sched_model sched_model; };
// For internal use in 'libasound' module. @@ -76,4 +86,7 @@ extern const struct xfer_libasound_ops xfer_libasound_irq_rw_ops; extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_r_ops; extern const struct xfer_libasound_ops xfer_libasound_irq_mmap_w_ops;
+extern const struct xfer_libasound_ops xfer_libasound_timer_mmap_w_ops; +extern const struct xfer_libasound_ops xfer_libasound_timer_mmap_r_ops; + #endif
Althogh many options are actually supported by aplay, some of them are not enough good in practical points. For example, '--test-position' option is meaningless for some use cases. Furthermore, due to practical reasons, some options are not implemented well; e.g. vumeter.
This commit marks such options as 'obsoleted'.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp --- axfer/xfer-libasound.c | 13 ++++++++++++- axfer/xfer-options.c | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/axfer/xfer-libasound.c b/axfer/xfer-libasound.c index f6d0515..bacd835 100644 --- a/axfer/xfer-libasound.c +++ b/axfer/xfer-libasound.c @@ -26,9 +26,12 @@ enum no_short_opts { OPT_DISABLE_SOFTVOL, OPT_FATAL_ERRORS, OPT_TEST_NOWAIT, + // Obsoleted. + OPT_TEST_POSITION, + OPT_TEST_COEF, };
-#define S_OPTS "D:NMF:B:A:R:T:" +#define S_OPTS "D:NMF:B:A:R:T:m:" static const struct option l_opts[] = { {"device", 1, 0, 'D'}, {"nonblock", 0, 0, 'N'}, @@ -50,6 +53,10 @@ static const struct option l_opts[] = { // For debugging. {"fatal-errors", 0, 0, OPT_FATAL_ERRORS}, {"test-nowait", 0, 0, OPT_TEST_NOWAIT}, + // Obsoleted. + {"chmap", 1, 0, 'm'}, + {"test-position", 0, 0, OPT_TEST_POSITION}, + {"test-coef", 1, 0, OPT_TEST_COEF}, };
static int xfer_libasound_init(struct xfer_context *xfer, @@ -107,6 +114,10 @@ static int xfer_libasound_parse_opt(struct xfer_context *xfer, int key, state->no_auto_format = true; else if (key == OPT_DISABLE_SOFTVOL) state->no_softvol = true; + else if (key == 'm' || + key == OPT_TEST_POSITION || + key == OPT_TEST_COEF) + err = -EINVAL; else if (key == OPT_FATAL_ERRORS) state->finish_at_xrun = true; else if (key == OPT_TEST_NOWAIT) diff --git a/axfer/xfer-options.c b/axfer/xfer-options.c index d899134..5a68646 100644 --- a/axfer/xfer-options.c +++ b/axfer/xfer-options.c @@ -21,6 +21,8 @@ enum no_short_opts { OPT_BUFFER_SIZE, // Obsoleted. OPT_MAX_FILE_TIME, + OPT_USE_STRFTIME, + OPT_PROCESS_ID_FILE, };
static int allocate_paths(struct xfer_context *xfer, char *const *paths, @@ -232,7 +234,7 @@ int xfer_options_parse_args(struct xfer_context *xfer, const struct xfer_data *data, int argc, char *const *argv) { - static const char *short_opts = "CPhvqd:s:f:c:r:t:I"; + static const char *short_opts = "CPhvqd:s:f:c:r:t:IV:i"; static const struct option long_opts[] = { // For generic purposes. {"capture", 0, 0, 'C'}, @@ -255,6 +257,10 @@ int xfer_options_parse_args(struct xfer_context *xfer, {"dump-hw-params", 0, 0, OPT_DUMP_HW_PARAMS}, // Obsoleted. {"max-file-time", 1, 0, OPT_MAX_FILE_TIME}, + {"use-strftime", 0, 0, OPT_USE_STRFTIME}, + {"process-id-file", 1, 0, OPT_PROCESS_ID_FILE}, + {"vumeter", 1, 0, 'V'}, + {"interactive", 0, 0, 'i'}, }; char *s_opts; struct option *l_opts; @@ -321,7 +327,11 @@ int xfer_options_parse_args(struct xfer_context *xfer, xfer->dump_hw_params = true; else if (key == '?') return -EINVAL; - else if (key == OPT_MAX_FILE_TIME) { + else if (key == OPT_MAX_FILE_TIME || + key == OPT_USE_STRFTIME || + key == OPT_PROCESS_ID_FILE || + key == 'V' || + key == 'i') { fprintf(stderr, "An option '--%s' is obsoleted and has no " "effect.\n",
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 --- 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 00000000..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 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), +}; 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 <getopt.h>
+#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=
On Tue, 13 Nov 2018 07:41:13 +0100, Takashi Sakamoto wrote:
This commit adds a new command, 'axfer' ('ALSA transfer'), to transfer data frames described in asound.h. This command is intended to replace current aplay. The most of features and command line parameters come from aplay as much as possible, while it has more better feature and code to maintain.
This commit adds an entry point for this command. Current option system of aplay is still available, while this command has a sub-command system like commands in iproute2.
Currently, two sub-commands are supported; 'list' and 'transfer'. The 'list' sub-command has the same effect as '-l' and '-L' options of aplay. The 'transfer' sub-command has the same effect as the main feature of aplay. For the sub-command system, an option for stream direction is required; '-P' for playback and '-C' for capture. If you create symbolic links to this binary for aplay/arecord, please execute: $ ln -s axfer aplay $ ln -s axfer arecord
Actual code for each sub-command will be implemented in later commits.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp
Although all patches have been merged, below are a few suggestions:
- Please make help working. It took some time for me until figuring out the fact that the direction option is mandatory, for example.
- The input/output from/to a terminal can be checked via isatty(). That allows us to see garbages by a mistakenly started command.
- A man page. (I thought Debian mandates it?)
In anyway, thanks for your hard work!
Takashi
Hi,
On Tue, 13 Nov 2018, Takashi Iwai wrote:
On Tue, 13 Nov 2018 07:41:13 +0100, Takashi Sakamoto wrote:
This commit adds a new command, 'axfer' ('ALSA transfer'), to transfer data frames described in asound.h. This command is intended to replace current aplay. The most of features and command line parameters come from aplay as much as possible, while it has more better feature and code to maintain.
This commit adds an entry point for this command. Current option system of aplay is still available, while this command has a sub-command system like commands in iproute2.
Currently, two sub-commands are supported; 'list' and 'transfer'. The 'list' sub-command has the same effect as '-l' and '-L' options of aplay. The 'transfer' sub-command has the same effect as the main feature of aplay. For the sub-command system, an option for stream direction is required; '-P' for playback and '-C' for capture. If you create symbolic links to this binary for aplay/arecord, please execute: $ ln -s axfer aplay $ ln -s axfer arecord
Actual code for each sub-command will be implemented in later commits.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp
Although all patches have been merged, below are a few suggestions:
- Please make help working. It took some time for me until figuring
out the fact that the direction option is mandatory, for example.
- The input/output from/to a terminal can be checked via isatty().
That allows us to see garbages by a mistakenly started command.
If my understanding is correct, usage of 'isatty(3)' can return error in a case that users just run 'axfer transfer -P (-)'. In current implementation, axfer continues to call of 'read(2)' and receive '-EAGAIN' till receiving teminate signals.
- A man page. (I thought Debian mandates it?)
I have a plan to write help/man in this development period, within this year.
In anyway, thanks for your hard work!
Yep. I'm a bit exhausted from this tough work, and need refresh time. Anyway, thank you for applying this PR.
Regards
Takashi Sakamoto
On Wed, 14 Nov 2018 17:05:49 +0100, Takashi Sakamoto wrote:
Hi,
On Tue, 13 Nov 2018, Takashi Iwai wrote:
On Tue, 13 Nov 2018 07:41:13 +0100, Takashi Sakamoto wrote:
This commit adds a new command, 'axfer' ('ALSA transfer'), to transfer data frames described in asound.h. This command is intended to replace current aplay. The most of features and command line parameters come from aplay as much as possible, while it has more better feature and code to maintain.
This commit adds an entry point for this command. Current option system of aplay is still available, while this command has a sub-command system like commands in iproute2.
Currently, two sub-commands are supported; 'list' and 'transfer'. The 'list' sub-command has the same effect as '-l' and '-L' options of aplay. The 'transfer' sub-command has the same effect as the main feature of aplay. For the sub-command system, an option for stream direction is required; '-P' for playback and '-C' for capture. If you create symbolic links to this binary for aplay/arecord, please execute: $ ln -s axfer aplay $ ln -s axfer arecord
Actual code for each sub-command will be implemented in later commits.
Signed-off-by: Takashi Sakamoto o-takashi@sakamocchi.jp
Although all patches have been merged, below are a few suggestions:
- Please make help working. It took some time for me until figuring
out the fact that the direction option is mandatory, for example.
- The input/output from/to a terminal can be checked via isatty().
That allows us to see garbages by a mistakenly started command.
If my understanding is correct, usage of 'isatty(3)' can return error in a case that users just run 'axfer transfer -P (-)'.
Right.
In current implementation, axfer continues to call of 'read(2)' and receive '-EAGAIN' till receiving teminate signals.
The playback isn't too bad (although I don't see any useful scenario, either). The worse is the capture that prints junk texts that sometimes mandates the terminal reset.
- A man page. (I thought Debian mandates it?)
I have a plan to write help/man in this development period, within this year.
Not only children cheer for a Christmas gift, yeah :)
thanks,
Takashi
In anyway, thanks for your hard work!
Yep. I'm a bit exhausted from this tough work, and need refresh time. Anyway, thank you for applying this PR.
Regards
Takashi Sakamoto
participants (3)
-
GitHub pull_request - opened
-
Takashi Iwai
-
Takashi Sakamoto