From aefdb3349e4a26d2b85d669ece177833c9c80622 Mon Sep 17 00:00:00 2001 From: NY00123 Date: Mon, 11 Dec 2023 19:37:08 +0200 Subject: [PATCH 1/2] Added an experimental ALSA sequencer client: - Essentially a user-level ALSA equivalent of the WinMM driver. - The given implementation uses SDL2 for outputting audio. - The user has to select an FM bank via a command-line argument. - Other options are not configurable (except for cases like SDL2's environment variables), and Nuked OPL3 v1.8 is always used. --- CMakeLists.txt | 11 ++ README.md | 1 + utils/adlalsaseq/CMakeLists.txt | 18 ++ utils/adlalsaseq/adlalsaseq.c | 292 ++++++++++++++++++++++++++++++++ 4 files changed, 322 insertions(+) create mode 100644 utils/adlalsaseq/CMakeLists.txt create mode 100644 utils/adlalsaseq/adlalsaseq.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fd89e9..126905a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -208,6 +208,10 @@ if(WIN32) option(WITH_WINMMDRV "Build a WinMM MIDI driver" OFF) endif() +if(UNIX) + option(WITH_ALSASEQ "Build an ALSA sequencer" OFF) +endif() + if(NOT WIN32 AND NOT VITA AND NOT NINTENDO_3DS @@ -461,6 +465,10 @@ if(WIN32 AND WITH_WINMMDRV) add_subdirectory(utils/winmm_drv) endif() +if(UNIX AND WITH_ALSASEQ) + add_subdirectory(utils/adlalsaseq) +endif() + if(libADLMIDI_STATIC) install(TARGETS ADLMIDI_static EXPORT libADLMIDIStaticTargets @@ -563,3 +571,6 @@ message("EXAMPLE_SDL2_AUDIO = ${EXAMPLE_SDL2_AUDIO}") if(WIN32) message("WITH_WINMMDRV = ${WITH_WINMMDRV}") endif() +if(UNIX) + message("WITH_ALSASEQ = ${WITH_ALSASEQ}") +endif() diff --git a/README.md b/README.md index 215ed75..90f014b 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ The library is licensed under in it's parts LGPL 2.1+, GPL v2+, GPL v3+, and MIT * **WITH_WINMMDRV** - (ON/OFF, default OFF) (Windows platform only) Compile the WinMM MIDI driver to use libOPNMIDI as a system MIDI device. * **WITH_WINMMDRV_PTHREADS** - (ON/OFF, default ON) Link libwinpthreads statically (when using pthread-based builds). * **WITH_WINMMDRV_MINGWEX** - (ON/OFF, default OFF) Link libmingwex statically (when using vanilla MinGW builds). Useful for targetting to pre-XP Windows versions. +* **WITH_ALSASEQ** - (ON/OFF, default OFF) (Linux and compatible environments) Build the ADLMIDI ALSA sequencer client (Requires SDL2). * **WITH_OLD_UTILS** - (ON/OFF, default OFF) Build old utilities to dump some bank formats, made by original creator of ADLMIDI * **EXAMPLE_SDL2_AUDIO** - (ON/OFF, default OFF) Build also a simple SDL2 demo MIDI player diff --git a/utils/adlalsaseq/CMakeLists.txt b/utils/adlalsaseq/CMakeLists.txt new file mode 100644 index 0000000..677b5c0 --- /dev/null +++ b/utils/adlalsaseq/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required (VERSION 3.2) + +if(NOT UNIX) + message(FATAL_ERROR "ALSASeq: This component is for UNIX and UNIX-like platforms only") +endif() + +add_executable(adlalsaseq adlalsaseq.c) + +target_link_libraries(adlalsaseq PRIVATE ADLMIDI) +libADLMIDI_find_SDL2() +target_link_libraries(adlalsaseq PRIVATE ADLMIDI_SDL2) + +find_package(ALSA REQUIRED) +include_directories(${ALSA_INCLUDE_DIRS}) +target_link_libraries(adlalsaseq PRIVATE ${ALSA_LIBRARIES}) + +install(TARGETS adlalsaseq + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/utils/adlalsaseq/adlalsaseq.c b/utils/adlalsaseq/adlalsaseq.c new file mode 100644 index 0000000..4840afe --- /dev/null +++ b/utils/adlalsaseq/adlalsaseq.c @@ -0,0 +1,292 @@ +/* Copyright (C) 2023 NY00123 + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include "SDL.h" + +typedef struct ALSA_Seq_Data +{ + snd_seq_t *handle; + int port; + unsigned n_connections; +} ALSA_Seq_Data; + +typedef struct Audio_Device_Data +{ + SDL_AudioDeviceID id; + int freq; +} Audio_Device_Data; + +// ALSA sequencer + +static void check_seq_error(int ret, const char *msg) +{ + if (ret < 0) + { + puts(msg); + exit(1); + } +} + +static ALSA_Seq_Data midi_open(void) +{ + ALSA_Seq_Data data; + + check_seq_error(snd_seq_open(&data.handle, "default", SND_SEQ_OPEN_INPUT, 0), + "Could not open sequencer"); + + check_seq_error(snd_seq_set_client_name(data.handle, "libADLMIDI"), + "Could not set client name"); + + data.port = snd_seq_create_simple_port( + data.handle, "libADLMIDI port", + SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_APPLICATION); + check_seq_error(data.port, "Could not create sequencer port"); + + data.n_connections = 0; + printf("Opened sequencer on %d:%d\n", + snd_seq_client_id(data.handle), data.port); + + + return data; +} + +static void midi_close(const ALSA_Seq_Data *data) +{ + check_seq_error(snd_seq_delete_simple_port(data->handle, data->port), + "Could not delete sequencer port"); + check_seq_error(snd_seq_close(data->handle), "Could not close sequencer"); +} + +// ADLMIDI player + +static void check_adl_error(int ret, struct ADL_MIDIPlayer *player, + const char *msg) +{ + if (ret < 0) + { + printf("%s: %s\n", msg, adl_errorInfo(player)); + exit(1); + } +} + +static struct ADL_MIDIPlayer *player_open(int bank_id, int freq) +{ + struct ADL_MIDIPlayer *player = adl_init(freq); + if (!player) + { + printf("ADLMIDI player initialization failed: %s\n", adl_errorString()); + exit(1); + } + + check_adl_error(adl_switchEmulator(player, ADLMIDI_EMU_NUKED), + player, "Switching ADLMIDI emulator failed"); + check_adl_error(adl_setBank(player, bank_id), + player, "Setting ADLMIDI bank failed"); + + adl_rt_resetState(player); + return player; +} + +static void player_close(struct ADL_MIDIPlayer *player) { adl_close (player); } + +// SDL audio device + +static Audio_Device_Data digi_open(struct ADL_MIDIPlayer **player_ptr, + SDL_AudioCallback callback) +{ + SDL_AudioSpec desired_spec, obtained_spec; + Audio_Device_Data data; + if (SDL_Init(SDL_INIT_AUDIO) < 0) + { + printf("SDL_Init failed, %s\n", SDL_GetError()); + exit(3); + } + desired_spec.format = AUDIO_S16SYS; + desired_spec.channels = 2; + desired_spec.freq = 49716; + desired_spec.samples = 1024; + desired_spec.callback = callback; + desired_spec.userdata = player_ptr; + data.id = SDL_OpenAudioDevice(NULL, 0, &desired_spec, &obtained_spec, + SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); + if (data.id <= 0) + { + printf("SDL_OpenAudioDevice failed, %s\n", SDL_GetError()); + exit(3); + } + + printf("SDL audio device opened, requested rate %d, got %d\n", + desired_spec.freq, obtained_spec.freq); + data.freq = obtained_spec.freq; + + return data; +} + +static void digi_close(const Audio_Device_Data *data) +{ + SDL_CloseAudioDevice(data->id); + SDL_Quit(); +} + +// Making it all work + +void midi_process(struct ADL_MIDIPlayer *player, ALSA_Seq_Data *seq_data, + SDL_AudioDeviceID audio_device) +{ + snd_seq_event_t *ev = NULL; + snd_seq_event_input(seq_data->handle, &ev); + + if (!ev) + return; + + if ((ev->type == SND_SEQ_EVENT_PORT_SUBSCRIBED) || + (ev->type == SND_SEQ_EVENT_PORT_UNSUBSCRIBED)) + { + printf("%s, sender %u:%u, dst %u:%u\n", + ev->type == SND_SEQ_EVENT_PORT_SUBSCRIBED ? "Subscribed" : "Unsubscribed", + ev->data.connect.sender.client, ev->data.connect.sender.port, + ev->data.connect.dest.client, ev->data.connect.dest.port); + + if (ev->type == SND_SEQ_EVENT_PORT_SUBSCRIBED) + { + if (!(seq_data->n_connections++)) + SDL_PauseAudioDevice(audio_device, 0); + } + else + { + if (!(--seq_data->n_connections)) + SDL_PauseAudioDevice(audio_device, 1); + } + } + + SDL_LockAudioDevice(audio_device); + + switch (ev->type) + { + case SND_SEQ_EVENT_NOTEON: + adl_rt_noteOn(player, ev->data.note.channel, ev->data.note.note, ev->data.note.velocity); + break; + case SND_SEQ_EVENT_NOTEOFF: + adl_rt_noteOff(player, ev->data.note.channel, ev->data.note.note); + break; + case SND_SEQ_EVENT_KEYPRESS: + adl_rt_noteAfterTouch(player, ev->data.note.channel, ev->data.note.note, ev->data.note.velocity); + break; + case SND_SEQ_EVENT_CONTROLLER: + adl_rt_controllerChange(player, ev->data.control.channel, ev->data.control.param, ev->data.control.value); + break; + case SND_SEQ_EVENT_PGMCHANGE: + adl_rt_patchChange(player, ev->data.control.channel, ev->data.control.value); + break; + case SND_SEQ_EVENT_CHANPRESS: + adl_rt_channelAfterTouch(player, ev->data.control.channel, ev->data.control.value); + break; + case SND_SEQ_EVENT_PITCHBEND: + adl_rt_pitchBend(player, ev->data.control.channel, ev->data.control.value + 8192); + break; + case SND_SEQ_EVENT_SYSEX: + adl_rt_systemExclusive(player, (ADL_UInt8 *)ev->data.ext.ptr, ev->data.ext.len); + break; + + case SND_SEQ_EVENT_PORT_SUBSCRIBED: + case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: + adl_reset(player); + break; + } + + SDL_UnlockAudioDevice(audio_device); +} + +static void sound_callback(void *player_ptr, Uint8 *stream, int len) +{ + adl_generate(*(struct ADL_MIDIPlayer **)player_ptr, + len / 2, (short *)stream); +} + +// The rest of the code + +static volatile sig_atomic_t keep_running = 1; + +static void signal_callback(int signum) +{ + if (signum == SIGINT) + keep_running = 0; +} + +static void check_args(int argc, char **argv, int *out_bank_id) +{ + int i, n_banks = adl_getBanksCount(); + const char *const *bank_names = adl_getBankNames(); + + int bank_id = (argc == 2) ? atoi(argv[1]) : -1; + + if ((bank_id < 0) || (bank_id >= n_banks)) + { + puts("=============================\n" + " libADLMIDI ALSA sequencer \n" + "=============================\n" + "\n" + "Usage: adlalsaseq \n" + "\n" + " Available embedded banks by number:\n"); + for (i = 0; i < n_banks; i++) + printf(" %2d = %s\n", i, bank_names[i]); + puts(""); + exit(0); + } + *out_bank_id = bank_id; +} + +int main(int argc, char **argv) +{ + int bank_id = 0; + check_args(argc, argv, &bank_id); + + ALSA_Seq_Data seq_data = midi_open(); + struct ADL_MIDIPlayer *player = 0; + Audio_Device_Data device_data = digi_open(&player, sound_callback); + + struct sigaction action; + memset(&action, 0, sizeof(action)); + action.sa_handler = signal_callback; + if (sigaction(SIGINT, &action, NULL) < 0) + { + perror("Setting signal handler failed: "); + exit(1); + } + + player = player_open(bank_id, device_data.freq); + + while (keep_running) + midi_process(player, &seq_data, device_data.id); + + digi_close(&device_data); + player_close(player); + midi_close(&seq_data); + puts("\nDone"); + + return 0; +} From 16e9f74df309b73887dec5f9d96d5728cd406f60 Mon Sep 17 00:00:00 2001 From: NY00123 Date: Thu, 21 Dec 2023 00:06:28 +0200 Subject: [PATCH 2/2] ALSA sequencer client: Define port as SND_SEQ_PORT_TYPE_MIDI_GENERIC --- utils/adlalsaseq/adlalsaseq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/adlalsaseq/adlalsaseq.c b/utils/adlalsaseq/adlalsaseq.c index 4840afe..37650e2 100644 --- a/utils/adlalsaseq/adlalsaseq.c +++ b/utils/adlalsaseq/adlalsaseq.c @@ -62,7 +62,7 @@ static ALSA_Seq_Data midi_open(void) data.port = snd_seq_create_simple_port( data.handle, "libADLMIDI port", SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE, - SND_SEQ_PORT_TYPE_APPLICATION); + SND_SEQ_PORT_TYPE_APPLICATION|SND_SEQ_PORT_TYPE_MIDI_GENERIC); check_seq_error(data.port, "Could not create sequencer port"); data.n_connections = 0;