#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 { } libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; } // Must be a string such as: "hw:2,4,1" [[nodiscard]] stdx::error 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) { libremidi_handle_error(this->configuration, "cannot open device."); return from_errc(err); } snd_rawmidi_params_t* params{}; snd_rawmidi_params_alloca(¶ms); if (const int err = snd.rawmidi.params_current(midiport_, params); err < 0) return from_errc(err); if (const int err = snd.rawmidi.params_set_no_active_sensing(midiport_, params, 1); err < 0) return from_errc(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 from_errc(err); if (const int err = snd.rawmidi.params_set_clock_type(midiport_, params, SND_RAWMIDI_CLOCK_NONE); err < 0) return from_errc(err); } else { if (const int err = snd.rawmidi.params_set_read_mode(midiport_, params, SND_RAWMIDI_READ_TSTAMP); err < 0) return from_errc(err); if (const int err = snd.rawmidi.params_set_clock_type(midiport_, params, SND_RAWMIDI_CLOCK_MONOTONIC); err < 0) return from_errc(err); } #endif if (const int err = snd.rawmidi.params(midiport_, params); err < 0) return from_errc(err); return init_pollfd(); } [[nodiscard]] stdx::error init_port(const port_information& p) { return do_init_port(raw_from_port_handle(p.port).to_string().c_str()); } [[nodiscard]] stdx::error init_pollfd() { const int num_fds = snd.rawmidi.poll_descriptors_count(this->midiport_); this->fds_.clear(); this->fds_.resize(num_fds); int ret = snd.rawmidi.poll_descriptors(this->midiport_, fds_.data(), num_fds); if (ret < 0) return from_errc(ret); return stdx::error{}; } 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(); }; m_processing.on_bytes( {bytes, bytes + err}, m_processing.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); }; m_processing.on_bytes( {bytes, bytes + err}, m_processing.timestamp(to_ns, 0)); } return err; } #else ssize_t read_input_buffer_with_timestamps() { return read_input_buffer(); } #endif stdx::error close_port() override { if (midiport_) snd.rawmidi.close(midiport_); midiport_ = nullptr; return stdx::error{}; } timestamp absolute_timestamp() const noexcept final override { return system_ns(); } snd_rawmidi_t* midiport_{}; std::vector fds_; midi1::input_state_machine m_processing{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->m_termination_event < 0) { libremidi_handle_error(this->configuration, "error creating eventfd."); return; } client_open_ = stdx::error{}; } ~midi_in_alsa_raw_threaded() override { // Close a connection if it exists. this->midi_in_alsa_raw_threaded::close_port(); client_open_ = std::errc::not_connected; } private: void run_thread(auto parse_func) { fds_.push_back(this->m_termination_event); const auto period = std::chrono::duration_cast(this->configuration.poll_period) .count(); for (;;) { // Poll ssize_t err = poll(fds_.data(), fds_.size(), static_cast(period)); if (err == -EAGAIN) continue; else if (err < 0) return; else if (m_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]] stdx::error start_thread() { try { if (configuration.timestamps == timestamp_mode::NoTimestamp) { this->m_thread = std::thread{[this] { run_thread(&midi_in_impl::read_input_buffer); }}; } else { this->m_thread = std::thread{ [this] { run_thread(&midi_in_impl::read_input_buffer_with_timestamps); }}; } return stdx::error{}; } catch (const std::system_error& e) { using namespace std::literals; libremidi_handle_error( this->configuration, "error starting MIDI input thread: "s + e.what()); return e.code(); } return stdx::error{}; } stdx::error open_port(const input_port& port, std::string_view /*name*/) override { if (auto err = midi_in_impl::init_port(port); err != stdx::error{}) return err; if (auto err = start_thread(); err != stdx::error{}) return err; return stdx::error{}; } stdx::error close_port() override { m_termination_event.notify(); if (m_thread.joinable()) m_thread.join(); m_termination_event.consume(); // Reset to zero return midi_in_impl::close_port(); } std::thread m_thread; eventfd_notifier m_termination_event{}; }; class midi_in_alsa_raw_manual : public midi_in_impl { public: midi_in_alsa_raw_manual(input_configuration&& conf, alsa_raw_input_configuration&& apiconf) : midi_in_impl{std::move(conf), std::move(apiconf)} { client_open_ = stdx::error{}; } ~midi_in_alsa_raw_manual() { // Close a connection if it exists. this->close_port(); client_open_ = std::errc::not_connected; } 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); }}); } } stdx::error open_port(const input_port& p, std::string_view /*name*/) override { if (auto err = midi_in_impl::init_port(p); err != stdx::error{}) return err; send_poll_callback(); return stdx::error{}; } }; } 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)); } }