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..b8c068d --- /dev/null +++ b/src/LogNode.cpp @@ -0,0 +1,175 @@ +#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, 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::ControlChangeMessageViewTag, auto control_change) -> BufferElement { + return BufferElement{"Control Change"s, + control_change.get_channel_human(), + std::format("Controller: {}", control_change.get_controller()), + 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 diff --git a/src/midi/ChannelMapNode.cpp b/src/midi/ChannelMapNode.cpp index 9421452..1868b5f 100644 --- a/src/midi/ChannelMapNode.cpp +++ b/src/midi/ChannelMapNode.cpp @@ -4,6 +4,9 @@ #include "fmt/format.h" +#include +#include + mc::midi::ChannelMapNode::ChannelMapNode(const ChannelMap& map) : m_map(map) { } @@ -15,19 +18,21 @@ std::string_view mc::midi::ChannelMapNode::name() bool mc::midi::ChannelMapNode::process(data_span data) { - MessageView view(data); - if (!view.is_system()) - { - const auto current_channel = view.get_channel(); - const auto mapped_channel = m_map.get(current_channel); - if (mapped_channel != ChannelMap::no_channel) - { - view.set_channel(mapped_channel); - return true; - } - return false; - } - return true; + MessageView view(data); + midi::tag_overloads visitor{[this](ChannelMessageViewTag, auto channel_message_view) { + const auto current_channel = channel_message_view.get_channel(); + const auto mapped_channel = m_map.get(current_channel); + if (mapped_channel != ChannelMap::no_channel) + { + channel_message_view.set_channel(mapped_channel); + return true; + } + return false; + }, + [](auto /*any_tag*/, auto /*any_message_view*/) { + return true; + }}; + return std::visit(visitor, view.parse()); } const mc::midi::ChannelMap& mc::midi::ChannelMapNode::map() const diff --git a/src/midi/MessageView.hpp b/src/midi/MessageView.hpp index 3db9088..9c4a890 100644 --- a/src/midi/MessageView.hpp +++ b/src/midi/MessageView.hpp @@ -1,42 +1,380 @@ #pragma once + +#include "../Utils.hpp" + #include #include +#include +#include namespace mc::midi { -class MessageView final +template +using message_view_tag_t = typename T::tag_t; + +template +struct tag_overloads : Funs... +{ + template + decltype(auto) operator()(MessageV&& message_view) + { + return this->operator()(message_view_tag_t{}, + std::forward(message_view)); + } + +private: + using Funs::operator()...; +}; + +template +tag_overloads(Funs...) -> tag_overloads; + +struct MessageViewTag +{ +}; + +template +class ChannelMessageView; +template +class NoteOffMessageView; +template +class NoteOnMessageView; +template +class PolyKeyPressureMessageView; +template +class ControlChangeMessageView; +template +class ProgramChangeMessageView; +template +class ChannelPressureMessageView; +template +class PitchBendMessageView; + +template +class MessageView { public: - explicit MessageView(std::span message_data) : m_message_data(message_data) + using tag_t = MessageViewTag; + using span_t = + std::conditional_t, std::span>; + + explicit MessageView(span_t message_data) : m_message_data(message_data) { assert(!m_message_data.empty()); } + span_t get_bytes() { return m_message_data; } + bool is_system() const { // system messages start with 0b1111xxxx return (m_message_data[0] & 0xf0) == 0xf0; } + bool is_channel() const { return !is_system(); } + + std::variant, + NoteOffMessageView, + PolyKeyPressureMessageView, + ControlChangeMessageView, + ProgramChangeMessageView, + ChannelPressureMessageView, + PitchBendMessageView, + MessageView> + parse() const + { + if (is_channel()) + { + return utils::variant_cast(ChannelMessageView(m_message_data).parse()); + } + return *this; + } + +protected: + span_t m_message_data; +}; + +MessageView(std::span) -> MessageView; +MessageView(std::span) -> MessageView; + +struct ChannelMessageViewTag +{ +}; + +template +class ChannelMessageView : public MessageView +{ +public: + using tag_t = ChannelMessageViewTag; + explicit ChannelMessageView(MessageView::span_t message_data) + : MessageView(message_data) + { + } + char get_channel() const { // channel is cccc in 0bxxxxcccc of the first byte - // if not a system message - assert(!is_system()); - return m_message_data[0] & 0x0f; + return this->m_message_data[0] & 0x0f; } - void set_channel(unsigned char ch) + char get_channel_human() const { return get_channel() + 1; } + + template + auto set_channel(unsigned char ch) -> std::enable_if_t { // channel is cccc in 0bxxxxcccc of the first byte - // if not a system message - assert(!is_system()); - m_message_data[0] = (m_message_data[0] & 0xf0) | (ch & 0x0f); + this->m_message_data[0] = (this->m_message_data[0] & 0xf0) | (ch & 0x0f); } -private: - std::span m_message_data; + std::variant, + NoteOnMessageView, + PolyKeyPressureMessageView, + ControlChangeMessageView, + ProgramChangeMessageView, + ChannelPressureMessageView, + PitchBendMessageView> + parse() const + { + switch (this->m_message_data[0] & 0xf0) + { + case 0x80: + return NoteOffMessageView(this->m_message_data); + case 0x90: + if (this->m_message_data[2] > 0) + { + return NoteOnMessageView(this->m_message_data); + } + else + { + return NoteOffMessageView(this->m_message_data); + } + case 0xa0: + return PolyKeyPressureMessageView(this->m_message_data); + case 0xb0: + return ControlChangeMessageView(this->m_message_data); + case 0xc0: + return ProgramChangeMessageView(this->m_message_data); + case 0xd0: + return ChannelPressureMessageView(this->m_message_data); + case 0xe0: + return PitchBendMessageView(this->m_message_data); + default: + throw std::runtime_error("Unrecognized channel message"); + } + } +}; + +struct NoteMessageViewTag +{ +}; + +template +class NoteMessageView : public ChannelMessageView +{ +public: + using tag_t = NoteMessageViewTag; + + explicit NoteMessageView(MessageView::span_t message_data) + : ChannelMessageView(message_data) + { + assert(message_data.size() == 3); + } + + unsigned char get_note() const { return this->m_message_data[1]; } + + template + auto set_note(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[1] = val; + } + + unsigned char get_velocity() const { return this->m_message_data[2]; } + + template + auto set_velocity(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[2] = val; + } +}; + +struct NoteOffMessageViewTag : public NoteMessageViewTag +{ +}; + +template +class NoteOffMessageView : public NoteMessageView +{ +public: + using tag_t = NoteOffMessageViewTag; + explicit NoteOffMessageView(MessageView::span_t message_data) + : NoteMessageView(message_data) + { + } +}; + +struct NoteOnMessageViewTag : public NoteMessageViewTag +{ +}; + +template +class NoteOnMessageView : public NoteMessageView +{ +public: + using tag_t = NoteOnMessageViewTag; + explicit NoteOnMessageView(MessageView::span_t message_data) + : NoteMessageView(message_data) + { + } +}; + +struct PolyKeyPressureMessageViewTag : public ChannelMessageViewTag +{ +}; + +template +class PolyKeyPressureMessageView : public ChannelMessageView +{ +public: + using tag_t = PolyKeyPressureMessageViewTag; + explicit PolyKeyPressureMessageView(MessageView::span_t message_data) + : ChannelMessageView(message_data) + { + assert(message_data.size() == 3); + } + + unsigned char get_note() const { return this->m_message_data[1]; } + + template + auto set_note(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[1] = val; + } + + unsigned char get_pressure() const { return this->m_message_data[2]; } + + template + auto set_pressure(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[2] = val; + } +}; + +struct ControlChangeMessageViewTag : public ChannelMessageViewTag +{ +}; + +template +class ControlChangeMessageView : public ChannelMessageView +{ +public: + using tag_t = ControlChangeMessageViewTag; + explicit ControlChangeMessageView(MessageView::span_t message_data) + : ChannelMessageView(message_data) + { + } + + unsigned char get_controller() const { return this->m_message_data[1]; } + + template + auto set_controller(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[1] = val; + } + + unsigned char get_value() const { return this->m_message_data[2]; } + + template + auto set_value(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[2] = val; + } +}; + +struct ProgramChangeMessageViewTag : public ChannelMessageViewTag +{ +}; + +template +class ProgramChangeMessageView : public ChannelMessageView +{ +public: + using tag_t = ProgramChangeMessageViewTag; + explicit ProgramChangeMessageView(MessageView::span_t message_data) + : ChannelMessageView(message_data) + { + } + + unsigned char get_program_number() const { return this->m_message_data[1]; } + + template + auto set_program_number(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[1] = val; + } +}; + +struct ChannelPressureMessageViewTag : public ChannelMessageViewTag +{ +}; + +template +class ChannelPressureMessageView : public ChannelMessageView +{ +public: + using tag_t = ChannelPressureMessageViewTag; + explicit ChannelPressureMessageView(MessageView::span_t message_data) + : ChannelMessageView(message_data) + { + assert(message_data.size() == 2); + } + + unsigned char get_pressure() const { return this->m_message_data[1]; } + + template + auto set_pressure(unsigned char val) -> std::enable_if_t + { + assert(val < 128); + this->m_message_data[1] = val; + } +}; + +struct PitchBendMessageViewTag : public ChannelMessageViewTag +{ +}; + +template +class PitchBendMessageView : public ChannelMessageView +{ +public: + using tag_t = PitchBendMessageViewTag; + explicit PitchBendMessageView(MessageView::span_t message_data) + : ChannelMessageView(message_data) + { + assert(message_data.size() == 3); + } + + std::uint16_t get_value() const + { + return this->m_message_data[1] + std::uint16_t{127} * this->m_message_data[2]; + } + + int get_value_human() const { return get_value() - 8128; } + + template + auto set_value(std::uint16_t val) -> std::enable_if_t + { + assert(val < 127 * 127); + this->m_message_data[1] = static_cast(val % 127); + this->m_message_data[2] = static_cast(val / 127); + } }; } // namespace mc::midi diff --git a/src/midi/MidiGraph.cpp b/src/midi/MidiGraph.cpp index fef4494..d9cc531 100644 --- a/src/midi/MidiGraph.cpp +++ b/src/midi/MidiGraph.cpp @@ -2,6 +2,7 @@ #include +#include #include namespace mc::midi diff --git a/src/midi/MidiGraph.hpp b/src/midi/MidiGraph.hpp index bf8b597..6fb3865 100644 --- a/src/midi/MidiGraph.hpp +++ b/src/midi/MidiGraph.hpp @@ -2,7 +2,6 @@ #include "GraphObserver.hpp" -#include #include #include #include diff --git a/src/midi/Note.hpp b/src/midi/Note.hpp new file mode 100644 index 0000000..653618e --- /dev/null +++ b/src/midi/Note.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +namespace mc::midi +{ + +class Note final +{ +public: + explicit Note(unsigned char note) : m_note(note) {} + +private: + unsigned char m_note; + + friend struct std::formatter; +}; + +} // namespace mc::midi + +template <> +struct std::formatter +{ + template + constexpr ParseContext::iterator parse(ParseContext& ctx) + { + return ctx.begin(); + } + + template + FmtContext::iterator format(mc::midi::Note note, FmtContext& ctx) const + { + using namespace std::string_view_literals; + const auto key = [=] { + switch (note.m_note % 12) + { + case 0: + return "C"sv; + case 1: + return "C#"sv; + case 2: + return "D"sv; + case 3: + return "D#"sv; + case 4: + return "E"sv; + case 5: + return "F"sv; + case 6: + return "F#"sv; + case 7: + return "G"sv; + case 8: + return "G#"sv; + case 9: + return "A"sv; + case 10: + return "A#"sv; + case 11: + return "B"sv; + default: + return ""sv; + } + }(); + const auto octave = note.m_note / 12; + std::ostringstream out; + out << key << octave; + return std::ranges::copy(std::move(out).str(), ctx.out()).out; + } +};