From 7547242d0faf39046d68a762b323517317a4681a Mon Sep 17 00:00:00 2001 From: Miroslav Kovac Date: Sun, 28 Apr 2024 15:53:15 +0200 Subject: [PATCH] aseqsend: initial version aseqsend is a command-line utility which allows one to send SysEx (system exclusive) data to ALSA MIDI seqencer port. It can also send any other MIDI commands. Closes: https://github.com/alsa-project/alsa-utils/pull/257 Signed-off-by: Miroslav Kovac Signed-off-by: Jaroslav Kysela --- .gitignore | 1 + configure.ac | 2 +- seq/Makefile.am | 2 +- seq/aseqsend/Makefile.am | 5 + seq/aseqsend/aseqsend.1 | 59 +++++ seq/aseqsend/aseqsend.c | 478 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 seq/aseqsend/Makefile.am create mode 100644 seq/aseqsend/aseqsend.1 create mode 100644 seq/aseqsend/aseqsend.c diff --git a/.gitignore b/.gitignore index ad4b3f4..b61e1df 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ seq/aconnect/aconnect seq/aplaymidi/aplaymidi seq/aplaymidi/arecordmidi seq/aseqdump/aseqdump +seq/aseqsend/aseqsend seq/aseqnet/aseqnet speaker-test/speaker-test topology/alsatplg.1 diff --git a/configure.ac b/configure.ac index 02b47fc..a3d2e06 100644 --- a/configure.ac +++ b/configure.ac @@ -487,7 +487,7 @@ AC_OUTPUT(Makefile alsactl/Makefile alsactl/init/Makefile \ aplay/Makefile include/Makefile iecset/Makefile utils/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 \ + seq/aseqsend/Makefile speaker-test/Makefile speaker-test/samples/Makefile \ alsaloop/Makefile alsa-info/Makefile \ axfer/Makefile axfer/test/Makefile \ nhlt/Makefile) diff --git a/seq/Makefile.am b/seq/Makefile.am index 2c84cee..b0f628a 100644 --- a/seq/Makefile.am +++ b/seq/Makefile.am @@ -1 +1 @@ -SUBDIRS=aconnect aplaymidi aseqdump aseqnet +SUBDIRS=aconnect aplaymidi aseqdump aseqnet aseqsend diff --git a/seq/aseqsend/Makefile.am b/seq/aseqsend/Makefile.am new file mode 100644 index 0000000..62da1ef --- /dev/null +++ b/seq/aseqsend/Makefile.am @@ -0,0 +1,5 @@ +AM_CPPFLAGS = -I$(top_srcdir)/include +EXTRA_DIST = aseqsend.1 + +bin_PROGRAMS = aseqsend +man_MANS = aseqsend.1 diff --git a/seq/aseqsend/aseqsend.1 b/seq/aseqsend/aseqsend.1 new file mode 100644 index 0000000..340c329 --- /dev/null +++ b/seq/aseqsend/aseqsend.1 @@ -0,0 +1,59 @@ +.TH ASEQSEND 1 "11 Mar 2024" + +.SH NAME +.B aseqsend +\- send arbitrary messages to selected ALSA MIDI seqencer port + +.SH SYNOPSIS +aseqsend \-p client:port -s file-name|"hex encoded byte-string" + +.SH DESCRIPTION +.B aseqsend +is a command-line utility which allows one to send SysEx (system exclusive) data to ALSA MIDI seqencer port. +It can also send any other MIDI commands. +Messages to be send can be given in the last argument as hex encoded byte string or can be read from raw binary file. +When sending several SysEx messages at once there is a delay of 1ms after each message as deafult and can be set to different value with option \-i. + +.SH OPTIONS + +.TP +\-h +Prints a list of options. + +.TP +\-V +Prints the current version. + +.TP +\-l +Prints a list of possible output ports. + +.TP +\-v +Prints number of bytes actually sent + +.TP +\-p +Target port by number or name + +.TP +\-s +Send raw binary data from given file name + +.TP +\-i +Interval between SysEx messages in miliseconds + + +A client can be specified by its number, its name, or a prefix of its +name. A port is specified by its number; for port 0 of a client, the +":0" part of the port specification can be omitted. + +.SH EXAMPLES + +aseqsend -p 128:0 "F0 41 10 00 00 64 12 18 00 21 06 59 41 59 4E F7" + +aseqsend -p 128:0 -s I7BulkDump.syx + +.SH AUTHOR +Miroslav Kovac diff --git a/seq/aseqsend/aseqsend.c b/seq/aseqsend/aseqsend.c new file mode 100644 index 0000000..f06e3b1 --- /dev/null +++ b/seq/aseqsend/aseqsend.c @@ -0,0 +1,478 @@ +/* + * aseqsend.c - send arbitrary MIDI messages to selected ALSA MIDI seqencer port + * + * Copyright (c) 2005 Clemens Ladisch + * Copyright (c) 2024 Miroslav Kovac + * + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define _GNU_SOURCE +#include "aconfig.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef unsigned char mbyte_t; + +static snd_seq_t *seq; +static char *port_name = NULL; +static char *send_file_name = NULL; +static char *send_hex; +static mbyte_t *send_data; +static snd_seq_addr_t addr; +static int send_data_length; + +static void error(const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + vfprintf(stderr, format, ap); + va_end(ap); + putc('\n', stderr); +} + +/* prints an error message to stderr, and dies */ +static void fatal(const char *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +static void usage(void) +{ + printf( + "\nUsage: aseqsend -p target-port -s file-name|\"hex encoded bytes\"\n\n" + " -h this help\n" + " -V print current version\n" + " -v verbose\n" + " -l list all sequencer ports\n" + " -p target port by number or name\n" + " -s send binary data from given file name\n" + " -i interval between SysEx messages in miliseconds\n\n"); +} + +static void version(void) +{ + puts("aseqsend version " SND_UTIL_VERSION_STR); +} + +static void *my_malloc(size_t size) +{ + void *p = malloc(size); + if (!p) { + fatal("out of memory"); + exit(EXIT_FAILURE); + } + return p; +} + +static int hex_value(char c) +{ + if ('0' <= c && c <= '9') + return c - '0'; + if ('A' <= c && c <= 'F') + return c - 'A' + 10; + if ('a' <= c && c <= 'f') + return c - 'a' + 10; + error("invalid character %c", c); + return -1; +} + +static void parse_data(void) +{ + const char *p; + int i, value; + + send_data = my_malloc(strlen(send_hex)); + i = 0; + value = -1; /* value is >= 0 when the first hex digit of a byte has been read */ + for (p = send_hex; *p; ++p) { + int digit; + if (isspace((unsigned char)*p)) { + if (value >= 0) { + send_data[i++] = value; + value = -1; + } + continue; + } + digit = hex_value(*p); + if (digit < 0) { + exit(EXIT_FAILURE); + } + if (value < 0) { + value = digit; + } else { + send_data[i++] = (value << 4) | digit; + value = -1; + } + } + if (value >= 0) + send_data[i++] = value; + send_data_length = i; +} + +static void add_send_hex_data(const char *str) +{ + int length; + char *s; + + length = (send_hex ? strlen(send_hex) + 1 : 0) + strlen(str) + 1; + s = my_malloc(length); + if (send_hex) { + strcpy(s, send_hex); + strcat(s, " "); + } else { + s[0] = '\0'; + } + strcat(s, str); + free(send_hex); + send_hex = s; +} + +static void load_file(void) +{ + int fd; + off_t length; + + fd = open(send_file_name, O_RDONLY); + if (fd == -1) { + error("cannot open %s - %s", send_file_name, strerror(errno)); + return; + } + length = lseek(fd, 0, SEEK_END); + if (length == (off_t)-1) { + error("cannot determine length of %s: %s", send_file_name, strerror(errno)); + goto _error; + } + send_data = my_malloc(length); + lseek(fd, 0, SEEK_SET); + if (read(fd, send_data, length) != length) { + error("cannot read from %s: %s", send_file_name, strerror(errno)); + goto _error; + } + if (length >= 4 && !memcmp(send_data, "MThd", 4)) { + error("%s is a Standard MIDI File; use aplaymidi to send it", send_file_name); + goto _error; + } + send_data_length = length; + goto _exit; +_error: + free(send_data); + send_data = NULL; +_exit: + close(fd); +} + +/* error handling for ALSA functions */ +static void check_snd(const char *operation, int err) +{ + if (err < 0) + fatal("Cannot %s - %s", operation, snd_strerror(err)); +} + +static void init_seq(void) +{ + int err; + + /* open sequencer */ + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_OUTPUT, 0); + check_snd("open sequencer", err); + + /* set our client's name */ + err = snd_seq_set_client_name(seq, "aseqsend"); + check_snd("set client name", err); +} + +static void create_port(void) +{ + int err; + + err = snd_seq_create_simple_port(seq, "aseqsend", + SND_SEQ_PORT_CAP_READ, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION); + check_snd("create port", err); +} + + +static void list_ports(void) +{ + snd_seq_client_info_t *cinfo; + snd_seq_port_info_t *pinfo; + + snd_seq_client_info_alloca(&cinfo); + snd_seq_port_info_alloca(&pinfo); + + puts(" Port Client name Port name"); + + snd_seq_client_info_set_client(cinfo, -1); + while (snd_seq_query_next_client(seq, cinfo) >= 0) { + int client = snd_seq_client_info_get_client(cinfo); + + snd_seq_port_info_set_client(pinfo, client); + snd_seq_port_info_set_port(pinfo, -1); + while (snd_seq_query_next_port(seq, pinfo) >= 0) { + + if ((snd_seq_port_info_get_capability(pinfo) + & SND_SEQ_PORT_CAP_WRITE) + != SND_SEQ_PORT_CAP_WRITE) + continue; + printf("%3d:%-3d %-32.32s %s\n", + snd_seq_port_info_get_client(pinfo), + snd_seq_port_info_get_port(pinfo), + snd_seq_client_info_get_name(cinfo), + snd_seq_port_info_get_name(pinfo)); + } + } +} + +void send_midi_msg(snd_seq_event_type_t type, mbyte_t *data, int len) +{ + snd_seq_event_t ev; + + snd_seq_ev_clear(&ev); + snd_seq_ev_set_source(&ev, 0); + snd_seq_ev_set_dest(&ev,addr.client,addr.port); + snd_seq_ev_set_direct(&ev); + + if (type == SND_SEQ_EVENT_SYSEX) { + + snd_seq_ev_set_sysex(&ev,len,data); + + } else { + + mbyte_t ch = data[0] & 0xF; + + switch (type) { + case SND_SEQ_EVENT_NOTEON: + snd_seq_ev_set_noteon(&ev,ch,data[1],data[2]); + break; + case SND_SEQ_EVENT_NOTEOFF: + snd_seq_ev_set_noteoff(&ev,ch,data[1],data[2]); + break; + case SND_SEQ_EVENT_KEYPRESS: + snd_seq_ev_set_keypress(&ev,ch,data[1],data[2]); + break; + case SND_SEQ_EVENT_CONTROLLER: + snd_seq_ev_set_controller(&ev,ch,data[1],data[2]); + break; + case SND_SEQ_EVENT_PITCHBEND: + snd_seq_ev_set_pitchbend(&ev,ch,(data[1]<<7|data[2])-8192); + break; + case SND_SEQ_EVENT_PGMCHANGE: + snd_seq_ev_set_pgmchange(&ev,ch,data[1]); + break; + case SND_SEQ_EVENT_CHANPRESS: + snd_seq_ev_set_chanpress(&ev,ch,data[1]); + break; + default: + ev.type = SND_SEQ_EVENT_NONE; + } + } + + snd_seq_event_output(seq, &ev); + snd_seq_drain_output(seq); + +} + +static int msg_byte_in_range(mbyte_t *data, mbyte_t len) +{ + for (int i=0;i 0x7F) { + error("msg byte value out of range 0-127"); + return 0; + } + } + return 1; +} + + +int main(int argc, char *argv[]) +{ + char c = 0; + char do_send_file = 0; + char do_port_list = 0; + char verbose = 0; + int sysex_interval = 1000; //us + + while ((c = getopt(argc, argv, "hi:Vvlp:s:")) != -1) { + switch (c) { + case 'h': + usage(); + return 0; + case 'V': + version(); + return 0; + case 'v': + verbose = 1; + break; + case 'l': + do_port_list = 1; + break; + case 'p': + port_name = optarg; + break; + case 's': + send_file_name = optarg; + do_send_file = 1; + break; + case 'i': + sysex_interval = atoi(optarg) * 1000; //ms--->us + break; + default: + error("Try 'aseqsend -h' for more information."); + exit(EXIT_FAILURE); + } + } + + if (argc < 2) { + usage(); + exit(EXIT_FAILURE); + } + + if (do_port_list){ + init_seq(); + list_ports(); + exit(EXIT_SUCCESS); + } + + if (port_name == NULL) + fatal("Output port must be specified!"); + + if (do_send_file) { + load_file(); + } else { + /* no file specified ---> send hex bytes from cmd arguments*/ + /* data for send can be specified as multiple arguments */ + for (; argv[optind]; ++optind) { + add_send_hex_data(argv[optind]); + } + if (send_hex) parse_data(); + } + + if (send_data) { + + init_seq(); + create_port(); + + if (snd_seq_parse_address(seq,&addr,port_name) == 0) { + + int sent_data_c = 0;//counter of actually sent bytes + + int k = 0; + + while (k < send_data_length) { + + if (send_data[k] == 0xF0) { + + int c1 = k; + while (c1 < send_data_length) + { + if (send_data[c1] == 0xF7) break; + c1++; + } + + if (c1 == send_data_length) + fatal("SysEx is missing terminating byte (0xF7)"); + + int sl = c1-k+1; + sent_data_c += sl; + + send_midi_msg(SND_SEQ_EVENT_SYSEX, send_data+k,sl); + + usleep(sysex_interval); + + k = c1+1; + + } else { + + mbyte_t tp = send_data[k] >> 4; + + if (tp == 0x8) { + if (msg_byte_in_range(send_data + k + 1, 2)) { + send_midi_msg(SND_SEQ_EVENT_NOTEOFF, send_data+k,3); + sent_data_c += 3; + } + k = k+3; + } else if (tp == 0x9) { + if (msg_byte_in_range(send_data + k + 1, 2)) { + send_midi_msg(SND_SEQ_EVENT_NOTEON, send_data+k,3); + sent_data_c += 3; + } + k = k+3; + } else if (tp == 0xA) { + if (msg_byte_in_range(send_data + k + 1, 2)) { + send_midi_msg(SND_SEQ_EVENT_KEYPRESS, send_data+k,3); + sent_data_c += 3; + } + k = k+3; + } else if (tp == 0xB) { + if (msg_byte_in_range(send_data + k + 1, 2)) { + send_midi_msg(SND_SEQ_EVENT_CONTROLLER, send_data+k,3); + sent_data_c += 3; + } + k = k+3; + } else if (tp == 0xC) { + if (msg_byte_in_range(send_data + k + 1, 1)) { + send_midi_msg(SND_SEQ_EVENT_PGMCHANGE, send_data+k,2); + sent_data_c += 2; + } + k = k+2; + } else if (tp == 0xD) { + if (msg_byte_in_range(send_data + k + 1, 1)) { + send_midi_msg(SND_SEQ_EVENT_CHANPRESS, send_data+k,2); + sent_data_c += 2; + } + k = k+2; + } else if (tp == 0xE) { + if (msg_byte_in_range(send_data + k + 1, 2)) { + send_midi_msg(SND_SEQ_EVENT_PITCHBEND, send_data+k,3); + sent_data_c += 3; + } + k = k+3; + } else k++; + } + } + + if (verbose) + printf("Sent : %u bytes\n",sent_data_c); + + } else { + + error("Unable to parse port name!"); + exit(EXIT_FAILURE); + + } + snd_seq_close(seq); + } + + exit(EXIT_SUCCESS); +}