From 63895354431cd8b0c107e6f4e35e360ad1fb89c7 Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Fri, 7 Oct 2022 18:06:30 +0200 Subject: [PATCH] integrated libmidi into Musique codebase --- .gitmodules | 3 - CHANGELOG.md | 4 + README.md | 10 -- config.mk | 6 +- lib/midi | 1 - musique/interpreter/interpreter.hh | 2 +- musique/main.cc | 4 +- musique/midi/.gitignore | 5 + musique/midi/Makefile | 13 ++ musique/midi/README.md | 13 ++ musique/midi/alsa.cc | 237 +++++++++++++++++++++++++++++ musique/midi/midi.cc | 18 +++ musique/midi/midi.hh | 80 ++++++++++ scripts/release.mk | 2 +- 14 files changed, 377 insertions(+), 21 deletions(-) delete mode 100644 .gitmodules delete mode 160000 lib/midi create mode 100644 musique/midi/.gitignore create mode 100644 musique/midi/Makefile create mode 100644 musique/midi/README.md create mode 100644 musique/midi/alsa.cc create mode 100644 musique/midi/midi.cc create mode 100644 musique/midi/midi.hh diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ca0148a..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lib/midi"] - path = lib/midi - url = https://git.wmi.amu.edu.pl/s416496/midi.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 66251a5..0caa374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `scan` builtin, which computes prefix sum of passed values when provided with addition operator +### Changed + +* Integrated libmidi library into Musique codebase + ### Fixed * Prevented accidental recursive construction of Arrays and Values by making convinience constructor `Value::Value(std::vector&&)` explicit diff --git a/README.md b/README.md index d4f73b9..1a6b7cf 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,6 @@ $ sudo apt install -y build-essential libasound2-dev ## Budowanie interpretera -Jeśli nie posiadasz zależności `lib/midi` to: - -```console -$ git submodule init -$ git submodule update -$ (cd lib/midi; make) -``` - -A następnie zbuduj interpreter: - ```console $ make bin/musique ``` diff --git a/config.mk b/config.mk index 6245c40..9778dfb 100644 --- a/config.mk +++ b/config.mk @@ -1,12 +1,12 @@ MAKEFLAGS="-j $(grep -c ^processor /proc/cpuinfo)" CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-type -Werror=unused-result -Wno-maybe-uninitialized -CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -Ilib/midi/include -I. -Ilib/bestline/ +CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -I. -Ilib/bestline/ RELEASE_FLAGS=-O2 DEBUG_FLAGS=-O0 -ggdb -fsanitize=undefined -DDebug CXX=g++ -LDFLAGS=-L./lib/midi/ -flto -LDLIBS=-lmidi-alsa -lasound -lpthread -static-libgcc -static-libstdc++ +LDFLAGS=-flto +LDLIBS=-lasound -lpthread -static-libgcc -static-libstdc++ diff --git a/lib/midi b/lib/midi deleted file mode 160000 index f42b663..0000000 --- a/lib/midi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f42b663f0d08fc629c7deb26cd32ee06fba76d83 diff --git a/musique/interpreter/interpreter.hh b/musique/interpreter/interpreter.hh index fd796ad..461e722 100644 --- a/musique/interpreter/interpreter.hh +++ b/musique/interpreter/interpreter.hh @@ -1,7 +1,7 @@ #ifndef MUSIQUE_INTERPRETER_HH #define MUSIQUE_INTERPRETER_HH -#include +#include #include #include diff --git a/musique/main.cc b/musique/main.cc index a863023..e9078eb 100644 --- a/musique/main.cc +++ b/musique/main.cc @@ -6,11 +6,11 @@ #include #include -#include -#include #include +#include #include #include +#include #include #include #include diff --git a/musique/midi/.gitignore b/musique/midi/.gitignore new file mode 100644 index 0000000..aa52eb3 --- /dev/null +++ b/musique/midi/.gitignore @@ -0,0 +1,5 @@ +example-usage-alsa +compile_commands.json +.ccls-cache +*.a +*.o diff --git a/musique/midi/Makefile b/musique/midi/Makefile new file mode 100644 index 0000000..fbe9cb5 --- /dev/null +++ b/musique/midi/Makefile @@ -0,0 +1,13 @@ +all: libmidi-alsa.a + +midi.o: midi.cc midi.hh + g++ -c -std=c++20 -Wall -Wextra $< -o $@ -I. + +midi-alsa.o: alsa.cc midi.hh + g++ -c -std=c++20 -Wall -Wextra $< -o $@ -I. + +libmidi-alsa.a: midi.o midi-alsa.o + ar rcs $@ $^ + +example-usage-alsa: example-usage-alsa.cc libmidi-alsa.a + g++ -std=c++20 -Wall -Wextra $< -o $@ -L. -lmidi-alsa -lasound -Iinclude -g diff --git a/musique/midi/README.md b/musique/midi/README.md new file mode 100644 index 0000000..ed1223b --- /dev/null +++ b/musique/midi/README.md @@ -0,0 +1,13 @@ +# `midi` sub library + +Utility library for interfacing MIDI protocol in different environments. + +## Available interfaces + +### ALSA + +To use with ALSA before including library define `MIDI_ENABLE_ALSA_SUPPORT` macro. + +To compile static library use `make libmidi-alsa`. + +When in doubt, see [`example-usage-alsa.cc`](./example-usage-alsa.cc). diff --git a/musique/midi/alsa.cc b/musique/midi/alsa.cc new file mode 100644 index 0000000..1528a1d --- /dev/null +++ b/musique/midi/alsa.cc @@ -0,0 +1,237 @@ +#define MIDI_ENABLE_ALSA_SUPPORT +#include + +#include +#include +#include + +/// Ensures that operation performed by ALSA was successfull +static int ensure_snd(int maybe_error, std::string_view message) +{ + if (maybe_error < 0) { + std::cerr << "ALSA ERROR: " << message << ": " << snd_strerror(maybe_error) << std::endl; + std::exit(1); + } + return maybe_error; +} + +static void create_source_port(midi::ALSA &alsa); +static void create_queue(midi::ALSA &alsa); +static void connect_port(midi::ALSA &alsa); +static void send_note_event(midi::ALSA &alsa, snd_seq_event_type type, uint8_t channel, uint8_t note, uint8_t velocity); + +midi::ALSA::ALSA(std::string connection_name) + : connection_name(connection_name) +{ +} + +void midi::ALSA::init_sequencer() +{ + ensure_snd(snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0), "opening sequencer"); + ensure_snd(snd_seq_set_client_name(seq, connection_name.c_str()), "setting client name"); + ensure_snd(client = snd_seq_client_id(seq), "getting client id"); +} + +void midi::ALSA::connect_output(std::string const& target) +{ + assert(!connected); + ensure_snd(snd_seq_parse_address(seq, &output_port_addr, target.c_str()), "Invalid port specification"); + create_source_port(*this); + create_queue(*this); + connect_port(*this); + connected = true; +} + +void midi::ALSA::connect_input(std::string const& target) +{ + ensure_snd(snd_seq_parse_address(seq, &input_port_addr, target.c_str()), "Invalid port specification"); + input_port = ensure_snd(snd_seq_create_simple_port(seq, connection_name.c_str(), + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_APPLICATION), "create simple port"); + + ensure_snd(snd_seq_connect_from(seq, input_port, input_port_addr.client, input_port_addr.port), + "connect from"); +} + +void midi::ALSA::list_ports(std::ostream& out) const +{ + snd_seq_client_info_t *c; + snd_seq_port_info_t *p; + + // Wonders of C based API <3 + snd_seq_client_info_alloca(&c); + snd_seq_port_info_alloca(&p); + + out << " Port Client name Port name\n"; + + snd_seq_client_info_set_client(c, -1); + while (snd_seq_query_next_client(seq, c) >= 0) { + int client = snd_seq_client_info_get_client(c); + + snd_seq_port_info_set_client(p, client); + snd_seq_port_info_set_port(p, -1); + while (snd_seq_query_next_port(seq, p) >= 0) { + // Port must support MIDI messages + if (!(snd_seq_port_info_get_type(p) & SND_SEQ_PORT_TYPE_MIDI_GENERIC)) continue; + + // Both WRITE and SUBS_WRITE are required + // TODO Aquire knowladge why + auto const exp = SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE; + if ((snd_seq_port_info_get_capability(p) & exp) != exp) continue; + + out << + std::setw(3) << std::right << snd_seq_port_info_get_client(p) << ':' << + std::setw(3) << std::left << snd_seq_port_info_get_port(p) << " " << + std::setw(32) << std::left << snd_seq_client_info_get_name(c) << " " << + snd_seq_port_info_get_name(p) << '\n'; + } + } + out << std::flush; +} + +void midi::ALSA::send_note_on(uint8_t channel, uint8_t note_number, uint8_t velocity) +{ + send_note_event(*this, SND_SEQ_EVENT_NOTEON, channel, note_number, velocity); +} + +void midi::ALSA::send_note_off(uint8_t channel, uint8_t note_number, uint8_t velocity) +{ + send_note_event(*this, SND_SEQ_EVENT_NOTEOFF, channel, note_number, velocity); +} + +void midi::ALSA::send_program_change(uint8_t channel, uint8_t program) +{ + snd_seq_event_t ev; + snd_seq_ev_clear(&ev); + ev.queue = queue; + ev.source.port = 0; + ev.flags = SND_SEQ_TIME_STAMP_TICK; + ev.type = SND_SEQ_EVENT_PGMCHANGE; + ev.time.tick = 0; + ev.dest = output_port_addr; + + snd_seq_ev_set_fixed(&ev); + ev.data.control.channel = channel; + ev.data.control.value = program; + ensure_snd(snd_seq_event_output(seq, &ev), "output event"); + ensure_snd(snd_seq_drain_output(seq), "drain output queue"); +} + +void midi::ALSA::send_controller_change(uint8_t channel, uint8_t controller_number, uint8_t value) +{ + snd_seq_event_t ev; + snd_seq_ev_clear(&ev); + ev.queue = queue; + ev.source.port = 0; + ev.flags = SND_SEQ_TIME_STAMP_TICK; + ev.type = SND_SEQ_EVENT_CONTROLLER; + ev.time.tick = 0; + ev.dest = output_port_addr; + + snd_seq_ev_set_fixed(&ev); + ev.data.control.channel = channel; + ev.data.control.param = controller_number; + ev.data.control.value = value; + ensure_snd(snd_seq_event_output(seq, &ev), "output event"); + ensure_snd(snd_seq_drain_output(seq), "drain output queue"); +} + +void midi::ALSA::input_event_loop(std::stop_token stop_token) +{ + int const poll_fd_count = snd_seq_poll_descriptors_count(seq, POLLIN); + std::vector poll_fds(poll_fd_count); + ensure_snd(snd_seq_poll_descriptors(seq, poll_fds.data(), poll_fds.size(), POLLIN), "poll descriptors"); + while (!stop_token.stop_requested()) { + if (poll(poll_fds.data(), poll_fds.size(), -1) < 0) { + return; + } + while (!stop_token.stop_requested()) { + snd_seq_event_t *event; + if (snd_seq_event_input(seq, &event) < 0) { break; } + if (!event) { continue; } + + switch (event->type) { + case SND_SEQ_EVENT_NOTEON: + if (!event->data.note.velocity) + goto noteoff; + if (note_on_callback) + note_on_callback(event->data.note.channel, event->data.note.note, event->data.note.velocity); + break; + + case SND_SEQ_EVENT_NOTEOFF: + noteoff: + if (note_off_callback) + note_off_callback(event->data.note.channel, event->data.note.note); + break; + + default: + // TODO Add all events as callbacks + break; + } + } + } +} + +bool midi::ALSA::supports_output() const +{ + return output_port < 0; +} + +bool midi::ALSA::supports_input () const +{ + return input_port < 0; +} + +static void create_source_port(midi::ALSA &alsa) +{ + snd_seq_port_info_t *pinfo; + snd_seq_port_info_alloca(&pinfo); + + snd_seq_port_info_set_port(pinfo, alsa.output_port); + snd_seq_port_info_set_port_specified(pinfo, 1); + snd_seq_port_info_set_name(pinfo, alsa.connection_name.c_str()); + + snd_seq_port_info_set_capability(pinfo, 0); + snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + ensure_snd(snd_seq_create_port(alsa.seq, pinfo), "create port"); +} + +static void create_queue(midi::ALSA &alsa) +{ + ensure_snd(alsa.queue = snd_seq_alloc_named_queue(alsa.seq, alsa.connection_name.c_str()), "create queue"); + snd_seq_queue_tempo_t *queue_tempo; + snd_seq_queue_tempo_alloca(&queue_tempo); + + // We assume time division as ticks per quarter + snd_seq_queue_tempo_set_tempo(queue_tempo, 500'000); // 120 BPM + snd_seq_queue_tempo_set_ppq(queue_tempo, 96); + ensure_snd(snd_seq_set_queue_tempo(alsa.seq, alsa.queue, queue_tempo), "set queue tempo"); +} + +static void connect_port(midi::ALSA &alsa) +{ + ensure_snd(snd_seq_connect_to(alsa.seq, alsa.output_port, alsa.output_port_addr.client, alsa.output_port_addr.port), "connect to port"); +} + +static void send_note_event(midi::ALSA &alsa, snd_seq_event_type type, uint8_t channel, uint8_t note, uint8_t velocity) +{ + snd_seq_event_t ev; + snd_seq_ev_clear(&ev); + ev.queue = alsa.queue; + ev.source.port = alsa.output_port; + ev.flags = SND_SEQ_TIME_STAMP_TICK; + + ev.type = type; + ev.time.tick = 0; + ev.dest = alsa.output_port_addr; + + snd_seq_ev_set_fixed(&ev); + ev.data.note.channel = channel; + ev.data.note.note = note; + ev.data.note.velocity = velocity; + + ensure_snd(snd_seq_event_output(alsa.seq, &ev), "output event"); + ensure_snd(snd_seq_drain_output(alsa.seq), "drain output queue"); +} diff --git a/musique/midi/midi.cc b/musique/midi/midi.cc new file mode 100644 index 0000000..a4f11a0 --- /dev/null +++ b/musique/midi/midi.cc @@ -0,0 +1,18 @@ +#include + +#include +#include + +template +concept Enum = std::is_enum_v; + +/// Converts enum to underlying type +static constexpr auto u(Enum auto e) +{ + return static_cast>(e); +} + +void midi::Connection::send_all_sounds_off(uint8_t channel) +{ + send_controller_change(channel, u(Controller::All_Notes_Off), 0); +} diff --git a/musique/midi/midi.hh b/musique/midi/midi.hh new file mode 100644 index 0000000..a8d8db5 --- /dev/null +++ b/musique/midi/midi.hh @@ -0,0 +1,80 @@ +#pragma once +#include +#include + +#ifdef MIDI_ENABLE_ALSA_SUPPORT +#include +#include +#include + +#ifdef assert +#undef assert +#endif + +#endif + +// Documentation of midi messages available at http://midi.teragonaudio.com/tech/midispec.htm +namespace midi +{ + struct Connection + { + virtual bool supports_output() const = 0; + virtual bool supports_input () const = 0; + + virtual void send_note_on (uint8_t channel, uint8_t note_number, uint8_t velocity) = 0; + virtual void send_note_off(uint8_t channel, uint8_t note_number, uint8_t velocity) = 0; + virtual void send_program_change(uint8_t channel, uint8_t program) = 0; + virtual void send_controller_change(uint8_t channel, uint8_t controller_number, uint8_t value) = 0; + + void send_all_sounds_off(uint8_t channel); + + std::function note_on_callback = nullptr; + std::function note_off_callback = nullptr; + }; + +#ifdef MIDI_ENABLE_ALSA_SUPPORT + struct ALSA : Connection + { + explicit ALSA(std::string connection_name); + + /// Initialize connection with ALSA + void init_sequencer(); + + /// Connect with specific ALSA port for outputing MIDI messages + void connect_output(std::string const& target); + + /// Connect with specific ALSA port for reading MIDI messages + void connect_input(std::string const& target); + + /// List available ports + void list_ports(std::ostream &out) const; + + bool supports_output() const override; + bool supports_input () const override; + + void send_note_on (uint8_t channel, uint8_t note_number, uint8_t velocity) override; + void send_note_off(uint8_t channel, uint8_t note_number, uint8_t velocity) override; + void send_program_change(uint8_t channel, uint8_t program) override; + void send_controller_change(uint8_t channel, uint8_t controller_number, uint8_t value) override; + + void input_event_loop(std::stop_token); + + /// Name that is used by ALSA to describe our connection + std::string connection_name = {}; + bool connected = false; + snd_seq_t *seq = nullptr; + long client = 0; + int queue = 0; + snd_seq_addr_t input_port_addr{}; + snd_seq_addr_t output_port_addr{}; + int input_port = -1; + int output_port = -1; + }; +#endif + + /// All defined controllers for controller change message + enum class Controller : uint8_t + { + All_Notes_Off = 120, + }; +} diff --git a/scripts/release.mk b/scripts/release.mk index 13e8ad3..c349850 100644 --- a/scripts/release.mk +++ b/scripts/release.mk @@ -4,6 +4,6 @@ bin/%.o: musique/%.cc @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $< -c -bin/musique: $(Release_Obj) bin/main.o bin/bestline.o lib/midi/libmidi-alsa.a +bin/musique: $(Release_Obj) bin/main.o bin/bestline.o @echo "CXX $@" @$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $(Release_Obj) bin/bestline.o $(LDFLAGS) $(LDLIBS)