#pragma once #include #include #include #include #include namespace libremidi::alsa_seq { struct dummy_processing { explicit dummy_processing(auto&&...) { } }; template using midi_in_base = std::conditional_t; template using midi_in_processing = std::conditional_t< ConfigurationImpl::midi_version == 1, midi1::input_state_machine, dummy_processing>; template class midi_in_impl : public midi_in_base , protected alsa_data , public error_handler { public: struct : ConfigurationBase , ConfigurationImpl { } configuration; midi_in_processing m_processing{this->configuration}; bool require_timestamps() const noexcept { switch (configuration.timestamps) { case timestamp_mode::NoTimestamp: case timestamp_mode::SystemMonotonic: case timestamp_mode::AudioFrame: return false; case timestamp_mode::Absolute: case timestamp_mode::Relative: case timestamp_mode::Custom: return true; } return true; } explicit midi_in_impl(ConfigurationBase&& conf, ConfigurationImpl&& apiconf) : midi_in_base{} , configuration{std::move(conf), std::move(apiconf)} { if (init_client(configuration) < 0) { error( this->configuration, "midi_in_alsa::initialize: error creating ALSA sequencer client " "object."); return; } // Create the input queue if (require_timestamps()) { this->queue_id = snd.seq.alloc_queue(seq); // Set arbitrary tempo (mm=100) and resolution (240) snd_seq_queue_tempo_t* qtempo{}; snd_seq_queue_tempo_alloca(&qtempo); snd.seq.queue_tempo_set_tempo(qtempo, 600000); snd.seq.queue_tempo_set_ppq(qtempo, 240); snd.seq.set_queue_tempo(this->seq, this->queue_id, qtempo); snd.seq.drain_output(this->seq); } // Create the event -> midi encoder { int result = snd.midi.event_new(0, &coder); if (result < 0) { error( this->configuration, "midi_in_alsa::initialize: error during snd_midi_event_new."); return; } snd.midi.event_init(coder); snd.midi.event_no_status(coder, 1); } } ~midi_in_impl() override { // Cleanup. if (this->vport >= 0) snd.seq.delete_port(this->seq, this->vport); if (require_timestamps()) snd.seq.free_queue(this->seq, this->queue_id); snd.midi.event_free(coder); // Close if we do not have an user-provided client object if (!configuration.context) snd.seq.close(this->seq); } libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_SEQ; } [[nodiscard]] int create_port(std::string_view portName) { return alsa_data::create_port( *this, portName, SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION, require_timestamps() ? std::optional{this->queue_id} : std::nullopt); } void start_queue() { if (require_timestamps()) { snd.seq.control_queue(this->seq, this->queue_id, SND_SEQ_EVENT_START, 0, nullptr); this->queue_creation_time = std::chrono::steady_clock::now(); snd.seq.drain_output(this->seq); } } void stop_queue() { if (require_timestamps()) { snd.seq.control_queue(this->seq, this->queue_id, SND_SEQ_EVENT_STOP, 0, nullptr); snd.seq.drain_output(this->seq); } } int connect_port(snd_seq_addr_t sender) { snd_seq_addr_t receiver{}; receiver.client = snd.seq.client_id(this->seq); receiver.port = this->vport; return create_connection(*this, sender, receiver, false); } std::optional to_address(const port_information& p) { return alsa_data::get_port_info(p); } int init_port(std::optional source, std::string_view portName) { this->close_port(); if (!source) return -1; if (int ret = create_port(portName); ret < 0) { error(configuration, "midi_in_alsa::create_port: ALSA error creating port."); return ret; } if (int ret = connect_port(*source); ret < 0) { error( configuration, "midi_in_alsa::create_port: ALSA error making port connection."); return ret; } start_queue(); return 0; } int init_virtual_port(std::string_view portName) { this->close_port(); if (int ret = create_port(portName); ret < 0) return ret; start_queue(); return 0; } void close_port() override { unsubscribe(); stop_queue(); } void set_client_name(std::string_view clientName) override { alsa_data::set_client_name(clientName); } void set_port_name(std::string_view portName) override { alsa_data::set_port_name(portName); } protected: void set_timestamp(const auto& ev, auto& msg) noexcept { static constexpr int64_t nanos = 1e9; switch (configuration.timestamps) { case timestamp_mode::NoTimestamp: msg.timestamp = 0; return; case timestamp_mode::Relative: { const auto t1 = static_cast(ev.time.time.tv_sec) * nanos + static_cast(ev.time.time.tv_nsec); const auto t0 = static_cast(last_time.tv_sec) * nanos + static_cast(last_time.tv_nsec); const auto time = t1 - t0; last_time = ev.time.time; if (this->firstMessage == true) { this->firstMessage = false; msg.timestamp = 0; } else { msg.timestamp = time; } return; } case timestamp_mode::Absolute: { msg.timestamp = ev.time.time.tv_sec * nanos + ev.time.time.tv_nsec; break; } case timestamp_mode::Custom: { msg.timestamp = configuration.get_timestamp(ev.time.time.tv_sec * nanos + ev.time.time.tv_nsec); break; } case timestamp_mode::SystemMonotonic: { namespace clk = std::chrono; msg.timestamp = system_ns(); break; } } } timestamp absolute_timestamp() const noexcept override { return std::chrono::duration_cast( std::chrono::steady_clock::now() - this->queue_creation_time) .count(); } int process_event(const snd_seq_event_t& ev) { if constexpr (ConfigurationImpl::midi_version == 1) { switch (ev.type) { case SND_SEQ_EVENT_PORT_SUBSCRIBED: case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: return 0; case SND_SEQ_EVENT_SYSEX: { if (configuration.ignore_sysex) return 0; else if (ev.data.ext.len > decoding_buffer.size()) decoding_buffer.resize(ev.data.ext.len); break; } } static constexpr timestamp_backend_info timestamp_info{ .has_absolute_timestamps = true, .absolute_is_monotonic = false, .has_samples = false, }; const auto to_ns = [ts = ev.time.time] { return static_cast(ts.tv_sec) * 1'000'000'000 + static_cast(ts.tv_nsec); }; auto buf = decoding_buffer.data(); auto buf_space = decoding_buffer.size(); // FIXME according to the doc snd_midi_event_decode can apparently return multiple events???? const auto avail = snd.midi.event_decode(coder, buf, buf_space, &ev); if (avail > 0) { m_processing.on_bytes( {buf, buf + avail}, m_processing.template timestamp(to_ns, 0)); return 0; } else { return avail; } } return 0; } int process_events() { if constexpr (ConfigurationImpl::midi_version == 1) { snd_seq_event_t* ev{}; event_handle handle{snd}; int result = 0; while ((result = snd.seq.event_input(seq, &ev)) > 0) { handle.reset(ev); if (int err = process_event(*ev); err < 0) return err; } return result; } else { return 0; } } #if __has_include() int process_ump_event(const snd_seq_ump_event_t& ev) { // Filter the message types before any decoding switch (ev.type) { case SND_SEQ_EVENT_PORT_SUBSCRIBED: case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: return 0; case SND_SEQ_EVENT_QFRAME: // MIDI time code case SND_SEQ_EVENT_TICK: // 0xF9 ... MIDI timing tick case SND_SEQ_EVENT_CLOCK: // 0xF8 ... MIDI timing (clock) tick if (configuration.ignore_timing) return 0; break; case SND_SEQ_EVENT_SENSING: // Active sensing if (configuration.ignore_sensing) return 0; break; case SND_SEQ_EVENT_SYSEX: { if (configuration.ignore_sysex) return 0; break; } } // MIDI 2 : no decoder, we can just send the UMP data directly, yay libremidi::ump ump; std::memcpy(ump.data, ev.ump, sizeof(ev.ump)); set_timestamp(ev, ump); configuration.on_message(std::move(ump)); return 0; } int process_ump_events() { snd_seq_ump_event_t* ev{}; event_handle handle{snd}; int result = 0; while ((result = snd.seq.ump.event_input(seq, &ev)) > 0) { handle.reset((snd_seq_event_t*)ev); if (int err = process_ump_event(*ev); err < 0) return err; } return result; } #endif int queue_id{}; // an input queue is needed to get timestamped events snd_seq_real_time_t last_time{}; // Only needed for midi 1 std::vector decoding_buffer = std::vector(4096); std::chrono::steady_clock::time_point queue_creation_time; }; template class midi_in_alsa_threaded : public midi_in_impl { public: midi_in_alsa_threaded(ConfigurationBase&& conf, ConfigurationImpl&& apiconf) : midi_in_impl{std::move(conf), std::move(apiconf)} { if (this->termination_event < 0) { this->template error( this->configuration, "midi_in_alsa::initialize: error creating eventfd."); } } ~midi_in_alsa_threaded() { this->close_port(); } private: bool open_port(const input_port& pt, std::string_view local_port_name) override { if (int err = this->init_port(this->to_address(pt), local_port_name); err < 0) return false; if (!start_thread()) return false; return true; } bool open_virtual_port(std::string_view portName) override { if (int err = this->init_virtual_port(portName); err < 0) return false; if (!this->start_thread()) return false; return true; } void close_port() override { midi_in_impl::close_port(); stop_thread(); } [[nodiscard]] int start_thread() { try { this->thread = std::thread([this] { thread_handler(); }); } catch (const std::system_error& e) { using namespace std::literals; this->unsubscribe(); this->template error( this->configuration, "midi_in_alsa::start_thread: error starting MIDI input thread: "s + e.what()); return false; } return true; } void stop_thread() { termination_event.notify(); if (this->thread.joinable()) this->thread.join(); termination_event.consume(); } void thread_handler() { int poll_fd_count = alsa_data::snd.seq.poll_descriptors_count(this->seq, POLLIN) + 1; auto poll_fds = (struct pollfd*)alloca(poll_fd_count * sizeof(struct pollfd)); poll_fds[0] = this->termination_event; alsa_data::snd.seq.poll_descriptors(this->seq, poll_fds + 1, poll_fd_count - 1, POLLIN); for (;;) { if (alsa_data::snd.seq.event_input_pending(this->seq, 1) == 0) { // No data pending if (poll(poll_fds, poll_fd_count, -1) >= 0) { // We got our stop-thread signal if (termination_event.ready(poll_fds[0])) { break; } } continue; } int res{}; if constexpr (ConfigurationImpl::midi_version == 1) { res = this->process_events(); } #if __has_include() else if constexpr (ConfigurationImpl::midi_version == 2) { res = this->process_ump_events(); } #endif (void)res; #if defined(__LIBREMIDI_DEBUG__) if (res < 0) std::cerr << "midi_in_alsa::thread_handler: MIDI input error: " << snd.strerror(res) << "\n"; #endif } } std::thread thread{}; eventfd_notifier termination_event{}; }; template class midi_in_alsa_manual : public midi_in_impl { public: midi_in_alsa_manual(ConfigurationBase&& conf, ConfigurationImpl&& apiconf) : midi_in_impl{std::move(conf), std::move(apiconf)} { assert(this->configuration.manual_poll); assert(this->configuration.stop_poll); } [[nodiscard]] int init_callback() { using poll_params = typename ConfigurationImpl::poll_parameters_type; this->configuration.manual_poll( poll_params{.addr = this->vaddr, .callback = [this](const auto& ev) { if constexpr (ConfigurationImpl::midi_version == 1) return this->process_event(ev); #if __has_include() else return this->process_ump_event(ev); #endif }}); return 0; } ~midi_in_alsa_manual() { this->close_port(); } bool open_port(const input_port& pt, std::string_view local_port_name) override { if (int err = this->init_port(this->to_address(pt), local_port_name); err < 0) return false; if (int err = init_callback(); err < 0) return false; return true; } bool open_virtual_port(std::string_view name) override { if (int err = this->init_virtual_port(name); err < 0) return false; if (int err = init_callback(); err < 0) return false; return true; } void close_port() override { this->configuration.stop_poll(this->vaddr); midi_in_impl::close_port(); } }; } namespace libremidi { template <> inline std::unique_ptr make>( libremidi::input_configuration&& conf, libremidi::alsa_seq::input_configuration&& api) { if (api.manual_poll) return std::make_unique>( std::move(conf), std::move(api)); else return std::make_unique>( std::move(conf), std::move(api)); } }