[alsa-devel] [PATCH - alsa-utils 0/5] The amidicat program as a patch
This is my "amidicat" program, which makes it easy and straightforward to interact with the ALSA MIDI sequencer from the command line, by simply using standard input and output.
It took just over a year, to find the time to work on it again, but here it is, formatted as a patch to alsa-utils, as requested.
http://mailman.alsa-project.org/pipermail/alsa-devel/2013-May/062353.html
It has been cleaned up to match "checkpatch" coding standards, and all compiler warnings have been fixed.
The documentation has also been cleaned up, in particular, the Perl examples have been brought up to date: the raw bytes no longer cause errors on Unicode systems.
I respectfully submit this program to alsa-utils, and hope that it makes it in there :)
Josh Lehan
Josh Lehan (5): Adding amidicat subdirectory to SUBDIRS Add amidicat Makefile to AC_OUTPUT list Makefile for amidicat, includes manual page Documentation manual page for amidicat The amidicat program itself, better late than never
Makefile.am | 2 +- amidicat/Makefile.am | 5 + amidicat/amidicat.1 | 392 ++++++++++++ amidicat/amidicat.c | 1708 ++++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 2 +- 5 files changed, 2107 insertions(+), 2 deletions(-) create mode 100644 amidicat/Makefile.am create mode 100644 amidicat/amidicat.1 create mode 100644 amidicat/amidicat.c
Signed-off-by: Josh Lehan alsa@krellan.com
diff --git a/Makefile.am b/Makefile.am index f25eee2..43fb503 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,6 +1,6 @@ AM_CPPFLAGS=-I$(top_srcdir)/include
-SUBDIRS = include alsactl alsaucm utils m4 po +SUBDIRS = include alsactl alsaucm utils m4 po amidicat if ALSAMIXER SUBDIRS += alsamixer endif
Josh Lehan wrote:
+++ b/Makefile.am -SUBDIRS = include alsactl alsaucm utils m4 po +SUBDIRS = include alsactl alsaucm utils m4 po amidicat
This belongs into the seq directory.
Regards, Clemens
On 06/30/2014 04:32 AM, Clemens Ladisch wrote:
Josh Lehan wrote:
+++ b/Makefile.am -SUBDIRS = include alsactl alsaucm utils m4 po +SUBDIRS = include alsactl alsaucm utils m4 po amidicat
This belongs into the seq directory.
OK, good idea, I'll move it there.
Josh
Signed-off-by: Josh Lehan alsa@krellan.com
diff --git a/configure.ac b/configure.ac index a4d2db3..3f43ff9 100644 --- a/configure.ac +++ b/configure.ac @@ -360,4 +360,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) + alsaloop/Makefile amidicat/Makefile)
Signed-off-by: Josh Lehan alsa@krellan.com
diff --git a/amidicat/Makefile.am b/amidicat/Makefile.am new file mode 100644 index 0000000..02a896d --- /dev/null +++ b/amidicat/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = amidicat.1 + +bin_PROGRAMS = amidicat +man_MANS = amidicat.1
Signed-off-by: Josh Lehan alsa@krellan.com
diff --git a/amidicat/amidicat.1 b/amidicat/amidicat.1 new file mode 100644 index 0000000..168a8b9 --- /dev/null +++ b/amidicat/amidicat.1 @@ -0,0 +1,392 @@ +'" t +." Title: amidicat +." Author: Josh Lehan amidicat@krellan.com +." Generator: DocBook XSL Stylesheets v1.75.2 http://docbook.sf.net/ +." Date: 2014-06-29 +." Manual: amidicat +." Source: amidicat +." Language: English +." +.TH "AMIDICAT" "1" "2014-06-29" "amidicat" "amidicat" +." ----------------------------------------------------------------- +." * set default formatting +." ----------------------------------------------------------------- +." disable hyphenation +.nh +." disable justification (adjust text to left margin only) +.ad l +." ----------------------------------------------------------------- +." * MAIN CONTENT STARTS HERE * +." ----------------------------------------------------------------- +.SH "NAME" +amidicat - Hooks up standard input and standard output to the ALSA MIDI +sequencer +.SH "SYNOPSIS" +.HP \w'\fBamidicat\fR\ 'u +\fBamidicat\fR [--help] [--version] [--list] [--name\ \fISTRING\fR] +[--port\ \fICLIENT:PORT\fR] [--addr\ \fICLIENT:PORT\fR] [--hex] +[--verbose] [--nowrite] [--noread] [--delay\ \fIMILLISECONDS\fR] +[--wait\ \fISECONDS\fR] [file...] +.SH "ARGUMENTS" +.PP +All of the following arguments are optional: +.PP +\fB--help\fR +.RS 4 +Displays a help screen, then exits&. +It is a quick summary of the +documentation you are now reading&. +.RE +.PP +\fB--version\fR +.RS 4 +Displays version number, then exits&. +.RE +.PP +\fB--list\fR +.RS 4 +Shows a listing of all ALSA MIDI sequencer devices currently available +for use, then exits&. +.sp +Devices are displayed one per line, containing ALSA port +number (CLIENT:PORT syntax), client name, port name, and flags&. +The flags are: +.PP +r +.RS 4 +Device can be directly read from +.RE +.PP +w +.RS 4 +Device can be directly written to +.RE +.PP +R +.RS 4 +Device can be read from, either directly or via ALSA read subscription +.RE +.PP +W +.RS 4 +Device can be written to, either directly or via ALSA write subscription +.RE +.RE +.PP +\fB--name \fR\fB\fISTRING\fR\fR +.RS 4 +Sets the client name of this program's ALSA connection&. +This might be helpful to identify the amidicat port number, +when using other applications&. +.sp +The effects of setting this option can be observed in the +\fB--list\fR +output&. The default client name is +(lqamidicat(rq&. +.RE +.PP +\fB--port \fR\fB\fICLIENT:PORT\fR\fR, \fB--addr \fR\fB\fICLIENT:PORT\fR\fR +.RS 4 +Makes a direct connection to the given ALSA port number, +for reading and writing&. +If this option is not given, +the default is to only set up a passive connection +for use with the ALSA subscription mechanism&. +.sp +The parameter is either a numeric +\fICLIENT:PORT\fR +(example: +128:0), or the client name or port name of another program's ALSA +connection (use the +\fB--list\fR +option to see what is available)&. +If the name contains spaces, remember to put it in quotes&. +.sp +The +\fB--port\fR +and +\fB--addr\fR +options are identical&. +For syntax compatibility with other ALSA programs, +you have the choice of using either, +but only one of these options may be given&. +.RE +.PP +\fB--hex\fR +.RS 4 +Changes the syntax of input and output, +to be human-readable hex digits, +optionally separated by whitespace&. +This option applies to all input files&. +If this option is not given, the default is raw binary data&. +.sp +Hex digits must be in the range +0 +through +9, +A +through +F, or +a +through +f&. Bytes are optionally separated by whitespace&. +If whitespace is not given, every two hex digits represent a byte&. +An error happens if input is not a hex digit or whitespace&. +.sp +With this option, the MIDI output data will be grouped&. +Each MIDI event will be separated by a newline character, +but each individual byte within the event will only be separated by a space&. +This makes it easy to observe the individual MIDI events&. +.RE +.PP +\fB--verbose\fR +.RS 4 +Provides additional output, on standard error&. +This option is highly recommended&. +.sp +Among other useful things, +it displays this program's ALSA port number, +once the connection to ALSA is made&. +So, if you start another ALSA program afterwards, +with the intention of connecting to this program, +you will know what port number to use&. +.RE +.PP +\fB--nowrite\fR +.RS 4 +Disables writing data into ALSA from standard input&. +When this option is used, +no input files may be given on the command line&. +.sp +The intent of this option is to allow a read-only connection to devices +that do not give write permission&. +These devices can be identified in the +\fB--list\fR +output, as they do not have the +\fBw\fR +or +\fBW\fR +flags&. +.RE +.PP +\fB--noread\fR +.RS 4 +Disables reading data out of ALSA to standard output&. +This option can not be combined with the +\fB--nowrite\fR +option&. +.sp +The intent of this option is to allow a write-only connection +to devices that do not give read permission&. +These devices can be identified in the +\fB--list\fR +output, as they do not have the +\fBr\fR +or +\fBR\fR +flags&. +.RE +.PP +\fB--delay\fR \fIMILLISECONDS\fR +.RS 4 +Inserts a delay, +in milliseconds, + between each MIDI event written to ALSA from input&. +.sp +This is useful for avoiding event loss due to ALSA queue congestion&. +Although this program tries to avoid overflowing ALSA's internal buffer, +events will still be lost if they arrive too quickly&. +Unlike other MIDI programs, +this program does not take any tempo or other timing information +at all into account, +and will write data into ALSA as quickly as it is read from input&. +So, when using this program, +event overflow is highly likely, +unless this option is used&. +.sp +This option will slow down the processing of input&. +A value of 10 milliseconds is usually sufficient to avoid event loss&. +When using the +\fB--verbose\fR +option, +total counts of events processed will be output when this program exits, +so these counters can be used to verify that no events were lost&. +.RE +.PP +\fB--wait\fR \fISECONDS\fR +.RS 4 +After all input has finished being processed, +continue running this program for the given number of seconds, +then exit&. This option can not be combined with the +\fB--nowrite\fR +option&. +.sp +Unless this option is given, +this program will exit immediately after completing all +input&. This includes processing all input files, +and reaching EOF on standard input, if standard input is being used&. +.sp +This option is useful for allowing some additional time +for MIDI data to be received from ALSA&. +For example, this might be useful for sending MIDI commands to a synthesizer, +and then waiting for any responses to come back&. +.RE +.PP +Everything else on the command line is interpreted as +the filename of an input file&. +The special filename +- +represents standard input&. +Filenames are processed left to right, +so this can be used to insert other files before or after standard input&. +If no filenames are given on the command line, +standard input is the default&. +.SH "DESCRIPTION" +.PP +\fBamidicat\fR +hooks up standard input and standard output to the ALSA MIDI sequencer&. +Like +\fBcat\fR(1), +this program will concatenate multiple input files together&. +.SH "EXAMPLES" +.PP +All of these examples use the +\fBTiMidity\fR +software synthesizer&. Change the +\fB--port "TiMidity"\fR +parameter to use anything else you have&. +\fBTiMidity\fR does not provide read permission to ALSA, +so the +\fB--noread\fR +parameter is required. +If you are using a real hardware synthesizer, +you can omit the +\fB--noread\fR +parameter&. +Use caution when trying the examples +that generate random data or play arbitrary files, +as they could send unwanted SysEx commands&. +.PP +\fBExample\ &1.\ &List all ALSA sequencer devices\fR +.PP +\fBamidicat -l\fR +.PP +This will list all MIDI devices on your system +that are visible to the ALSA sequencer&. +Here is sample output: +.sp +.if n {\ +.RS 4 +.} +.nf + Port Client name Port name rwRW + 0:0 System Timer rwR- + 0:1 System Announce r-R- + 14:0 Midi Through Midi Through Port-0 rwRW + 15:0 OSS sequencer Receiver -w-- +128:0 TiMidity TiMidity port 0 -w-W +128:1 TiMidity TiMidity port 1 -w-W +128:2 TiMidity TiMidity port 2 -w-W +128:3 TiMidity TiMidity port 3 -w-W +129:0 Virtual Keyboard Virtual Keyboard r-R- +130:0 amidicat amidicat rwRW +.fi +.if n {\ +.RE +.} +.sp +.PP +\fBExample\ &2.\ &Play MIDI files very quickly\fR +.PP +\fBamidicat --port "TiMidity" --noread --delay 10 *&.mid\fR +.PP +This will play all MIDI files in the current directory very quickly, +without regard for tempo&. The +&.mid +file format will not be recognized, +so headers and other information will play as garbage&. +This would be more useful for files containing nothing but raw MIDI data, +perhaps SysEx commands that you wish to load into a hardware synth&. +.PP +\fBExample\ &3.\ &A better "beep" command\fR +.PP +\fBecho "903C7F" | amidicat --port "TiMidity" --noread --hex\fR +.PP +This plays Middle C&. +It's easy to elaborate on this&. +If you have an application that makes many beeps, +replacing the beeps with MIDI can make it more pleasant-sounding&. +.PP +\fBExample\ &4.\ &Transport MIDI data over the network\fR +.PP +\fBnc -v -l -p 12345 | amidicat --port "TiMidity" --noread\fR +.PP +This command sets up a server listening on port 12345&. +Now, from somewhere else on the Internet, use +\fBnc\fR +to connect to port 12345 of this machine running +\fBamidicat\fR, and start sending it MIDI data&. It should play&. +.PP +\fBExample\ &5.\ &View MIDI notes being played in real time\fR +.PP +\fBamidicat --verbose --hex\fR +.PP +\fBvkeybd --addr 128:0\fR +.PP +After +\fBamidicat\fR +starts up, replace the --addr argument of this example +\fBvkeybd\fR +command, to match what is displayed by +\fBamidicat\fR, if it is different&. +When \fBvkeybd\fR appears, +notes played on its virtual keyboard should be displayed on +\fBamidicat\fR +standard output&. +The bytes of each MIDI command will be displayed, +one per line&. +.PP +\fBExample\ &6.\ &Stress-test your softsynth by playing random data\fR +.PP +\fBcat /dev/urandom | amidicat --port "TiMidity" --noread --delay 1\fR +.PP +Turn down your speakers before trying this&. The +delay +parameter is necessary to avoid flooding ALSA with a deluge of random data&. +Do not use this with a real hardware synthesizer, +or you could damage your settings if, by bad luck, +a random SysEx command is formed! +Hit Control-C to stop the program, +and you might get to hear a rather interesting sound effect +that was caused by playing random data&. +.PP +\fBExample\ &7.\ &Panic button\fR +.PP +\fBperl -e 'for($i=0;$i<16;$i++){print +pack('C3',176+$i,120,0);}' | amidicat --port "TiMidity" +--noread\fR +.PP +This Perl one-liner sends MIDI "All Sound Off" commands to every channel&. +It should silence all audio currently being played&. +You will want to use this command after playing the stress-test above :) +.PP +\fBExample\ &8.\ &Another panic button\fR +.PP +\fBperl -e 'for($i=0;$i<16;$i++){for($j=0;$j<128;$j++){print +pack('C3',128+$i,$j,127);}}' | amidicat --port "TiMidity" +--noread\fR +.PP +Another Perl one-liner, which sends all individual "Note Off" commands&. +It is slower than the above, +but perhaps useful for an older synth that doesn't +understand the "All Sound Off" command&. +.SH "SEE ALSO" +.PP +\fBamidi\fR(1), +\fBaconnect\fR(1) +.SH "AUTHOR" +.PP +\fBJoshua Lehan\fR <&alsa@krellan&.com&> +.RS 4 +Author +.RE
Josh Lehan wrote:
+++ b/amidicat/amidicat.1 +." Generator: DocBook XSL Stylesheets v1.75.2 http://docbook.sf.net/
So this is not the actual source code?
+\fB--hex\fR ... +If this option is not given, the default is raw binary data&.
This is the only place in the documentation where the default input/ output format is described. Please move it to a more prominent location.
Regards, Clemens
On 06/30/2014 04:32 AM, Clemens Ladisch wrote:
Josh Lehan wrote:
+++ b/amidicat/amidicat.1 +." Generator: DocBook XSL Stylesheets v1.75.2 http://docbook.sf.net/
So this is not the actual source code?
It is. In my original release I wrote the documentation page in DocBook format. This was handy, because both HTML and manpage formats could be rendered from there.
However, I noticed DocBook wasn't used anywhere else in alsa-utils, and I didn't want to pull in an external package just for that, so I made the manpage format primary. The "Generator" comment is now superfluous, I will remove it.
+\fB--hex\fR ... +If this option is not given, the default is raw binary data&.
This is the only place in the documentation where the default input/ output format is described. Please move it to a more prominent location.
Good idea, thanks.
Josh
Signed-off-by: Josh Lehan alsa@krellan.com
diff --git a/amidicat/amidicat.c b/amidicat/amidicat.c new file mode 100644 index 0000000..30d6356 --- /dev/null +++ b/amidicat/amidicat.c @@ -0,0 +1,1708 @@ +/* + * amidicat + * + * Copyright © 2010-2014 Joshua Lehan alsa@krellan.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + + +/* + * After searching for a while, it seems there is no program + * to simply send MIDI data into the ALSA MIDI system, + * with a minimum of fuss. + * + * The program aplaymidi(1) will play .MID files, + * but not "live" MIDI data from standard input. + * + * The program amidi(1) can send live data, but only works for + * devices in the ALSA "RawMIDI" namespace, not the overall + * MIDI system, so it can't be used to drive software + * MIDI synthesizers such as TiMidity. + * + * Creating stub .MID files on-the-fly and rapidly playing them + * is a common workaround, but this is undesirable for + * all but the most trivial of uses. + * + * This program attempts to rectify these limitations, + * similarly to the purpose of netcat(1). + * + * What's more, this program also works in both directions + * simultaneously: MIDI data can also be read out from + * the ALSA MIDI system, and made available as standard + * output. + * + * This program is an ALSA sequencer client. + * + * Much code was borrowed from vkeybd(1). + */ + + +#include <pthread.h> +#include <stdio.h> +#include <getopt.h> +#include <alsa/asoundlib.h> +#include <errno.h> +#include <time.h> + +#include "version.h" + + +/* Constants */ +#define DEFAULT_SEQ_NAME ("default") +#define DEFAULT_CLI_NAME ("amidicat") +#define ERR_OPEN (10) +#define ERR_FINDPORT (11) +#define ERR_CONNPORT (12) +#define ERR_PARAM (13) +#define ERR_THREAD (14) +#define DEFAULT_BUFSIZE (65536) +#define MAX_WAIT (1000) +#define MAX_DELAY (1000) +#define FILE_STDIN ("-") +#define MAX_HEX_DIGITS (2) + + +/* Globals */ +pthread_mutex_t g_mutex; +pthread_cond_t g_cond; +int g_is_endinput; + + +/* Structures */ +struct in_args_t { + char **args_ptr; + snd_seq_t *handle; + int is_hex; + int cli_id; + int port_id; + int target_cli; + int target_port; + int spacing_us; + int wait_sec; + int is_read; +}; + +struct out_args_t { + snd_seq_t *handle; + int is_hex; +}; + + +/* + * Better atoi() that returns -1, not 0, if error + * and checks entire string, not just first part, for validity + */ +int +better_atoi(const char *nptr) +{ + char *endptr; + long num; + int ret; + + num = strtol(nptr, &endptr, 0); + if (NULL != nptr && '\0' == *endptr) { + /* String is valid */ + ret = num; + + /* Guard against size mismatch by comparing int and long */ + if (ret == num) + return ret; + } + + return -1; +} + + +/* + * Better usleep() that does not use signals, + * so is safe to use along with threads + */ +void +microsleep(long usec) +{ + struct timespec tv; + + /* No need to sleep if time is not positive */ + if (usec > 0) { + tv.tv_sec = usec / 1000000; + tv.tv_nsec = (usec % 1000000) * 1000; + + /* Ignore return value, waking up early is not a problem */ + nanosleep(&tv, NULL); + } +} + + +/* Prettyprints capabilities mask */ +void +prettyprint_capmask(unsigned int capmask) +{ + /* The r/w letters indicate read/write capability */ + /* Capital R/W letters indicate subscription */ + printf("%s%s%s%s" + , ((capmask & SND_SEQ_PORT_CAP_READ) ? "r" : "-") + , ((capmask & SND_SEQ_PORT_CAP_WRITE) ? "w" : "-") + , ((capmask & SND_SEQ_PORT_CAP_SUBS_READ) ? "R" : "-") + , ((capmask & SND_SEQ_PORT_CAP_SUBS_WRITE) ? "W" : "-") + ); +} + + +/* + * Iterates through list of all active ALSA sequencer ports + * handle = ALSA sequencer handle + * str = String to search for a match for, or NULL to ignore + * is_print = Set to 1 if you want to see output + * outcli = Receives found client ID, if searching + * outport = Receives found port ID, if searching + * Returns 0 if a match was found, -1 if no matches were found + * The search is by exact string match (no wildcards or + * substrings yet). The column of port names will be + * searched first, in top-down order, then the column of + * client names will be similarly searched. First match wins. + */ +int +discover_ports(snd_seq_t *handle, char *str, int is_print, +int *outcli, int *outport) +{ + snd_seq_client_info_t *cli_info; + snd_seq_port_info_t *port_info; + char *cli_name; + char *port_name; + unsigned int port_capmask; + unsigned int port_typemask; + int r; + int cli_id; + int cli_num_ports; + int port_id; + int lowest_port; + int match_bycli_cli_id = -1; + int match_bycli_port_id = -1; + int match_byport_cli_id = -1; + int match_byport_port_id = -1; + int is_first = 1; + + snd_seq_client_info_malloc(&cli_info); + snd_seq_port_info_malloc(&port_info); + + /* Iterate through all clients */ + snd_seq_client_info_set_client(cli_info, -1); + for (;;) { + r = snd_seq_query_next_client(handle, cli_info); + if (r < 0) + break; + + /* Got a client */ + cli_id = snd_seq_client_info_get_client(cli_info); + cli_name = strdup(snd_seq_client_info_get_name(cli_info)); + cli_num_ports = snd_seq_client_info_get_num_ports(cli_info); + + /* Iterate through all ports on this client */ + snd_seq_port_info_set_client(port_info, cli_id); + lowest_port = -1; + snd_seq_port_info_set_port(port_info, lowest_port); + for (;;) { + r = snd_seq_query_next_port(handle, port_info); + if (r < 0) + break; + + /* Got a port */ + port_id = + snd_seq_port_info_get_port(port_info); + port_name = strdup( + snd_seq_port_info_get_name(port_info)); + port_typemask = + snd_seq_port_info_get_type(port_info); + port_capmask = + snd_seq_port_info_get_capability(port_info); + + /* Arbitrarily use lowest port number on this client + * when matching by name */ + if (-1 == lowest_port) + lowest_port = port_id; + + /* Print header */ + if (is_first) { + /* The field lengths of strings + * are taken from "aplaymidi -l" */ + if (is_print) + printf(" Port %-32s %-32s rwRW\n" + , "Client name" + , "Port name" + ); + + is_first = 0; + } + + /* Print line */ + /* FUTURE: Perhaps also print type bits if relevant */ + if (is_print) { + printf("%3d:%-3d %-32s %-32s " + , cli_id + , port_id + , cli_name + , port_name + ); + prettyprint_capmask(port_capmask); + printf("\n"); + } + + /* FUTURE: Perhaps also make use of this information */ + /* Suppress warning message */ + (void)cli_num_ports; + (void)port_typemask; + + /* Test for match, if not already matched by port */ + if (NULL != str && -1 == match_byport_cli_id) { + if (0 == strcasecmp(str, port_name)) { + match_byport_cli_id = cli_id; + match_byport_port_id = port_id; + } + } + + free(port_name); + } + + /* Test for match, if not already matched by client */ + if (NULL != str && -1 == match_bycli_cli_id) { + if (0 == strcasecmp(str, cli_name)) { + if (-1 != lowest_port) { + match_bycli_cli_id = cli_id; + match_bycli_port_id = lowest_port; + } + } + } + + free(cli_name); + } + + snd_seq_port_info_free(port_info); + snd_seq_client_info_free(cli_info); + + /* If both matched, prefer port match (most specific) + * over client match */ + if (-1 != match_byport_cli_id) { + *outcli = match_byport_cli_id; + *outport = match_byport_port_id; + return 0; + } + if (-1 != match_bycli_cli_id) { + *outcli = match_bycli_cli_id; + *outport = match_bycli_port_id; + return 0; + } + + /* No match found, or no string given to match by */ + return 1; +} + + +/* + * Parses string into ALSA client:port numbers + * String can be 2 numbers separated by ":" or "." or "-" delimiters, + * or the name of a client or port can be given + * and a lookup will be done in order to learn the numbers. + * Results stored in "outcli" and "outport". + * Returns 0 if successful, or -1 if unparseable or not found. + */ +int +str_to_cli_port(snd_seq_t *handle, char *str, int *outcli, int *outport) +{ + char *delim; + char *nextstr; + int cli_id = -1; + int port_id = -1; + int r; + char savedch; + + /* Fairly generous in choice of delimiters */ + delim = strpbrk(str, ":.-"); + if (delim) { + /* Look at string immediately following the delimiter */ + nextstr = delim + 1; + + /* Perform string fission */ + savedch = *delim; + *delim = '\0'; + + cli_id = better_atoi(str); + port_id = better_atoi(nextstr); + + *delim = savedch; + } + + /* If not parseable as numbers, try string match */ + if (cli_id < 0 || port_id < 0) { + r = discover_ports(handle, str, 0, &cli_id, &port_id); + + /* Return error, regardless of ID, if discovery failed */ + if (r < 0) + return -1; + } + + /* Not found or not parseable */ + if (cli_id < 0 || port_id < 0) + return -1; + + /* Both are good results */ + *outcli = cli_id; + *outport = port_id; + return 0; +} + + +/* + * Opens a new connection to the ALSA sequencer + * Can be opened for input (read), output (write), or both + * Returns handle or NULL if error, prints message if error + */ +snd_seq_t * +seq_open(int is_read, int is_write) +{ + snd_seq_t *handle; + int err; + int mode; + + /* Default to duplex unless told otherwise */ + mode = SND_SEQ_OPEN_DUPLEX; + + /* Read only */ + if (is_read && !is_write) + mode = SND_SEQ_OPEN_INPUT; + + /* Write only */ + if (is_write && !is_read) + mode = SND_SEQ_OPEN_OUTPUT; + + /* Always use the default "sequencer name" here, + * this is not the client name */ + err = snd_seq_open(&handle, DEFAULT_SEQ_NAME, mode, 0); + if (err < 0) { + fprintf(stderr, "Unable to open ALSA sequencer: %s\n", + strerror(errno)); + return NULL; + } + return handle; +} + + +/* + * Closes the connection to the ALSA sequencer + */ +void +seq_close(snd_seq_t *handle) +{ + snd_seq_close(handle); +} + + +/* + * Sets up the ALSA sequencer for use + * Sets our client name + * Allocates our port + * Connects to remote client and port, + * unless they are -1 then use ALSA subscription mechanism instead + * Learns our own ID's and saves them in "outcli" and "outport" + * Returns 0 if all went well, or -1 if error, prints message if error + */ +int +seq_setup(snd_seq_t *handle, char *cli_name, int target_cli, int target_port, +int is_read, int is_write, int *outcli, int *outport) +{ + int cli_id; + int port_id; + int r; + int caps = 0; + int is_subs = 0; + + /* Get our client ID */ + cli_id = snd_seq_client_id(handle); + *outcli = cli_id; + + /* Set our name */ + r = snd_seq_set_client_name(handle, cli_name); + if (r < 0) { + /* This early in the program, it's not threaded, + * errno is OK to use */ + fprintf(stderr, "Unable to set ALSA client name: %s\n", + strerror(errno)); + return -1; + } + + /* Enable reading and/or writing */ + if (is_read) + caps |= SND_SEQ_PORT_CAP_READ; + if (is_write) + caps |= SND_SEQ_PORT_CAP_WRITE; + if (is_read && is_write) + caps |= SND_SEQ_PORT_CAP_DUPLEX; + + if (target_cli < 0 || target_port < 0) { + /* Use ALSA subscription mechanism if target not given */ + /* FUTURE: Might want both subscription and target at once */ + if (is_read) + caps |= SND_SEQ_PORT_CAP_SUBS_READ; + if (is_write) + caps |= SND_SEQ_PORT_CAP_SUBS_WRITE; + + /* There is no corresponding SUBS_DUPLEX flag */ + is_subs = 1; + } + + /* Open origin port */ + /* FUTURE: Do we need any more type bits here? */ + port_id = snd_seq_create_simple_port(handle, cli_name, caps, + SND_SEQ_PORT_TYPE_MIDI_GENERIC); + if (port_id < 0) { + fprintf(stderr, "Unable to open ALSA sequencer port: %s\n", + strerror(errno)); + return -1; + } + + /* Connect both to and from target port, if not using subscription */ + if (!is_subs) { + if (is_write) { + r = snd_seq_connect_to(handle, port_id, + target_cli, target_port); + if (r < 0) { + fprintf(stderr, + "Unable to connect to ALSA port %d:%d: %s\n" + , target_cli + , target_port + , strerror(errno) + ); + return -1; + } + } + if (is_read) { + r = snd_seq_connect_from(handle, port_id, + target_cli, target_port); + if (r < 0) { + fprintf(stderr, + "Unable to connect from ALSA port %d:%d: %s\n" + , target_cli + , target_port + , strerror(errno) + ); + return -1; + } + } + } + + /* Should be all good to go now */ + *outcli = cli_id; + *outport = port_id; + return 0; +} + + +/* + * Writes an event into ALSA + * Blocks/retries until ALSA has received and delivered + * the event (as best as we can verify) + * Tries its best to avoid flooding the ALSA input queue + * ev = The event to be sent into ALSA + * port_id = Our port ID + * target_cli, target_port = The target's client and port ID, + * or -1 to just send to "subscribers" + * spacing_us = Spacing time, in microseconds, + * to be used when busywaiting some loops + * Returns negative error code if error, + * or the number of retries required if successful + */ +int +write_event(snd_seq_t *handle, snd_seq_event_t *ev, int port_id, + int target_cli, int target_port, int spacing_us) +{ + int r; + int ct_output = 0; + int ct_drain = 0; + int ct_sync = 0; + int is_draingood; + int is_syncgood; + int total; + + /* Fill in event data structure, these are macros and never fail */ + snd_seq_ev_set_source(ev, port_id); + + if (target_cli < 0 || target_port < 0) + /* Send to all subscribers, + * possibly playing to an empty house */ + snd_seq_ev_set_subs(ev); + else + snd_seq_ev_set_dest(ev, target_cli, target_port); + + snd_seq_ev_set_direct(ev); + + /* Fire event */ + for (;;) { + /* FUTURE: Maybe have option to let user choose + * between output and output_direct? */ + r = snd_seq_event_output_direct(handle, ev); + if (r < 0) { + /* Return if a real error happened */ + if (-EAGAIN != r) + return r; + + /* Error was "Try again", wait and do just that */ + microsleep(spacing_us); + ct_output++; + continue; + } + + /* Event sent into ALSA, do NOT try again */ + break; + } + + /* Loop until output is fully pushed into ALSA */ + /* FUTURE: Even though this loop works, + * it's still too easy to flood the other end and overrun, + * perhaps a queuing bug internally within ALSA? */ + for (;;) { + is_draingood = 0; + is_syncgood = 0; + + r = snd_seq_drain_output(handle); + if (r < 0) { + /* Return if a real error happened */ + if (-EAGAIN != r) + return r; + } + + /* Stop retrying only when there are no more + * events left to be drained */ + if (0 == r) + is_draingood = 1; + else + ct_drain++; + + r = snd_seq_sync_output_queue(handle); + if (r < 0) { + /* Return if a real error happened */ + if (-EAGAIN != r) + return r; + } + + /* FUTURE: Why does snd_seq_sync_output_queue() + * always return 1, not 0 as it should? */ + if (0 == r || 1 == r) + is_syncgood = 1; + else + ct_sync++; + + /* Only return if both drain and sync are clear */ + if (is_draingood && is_syncgood) + break; + + /* Throttle CPU when busywaiting */ + microsleep(spacing_us); + } + + total = ct_output + ct_drain + ct_sync; + /* FUTURE: Suppress this text if verbose flag + * was given (it becomes redundant) */ + if (total > 0) { + fprintf(stderr, "Incoming congestion"); + if (ct_output > 0) + fprintf(stderr, ", %d output retries", ct_output); + if (ct_drain > 0) + fprintf(stderr, ", %d drain retries", ct_drain); + if (ct_sync > 0) + fprintf(stderr, ", %d sync retries", ct_sync); + fprintf(stderr, "\n"); + } + + return total; +} + + +/* + * Writes a buffer to stdout + * Returns 0 if successful or nonzero if error + * Writes either as binary bytes or hex digits + */ +int +write_stdout(unsigned char *buf, long bufsize, int is_hex) +{ + unsigned char *bufptr; + long size_left; + long size_written; + unsigned int ui; + int r; + unsigned char uc; + + bufptr = buf; + size_left = bufsize; + if (is_hex) { + /* Print hex bytes, e.g. 90 3C 7F */ + while (size_left > 0) { + uc = *bufptr; + ui = uc; + + /* Separate by spaces, + * unless it's the last one which gets newline */ + r = printf("%02X%s", ui, + ((size_left > 1) ? " " : "\n")); + if (r < 0) + return -1; + + bufptr++; + size_left--; + } + } else { + /* Full write to stdout */ + while (size_left > 0) { + size_written = write(STDOUT_FILENO, + bufptr, + size_left); + if (size_written < 0) + return -1; + + bufptr += size_written; + size_left -= size_written; + } + } + + return 0; +} + + +/* + * Parses an incoming unsigned byte of ASCII text, representing a + * hex digit, eventually building up and returning complete hex numbers + * Uses static variables to keep state across calls + * Hex digits are separated by whitespace, or if enough hex digits + * have been read and maximum size has been reached, no separator + * is necessary (so digits can all be ran together) + * If not whitespace, each byte of text must be in range [0-9A-Fa-f] + * Returns unsigned hex number if successful + * Returns -1 if error (unrecognized byte of ASCII text) + * Returns -2 if the complete hex number is not available yet (still good) + * As a special case, pass in a value of -2 when at an input boundary, + * this will reset the state and return the hex number that was in + * progress (if any) + */ +int +parse_hex_byte(int ch) +{ + static int hex_value; + static int read_nybbles; + + int ret; + int nybble; + + /* Parse human-readable digit into nybble value */ + nybble = -1; + if (ch >= '0' && ch <= '9') + nybble = ch - '0'; + if (ch >= 'A' && ch <= 'F') + nybble = 10 + (ch - 'A'); + if (ch >= 'a' && ch <= 'f') + nybble = 10 + (ch - 'a'); + + if (-1 == nybble) { + switch (ch) { + /* Special case accept -2 as a zero-length reset request */ + case -2: + /* Fall through */ + + /* Standard C whitespace */ + /* Avoid usage of isspace() + * because that would introduce locale variations */ + case ' ': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + /* Clear state, and return -2 + * if there was nothing to begin with */ + ret = -2; + if (read_nybbles > 0) + ret = hex_value; + + hex_value = 0; + read_nybbles = 0; + return ret; + + /* No default */ + } + + /* Unrecognized character */ + return -1; + } + + /* Digit is valid, build up a number with it */ + hex_value <<= 4; + hex_value += nybble; + read_nybbles++; + + /* Force number to be finished, if maximum digit count reached */ + if (read_nybbles >= MAX_HEX_DIGITS) { + ret = hex_value; + hex_value = 0; + read_nybbles = 0; + return ret; + } + + /* Successfully stored digit, but nothing to return yet */ + return -2; +} + + +/* + * Reads a byte of input from various sources + * Fills in byte_ptr with the byte that was read + * Uses static variables to keep state across calls + * Pass in the argc array from the command line, + * after all options have been removed. + * Each string in the array should be a filename, and will + * be opened and read from. + * The filename "-" is special, and means standard input. + * Passing in a pointer that is valid but points to NULL, + * indicating an empty command line, + * is also special, and means standard input. + * If is_hex is true, will read human-readable hex digits, + * assemble them into bytes, and return the bytes. + * Returns 1 if successful, -1 if error, + * or 0 if clean EOF after finishing all files. + */ +int +input_byte(char **args_ptr, int is_hex, char *byte_ptr) +{ + /* Static variables for holding state */ + static char **args_iter; + static char *file_name; + static int file_fd = -1; + static int need_open; + static int is_inited; + static int is_delayed_eof; + + unsigned char byte_buf; + int ret; + int val; + int r; + + /* Only do initialization once */ + if (!is_inited) { + args_iter = args_ptr; + need_open = 1; + + is_inited = 1; + } + + /* Keep looping around, opening next files as necessary, + * until we have something to return */ + for (;;) { + /* This gets set if previous file reached EOF, + * or during init */ + if (need_open) { + /* Open next file pointed to */ + file_name = *args_iter; + if (NULL != file_name) { + /* Filename given */ + if (0 == strcmp(FILE_STDIN, file_name)) { + /* Special case + * filename "-" is stdin */ + file_fd = STDIN_FILENO; + } else { + /* Open the given filename */ + if (-1 != file_fd) { + fprintf(stderr, + "Internal error, failed to close previous file\n" + ); + return -1; + } + file_fd = open(file_name, O_RDONLY); + if (-1 == file_fd) { + /* FUTURE: Should it + * auto-advance to next file, + * instead of erroring out? */ + fprintf(stderr, + "Unable to open file %s: %s\n" + , file_name + , strerror(errno) + ); + return file_fd; + } + } + } else { + /* No files at all on command line, + * special case read from stdin */ + file_name = "standard input"; + file_fd = STDIN_FILENO; + } + + /* Successfully at beginning of next file */ + if (is_hex) { + /* Init hex state, better already be empty + * from previous file */ + val = parse_hex_byte(-2); + if (-2 != val) { + fprintf(stderr, + "Internal error, failed to clear hex state across files\n" + ); + return -1; + } + } + need_open = 0; + } + + /* Should have a valid file_fd at this point */ + if (is_delayed_eof) { + /* No new reading of data during this pass, + * just held over EOF from last time */ + ret = 0; + is_delayed_eof = 0; + } else { + /* Read a byte of raw input (might be a hex digit) */ + ret = read(file_fd, &byte_buf, 1); + } + + if (-1 == ret) { + /* Ignore harmless errors and retry */ + if (EINTR == errno || EAGAIN == errno) + continue; + + fprintf(stderr, + "Problem reading from file %s: %s\n", + file_name, strerror(errno)); + return ret; + } + + /* Advance to next file, if EOF detected in this file */ + if (0 == ret) { + /* The file might have ended + * in the middle of a hex digit */ + if (is_hex) { + val = parse_hex_byte(-2); + if (-2 != val) { + /* Poor man's coroutine: delay EOF by + * one pass, insert this final byte */ + is_delayed_eof = 1; + + /* Return final byte */ + *byte_ptr = (char)val; + return 1; + } + } + + if (-1 != file_fd) { + /* Don't close stdin, we may need it again if + * user gives "-" more than once + * on command line */ + if (file_fd != STDIN_FILENO) { + r = close(file_fd); + if (0 != r) { + /* This error is nonfatal, + * don't return here */ + fprintf(stderr, + "Problem closing file %s: %s\n" + , file_name + , strerror(errno) + ); + } + } + file_fd = -1; + } + + /* Take another look at command line */ + file_name = *args_iter; + if (NULL != file_name) { + /* Advance to next file in sequence */ + args_iter++; + + file_name = *args_iter; + if (NULL != file_name) { + /* Next file will be opened + * after we loop around */ + need_open = 1; + continue; + } else { + /* Finished with all files + * on command line */ + return 0; + } + } else { + /* No files at all on command line, + * EOF means end of stdin */ + return 0; + } + } + + /* Successfully read a byte of data */ + if (1 == ret) { + /* Piece hex number together */ + if (is_hex) { + val = parse_hex_byte(byte_buf); + + /* Successfully read a byte, + * but hex number not complete yet */ + if (-2 == val) + continue; + + if (-1 == val) { + fprintf(stderr, + "Unrecognizable hex digit text in file %s: %c (%d)\n" + , file_name + , (int)byte_buf + , (int)byte_buf + ); + return -1; + } + + /* Successfully assembled hex number */ + *byte_ptr = (char)val; + return ret; + } + + /* Hex not used, return byte exactly as it was read */ + *byte_ptr = (char)byte_buf; + return ret; + } + + /* Should never get here */ + break; + } + + /* Should never get here */ + fprintf(stderr, "Internal error in file reading: %d\n", ret); + return -1; +} + + +void * +stdin_loop(void *args) +{ + struct in_args_t *in_args; + char **args_ptr; + snd_midi_event_t *parser; + snd_seq_t *handle; + snd_seq_event_t ev; + long ct_bytes = 0; + int cli_id; + int port_id; + int target_cli; + int target_port; + int spacing_us; + int wait_sec; + int is_read; + int is_hex; + int r; + int i; + int is_active = 1; + int ct_events = 0; + int ct_congested = 0; + char bytein; + + /* Recover arguments */ + in_args = (struct in_args_t *)args; + args_ptr = in_args->args_ptr; + is_hex = in_args->is_hex; + handle = in_args->handle; + cli_id = in_args->cli_id; + port_id = in_args->port_id; + target_cli = in_args->target_cli; + target_port = in_args->target_port; + spacing_us = in_args->spacing_us; + wait_sec = in_args->wait_sec; + is_read = in_args->is_read; + + snd_midi_event_new(DEFAULT_BUFSIZE, &parser); + snd_midi_event_init(parser); + snd_midi_event_reset_decode(parser); + + snd_midi_event_no_status(parser, 1); + + /* Reset event */ + snd_seq_ev_clear(&ev); + + /* FUTURE: Might need to maintain our own buffer, + * to overcome ALSA SysEx size limitation */ + while (is_active) { + /* Read from input, either stdin or files */ + r = input_byte(args_ptr, is_hex, &bytein); + switch (r) { + case 0: /* Clean EOF */ + is_active = 0; + break; + + case 1: /* Byte received */ + /* No action necessary */ + ct_bytes++; + break; + + default: + /* Error messages already printed by input_byte() */ + is_active = 0; + break; + } + + /* Feed byte into parser, as int */ + i = bytein; + r = snd_midi_event_encode_byte(parser, i, &ev); + switch (r) { + case 0: /* More bytes needed for event */ + /* No action necessary */ + break; + + case 1: /* Message complete */ + /* Send completed event into ALSA */ + r = write_event(handle, &ev, port_id, + target_cli, target_port, spacing_us); + if (r < 0) { + fprintf(stderr, + "Event write error: %s\n", + strerror(-r)); + is_active = 0; + break; + } + + /* The return value was the number of retries */ + if (r > 0) + ct_congested++; + + /* Reset event after write */ + snd_seq_ev_clear(&ev); + ct_events++; + + /* Wait for spacing between events, if desired */ + microsleep(spacing_us); + break; + + default: /* Error */ + fprintf(stderr, + "Internal error, from ALSA event encode byte: %s\n" + , strerror(-r) + ); + is_active = 0; + break; + } + } + + /* Input finished */ + /* FUTURE: Perhaps an option to suppress this */ + fprintf(stderr, + "Input total: %d MIDI messages, %ld bytes", + ct_events, ct_bytes); + if (ct_congested > 0) { + fprintf(stderr, + ", %d events congested", ct_congested); + } + fprintf(stderr, "\n"); + + /* Give some time for output-only if the user desires */ + microsleep(1000000 * wait_sec); + + /* Set global variable, under mutex, so other thread sees it */ + pthread_mutex_lock(&g_mutex); + g_is_endinput = 1; + pthread_mutex_unlock(&g_mutex); + + /* Tell the reader thread that we are done */ + if (is_read) { + /* Send a dummy message to ourself, + * so ALSA gets unblocked in other thread */ + snd_seq_ev_clear(&ev); + r = write_event(handle, &ev, port_id, cli_id, + port_id, spacing_us); + + /* FUTURE: Indicate error result to reader thread */ + if (r < 0) { + fprintf(stderr, + "Final event write error: %s\n" + , strerror(-r) + ); + } + } + + snd_midi_event_free(parser); + + /* FUTURE: Perhaps bubble up an error result */ + return NULL; +} + + +void * +stdout_loop(void *args) +{ + struct out_args_t *out_args; + unsigned char *buffer; + snd_seq_t *handle; + snd_midi_event_t *parser; + snd_seq_event_t *evptr; + long size_ev; + long ct_bytes = 0; + int is_hex; + int r; + int is_active = 1; + int ct_overruns = 0; + int ct_events = 0; + int ct_nonevents = 0; + int is_endinput; + + /* Recover arguments */ + out_args = (struct out_args_t *)args; + handle = out_args->handle; + is_hex = out_args->is_hex; + + snd_midi_event_new(DEFAULT_BUFSIZE, &parser); + snd_midi_event_init(parser); + snd_midi_event_reset_decode(parser); + + snd_midi_event_no_status(parser, 1); + + /* Allocate buffer */ + buffer = malloc(DEFAULT_BUFSIZE); + + while (is_active) { + /* ALSA will set this pointer to somewhere within itself, + * if successful */ + /* FUTURE: This is not threadsafe, but since this is + * the only thread that does ALSA event input, + * hopefully it's OK for now */ + evptr = NULL; + + /* BLOCK until event comes in from ALSA */ + r = snd_seq_event_input(handle, &evptr); + if (r < 0) { + /* ENOSPC indicates that ALSA's internal buffer + * overran and we lost some events */ + if (-ENOSPC == r) { + /* FUTURE: Only show this + * if verbose is turned on */ + fprintf(stderr, + "Reported overrun while reading from ALSA to output\n" + ); + ct_overruns++; + continue; + } else { + fprintf(stderr, + "Error reading event from ALSA to output: %s\n" + , strerror(-r) + ); + is_active = 0; + break; + } + } + if (NULL == evptr) { + /* FUTURE: Shouldn't happen, perhaps remove this */ + fprintf(stderr, + "Internal error reading event from ALSA\n"); + is_active = 0; + break; + } + + /* Check global flag, under lock, + * see if other thread is telling us to exit */ + pthread_mutex_lock(&g_mutex); + is_endinput = g_is_endinput; + pthread_mutex_unlock(&g_mutex); + if (is_endinput) { + /* Clean exit */ + is_active = 0; + break; + } + + /* Unpack event into bytes */ + size_ev = snd_midi_event_decode(parser, buffer, + DEFAULT_BUFSIZE, evptr); + if (size_ev < 0) { + /* ENOENT indicates an event that is + * not a MIDI message, silently skip it */ + if (-ENOENT == size_ev) { + ct_nonevents++; + /* FUTURE: Suppress this with quiet option */ + fprintf(stderr, + "Received non-MIDI message\n"); + continue; + } else { + fprintf(stderr, + "Error decoding event from ALSA to output: %s\n" + , strerror(-size_ev) + ); + is_active = 0; + break; + } + } + + /* FUTURE: Might need some code here to cat multiple + * SysEx events together (0xF0 ... 0xF7), to overcome ALSA + * internal size limit splitting them */ + + /* Output to stdout */ + if (size_ev > 0) { + r = write_stdout(buffer, size_ev, is_hex); + if (r < 0) { + fprintf(stderr, + "Error writing output: %s\n", + strerror(errno)); + is_active = 0; + break; + } + + ct_bytes += size_ev; + } + + ct_events++; + } + + /* This block will only be reached + * once other thread tells us to exit */ + /* If blocked in ALSA above, + * a dummy event will need to be faked up, + * to get ALSA to return */ + /* FUTURE: Perhaps suppress this text with quiet option */ + fprintf(stderr, + "Output total: %d MIDI messages, %ld bytes", + ct_events, ct_bytes); + if (ct_nonevents > 0) + fprintf(stderr, ", %d non-MIDI events", ct_nonevents); + if (ct_overruns > 0) + fprintf(stderr, ", %d ALSA read overruns", ct_overruns); + fprintf(stderr, "\n"); + + free(buffer); + snd_midi_event_free(parser); + + /* FUTURE: Perhaps bubble up an error result */ + return NULL; +} + + +/* Trivial function */ +void +show_version(void) +{ + printf("amidicat version %s by Joshua Lehan alsa@krellan.com\n", + SND_UTIL_VERSION_STR); +} + + +void +help_screen(char *exename) +{ + /* Put help screen on stdout, not stderr, + * because help screen replaces normal output */ + /* Apologies for the awkward line breaks, + * the mandate of the checkpatch script left no alternative */ + printf("Usage: %s\n", exename); + printf(" [--help] [--version] [--list]\n"); + printf(" [--name STRING]\n"); + printf(" [--port CLIENT:PORT] [--addr CLIENT:PORT]\n"); + printf(" [--hex] [--verbose] [--nowrite] [--noread]\n"); + printf(" [--delay MILLISECONDS] [--wait SECONDS]\n"); + printf(" input files....\n"); + printf( + "amidicat(1) hooks up standard input and standard output to the ALSA sequencer.\n" + ); + printf( + "Like cat(1), this program will concatenate multiple input files together.\n" + ); + printf("--help = Show this help screen and exit.\n"); + printf("--version = Show version line and exit. This version: %s\n", + SND_UTIL_VERSION_STR); + printf( + "--list = Show list of all ALSA sequencer devices and exit:\n" + ); + printf( + " For each usable ALSA client and port, number and name are shown,\n" + ); + printf( + " and flags: r,w = port can be read from or written to directly,\n" + ); + printf( + " R,W = also can use ALSA "subscription" to read or write.\n" + ); + printf( + "--name = Sets name of this program's ALSA connection, as shown\n" + ); + printf(" in --list, default is "%s".\n", + DEFAULT_CLI_NAME); + printf( + "--port = Makes direct connection to ALSA port, for reading and\n" + ); + printf( + " writing, instead of the default which is just to set up\n" + ); + printf( + " a passive connection for use with ALSA "subscription".\n" + ); + printf( + " Syntax is either numeric CLIENT:PORT (example: 128:0), or name of\n" + ); + printf( + " another program's ALSA connection (use --list to see available).\n" + ); + printf( + "--addr = For compatibility, an exact synonym of the --port option.\n" + ); + printf( + "--hex = Change input and output to be human-readable hex\n" + ); + printf( + " digits (example: 90 3C 7F) instead of binary MIDI bytes.\n" + ); + printf( + "--verbose = Provide additional, useful, output to standard error.\n" + ); + printf( + "--nowrite = Disable writing data to ALSA from standard input.\n" + ); + printf( + " Intended for allowing connection to read-only devices.\n" + ); + printf( + " Input files may not be given when this option is used.\n" + ); + printf( + "--noread = Disable reading data from ALSA for standard output.\n" + ); + printf( + " Intended for allowing connection to write-only devices.\n" + ); + printf( + "--delay = Inserts a delay, in milliseconds, between each MIDI\n" + ); + printf(" event submitted to ALSA from standard input.\n"); + printf( + " Intended for avoiding event loss due to queue congestion.\n" + ); + printf( + "--wait = After all input is finished, continue running program for\n" + ); + printf(" this amount of time, in seconds.\n"); + printf( + " Intended for allowing output to continue after input.\n" + ); +} + + +int +main(int argc, char **argv) +{ + /* For getopt */ + struct option long_options[] = { + { "help", 0, NULL, 'h' }, + { "list", 0, NULL, 'l' }, + { "name", 1, NULL, 'n' }, + { "port", 1, NULL, 'p' }, + { "addr", 1, NULL, 'a' }, + { "hex", 0, NULL, 'x' }, + { "delay", 1, NULL, 'd' }, + { "wait", 1, NULL, 'w' }, + { "verbose", 0, NULL, 'v' }, + { "version", 0, NULL, 'V' }, + { "nowrite", 0, NULL, 'W' }, + { "noread", 0, NULL, 'R' }, + { NULL, 0, NULL, 0 } + }; + + /* For threading */ + struct in_args_t in_args; + struct out_args_t out_args; + pthread_t in_thread; + pthread_t out_thread; + int is_in_started = 0; + int is_out_started = 0; + + snd_seq_t *handle = NULL; + char *cli_name; + int cli_id; + int port_id; + int target_cli = -1; + int target_port = -1; + int ret; + int c; + int r; + int is_write = 1; + int is_read = 1; + int is_done = 0; + int is_list = 0; + int is_hex = 0; + int is_verbose = 0; + int is_help = 0; + int is_version = 0; + int spacing_us = 0; + int wait_sec = 0; + + /* Initialize defaults */ + cli_name = strdup(DEFAULT_CLI_NAME); + + /* Parse options */ + while (!is_done) { + c = getopt_long(argc, argv, "hln:p:a:xd:w:vVWR", + long_options, NULL); + switch (c) { + case 'h': /* --help */ + is_help = 1; + break; + + case 'l': /* --list */ + is_list = 1; + break; + + case 'n': /* --name */ + free(cli_name); + cli_name = strdup(optarg); + break; + + case 'p': /* --port */ + case 'a': /* --addr, exactly the same */ + /* Open ALSA sequencer early, + * we need it for port lookup */ + if (NULL == handle) + handle = seq_open(is_read, is_write); + if (NULL == handle) { + /* Abort program if unable to open */ + ret = ERR_OPEN; + goto cleanup; + } + r = str_to_cli_port(handle, optarg, + &target_cli, &target_port); + if (r < 0) { + /* Abort program if unable + * to locate target port */ + fprintf(stderr, + "Unable to find ALSA sequencer port: %s\n" + , optarg + ); + ret = ERR_FINDPORT; + goto cleanup; + } + break; + + case 'x': /* --hex */ + is_hex = 1; + break; + + case 'd': /* --delay */ + spacing_us = better_atoi(optarg); + if (spacing_us < 0) { + fprintf(stderr, + "Parameter for --delay must be a positive integer: %s\n" + , optarg + ); + ret = ERR_PARAM; + goto cleanup; + } + if (spacing_us > MAX_DELAY) { + fprintf(stderr, + "Parameter for --delay must be %d or less: %s\n" + , MAX_DELAY + , optarg + ); + ret = ERR_PARAM; + goto cleanup; + } + /* Argument is in milliseconds, + * but stored value is in microseconds */ + spacing_us *= 1000; + break; + + case 'w': /* --wait */ + wait_sec = better_atoi(optarg); + if (wait_sec < 0) { + fprintf(stderr, + "Parameter for --wait must be a positive integer: %s\n" + , optarg + ); + ret = ERR_PARAM; + goto cleanup; + } + if (wait_sec > MAX_WAIT) { + fprintf(stderr, + "Parameter for --wait must be %d or less: %s\n" + , MAX_WAIT + , optarg + ); + ret = ERR_PARAM; + goto cleanup; + } + break; + + case 'v': /* --verbose */ + /* FUTURE: Perhaps multiple verbosity levels */ + is_verbose = 1; + break; + + case 'V': /* --version */ + is_version = 1; + break; + + case 'W': /* --nowrite */ + is_write = 0; + break; + + case 'R': /* --noread */ + is_read = 0; + break; + + case -1: /* Clean end of getopt processing */ + is_done = 1; + break; + + default: /* Error */ + /* Error message already has been printed by getopt */ + ret = ERR_PARAM; + goto cleanup; + break; + } + } + + /* If both read and write are disabled, there's no point in running */ + if (!is_read && !is_write) { + fprintf(stderr, + "Parameters --noread and --nowrite cannot coexist\n"); + ret = ERR_PARAM; + goto cleanup; + } + + /* If write to ALSA is disabled, nothing to do with input files */ + if (!is_write) { + /* Command line must be empty after the options */ + if (NULL != argv[optind]) { + fprintf(stderr, + "Parameter --nowrite must not be used with any input files\n" + ); + ret = ERR_PARAM; + goto cleanup; + } + } + + /* If write to ALSA is disabled, no input, + * so no waiting after input */ + if (!is_write) { + if (wait_sec > 0) { + fprintf(stderr, + "Parameters --nowrite and --wait cannot coexist\n" + ); + ret = ERR_PARAM; + goto cleanup; + } + } + + /* For --help, show help screen and exit successfully */ + if (is_help) { + help_screen(argv[0]); + ret = 0; + goto cleanup; + } + + /* For --version, show version line and exit successfully */ + if (is_version) { + show_version(); + ret = 0; + goto cleanup; + } + + /* Open ALSA sequencer, if it is not already open */ + if (NULL == handle) + handle = seq_open(is_read, is_write); + if (NULL == handle) { + /* Abort program if unable to open */ + ret = ERR_OPEN; + goto cleanup; + } + + /* Set up connection */ + r = seq_setup(handle, cli_name, target_cli, target_port, + is_read, is_write, &cli_id, &port_id); + if (r < 0) { + ret = ERR_CONNPORT; + goto cleanup; + } + + /* For --list, show list of ports and exit successfully */ + if (is_list) { + discover_ports(handle, NULL, 1, NULL, NULL); + ret = 0; + goto cleanup; + } + + if (is_verbose) { + fprintf(stderr, + "Connected to ALSA sequencer on port %d:%d\n", + cli_id, port_id); + } + + /* Initialize globals used for thread synchronization */ + pthread_mutex_init(&g_mutex, NULL); + pthread_cond_init(&g_cond, NULL); + g_is_endinput = 0; + + /* The output thread reads from ALSA and provides output */ + if (is_read) { + /* Start output thread first, to avoid input backlog */ + out_args.handle = handle; + out_args.is_hex = is_hex; + + r = pthread_create(&out_thread, NULL, stdout_loop, &out_args); + if (r != 0) { + fprintf(stderr, + "Unable to start output thread: %s\n", + strerror(errno)); + ret = ERR_THREAD; + goto threadwait; + } + + is_out_started = 1; + } + + /* The input thread takes input and writes to ALSA */ + if (is_write) { + /* Start input thread */ + /* Save pointer to rest of command line, after options */ + in_args.args_ptr = &(argv[optind]); + in_args.handle = handle; + in_args.is_hex = is_hex; + in_args.cli_id = cli_id; + in_args.port_id = port_id; + in_args.target_cli = target_cli; + in_args.target_port = target_port; + in_args.spacing_us = spacing_us; + in_args.wait_sec = wait_sec; + in_args.is_read = is_read; + + r = pthread_create(&in_thread, NULL, stdin_loop, &in_args); + if (r != 0) { + fprintf(stderr, + "Unable to start input thread: %s\n", + strerror(errno)); + ret = ERR_THREAD; + goto threadwait; + } + + is_in_started = 1; + } + + /* Initialization successful, now just wait for threads to exit */ + ret = 0; + +threadwait: + /* Reap threads */ + if (is_in_started) + pthread_join(in_thread, NULL); + if (is_out_started) + pthread_join(out_thread, NULL); + +cleanup: + /* Cleanup */ + if (handle != NULL) + seq_close(handle); + free(cli_name); + + return ret; +}
Josh Lehan wrote:
+++ b/amidicat/amidicat.c +#define MAX_HEX_DIGITS (2)
Is this symbol defined for future compatibility with larger bytes? ;-)
- int is_hex;
For booleans, use "bool" from <stdbool.h>.
+prettyprint_capmask(unsigned int capmask) +{
- /* The r/w letters indicate read/write capability */
- /* Capital R/W letters indicate subscription */
Would anybody understand the difference?
Just restrict the listing to ports that you can handle.
port_name = strdup(
snd_seq_port_info_get_name(port_info));
Please note that checkpatch is just a tool to find problems; introducing new problems like this just to shut it up is counterproductive.
(Overlong lines could be a sign that the nesting is too deep, but the ALSA function names do not help ...)
+str_to_cli_port(snd_seq_t *handle, char *str, int *outcli, int *outport)
Why not use snd_seq_parse_address()?
- r = snd_seq_set_client_name(handle, cli_name);
- if (r < 0) {
/* This early in the program, it's not threaded,
* errno is OK to use */
fprintf(stderr, "Unable to set ALSA client name: %s\n",
strerror(errno));
The error code is in r. Use snd_strerror().
(And errno _is_ thread safe.)
- /* FUTURE: Do we need any more type bits here? */
- port_id = snd_seq_create_simple_port(handle, cli_name, caps,
SND_SEQ_PORT_TYPE_MIDI_GENERIC);
You should set all appropriate TYPE bits. You must set CAP bits if you want to allow others to connect from/to this port.
r = snd_seq_event_output_direct(handle, ev);
if (r < 0) {
/* Return if a real error happened */
if (-EAGAIN != r)
EAGAIN can never happen if the device has not been configured to be non-blocking.
r = snd_seq_drain_output(handle);
snd_seq_event_output_direct() bypasses the output buffer, so this does not make sense.
r = snd_seq_sync_output_queue(handle);
You have not scheduled any events to be delivered later, so this does not make sense.
/* FUTURE: Why does snd_seq_sync_output_queue()
* always return 1, not 0 as it should? */
Because the documentation is wrong.
r = printf("%02X%s", ui,
((size_left > 1) ? " " : "\n"));
Why not use %c?
/* Standard C whitespace */
/* Avoid usage of isspace()
* because that would introduce locale variations */
Where does this program change the locale to be different from "C"? (And what would it matter if a Klingon space were to be ignored? :-)
/* Send a dummy message to ourself,
* so ALSA gets unblocked in other thread */
snd_seq_ev_clear(&ev);
This results in a SND_SEQ_EVENT_SYSTEM event. You might want to use one of the USR events ...
/* Check global flag, under lock,
* see if other thread is telling us to exit */
pthread_mutex_lock(&g_mutex);
is_endinput = g_is_endinput;
pthread_mutex_unlock(&g_mutex);
... and if you check for that event (and its source address) here, you don't need a separate mutex.
- /* Apologies for the awkward line breaks,
* the mandate of the checkpatch script left no alternative */
checkpatch looks for printk(). Ignore it.
- pthread_cond_init(&g_cond, NULL);
This is not used.
Regards, Clemens
On 06/30/2014 04:33 AM, Clemens Ladisch wrote:
Josh Lehan wrote:
+++ b/amidicat/amidicat.c +#define MAX_HEX_DIGITS (2)
Is this symbol defined for future compatibility with larger bytes? ;-)
No, it's just to avoid a magic number in the parser later on, makes it somewhat easier to read.
- int is_hex;
For booleans, use "bool" from <stdbool.h>.
Good suggestion.
+prettyprint_capmask(unsigned int capmask) +{
- /* The r/w letters indicate read/write capability */
- /* Capital R/W letters indicate subscription */
Would anybody understand the difference?
Just restrict the listing to ports that you can handle.
I wanted to give a complete listing, because it's useful for diagnostics and troubleshooting. It can handle both direct and subscription connections.
It's surprising how different the various permissions are, for the default devices on my system, and so it's useful to see this.
port_name = strdup(
snd_seq_port_info_get_name(port_info));
Please note that checkpatch is just a tool to find problems; introducing new problems like this just to shut it up is counterproductive.
Didn't think this was a problem, to copy this string locally. It's correctly freed later.
+str_to_cli_port(snd_seq_t *handle, char *str, int *outcli, int *outport)
Why not use snd_seq_parse_address()?
Nice, missed that function! Will do. Was looking for some helpful utility function like that.
- r = snd_seq_set_client_name(handle, cli_name);
- if (r < 0) {
/* This early in the program, it's not threaded,
* errno is OK to use */
fprintf(stderr, "Unable to set ALSA client name: %s\n",
strerror(errno));
The error code is in r. Use snd_strerror().
Good suggestion, thanks.
- /* FUTURE: Do we need any more type bits here? */
- port_id = snd_seq_create_simple_port(handle, cli_name, caps,
SND_SEQ_PORT_TYPE_MIDI_GENERIC);
You should set all appropriate TYPE bits. You must set CAP bits if you want to allow others to connect from/to this port.
I think I'm setting the appropriate CAP bits (including DUPLEX if both read and write are enabled).
As for the TYPE bits, that is a point of confusion for me. What would a program like this be classified as? Probably need to add at least SND_SEQ_PORT_TYPE_SOFTWARE. Are the TYPE bits used for any decision-making within the ALSA sequencer, or are they merely informational for users to see?
r = snd_seq_event_output_direct(handle, ev);
if (r < 0) {
/* Return if a real error happened */
if (-EAGAIN != r)
EAGAIN can never happen if the device has not been configured to be non-blocking.
I actually see EAGAIN quite commonly under high load. This one-liner demonstrates it, there's a high number of "output retries" seen, and the counter for this comes from here.
cat /dev/urandom | ./amidicat --port "Midi Through" --verbose > /dev/null
r = snd_seq_drain_output(handle);
snd_seq_event_output_direct() bypasses the output buffer, so this does not make sense.
r = snd_seq_sync_output_queue(handle);
You have not scheduled any events to be delivered later, so this does not make sense.
Interesting, perhaps I can remove this section entirely and it will still work!
/* FUTURE: Why does snd_seq_sync_output_queue()
* always return 1, not 0 as it should? */
Because the documentation is wrong.
Yikes!
r = printf("%02X%s", ui,
((size_left > 1) ? " " : "\n"));
Why not use %c?
I didn't want to print the byte as a raw byte here, but as a human-readable hex number. In the mode where raw bytes are output, write() is used instead, not printf().
/* Standard C whitespace */
/* Avoid usage of isspace()
* because that would introduce locale variations */
Where does this program change the locale to be different from "C"? (And what would it matter if a Klingon space were to be ignored? :-)
It doesn't change the locale. I wanted it to run the same no matter what locale the user had set, however. Nothing else in the program depends on locale, and I didn't want to bring that in just for this.
/* Send a dummy message to ourself,
* so ALSA gets unblocked in other thread */
snd_seq_ev_clear(&ev);
This results in a SND_SEQ_EVENT_SYSTEM event. You might want to use one of the USR events ...
Good suggestion.
/* Check global flag, under lock,
* see if other thread is telling us to exit */
pthread_mutex_lock(&g_mutex);
is_endinput = g_is_endinput;
pthread_mutex_unlock(&g_mutex);
... and if you check for that event (and its source address) here, you don't need a separate mutex.
Thanks, but it seems easier/cleaner just to check the global under a mutex here, as I get the same result with much less code.
- /* Apologies for the awkward line breaks,
* the mandate of the checkpatch script left no alternative */
checkpatch looks for printk(). Ignore it.
This is userspace code, can't use printk().
I learned that I'm granted an indulgence from checkpatch, if the offending long line contains only a string literal (and optional leading whitespace).
- pthread_cond_init(&g_cond, NULL);
This is not used.
Good catch! It was a leftover from an earlier draft.
Regards, Clemens
Thanks for reviewing. Other than these points above, what do you think of the program? Did you try running it? I'll submit a revised patch soon.
Josh
Jun 30 2014, Josh Lehan wrote:
Signed-off-by: Josh Lehan < alsa@krellan.com >
diff --git a/amidicat/amidicat.c b/amidicat/amidicat.c
This is kind of a bugreport for amidicat.
I tested amidicat. The first use case I thought was: [remotehost]$ timidity -iA -Os -B2,8 & [remotehost]$ nc -l -p 12345 | amidicat --port 129:0 --noread --verbose --hex [localhost]$ vkeybd & [localhost]$ amidicat --port 128:0 --nowrite --verbose --hex | nc remotehost 12345
But that have not worked from the beginning.
First, I noticed that "amidicat" is not listed in `aconnect -iol` output. Why? $ timidity -iA -Os -B2,8 & $ nc -l -p 12345 | amidicat --port 128:0 --noread --verbose --hex Then in another terminal: $ aconnect -iol client 0: 'System' [type=kernel] 0 'Timer ' 1 'Announce ' client 14: 'Midi Through' [type=kernel] 0 'Midi Through Port-0' client 128: 'TiMidity' [type=user] 0 'TiMidity port 0 ' Connected From: 129:0 1 'TiMidity port 1 ' 2 'TiMidity port 2 ' 3 'TiMidity port 3 ' Notice the "Connected From: 129:0" line, but no "client 129". However: $ amidicat --list Port Client name Port name rwRW 0:0 System Timer rwR- 0:1 System Announce r-R- 14:0 Midi Through Midi Through Port-0 rwRW 128:0 TiMidity TiMidity port 0 -w-W 128:1 TiMidity TiMidity port 1 -w-W 128:2 TiMidity TiMidity port 2 -w-W 128:3 TiMidity TiMidity port 3 -w-W 129:0 amidicat amidicat -w-- 130:0 amidicat amidicat rwRW
Also, it prints nothing in the following case: $ vkeybd & $ amidicat --port 128:0 --nowrite --verbose --hex except initial "Connected to ALSA sequencer on port 130:0" line which itself looks suspicious, since I asked it to connect on port 128:0.
Maybe that line could be like: Created ALSA sequencer port 130:0 or even Created ALSA sequencer port 130:0, connected to 128:0
However `aseqdump` works like that: $ vkeybd & $ aseqdump -p 128:0
Even more interesting test: run them all. [console1]$ vkeybd & [console1]$ aseqdump -p 128:0 press a key, as expected I see: 128:0 Note on 0, note 60, velocity 127 128:0 Note off 0, note 60, velocity 0 while `aseqdump` is still running in another terminal I start `amidicat`: [console2]$ amidicat --port 128:0 --nowrite --verbose --hex then press a few keys and in aseqdump terminal I see: 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 how could that be? I pressed different keys, why same "Note on"? then I interrupt `amidicat` with Ctrl+C and instantly see: 128:0 Note on 0, note 60, velocity 127 128:0 Note off 0, note 60, velocity 0 128:0 Note on 0, note 62, velocity 127 128:0 Note off 0, note 62, velocity 0 128:0 Note on 0, note 64, velocity 127 128:0 Note off 0, note 64, velocity 0 128:0 Note on 0, note 65, velocity 127 128:0 Note off 0, note 65, velocity 0 128:0 Note on 0, note 67, velocity 127 128:0 Note off 0, note 67, velocity 0 amidicat terminal was still empty, nothing except initial "Connected to ..." line.
Expected results: amidicat prints events from vkeybd does not affect other apps, e.g. aseqdump and is listed in `aconnect -iol` output
Note: I tested that on debian oldstable with alsa 1.0.23, could that be the reason? Can you reproduce that on your system?
Thank you for an interesting tool. -- Sergey
On 07/08/2014 04:55 PM, Sergey wrote:
Jun 30 2014, Josh Lehan wrote:
Signed-off-by: Josh Lehan < alsa@krellan.com >
diff --git a/amidicat/amidicat.c b/amidicat/amidicat.c
This is kind of a bugreport for amidicat.
I tested amidicat. The first use case I thought was: [remotehost]$ timidity -iA -Os -B2,8 & [remotehost]$ nc -l -p 12345 | amidicat --port 129:0 --noread --verbose --hex [localhost]$ vkeybd & [localhost]$ amidicat --port 128:0 --nowrite --verbose --hex | nc remotehost 12345
But that have not worked from the beginning.
I tried that myself, and ran into problems, you're right.
1) Subscribers vs. direct
There seems to be a limitation in vkeybd, or a bug in how amidicat connects to vkeybd (but I can't find where the bug might be in amidicat, it seems to be calling ALSA correctly).
The limitation is this: When vkeybd is ran without the --addr argument (in "subscribers" mode), vkeybd won't send data. You hit the keys and nothing is sent.
This doesn't happen on a real synthesizer keyboard hooked up to ALSA, I believe (this needs more testing, though).
However, when vkeybd is ran with --addr, explicitly connecting to another ALSA port (not in "subscribers" mode), it works just fine then! To do this, though, you have to start amidicat first, note the ALSA port that it has received, then start vkeybd after that, using that as the --addr argument to vkeybd.
Somehow, the aseqdump program overcomes this limitation, though! I need to figure out how it does that. I think it has something to do with how the connection is made: simultaneously acting as both a direct connection and a subscription connection at the same time.
2) Unwanted buffering with --hex
My bug here, good catch. I'm not flushing streams often enough, so unwanted buffering takes place. In the default mode (binary output), amidicat uses write(), so output is entirely untouched by stdio. However, when --hex is used, I'm using printf() instead of write()! Easy fix, just haven't done it yet.
3) Confusing options
Assuming bug above is fixed, I'm thinking of making --hex the default, and adding the --binary option to get the previous default of raw binary output. Similarly, make --verbose the default, with corresponding --quiet option.
Also, some recalcitrant programs (like timidity and vkeybd) are very picky about ALSA permissions when trying to make a connection. The --noread and --nowrite options have to be used way too often, and they are annoying and restrictive.
I'm thinking of having amidicat automatically probe for permissions by default, so the user doesn't need to manually apply the --noread and --nowrite constraints. Upon encountering permissions failure, it would give up the direction of I/O through ALSA that failed, either input or output, as if the user had specified --noread or --nowrite, thus reducing required permissions automatically, but otherwise carry on as normal.
However, if this is to become the default, the user should have a way of requiring that a successful read or write connection be made, such as --requireread or --requirewrite. The reason is that silently ignoring permissions failures might lead to unwanted/surprising behavior, and user might want to guard against this.
An example of surprising behavior is that when input is closed, you have to provide the --wait option to amidicat, otherwise it doesn't know how long to keep running for. Usually, closure of input is what notifies the program to stop running. Reason for choosing to do it this way was that I didn't want the program to simply block forever if it has nothing more to do, as that would cause any callers to also block. The --nowrite option defeats this, though, causing the program to run forever until manually stopped. Should simplify this as well, calling it --linger instead of --wait would make more sense.
4) Permissions backwards
Also, what about ALSA permissions that amidicat itself advertises? To make a long story short, I think I have this backwards. No wonder the above options are confusing, they don't work as originally intended!
Try this workflow, it should work:
[remotehost]$ timidity -iA -Os -B2,8 & [remotehost]$ nc -l -p 12345 | amidicat --port "TiMidity" --noread --verbose [localhost]$ amidicat --verbose --wait 300 < /dev/null | nc remotehost 12345 & [localhost]$ vkeybd --addr 999:0
On localhost, replace 999:0 with the actual ALSA port you see on your terminal (amidicat will show it to you by writing to standard error) before starting vkeybd.
I'm referencing TiMidity by name instead of by number, which is a safer behavior to do, since numbers often change.
I'm using binary, not hex, to work around the blocking bug for now (this will shortly not be necessary).
On localhost, I had to give "< /dev/null" because otherwise I wouldn't be able to disassociate standard input from the terminal, a requirement for successfully running in the background. This necessitated the use of the --wait option, which I'm not happy about, because it imposes a time limit. I wouldn't have needed it if I could have given the --nowrite option, however, if I did this, it wouldn't advertise ALSA write permission to other programs. The vkeybd program requires write permission.
First, I noticed that "amidicat" is not listed in `aconnect -iol` output. Why?
That's strange, it's showing up just fine for me. Here's my output:
client 0: 'System' [type=kernel] 0 'Timer ' 1 'Announce ' Connecting To: 15:0 client 14: 'Midi Through' [type=kernel] 0 'Midi Through Port-0' client 128: 'TiMidity' [type=user] 0 'TiMidity port 0 ' 1 'TiMidity port 1 ' 2 'TiMidity port 2 ' 3 'TiMidity port 3 ' client 129: 'amidicat' [type=user] 0 'amidicat '
Also, it prints nothing in the following case: $ vkeybd & $ amidicat --port 128:0 --nowrite --verbose --hex except initial "Connected to ALSA sequencer on port 130:0" line which itself looks suspicious, since I asked it to connect on port 128:0.
That's a point of confusion. There's two ALSA ports in the above example: port 128 (I assume this is vkeybd), and port 130 (amidicat).
When it starts up, the amidicat program is assigned port 130 by ALSA, and this port number is not knowable until runtime, which is why it's printed at runtime. It will still make an outbound connection to the port you specified, which is port 128, though. It's just like a TCP connection on the Internet: in order to communicate, you must have both a source port number and a destination port number.
I probably should add a --localport option, to make this more explicit (and give you the opportunity to request a known local port number instead of having to take whatever ALSA randomly assigns).
However `aseqdump` works like that: $ vkeybd & $ aseqdump -p 128:0
The --port option of amidicat should be equivalent to the -p option of aseqdump.
Even more interesting test: run them all. [console1]$ vkeybd & [console1]$ aseqdump -p 128:0 press a key, as expected I see: 128:0 Note on 0, note 60, velocity 127 128:0 Note off 0, note 60, velocity 0 while `aseqdump` is still running in another terminal I start `amidicat`: [console2]$ amidicat --port 128:0 --nowrite --verbose --hex then press a few keys and in aseqdump terminal I see: 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 128:0 Note on 0, note 60, velocity 127 how could that be? I pressed different keys, why same "Note on"? then I interrupt `amidicat` with Ctrl+C and instantly see: 128:0 Note on 0, note 60, velocity 127 128:0 Note off 0, note 60, velocity 0 128:0 Note on 0, note 62, velocity 127 128:0 Note off 0, note 62, velocity 0 128:0 Note on 0, note 64, velocity 127 128:0 Note off 0, note 64, velocity 0 128:0 Note on 0, note 65, velocity 127 128:0 Note off 0, note 65, velocity 0 128:0 Note on 0, note 67, velocity 127 128:0 Note off 0, note 67, velocity 0 amidicat terminal was still empty, nothing except initial "Connected to ..." line.
I'm surprised about that as well. Multiple copies of aseqdump can be ran at the same time, and everything still works (ALSA correctly multiplexes the output of vkeybd to all interested subscribers).
However, this doesn't work for amidicat, and that's a bug. When amidicat is running, ALSA blocks delivery of vkeybd events to the aseqdump clients. When amidicat exits, this blockage clears, and all the blocked events are suddenly delivered to aseqdump (that's why you get the flood of output in aseqdump when amidicat exits). Obviously, amidicat is doing something wrong, and confusing ALSA.
Expected results: amidicat prints events from vkeybd does not affect other apps, e.g. aseqdump and is listed in `aconnect -iol` output
Agreed.
Note: I tested that on debian oldstable with alsa 1.0.23, could that be the reason? Can you reproduce that on your system?
As above, I can reproduce the vkeybd and aseqdump bugs/limitations.
However, I can't reproduce "aconnect -iol", it works fine for me, I see amidicat correctly listed in the output.
Also, try "amidicat --list", which will give you output similar to "aplaymidi -l" but include more devices (unlike aplaymidi, amidicat does no filtering, it shows you everything, even including itself in the list).
The list of devices should be the same for both aconnect and amidicat, however, since amidicat includes itself in the listing, you will see an additional entry for that as well.
Thank you for an interesting tool.
You're welcome!
I'm proud of the program, it fills a useful niche.
Thanks for testing. You've exposed some bugs, and use cases I hadn't thought about, so it's back to the drawing board for me, and I hope to have an updated version finished soon.
Josh Lehan
Josh Lehan wrote:
Somehow, the aseqdump program overcomes this limitation, though! I need to figure out how it does that. I think it has something to do with how the connection is made: simultaneously acting as both a direct connection and a subscription connection at the same time.
Have a look at how aseqdump decides what PORT_CAP bits to set.
Also, what about ALSA permissions that amidicat itself advertises? To make a long story short, I think I have this backwards.
These bits specify what _other_ clients can do with the port.
Multiple copies of aseqdump can be ran at the same time, and everything still works (ALSA correctly multiplexes the output of vkeybd to all interested subscribers).
However, this doesn't work for amidicat, and that's a bug. When amidicat is running, ALSA blocks delivery of vkeybd events to the aseqdump clients. When amidicat exits, this blockage clears, and all the blocked events are suddenly delivered to aseqdump (that's why you get the flood of output in aseqdump when amidicat exits). Obviously, amidicat is doing something wrong, and confusing ALSA.
Does the thread actually read the delivered events from the kernel buffer?
Also, try "amidicat --list", which will give you output similar to "aplaymidi -l" but include more devices (unlike aplaymidi, amidicat does no filtering, it shows you everything, even including itself in the list).
It should list only those ports it can use, i.e., connect from/to.
Regards, Clemens
On 07/09/2014 12:54 AM, Clemens Ladisch wrote:
Have a look at how aseqdump decides what PORT_CAP bits to set.
Thanks, will do.
Also, what about ALSA permissions that amidicat itself advertises? To make a long story short, I think I have this backwards.
These bits specify what _other_ clients can do with the port.
Makes sense to me.
Does the thread actually read the delivered events from the kernel buffer?
Should be, I'm calling snd_seq_event_input() in a tight loop. Hoping this is the appropriate function to be calling, and that all the various structures around it are initialized correctly.
Also, try "amidicat --list", which will give you output similar to "aplaymidi -l" but include more devices (unlike aplaymidi, amidicat does no filtering, it shows you everything, even including itself in the list).
It should list only those ports it can use, i.e., connect from/to.
It already does, in a way. Anything that has read/write permission, direct and/or subscription, is usable. I like showing everything, it's useful for diagnostics/troubleshooting. If user wants filtered output they can apply that later (or perhaps I'll add it when adding the --quiet flag). I still think the most complete/useful output should be the default.
Josh
Josh Lehan wrote:
On 07/09/2014 12:54 AM, Clemens Ladisch wrote:
(unlike aplaymidi, amidicat does no filtering, it shows you everything, even including itself in the list).
It should list only those ports it can use, i.e., connect from/to.
It already does, in a way. Anything that has read/write permission, direct and/or subscription, is usable. I like showing everything, it's useful for diagnostics/troubleshooting.
Showing unusable ports _will_ introduce user errors.
If you are doing troubleshooting for something that does not involve amidicat, the complete port list is already available in /proc/asound/seq/clients.
Regards, Clemens
participants (3)
-
Clemens Ladisch
-
Josh Lehan
-
Sergey