#pragma once #include #include #include #include #include #include #include #include namespace libremidi::alsa_raw { class midi_in_impl : public midi1::in_api , public error_handler { public: struct : input_configuration , alsa_raw_input_configuration { } configuration; const libasound& snd = libasound::instance(); explicit midi_in_impl(input_configuration&& conf, alsa_raw_input_configuration&& apiconf) : configuration{std::move(conf), std::move(apiconf)} { fds_.reserve(4); } ~midi_in_impl() override { } bool open_virtual_port(std::string_view) override { warning(configuration, "midi_in_alsa_raw: open_virtual_port unsupported"); return false; } void set_client_name(std::string_view) override { warning(configuration, "midi_in_alsa_raw: set_client_name unsupported"); } void set_port_name(std::string_view) override { warning(configuration, "midi_in_alsa_raw: set_port_name unsupported"); } libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; } // Must be a string such as: "hw:2,4,1" [[nodiscard]] int do_init_port(const char* portname) { constexpr int mode = SND_RAWMIDI_NONBLOCK; if (const int err = snd.rawmidi.open(&midiport_, nullptr, portname, mode); err < 0) { error(this->configuration, "midi_in_alsa_raw::open_port: cannot open device."); return err; } snd_rawmidi_params_t* params{}; snd_rawmidi_params_alloca(¶ms); if (const int err = snd.rawmidi.params_current(midiport_, params); err < 0) return err; if (const int err = snd.rawmidi.params_set_no_active_sensing(midiport_, params, 1); err < 0) return err; #if LIBREMIDI_ALSA_HAS_RAWMIDI_TREAD if (configuration.timestamps == timestamp_mode::NoTimestamp) { if (const int err = snd.rawmidi.params_set_read_mode(midiport_, params, SND_RAWMIDI_READ_STANDARD); err < 0) return err; if (const int err = snd.rawmidi.params_set_clock_type(midiport_, params, SND_RAWMIDI_CLOCK_NONE); err < 0) return err; } else { if (const int err = snd.rawmidi.params_set_read_mode(midiport_, params, SND_RAWMIDI_READ_TSTAMP); err < 0) return err; if (const int err = snd.rawmidi.params_set_clock_type(midiport_, params, SND_RAWMIDI_CLOCK_MONOTONIC); err < 0) return err; } #endif if (const int err = snd.rawmidi.params(midiport_, params); err < 0) return err; return init_pollfd(); } [[nodiscard]] int init_port(const port_information& p) { return do_init_port(raw_from_port_handle(p.port).to_string().c_str()); } [[nodiscard]] int init_pollfd() { const int num_fds = snd.rawmidi.poll_descriptors_count(this->midiport_); this->fds_.clear(); this->fds_.resize(num_fds); return snd.rawmidi.poll_descriptors(this->midiport_, fds_.data(), num_fds); } ssize_t do_read_events(auto parse_func, std::span fds) { // Read events if (fds.empty()) { return (this->*parse_func)(); } else { unsigned short res{}; const int err = snd.rawmidi.poll_descriptors_revents( this->midiport_, fds.data(), static_cast(fds.size()), &res); if (err < 0) return err; // Did we encounter an error during polling if (res & (POLLERR | POLLHUP)) return -EIO; // Is there data to read if (res & POLLIN) return (this->*parse_func)(); } return 0; } ssize_t read_input_buffer() { static const constexpr int nbytes = 1024; static constexpr timestamp_backend_info timestamp_info{ .has_absolute_timestamps = false, .absolute_is_monotonic = false, .has_samples = false, }; unsigned char bytes[nbytes]; ssize_t err = 0; // err is the amount of bytes read while ((err = snd.rawmidi.read(this->midiport_, bytes, nbytes)) > 0) { const auto to_ns = [this] { return absolute_timestamp(); }; decoder_.on_bytes({bytes, bytes + err}, decoder_.timestamp(to_ns, 0)); } return err; } #if LIBREMIDI_ALSA_HAS_RAWMIDI_TREAD ssize_t read_input_buffer_with_timestamps() { static constexpr int nbytes = 1024; static constexpr timestamp_backend_info timestamp_info{ .has_absolute_timestamps = true, .absolute_is_monotonic = true, .has_samples = false, }; unsigned char bytes[nbytes]; struct timespec ts; ssize_t err = 0; // err is the amount of bytes read while ((err = snd.rawmidi.tread(this->midiport_, &ts, bytes, nbytes)) > 0) { const auto to_ns = [ts] { return static_cast(ts.tv_sec) * 1'000'000'000 + static_cast(ts.tv_nsec); }; decoder_.on_bytes({bytes, bytes + err}, decoder_.timestamp(to_ns, 0)); } return err; } #else ssize_t read_input_buffer_with_timestamps() { return read_input_buffer(); } #endif void close_port() override { if (midiport_) snd.rawmidi.close(midiport_); midiport_ = nullptr; } timestamp absolute_timestamp() const noexcept final override { return system_ns(); } snd_rawmidi_t* midiport_{}; std::vector fds_; midi1::input_state_machine decoder_{this->configuration}; }; class midi_in_alsa_raw_threaded : public midi_in_impl { public: midi_in_alsa_raw_threaded(input_configuration&& conf, alsa_raw_input_configuration&& apiconf) : midi_in_impl{std::move(conf), std::move(apiconf)} { if (this->termination_event < 0) { error( this->configuration, "midi_in_alsa::initialize: error creating eventfd."); } } ~midi_in_alsa_raw_threaded() override { // Close a connection if it exists. this->midi_in_alsa_raw_threaded::close_port(); } private: void run_thread(auto parse_func) { fds_.push_back(this->termination_event); for (;;) { // Poll ssize_t err = poll(fds_.data(), fds_.size(), -1); if (err == -EAGAIN) continue; else if (err < 0) return; else if (termination_event.ready(fds_.back())) break; err = do_read_events(parse_func, {fds_.data(), fds_.size() - 1}); if (err == -EAGAIN) continue; else if (err < 0) return; } } [[nodiscard]] ssize_t start_thread() { try { if (configuration.timestamps == timestamp_mode::NoTimestamp) { this->thread_ = std::thread{[this] { run_thread(&midi_in_impl::read_input_buffer); }}; } else { this->thread_ = std::thread{ [this] { run_thread(&midi_in_impl::read_input_buffer_with_timestamps); }}; } } catch (const std::system_error& e) { using namespace std::literals; error( this->configuration, "midi_in_alsa::start_thread: error starting MIDI input thread: "s + e.what()); return false; } return true; } bool open_port(const input_port& port, std::string_view /*name*/) override { if (const int err = midi_in_impl::init_port(port); err < 0) return false; if (!start_thread()) return false; return true; } void close_port() override { termination_event.notify(); if (thread_.joinable()) thread_.join(); termination_event.consume(); // Reset to zero midi_in_impl::close_port(); } std::thread thread_; eventfd_notifier termination_event{}; }; class midi_in_alsa_raw_manual : public midi_in_impl { public: using midi_in_impl::midi_in_impl; ~midi_in_alsa_raw_manual() { // Close a connection if it exists. this->close_port(); } private: void send_poll_callback() { if (configuration.timestamps == timestamp_mode::NoTimestamp) { configuration.manual_poll(manual_poll_parameters{ .fds = {this->fds_.data(), this->fds_.size()}, .callback = [this](std::span fds) { return do_read_events(&midi_in_impl::read_input_buffer, fds); }}); } else { configuration.manual_poll(manual_poll_parameters{ .fds = {this->fds_.data(), this->fds_.size()}, .callback = [this](std::span fds) { return do_read_events(&midi_in_impl::read_input_buffer_with_timestamps, fds); }}); } } bool open_port(const input_port& p, std::string_view /*name*/) override { if (midi_in_impl::init_port(p) < 0) return false; send_poll_callback(); return true; } }; } namespace libremidi { template <> inline std::unique_ptr make( libremidi::input_configuration&& conf, libremidi::alsa_raw_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)); } }