diff --git a/CMakeLists.txt b/CMakeLists.txt index 56ac622..055c839 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ set(sources src/DisconnectedMidiOutNode.cpp src/KeyboardShortcutAggregator.cpp src/Licenses.cpp + src/LogNode.cpp src/MidiChannelNode.cpp src/MidiInNode.cpp src/MidiOutNode.cpp diff --git a/src/LogNode.cpp b/src/LogNode.cpp new file mode 100644 index 0000000..157c45e --- /dev/null +++ b/src/LogNode.cpp @@ -0,0 +1,209 @@ +#include "LogNode.hpp" + +#include "NodeSerializer.hpp" + +#include "midi/MessageView.hpp" +#include "midi/Note.hpp" + +#include "imgui.h" +#include "imnodes.h" + +#include + +std::string_view mc::LogNode::LogMidiNode::name() +{ + return "Log Node"; +} + +mc::LogNode::LogNode() +{ + m_log_midi_node.add_observer(this); +} + +mc::LogNode::~LogNode() +{ + m_log_midi_node.remove_observer(this); +} + +void mc::LogNode::accept_serializer(nlohmann::json& j, const NodeSerializer& serializer) const +{ + serializer.serialize_node(j, *this); +} + +mc::midi::Node* mc::LogNode::get_midi_node() +{ + return &m_log_midi_node; +} + +void mc::LogNode::render_internal() +{ + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("Message log"); + ImNodes::EndNodeTitleBar(); + ImNodes::BeginInputAttribute(in_id()); + m_input_indicator.render(); + ImGui::SameLine(); + ImGui::TextUnformatted("MIDI in"); + ImNodes::EndInputAttribute(); + + int new_buffer_size = static_cast(m_max_buffer_size); + ImGui::SetNextItemWidth(150.0F); // ToDo scale + if (ImGui::InputInt("Buffer Size", &new_buffer_size)) + { + if (new_buffer_size > 0 && new_buffer_size < 100000) + { + m_max_buffer_size = static_cast(new_buffer_size); + } + } + + std::unique_lock lock(m_buffer_mutex); + while (!m_message_buffer.empty() && m_message_buffer.size() > m_max_buffer_size) + { + m_message_buffer.pop_back(); + } + lock.unlock(); + + if (ImGui::BeginTable( + "Log", 5, ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable, ImVec2{800.F, 300.F})) + { // ToDo scale + + ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 140.F); + ImGui::TableSetupColumn("Type"); + ImGui::TableSetupColumn("Channel", ImGuiTableColumnFlags_WidthFixed, 70.F); + ImGui::TableSetupColumn("Data #0"); + ImGui::TableSetupColumn("Data #1"); + ImGui::TableHeadersRow(); + + lock.lock(); + for (const auto& item : m_message_buffer) + { + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + const auto local = std::chrono::zoned_time(std::chrono::current_zone(), item.m_arrived) + .get_local_time(); + const auto seconds = + std::chrono::floor(local.time_since_epoch()).count() % 60; + const auto milliseconds = + std::chrono::floor(local.time_since_epoch()).count() % + 1000; + ImGui::TextUnformatted( + std::format("{:%H:%M}:{:02}.{:03}", local, seconds, milliseconds).c_str()); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(item.m_name.c_str()); + + ImGui::TableNextColumn(); + if (item.m_channel) + { + ImGui::TextUnformatted(std::to_string(*item.m_channel).c_str()); + } + + ImGui::TableNextColumn(); + if (!item.m_data_0.empty()) + { + ImGui::TextUnformatted(item.m_data_0.c_str()); + } + + ImGui::TableNextColumn(); + if (!item.m_data_1.empty()) + { + ImGui::TextUnformatted(item.m_data_1.c_str()); + } + } + lock.unlock(); + + ImGui::EndTable(); + } +} + +void mc::LogNode::message_received(std::span message_bytes) +{ + using namespace std::string_literals; + + m_input_indicator.trigger(); + midi::MessageView message(message_bytes); + midi::tag_overloads message_visitor{ + [](midi::NoteOnMessageViewTag, auto note_on) -> BufferElement { + return BufferElement{"Note On"s, + note_on.get_channel_human(), + std::format("{}", midi::Note(note_on.get_note())), + std::format("Velocity: {}", note_on.get_velocity())}; + }, + [](midi::NoteOffMessageViewTag, auto note_off) -> BufferElement { + return BufferElement{"Note Off"s, + note_off.get_channel_human(), + std::format("{}", midi::Note(note_off.get_note())), + std::format("Velocity: {}", note_off.get_velocity())}; + }, + [](midi::PolyKeyPressureMessageViewTag, auto poly_key_pressure) -> BufferElement { + return BufferElement{"Poly Aftertouch"s, + poly_key_pressure.get_channel_human(), + std::format("{}", midi::Note(poly_key_pressure.get_note())), + std::format("Pressure: {}", poly_key_pressure.get_pressure())}; + }, + [](midi::AllSoundOffMessageViewTag, auto all_sound_off) -> BufferElement { + return BufferElement{"All Sound Off"s, all_sound_off.get_channel_human(), ""s, ""s}; + }, + [](midi::ResetAllControllersMessageViewTag, auto reset_all_controllers) -> BufferElement { + return BufferElement{ + "Reset All Controllers"s, reset_all_controllers.get_channel_human(), ""s, ""s}; + }, + [](midi::LocalControlMessageViewTag, auto local_control) -> BufferElement { + return BufferElement{"Local Control"s, + local_control.get_channel_human(), + local_control.get_value() ? "On"s : "Off"s, + ""s}; + }, + [](midi::AllNotesOffMessageViewTag, auto all_notes_off) -> BufferElement { + return BufferElement{"All Notes Off"s, all_notes_off.get_channel_human(), ""s, ""s}; + }, + [](midi::OmniModeOffMessageViewTag, auto omni_off) -> BufferElement { + return BufferElement{"Omni Mode Off"s, omni_off.get_channel_human(), ""s, ""s}; + }, + [](midi::OmniModeOnMessageViewTag, auto omni_on) -> BufferElement { + return BufferElement{"Omni Mode On"s, omni_on.get_channel_human(), ""s, ""s}; + }, + [](midi::MonoModeOnMessageViewTag, auto mono_mode) -> BufferElement { + return BufferElement{"Mono Mode On"s, + mono_mode.get_channel_human(), + std::format("Channels: {}", mono_mode.get_num_channels()), + ""s}; + }, + [](midi::PolyModeOnMessageViewTag, auto poly_mode) -> BufferElement { + return BufferElement{"Poly Mode On"s, poly_mode.get_channel_human(), ""s, ""s}; + }, + [](midi::ControlChangeMessageViewTag, auto control_change) -> BufferElement { + return BufferElement{"Control Change"s, + control_change.get_channel_human(), + std::format("CC {} ({})", + control_change.get_controller(), + control_change.get_function_name()), + std::format("Value: {}", control_change.get_value())}; + }, + [](midi::ProgramChangeMessageViewTag, auto program_change) -> BufferElement { + return BufferElement{"Program Change"s, + program_change.get_channel_human(), + std::format("Program: {}", program_change.get_program_number()), + ""s}; + }, + [](midi::ChannelPressureMessageViewTag, auto channel_pressure) -> BufferElement { + return BufferElement{"Channel Aftertouch"s, + channel_pressure.get_channel_human(), + std::format("Pressure: {}", channel_pressure.get_pressure()), + ""s}; + }, + [](midi::PitchBendMessageViewTag, auto pitch_bend) -> BufferElement { + return BufferElement{"Pitch bend"s, + pitch_bend.get_channel_human(), + std::format("Value: {}", pitch_bend.get_value_human()), + ""s}; + }, + [](auto, auto) -> BufferElement { + return BufferElement{"Unknown"s, std::nullopt, ""s, ""s}; + }}; + const BufferElement new_element = std::visit(message_visitor, message.parse()); + + std::lock_guard guard(m_buffer_mutex); + m_message_buffer.push_front(std::move(new_element)); +} diff --git a/src/LogNode.hpp b/src/LogNode.hpp new file mode 100644 index 0000000..e382e58 --- /dev/null +++ b/src/LogNode.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "ActivityIndicator.hpp" +#include "Node.hpp" +#include "midi/MidiGraph.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace mc +{ + +class LogNode final : public Node, private midi::GraphObserver +{ +private: + class LogMidiNode final : public midi::Node + { + public: + std::string_view name() override; + }; + +public: + LogNode(); + ~LogNode(); + + void accept_serializer(nlohmann::json& j, const NodeSerializer& serializer) const override; + +protected: + midi::Node* get_midi_node() override; + void render_internal() override; + +private: + void message_received(std::span message_bytes) override; + + LogMidiNode m_log_midi_node; + ActivityIndicator m_input_indicator; + + static inline constexpr std::size_t default_max_buffer_size = 500; + + std::size_t m_max_buffer_size = default_max_buffer_size; + + struct BufferElement + { + BufferElement(std::string&& name, + std::optional&& channel, + std::string&& data_0, + std::string&& data_1) + : m_arrived(std::chrono::system_clock::now()), m_name(std::move(name)), + m_channel(std::move(channel)), m_data_0(std::move(data_0)), + m_data_1(std::move(data_1)) + { + } + + std::chrono::time_point m_arrived; + std::string m_name; + std::optional m_channel; + std::string m_data_0; + std::string m_data_1; + }; + + std::mutex m_buffer_mutex; + std::deque m_message_buffer; + + friend class NodeSerializer; +}; + +} // namespace mc diff --git a/src/NodeEditor.cpp b/src/NodeEditor.cpp index 7432dfe..6f1cf75 100644 --- a/src/NodeEditor.cpp +++ b/src/NodeEditor.cpp @@ -7,6 +7,7 @@ #include "imnodes.h" #include "nlohmann/json.hpp" +#include "LogNode.hpp" #include "MidiChannelNode.hpp" #include "MidiInNode.hpp" #include "MidiOutNode.hpp" @@ -143,21 +144,37 @@ NodeEditor NodeEditor::from_json(NodeFactory& node_factory, std::shared_ptr NodeEditor::renderContextMenu(bool show_outputting_nodes, bool show_inputting_nodes) { - constexpr ImGuiTreeNodeFlags leaf_flags = - ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; - auto render_contents = [this](auto& infos) -> std::shared_ptr { + auto render_add_node = [this](auto node_builder, + const std::string& name) -> std::shared_ptr { + constexpr ImGuiTreeNodeFlags leaf_flags = + ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + ImGui::TreeNodeEx(name.c_str(), leaf_flags); + if (ImGui::IsItemClicked()) + { + std::shared_ptr node = node_builder(); + m_nodes.push_back(node); + + ImNodes::SetNodeScreenSpacePos(node->id(), ImGui::GetMousePosOnOpeningCurrentPopup()); + ImGui::CloseCurrentPopup(); + return node; + } + else + { + return nullptr; + } + }; + + auto render_midi_inout_list = [this, render_add_node](auto& infos) -> std::shared_ptr { for (const auto& info : infos) { const std::string port_name = m_port_name_display->get_port_name(info); - ImGui::TreeNodeEx(port_name.c_str(), leaf_flags); - if (ImGui::IsItemClicked()) + if (auto node = render_add_node( + [this, &info] { + return m_node_factory->build_midi_node(info); + }, + port_name); + node != nullptr) { - std::shared_ptr node = m_node_factory->build_midi_node(info); - m_nodes.push_back(node); - - ImNodes::SetNodeScreenSpacePos(node->id(), - ImGui::GetMousePosOnOpeningCurrentPopup()); - ImGui::CloseCurrentPopup(); return node; } } @@ -178,19 +195,23 @@ std::shared_ptr NodeEditor::renderContextMenu(bool show_outputting_nodes, } if (show_outputting_nodes || show_inputting_nodes) { - ImGui::TreeNodeEx("Channel map", leaf_flags); - if (ImGui::IsItemClicked()) - { - node = m_node_factory->build_midi_channel_node(); - ImNodes::SetNodeScreenSpacePos(node->id(), - ImGui::GetMousePosOnOpeningCurrentPopup()); - m_nodes.push_back(node); - ImGui::CloseCurrentPopup(); - } + node = render_add_node( + [this] { + return m_node_factory->build_midi_channel_node(); + }, + "Channel map"); + } + if (show_inputting_nodes) + { + node = render_add_node( + [this] { + return m_node_factory->build_log_node(); + }, + "Message log"); } if (show_outputting_nodes && ImGui::TreeNode("MIDI inputs")) { - auto tmp_node = render_contents(m_input_infos); + auto tmp_node = render_midi_inout_list(m_input_infos); if (tmp_node) // new node was created { node = tmp_node; @@ -199,7 +220,7 @@ std::shared_ptr NodeEditor::renderContextMenu(bool show_outputting_nodes, } if (show_inputting_nodes && ImGui::TreeNode("MIDI outputs")) { - auto tmp_node = render_contents(m_output_infos); + auto tmp_node = render_midi_inout_list(m_output_infos); if (tmp_node) // new node was created { node = tmp_node; diff --git a/src/NodeFactory.cpp b/src/NodeFactory.cpp index 2215d92..04316a7 100644 --- a/src/NodeFactory.cpp +++ b/src/NodeFactory.cpp @@ -2,6 +2,7 @@ #include "DisconnectedMidiInNode.hpp" #include "DisconnectedMidiOutNode.hpp" +#include "LogNode.hpp" #include "MidiChannelNode.hpp" #include "MidiInNode.hpp" #include "MidiOutNode.hpp" @@ -90,6 +91,11 @@ std::shared_ptr NodeFactory::build_midi_channel_node() }); } +std::shared_ptr NodeFactory::build_log_node() +{ + return std::make_shared(); +} + bool NodeFactory::is_node_instantiated(const midi::InputInfo& input_info) { return nullptr != get_cached_midi_node(m_input_nodes, input_info); diff --git a/src/NodeFactory.hpp b/src/NodeFactory.hpp index 9b4b31c..ccf7c2e 100644 --- a/src/NodeFactory.hpp +++ b/src/NodeFactory.hpp @@ -17,6 +17,7 @@ class OutputNode; class DisconnectedMidiInNode; class DisconnectedMidiOutNode; +class LogNode; class MidiChannelNode; class MidiInNode; class MidiOutNode; @@ -27,6 +28,11 @@ class NodeFactory final { public: NodeFactory(const ThemeControl& theme_control, const PortNameDisplay& port_name_display); + NodeFactory(const NodeFactory&) = delete; + NodeFactory(NodeFactory&&) = delete; + + NodeFactory& operator=(const NodeFactory&) = delete; + NodeFactory& operator=(NodeFactory&&) = delete; std::shared_ptr build_midi_node(const midi::InputInfo& input_info); std::shared_ptr build_midi_node(const midi::OutputInfo& output_info); @@ -35,13 +41,14 @@ class NodeFactory final std::shared_ptr build_disconnected_midi_out_node( const std::string& output_name); std::shared_ptr build_midi_channel_node(); + std::shared_ptr build_log_node(); bool is_node_instantiated(const midi::InputInfo& input_info); bool is_node_instantiated(const midi::OutputInfo& output_info); private: - const ThemeControl* m_theme_control; - const PortNameDisplay* m_port_name_display; + const ThemeControl* m_theme_control; + const PortNameDisplay* m_port_name_display; std::map> m_input_nodes; std::map> m_output_nodes; }; diff --git a/src/NodeSerializer.cpp b/src/NodeSerializer.cpp index f7e8fcd..217d995 100644 --- a/src/NodeSerializer.cpp +++ b/src/NodeSerializer.cpp @@ -7,6 +7,7 @@ #include "DisconnectedMidiInNode.hpp" #include "DisconnectedMidiOutNode.hpp" +#include "LogNode.hpp" #include "MidiChannelNode.hpp" #include "MidiInNode.hpp" #include "MidiOutNode.hpp" @@ -71,6 +72,14 @@ void NodeSerializer::serialize_node(json& j, const DisconnectedMidiOutNode& node }; } +void NodeSerializer::serialize_node(nlohmann::json& j, const LogNode& node) const +{ + j = json{ + {"type", "log" }, + {"max_buffer_size", node.m_max_buffer_size}, + }; +} + void NodeSerializer::serialize_node(json& j, const MidiChannelNode& node) const { j = json{ @@ -116,6 +125,12 @@ std::shared_ptr NodeSerializer::deserialize_node(const json& j) const j.at("channels").get(); node = channel_node; } + else if (node_type == "log") + { + auto log_node = m_node_factory->build_log_node(); + j["max_buffer_size"].get_to(log_node->m_max_buffer_size); + node = log_node; + } else { throw std::logic_error("Unexpected node type"); diff --git a/src/NodeSerializer.hpp b/src/NodeSerializer.hpp index 9b69870..3b6c19a 100644 --- a/src/NodeSerializer.hpp +++ b/src/NodeSerializer.hpp @@ -8,6 +8,7 @@ namespace mc class DisconnectedMidiInNode; class DisconnectedMidiOutNode; +class LogNode; class MidiChannelNode; class MidiInNode; class MidiOutNode; @@ -21,11 +22,12 @@ class NodeSerializer final void serialize_node(nlohmann::json& j, const Node& node) const; - void serialize_node(nlohmann::json& j, const MidiInNode& node) const; void serialize_node(nlohmann::json& j, const DisconnectedMidiInNode& node) const; - void serialize_node(nlohmann::json& j, const MidiOutNode& node) const; void serialize_node(nlohmann::json& j, const DisconnectedMidiOutNode& node) const; + void serialize_node(nlohmann::json& j, const LogNode& node) const; void serialize_node(nlohmann::json& j, const MidiChannelNode& node) const; + void serialize_node(nlohmann::json& j, const MidiInNode& node) const; + void serialize_node(nlohmann::json& j, const MidiOutNode& node) const; std::shared_ptr deserialize_node(const nlohmann::json& j) const; diff --git a/src/Utils.hpp b/src/Utils.hpp index 2d3bb68..c3f3827 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace mc::utils { @@ -51,4 +53,26 @@ inline std::string path_to_utf8str(const std::filesystem::path& path) return {u8str.begin(), u8str.end()}; } +template +struct variant_cast_proxy +{ + std::variant v; + + template + operator std::variant() const + { + return std::visit( + [](auto&& arg) -> std::variant { + return arg; + }, + v); + } +}; + +template +auto variant_cast(const std::variant& v) -> variant_cast_proxy +{ + return {v}; +} + } // namespace mc::utils